前言
咱们每天写的vue
代码都是写在vue
文件中,可是浏览器却只认识html
、css
、js
等文件类型。所以这个时分就需求一个东西将vue
文件转换为浏览器能够认识的js
文件,想必你第一时间就想到了webpack
或者vite
。可是webpack
和vite
自身是没有才能处理vue
文件的,其实实践背后生效的是vue-loader和@vitejs/plugin-vue。本文以@vitejs/plugin-vue
举例,经过debug
的方式带你一步一步的搞清楚vue
文件是怎样编译为js
文件的,看不懂你来打我。
举个比如
这个是我的源代码App.vue
文件:
<template>
<h1 class="msg">{{ msg }}</h1>
</template>
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("hello word");
</script>
<style scoped>
.msg {
color: red;
font-weight: bold;
}
</style>
这个比如很简单,在setup
中界说了msg
变量,然后在template
中将msg
渲染出来。
下面这个是我从network
中找到的编译后的js
文件,现已精简过了:
import {
createElementBlock as _createElementBlock,
defineComponent as _defineComponent,
openBlock as _openBlock,
toDisplayString as _toDisplayString,
ref,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
const _sfc_main = _defineComponent({
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const msg = ref("hello word");
const __returned__ = { msg };
return __returned__;
},
});
const _hoisted_1 = { class: "msg" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"h1",
_hoisted_1,
_toDisplayString($setup.msg),
1
/* TEXT */
)
);
}
__sfc__.render = render;
export default _sfc_main;
编译后的js
代码中咱们能够看到首要有三部分,想必你也猜到了这三部分刚好对应vue
文件的那三块。
-
_sfc_main
目标的setup
方法对应vue
文件中的<script setup lang="ts">
模块。 -
_sfc_render
函数对应vue
文件中的<template>
模块。 -
import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
对应vue
文件中的<style scoped>
模块。
debug搞清楚怎样将vue
文件编译为js
文件
咱们应该都知道,前端代码运行环境首要有两个,node
端和浏览器端,别离对应咱们了解的编译时和运行时。浏览器显着是不认识vue
文件的,所以vue
文件编译成js
这一进程肯定不是在运行时的浏览器端。很显着这一进程是在编译时的node
端。
要在node
端打断点,咱们需求发动一个debug 终端。这儿以vscode
举例,首先咱们需求打开终端,然后点击终端中的+
号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal
就能够发动一个debug
终端。
假设vue
文件编译为js
文件是一个毛线团,那么他的线头一定是vite.config.ts
文件中运用@vitejs/plugin-vue
的当地。经过这个线头开始debug
咱们就能够整理清楚完好的作业流程。
vuePlugin函数
咱们给上方图片的vue
函数打了一个断点,然后在debug
终端上面履行yarn dev
,咱们看到断点现已停留在了vue
函数这儿。然后点击step into
,断点走到了@vitejs/plugin-vue
库中的一个vuePlugin
函数中。咱们看到vuePlugin
函数中的内容代码大概是这样的:
function vuePlugin(rawOptions = {}) {
const options = shallowRef({
compiler: null,
// 省掉...
});
return {
name: "vite:vue",
handleHotUpdate(ctx) {
// ...
},
config(config) {
// ..
},
configResolved(config) {
// ..
},
configureServer(server) {
// ..
},
buildStart() {
// ..
},
async resolveId(id) {
// ..
},
load(id, opt) {
// ..
},
transform(code, id, opt) {
// ..
}
};
}
@vitejs/plugin-vue
是作为一个plugins
插件在vite
中运用,vuePlugin
函数回来的目标中的buildStart
、transform
方法便是对应的插件钩子函数。vite
会在对应的时分调用这些插件的钩子函数,比如当vite
服务器发动时就会调用插件里边的buildStart
等函数,当vite
解析每个模块时就会调用transform
等函数。更多vite
钩子相关内容检查官网。
咱们这儿首要看buildStart
和transform
两个钩子函数,别离是服务器发动时调用和解析每个模块时调用。给这两个钩子函数打上断点。
然后点击Continue(F5),vite
服务发动后就会走到buildStart
钩子函数中打的断点。咱们能够看到buildStart
钩子函数的代码是这样的:
buildStart() {
const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
}
将鼠标放到options.value.compiler
上面咱们看到此刻options.value.compiler
的值为null
,所以代码会走到resolveCompiler
函数中,点击Step Into(F11)走到resolveCompiler
函数中。看到resolveCompiler
函数代码如下:
function resolveCompiler(root) {
const compiler = tryResolveCompiler(root) || tryResolveCompiler();
return compiler;
}
function tryResolveCompiler(root) {
const vueMeta = tryRequire("vue/package.json", root);
if (vueMeta && vueMeta.version.split(".")[0] >= 3) {
return tryRequire("vue/compiler-sfc", root);
}
}
在resolveCompiler
函数中调用了tryResolveCompiler
函数,在tryResolveCompiler
函数中判别当时项目是否是vue3.x
版本,然后将vue/compiler-sfc
包回来。所以经过初始化后options.value.compiler
的值便是vue
的底层库vue/compiler-sfc
,记住这个后边会用。
然后点击Continue(F5)放掉断点,在浏览器中打开对应的页面,比如:http://localhost:5173/ 。此刻vite
将会编译这个页面要用到的一切文件,就会走到transform
钩子函数断点中了。由于解析每个文件都会走到transform
钩子函数中,可是咱们只重视App.vue
文件是怎样解析的,所以为了便利咱们直接在transform
函数中添加了下面这段代码,而且删掉了本来在transform
钩子函数中打的断点,这样就只要解析到App.vue
文件的时分才会走到断点中去。
经过debug咱们发现解析App.vue
文件时transform
函数实践便是履行了transformMain
函数,至于transformStyle
函数后边讲解析style
的时分会讲:
transform(code, id, opt) {
const { filename, query } = parseVueRequest(id);
if (!query.vue) {
return transformMain(
code,
filename,
options.value,
this,
ssr,
customElementFilter.value(filename)
);
} else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.type === "style") {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename
);
}
}
}
transformMain
函数
持续debug断点走进transformMain
函数,发现transformMain
函数中代码逻辑很明晰。依照次序别离是:
- 依据源代码code字符串调用
createDescriptor
函数生成一个descriptor
目标。 - 调用
genScriptCode
函数传入第一步生成的descriptor
目标将<script setup>
模块编译为浏览器可履行的js
代码。 - 调用
genTemplateCode
函数传入第一步生成的descriptor
目标将<template>
模块编译为render
函数。 - 调用
genStyleCode
函数传入第一步生成的descriptor
目标将<style scoped>
模块编译为类似这样的import
句子,import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
。
createDescriptor
函数
咱们先来看看createDescriptor
函数,将断点走到createDescriptor(filename, code, options)
这一行代码,能够看到传入的filename
便是App.vue
的文件路径,code
便是App.vue
中咱们写的源代码。
debug
走进createDescriptor
函数,看到createDescriptor
函数的代码如下:
function createDescriptor(filename, source, { root, isProduction, sourceMap, compiler, template }, hmr = false) {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap,
templateParseOptions: template?.compilerOptions
});
const normalizedPath = slash(path.normalize(path.relative(root, filename)));
descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));
return { descriptor, errors };
}
这个compiler
是不是觉得有点了解?compiler
是调用createDescriptor
函数时传入的第三个参数解构而来,而第三个参数便是options
。还记得咱们之前在vite
发动时调用了buildStart
钩子函数,然后将vue
底层包vue/compiler-sfc
赋值给options
的compiler
特点。那这儿的compiler.parse
其实便是调用的vue/compiler-sfc
包露出出来的parse
函数,这是一个vue
露出出来的底层的API
,这篇文章咱们不会对底层API进行源码解析,经过检查parse
函数的输入和输出根本就能够搞清楚parse
函数的效果。下面这个是parse
函数的类型界说:
export function parse(
source: string,
options: SFCParseOptions = {},
): SFCParseResult {}
从上面咱们能够看到parse
函数接纳两个参数,第一个参数为vue
文件的源代码,在咱们这儿便是App.vue
中的code
字符串,第二个参数是一些options
选项。
咱们再来看看parse
函数的回来值SFCParseResult
,首要有类型为SFCDescriptor
的descriptor
特点需求重视。
export interface SFCParseResult {
descriptor: SFCDescriptor
errors: (CompilerError | SyntaxError)[]
}
export interface SFCDescriptor {
filename: string
source: string
template: SFCTemplateBlock | null
script: SFCScriptBlock | null
scriptSetup: SFCScriptBlock | null
styles: SFCStyleBlock[]
customBlocks: SFCBlock[]
cssVars: string[]
slotted: boolean
shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
}
细心看看SFCDescriptor
类型,其中的template
特点便是App.vue
文件对应的template
标签中的内容,里边包含了由App.vue
文件中的template
模块编译成的AST笼统语法树
和原始的template
中的代码。
咱们再来看script
和scriptSetup
特点,由于vue
文件中能够写多个script
标签,scriptSetup
对应的便是有setup
的script
标签,script
对应的便是没有setup
对应的script
标签。咱们这个场景中只要scriptSetup
特点,里边相同包含了App.vue
中的script
模块中的内容。
咱们再来看看styles
特点,这儿的styles
特点是一个数组,是由于咱们能够在vue
文件中写多个style
模块,里边相同包含了App.vue
中的style
模块中的内容。
所以这一步履行createDescriptor
函数生成的descriptor
目标中首要有三个特点,template
特点包含了App.vue
文件中的template
模块code
字符串和AST笼统语法树
,scriptSetup
特点包含了App.vue
文件中的<script setup>
模块的code
字符串,styles
特点包含了App.vue
文件中<style>
模块中的code
字符串。createDescriptor
函数的履行流程图如下:
genScriptCode
函数
咱们再来看genScriptCode
函数是怎样将<script setup>
模块编译成可履行的js
代码,相同将断点走到调用genScriptCode
函数的当地,genScriptCode
函数首要接纳咱们上一步生成的descriptor
目标,调用genScriptCode
函数后会将编译后的script
模块代码赋值给scriptCode
变量。
const { code: scriptCode, map: scriptMap } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr,
customElement
);
将断点走到genScriptCode
函数内部,在genScriptCode
函数中首要便是这行代码: const script = resolveScript(descriptor, options, ssr, customElement);
。将第一步生成的descriptor
目标作为参数传给resolveScript
函数,回来值便是编译后的js
代码,genScriptCode
函数的代码简化后如下:
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
let scriptCode = `const ${scriptIdentifier} = {}`;
const script = resolveScript(descriptor, options, ssr, customElement);
if (script) {
scriptCode = script.content;
map = script.map;
}
return {
code: scriptCode,
map
};
}
咱们持续将断点走到resolveScript
函数内部,发现resolveScript
中的代码其实也很简单,简化后的代码如下:
function resolveScript(descriptor, options, ssr, customElement) {
let resolved = null;
resolved = options.compiler.compileScript(descriptor, {
...options.script,
id: descriptor.id,
isProd: options.isProduction,
inlineTemplate: isUseInlineTemplate(descriptor, !options.devServer),
templateOptions: resolveTemplateCompilerOptions(descriptor, options, ssr),
sourceMap: options.sourceMap,
genDefaultAs: canInlineMain(descriptor, options) ? scriptIdentifier : void 0,
customElement
});
return resolved;
}
这儿的options.compiler
咱们前面第一步的时分现已解说过了,options.compiler
目标实践便是vue
底层包vue/compiler-sfc
露出的目标,这儿的options.compiler.compileScript()
其实便是调用的vue/compiler-sfc
包露出出来的compileScript
函数,相同也是一个vue
露出出来的底层的API
,后边咱们的剖析defineOptions
等文章时会去深入剖析compileScript
函数,这篇文章咱们不会去读compileScript
函数的源码。经过检查compileScript
函数的输入和输出根本就能够搞清楚compileScript
函数的效果。下面这个是compileScript
函数的类型界说:
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions,
): SFCScriptBlock{}
这个函数的入参是一个SFCDescriptor
目标,便是咱们第一步调用生成createDescriptor
函数生成的descriptor
目标,第二个参数是一些options
选项。咱们再来看回来值SFCScriptBlock
类型:
export interface SFCScriptBlock extends SFCBlock {
type: 'script'
setup?: string | boolean
bindings?: BindingMetadata
imports?: Record<string, ImportBinding>
scriptAst?: import('@babel/types').Statement[]
scriptSetupAst?: import('@babel/types').Statement[]
warnings?: string[]
/**
* Fully resolved dependency file paths (unix slashes) with imported types
* used in macros, used for HMR cache busting in @vitejs/plugin-vue and
* vue-loader.
*/
deps?: string[]
}
export interface SFCBlock {
type: string
content: string
attrs: Record<string, string | true>
loc: SourceLocation
map?: RawSourceMap
lang?: string
src?: string
}
回来值类型中首要有scriptAst
、scriptSetupAst
、content
这三个特点,scriptAst
为编译不带setup
特点的script
标签生成的AST笼统语法树。scriptSetupAst
为编译带setup
特点的script
标签生成的AST笼统语法树,content
为vue
文件中的script
模块编译后生成的浏览器可履行的js
代码。下面这个是履行vue/compiler-sfc
的compileScript
函数回来结果:
持续将断点走回genScriptCode
函数,现在逻辑就很明晰了。这儿的script
目标便是调用vue/compiler-sfc
的compileScript
函数回来目标,scriptCode
便是script
目标的content
特点 ,也便是将vue
文件中的script
模块经过编译后生成浏览器可直接履行的js
代码code
字符串。
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
let scriptCode = `const ${scriptIdentifier} = {}`;
const script = resolveScript(descriptor, options, ssr, customElement);
if (script) {
scriptCode = script.content;
map = script.map;
}
return {
code: scriptCode,
map
};
}
genScriptCode
函数的履行流程图如下:
genTemplateCode
函数
咱们再来看genTemplateCode
函数是怎样将template
模块编译成render
函数的,相同将断点走到调用genTemplateCode
函数的当地,genTemplateCode
函数首要接纳咱们上一步生成的descriptor
目标,调用genTemplateCode
函数后会将编译后的template
模块代码赋值给templateCode
变量。
({ code: templateCode, map: templateMap } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr,
customElement
))
相同将断点走到genTemplateCode
函数内部,在genTemplateCode
函数中首要便是回来transformTemplateInMain
函数的回来值,genTemplateCode
函数的代码简化后如下:
async function genTemplateCode(descriptor, options, pluginContext, ssr, customElement) {
const template = descriptor.template;
return transformTemplateInMain(
template.content,
descriptor,
options,
pluginContext,
ssr,
customElement
);
}
咱们持续将断点走进transformTemplateInMain
函数,发现这儿也首要是调用compile
函数,代码如下:
function transformTemplateInMain(code, descriptor, options, pluginContext, ssr, customElement) {
const result = compile(
code,
descriptor,
options,
pluginContext,
ssr,
customElement
);
return {
...result,
code: result.code.replace(
/nexport (function|const) (render|ssrRender)/,
"n$1 _sfc_$2"
)
};
}
同理将断点走进到compile
函数内部,咱们看到compile
函数的代码是下面这样的:
function compile(code, descriptor, options, pluginContext, ssr, customElement) {
const result = options.compiler.compileTemplate({
...resolveTemplateCompilerOptions(descriptor, options, ssr),
source: code
});
return result;
}
相同这儿也用到了options.compiler
,调用options.compiler.compileTemplate()
其实便是调用的vue/compiler-sfc
包露出出来的compileTemplate
函数,这也是一个vue
露出出来的底层的API
。不过这儿和前面不同的是compileTemplate
接纳的不是descriptor
目标,而是一个SFCTemplateCompileOptions
类型的目标,所以这儿需求调用resolveTemplateCompilerOptions
函数将参数转换成SFCTemplateCompileOptions
类型的目标。这篇文章咱们不会对底层API进行解析。经过检查compileTemplate
函数的输入和输出根本就能够搞清楚compileTemplate
函数的效果。下面这个是compileTemplate
函数的类型界说:
export function compileTemplate(
options: SFCTemplateCompileOptions,
): SFCTemplateCompileResults {}
入参options
首要便是需求编译的template
中的源代码和对应的AST笼统语法树
。咱们来看看回来值SFCTemplateCompileResults
,这儿面的code
便是编译后的render
函数字符串。
export interface SFCTemplateCompileResults {
code: string
ast?: RootNode
preamble?: string
source: string
tips: string[]
errors: (string | CompilerError)[]
map?: RawSourceMap
}
genTemplateCode
函数的履行流程图如下:
genStyleCode
函数
咱们再来看最终一个genStyleCode
函数,相同将断点走到调用genStyleCode
的当地。一样的接纳descriptor
目标。代码如下:
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
customElement,
attachedProps
);
咱们将断点走进genStyleCode
函数内部,发现和前面genScriptCode
和genTemplateCode
函数有点不一样,下面这个是我简化后的genStyleCode
函数代码:
async function genStyleCode(descriptor, pluginContext, customElement, attachedProps) {
let stylesCode = ``;
if (descriptor.styles.length) {
for (let i = 0; i < descriptor.styles.length; i++) {
const style = descriptor.styles[i];
const src = style.src || descriptor.filename;
const attrsQuery = attrsToQuery(style.attrs, "css");
const srcQuery = style.src ? style.scoped ? `&src=${descriptor.id}` : "&src=true" : "";
const directQuery = customElement ? `&inline` : ``;
const scopedQuery = style.scoped ? `&scoped=${descriptor.id}` : ``;
const query = `?vue&type=style&index=${i}${srcQuery}${directQuery}${scopedQuery}`;
const styleRequest = src + query + attrsQuery;
stylesCode += `
import ${JSON.stringify(styleRequest)}`;
}
}
return stylesCode;
}
咱们前面讲过由于vue
文件中可能会有多个style
标签,所以descriptor
目标的styles
特点是一个数组。遍历descriptor.styles
数组,咱们发现for
循环内全部都是一堆赋值操作,没有调用vue/compiler-sfc
包露出出来的任何API
。将断点走到 return stylesCode;
,看看stylesCode
到底是什么东西?
经过打印咱们发现stylesCode
居然变成了一条import
句子,而且import
的仍是当时App.vue
文件,仅仅多了几个query
别离是:vue
、type
、index
、scoped
、lang
。再来回忆一下前面讲的@vitejs/plugin-vue
的transform
钩子函数,当vite
解析每个模块时就会调用transform
等函数。所以当代码运行到这行import
句子的时分会再次走到transform
钩子函数中。咱们再来看看transform
钩子函数的代码:
transform(code, id, opt) {
const { filename, query } = parseVueRequest(id);
if (!query.vue) {
// 省掉
} else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.type === "style") {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename
);
}
}
}
当query
中有vue
字段,而且query
中type
字段值为style
时就会履行transformStyle
函数,咱们给transformStyle
函数打个断点。当履行上面那条import
句子时就会走到断点中,咱们进到transformStyle
中看看。
async function transformStyle(code, descriptor, index, options, pluginContext, filename) {
const block = descriptor.styles[index];
const result = await options.compiler.compileStyleAsync({
...options.style,
filename: descriptor.filename,
id: `data-v-${descriptor.id}`,
isProd: options.isProduction,
source: code,
scoped: block.scoped,
...options.cssDevSourcemap ? {
postcssOptions: {
map: {
from: filename,
inline: false,
annotation: false
}
}
} : {}
});
return {
code: result.code,
map
};
}
transformStyle
函数的实现咱们看着就很了解了,和前面处理template
和script
一样都是调用的vue/compiler-sfc
包露出出来的compileStyleAsync
函数,这也是一个vue
露出出来的底层的API
。相同咱们不会对底层API进行解析。经过检查compileStyleAsync
函数的输入和输出根本就能够搞清楚compileStyleAsync
函数的效果。
export function compileStyleAsync(
options: SFCAsyncStyleCompileOptions,
): Promise<SFCStyleCompileResults> {}
咱们先来看看SFCAsyncStyleCompileOptions
入参:
interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
isAsync?: boolean
modules?: boolean
modulesOptions?: CSSModulesOptions
}
interface SFCStyleCompileOptions {
source: string
filename: string
id: string
scoped?: boolean
trim?: boolean
isProd?: boolean
inMap?: RawSourceMap
preprocessLang?: PreprocessLang
preprocessOptions?: any
preprocessCustomRequire?: (id: string) => any
postcssOptions?: any
postcssPlugins?: any[]
map?: RawSourceMap
}
入参首要重视几个字段,source
字段为style
标签中的css
原始代码。scoped
字段为style
标签中是否有scoped
attribute。id
字段为咱们在调查 DOM 结构时看到的 data-v-xxxxx
。这个是debug
时入参截图:
再来看看回来值SFCStyleCompileResults
目标,首要便是code
特点,这个是经过编译后的css
字符串,现已加上了data-v-xxxxx
。
interface SFCStyleCompileResults {
code: string
map: RawSourceMap | undefined
rawResult: Result | LazyResult | undefined
errors: Error[]
modules?: Record<string, string>
dependencies: Set<string>
}
这个是debug
时compileStyleAsync
函数回来值的截图:
genStyleCode
函数的履行流程图如下:
transformMain
函数简化后的代码
现在咱们能够来看transformMain
函数简化后的代码:
async function transformMain(code, filename, options, pluginContext, ssr, customElement) {
const { descriptor, errors } = createDescriptor(filename, code, options);
const { code: scriptCode, map: scriptMap } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr,
customElement
);
let templateCode = "";
({ code: templateCode, map: templateMap } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr,
customElement
));
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
customElement,
attachedProps
);
const output = [
scriptCode,
templateCode,
stylesCode
];
let resolvedCode = output.join("n");
return {
code: resolvedCode,
map: resolvedMap || {
mappings: ""
},
meta: {
vite: {
lang: descriptor.script?.lang || descriptor.scriptSetup?.lang || "js"
}
}
};
}
transformMain
函数中的代码履行主流程,其实便是对应了一个vue
文件编译成js
文件的流程。
首先调用createDescriptor
函数将一个vue
文件解析为一个descriptor
目标。
然后以descriptor
目标为参数调用genScriptCode
函数,将vue
文件中的<script>
模块代码编译成浏览器可履行的js
代码code
字符串,赋值给scriptCode
变量。
接着以descriptor
目标为参数调用genTemplateCode
函数,将vue
文件中的<template>
模块代码编译成render
函数code
字符串,赋值给templateCode
变量。
然后以descriptor
目标为参数调用genStyleCode
函数,将vue
文件中的<style>
模块代码编译成了import
句子code
字符串,比如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
,赋值给stylesCode
变量。
然后将scriptCode
、templateCode
、stylesCode
运用换行符n
拼接起来得到resolvedCode
,这个resolvedCode
便是一个vue
文件编译成js
文件的代码code
字符串。这个是debug
时resolvedCode
变量值的截图:
总结
这篇文章经过debug
的方式一步一步的带你了解vue
文件编译成js
文件的完好流程,下面是一个完好的流程图。假如文字太小看不清,能够将图片保存下来或者扩大看:
@vitejs/plugin-vue-jsx
库中有个叫transform
的钩子函数,每当vite
加载模块的时分就会触发这个钩子函数。所以当import
一个vue
文件的时分,就会走到@vitejs/plugin-vue-jsx
中的transform
钩子函数中,在transform
钩子函数中首要调用了transformMain
函数。
第一次解析这个vue
文件时,在transform
钩子函数中首要调用了transformMain
函数。在transformMain
函数中首要调用了4个函数,别离是:createDescriptor
、genScriptCode
、genTemplateCode
、genStyleCode
。
createDescriptor
接纳的参数为当时vue
文件代码code
字符串,回来值为一个descriptor
目标。目标中首要有四个特点template
、scriptSetup
、script
、styles
。
-
descriptor.template.ast
便是由vue
文件中的template
模块生成的AST笼统语法树
。 -
descriptor.template.content
便是vue
文件中的template
模块的代码字符串。 -
scriptSetup
和script
的区别是别离对应的是vue
文件中有setup
特点的<script>
模块和无setup
特点的<script>
模块。descriptor.scriptSetup.content
便是vue
文件中的<script setup>
模块的代码字符串。
genScriptCode
函数为底层调用vue/compiler-sfc
的compileScript
函数,依据第一步的descriptor
目标将vue
文件的<script setup>
模块转换为浏览器可直接履行的js
代码。
genTemplateCode
函数为底层调用vue/compiler-sfc
的compileTemplate
函数,依据第一步的descriptor
目标将vue
文件的<template>
模块转换为render
函数。
genStyleCode
函数为将vue
文件的style
模块转换为import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
样子的import
句子。
然后运用换行符n
将genScriptCode
函数、genTemplateCode
函数、genStyleCode
函数的回来值拼接起来赋值给变量resolvedCode
,这个resolvedCode
便是vue
文件编译成js
文件的code
字符串。
当浏览器履行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
句子时,触发了加载模块操作,再次触发了@vitejs/plugin-vue-jsx
中的transform
钩子函数。此刻由于有了type=style
的query
,所以在transform
函数中会履行transformStyle
函数,在transformStyle
函数中相同也是调用vue/compiler-sfc
的compileStyleAsync
函数,依据第一步的descriptor
目标将vue
文件的<style>
模块转换为编译后的css
代码code
字符串,至此编译style
部分也讲完了。
重视大众号:前端欧阳,解锁我更多vue
干货文章,而且能够免费向我咨询vue
相关问题。