通過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è)例子很簡(jiǎn)單,在setup
中定義了msg
變量,然后在template
中將msg
渲染出來。
下面這個(gè)是我從network
中找到的編譯后的js
文件,已經(jīng)精簡(jiǎn)過了:
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
庫(kù)中的一個(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
的底層庫(kù)vue/compiler-sfc
,記住這個(gè)后面會(huì)用。
然后點(diǎn)擊Continue(F5)放掉斷點(diǎn),在瀏覽器中打開對(duì)應(yīng)的頁(yè)面,比如:http://localhost:5173/ 。此時(shí)vite
將會(huì)編譯這個(gè)頁(yè)面要用到的所有文件,就會(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
語(yǔ)句,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)。我們?cè)賮砜纯?code>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抽象語(yǔ)法樹
和原始的template
中的代碼。
我們?cè)賮砜?code>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è)場(chǎng)景中只有scriptSetup
屬性,里面同樣包含了App.vue
中的script
模塊中的內(nèi)容。
我們?cè)賮砜纯?code>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抽象語(yǔ)法樹
,scriptSetup
屬性包含了App.vue
文件中 <script setup>
的模塊的code
字符串,styles
屬性包含了App.vue
文件中 <style>
模塊中的code
字符串。createDescriptor
函數(shù)的執(zhí)行流程圖如下:
genScriptCode函數(shù)
我們?cè)賮砜?code>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ù)的代碼簡(jiǎn)化后如下:
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í)也很簡(jiǎn)單,簡(jiǎn)化后的代碼如下:
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)。我們?cè)賮砜捶祷刂?code>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抽象語(yǔ)法樹。scriptSetupAst
為編譯帶setup
屬性的script
標(biāo)簽生成的AST抽象語(yǔ)法樹,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ù)
我們?cè)賮砜?code>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ù)的代碼簡(jiǎn)化后如下:
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抽象語(yǔ)法樹
。我們來看看返回值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ù)
我們?cè)賮砜醋詈笠粋€(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è)是我簡(jiǎn)化后的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
語(yǔ)句,并且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
語(yǔ)句的時(shí)候會(huì)再次走到transform
鉤子函數(shù)中。我們?cè)賮砜纯?code>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
語(yǔ)句時(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
字段為我們?cè)谟^察 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ù)簡(jiǎn)化后的代碼
現(xiàn)在我們可以來看transformMain
函數(shù)簡(jiǎn)化后的代碼:
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
語(yǔ)句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
庫(kù)中有個(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抽象語(yǔ)法樹
。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
語(yǔ)句。
然后使用換行符\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";
語(yǔ)句時(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue element Cascader級(jí)聯(lián)選擇器解決最后一級(jí)顯示空白問題
這篇文章主要介紹了vue element Cascader級(jí)聯(lián)選擇器解決最后一級(jí)顯示空白問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10vue3使用vue-i18n的方法詳解(ts中使用$t,?vue3不用this)
所謂的vue-i18n國(guó)際化方案就是根據(jù)它的規(guī)則自己建立一套語(yǔ)言字典,對(duì)于每一個(gè)字(message)都有一個(gè)統(tǒng)一的標(biāo)識(shí)符,下面這篇文章主要給大家介紹了關(guān)于vue3使用vue-i18n(ts中使用$t,?vue3不用this)的相關(guān)資料,需要的朋友可以參考下2022-12-12vue前端測(cè)試開發(fā)watch監(jiān)聽data的數(shù)據(jù)變化
這篇文章主要為大家介紹了vue測(cè)試開發(fā)watch監(jiān)聽data的數(shù)據(jù)變化,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05vue之如何配置默認(rèn)顯示頁(yè)面和默認(rèn)路由
這篇文章主要介紹了vue之如何配置默認(rèn)顯示頁(yè)面和默認(rèn)路由問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06vue element 中的table動(dòng)態(tài)渲染實(shí)現(xiàn)(動(dòng)態(tài)表頭)
這篇文章主要介紹了vue element 中的table動(dòng)態(tài)渲染實(shí)現(xiàn)(動(dòng)態(tài)表頭),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11在vue中實(shí)現(xiàn)表單驗(yàn)證碼與滑動(dòng)驗(yàn)證功能的代碼詳解
在Web應(yīng)用程序中,表單驗(yàn)證碼和滑動(dòng)驗(yàn)證是常見的安全機(jī)制,用于防止惡意攻擊和機(jī)器人攻擊,本文將介紹如何使用Vue和vue-verify-code庫(kù)來實(shí)現(xiàn)表單驗(yàn)證碼和滑動(dòng)驗(yàn)證功能,需要的朋友可以參考下2023-06-06Vue3中如何使用Three.js詳解(包括各種樣例、常見場(chǎng)景、問題及解決方案)
Three.js是一個(gè)常見的需求,Three.js是一個(gè)用于在瀏覽器中創(chuàng)建和顯示動(dòng)畫3D計(jì)算機(jī)圖形的JavaScript庫(kù),這篇文章主要介紹了Vue3中如何使用Three.js的相關(guān)資料,包括各種樣例、常見場(chǎng)景、問題及解決方案,需要的朋友可以參考下2025-04-04