通過debug搞清楚.vue文件如何變成.js文件(案例詳解)
我們每天寫的vue
代碼都是寫在vue
文件中,但是瀏覽器卻只認識html
、css
、js
等文件類型。所以這個時候就需要一個工具將vue
文件轉(zhuǎn)換為瀏覽器能夠認識的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
文件,已經(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
代碼中我們可以看到主要有三部分,想必你也猜到了這三部分剛好對應(yīng)vue
文件的那三塊。
_sfc_main
對象的setup
方法對應(yīng)vue
文件中的<script setup lang="ts">
模塊。_sfc_render
函數(shù)對應(yīng)vue
文件中的<template>
模塊。import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
對應(yīng)vue
文件中的<style scoped>
模塊。
debug搞清楚如何將vue文件編譯為js文件
大家應(yīng)該都知道,前端代碼運行環(huán)境主要有兩個,node
端和瀏覽器端,分別對應(yīng)我們熟悉的編譯時和運行時。瀏覽器明顯是不認識vue
文件的,所以vue
文件編譯成js
這一過程肯定不是在運行時的瀏覽器端。很明顯這一過程是在編譯時的node
端。
要在node
端打斷點,我們需要啟動一個debug 終端。這里以vscode
舉例,首先我們需要打開終端,然后點擊終端中的+
號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal
就可以啟動一個debug
終端。
假如vue
文件編譯為js
文件是一個毛線團,那么他的線頭一定是vite.config.ts
文件中使用@vitejs/plugin-vue
的地方。通過這個線頭開始debug
我們就能夠梳理清楚完整的工作流程。
vuePlugin函數(shù)
我們給上方圖片的vue
函數(shù)打了一個斷點,然后在debug
終端上面執(zhí)行yarn dev
,我們看到斷點已經(jīng)停留在了vue
函數(shù)這里。然后點擊step into
,斷點走到了@vitejs/plugin-vue
庫中的一個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
是作為一個plugins
插件在vite
中使用,vuePlugin
函數(shù)返回的對象中的buildStart
、transform
方法就是對應(yīng)的插件鉤子函數(shù)。vite
會在對應(yīng)的時候調(diào)用這些插件的鉤子函數(shù),比如當(dāng)vite
服務(wù)器啟動時就會調(diào)用插件里面的buildStart
等函數(shù),當(dāng)vite
解析每個模塊時就會調(diào)用transform
等函數(shù)。更多vite
鉤子相關(guān)內(nèi)容查看官網(wǎng)。
我們這里主要看buildStart
和transform
兩個鉤子函數(shù),分別是服務(wù)器啟動時調(diào)用和解析每個模塊時調(diào)用。給這兩個鉤子函數(shù)打上斷點。
然后點擊Continue(F5),vite
服務(wù)啟動后就會走到buildStart
鉤子函數(shù)中打的斷點。我們可以看到buildStart
鉤子函數(shù)的代碼是這樣的:
buildStart() { const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root); }
將鼠標(biāo)放到options.value.compiler
上面我們看到此時options.value.compiler
的值為null
,所以代碼會走到resolveCompiler
函數(shù)中,點擊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)前項目是否是vue3.x
版本,然后將vue/compiler-sfc
包返回。所以經(jīng)過初始化后options.value.compiler
的值就是vue
的底層庫vue/compiler-sfc
,記住這個后面會用。
然后點擊Continue(F5)放掉斷點,在瀏覽器中打開對應(yīng)的頁面,比如:http://localhost:5173/ 。此時vite
將會編譯這個頁面要用到的所有文件,就會走到transform
鉤子函數(shù)斷點中了。由于解析每個文件都會走到transform
鉤子函數(shù)中,但是我們只關(guān)注App.vue
文件是如何解析的,所以為了方便我們直接在transform
函數(shù)中添加了下面這段代碼,并且刪掉了原來在transform
鉤子函數(shù)中打的斷點,這樣就只有解析到App.vue
文件的時候才會走到斷點中去。
經(jīng)過debug我們發(fā)現(xiàn)解析App.vue
文件時transform
函數(shù)實際就是執(zhí)行了transformMain
函數(shù),至于transformStyle
函數(shù)后面講解析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
函數(shù)
繼續(xù)debug斷點走進transformMain
函數(shù),發(fā)現(xiàn)transformMain
函數(shù)中代碼邏輯很清晰。按照順序分別是:
- 根據(jù)源代碼code字符串調(diào)用
createDescriptor
函數(shù)生成一個descriptor
對象。 - 調(diào)用
genScriptCode
函數(shù)傳入第一步生成的descriptor
對象將<script setup>
模塊編譯為瀏覽器可執(zhí)行的js
代碼。 - 調(diào)用
genTemplateCode
函數(shù)傳入第一步生成的descriptor
對象將<template>
模塊編譯為render
函數(shù)。 - 調(diào)用
genStyleCode
函數(shù)傳入第一步生成的descriptor
對象將<style scoped>
模塊編譯為類似這樣的import
語句,import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
。
createDescriptor
我們先來看看createDescriptor
函數(shù),將斷點走到createDescriptor(filename, code, options)
這一行代碼,可以看到傳入的filename
就是App.vue
的文件路徑,code
就是App.vue
中我們寫的源代碼。
debug
走進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 }; }
這個compiler
是不是覺得有點熟悉?compiler
是調(diào)用createDescriptor
函數(shù)時傳入的第三個參數(shù)解構(gòu)而來,而第三個參數(shù)就是options
。還記得我們之前在vite
啟動時調(diào)用了buildStart
鉤子函數(shù),然后將vue
底層包vue/compiler-sfc
賦值給options
的compiler
屬性。那這里的compiler.parse
其實就是調(diào)用的vue/compiler-sfc
包暴露出來的parse
函數(shù),這是一個vue
暴露出來的底層的API
,這篇文章我們不會對底層API進行源碼解析,通過查看parse
函數(shù)的輸入和輸出基本就可以搞清楚parse
函數(shù)的作用。下面這個是parse
函數(shù)的類型定義:
export function parse( source: string, options: SFCParseOptions = {}, ): SFCParseResult {}
從上面我們可以看到parse
函數(shù)接收兩個參數(shù),第一個參數(shù)為vue
文件的源代碼,在我們這里就是App.vue
中的code
字符串,第二個參數(shù)是一些options
選項。我們再來看看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 }
仔細看看SFCDescriptor
類型,其中的template
屬性就是App.vue
文件對應(yīng)的template
標(biāo)簽中的內(nèi)容,里面包含了由App.vue
文件中的template
模塊編譯成的AST抽象語法樹
和原始的template
中的代碼。
我們再來看script
和scriptSetup
屬性,由于vue
文件中可以寫多個script
標(biāo)簽,scriptSetup
對應(yīng)的就是有setup
的script
標(biāo)簽,script
對應(yīng)的就是沒有setup
對應(yīng)的script
標(biāo)簽。我們這個場景中只有scriptSetup
屬性,里面同樣包含了App.vue
中的script
模塊中的內(nèi)容。
我們再來看看styles
屬性,這里的styles
屬性是一個數(shù)組,是因為我們可以在vue
文件中寫多個style
模塊,里面同樣包含了App.vue
中的style
模塊中的內(nèi)容。
所以這一步執(zhí)行createDescriptor
函數(shù)生成的descriptor
對象中主要有三個屬性,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ào)用genScriptCode
函數(shù)的地方,genScriptCode
函數(shù)主要接收我們上一步生成的descriptor
對象,調(diào)用genScriptCode
函數(shù)后會將編譯后的script
模塊代碼賦值給scriptCode
變量。
const { code: scriptCode, map: scriptMap } = await genScriptCode( descriptor, options, pluginContext, ssr, customElement );
將斷點走到genScriptCode
函數(shù)內(nèi)部,在genScriptCode
函數(shù)中主要就是這行代碼: const script = resolveScript(descriptor, options, ssr, customElement);
。將第一步生成的descriptor
對象作為參數(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ù)將斷點走到resolveScript
函數(shù)內(nèi)部,發(fā)現(xiàn)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
我們前面第一步的時候已經(jīng)解釋過了,options.compiler
對象實際就是vue
底層包vue/compiler-sfc
暴露的對象,這里的options.compiler.compileScript()
其實就是調(diào)用的vue/compiler-sfc
包暴露出來的compileScript
函數(shù),同樣也是一個vue
暴露出來的底層的API
。通過查看compileScript
函數(shù)的輸入和輸出基本就可以搞清楚compileScript
函數(shù)的作用。下面這個是compileScript
函數(shù)的類型定義:
export function compileScript( sfc: SFCDescriptor, options: SFCScriptCompileOptions, ): SFCScriptBlock{}
這個函數(shù)的入?yún)⑹且粋€SFCDescriptor
對象,就是我們第一步調(diào)用生成createDescriptor
函數(shù)生成的descriptor
對象,第二個參數(shù)是一些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
標(biāo)簽生成的AST抽象語法樹。scriptSetupAst
為編譯帶setup
屬性的script
標(biāo)簽生成的AST抽象語法樹,content
為vue
文件中的script
模塊編譯后生成的瀏覽器可執(zhí)行的js
代碼。下面這個是執(zhí)行vue/compiler-sfc
的compileScript
函數(shù)返回結(jié)果:
繼續(xù)將斷點走回genScriptCode
函數(shù),現(xiàn)在邏輯就很清晰了。這里的script
對象就是調(diào)用vue/compiler-sfc
的compileScript
函數(shù)返回對象,scriptCode
就是script
對象的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ào)用genTemplateCode
函數(shù)的地方,genTemplateCode
函數(shù)主要接收我們上一步生成的descriptor
對象,調(diào)用genTemplateCode
函數(shù)后會將編譯后的template
模塊代碼賦值給templateCode
變量。
({ code: templateCode, map: templateMap } = await genTemplateCode( descriptor, options, pluginContext, ssr, customElement ))
同樣將斷點走到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ù)將斷點走進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" ) }; }
同理將斷點走進到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()
其實就是調(diào)用的vue/compiler-sfc
包暴露出來的compileTemplate
函數(shù),這也是一個vue
暴露出來的底層的API
。不過這里和前面不同的是compileTemplate
接收的不是descriptor
對象,而是一個SFCTemplateCompileOptions
類型的對象,所以這里需要調(diào)用resolveTemplateCompilerOptions
函數(shù)將參數(shù)轉(zhuǎn)換成SFCTemplateCompileOptions
類型的對象。通過查看compileTemplate
函數(shù)的輸入和輸出基本就可以搞清楚compileTemplate
函數(shù)的作用。下面這個是compileTemplate
函數(shù)的類型定義:
export function compileTemplate( options: SFCTemplateCompileOptions, ): SFCTemplateCompileResults {}
入?yún)?code>options主要就是需要編譯的template
中的源代碼和對應(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ù)
我們再來看最后一個genStyleCode
函數(shù),同樣將斷點走到調(diào)用genStyleCode
的地方。一樣的接收descriptor
對象。代碼如下:
const stylesCode = await genStyleCode( descriptor, pluginContext, customElement, attachedProps );
我們將斷點走進genStyleCode
函數(shù)內(nèi)部,發(fā)現(xiàn)和前面genScriptCode
和genTemplateCode
函數(shù)有點不一樣,下面這個是我簡化后的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; }
我們前面講過因為vue
文件中可能會有多個style
標(biāo)簽,所以descriptor
對象的styles
屬性是一個數(shù)組。遍歷descriptor.styles
數(shù)組,我們發(fā)現(xiàn)for
循環(huán)內(nèi)全部都是一堆賦值操作,沒有調(diào)用vue/compiler-sfc
包暴露出來的任何API
。將斷點走到 return stylesCode;
,看看stylesCode
到底是什么東西?
通過打印我們發(fā)現(xiàn)stylesCode
竟然變成了一條import
語句,并且import
的還是當(dāng)前App.vue
文件,只是多了幾個query
分別是:vue
、type
、index
、scoped
、lang
。再來回憶一下前面講的@vitejs/plugin-vue
的transform
鉤子函數(shù),當(dāng)vite
解析每個模塊時就會調(diào)用transform
等函數(shù)。所以當(dāng)代碼運行到這行import
語句的時候會再次走到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
時就會執(zhí)行transformStyle
函數(shù),我們給transformStyle
函數(shù)打個斷點。當(dāng)執(zhí)行上面那條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
函數(shù)的實現(xiàn)我們看著就很熟悉了,和前面處理template
和script
一樣都是調(diào)用的vue/compiler-sfc
包暴露出來的compileStyleAsync
函數(shù),這也是一個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)注幾個字段,source
字段為style
標(biāo)簽中的css
原始代碼。scoped
字段為style
標(biāo)簽中是否有scoped
attribute。id
字段為我們在觀察 DOM 結(jié)構(gòu)時看到的 data-v-xxxxx
。這個是debug
時入?yún)⒔貓D:
再來看看返回值SFCStyleCompileResults
對象,主要就是code
屬性,這個是經(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> }
這個是debug
時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í)行主流程,其實就是對應(yīng)了一個vue
文件編譯成js
文件的流程。
首先調(diào)用createDescriptor
函數(shù)將一個vue
文件解析為一個descriptor
對象。
然后以descriptor
對象為參數(shù)調(diào)用genScriptCode
函數(shù),將vue
文件中的 <script>
模塊代碼編譯成瀏覽器可執(zhí)行的js
代碼code
字符串,賦值給scriptCode
變量。
接著以descriptor
對象為參數(shù)調(diào)用genTemplateCode
函數(shù),將vue
文件中的 <template>
模塊代碼編譯成render
函數(shù)code
字符串,賦值給templateCode
變量。
然后以descriptor
對象為參數(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
,這個resolvedCode
就是一個vue
文件編譯成js
文件的代碼code
字符串。這個是debug
時resolvedCode
變量值的截圖:
總結(jié)
這篇文章通過debug
的方式一步一步的帶你了解vue
文件編譯成js
文件的完整流程,下面是一個完整的流程圖。如果文字太小看不清,可以將圖片保存下來或者放大看:
@vitejs/plugin-vue
庫中有個叫transform
的鉤子函數(shù),每當(dāng)vite
加載模塊的時候就會觸發(fā)這個鉤子函數(shù)。所以當(dāng)import
一個vue
文件的時候,就會走到@vitejs/plugin-vue
中的transform
鉤子函數(shù)中,在transform
鉤子函數(shù)中主要調(diào)用了transformMain
函數(shù)。
第一次解析這個vue
文件時,在transform
鉤子函數(shù)中主要調(diào)用了transformMain
函數(shù)。在transformMain
函數(shù)中主要調(diào)用了4個函數(shù),分別是:createDescriptor
、genScriptCode
、genTemplateCode
、genStyleCode
。
createDescriptor
接收的參數(shù)為當(dāng)前vue
文件代碼code
字符串,返回值為一個descriptor
對象。對象中主要有四個屬性template
、scriptSetup
、script
、styles
。
descriptor.template.ast
就是由vue
文件中的template
模塊生成的AST抽象語法樹
。descriptor.template.content
就是vue
文件中的template
模塊的代碼字符串。scriptSetup
和script
的區(qū)別是分別對應(yīng)的是vue
文件中有setup
屬性的<script>
模塊和無setup
屬性的<script>
模塊。descriptor.scriptSetup.content
就是vue
文件中的<script setup>
模塊的代碼字符串。genScriptCode
函數(shù)為底層調(diào)用vue/compiler-sfc
的compileScript
函數(shù),根據(jù)第一步的descriptor
對象將vue
文件的<script setup>
模塊轉(zhuǎn)換為瀏覽器可直接執(zhí)行的js
代碼。
genTemplateCode
函數(shù)為底層調(diào)用vue/compiler-sfc
的compileTemplate
函數(shù),根據(jù)第一步的descriptor
對象將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
,這個resolvedCode
就是vue
文件編譯成js
文件的code
字符串。
當(dāng)瀏覽器執(zhí)行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
語句時,觸發(fā)了加載模塊操作,再次觸發(fā)了@vitejs/plugin-vue
中的transform
鉤子函數(shù)。此時由于有了type=style
的query
,所以在transform
函數(shù)中會執(zhí)行transformStyle
函數(shù),在transformStyle
函數(shù)中同樣也是調(diào)用vue/compiler-sfc
的compileStyleAsync
函數(shù),根據(jù)第一步的descriptor
對象將vue
文件的 <style>
模塊轉(zhuǎn)換為編譯后的css
代碼code
字符串, 至此編譯style
部分也講完了。
到此這篇關(guān)于通過debug搞清楚.vue文件如何變成.js文件的文章就介紹到這了,更多相關(guān).vue文件變成.js文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue element Cascader級聯(lián)選擇器解決最后一級顯示空白問題
這篇文章主要介紹了vue element Cascader級聯(lián)選擇器解決最后一級顯示空白問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10vue3使用vue-i18n的方法詳解(ts中使用$t,?vue3不用this)
所謂的vue-i18n國際化方案就是根據(jù)它的規(guī)則自己建立一套語言字典,對于每一個字(message)都有一個統(tǒng)一的標(biāo)識符,下面這篇文章主要給大家介紹了關(guān)于vue3使用vue-i18n(ts中使用$t,?vue3不用this)的相關(guān)資料,需要的朋友可以參考下2022-12-12vue前端測試開發(fā)watch監(jiān)聽data的數(shù)據(jù)變化
這篇文章主要為大家介紹了vue測試開發(fā)watch監(jiān)聽data的數(shù)據(jù)變化,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05vue element 中的table動態(tài)渲染實現(xiàn)(動態(tài)表頭)
這篇文章主要介紹了vue element 中的table動態(tài)渲染實現(xiàn)(動態(tài)表頭),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11在vue中實現(xiàn)表單驗證碼與滑動驗證功能的代碼詳解
在Web應(yīng)用程序中,表單驗證碼和滑動驗證是常見的安全機制,用于防止惡意攻擊和機器人攻擊,本文將介紹如何使用Vue和vue-verify-code庫來實現(xiàn)表單驗證碼和滑動驗證功能,需要的朋友可以參考下2023-06-06Vue3中如何使用Three.js詳解(包括各種樣例、常見場景、問題及解決方案)
Three.js是一個常見的需求,Three.js是一個用于在瀏覽器中創(chuàng)建和顯示動畫3D計算機圖形的JavaScript庫,這篇文章主要介紹了Vue3中如何使用Three.js的相關(guān)資料,包括各種樣例、常見場景、問題及解決方案,需要的朋友可以參考下2025-04-04