Vue Vine實現(xiàn)一個文件中寫多個組件的方法(最近很火)
前言
在今年的Vue Conf 2024大會上,沈青川大佬(維護Vue/Vite 中文文檔)在會上介紹了他的新項目Vue Vine。Vue Vine提供了全新Vue組件書寫方式,主要的賣點是可以在一個文件里面寫多個vue組件。相信你最近應該看到了不少介紹Vue Vine的文章,這篇文章我們另辟蹊徑來講講Vue Vine是如何實現(xiàn)在一個文件里面寫多個vue組件。
看個demo
我們先來看普通的vue組件,about.vue
代碼如下:
<template> <h3>i am about page</h3> </template> <script lang="ts" setup></script>
我們在瀏覽器中來看看編譯后的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文件會export default
導出一個_sfc_main
組件對象,并且這個組件對象上面有個大名鼎鼎的render
函數(shù)。父組件只需要import導入子組件里面export default
導出的_sfc_main
組件對象就可以啦。
搞清楚普通的vue組件編譯后是什么樣的,我們接著來看一個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,你會發(fā)現(xiàn)Vine 組件函數(shù)和react比較相似,不同的是return的時候需要在其返回值上顯式使用 vine
標記的模板字符串。
在瀏覽器中來看看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
編譯后是一個立即調(diào)用函數(shù),在函數(shù)中return了__vine
組件對象,并且這個組件對象上面也有render函數(shù)。想必細心的你已經(jīng)發(fā)現(xiàn)了在同一個文件里面定義的多個組件經(jīng)過編譯后,從常規(guī)的export default導出一個默認的vue組件對象變成了export導出多個具名的vue組件對象。
接下來我們將通過debug的方式帶你搞清楚Vue Vine是如何實現(xiàn)一個文件內(nèi)導出多個vue組件對象。
createVinePlugin
函數(shù)
我們遇見的第一個問題是需要找到從哪里開始著手debug?
來看一下官方文檔是接入vue vine的,如下圖:
從上圖中可以看到vine是一個vite插件,以插件的形式起作用的。
現(xiàn)在我們找到了一切起源就是這個VineVitePlugin
函數(shù),所以我們需要給vite.config.ts
文件中的VineVitePlugin
函數(shù)打個斷點。如下圖:
接下來我們需要啟動一個debug終端。這里以vscode
舉例,打開終端然后點擊終端中的+
號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal
就可以啟動一個debug
終端。
在debug終端執(zhí)行yarn dev
,在瀏覽器中打開對應的頁面,比如:http://localhost:3333/ 。此時代碼將會停留在我們打的斷點VineVitePlugin
函數(shù)調(diào)用處,讓代碼走進VineVitePlugin
函數(shù),發(fā)現(xiàn)這個函數(shù)實際定義的名字叫createVinePlugin
,在我們這個場景中簡化后的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
會在對應的時候調(diào)用這些插件的鉤子函數(shù),比如當vite
解析每個模塊時就會調(diào)用transform
等函數(shù)。
transform
鉤子函數(shù)的接收的第一個參數(shù)為code
,是當前文件的code代碼字符串。第二個參數(shù)為id,是當前文件路徑,這個路徑可能帶有query。
在transform
鉤子函數(shù)中先調(diào)用parseQuery
函數(shù)根據(jù)當前文件路徑拿到去除query的文件路徑,以及query對象。
!fileId.endsWith(".vine.ts")
的意思是判斷當前文件是不是.vine.ts
結(jié)尾的文件,如果不是則不進行任何處理,這也就是為什么文檔中會寫Vue Vine只支持.vine.ts
結(jié)尾的文件。
query.type === QUERY_TYPE_STYLE
的意思是判斷當前文件是不是css文件,因為同一個vue文件會被處理兩次,第一次處理時只會處理template和script這兩個模塊,第二次再去單獨處理style模塊。
在transform
鉤子函數(shù)的最后就是調(diào)用runCompileScript(code, id)
函數(shù),并且將其執(zhí)行結(jié)果進行返回。
runCompileScript
函數(shù)
接著將斷點走進runCompileScript
函數(shù),在我們這個場景中簡化后的runCompileScript
函數(shù)代碼如下:
const runCompileScript = (code, fileId) => { const vineFileCtx = compileVineTypeScriptFile( code, fileId, compilerHooks, fileCtxMap, ); return { code: vineFileCtx.fileMagicCode.toString(), }; };
從上面的代碼可以看到首先會以code
(當前文件的code代碼字符串)為參數(shù)去執(zhí)行compileVineTypeScriptFile
函數(shù),這個函數(shù)會返回一個vineFileCtx
上下文對象。這個上下文對象的fileMagicCode.toString(),
就是前面我們在瀏覽器中看到的最終編譯好的js代碼。
compileVineTypeScriptFile
函數(shù)
接著將斷點走進compileVineTypeScriptFile
函數(shù),在我們這個場景中簡化后的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ù)之前,我們在debug終端來看看接收的第一個參數(shù)code
,如下圖:
從上圖中可以看到第一個參數(shù)code
就是我們寫的home.vine.ts
文件中的源代碼。
createVineFileCtx
函數(shù)
接下來看第一個函數(shù)調(diào)用createVineFileCtx
,這個函數(shù)返回一個vineFileCtx
上下文對象。將斷點走進createVineFileCtx
函數(shù),在我們這個場景中簡化后的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ù)中當然全部都是js代碼。既然是js代碼那么就可以使用babel的parser
函數(shù)將組件函數(shù)的js代碼編譯成AST抽象語法樹,所以第一步就是使用code
去調(diào)用babel的parser
函數(shù)生成AST抽象語法樹,然后賦值給root
變量。
我們在debug終端來看看得到的AST抽象語法樹是什么樣的,如下圖:
從上圖中可以看到在body數(shù)組中有兩項,分別對應的就是ChildComp
組件函數(shù)和Home
組件函數(shù)。
接下來就是return返回一個vineFileCtx
上下文對象,對象上面的幾個屬性我們需要講一下。
root
:由.vine.ts
文件轉(zhuǎn)換后的AST抽象語法樹。vineCompFns
:數(shù)組中存了文件中定義的多個vue組件,初始化時為空數(shù)組。fileMagicCode
:是一個由magic-string
庫new的一個對象,對象中存了在編譯時生成的js代碼字符串。
magic-string
是由svelte的作者寫的一個庫,用于處理字符串的JavaScript
庫。它可以讓你在字符串中進行插入、刪除、替換等操作,在編譯時就是利用這個庫生成編譯后的js代碼。
toString
方法返回經(jīng)過處理后的字符串,前面的runCompileScript
函數(shù)中就是最終調(diào)用vineFileCtx.fileMagicCode.toString()
方法返回經(jīng)過編譯階段處理得到的js代碼。
findVineCompFnDecls
函數(shù)
我們接著來看compileVineTypeScriptFile
函數(shù)中的第二個函數(shù)調(diào)用findVineCompFnDecls
:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx, ) { // ...省略 const vineCompFnDecls = findVineCompFnDecls(vineFileCtx.root); // ...省略 }
通過前一步我們拿到了一個vineFileCtx
上下文對象,vineFileCtx.root
中存的是編譯后的AST抽象語法樹。
所以這一步就是調(diào)用findVineCompFnDecls
函數(shù)從AST抽象語法樹中提取出在.vine.ts
文件中定義的多個vue組件對象對應的Node節(jié)點。我們在debug終端來看看組件對象對應的Node節(jié)點組成的數(shù)組vineCompFnDecls
,如下圖:
從上圖中可以看到數(shù)組由兩個Node節(jié)點組成,分別對應的是ChildComp
組件函數(shù)和Home
組件函數(shù)。
doAnalyzeVine
函數(shù)
我們接著來看compileVineTypeScriptFile
函數(shù)中的第三個函數(shù)調(diào)用doAnalyzeVine
:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx, ) { // ...省略 doAnalyzeVine(compilerHooks, vineFileCtx, vineCompFnDecls); // ...省略 }
經(jīng)過上一步的處理我們拿到了兩個組件對象的Node節(jié)點,并且將這兩個Node節(jié)點存到了vineCompFnDecls
數(shù)組中。
由于組件對象的Node節(jié)點是一個標準的AST抽象語法樹的Node節(jié)點,并不能清晰的描述一個vue組件對象。所以接下來就是調(diào)用doAnalyzeVine
函數(shù)遍歷組件對象的Node節(jié)點,將其轉(zhuǎn)換為能夠清晰的描述一個vue組件的對象,將這些vue組件對象組成數(shù)組塞到vineFileCtx
上下文對象的vineCompFns
屬性上。
我們在debug終端來看看經(jīng)過doAnalyzeVine
函數(shù)處理后生成的vineFileCtx.vineCompFns
屬性是什么樣的,如下圖:
從上圖中可以看到vineCompFns
屬性中存的組件對象已經(jīng)能夠清晰的描述一個vue組件,上面有一些我們熟悉的屬性props
、slots
等。
transformFile
函數(shù)
我們接著來看compileVineTypeScriptFile
函數(shù)中的第四個函數(shù)調(diào)用transformFile
:
function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx, ) { // ...省略 transformFile( vineFileCtx, compilerHooks, compilerOptions?.inlineTemplate ?? true, ); // ...省略 }
經(jīng)過上一步的處理后在vineFileCtx
上下文對象的vineCompFns
屬性數(shù)組中已經(jīng)存了一系列能夠清晰描述vue組件的對象。
在前面我們講過了vineFileCtx.vineCompFns
數(shù)組中存的對象能夠清晰的描述一個vue組件,但是對象中并沒有我們期望的render函數(shù)、setup函數(shù)等。
所以接下來就需要調(diào)用transformFile
函數(shù),遍歷上一步拿到的vineFileCtx.vineCompFns
數(shù)組,將所有的vue組件轉(zhuǎn)換成對應的立即調(diào)用函數(shù)。在每個立即調(diào)用函數(shù)中都會return一個__vine
組件對象,并且這個__vine
組件對象上都有一個render屬性。
之所以包裝成一個立即調(diào)用函數(shù),是因為每個組件都會生成一個名為__vine
組件對象,所以才需要立即調(diào)用函數(shù)將作用域進行隔離。
我們在debug終端來看看經(jīng)過transformFile
函數(shù)處理后拿到的js code代碼字符串,如下圖:
從上圖中可以看到此時的js code代碼字符串已經(jīng)和我們之前在瀏覽器中看到的編譯后的代碼一模一樣了。
總結(jié)
Vue Vine是一個vite插件,vite解析每個模塊時都會觸發(fā)插件的transform
鉤子函數(shù)。在鉤子函數(shù)中會去判斷當前文件是否以.vine.ts
結(jié)尾的,如果不是則return。
在transform
鉤子函數(shù)中會去調(diào)用runCompileScript
函數(shù),runCompileScript
函數(shù)并不是實際干活的地方,而是去調(diào)用compileVineTypeScriptFile
函數(shù)。
在compileVineTypeScriptFile
函數(shù)中先new一個vineFileCtx
上下文對象,對象中的root
屬性存了由.vine.ts
文件轉(zhuǎn)換成的AST抽象語法樹。
接著就是調(diào)用findVineCompFnDecls
函數(shù)從AST抽象語法樹中找到組件對象對應的Node節(jié)點。
由于Node節(jié)點并不能清晰的描述一個vue組件,所以需要調(diào)用doAnalyzeVine
函數(shù)將這些Node節(jié)點轉(zhuǎn)換成能夠清晰描述vue組件的對象。
最后就是遍歷這些vue組件對象將其轉(zhuǎn)換成立即調(diào)用函數(shù)。在每個立即調(diào)用函數(shù)中都會return一個__vine
組件對象,并且這個__vine
組件對象上都有一個render屬性。
到此這篇關(guān)于Vue Vine是如何實現(xiàn)一個文件中寫多個組件的文章就介紹到這了,更多相關(guān)Vue Vine 一個文件中寫多個組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue實現(xiàn)跳轉(zhuǎn)接口push 轉(zhuǎn)場動畫示例
今天小編就為大家分享一篇vue實現(xiàn)跳轉(zhuǎn)接口push 轉(zhuǎn)場動畫示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11vue render函數(shù)動態(tài)加載img的src路徑操作
這篇文章主要介紹了vue render函數(shù)動態(tài)加載img的src路徑操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10解決@vue/cli安裝成功后,運行vue -V報:不是內(nèi)部或外部命令的問題
這篇文章主要介紹了解決@vue/cli安裝成功后,運行vue -V報:不是內(nèi)部或外部命令的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10vuex 第三方包實現(xiàn)數(shù)據(jù)持久化的方法
本文主要介紹了vuex 第三方包實現(xiàn)數(shù)據(jù)持久化的方法,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09淺談vue websocket nodeJS 進行實時通信踩到的坑
這篇文章主要介紹了淺談vue websocket nodeJS 進行實時通信踩到的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09