了解vue文件的编译
- vue文件的编译主要是依赖于webpack和vite这两个前端构建工具,将vue文件编译成js。webpack使用vue-loader进行编译,vite使用@vitejs/plugin-vue、@vitejs/plugin-vue2进行编译。
- vue-loader, @vitejs/plugin-vue, @vitejs/plugin-vue2 三者都依赖了一个核心模块 vue/compiler-sfc
- 这里要注意 vue/compiler-sfc 和 @vue/compiler-sfc,前者相当于是一个调用入口和版本锁的作用,保证 vue 和 compiler-sfc 的版本一致性;后者是真正的代码实现。
@vue/compiler-sfc介绍
@vue/compiler-sfc 具备两大能力,一个是 parse,一个是 compile。涉及到三个部分,template、script、style,分别表示模版、脚本、样式三块内容。
parse
- template被 parse 成SFCTemplateBlock。SFCTemplateBlock继承了SFCBlock,并且有ast属性,ast就是抽象语法树,可以用结构化的数据描述模板的结构。
- script 部分分为两块来看,一个是不带setup属性的常规script,一个是带setup属性的 scriptSetup,script 和 scriptSetup 的 AST 结构是不一样的。
- style 部分则是被 parse 成一个数组styles,它的类型是SFCStyleBlock[]。为什么 style 的 parse 结果会是一个数组呢?这是因为我们可以在 .vue 文件中写多个 style 块。SFCStyleBlock包含了两个标记类的属性scoped, module,分别代表有没有启用 scoped 和 css module 特性。scoped和module的区别。
新建一个node脚步进行测试
const { parse } = require("@vue/compiler-sfc")
const fs = require("fs")
fs.readFile("./test.vue", (err, data) => {
let parsed = parse(data.toString(), {
filename: 'test.vue'
})
console.log('parsed', parsed);
})
compile
- compileTemplate:拿到之前parse后的结果后,需要对template进行进一步的转换,把template结果进一步编译成对应的js vnode函数(模板部分会被 Vue 的模板编译器编译为渲染函数。渲染函数是返回虚拟 DOM(VNode)的 JavaScript 函数)
let compileredTemplate = compileTemplate({
id: '',
filename: 'test.vue',
source: parsed.descriptor.template.content
})
- compilerScript:根据parsed的结果来解析脚本部分,compileScript接收2个参数,第一个就是之前parse的结果, 然后再传入相应的option。脚本部分会被转换为标准的 ES 模块。如果使用了 TypeScript 或其他预处理器,还会进行相应的转换。
let compileredScript = compileScript(parsed.descriptor, {
id: ''
})
- compileStyle:解析SFC style模块的入口函数。样式部分会被处理并转换为 CSS。这一步可能涉及使用预处理器(如 Sass、Less)和处理 scoped 样式。
let compileredStyle = compileStyle({
source: parsed.descriptor.styles[0].content,
scoped: true,
id: 'data-v-xxx'
})
编译中涉及props变量声明提升部分
- vue3.3.4版本之前是存在props变量声明提升的。后续被删除,具体看这个commit:https://github.com/vuejs/core/commit/24db9516d8b4857182ec1a3af86cb7346691679b
// compiler-sfc/src/compileScript.ts
let args = __props
if (ctx.propsTypeDecl) {
// mark as any and only cast on assignment
// since the user defined complex types may be incompatible with the
// inferred type from generated runtime declarations
args += : any // 如果 ctx.propsTypeDecl 存在,表示用户定义了 props 的类型声明。由于用户定义的复杂类型可能与生成的运行时声明中的推断类型不兼容,因此将 args 标记为 any 类型,这意味着在实际赋值时才进行类型转换。
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
// can use it directly
if (ctx.propsIdentifier) {
ctx.s.prependLeft(
startOffset,
\nconst ${ctx.propsIdentifier} = __props;\n // 该代码会在指定的 startOffset 位置之前插入一行代码:const [propsIdentifier] = __props;,以便在模板表达式中直接使用该标识符引用 props。startOffset 用来表示代码插入的起始位置的偏移量。通常,startOffset 是根据解析的 AST(抽象语法树)节点计算的,用于在特定的位置插入、删除或替换代码片段。
)
}
// compiler-sfc/src/script/context.ts
// 在这个文件中有对于startOffset的定义
startOffset = this.descriptor.scriptSetup?.loc.start.offset // 它表示了 Vue 单文件组件(SFC)中 <script setup> 标签的起始位置偏移量。