用vite初始化vue3项目,我们会发现vite启动快,因为它不需要打包,你肯定也会好奇vue文件中的各部分怎么被解析的,这时就需要运用官方提供的一些核心插件来解析vue文件,如@vitejs/plugin-vue插件,用来解析vue文件,一般都是函数调用,可以传入配置项,返回插件在plugins数组中。
项目中每次出现import,浏览器就会发送请求到devServer,就会通过vite.config.ts中添加的插件来解析文件、处理配置项及优化打包等,可以更好的扩展vite能力。
示例
vite 插件应该带 vite-plugin- 前缀,vite-plugin-vue- 前缀作为 vue 插件,接下来先通过简单的示例来实现一下插件,其中会涉及一下钩子函数,如
resolveId用于命中第三方依赖load加载函数,可返回自定义的内容、
// my-plugin
export default function virtualModule() {
const virtualModuleId = 'virtual-module';
return {
name: 'virtual-module', // 必须的,将会在 warning 和 error 中显示
resolveId(id) { // 命中第三方依赖,执行load加载方法
if (id === virtualModuleId) {
return virtualModuleId;
}
return null; // 返回null表明是其他id要继续处理
},
load(id) {
if (id === virtualModuleId) {
return `export const msg = "this is virtual module"`
}
return null;
}
}
}
// main.ts
import {msg} from 'virtual-module';
console.log(msg) // 输出: this is virtual module
了解了插件的写法后,去看看plugin-vue插件的运行过程中插件钩子都起了哪些作用:
@vitejs/plugin-vue
在开发过程中,总会想.vue文件中的template、script、style是经过哪些处理被浏览器识别的呢。上面的示例知道插件导出配置对象,具体的解析细节还得去源码中寻找,对应每个钩子中的处理函数。
插件钩子执行顺序如下:
config=> 解析vite配置前调用可以修改vite配置configResolved=> 解析vite配置后调用,不可修改,读取配置进行操作options=> 打包时,可以修改rollup选项,如果插件不是用于打包,不会用到configureServer=> 是用于配置dev server。在 connect 中添加自定义中间件。例如mock请求数据。biuldStart=> 开始构建transformIndexHtml=> 可以注入或者删除index.html中的内容resolvedId=> 用于命中第三方依赖load=> 加载函数,可返回自定义的内容transform=> 对已经加载的模块内容进行修改buildEnd=> 结束构建
接下来按照运行顺序看看都执行了哪些操作。
config、configResolved
config(config) {
return {
define: {
__VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
__VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false
},
ssr: {
external: ['vue', '@vue/server-renderer']
}
}
},
configResolved(config) {
options = {
...options,
root: config.root,
sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
isProduction: config.isProduction
}
},
config中返回的配置会与vite配置合并,configResolved在options中添加根目录root及isProduction属性,以便提供给后续的钩子函数使用。
configureServer
configureServer(server) {
options.devServer = server
}
添加devServer服务
biuldStart
configureServer(server) {
options.compiler = options.compiler || resolveCompiler(options.root)
}
添加编译方法,涉及到一些编译的内置方法
load
点击链接,浏览器发起请求时,
vite会处理文件在其路径上添加?vue,并通过参数&type来区分类型,如template(模板)、script(js 脚本)、css等,进入load钩子,根据不同类型进行编译处理。
在钩子函数中通过
parseVueRequest方法获取文件名及携带参数。
export function parseVueRequest(id: string): {
filename: string
query: VueQuery
} {
const [filename, rawQuery] = id.split(`?`, 2)
const query = qs.parse(rawQuery) as VueQuery
if (query.vue != null) {
query.vue = true
}
if (query.index != null) {
query.index = Number(query.index)
}
if (query.raw != null) {
query.raw = true
}
return {
filename,
query
}
}
通过文件目录解析编译获取文件中不同模块,返回文件中对应的template、script、style模块,加载完成.
load(id, opt) {
...
const { filename, query } = parseVueRequest(id)
if (query.vue) {
if (query.src) {
return fs.readFileSync(filename, 'utf-8')
}
const descriptor = getDescriptor(filename, options)!
let block: SFCBlock | null | undefined
if (query.type === 'script') {
block = getResolvedScript(descriptor, ssr)
} else if (query.type === 'template') {
block = descriptor.template!
} else if (query.type === 'style') {
block = descriptor.styles[query.index!]
} else if (query.index != null) {
block = descriptor.customBlocks[query.index]
}
if (block) {
return {
code: block.content,
map: block.map as any
}
}
}
},
transform
对load返回的内容进行转译,浏览器并不识别vue文件中的组件、事件处理以及其他语法糖等,需要先将 template 模板语法解析成了 AST 语法树,根据生成的对象,createVnode创建虚拟dom,通过vue diff算法,移动或创建真实dom,完成解析。
transform(code, id, opt) {
...
if (!query.vue) {
return transformMain(
code,
filename,
options,
this,
ssr,
customElementFilter(filename)
)
} else {
...
// 拆分后的template、style转译
if (query.type === 'template') {
return transformTemplateAsModule(code, descriptor, options, this, ssr)
} else if (query.type === 'style') {
return transformStyle(
code,
descriptor,
Number(query.index),
options,
this
)
}
}
}
总结
通过plugin-vue插件的运行过程,进一步了解了vite插件,知道各钩子函数的执行顺序及运行过程,也可以在解决问题过程中尝试另一种方案。