通過debug搞清楚.vue文件如何變成.js文件(案例詳解)
我們每天寫的vue代碼都是寫在vue文件中,但是瀏覽器卻只認(rèn)識(shí)html、css、js等文件類型。所以這個(gè)時(shí)候就需要一個(gè)工具將vue文件轉(zhuǎn)換為瀏覽器能夠認(rèn)識(shí)的js文件,想必你第一時(shí)間就想到了webpack或者vite。但是webpack和vite本身是沒有能力處理vue文件的,其實(shí)實(shí)際背后生效的是vue-loader和@vitejs/plugin-vue。本文以@vitejs/plugin-vue舉例,通過debug的方式帶你一步一步的搞清楚vue文件是如何編譯為js文件的。
舉個(gè)例子
這個(gè)是我的源代碼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>這個(gè)例子很簡單,在setup中定義了msg變量,然后在template中將msg渲染出來。
下面這個(gè)是我從network中找到的編譯后的js文件,已經(jīng)精簡過了:
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代碼中我們可以看到主要有三部分,想必你也猜到了這三部分剛好對(duì)應(yīng)vue文件的那三塊。
_sfc_main對(duì)象的setup方法對(duì)應(yīng)vue文件中的<script setup lang="ts">模塊。_sfc_render函數(shù)對(duì)應(yīng)vue文件中的<template>模塊。import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";對(duì)應(yīng)vue文件中的<style scoped>模塊。
debug搞清楚如何將vue文件編譯為js文件
大家應(yīng)該都知道,前端代碼運(yùn)行環(huán)境主要有兩個(gè),node端和瀏覽器端,分別對(duì)應(yīng)我們熟悉的編譯時(shí)和運(yùn)行時(shí)。瀏覽器明顯是不認(rèn)識(shí)vue文件的,所以vue文件編譯成js這一過程肯定不是在運(yùn)行時(shí)的瀏覽器端。很明顯這一過程是在編譯時(shí)的node端。
要在node端打斷點(diǎn),我們需要啟動(dòng)一個(gè)debug 終端。這里以vscode舉例,首先我們需要打開終端,然后點(diǎn)擊終端中的+號(hào)旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal就可以啟動(dòng)一個(gè)debug終端。

假如vue文件編譯為js文件是一個(gè)毛線團(tuán),那么他的線頭一定是vite.config.ts文件中使用@vitejs/plugin-vue的地方。通過這個(gè)線頭開始debug我們就能夠梳理清楚完整的工作流程。

vuePlugin函數(shù)
我們給上方圖片的vue函數(shù)打了一個(gè)斷點(diǎn),然后在debug終端上面執(zhí)行yarn dev,我們看到斷點(diǎn)已經(jīng)停留在了vue函數(shù)這里。然后點(diǎn)擊step into,斷點(diǎn)走到了@vitejs/plugin-vue庫中的一個(gè)vuePlugin函數(shù)中。我們看到vuePlugin函數(shù)中的內(nèi)容代碼大概是這樣的:
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是作為一個(gè)plugins插件在vite中使用,vuePlugin函數(shù)返回的對(duì)象中的buildStart、transform方法就是對(duì)應(yīng)的插件鉤子函數(shù)。vite會(huì)在對(duì)應(yīng)的時(shí)候調(diào)用這些插件的鉤子函數(shù),比如當(dāng)vite服務(wù)器啟動(dòng)時(shí)就會(huì)調(diào)用插件里面的buildStart等函數(shù),當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。更多vite鉤子相關(guān)內(nèi)容查看官網(wǎng)。
我們這里主要看buildStart和transform兩個(gè)鉤子函數(shù),分別是服務(wù)器啟動(dòng)時(shí)調(diào)用和解析每個(gè)模塊時(shí)調(diào)用。給這兩個(gè)鉤子函數(shù)打上斷點(diǎn)。

