前言 我們每天寫的vue代碼都是寫在vue文件中,但是瀏覽器卻只認識html、css、js等文件類型。所以這個時候就需要一個工具將vue文件轉換為瀏覽器能夠認識的js文件,想必你第一時間就想到了webpack或者vite。但是webpack和vite本身是沒有能力處理vue文件的,其實實際背後生效的 ...
前言
我們每天寫的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
相關問題。