time: 2022-03-24 20:13:24
author: heyunjiang
最近想做 webpack loader ast 类似的分析处理 template 代码,场景是在 vue3 component.is 动态组件中,使用 script setup 时是直接使用的变量
<component :is="$route.path.replace('/', '')" />
<script setup>
import A from 'A.vue'
</script>
此刻的组件 A 就不能被使用,应该改成如下写法
<component :is="danymicComponents[$route.path.replace('/', '')]" />
<script setup>
import A from 'A.vue'
const danymicComponents = {
A
}
</script>
想做的:不想声明额外的 danymicComponents 对象,考虑到 setup 中引入的组件,是挂载在 this.$setup 对象上,如果直接写成如下格式能识别就好了
<component :is="$setup[$route.path.replace('/', '')]" />
<script setup>
import A from 'A.vue'
</script>
但是 @vue/compiler-sfc
编译的结果却是如下
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _createBlock(_ctx.$setup.danymicComponents[_ctx.$route.path.replace("/", "")])
}
想要的是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _createBlock($setup.danymicComponents[_ctx.$route.path.replace("/", "")])
}
现在就想做一个判断,如果代码中直接写了 $setup,就不再编译到 _ctx 上了
解决思路
自己在开发 vue3 项目时,有如下问题
vite 使用的是 @vue/compiler-sfc
来执行的编译,在 @vitejs/plugin-vue
中,调用编译的流程如下
export default function vuePlugin(rawOptions: Options = {}): Plugin {
return {
buildStart() {
options.compiler = options.compiler || resolveCompiler(options.root)
},
transform(code, id, opt) {
return transformMain(
code,
filename,
options,
this,
ssr,
customElementFilter(filename)
)
}
}
}
export function resolveCompiler(root: string): typeof _compiler {
const compiler =
tryRequire('vue/compiler-sfc', root) || tryRequire('vue/compiler-sfc') // 使用的就是 @vue/compiler-sfc
return compiler
}
export async function transformMain(
code: string,
filename: string,
options: ResolvedOptions,
pluginContext: TransformPluginContext,
ssr: boolean,
asCustomElement: boolean
) {
// 1 descriptor
const { descriptor, errors } = createDescriptor(filename, code, options)
// 2 script
const { code: scriptCode, map } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr
)
// 3 template
let templateCode = ''
let templateMap: RawSourceMap | undefined
if (hasTemplateImport) {
;({ code: templateCode, map: templateMap } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr
))
}
// 4 styles
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
asCustomElement,
attachedProps
)
...
}
export function createDescriptor(
filename: string,
source: string,
{ root, isProduction, sourceMap, compiler }: ResolvedOptions
): SFCParseResult {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap
})
return { descriptor, errors }
}
async function genTemplateCode(
descriptor: SFCDescriptor,
options: ResolvedOptions,
pluginContext: PluginContext,
ssr: boolean
) {
const template = descriptor.template!
return transformTemplateInMain(
template.content,
descriptor,
options,
pluginContext,
ssr
)
}
export function transformTemplateInMain(
code: string,
descriptor: SFCDescriptor,
options: ResolvedOptions,
pluginContext: PluginContext,
ssr: boolean
): SFCTemplateCompileResults {
const result = compile(code, descriptor, options, pluginContext, ssr)
return {
...result,
code: result.code.replace(
/\nexport (function|const) (render|ssrRender)/,
'\n$1 _sfc_$2'
)
}
}
export function compile(
code: string,
descriptor: SFCDescriptor,
options: ResolvedOptions,
pluginContext: PluginContext,
ssr: boolean
) {
const filename = descriptor.filename
const result = options.compiler.compileTemplate({
...resolveTemplateCompilerOptions(descriptor, options, ssr)!,
source: code
})
return result
}
流程归纳
tryRequire(‘vue/compiler-sfc’),这里的 parse 也就是 @vue/compiler-sfc
里面的 parse,用于生成 descriptor 对象
import * as CompilerDOM from '@vue/compiler-dom'
export function parse(
source: string,
{
...
compiler = CompilerDOM
}: SFCParseOptions = {}
): SFCParseResult {
const descriptor: SFCDescriptor = {...}
const ast = compiler.parse(source, {...})
ast.children.forEach(node => {...})
const result = {
descriptor,
errors
}
return result
}
而 @vue/compiler-dom
里面调用的 parse 才是实际调用 @vue/compiler/core
里面的 baseParse 实现
import {
baseParse
} from '@vue/compiler-core'
export function parse(template: string, options: ParserOptions = {}): RootNode {
return baseParse(template, extend({}, parserOptions, options))
}
baseParse 用于生成 ast,看下节分析
这里作为 template 核心编译流程解析
@vue/compiler-sfc
初次调用 compileTemplate
export function compileTemplate(
options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
return doCompileTemplate(options)
}
function doCompileTemplate({
filename,
id,
scoped,
slotted,
inMap,
source,
ssr = false,
ssrCssVars,
isProd = false,
compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
compilerOptions = {},
transformAssetUrls
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
let { code, ast, preamble, map } = compiler.compile(source, {...})
return { code, ast, preamble, source, errors, tips, map }
}
@vue/compiler-core
baseCompile 方法实现
```javascript
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)transform( ast, extend({}, options, { prefixIdentifiers, nodeTransforms: [ …nodeTransforms, …(options.nodeTransforms || []) // user transforms ], directiveTransforms: extend( {}, directiveTransforms, options.directiveTransforms || {} // user transforms ) }) )
return generate( ast, extend({}, options, { prefixIdentifiers }) ) } ```
归纳一下 compiler 编译 template 流程
@vue/compiler-core
parse 生成 ast,这里使用正则匹配生成 vue 特定 ast 对象前面3节都是讲述的 sfc 如何接入 baseParse 生成的 ast,现在开始分析