然后點(diǎn)擊Continue(F5),vite服務(wù)啟動(dòng)后就會(huì)走到buildStart鉤子函數(shù)中打的斷點(diǎn)。我們可以看到buildStart鉤子函數(shù)的代碼是這樣的:
buildStart() {
const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
}將鼠標(biāo)放到options.value.compiler上面我們看到此時(shí)options.value.compiler的值為null,所以代碼會(huì)走到resolveCompiler函數(shù)中,點(diǎn)擊Step Into(F11)走到resolveCompiler函數(shù)中??吹?code>resolveCompiler函數(shù)代碼如下:
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函數(shù)中調(diào)用了tryResolveCompiler函數(shù),在tryResolveCompiler函數(shù)中判斷當(dāng)前項(xiàng)目是否是vue3.x版本,然后將vue/compiler-sfc包返回。所以經(jīng)過初始化后options.value.compiler的值就是vue的底層庫vue/compiler-sfc,記住這個(gè)后面會(huì)用。
然后點(diǎn)擊Continue(F5)放掉斷點(diǎn),在瀏覽器中打開對(duì)應(yīng)的頁面,比如:http://localhost:5173/ 。此時(shí)vite將會(huì)編譯這個(gè)頁面要用到的所有文件,就會(huì)走到transform鉤子函數(shù)斷點(diǎn)中了。由于解析每個(gè)文件都會(huì)走到transform鉤子函數(shù)中,但是我們只關(guān)注App.vue文件是如何解析的,所以為了方便我們直接在transform函數(shù)中添加了下面這段代碼,并且刪掉了原來在transform鉤子函數(shù)中打的斷點(diǎn),這樣就只有解析到App.vue文件的時(shí)候才會(huì)走到斷點(diǎn)中去。

經(jīng)過debug我們發(fā)現(xiàn)解析App.vue文件時(shí)transform函數(shù)實(shí)際就是執(zhí)行了transformMain函數(shù),至于transformStyle函數(shù)后面講解析style的時(shí)候會(huì)講:
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函數(shù)
繼續(xù)debug斷點(diǎn)走進(jìn)transformMain函數(shù),發(fā)現(xiàn)transformMain函數(shù)中代碼邏輯很清晰。按照順序分別是:
- 根據(jù)源代碼code字符串調(diào)用
createDescriptor函數(shù)生成一個(gè)descriptor對(duì)象。 - 調(diào)用
genScriptCode函數(shù)傳入第一步生成的descriptor對(duì)象將<script setup>模塊編譯為瀏覽器可執(zhí)行的js代碼。 - 調(diào)用
genTemplateCode函數(shù)傳入第一步生成的descriptor對(duì)象將<template>模塊編譯為render函數(shù)。 - 調(diào)用
genStyleCode函數(shù)傳入第一步生成的descriptor對(duì)象將<style scoped>模塊編譯為類似這樣的import語句,import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";。
createDescriptor
我們先來看看createDescriptor函數(shù),將斷點(diǎn)走到createDescriptor(filename, code, options)這一行代碼,可以看到傳入的filename就是App.vue的文件路徑,code就是App.vue中我們寫的源代碼。

debug走進(jìn)createDescriptor函數(shù),看到createDescriptor函數(shù)的代碼如下:
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 };
}這個(gè)compiler是不是覺得有點(diǎn)熟悉?compiler是調(diào)用createDescriptor函數(shù)時(shí)傳入的第三個(gè)參數(shù)解構(gòu)而來,而第三個(gè)參數(shù)就是options。還記得我們之前在vite啟動(dòng)時(shí)調(diào)用了buildStart鉤子函數(shù),然后將vue底層包vue/compiler-sfc賦值給options的compiler屬性。那這里的compiler.parse其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的parse函數(shù),這是一個(gè)vue暴露出來的底層的API,這篇文章我們不會(huì)對(duì)底層API進(jìn)行源碼解析,通過查看parse函數(shù)的輸入和輸出基本就可以搞清楚parse函數(shù)的作用。下面這個(gè)是parse函數(shù)的類型定義:
export function parse(
source: string,
options: SFCParseOptions = {},
): SFCParseResult {}從上面我們可以看到parse函數(shù)接收兩個(gè)參數(shù),第一個(gè)參數(shù)為vue文件的源代碼,在我們這里就是App.vue中的code字符串,第二個(gè)參數(shù)是一些options選項(xiàng)。我們再來看看parse函數(shù)的返回值SFCParseResult,主要有類型為SFCDescriptor的descriptor屬性需要關(guān)注。
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
}仔細(xì)看看SFCDescriptor類型,其中的template屬性就是App.vue文件對(duì)應(yīng)的template標(biāo)簽中的內(nèi)容,里面包含了由App.vue文件中的template模塊編譯成的AST抽象語法樹和原始的template中的代碼。

