【看的见的思考】compiler-sfc
sfc 这个模块是由 vue-loader 来调用的,目的是解析 SFC文件
主入口是 parse
接着来分析一下
一开始的时候还是基于 compiler.parse 去生成 ast 对象
const ast = compiler.parse(source, {
// there are no components at SFC parsing level
isNativeTag: () => true,
// preserve all whitespaces
isPreTag: () => true,
getTextMode: ({ tag, props }, parent) => {
// all top level elements except <template> are parsed as raw text
// containers
if (
(!parent && tag !== 'template') ||
// <template lang="xxx"> should also be treated as raw text
(tag === 'template' &&
props.some(
p =>
p.type === NodeTypes.ATTRIBUTE &&
p.name === 'lang' &&
p.value &&
p.value.content &&
p.value.content !== 'html'
))
) {
return TextModes.RAWTEXT
} else {
return TextModes.DATA
}
},
onError: e => {
errors.push(e)
}
})接着遍历 ast 来处理 template、script、style
switch (node.tag) {
case 'template':
if (!descriptor.template) {
const templateBlock = (descriptor.template = createBlock(
node,
source,
false
) as SFCTemplateBlock)
templateBlock.ast = node
break是创建一个 block 然后存放到 descriptor.template 内
case 'script':
const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
const isSetup = !!scriptBlock.attrs.setup
if (isSetup && !descriptor.scriptSetup) {
descriptor.scriptSetup = scriptBlock
break
}
if (!isSetup && !descriptor.script) {
descriptor.script = scriptBlock
break
}
errors.push(createDuplicateBlockError(node, isSetup))
break也是创建一个 block 放到 descriptor.scriptSetup
case 'style':
const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
descriptor.styles.push(styleBlock)
breakstyle 和上面不同的话,是可以有多个
最后是处理自定义的
descriptor.customBlocks.push(createBlock(node, source, pad))都不是的话,那么就是 custom类型的blocks
最后是返回了 result 对象
const result = {
descriptor,
errors
}
sourceToSFC.set(sourceKey, result)
return result这个东西就需要和 vue-loader 结合去看了
接着我们从单元测试看
test('nested templates', () => {
const content = `
<template v-if="ok">ok</template>
<div><div></div></div>
`
const { descriptor } = parse(`<template>${content}</template>`)
console.log(descriptor)
expect(descriptor.template!.content).toBe(content)
})然后我们看看 descriptor 长什么样子
{
filename: 'anonymous.vue',
source: '<template>\n' +
' <template v-if="ok">ok</template>\n' +
' <div><div></div></div>\n' +
' </template>',
template: {
type: 'template',
content: '\n <template v-if="ok">ok</template>\n <div><div></div></div>\n ',
loc: {
source: '\n <template v-if="ok">ok</template>\n <div><div></div></div>\n ',
start: [Object],
end: [Object]
},
attrs: {},
ast: {
type: 1,
ns: 0,
tag: 'template',
tagType: 0,
props: [],
isSelfClosing: false,
children: [Array],
loc: [Object],
codegenNode: undefined
},
map: {
version: 3,
sources: [Array],
names: [],
mappings: ';IACI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC',
file: 'anonymous.vue',
sourceRoot: '',
sourcesContent: [Array]
}
},
script: null,
scriptSetup: null,
styles: [],
customBlocks: [],
cssVars: [],
slotted: false
}
compileTemplate
先看看是如何解析 template 的
先从单元测试看起
test('should work', () => {
const source = `<div><p>{{ render }}</p></div>`
const result = compile({ filename: 'example.vue', source })
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
// should expose render fn
expect(result.code).toMatch(`export function render(`)
})compile 就是调用的 compileTemplate
export function compileTemplate(
options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
const { preprocessLang, preprocessCustomRequire } = options
const preprocessor = preprocessLang
? preprocessCustomRequire
? preprocessCustomRequire(preprocessLang)
: require('consolidate')[preprocessLang as keyof typeof consolidate]
: false
if (preprocessor) {
try {
return doCompileTemplate({
...options,
source: preprocess(options, preprocessor)
})
} catch (e) {
return {
code: `export default function render() {}`,
source: options.source,
tips: [],
errors: [e]
}
}
} else if (preprocessLang) {
return {
code: `export default function render() {}`,
source: options.source,
tips: [
`Component ${options.filename} uses lang ${preprocessLang} for template. Please install the language preprocessor.`
],
errors: [
`Component ${options.filename} uses lang ${preprocessLang} for template, however it is not installed.`
]
}
} else {
return doCompileTemplate(options)
}
}接着我们去把逻辑给拆分
const { preprocessLang, preprocessCustomRequire } = options是给用户做扩展用的接口,可以先忽略掉
接着是调用了 doCompileTemplate
else {
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 {
const errors: CompilerError[] = []
const warnings: CompilerError[] = []
let nodeTransforms: NodeTransform[] = []
if (isObject(transformAssetUrls)) {
const assetOptions = normalizeOptions(transformAssetUrls)
nodeTransforms = [
createAssetUrlTransformWithOptions(assetOptions),
createSrcsetTransformWithOptions(assetOptions)
]
} else if (transformAssetUrls !== false) {
nodeTransforms = [transformAssetUrl, transformSrcset]
}
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
let { code, ast, preamble, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
cacheHandlers: true,
ssrCssVars:
ssr && ssrCssVars && ssrCssVars.length
? genCssVarsFromList(ssrCssVars, shortId, isProd)
: '',
scopeId: scoped ? longId : undefined,
slotted,
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
sourceMap: true,
onError: e => errors.push(e),
onWarn: w => warnings.push(w)
})
// inMap should be the map produced by ./parse.ts which is a simple line-only
// mapping. If it is present, we need to adjust the final map and errors to
// reflect the original line numbers.
if (inMap) {
if (map) {
map = mapLines(inMap, map)
}
if (errors.length) {
patchErrors(errors, source, inMap)
}
}
const tips = warnings.map(w => {
let msg = w.message
if (w.loc) {
msg += `\n${generateCodeFrame(
source,
w.loc.start.offset,
w.loc.end.offset
)}`
}
return msg
})
return { code, ast, preamble, source, errors, tips, map }
}继续拆分
let nodeTransforms: NodeTransform[] = []
if (isObject(transformAssetUrls)) {
const assetOptions = normalizeOptions(transformAssetUrls)
nodeTransforms = [
createAssetUrlTransformWithOptions(assetOptions),
createSrcsetTransformWithOptions(assetOptions)
]
} else if (transformAssetUrls !== false) {
nodeTransforms = [transformAssetUrl, transformSrcset]
}
这里依然是给用户做扩展的
最终会影响 nodeTransforms 的值
也就是说 不同的 transformAssetUrls 会有不同的 transform
下面是核心代码
let { code, ast, preamble, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
cacheHandlers: true,
ssrCssVars:
ssr && ssrCssVars && ssrCssVars.length
? genCssVarsFromList(ssrCssVars, shortId, isProd)
: '',
scopeId: scoped ? longId : undefined,
slotted,
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
sourceMap: true,
onError: e => errors.push(e),
onWarn: w => warnings.push(w)
})这里的 compiler 是来自 compiler-dom ,执行 compile 开始编译
所以 template 就是收集一些特有的数据 ,然后给到 compiler.compile 进行编译,最后得到数据完事。
看看 template 如果是 pug 的话,是如何处理的
test('preprocess pug', () => {
const template = parse(
`
<template lang="pug">
body
h1 Pug Examples
div.container
p Cool Pug example!
</template>
`,
{ filename: 'example.vue', sourceMap: true }
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang
})
expect(result.errors.length).toBe(0)
})如果template 是 pug 的话,那么 vue3 会调用 consolidate 这个库来处理解析
consolidate 是个template 大杂烩,做了一个中间层
那么换个角度来讲的话,只要是 consolidate 支持的template ,vue3 的template 就会支持
compileScript
接着看看如何处理 script 的
还是先从单元测试入手
test('should expose top level declarations', () => {
const { content, bindings } = compile(`
<script setup>
import { x } from './x'
let a = 1
const b = 2
function c() {}
class d {}
</script>
<script>
import { xx } from './x'
let aa = 1
const bb = 2
function cc() {}
class dd {}
</script>
`)
expect(content).toMatch('return { aa, bb, cc, dd, a, b, c, d, xx, x }')
expect(bindings).toStrictEqual({
x: BindingTypes.SETUP_MAYBE_REF,
a: BindingTypes.SETUP_LET,
b: BindingTypes.SETUP_CONST,
c: BindingTypes.SETUP_CONST,
d: BindingTypes.SETUP_CONST,
xx: BindingTypes.SETUP_MAYBE_REF,
aa: BindingTypes.SETUP_LET,
bb: BindingTypes.SETUP_CONST,
cc: BindingTypes.SETUP_CONST,
dd: BindingTypes.SETUP_CONST
})
assertCode(content)
})这里的 compile 实际是调用了 compileSFCScript
这里最终实现的就是把 script 代码编译成可以让 runtime 执行的js代码
import { x } from './x'\n \nexport default {\n setup(__props, { expose }) {\n expose()\n\n let a = 1\n const b = 2\n function c() {}\n class d {}\n \nreturn { aa, bb, cc, dd, a, b, c, d, xx, x }\n}\n\n}\n import { xx } from './x'\n let aa = 1\n const bb = 2\n function cc() {}\n class dd {}接着看看调用的主流程
会调用compileScript,代码量很大,我们分拆这来看
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions
): SFCScriptBlock { const { descriptor } = parse(src)先看看 input ,这里的 sfc 是通过 parse 过得到的对象,是已经把src也就是string代码编译成对象了
接着就是通过这个对象上面的数据信息来进行处理就可以了
在一开始的时候先收集数据,进行初始化
const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
const cssVars = sfc.cssVars
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
const isTS =
scriptLang === 'ts' ||
scriptLang === 'tsx' ||
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
const plugins: ParserPlugin[] = [...babelParserDefaultPlugins]
if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
plugins.push('jsx')
}
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
if (isTS) plugins.push('typescript', 'decorators-legacy')然后先处理的是 普通的 scirpt
处理普通的 script
try {
let content = script.content
let map = script.map
const scriptAst = _parse(content, {
plugins,
sourceType: 'module'
}).program
const bindings = analyzeScriptBindings(scriptAst.body)
if (enableRefTransform && shouldTransformRef(content)) {
const s = new MagicString(source)
const startOffset = script.loc.start.offset
const endOffset = script.loc.end.offset
const { importedHelpers } = transformRefAST(scriptAst, s, startOffset)
if (importedHelpers.length) {
s.prepend(
`import { ${importedHelpers
.map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`
)
}
s.remove(0, startOffset)
s.remove(endOffset, source.length)
content = s.toString()
map = s.generateMap({
source: filename,
hires: true,
includeContent: true
}) as unknown as RawSourceMap
}先解析 script里面的代码,搞成 ast 对象
这里是基于 babel 来解析的 JS 代码
基于 ast.body 生成bindings
看看有没有开启 enableRefTransform,开启的话处理
if (cssVars.length) {
content = rewriteDefault(content, `__default__`, plugins)
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
scopeId,
!!options.isProd
)
content += `\nexport default __default__`
}
return {
...script,
content,
map,
bindings,
scriptAst: scriptAst.body
}- 处理 cssVars
完事就直接返回了,可以看到普通的 script 的处理过程是比较简单的
处理 script setup
处理 setup 代码就多了去了
// metadata that needs to be returned
const bindingMetadata: BindingMetadata = {}
const defaultTempVar = `__default__`
const helperImports: Set<string> = new Set()
const userImports: Record<string, ImportBinding> = Object.create(null)
const userImportAlias: Record<string, string> = Object.create(null)
const setupBindings: Record<string, BindingTypes> = Object.create(null)
let defaultExport: Node | undefined
let hasDefinePropsCall = false
let hasDefineEmitCall = false
let hasDefineExposeCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
let propsTypeDeclRaw: Node | undefined
let propsIdentifier: string | undefined
let emitsRuntimeDecl: Node | undefined
let emitsTypeDecl:
| TSFunctionType
| TSTypeLiteral
| TSInterfaceBody
| undefined
let emitsTypeDeclRaw: Node | undefined
let emitIdentifier: string | undefined
let hasAwait = false
let hasInlinedSsrRenderFn = false
// props/emits declared via types先初始化后面需要用到的数据
总结
目标:compiler-sfc 是把 sfc 里面的string生成对应的用 js 表达的组件代码
基本的逻辑是先解析 SFC 文件,生成对应的对象。这个对象里面包含了所有 SFC 的信息
然后在把 template script style 分别交给各自的编译器来编译成对象
template 就是用的compiler-dom 来解析的
script 用的是 babel 来解析的
style 暂时还不知道 TODO
然后基于ast 对象就可以对代码做手术了,比如获取到某些信息,然后基于这些信息生成对应的 js 代码
Source: cuixiaorui/mini-vue