0%

VueRepl源码解析之如何实现实时编译渲染

简介

Vue Playground支持选择不同的Vue版本,实时编码并实时预览。本文主要介绍其如何实现实时编码和预览。

image-20230110185644361

代码跟踪

CodeMirror.vue — vuejs/repl — GitHub1s

1
2
3
4
// code mirror 这里 在代码变化之后抛出了 change 事件
editor.on('change', () => {
emit('change', editor.getValue())
})

Editor.vue — vuejs/repl — GitHub1s

1
2
3
4
// editor 这里 在监听到change事件之后 修改了 store activeFile 的 code属性
const onChange = debounce((code: string) => {
store.state.activeFile.code = code
}, 250)

store.ts — vuejs/repl — GitHub1s

1
2
// store 里在初始化的时候监听了 activeFile 的变化 并在变化后重新计算了文件
watchEffect(() => compileFile(this, this.state.activeFile))

transform.ts — vuejs/repl — GitHub1s

1
2
3
4
5
6
7
8
9
// 监听到文件变化后重新编译了文件
//
// store.compiler本质上就是一个vue文件的解析器
// 详情可以见
// https://www.npmjs.com/package/@vue/compiler-sfc
const { errors, descriptor } = store.compiler.parse(code, {
filename,
sourceMap: true
})

transform.ts — vuejs/repl — GitHub1s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 之后分别对 clientScript ssrScript clientTemplate ssrTemplate 以及style等做了编译和解析
const clientScriptResult = await doCompileScript(
store,
descriptor,
id,
false,
isTS
)
clientCode += clientScriptResult
...
const ssrScriptResult = await doCompileScript(
store,
descriptor,
id,
true,
isTS
)
...
const clientTemplateResult = await doCompileTemplate(
store,
descriptor,
id,
bindings,
false,
isTS
)
clientCode += clientTemplateResult
...
const ssrTemplateResult = await doCompileTemplate(
store,
descriptor,
id,
bindings,
true,
isTS
)
clientCode += ssrTemplateResult
...
const styleResult = await store.compiler.compileStyleAsync({
...store.options?.style,
source: style.content,
filename,
id,
scoped: style.scoped,
modules: !!style.module
})

transform.ts — vuejs/repl — GitHub1s

1
2
3
// 最终将 解析完毕的文件 代码写入了 file 的 compiled 属性
compiled.js = clientCode.trimStart()
compiled.ssr = ssrCode.trimStart()

Preview.vue — vuejs/repl — GitHub1s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// reset sandbox when import map changes
watch(
() => store.state.files['import-map.json'].code,
(raw) => {
try {
const map = JSON.parse(raw)
if (!map.imports) {
store.state.errors = [`import-map.json is missing "imports" field.`]
return
}
createSandbox()
} catch (e: any) {
store.state.errors = [e as Error]
return
}
}
)

...

async function updatePreview() {
if (import.meta.env.PROD && clearConsole.value) {
console.clear()
}
runtimeError.value = null
runtimeWarning.value = null

let isSSR = props.ssr
if (store.vueVersion) {
const [_, minor, patch] = store.vueVersion.split('.')
if (parseInt(minor, 10) < 2 || parseInt(patch, 10) < 27) {
alert(
`The selected version of Vue (${store.vueVersion}) does not support in-browser SSR.` +
` Rendering in client mode instead.`
)
isSSR = false
}
}

try {
const mainFile = store.state.mainFile

// if SSR, generate the SSR bundle and eval it to render the HTML
if (isSSR && mainFile.endsWith('.vue')) {
const ssrModules = compileModulesForPreview(store, true)
console.log(
`[@vue/repl] successfully compiled ${ssrModules.length} modules for SSR.`
)
await proxy.eval([
`const __modules__ = {};`,
...ssrModules,
`import { renderToString as _renderToString } from 'vue/server-renderer'
import { createSSRApp as _createApp } from 'vue'
const AppComponent = __modules__["${mainFile}"].default
AppComponent.name = 'Repl'
const app = _createApp(AppComponent)
app.config.unwrapInjectedRef = true
app.config.warnHandler = () => {}
window.__ssr_promise__ = _renderToString(app).then(html => {
document.body.innerHTML = '<div id="app">' + html + '</div>'
}).catch(err => {
console.error("SSR Error", err)
})
`
])
}

// compile code to simulated module system
const modules = compileModulesForPreview(store)
console.log(
`[@vue/repl] successfully compiled ${modules.length} module${
modules.length > 1 ? `s` : ``
}.`
)
// 这里直接将 store 中经过编译的js code 直接 Eval 进了 playground 用于渲染
const codeToEval = [
`window.__modules__ = {}\nwindow.__css__ = ''\n` +
`if (window.__app__) window.__app__.unmount()\n` +
(isSSR ? `` : `document.body.innerHTML = '<div id="app"></div>'`),
...modules,
`document.getElementById('__sfc-styles').innerHTML = window.__css__`
]

// if main file is a vue file, mount it.
if (mainFile.endsWith('.vue')) {
codeToEval.push(
`import { ${
isSSR ? `createSSRApp` : `createApp`
} as _createApp } from "vue"
const _mount = () => {
const AppComponent = __modules__["${mainFile}"].default
AppComponent.name = 'Repl'
const app = window.__app__ = _createApp(AppComponent)
app.config.unwrapInjectedRef = true
app.config.errorHandler = e => console.error(e)
app.mount('#app')
}
if (window.__ssr_promise__) {
window.__ssr_promise__.then(_mount)
} else {
_mount()
}`
)
}

// eval code in sandbox
await proxy.eval(codeToEval)
} catch (e: any) {
runtimeError.value = (e as Error).message
}
}

结论

moduleCompiler.ts代码中可知,编译器模块从'vue/compiler-sfc'引入了babelParse并基于babelParse对用户输入的VueCSSJS等代码做了编译。并且Preview.vue预览器模块监听了其编译结果,当发生变化时替换预览器ifream中的JS脚本以体现实时预览的效果。

参考 & 引用

https://sfc.vuejs.org/

core/packages/sfc-playground/src at main · vuejs/core (github.com)

vuejs/repl: Vue SFC REPL as a Vue 3 component (github.com)