<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函数
父组件只须要import导入子组件里面export default导出的_sfc_main组件工具就可以啦。

搞清楚普通的vue组件编译后是什么样的,我们接着来看一个Vue Vine的demo,Vue Vine的组件必须以.vine.ts 结尾,home.vine.ts代码如下:

async function ChildComp() { return vine` <h3>我是子组件</h3> `;}export async function Home() { return vine` <h3>我是父组件</h3> <ChildComp /> `;}

如果你熟习react,你会创造Vine 组件函数和react比较相似,不同的是return的时候须要在其返回值上显式利用 vine 标记的模板字符串。

比来很火的Vue 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编译后是一个立即调用函数,在函数中return了__vine组件工具,并且这个组件工具上面也有render函数。
想必细心的你已经创造了在同一个文件里面定义的多个组件经由编译后,从常规的export default导出一个默认的vue组件工具变成了export导出多个具名的vue组件工具。

接下来我们将通过debug的办法带你搞清楚Vue Vine是如何实现一个文件内导出多个vue组件工具。

createVinePlugin函数

我们遇见的第一个问题是须要找到从哪里开始动手debug?

来看一下官方文档是接入vue vine的,如下图:

从上图中可以看到vine是一个vite插件,以插件的形式起浸染的。

现在我们找到了统统起源便是这个VineVitePlugin函数,以是我们须要给vite.config.ts文件中的VineVitePlugin函数打个断点。
如下图:

接下来我们须要启动一个debug终端。
这里以vscode举例,打开终端然后点击终端中的+号阁下的下拉箭头,不才拉中点击Javascript Debug Terminal就可以启动一个debug终端。

在debug终端实行yarn dev,在浏览器中打开对应的页面,比如:http://localhost:3333/ 。
此时期码将会勾留在我们打的断点VineVitePlugin函数调用处,让代码走进VineVitePlugin函数,创造这个函数实际定义的名字叫createVinePlugin,在我们这个场景中简化后的createVinePlugin函数代码如下:

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) { // ...省略 } };}

从上面的代码可以看到插件中有不少钩子函数,vite会在对应的时候调用这些插件的钩子函数,比如当vite解析每个模块时就会调用transform等函数。

transform钩子函数的吸收的第一个参数为code,是当前文件的code代码字符串。
第二个参数为id,是当前文件路径,这个路径可能带有query。

在transform钩子函数中先调用parseQuery函数根据当前文件路径拿到去除query的文件路径,以及query工具。

!fileId.endsWith(".vine.ts") 的意思是判断当前文件是不是.vine.ts结尾的文件,如果不是则不进行任何处理,这也便是为什么文档中会写Vue Vine只支持.vine.ts结尾的文件。

query.type === QUERY_TYPE_STYLE的意思是判断当前文件是不是css文件,由于同一个vue文件会被处理两次,第一次处理时只会处理template和script这两个模块,第二次再去单独处理style模块。

在transform钩子函数的末了便是调用runCompileScript(code, id)函数,并且将其实行结果进行返回。

runCompileScript函数

接着将断点走进runCompileScript函数,在我们这个场景中简化后的runCompileScript函数代码如下:

const runCompileScript = (code, fileId) => { const vineFileCtx = compileVineTypeScriptFile( code, fileId, compilerHooks, fileCtxMap, ); return { code: vineFileCtx.fileMagicCode.toString(), };};

从上面的代码可以看到首先会以code(当前文件的code代码字符串)为参数去实行compileVineTypeScriptFile函数,这个函数会返回一个vineFileCtx高下文工具。
这个高下文工具的fileMagicCode.toString(),便是前面我们在浏览器中看到的终极编译好的js代码。

compileVineTypeScriptFile函数

接着将断点走进compileVineTypeScriptFile函数,在我们这个场景中简化后的compileVineTypeScriptFile函数代码如下:

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;}

在实行compileVineTypeScriptFile函数之前,我们在debug终端来看看吸收的第一个参数code,如下图:

从上图中可以看到第一个参数code便是我们写的home.vine.ts文件中的源代码。

createVineFileCtx函数

接下来看第一个函数调用createVineFileCtx,这个函数返回一个vineFileCtx高下文工具。
将断点走进createVineFileCtx函数,在我们这个场景中简化后的createVineFileCtx函数代码如下:

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相似是组件函数,组件函数中当然全部都是js代码。
既然是js代码那么就可以利用babel的parser函数将组件函数的js代码编译成AST抽象语法树,以是第一步便是利用code去调用babel的parser函数天生AST抽象语法树,然后赋值给root变量。

我们在debug终端来看看得到的AST抽象语法树是什么样的,如下图:

从上图中可以看到在body数组中有两项,分别对应的便是ChildComp组件函数和Home组件函数。

接下来便是return返回一个vineFileCtx高下文工具,工具上面的几个属性我们须要讲一下。