我們再來看script和scriptSetup屬性,由于vue文件中可以寫多個(gè)script標(biāo)簽,scriptSetup對(duì)應(yīng)的就是有setup的script標(biāo)簽,script對(duì)應(yīng)的就是沒有setup對(duì)應(yīng)的script標(biāo)簽。我們這個(gè)場景中只有scriptSetup屬性,里面同樣包含了App.vue中的script模塊中的內(nèi)容。

我們再來看看styles屬性,這里的styles屬性是一個(gè)數(shù)組,是因?yàn)槲覀兛梢栽?code>vue文件中寫多個(gè)style模塊,里面同樣包含了App.vue中的style模塊中的內(nèi)容。

所以這一步執(zhí)行createDescriptor函數(shù)生成的descriptor對(duì)象中主要有三個(gè)屬性,template屬性包含了App.vue文件中的template模塊code字符串和AST抽象語法樹,scriptSetup屬性包含了App.vue文件中 <script setup> 的模塊的code字符串,styles屬性包含了App.vue文件中 <style> 模塊中的code字符串。createDescriptor函數(shù)的執(zhí)行流程圖如下:

genScriptCode函數(shù)
我們再來看genScriptCode函數(shù)是如何將 <script setup> 模塊編譯成可執(zhí)行的js代碼,同樣將斷點(diǎn)走到調(diào)用genScriptCode函數(shù)的地方,genScriptCode函數(shù)主要接收我們上一步生成的descriptor對(duì)象,調(diào)用genScriptCode函數(shù)后會(huì)將編譯后的script模塊代碼賦值給scriptCode變量。
const { code: scriptCode, map: scriptMap } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr,
customElement
);將斷點(diǎn)走到genScriptCode函數(shù)內(nèi)部,在genScriptCode函數(shù)中主要就是這行代碼: const script = resolveScript(descriptor, options, ssr, customElement);。將第一步生成的descriptor對(duì)象作為參數(shù)傳給resolveScript函數(shù),返回值就是編譯后的js代碼,genScriptCode函數(shù)的代碼簡化后如下:
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
};
}我們繼續(xù)將斷點(diǎn)走到resolveScript函數(shù)內(nèi)部,發(fā)現(xiàn)resolveScript中的代碼其實(shí)也很簡單,簡化后的代碼如下:
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我們前面第一步的時(shí)候已經(jīng)解釋過了,options.compiler對(duì)象實(shí)際就是vue底層包vue/compiler-sfc暴露的對(duì)象,這里的options.compiler.compileScript()其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的compileScript函數(shù),同樣也是一個(gè)vue暴露出來的底層的API。通過查看compileScript函數(shù)的輸入和輸出基本就可以搞清楚compileScript函數(shù)的作用。下面這個(gè)是compileScript函數(shù)的類型定義:
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions,
): SFCScriptBlock{}這個(gè)函數(shù)的入?yún)⑹且粋€(gè)SFCDescriptor對(duì)象,就是我們第一步調(diào)用生成createDescriptor函數(shù)生成的descriptor對(duì)象,第二個(gè)參數(shù)是一些options選項(xiàng)。我們再來看返回值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這三個(gè)屬性,scriptAst為編譯不帶setup屬性的script標(biāo)簽生成的AST抽象語法樹。scriptSetupAst為編譯帶setup屬性的script標(biāo)簽生成的AST抽象語法樹,content為vue文件中的script模塊編譯后生成的瀏覽器可執(zhí)行的js代碼。下面這個(gè)是執(zhí)行vue/compiler-sfc的compileScript函數(shù)返回結(jié)果:

繼續(xù)將斷點(diǎn)走回genScriptCode函數(shù),現(xiàn)在邏輯就很清晰了。這里的script對(duì)象就是調(diào)用vue/compiler-sfc的compileScript函數(shù)返回對(duì)象,scriptCode就是script對(duì)象的content屬性 ,也就是將vue文件中的script模塊經(jīng)過編譯后生成瀏覽器可直接執(zhí)行的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函數(shù)的執(zhí)行流程圖如下:

