Vue 3 文件编译流程详解与 Babel 的使用
文章目录
- 一、背景
- 二、结论
- 三、@vitejs/plugin-vue 插件
- 调试前物料准备
- vuePlugin 入口
- buildStart 方法
- transform 方法
- 四、@vue/compiler-sfc 核心包
- parse 方法
- compileScript、rewriteDefault 方法
- compileTemplate 方法
- 五、整体架构
- 六、总结
- 参考资料
一、背景
最近正在研究 react jsx 转化为 js 的过程,在学习完成之后,突然想到既然 react 是用过 babel 完成 jsx 的转化,那 vue 是不是也是使用 babel 的呢,带着这些疑问我开始探索 vue 的编译过程,经过一些资料的查询发现好像和 react 不太一样,但是网上却很少有深入的文章,因此我开始对源码的探索。
二、结论
vue 3 组件在编译过程中使用了 babel/parse 处理 script 中 js 代码处理为普通 js 代码,其余解析均未使用;下面我们将从源码层面解读。
三、@vitejs/plugin-vue 插件
调试前物料准备
- vue 3 + vite
- vscode
添加入口断点:
- 找到入口文件 vite.config.js
- 找到对应解析插件 vitejs/plugin-vue 插件 并在对应使用处打上断点
- 在控制台打开 js 调试终端 (调试方法和浏览器类似)
- 在对应终端启动我们的项目
vuePlugin 入口
进入 vue()
方法调用中,会出现 vuePlugin 方法这个方法暴露了很多配置属性,因为我们目的关注编译打包过程,所以我们这次只需关系核心的 buildStart 和 transform 即可,因此我们在这两个方法中打上断点
// src/index.ts
function vuePlugin(rawOptions = {}) {
// xxxx 省略
return {
name: "vite:vue",
handleHotUpdate(ctx) {
},
config(config) {
},
configResolved(config) {
},
configureServer(server) {
},
// 开始打打包时处理的内容
buildStart() {
options.compiler = options.compiler || resolveCompiler(options.root);
},
async resolveId(id) {
},
load(id, opt) {
},
// 转化代码时执行的内容
transform(code, id, opt) {
// xxxx 省略
}
};
}
buildStart 方法
buildStart 方法主要是在服务启动前拿到编译的配置
接下来我们在 build 处打上断点看之后的流程,在控制台输入 options 查看其中内容:
此图可以看出最初 compiler 为空,我们的代码执行了 resolveCompiler
方法,并把结果存入了 options.compiler
中,为了清楚该方法做了什么,我们 step into 看一下这个方法的实现。
这个代码可以看出其实就是加载了并且返回了 vue/compiler-sfc
这个核心包,那加载这个包是做什么呢,我们可以接着往下看,继续走下一个断点,然后会发现我们项目启动了,并没有走到下一个断点。
transform 方法
那么是什么时候怎么才能走到我们的 transform 中呢?,(vite 的特点:先启动再根据加载页面按需加载所需要编译的代码),明白这一点之后我们只需要在浏览器访问这个地址:我这里就是 http://localhost:3000/
访问之后发现我们的断点进入到了 transform 中:
从这边也可以看出 vite 打包为什么比 webpack 快
继续看 transform
里面的代码,可以看到 return transformMain()
这个就是我们的核心转化方法,我们继续 step into 该方法:
接着我们看一下这个方法的入参:
- code: 就是我们的源代码
- filename:这个文件的路径
- options:配置项
- pluginContext:插件的上下文(this)
- ssr:暂不考虑
- asCustomElement:暂不考虑
然后我们其实核心要看的就是这个 code -> 原生 js 的转化,因此往下看这个 code 都在哪里使用,首先我们就可以看到 createDescriptor
这个方法,这个时候我们打印一下descriptor 得到如下图:
发现我们 code 被解析如下三个部分 template
、styles
、script
。接着看这里的代码发现如下图 3 行代码:
// 解析 descriptor 中的 script 部分
const { code: scriptCode, map } = await genScriptCode(descriptor, options, pluginContext, ssr);
// 解析 descriptor 中的 style 部分
const stylesCode = await genStyleCode(descriptor, pluginContext, asCustomElement, attachedProps);
// 解析 descriptor 中的 template 部分
const ({ code: templateCode, map: templateMap } = await genTemplateCode(descriptor, options, pluginContext, ssr));
然后在这里其实就比较清晰了,createDescriptor
先对源代码进行了初步解析,然后返回了 descriptor
,然后我们进入 genScriptCode
、genStyleCode
、genTemplateCode
这三个方法进行具体代码转化。
接下来我们看下这四个方法的具体实现(只保留核心代码):
// 得到原始的 descriptor
function createDescriptor(filename, source, { root, isProduction, sourceMap, compiler }) {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap
});
return { descriptor, errors };
}
// 处理 descriptor 中 script 部分
async function genScriptCode(descriptor, options, pluginContext, ssr) {
// 只保留核心代码
const script = resolveScript(descriptor, options, ssr);
scriptCode = options.compiler.rewriteDefault(script.content, "_sfc_main", xxx);
return {
code: scriptCode,
map
};
}
// 处理 css
async function genStyleCode(descriptor, pluginContext, asCustomElement, attachedProps) {
// 没有使用 compiler
}
// 处理 template
async function genTemplateCode(descriptor, options, pluginContext, ssr) {
const template = descriptor.template;
if (!template.lang && !template.src) {
return transformTemplateInMain(template.content, descriptor, options, pluginContext, ssr);
}
}
function transformTemplateInMain(code, descriptor, options, pluginContext, ssr) {
const result = compile(code, descriptor, options, pluginContext, ssr);
}
function compile(code, descriptor, options, pluginContext, ssr) {
const result = options.compiler.compileTemplate(__spreadProps(__spreadValues({}, resolveTemplateCompilerOptions(descriptor, options, ssr)), {
source: code
}));
return result;
}
function resolveScript(descriptor, options, ssr) {
resolved = options.compiler.compileScript(descriptor, __spreadProps(__spreadValues({}, options.script), {
// xxx
}));
return resolved;
}
genStyleCode
则处理为 import "/Users/zcy/Desktop/毕设/smart-port/src/App.vue?vue&type=style&index=0&lang.less"
后续文章中会介绍为什么这个东西是怎么解析的,本文不会过多讲解。
经过上面代码分析:可以看出核心处理方法为:
options.compiler.compileTemplate、
options.compiler.compileScript、
options.compiler.rewriteDefault、
options.compiler.parse
此时再看 options.compiler
这个对象是不是很眼熟呢?这个就是在 buildStart 中 获取到的 vue/compiler-sfc
核心包源码如下:
options.compiler = options.compiler || resolveCompiler(options.root);
因此我们要看懂到底是怎么解析 vue 组件的,就需要深入这个包中。
四、@vue/compiler-sfc 核心包
parse 方法
我们在 options.compiler.parse 方法调用处打上断点,然后我们逐层进入方法:
我们进入到了 parse 方法这边可以看到有一个 ast 的转化,如下图调用了 compiler.parse 方法,经过分析发现这个方法源于,@vue/compiler-dom ,从这个包的依赖看出并没有使用 babel,略过这个核心包,因此得出结论 parse 方法中没有使用 babel。
compileScript、rewriteDefault 方法
继续刚才操作我们看 compileScript 方法:
顺腾摸瓜找到 parser$2
这个变量的源头
var parser$2 = require('@babel/parser');
rewriteDefault 也是如此:
因此得出结论在 script 的解析中会使用 @babel/parse。下面为解析后的源码:
import { ref } from 'vue';
const _sfc_main = {
setup(__props, { expose }) {
expose();
const state = ref(1)
const __returned__ = { state, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
}
compileTemplate 方法
同样使用了 @vue/compiler-dom 进行转化,具体转化细节就不进行详细展开了, 结果会转化为一个 render 函数如下:
import { resolveComponent as _resolveComponent, createVNode as _createVNode, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { id: "nav" }
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_router_link = _resolveComponent("router-link")
const _component_router_view = _resolveComponent("router-view")
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", _hoisted_1, [
_createVNode(_component_router_link, { to: "/login" }),
_createVNode(_component_router_link, { to: "/" }),
_createElementVNode("div", null, _toDisplayString($setup.state), 1 /* TEXT */)
]),
_createVNode(_component_router_view)
], 64 /* STABLE_FRAGMENT */))
}ƒ
五、整体架构
六、总结
本文只是出于好奇浅浅研究下了一下 vue3 的编译过程,和其中涉及到 babel 使用的点,对于很多细节没有深入研究,因此只做了一个简单的分析,希望对大家有参考价值。
参考资料
- 原来 vue3 文件编译是这样工作的!看完后更懂vue3了