root:由.vine.ts文件转换后的AST抽象语法树。
vineCompFns:数组中存了文件中定义的多个vue组件,初始化时为空数组。
fileMagicCode:是一个由magic-string库new的一个工具,工具中存了在编译时天生的js代码字符串。
magic-string是由svelte的作者写的一个库,用于处理字符串的JavaScript库。
它可以让你在字符串中进行插入、删除、更换等操作,在编译时便是利用这个库天生编译后的js代码。
toString方法返回经由处理后的字符串,前面的runCompileScript函数中便是终极调用vineFileCtx.fileMagicCode.toString()方法返回经由编译阶段处理得到的js代码。
findVineCompFnDecls函数

我们接着来看compileVineTypeScriptFile函数中的第二个函数调用findVineCompFnDecls:

function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { // ...省略 const vineCompFnDecls = findVineCompFnDecls(vineFileCtx.root); // ...省略}

通过前一步我们拿到了一个vineFileCtx高下文工具,vineFileCtx.root中存的是编译后的AST抽象语法树。

以是这一步便是调用findVineCompFnDecls函数从AST抽象语法树中提取出在.vine.ts文件中定义的多个vue组件工具对应的Node节点。
我们在debug终端来看看组件工具对应的Node节点组成的数组vineCompFnDecls,如下图:

从上图中可以看到数组由两个Node节点组成,分别对应的是ChildComp组件函数和Home组件函数。

doAnalyzeVine函数

我们接着来看compileVineTypeScriptFile函数中的第三个函数调用doAnalyzeVine:

function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { // ...省略 doAnalyzeVine(compilerHooks, vineFileCtx, vineCompFnDecls); // ...省略}

经由上一步的处理我们拿到了两个组件工具的Node节点,并且将这两个Node节点存到了vineCompFnDecls数组中。

由于组件工具的Node节点是一个标准的AST抽象语法树的Node节点,并不能清晰的描述一个vue组件工具。
以是接下来便是调用doAnalyzeVine函数遍历组件工具的Node节点,将其转换为能够清晰的描述一个vue组件的工具,将这些vue组件工具组成数组塞到vineFileCtx高下文工具的vineCompFns属性上。

我们在debug终端来看看经由doAnalyzeVine函数处理后天生的vineFileCtx.vineCompFns属性是什么样的,如下图:

从上图中可以看到vineCompFns属性中存的组件工具已经能够清晰的描述一个vue组件,上面有一些我们熟习的属性props、slots等。

transformFile函数

我们接着来看compileVineTypeScriptFile函数中的第四个函数调用transformFile:

function compileVineTypeScriptFile( code: string, fileId: string, compilerHooks: VineCompilerHooks, fileCtxCache?: VineFileCtx,) { // ...省略 transformFile( vineFileCtx, compilerHooks, compilerOptions?.inlineTemplate ?? true, ); // ...省略}

经由上一步的处理后在vineFileCtx高下文工具的vineCompFns属性数组中已经存了一系列能够清晰描述vue组件的工具。

在前面我们讲过了vineFileCtx.vineCompFns数组中存的工具能够清晰的描述一个vue组件,但是工具中并没有我们期望的render函数、setup函数等。

以是接下来就须要调用transformFile函数,遍历上一步拿到的vineFileCtx.vineCompFns数组,将所有的vue组件转换成对应的立即调用函数。
在每个立即调用函数中都会return一个__vine组件工具,并且这个__vine组件工具上都有一个render属性。

之以是包装成一个立即调用函数,是由于每个组件都会天生一个名为__vine组件工具,以是才须要立即调用函数将浸染域进行隔离。

我们在debug终端来看看经由transformFile函数处理后拿到的js code代码字符串,如下图:

从上图中可以看到此时的js code代码字符串已经和我们之前在浏览器中看到的编译后的代码千篇一律了。

总结

Vue Vine是一个vite插件,vite解析每个模块时都会触发插件的transform钩子函数。
在钩子函数中会去判断当前文件是否以.vine.ts结尾的,如果不是则return。

在transform钩子函数中会去调用runCompileScript函数,runCompileScript函数并不是实际干活的地方,而是去调用compileVineTypeScriptFile函数。

在compileVineTypeScriptFile函数中先new一个vineFileCtx高下文工具,工具中的root属性存了由.vine.ts文件转换成的AST抽象语法树。

接着便是调用findVineCompFnDecls函数从AST抽象语法树中找到组件工具对应的Node节点。

由于Node节点并不能清晰的描述一个vue组件,以是须要调用doAnalyzeVine函数将这些Node节点转换成能够清晰描述vue组件的工具。

末了便是遍历这些vue组件工具将其转换成立即调用函数。
在每个立即调用函数中都会return一个__vine组件工具,并且这个__vine组件工具上都有一个render属性。