genTemplateCode函數(shù)
我們再來看genTemplateCode函數(shù)是如何將template模塊編譯成render函數(shù)的,同樣將斷點(diǎn)走到調(diào)用genTemplateCode函數(shù)的地方,genTemplateCode函數(shù)主要接收我們上一步生成的descriptor對(duì)象,調(diào)用genTemplateCode函數(shù)后會(huì)將編譯后的template模塊代碼賦值給templateCode變量。
({ code: templateCode, map: templateMap } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr,
customElement
))同樣將斷點(diǎn)走到genTemplateCode函數(shù)內(nèi)部,在genTemplateCode函數(shù)中主要就是返回transformTemplateInMain函數(shù)的返回值,genTemplateCode函數(shù)的代碼簡化后如下:
async function genTemplateCode(descriptor, options, pluginContext, ssr, customElement) {
const template = descriptor.template;
return transformTemplateInMain(
template.content,
descriptor,
options,
pluginContext,
ssr,
customElement
);
}我們繼續(xù)將斷點(diǎn)走進(jìn)transformTemplateInMain函數(shù),發(fā)現(xiàn)這里也主要是調(diào)用compile函數(shù),代碼如下:
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"
)
};
}同理將斷點(diǎn)走進(jìn)到compile函數(shù)內(nèi)部,我們看到compile函數(shù)的代碼是下面這樣的:
function compile(code, descriptor, options, pluginContext, ssr, customElement) {
const result = options.compiler.compileTemplate({
...resolveTemplateCompilerOptions(descriptor, options, ssr),
source: code
});
return result;
}同樣這里也用到了options.compiler,調(diào)用options.compiler.compileTemplate()其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的compileTemplate函數(shù),這也是一個(gè)vue暴露出來的底層的API。不過這里和前面不同的是compileTemplate接收的不是descriptor對(duì)象,而是一個(gè)SFCTemplateCompileOptions類型的對(duì)象,所以這里需要調(diào)用resolveTemplateCompilerOptions函數(shù)將參數(shù)轉(zhuǎn)換成SFCTemplateCompileOptions類型的對(duì)象。通過查看compileTemplate函數(shù)的輸入和輸出基本就可以搞清楚compileTemplate函數(shù)的作用。下面這個(gè)是compileTemplate函數(shù)的類型定義:
export function compileTemplate(
options: SFCTemplateCompileOptions,
): SFCTemplateCompileResults {}入?yún)?code>options主要就是需要編譯的template中的源代碼和對(duì)應(yīng)的AST抽象語法樹。我們來看看返回值SFCTemplateCompileResults,這里面的code就是編譯后的render函數(shù)字符串。
export interface SFCTemplateCompileResults {
code: string
ast?: RootNode
preamble?: string
source: string
tips: string[]
errors: (string | CompilerError)[]
map?: RawSourceMap
}
genTemplateCode函數(shù)的執(zhí)行流程圖如下:

genStyleCode函數(shù)
我們再來看最后一個(gè)genStyleCode函數(shù),同樣將斷點(diǎn)走到調(diào)用genStyleCode的地方。一樣的接收descriptor對(duì)象。代碼如下:
const stylesCode = await genStyleCode( descriptor, pluginContext, customElement, attachedProps );
我們將斷點(diǎn)走進(jìn)genStyleCode函數(shù)內(nèi)部,發(fā)現(xiàn)和前面genScriptCode和genTemplateCode函數(shù)有點(diǎn)不一樣,下面這個(gè)是我簡化后的genStyleCode函數(shù)代碼:
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;
}我們前面講過因?yàn)?code>vue文件中可能會(huì)有多個(gè)style標(biāo)簽,所以descriptor對(duì)象的styles屬性是一個(gè)數(shù)組。遍歷descriptor.styles數(shù)組,我們發(fā)現(xiàn)for循環(huán)內(nèi)全部都是一堆賦值操作,沒有調(diào)用vue/compiler-sfc包暴露出來的任何API。將斷點(diǎn)走到 return stylesCode;,看看stylesCode到底是什么東西?

