最近产品给我提了一个非常好玩(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的源码。