vue3单文件组件编译过程

最近产品给我提了一个非常好玩(e xin)的需求:用户输入单文件组件(sfc)的代码就能显示对应的界面。具体可以参考vue playground。

提出问题

作为一个成熟的前端,要善于挖掘产品的隐含意思:

我:“用户输入的代码中会包含UI库的组件吗,例如element plus”。

产品:“当然要啊,不然用户怎么用”。

我: “你知道的,vue3有多种不同的写法,主要是optional和composition,其中composition还能使用< script setup>的写法”

产品: “不能把用户局限住了,要支持的”。

ok,需求弄清楚了咱们开始做技术调研。

技术调研

用户输入代码可以使用vscode的web版编辑器monaco-editor,这个很简单。主要问题是输入的代码怎么显示出对应的界面呢?

createApp

vue3中,我们通常会在main.js中通过下面的代码,将根组件挂载到dom节点中:

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app');
复制代码

createApp可以接收不同的参数,在optional的写法中,只要将sfc的template,data,methods这些对象获取到传入其中,不就可以显示出界面了嘛。

// 伪代码
let code = monacoInstance.getValue()  // 用户输入的代码字符串
// 假设compile函数是对code做一系列正则匹配,用于获取template,data等内容
let { template,data,methods } = compile(code)
createApp({
    template,
    data,
    methods
}).mount('#container');
复制代码

这么干确实可以把界面显示出来,但是有一个问题,compile函数超级难写,因为我们要支持多种不同的写法,自己写这个compile不太现实,有没有现成的库可以用呢?

@vue/compiler-sfc

当然有了,不然vue怎么去编译sfc呢,在vue3中编译sfc主要会使用@vue/compiler-sfc这个包,大概的流程是这样的:

                                  +--------------------+
                                  |                    |
                                  |  script transform  |
                           +----->+                    |
                           |      +--------------------+
                           |
+--------------------+     |      +--------------------+
|                    |     |      |                    |
|  facade transform  +----------->+ template transform |
|                    |     |      |                    |
+--------------------+     |      +--------------------+
                           |
                           |      +--------------------+
                           +----->+                    |
                                  |  style transform   |
                                  |                    |
                                  +--------------------+
复制代码

用伪代码再描述一下:

import { parse,compileTemplate,compileScript,compileStyleAsync } from '@vue/compiler-sfc'

let code = monacoInstance.getValue()  // 用户输入的代码字符串

const descriptor = parse(code) //  facade transform,生成的descriptor中已经可以找到我们所需要的tempplate,但是methods,data这些数据还无法获取

const compiledScript = compileScript(descriptor)

const result = rewriteDefault(compiledScript.content) // result是一个字符串,通过动态生成script来执行字符串的内容,最后会返回一个__sfc__,这也就是我们需要传入到createApp里面的
复制代码

上述这段伪代码的思路,来自于sfc-playground这个项目,有兴趣可以去读一下。

原本我以为,要提取data,methods等传入createApp中才能显示出一个完成的sfc,实际上如果你输入的代码中包含setup,并不会有data,methods这些属性,而是会产生一个setup函数,这个函数会返回data和methods,将这个setup函数传入createApp即可。

@vue/compiler-sfc有broswer版本,也有nodejs的版本,我选择在服务端编译,因为在使用broswer版本中爆了一些我无法解决的错误,能力有限,希望有人能出一个broswer版本的使用教程。

在做完这些后,已经可以将用户输入的sfc显示出来了,但是还有一个问题,如果输入的代码中有UI库组件,则编译不出来,例如使用了el-button。

UI库组件的不显示

我发现当使用optional的写法,传入createApp中是template,data,methods这些的时候,只需要这样写就可以正常显示出UI库的组件:

import { createApp,nextTick } from 'vue'
let app = createApp({
    template,
    methods,
    ...
})
nextTick(async () => {
    // 根据条件判断,导入哪些依赖
    const ElementPlus = await import('element-plus');
    _app.use(ElementPlus).mount(#container)
})
复制代码

而使用setup则会导致UI库的组件无法渲染,这个问题其实很简单。使用setup之后会sfc会包含一个render函数,我们知道sfc的编译过程,其实就是将template编译成render函数,我们传入template可以显示出UI组件,而传入render函数无法显示UI组件,那么原因就是当传入createApp中存在render函数时,不会再次进行compile。

如果希望use(ElementPlus)能成功执行,则需要在前端再compile一次,这时只需将传入createApp中的render设置为null,以及传入从descriptor中获取的template即可。另外:

// 如果设置inlineTemplate为true,那么setup函数返回的并不是data,methods这些对象而是一个render函数
const compiledScript = SFCCompiler.compileScript(descriptor, {
    inlineTemplate: true
})
复制代码

在sfc-playground中,解析< script setup>的代码,inlineTemplate是为true的,所以setup中会返回render函数。设置inlineTemplate为false,则会生成单独的render函数,设置这个render函数为null,就可以触发前端的compile。

最后

如果你也遇到类似的需求,我建议可以先看看@vue/compiler-sfc和sfc-playground的源码。

你可能感兴趣的:(vue3单文件组件编译过程)