在今年的Vue Conf 2024大會(huì)上,沈青川大佬(維護(hù)Vue/Vite 中文文檔)在會(huì)上介紹了他的新項(xiàng)目Vue Vine。Vue Vine提供了全新Vue組件書寫方式,主要的賣點(diǎn)是可以在一個(gè)文件里面寫多個(gè)vue組件。相信你最近應(yīng)該看到了不少介紹Vue Vine的文章,這篇文章我們另辟蹊徑來講講Vue Vine是如何實(shí)現(xiàn)在一個(gè)文件里面寫多個(gè)vue組件。
我們先來看普通的vue組件,about.vue代碼如下:
<template> <h3>i am about page</h3></template><script lang="ts" setup></script>
我們?cè)跒g覽器中來看看編譯后的js代碼,代碼如下:
const _sfc_main = {};function _sfc_render(_ctx, _cache) { return _openBlock(), _createElementBlock("h3", null, "i am about page");}_sfc_main.render = _sfc_render;export default _sfc_main;
從上面的代碼可以看到普通的vue組件編譯后生成的js文件會(huì)export default導(dǎo)出一個(gè)_sfc_main組件對(duì)象,并且這個(gè)組件對(duì)象上面有個(gè)大名鼎鼎的render函數(shù)。父組件只需要import導(dǎo)入子組件里面export default導(dǎo)出的_sfc_main組件對(duì)象就可以啦。
搞清楚普通的vue組件編譯后是什么樣的,我們接著來看一個(gè)Vue Vine的demo,Vue Vine的組件必須以.vine.ts 結(jié)尾,home.vine.ts代碼如下:
async function ChildComp() { return vine` <h3>我是子組件</h3> `;}export async function Home() { return vine` <h3>我是父組件</h3> <ChildComp /> `;}
如果你熟悉react,你會(huì)發(fā)現(xiàn)Vine 組件函數(shù)和react比較相似,不同的是return的時(shí)候需要在其返回值上顯式使用 vine 標(biāo)記的模板字符串。
在瀏覽器中來看看home.vine.ts編譯后的代碼,代碼如下:
export const ChildComp = (() => { const __vine = _defineComponent({ name: "ChildComp", setup(__props, { expose: __expose }) { // ...省略 }, }); function __sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return _openBlock(), _createElementBlock("h3", null, "我是子組件"); } __vine.render = __sfc_render; return __vine;})();export const Home = (() => { const __vine = _defineComponent({ name: "Home", setup(__props, { expose: __expose }) { // ...省略 }, }); function __sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return ( _openBlock(), _createElementBlock( _Fragment, null, [_hoisted_1, _createVNode($setup["ChildComp"])], 64, ) ); } __vine.render = __sfc_render; return __vine;})();
從上面的代碼可以看到組件ChildComp和Home編譯后是一個(gè)立即調(diào)用函數(shù),在函數(shù)中return了__vine組件對(duì)象,并且這個(gè)組件對(duì)象上面也有render函數(shù)。想必細(xì)心的你已經(jīng)發(fā)現(xiàn)了在同一個(gè)文件里面定義的多個(gè)組件經(jīng)過編譯后,從常規(guī)的export default導(dǎo)出一個(gè)默認(rèn)的vue組件對(duì)象變成了export導(dǎo)出多個(gè)具名的vue組件對(duì)象。
接下來我們將通過debug的方式帶你搞清楚Vue Vine是如何實(shí)現(xiàn)一個(gè)文件內(nèi)導(dǎo)出多個(gè)vue組件對(duì)象。
我們遇見的第一個(gè)問題是需要找到從哪里開始著手debug?
來看一下官方文檔是接入vue vine的,如下圖:
圖片
從上圖中可以看到vine是一個(gè)vite插件,以插件的形式起作用的。
現(xiàn)在我們找到了一切起源就是這個(gè)VineVitePlugin函數(shù),所以我們需要給vite.config.ts文件中的VineVitePlugin函數(shù)打個(gè)斷點(diǎn)。如下圖:
圖片
接下來我們需要啟動(dòng)一個(gè)debug終端。這里以vscode舉例,打開終端然后點(diǎn)擊終端中的+號(hào)旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal就可以啟動(dòng)一個(gè)debug終端。
圖片
在debug終端執(zhí)行yarn dev,在瀏覽器中打開對(duì)應(yīng)的頁(yè)面,比如:http://localhost:3333/ 。此時(shí)代碼將會(huì)停留在我們打的斷點(diǎn)VineVitePlugin函數(shù)調(diào)用處,讓代碼走進(jìn)VineVitePlugin函數(shù),發(fā)現(xiàn)這個(gè)函數(shù)實(shí)際定義的名字叫createVinePlugin,在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的createVinePlugin函數(shù)代碼如下:
function createVinePlugin() { return { name: "vue-vine-plugin", async resolveId(id) { // ...省略 }, async load(id) { // ...省略 }, async transform(code, id) { const { fileId, query } = parseQuery(id); if (!fileId.endsWith(".vine.ts") || query.type === QUERY_TYPE_STYLE) { return; } return runCompileScript(code, id); }, async handleHotUpdate(ctx) { // ...省略 } };}
從上面的代碼可以看到插件中有不少鉤子函數(shù),vite會(huì)在對(duì)應(yīng)的時(shí)候調(diào)用這些插件的鉤子函數(shù),比如當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。
transform鉤子函數(shù)的接收的第一個(gè)參數(shù)為code,是當(dāng)前文件的code代碼字符串。第二個(gè)參數(shù)為id,是當(dāng)前文件路徑,這個(gè)路徑可能帶有query。
在transform鉤子函數(shù)中先調(diào)用parseQuery函數(shù)根據(jù)當(dāng)前文件路徑拿到去除query的文件路徑,以及query對(duì)象。
!fileId.endsWith(".vine.ts") 的意思是判斷當(dāng)前文件是不是.vine.ts結(jié)尾的文件,如果不是則不進(jìn)行任何處理,這也就是為什么文檔中會(huì)寫Vue Vine只支持.vine.ts結(jié)尾的文件。
query.type === QUERY_TYPE_STYLE的意思是判斷當(dāng)前文件是不是css文件,因?yàn)橥粋€(gè)vue文件會(huì)被處理兩次,第一次處理時(shí)只會(huì)處理template和script這兩個(gè)模塊,第二次再去單獨(dú)處理style模塊。
在transform鉤子函數(shù)的最后就是調(diào)用runCompileScript(code, id)函數(shù),并且將其執(zhí)行結(jié)果進(jìn)行返回。
接著將斷點(diǎn)走進(jìn)runCompileScript函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的runCompileScript函數(shù)代碼如下:
const runCompileScript = (code, fileId) => { const vineFileCtx = compileVineTypeScriptFile( code, fileId, compilerHooks, fileCtxMap, ); return { code: vineFileCtx.fileMagicCode.toString(), };};
從上面的代碼可以看到首先會(huì)以code(當(dāng)前文件的code代碼字符串)為參數(shù)去執(zhí)行compileVineTypeScriptFile函數(shù),這個(gè)函數(shù)會(huì)返回一個(gè)vineFileCtx上下文對(duì)象。這個(gè)上下文對(duì)象的fileMagicCode.toString(),就是前面我們?cè)跒g覽器中看到的最終編譯好的js代碼。
接著將斷點(diǎn)走進(jìn)compileVineTypeScriptFile函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的compileVineTypeScriptFile函數(shù)代碼如下:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { const vineFileCtx: VineFileCtx = createVineFileCtx( code, fileId, fileCtxCache, ); const vineCompFnDecls = findVineCompFnDecls(vineFileCtx.root); doAnalyzeVine(compilerHooks, vineFileCtx, vineCompFnDecls); transformFile( vineFileCtx, compilerHooks, compilerOptions?.inlineTemplate ?? true, ); return vineFileCtx;}
在執(zhí)行compileVineTypeScriptFile函數(shù)之前,我們?cè)赿ebug終端來看看接收的第一個(gè)參數(shù)code,如下圖:
圖片
從上圖中可以看到第一個(gè)參數(shù)code就是我們寫的home.vine.ts文件中的源代碼。
接下來看第一個(gè)函數(shù)調(diào)用createVineFileCtx,這個(gè)函數(shù)返回一個(gè)vineFileCtx上下文對(duì)象。將斷點(diǎn)走進(jìn)createVineFileCtx函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的createVineFileCtx函數(shù)代碼如下:
import MagicString from 'magic-string'function createVineFileCtx(code: string, fileId: string) { const root = babelParse(code); const vineFileCtx: VineFileCtx = { root, fileMagicCode: new MagicString(code), vineCompFns: [], // ...省略 }; return vineFileCtx;}
由于Vue Vine中的組件和react相似是組件函數(shù),組件函數(shù)中當(dāng)然全部都是js代碼。既然是js代碼那么就可以使用babel的parser函數(shù)將組件函數(shù)的js代碼編譯成AST抽象語(yǔ)法樹,所以第一步就是使用code去調(diào)用babel的parser函數(shù)生成AST抽象語(yǔ)法樹,然后賦值給root變量。
我們?cè)赿ebug終端來看看得到的AST抽象語(yǔ)法樹是什么樣的,如下圖:
圖片
從上圖中可以看到在body數(shù)組中有兩項(xiàng),分別對(duì)應(yīng)的就是ChildComp組件函數(shù)和Home組件函數(shù)。
接下來就是return返回一個(gè)vineFileCtx上下文對(duì)象,對(duì)象上面的幾個(gè)屬性我們需要講一下。
我們接著來看compileVineTypeScriptFile函數(shù)中的第二個(gè)函數(shù)調(diào)用findVineCompFnDecls:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { // ...省略 const vineCompFnDecls = findVineCompFnDecls(vineFileCtx.root); // ...省略}
通過前一步我們拿到了一個(gè)vineFileCtx上下文對(duì)象,vineFileCtx.root中存的是編譯后的AST抽象語(yǔ)法樹。
所以這一步就是調(diào)用findVineCompFnDecls函數(shù)從AST抽象語(yǔ)法樹中提取出在.vine.ts文件中定義的多個(gè)vue組件對(duì)象對(duì)應(yīng)的Node節(jié)點(diǎn)。我們?cè)赿ebug終端來看看組件對(duì)象對(duì)應(yīng)的Node節(jié)點(diǎn)組成的數(shù)組vineCompFnDecls,如下圖:
圖片
從上圖中可以看到數(shù)組由兩個(gè)Node節(jié)點(diǎn)組成,分別對(duì)應(yīng)的是ChildComp組件函數(shù)和Home組件函數(shù)。
我們接著來看compileVineTypeScriptFile函數(shù)中的第三個(gè)函數(shù)調(diào)用doAnalyzeVine:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { // ...省略 doAnalyzeVine(compilerHooks, vineFileCtx, vineCompFnDecls); // ...省略}
經(jīng)過上一步的處理我們拿到了兩個(gè)組件對(duì)象的Node節(jié)點(diǎn),并且將這兩個(gè)Node節(jié)點(diǎn)存到了vineCompFnDecls數(shù)組中。
由于組件對(duì)象的Node節(jié)點(diǎn)是一個(gè)標(biāo)準(zhǔn)的AST抽象語(yǔ)法樹的Node節(jié)點(diǎn),并不能清晰的描述一個(gè)vue組件對(duì)象。所以接下來就是調(diào)用doAnalyzeVine函數(shù)遍歷組件對(duì)象的Node節(jié)點(diǎn),將其轉(zhuǎn)換為能夠清晰的描述一個(gè)vue組件的對(duì)象,將這些vue組件對(duì)象組成數(shù)組塞到vineFileCtx上下文對(duì)象的vineCompFns屬性上。
我們?cè)赿ebug終端來看看經(jīng)過doAnalyzeVine函數(shù)處理后生成的vineFileCtx.vineCompFns屬性是什么樣的,如下圖:
圖片
從上圖中可以看到vineCompFns屬性中存的組件對(duì)象已經(jīng)能夠清晰的描述一個(gè)vue組件,上面有一些我們熟悉的屬性props、slots等。
我們接著來看compileVineTypeScriptFile函數(shù)中的第四個(gè)函數(shù)調(diào)用transformFile:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { // ...省略 transformFile( vineFileCtx, compilerHooks, compilerOptions?.inlineTemplate ?? true, ); // ...省略}
經(jīng)過上一步的處理后在vineFileCtx上下文對(duì)象的vineCompFns屬性數(shù)組中已經(jīng)存了一系列能夠清晰描述vue組件的對(duì)象。
在前面我們講過了vineFileCtx.vineCompFns數(shù)組中存的對(duì)象能夠清晰的描述一個(gè)vue組件,但是對(duì)象中并沒有我們期望的render函數(shù)、setup函數(shù)等。
所以接下來就需要調(diào)用transformFile函數(shù),遍歷上一步拿到的vineFileCtx.vineCompFns數(shù)組,將所有的vue組件轉(zhuǎn)換成對(duì)應(yīng)的立即調(diào)用函數(shù)。在每個(gè)立即調(diào)用函數(shù)中都會(huì)return一個(gè)__vine組件對(duì)象,并且這個(gè)__vine組件對(duì)象上都有一個(gè)render屬性。
之所以包裝成一個(gè)立即調(diào)用函數(shù),是因?yàn)槊總€(gè)組件都會(huì)生成一個(gè)名為__vine組件對(duì)象,所以才需要立即調(diào)用函數(shù)將作用域進(jìn)行隔離。
我們?cè)赿ebug終端來看看經(jīng)過transformFile函數(shù)處理后拿到的js code代碼字符串,如下圖:
圖片
從上圖中可以看到此時(shí)的js code代碼字符串已經(jīng)和我們之前在瀏覽器中看到的編譯后的代碼一模一樣了。
Vue Vine是一個(gè)vite插件,vite解析每個(gè)模塊時(shí)都會(huì)觸發(fā)插件的transform鉤子函數(shù)。在鉤子函數(shù)中會(huì)去判斷當(dāng)前文件是否以.vine.ts結(jié)尾的,如果不是則return。
在transform鉤子函數(shù)中會(huì)去調(diào)用runCompileScript函數(shù),runCompileScript函數(shù)并不是實(shí)際干活的地方,而是去調(diào)用compileVineTypeScriptFile函數(shù)。
在compileVineTypeScriptFile函數(shù)中先new一個(gè)vineFileCtx上下文對(duì)象,對(duì)象中的root屬性存了由.vine.ts文件轉(zhuǎn)換成的AST抽象語(yǔ)法樹。
接著就是調(diào)用findVineCompFnDecls函數(shù)從AST抽象語(yǔ)法樹中找到組件對(duì)象對(duì)應(yīng)的Node節(jié)點(diǎn)。
由于Node節(jié)點(diǎn)并不能清晰的描述一個(gè)vue組件,所以需要調(diào)用doAnalyzeVine函數(shù)將這些Node節(jié)點(diǎn)轉(zhuǎn)換成能夠清晰描述vue組件的對(duì)象。
最后就是遍歷這些vue組件對(duì)象將其轉(zhuǎn)換成立即調(diào)用函數(shù)。在每個(gè)立即調(diào)用函數(shù)中都會(huì)return一個(gè)__vine組件對(duì)象,并且這個(gè)__vine組件對(duì)象上都有一個(gè)render屬性。
本文鏈接:http://www.www897cc.com/showinfo-26-100190-0.html最近很火的Vue Vine是如何實(shí)現(xiàn)一個(gè)文件中寫多個(gè)組件
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com