通過打印我們發(fā)現(xiàn)stylesCode竟然變成了一條import語句,并且import的還是當(dāng)前App.vue文件,只是多了幾個(gè)query分別是:vue、type、index、scoped、lang。再來回憶一下前面講的@vitejs/plugin-vue的transform鉤子函數(shù),當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。所以當(dāng)代碼運(yùn)行到這行import語句的時(shí)候會(huì)再次走到transform鉤子函數(shù)中。我們再來看看transform鉤子函數(shù)的代碼:
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
);
}
}
}當(dāng)query中有vue字段,并且query中type字段值為style時(shí)就會(huì)執(zhí)行transformStyle函數(shù),我們給transformStyle函數(shù)打個(gè)斷點(diǎn)。當(dāng)執(zhí)行上面那條import語句時(shí)就會(huì)走到斷點(diǎn)中,我們進(jìn)到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函數(shù)的實(shí)現(xiàn)我們看著就很熟悉了,和前面處理template和script一樣都是調(diào)用的vue/compiler-sfc包暴露出來的compileStyleAsync函數(shù),這也是一個(gè)vue暴露出來的底層的API。通過查看compileStyleAsync函數(shù)的輸入和輸出基本就可以搞清楚compileStyleAsync函數(shù)的作用。
export function compileStyleAsync(
options: SFCAsyncStyleCompileOptions,
): Promise<SFCStyleCompileResults> {}我們先來看看SFCAsyncStyleCompileOptions入?yún)ⅲ?/p>
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
}入?yún)⒅饕P(guān)注幾個(gè)字段,source字段為style標(biāo)簽中的css原始代碼。scoped字段為style標(biāo)簽中是否有scoped attribute。id字段為我們在觀察 DOM 結(jié)構(gòu)時(shí)看到的 data-v-xxxxx。這個(gè)是debug時(shí)入?yún)⒔貓D:

再來看看返回值SFCStyleCompileResults對(duì)象,主要就是code屬性,這個(gè)是經(jīng)過編譯后的css字符串,已經(jīng)加上了data-v-xxxxx。
interface SFCStyleCompileResults {
code: string
map: RawSourceMap | undefined
rawResult: Result | LazyResult | undefined
errors: Error[]
modules?: Record<string, string>
dependencies: Set<string>
}這個(gè)是debug時(shí)compileStyleAsync函數(shù)返回值的截圖:

genStyleCode函數(shù)的執(zhí)行流程圖如下:

transformMain函數(shù)簡化后的代碼
現(xiàn)在我們可以來看transformMain函數(shù)簡化后的代碼:
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函數(shù)中的代碼執(zhí)行主流程,其實(shí)就是對(duì)應(yīng)了一個(gè)vue文件編譯成js文件的流程。
首先調(diào)用createDescriptor函數(shù)將一個(gè)vue文件解析為一個(gè)descriptor對(duì)象。
然后以descriptor對(duì)象為參數(shù)調(diào)用genScriptCode函數(shù),將vue文件中的 <script> 模塊代碼編譯成瀏覽器可執(zhí)行的js代碼code字符串,賦值給scriptCode變量。
接著以descriptor對(duì)象為參數(shù)調(diào)用genTemplateCode函數(shù),將vue文件中的 <template>模塊代碼編譯成render函數(shù)code字符串,賦值給templateCode變量。
然后以descriptor對(duì)象為參數(shù)調(diào)用genStyleCode函數(shù),將vue文件中的 <style> 模塊代碼編譯成了import語句code字符串,比如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";,賦值給stylesCode變量。
然后將scriptCode、templateCode、stylesCode使用換行符\n拼接起來得到resolvedCode,這個(gè)resolvedCode就是一個(gè)vue文件編譯成js文件的代碼code字符串。這個(gè)是debug時(shí)resolvedCode變量值的截圖:

總結(jié)
這篇文章通過debug的方式一步一步的帶你了解vue文件編譯成js文件的完整流程,下面是一個(gè)完整的流程圖。如果文字太小看不清,可以將圖片保存下來或者放大看:

@vitejs/plugin-vue庫中有個(gè)叫transform的鉤子函數(shù),每當(dāng)vite加載模塊的時(shí)候就會(huì)觸發(fā)這個(gè)鉤子函數(shù)。所以當(dāng)import一個(gè)vue文件的時(shí)候,就會(huì)走到@vitejs/plugin-vue中的transform鉤子函數(shù)中,在transform鉤子函數(shù)中主要調(diào)用了transformMain函數(shù)。
第一次解析這個(gè)vue文件時(shí),在transform鉤子函數(shù)中主要調(diào)用了transformMain函數(shù)。在transformMain函數(shù)中主要調(diào)用了4個(gè)函數(shù),分別是:createDescriptor、genScriptCode、genTemplateCode、genStyleCode。
createDescriptor接收的參數(shù)為當(dāng)前vue文件代碼code字符串,返回值為一個(gè)descriptor對(duì)象。對(duì)象中主要有四個(gè)屬性template、scriptSetup、script、styles。
descriptor.template.ast就是由vue文件中的template模塊生成的AST抽象語法樹。descriptor.template.content就是vue文件中的template模塊的代碼字符串。scriptSetup和script的區(qū)別是分別對(duì)應(yīng)的是vue文件中有setup屬性的<script>模塊和無setup屬性的<script>模塊。descriptor.scriptSetup.content就是vue文件中的<script setup>模塊的代碼字符串。genScriptCode函數(shù)為底層調(diào)用vue/compiler-sfc的compileScript函數(shù),根據(jù)第一步的descriptor對(duì)象將vue文件的<script setup>模塊轉(zhuǎn)換為瀏覽器可直接執(zhí)行的js代碼。
genTemplateCode函數(shù)為底層調(diào)用vue/compiler-sfc的compileTemplate函數(shù),根據(jù)第一步的descriptor對(duì)象將vue文件的<template>模塊轉(zhuǎn)換為render函數(shù)。
genStyleCode函數(shù)為將vue文件的style模塊轉(zhuǎn)換為import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";樣子的import語句。
然后使用換行符\n將genScriptCode函數(shù)、genTemplateCode函數(shù)、genStyleCode函數(shù)的返回值拼接起來賦值給變量resolvedCode,這個(gè)resolvedCode就是vue文件編譯成js文件的code字符串。
當(dāng)瀏覽器執(zhí)行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";語句時(shí),觸發(fā)了加載模塊操作,再次觸發(fā)了@vitejs/plugin-vue中的transform鉤子函數(shù)。此時(shí)由于有了type=style的query,所以在transform函數(shù)中會(huì)執(zhí)行transformStyle函數(shù),在transformStyle函數(shù)中同樣也是調(diào)用vue/compiler-sfc的compileStyleAsync函數(shù),根據(jù)第一步的descriptor對(duì)象將vue文件的 <style> 模塊轉(zhuǎn)換為編譯后的css代碼code字符串, 至此編譯style部分也講完了。
到此這篇關(guān)于通過debug搞清楚.vue文件如何變成.js文件的文章就介紹到這了,更多相關(guān).vue文件變成.js文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue 驗(yàn)證碼界面實(shí)現(xiàn)點(diǎn)擊后標(biāo)灰并設(shè)置div按鈕不可點(diǎn)擊狀態(tài)
今天小編就為大家分享一篇vue 驗(yàn)證碼界面實(shí)現(xiàn)點(diǎn)擊后標(biāo)灰并設(shè)置div按鈕不可點(diǎn)擊狀態(tài),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-10-10
Vue 實(shí)現(xiàn)監(jiān)聽窗口關(guān)閉事件,并在窗口關(guān)閉前發(fā)送請求
這篇文章主要介紹了Vue 實(shí)現(xiàn)監(jiān)聽窗口關(guān)閉事件,并在窗口關(guān)閉前發(fā)送請求,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
element 穿梭框性能優(yōu)化的實(shí)現(xiàn)
本文主要介紹了element 穿梭框性能優(yōu)化,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
前端vue如何監(jiān)聽對(duì)象或者數(shù)組某個(gè)屬性的變化詳解
這篇文章主要給大家介紹了關(guān)于前端vue如何監(jiān)聽對(duì)象或者數(shù)組某個(gè)屬性的變化的相關(guān)資料,在Vue中你可以使用watch或者computed來監(jiān)聽對(duì)象或數(shù)組某個(gè)屬性的變化,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03

