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的源码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容