Vue3
Various ways to use Vue3
- Standalone Script: standalone script
- Embedded Web Components: Embedded as Web Component
- Single-Page Application (SPA): Single-page application
- Fullstack/SSR: full stack/server-side rendering
- JAMStack/SSG: JAMStack Architecture/Static Site Generation
Standalone script
First, we create an html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
</html>
Secondly, since we need to create a Vue project, we need Vue dependencies. So how to import Vue’s dependencies? We can use CDN to introduce Vue’s independent scripts.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.min.js"></script>
</body>
</html>
So actually, according to this script tag, the browser will https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.js
send a network request to this address, and after the request is successful, it will respond with a JavaScript file. This file uses UMD’s JavaScript modularization solution, and it will eventually mount a Vue
property named on our globalThis object (window) . Can be viewed via console.log(globalThis.Vue)
. Then we can start creating the Vue project.
We first need to create an application instance App through the createApp function provided by Vue, and then mount it to the existing DOM element in html through the mount method on the application instance.
The types of createApp and application instance App are defined as follows:
function createApp(rootComponent: Component, rootProps?: object): App;
interface App {
mount(rootContainer: Element | string): ComponentPublicInstance;
}
const { createApp } = Vue;
// createApp rootComponent, rootProps
const app = Vue.createApp({});
// mount :rootContainer
app.mount('');
In the above, we did not pass in specific parameters to createApp and app.mount, but replaced them with empty objects and empty strings. So what are these two parameters actually needed? We answer them one by one.
rootComponent represents the root component, we need to pass a Vue component to this parameter.
rootContainer represents the existing DOM element to which we want to mount the application instance App. You can pass a css selector string or a real DOM element to this parameter.
What are Vue components?
In fact, a Vue component is a JavaScript object, but this object has some unique properties that allow Vue to identify it as a Vue component. But Vue components can be roughly divided into two categories:
- single file component
- non-single file component
Since we now use standalone scripts to build Vue projects in a single html file, we are currently unable to use the single-file component feature of Vue. But we can create a non-single file component.
const { ref } = Vue;
const customVueComponent = {
setup() {
const count = ref(0);
return {
count: count,
};
},
template: `
<button>You click me {{count}} times</button>
`,
};
Here is a Vue API – defineComponent, which is an auxiliary function that provides type derivation when we define Vue components. But here, we are developing directly in the html file, and the effect of using it is not significant.
Then we can now build the following project:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.js"></script>
<script>
const { ref, createApp } = Vue;
const customVueComponent = {
setup() {
const count = ref(0);
return {
count: count,
};
},
template: `
<button>You click me {{count}} times</button>
`,
};
const app = createApp(customVueComponent);
app.mount('#root');
</script>
</body>
</html>
This is building a Vue project through a stand-alone script. But here we need to note that the independent script we are introducing now contains Vue’s compiler and runtime modules.
What are Vue’s compiler and runtime
In Vue.js, there are two main build versions:
- One is the “full” build, including compiler and runtime, so it supports dynamic compilation of templates
- One is runtime-only, which only contains runtime and needs to precompile the template in the build step.
So what we imported above https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.js
is a full build version. The template field we defined in the Vue component can be compiled into a render function by the compiler.
If we change the CDN address in the code to https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.runtime.global.js
, then the Vue we import will no longer contain a compiler. This built version of Vue does not support dynamic compilation of templates. Therefore, the template field we define in the Vue component cannot be compiled into a render function by the compiler. Then an error will be reported in the console:
[Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Use "vue.global.js" instead.
at <App>
This means that the template option we provide in the Vue component does not support runtime compilation in this version of Vue. Use the “vue.global.js” build instead.
So we need to change the definition of the Vue component here. The template attribute can no longer be used, but is replaced by the render function.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.js"></script>
<script>
const { ref, createApp, h } = Vue;
const customVueComponent = {
setup() {
const count = ref(0);
return {
count: count,
};
},
render: (ctx) => {
return h('button', null, 'You click me ' + ctx.count + ' times');
},
};
const app = createApp(customVueComponent);
app.mount('#root');
</script>
</body>
</html>
Create a Vue project through create-vite scaffolding
npm init vite
# npm create vite
After the creation is completed, we focus on several files in the src directory as follows:
├── src
├── App.vue
├── mian.js
First, let’s take a look at the main.js file. We should be familiar with the content here, so we won’t go into details.
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';
createApp(App).mount('#app');
Next, let’s take a look at the App.vue file. This is another type of Vue component mentioned above – a single-file component.
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue';
</script>
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
What is Single File Component SFC
Vue official documentation introduces: Vue’s single-file component (i.e. *.vue file, English Single-File Component, referred to as SFC) is a special file format that allows us to encapsulate the template, logic and style of a Vue component in in a single file. Vue’s single-file components are a natural extension of the classic combination of HTML, CSS, and JavaScript in web development. The three blocks <template>
, , <script>
and , <style>
encapsulate and combine the component’s view, logic, and style in the same file.
How SFC works
Vue SFC is a framework-specified file format, so it must be compiled into standard JavaScript and CSS by @vue/compiler-sfc. A compiled SFC is a standard JavaScript(ES) module.
So without further ado, let’s feel how Vue SFC works by manually compiling an SFC file ourselves.
Manually compile SFC
Before we start, we can first print a log in main.js in the create-vite scaffolding console.log(App)
, and see what the App single-file component exported from the App.vue file looks like in the console. You can see what it looks like in the end. , this App is a standard JavaScript object.
// App
{
render: function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
_Fragment,
null,
[_hoisted_1, _createVNode($setup['HelloWorld'], { msg: 'Vite + Vue' })],
64
/* STABLE_FRAGMENT */
)
);
},
setup(__props, { expose: __expose }) {
__expose();
const __returned__ = { HelloWorld };
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
});
return __returned__;
},
__name: 'App',
__hmrId: '7a7a37b1',
__scopeId: 'data-v-7a7a37b1',
__file: '/Users/heartsco/Scoheart/code/vite-project/src/App.vue',
};
So in fact, when we manually compile an SFC file, we actually compile the template, script and style blocks in the SFC file into the JavaScript object seen above. Next, we officially start to manually write a SFC file.
First, initialize the project and install the @vue/compiler-sfc dependency mentioned above:
npm init; npm install @vue/compiler-sfc
Secondly, the directory structure we will prepare is as follows:
├── App.vue
├── compiler.js
├── package-lock.json
├── package.json
Then, we manually write an SFC file:
// App.vue
<template>
<div>
<h1>Hi, I'm Scoheart</h1>
</div>
</template>
<script setup lang="ts">
const a = 'Hello, Scoheart!';
console.log(a)
</script>
<style scoped>
h1 {
color: blue;
}
</style>
Continue to write a compiler.js file to compile SFC files. The content is as follows.
const {
parse,
compileScript,
compileTemplate,
compileStyle,
rewriteDefault,
} = require('@vue/compiler-sfc');
const fs = require('fs');
const path = require('path');
const sfcFile = 'App.vue';
const sfcPath = path.resolve(sfcFile);
const id = Date.now().toString().substring(0, 8);
//
const __file = sfcPath;
const __name = sfcFile.split('.').shift();
const __scopedId = `data-v-${id}`; /** css scope, */
const __hmrId = `${id}`;
//
const sfcContent = fs.readFileSync(sfcPath, 'utf-8');
//
const { descriptor } = parse(sfcContent, {
filename: sfcFile,
});
// script
const script = compileScript(descriptor, {
id: id,
});
// template
const template = compileTemplate({
source: descriptor.template.content,
id: id,
filename: sfcPath,
});
// style
let css = '';
for (const style of descriptor.styles) {
const { code } = compileStyle({
source: style.content,
id: id,
});
css += code;
}
//
const codeList = [];
codeList.push(template.code);
codeList.push(rewriteDefault(script.content, '__sfc_main__'));
codeList.push(`__sfc_main__.__name='${__name}'`);
codeList.push(`__sfc_main__.__file='${__file}'`);
codeList.push(`__sfc_main__.__scopeId='${__scopedId}'`);
codeList.push(`__sfc_main__.__hmrId='${__hmrId}'`);
codeList.push(`__sfc_main__.render=render`);
codeList.push(`export default __sfc_main__`);
const code = codeList.join('\n');
fs.writeFileSync('./target.js', code);
fs.writeFileSync('./target.css', css);
So let us analyze the principles of the entire process step by step.
parse function
The parse function is used to parse the content in SFC and return an object. There is a descriptor attribute in the object, which is used to store the description information of the SFC.
- descriptor.template is used to store template blocks in SFC
- descriptor.script is used to store script blocks in SFC
- descriptor.scriptSetup is used to store scriptSetup blocks in SFC
- descriptor.styles is used to store multiple style blocks in SFC.
{
descriptor: {
filename: "App.vue",
source: "<template>\n<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nconst res = ref('Hello, Scoheart!');\n</script>\n\n<style scoped>\nh1 {\ncolor: blue;\n}\n</style>\n",
template: {
type: "template",
content: "\n<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>\n",
loc: {
start: {
column: 11,
line: 1,
offset: 10,
},
end: {
column: 1,
line: 6,
offset: 73,
},
source: "\n<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>\n",
},
attrs: {
},
ast: {
type: 0,
source: "<template>\n<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nconst res = ref('Hello, Scoheart!');\n</script>\n\n<style scoped>\nh1 {\ncolor: blue;\n}\n</style>\n",
children: [
{
type: 1,
tag: "div",
ns: 0,
tagType: 0,
props: [
],
children: [
{
type: 1,
tag: "h1",
ns: 0,
tagType: 0,
props: [
],
children: [
{
type: 2,
content: "Hi, I'm Scoheart",
loc: {
start: {
column: 7,
line: 3,
offset: 23,
},
end: {
column: 23,
line: 3,
offset: 39,
},
source: "Hi, I'm Scoheart",
},
},
],
loc: {
start: {
column: 3,
line: 3,
offset: 19,
},
end: {
column: 28,
line: 3,
offset: 44,
},
source: "<h1>Hi, I'm Scoheart</h1>",
},
codegenNode: undefined,
},
{
type: 1,
tag: "h1",
ns: 0,
tagType: 0,
props: [
],
children: [
{
type: 5,
content: {
type: 4,
loc: {
start: {
column: 10,
line: 4,
offset: 54,
},
end: {
column: 13,
line: 4,
offset: 57,
},
source: "res",
},
content: "res",
isStatic: false,
constType: 0,
ast: null,
},
loc: {
start: {
column: 7,
line: 4,
offset: 51,
},
end: {
column: 16,
line: 4,
offset: 60,
},
source: "{{ res }}",
},
},
],
loc: {
start: {
column: 3,
line: 4,
offset: 47,
},
end: {
column: 21,
line: 4,
offset: 65,
},
source: "<h1>{{ res }}</h1>",
},
codegenNode: undefined,
},
],
loc: {
start: {
column: 1,
line: 2,
offset: 11,
},
end: {
column: 7,
line: 5,
offset: 72,
},
source: "<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>",
},
codegenNode: undefined,
},
],
helpers: {
},
components: [
],
directives: [
],
hoists: [
],
imports: [
],
cached: 0,
temps: 0,
codegenNode: undefined,
loc: {
start: {
line: 1,
column: 1,
offset: 0,
},
end: {
line: 1,
column: 1,
offset: 0,
},
source: "",
},
},
map: {
version: 3,
sources: [
"App.vue",
],
names: [
],
mappings: ";AACA,CAAC,CAAC,CAAC,CAAC;EACF,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;EACxB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC,CAAC,CAAC",
file: "App.vue",
sourceRoot: "",
sourcesContent: [
"<template>\n<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nconst res = ref('Hello, Scoheart!');\n</script>\n\n<style scoped>\nh1 {\ncolor: blue;\n}\n</style>\n",
],
},
},
script: null,
scriptSetup: {
type: "script",
content: "\nimport { ref } from 'vue';\nconst res = ref('Hello, Scoheart!');\n",
loc: {
start: {
column: 25,
line: 8,
offset: 110,
},
end: {
column: 1,
line: 11,
offset: 175,
},
source: "\nimport { ref } from 'vue';\nconst res = ref('Hello, Scoheart!');\n",
},
attrs: {
setup: true,
lang: "ts",
},
setup: true,
lang: "ts",
},
styles: [
{
type: "style",
content: "\nh1 {\ncolor: blue;\n}\n",
loc: {
start: {
column: 15,
line: 13,
offset: 200,
},
end: {
column: 1,
line: 17,
offset: 221,
},
source: "\nh1 {\ncolor: blue;\n}\n",
},
attrs: {
scoped: true,
},
scoped: true,
map: {
version: 3,
sources: [
"App.vue",
],
names: [
],
mappings: ";AAaA,CAAC,EAAE;AACH,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACX",
file: "App.vue",
sourceRoot: "",
sourcesContent: [
"<template>\n<div>\n <h1>Hi, I'm Scoheart</h1>\n <h1>{{ res }}</h1>\n</div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nconst res = ref('Hello, Scoheart!');\n</script>\n\n<style scoped>\nh1 {\ncolor: blue;\n}\n</style>\n",
],
},
},
],
customBlocks: [
],
cssVars: [
],
slotted: false,
shouldForceReload: (prevImports) => hmrShouldReload(prevImports, descriptor),
},
errors: [
],
}
What this step does is only parse the content in the SFC into a descriptor object, and does not compile it. It can be found that the content field in each block is the content in SFC.
And because we are using the script setup syntax, the content of descriptor.script is null, and the descriptor.scriptSetup field contains the content of the script setup syntax.
Also, we only used one style block, so there is only one element in the descriptor.styles array.
compileScript function
The comileScript function compiles the contents of a scriptSetup or script block. Here we only compiled the scriptSetup block because we don’t have a script block. The final compilation result is:
// script.content
import { defineComponent as _defineComponent } from 'vue';
import { ref } from 'vue';
export default /*#__PURE__*/ _defineComponent({
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const res = ref('Hello, Scoheart!');
const __returned__ = { res };
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
});
return __returned__;
},
});
compileTemplate function
The compileTemplate function compiles the template into a render function. The final compilation result is:
// template.code
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from 'vue';
const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
'h1',
null,
"Hi, I'm Scoheart",
-1 /* HOISTED */
);
export function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock('div', null, [
_hoisted_1,
_createElementVNode('h1', null, _toDisplayString(_ctx.res), 1 /* TEXT */),
])
);
}
We don’t pay attention to the processing of style blocks for the moment. Just looking at the compiled results of script and template, we will find that what is finally export default
exported through is a Vue component. But we will find that the exported Vue component does not have a render function. This is because the render function compiled by SFC’s template becomes an independent named export export function render
.
Does that mean that we now need to mount this render function to the object exported by export default? So how to achieve this?
Let’s first recall export default
where this object came from? That’s right, compiled through the compileScript function.
But here comes the headache. Since it is compiled from this function, we cannot manually export default
add a field to the object ourselves.
It doesn’t matter, in fact, in the @vue/compiler-sfc package, we have already been provided with a solution.
rewriteDefault function
The rewriteDefault function can receive our compiled script content and customize a default exported object name. He is export default
the one who helps to rewrite it.
rewriteDefault(script.content, '__sfc_main__');
The rewritten result is:
import { defineComponent as _defineComponent } from 'vue';
import { ref } from 'vue';
const __sfc_main__ = _defineComponent({
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const res = ref('Hello, Scoheart!');
const __returned__ = { res };
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
});
return __returned__;
},
});
Combine the rewritten script and render functions
So now, we only need to combine export default
the rewritten script and template compiled render function, and then export them by default.
const codeList = [];
codeList.push(template.code);
codeList.push(rewriteDefault(script.content, '__sfc_main__'));
codeList.push(`__sfc_main__.render=render`);
codeList.push(`export default __sfc_main__`);
The contents of the compiled target.js file after finally executing node compiler.js are as follows:
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from 'vue';
const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
'h1',
null,
"Hi, I'm Scoheart",
-1 /* HOISTED */
);
export function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock('div', null, [
_hoisted_1,
_createElementVNode('h1', null, _toDisplayString(_ctx.res), 1 /* TEXT */),
])
);
}
import { defineComponent as _defineComponent } from 'vue';
import { ref } from 'vue';
const __sfc_main__ = _defineComponent({
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const res = ref('Hello, Scoheart!');
const __returned__ = { res };
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
});
return __returned__;
},
});
__sfc_main__.__name = 'App';
__sfc_main__.__file = '/Users/heartsco/Scoheart/code/vite-project/src/App.vue';
__sfc_main__.__scopeId = 'data-v-17080226';
__sfc_main__.__hmrId = '17080226';
__sfc_main__.render = render;
export default __sfc_main__;
The exported object is as follows. It can be seen that it is basically the same as the App we originally printed in the create-vite scaffolding.
{
render: function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock('div', null, [
_hoisted_1,
_createElementVNode(
'h1',
null,
_toDisplayString(_ctx.res),
1 /* TEXT */
),
])
);
},
setup(__props, { expose: __expose }) {
__expose();
const res = ref('Hello, Scoheart!');
const __returned__ = { res };
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true,
});
return __returned__;
},
__name: 'App',
__file: '/Users/heartsco/Scoheart/code/vite-project/src/App.vue',
__scopeId: 'data-v-17080226',
__hmrId: '17080226',
};
compileStyle function
The compileStyle function is used to compile style. When manually compiling SFC, our process here is to output it to a separate css file target.css. The final result is:
h1 {
color: blue;
}
This is a process for manually compiling SFC.
In actual projects, we generally use build tools that integrate the SFC compiler, such as Vite or Vue CLI (based on webpack). @vue/compiler-sfc is used in @vitejs/plugin-vue and vue-loader respectively. .