前言
Vite 法语意为“快速的”,是一种前端构建工具,功能同 webpack、rollup 和 esbuild(这三者都是针对JavaScript 模块打包的工具),但仍是基于 rollup 和 esBuild 构建的。两个地址:
- 官网地址: cn.vitejs.dev/
- 源码地址: github.com/vitejs/vite
将源码下载到本地,开始学习~
整体梳理
从 package.json 文件知道项目名称 vite-monorepo,也暗含该项目使用 monorepo 方式来管理之意,其核心理念:一个仓库(repo)管理多个模块/包(package),模块/包间相互独立,单独管理(关于 monorepo 管理项目的优缺点可详细了解)。
该项目管理一个文档(docs,由 vitepress (静态站点生成器)搭建的文档站点)和六个模块(一个脚手架(create-vite)、一个构建工具(vite)和四个插件(plugin-legacy、 plugin-react、plugin-vue 和 plugin-vue-jsx),置于 packages目录下)。
模块名称 | 模块描述 | 打包工具 | Node内置模块 | 第三方模块 |
---|---|---|---|---|
create-vite | 脚手架 | unbuild | fs和 path | cross-spawn、kolorist、minmist 和prompts |
vite | 前端构建工具 | rollup | perf_hooks、fs、path和readline 等 | esbuild、rollup、cac、chokidar、picocolors、dotenv 等 |
plugin-legacy | 给传统浏览器提供支持 | unbuild | path、module、url和crypto 等 | vite、rollup 和 magic-string 等 |
plugin-react | 提供完整的 React 支持 | unbuild | path | vite和magic-string 等 |
plugin-vue | 提供 Vue3 单文件组件支持 | unbuild | path | vite 和 rollup 等 |
plugin-vue-jsx | 提供 Vue3 JSX 支持(通过专用的 Babel 转换插件) | unbuild | path | vite、@vue/babel-plugin-jsx和 @babel/core 等 |
下面进行单个模块的源码学习~
create-vite
create-vite 是搭建 vite 项目的脚手架(cli)
,脚手架命令 create-vite(由 package.json 文件 bin 字段指出), 其入口地址 index.js,通过一系列与用户的交互最终生成一个vite项目,供用户后续项目开发。过程中,使用 minimist
处理命令行参数 、promots
与用户交互、kolorist
将颜色放入标准输入/标准输出的库(即给 console.log 输出加颜色)及如果自定义脚手架框架时使用 cross-spawn 执行相应命令。
通过该源码的学习,可以了解到 自定义命令的执行过程
以及 node 与用户的交互
和 格式化输出
,可以看下笔者前面写的两篇文章:
命令 create-vite my-project --template vue
执行过程大致如下:
使用了 unbuild
构建工具(基于 rollup)进行打包,其默认配置文件 build.config.ts
,通过执行命令 yarn prepublishOnly
来打包,打包后项目根目录会生成一个 dist 目录,里面两个文件
- index.cjs:commonJs
- index.mjs:esm
项目入口文件 index.js 代码:
#!/usr/bin/env node
import './dist/index.mjs'
vite
vite 是基于 rollup 和 esbuild 的前端构建工具。esbuild 是 go 语言编写,而 go语言是 C 语言开发的,所以 esbuild 的构建速度极快,在 vite 的构建过程中负责编译和预编译工作,而 rollup 用来打包。
命令 vite 的入口文件 bin/vite.js,而入口文件引入并执行了 src/node/cli.ts,该文件使用 cac
创建了四个命令:
- serve/dev:默认命令,开启本地服务;
- build:构建打包,使用 rollup;
- optimize:依赖优化;
- preview:预览
执行这四个命令时,首先要做的是获取配置数据,主要分两部分:
- inlinConfig 命令行配置参数;
- userConfig(可通过 --config 显示指定配置文件,也可用官方提供的默认配置文件 vite.config.*)
// serve/dev
const config = await resolveConfig(inlineConfig, 'serve', 'development')
// build
const config = await resolveConfig(inlineConfig, 'build', 'production')
// optimize
const config = await resolveConfig(
{
root,
base: options.base,
configFile: options.config,
logLevel: options.logLevel
},
'build',
'development'
)
// preview
const config = await resolveConfig(inlineConfig, 'serve', 'production')
而 resolveConfig 方法作用:聚合这两部分的配置数据。
vite的工作机制:用户向vite请求资源时,vite内部开启了一个开发服务器vite devServer,在这个服务器内部有个工作机制,相当于一个流水线(插件)。插件又分为系统插件(vue)和用户插件。用户请求过来必定经过所有的插件,去处理一下。这就有点向过滤器、中间件。将请求所需的资源进行处理,处理完成之后把转换的结果返回给用户,得到用户想要的东西。
vite特点:请求一个页面,显示的就是这个页面的内容,无需加载和页面无关的东西。这就意味着,以后的项目不管多大,它的加载速度都是一样的。
插件的形式可以是对象也可以是一个函数:包含插件名称、相关钩子函数(在vite工作的时间节点去执行)。
未完待续~
vite 插件
前言
vite 插件基于 rollup 插件,vite 的构建过程中对插件的解析,先将插件进行排序,后执行。插件的执行有两种顺序:
(1)首先会根据enforce(取值范围和顺序:pre, normal, post)对插件排序得到 userPlugins;
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)
(2)根据插件内部的config钩子对userPlugins进行排序得到完整的插件并执行。
*// run config hooks***
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
for (const p of getSortedPluginsByHook('config', userPlugins)) {
const hook = p.config
const handler = hook && 'handler' in hook ? hook.handler : hook
if (handler) {
const res = await handler(config, configEnv)
if (res) {
config = mergeConfig(config, res)
}
}
}
插件的一些认识
-
插件的
定义
有两种方式:(1)对象;(2)函数,其中函数可接收参数。 -
插件的
编写
主要涉及两部分:(1)name:插件名称,用来定位;(2)钩子函数。按需应用:默认情况下插件在开发(serve)和构建(build)模式中都会调用,如果插件只需要在预览或构建期间有条件地引用,请使用apply属性指明它们仅在build或serve模式时调用,同时还可使用函数来进行更精准的控制。 -
插件的
使用
:在配置文件(通常是 vite.config.js 或 vite.config.ts)的plugins字段里插入。 -
vite 插件常用
钩子函数
(可细看vite插件API文档):(1)config(config) {...} 修改配置参数;
(2)configResolved(config) {...} 配置确认,这里不可修改配置参数;
(3)configServer(server) {...} 可以获取到 sever 实例,用于配置 dev server;
(4)buildStart(opt) {...} 每次构建的时候调用,用来获取打包的输入配置;
(5)resolved(source) {...} 从哪里引用,用来自定义解析行为。入参:source(解析的模块的名称)。返回值:返回字符串:一般是报名,作为id,传给load钩子函数;返回null:不予处理;返回false:说明这个包配置了external参数,不进行打包;
(6)load(id, opt) {...} 怎么加载,返回值:字符串(作为模块的code,可以用来在构建时修改模块的代码);null(什么都不做,延顺给下一步);对象({code,map,...})
(7)transform(code, id, opt) {...} 怎么转换,入参 code 是JSON 字符串(load钩子函数返回的值,默认情况下是模块的原始文本),返回值:字符串(代替code,传递给下一个);null(什么都不做,给下一个函数);对象({code, ast, ...});
上面入参id是请求的资源路径或导入第三方模块名称.
-
vite 官方维护了四个插件: plugin-legacy、plugin-react、plugin-vue 和 plugin-vue-jsx,下面我们来看看插件是如何实现和引用的,以及钩子函数是如何使用的~
plugin-vue
首先来看 plugin-vue,看到这个插件,首先考虑它实现了什么功能?如何实现的?带着问题去学习会事半功倍。
在真实 vue3 项目中,配置文件plugins中引入了该插件:
import { defineConfig } from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
export default defineConfig({
...,
plugins: [
vuePlugin, // 使用plugin-vue插件
...
]
})
小测一下,在配置文件的plugins注释掉vuePlugin,看到终端报错:
需要插件 plugin-vue 来处理 .vue 文件,因为无法识别 .vue 文件里的 template 和s cript 标签(如果有样式还会有 style 标签),得知该插件是来解析 .vue 文件里的 template 和 script ,以及 style 的,从而让 vite 能够识别。
那么问题来了,plugin-vue 是如何处理 .vue 文件的呢?
在看这个问题的过程中,发现用户开发的插件有两种形式:
(1)处理文件的,例如plugin-vue处理 .vue 文件,使用时只需一步:
- 在配置文件的plugins字段中插入该插件,这种情况下在插件编写过程中的id是当前请求的文件所在的绝对路径,通过文件路径进行接管的筛选。
例如:识别App.vue自定义块i18n插件vite-plugin-i18n
App.vue代码如下:
<template>...</template>
<script>...</script>
<style>...</style>
<i18n>
{
"en": {"language": "Language", "hello": "hello, world!"},
"zh": {"language": "语言","hello": "你好,世界!"},
}
</i18n>
编写插件 vite-plugin-i18n 的主要代码:
export default {
transform(*code*, *id*) { // id是请求的资源路径
if(!/vue&type=i18n/.test(*id*)) { // 对资源文件进行筛选
return null
}
return `export default Comp => {
Comp.i18n = ${*code*}
}`
}
}
使用该插件,只需在配置文件中plugins插入即可
import vitePluginI18n from './plugins/vite-plugin-i18n'
export default defineConfig({
...,
plugins: [..., vitePluginI18n,]
})
这样就可以识别App.vue自定义的块i18n,并且可通过getCurrentInstance获取当前实例从而拿到i18n里的值。
(2)处理第三方导入的插件,使用时需要二步:
- 在配置文件的plugins字段中插入该插件;
- 在需要使用该插件的文件中导入该插件,例如: import virtualModule from ‘virtual-module’,插件编写过程中的id是‘virtual-module’,据此来进行截关的筛选。
例如:自定义第三方模块my-module:首先根据create-vite创建一个vue项目,在项目根目录下创建plugins文件夹,并新建plugins/vite-plugin-example.js文件,代码如下:
const virtualModuleId = 'my-module-2'
export default function(*options*={}) {
return {
name: "my-module", // 插件名称, 在使用时不一定导入是这个name值
resolveId(id) { // id是在使用插件导入的模块名称
if(id === virtualModuleId) {
return virtualModuleId // 表示接管, vite不再询问其他插件处理该id请求
}
return null // 返回null表明是其他id要继续处理
},
load(id) { // id是在使用插件导入的模块名称
if(id === virtualModuleId) {
return 'export default "哈喽, 露水晰!"' // 返回加载模块代码
}
return null // 其他id继续处理
},
}
}
在配置文件中plugins插入该插件:
import vitePluginExample from './plugins/vite-plugin-example'
export default defineConfig({
...,
plugins: [..., vitePluginExample(),]
})
在main.js中引入并使用:
import myModule from 'my-module-2'
console.log("msg:", myModule) // msg: 哈喽, 露水晰!
注意:import的from后的值只要与插件中的virtualModuleId值保持一致即可。
针对这两种形式的插件,官方提供了两个示例:
网上一篇文章写的不错:segmentfault.com/a/119000004…
plugin-vue 解析 main.js 过程
入口文件:main.js,该文件导入了 App.vue(import App from '/src/App.vue'),因后缀是vue则进入插件plugin-vue:
(1)因当前文件(App.vue)的 query.vue不存在,进入transform 处理,通过 transformMain,使用vue/complile-sfc 根据 SFC 得到 SFCDescriptor(使用了compiler.parse方法)并存入 cache(new Map(),以便二次直接取用);
(2)根据 SFCDescriptor 将原 SFC 转为 JavaScript(编译 script、template、style 和 customBlock 使其各自转译为对应的 js,最后输出 App.vue 的内容顺序:scriptCode、templateCode、styleCode、customBlocksCode等),得到一个 JavaScript 模块:
// App.vue
...script-code...
...template-code(_sfc_render)...
import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css"
import block0 from "/src/App.vue?vue&type=i18n&index=0&lang.i18n"
if (typeof block0 === 'function') block0(_sfc_main)
...
(3)解析 App.vue,过程中有样式/自定义块的导入(例如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css"等),再次进入插件 plugin-vue 处理,通过 load 得到对应的请求资源:
// plugin-vue
load(id, opt) { // id是文件路径(包含文件名称)
const ssr = opt?.ssr === true;
if (id === EXPORT_HELPER_ID) {
return helperCode;
}
const { filename, query } = parseVueRequest(id);
if (query.vue) { // 只有在编译后的vue资源请求后进来
if (query.src) { // 如果是外部链接,则直接读取链接的内容
return fs.readFileSync(filename, "utf-8");
}
// 这里是拿到第一次解析的App.vue的SFCDescriptor,第一次拿到后存入到了cache
const descriptor = getDescriptor(filename, options);
let block;
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
};
}
}
},
未完待续~