vite项目vue的编译原理

如何从Vue文件编译成JS文件

在vite.config.ts中会加载一个plugin-vue插件,这个插件会注册一些钩子。

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
return {
name: "vite:vue",

// 服务启动时执行,用于修改或扩展 vite 配置
config(config) {
// 配置 resolve.dedupe、define 等选项
},

// 在解析 vite 配置后执行,此时可以读取最终配置
configResolved(config) {
// 配置解析完成后更新 options
},

// 创建开发服务器时执行,用于配置 dev server
configureServer(server) {
// 配置开发服务器,保存 server 实例
},

// 在项目构建开始时执行,用于初始化编译器
buildStart() {
// 构建开始时初始化 compiler
},

// 在每个模块请求时执行,解析模块路径
async resolveId(id) {
// 解析模块 ID
},

// 在确定模块路径后执行,用于加载模块内容
load(id, opt) {
// 加载模块内容
},

// 在模块内容加载后执行,用于转换代码
transform(code, id, opt) {
// 转换 Vue 单文件组件
},

// 在开发模式下,文件发生变化时执行,处理热更新
handleHotUpdate(ctx) {
// 处理热更新
}
}

这里需要重点注意buildStart和transform钩子。

执行yarn dev的时候buildStart钩子会执行
打开网站页面的时候,加载模块的时候,transform钩子会执行

transformMain

  • 在transform中,会执行一个transformMain的方法

关于transformMain

  • 首先会通过createDescriptor创建descriptor
1
2
3
4
5
6
7
8
9
10
11
function createDescriptor (filename, source, { root, isProduction, sourceMap, compiler, template }, hmr = false) {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap,
templateParseOptions: template?.compilerOptions
});
const normalizedPath = normalizePath$1(path.relative(root, filename));
descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));
(hmr ? hmrCache : cache).set(filename, descriptor);
return { descriptor, errors };
}

上面是createDescriptor,可以看到它接受一个参数compiler,而这个就是在buildStart时,注册的vue核心编译方法。通过compiler.parse()将.vue文件,编译为静态语法树(AST)。

传入的source是文件字符串,返回的是AST以及其他属性

createDescriptor params

createDescriptor return

  • 之后会通过genScriptCode、genTemplateCode、genStyleCode分别处理生成js文件代码,render函数,css文件代码。其中genScriptCode、genTemplateCode都是通过compiler的一些核心方法进行转化的。

以下是render函数

通过genTemplateCode生成render函数

值的一提的是 stylesCode的值

1
2
'
import "/Users/dansroh/Documents/study/vue-core-study/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css" '

可以看到,stylesCode的值并不是css代码。而是通过imort 导入的一条条语句。

为什么会是这样呢?

原因需要回到transform中。

首先明确一点,每次加载模块,都会执行一次transform。

当stylesCode编译完成,执行import的时候,就会重新回到transform。

以下是transform完整代码

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
transform (code, id, opt) {
const ssr = opt?.ssr === true;
const { filename, query } = parseVueRequest(id);
if (query.raw || query.url) {
return;
}
if (!filter.value(filename) && !query.vue) {
return;
}
if (!query.vue) {
return transformMain(
code,
filename,
options.value,
this,
ssr,
customElementFilter.value(filename)
);
} else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.type === "template") {
return transformTemplateAsModule(
code,
descriptor,
options.value,
this,
ssr,
customElementFilter.value(filename)
);
} else if (query.type === "style") {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename
);
}
}
}

可以看到在执行过程中,有通过query.vue和query.type来判断执行的方法。

所以样式文件最终会执行到transformStyle方法中。我们再来看transformStyle内部实现。

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
async function transformStyle (code, descriptor, index, options, pluginContext, filename) {
const block = descriptor.styles[ index ];
const result = await options.compiler.compileStyleAsync({
...options.style,
filename: descriptor.filename,
id: `data-v-${descriptor.id}`,
isProd: options.isProduction,
source: code,
scoped: block.scoped,
...options.cssDevSourcemap ? {
postcssOptions: {
map: {
from: filename,
inline: false,
annotation: false
}
}
} : {}
});
if (result.errors.length) {
result.errors.forEach((error) => {
if (error.line && error.column) {
error.loc = {
file: descriptor.filename,
line: error.line + block.loc.start.line,
column: error.column
};
}
pluginContext.error(error);
});
return null;
}
const map = result.map ? await formatPostcssSourceMap(
// version property of result.map is declared as string
// but actually it is a number
result.map,
filename
) : { mappings: "" };
return {
code: result.code,
map
};
}

可以看到此时,id以及被绑定为data-v。用于scope style的属性选择器。

最终的styles

Vue 文件编译流程总结

1. plugin-vue 插件核心流程

Vite 通过 plugin-vue 插件来处理 .vue 文件的编译,主要涉及两个关键钩子:

  • buildStart: 服务启动时执行,初始化 Vue 编译器
  • transform: 模块加载时执行,负责代码转换

2. 编译主要步骤

2.1 解析阶段 (Parse)

  • 通过 createDescriptor 方法将 .vue 文件解析成 AST
  • 使用 compiler.parse() 进行语法分析
  • 生成包含 template、script、style 等信息的描述符

2.2 转换阶段 (Transform)

分别处理三个主要部分:

  • genScriptCode: 生成 JS 代码
  • genTemplateCode: 生成 render 函数
  • genStyleCode: 处理样式代码

2.3 样式处理的特殊性

  • 样式代码不会直接编译,而是通过 import 语句导入
  • 当执行 import 时会触发新的 transform 钩子
  • 通过 transformStyle 方法处理样式:
    • 添加 scoped 样式的唯一标识 (data-v-xxx)
    • 编译预处理器语法
    • 生成最终的 CSS 代码

3. 关键特性

  • 模块化处理:每个部分(template/script/style)独立处理
  • 热更新支持:通过 handleHotUpdate 钩子实现
  • Scoped CSS:自动添加唯一标识符确保样式隔离

4. 编译结果

  • JS:包含组件逻辑和 render 函数
  • CSS:经过处理的样式代码,支持 scoped
  • Template:转换为 render 函数的模板代码