Vite 深入浅出

Vite是构建工具的高阶封装,它的核心是Rollup和Esbuild等构建工具
静态资源处理
资源引入方式
Vite会自动处理以下几种类型的静态资源, 在生产构建的时添加文件指纹hash
- 在 HTML 或者 JSX 中,通过元素标签来加载资源,如:
<img src="../assets/image.png"></img>
<video src="../assets/surround.mp4"></video>
<audio src="../assets/AudioTest.ogg"></audio>- 在 CSS 中使用静态路径,如:
background-url: url('../assets/image.png') norepeat;- 在 JavaScript 中,通过脚本的方式动态指定使用静态资源,如:
// 通过import会自动处理文件hash
import img from '../assets/image1.png'
// 通过运行时import也能自动处理文件hash
import('../assets/image2.png')
// 直接将静态URL赋值给src也能自动处理文件hash
document.getElementById('hero-img').src = '../assets/image.png'注意: 动态拼接URL的情况
如果业务开发中, 链接URL不是静态的, 而是通过变量来动态拼接出来的, 一定要注意以下几种场景
- 如果使用静态
import全量导入资源, 假设有多个文件就要引入多次, 不够优雅 - 如果使用动态
import('../assets/${val}.png')来拼接资源URL, 会在编译后的文件中产生大量的JS模块文件, 也不够优雅 - 对于动态URL的场景下, 首选使用
new URL('../assets/${val}.png', import.meta.url)来处理
@import alias
Vite 通过 postcss-import 预配置支持了 CSS @import 内联, 也在CSS/Less/Sass中支持了@import alias
在Vite中配置好resolve.alias别名来查找模块, 然便可以在样式代码中使用下面的语法
@import '@/styles/index.css';
@import url('@/styles/index.css');注意
在Wepback体系下的打包工具中, 要使用alias, 必须在前面加上波浪号~
@import '~@styles/index.css'css modules
在Vite项目中, 只要以.module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 模块, 在使用的过程中就可以使用css modules的语法
.container {
background-color: aqua;
}<template>
<div :class="cssMod.container">
Hello World
</div>
</template>
<script setup>
import cssMod from '@/styles/index.module.css';
</script>TS集成
在Typescipt集成的过程中, 需要注意以下问题
只编译不校验
Vite只做ts的编译, 并不做ts的类型校验。因此需要借助IDE和tsc --noEmit来做校验, 如果是vue项目使用vue-tsc --noEmit
配置文件
在Vite的项目中, ts配置文件tsconfig.json有几个关键选项必须开启
Vite项目默认所有模块都是ESM, 因此必须开启"module": "ESNext"如果是在
Vue项目中使用Vite, 同时Vue中使用jsx语法, 那必须开启"jsx": "preserve"和"jsxImportSource": "vue"
注意
从 Vue 3.4 开始,Vue 不再隐式注册全局JSX命名空间。要指示TypeScript 使用 Vue 的 JSX 类型定义,请确保在您的配置文件tsconfig.json中包含以下内容"jsxImportSource": "vue"
- 必须开启
"isolatedModules": true,Vite在编译时只会编译单个文件中的ts语法变成js, 不会读取该文件所关联的其他ts模块的信息, 导致很多ts的功能无法使用
开启后能解决以下问题
- 在
a.ts导入的外部类型A, 编译export { A }后的代码会报错。开启该选项后, 可以提前给出提示 - 开启后会在使用
const enum Num语法时提示报错。 因为Vite使用esbuild编译ts,编译后代码不会删除枚举的变量Num.flag, 却删除了枚举的定义ts代码, 导致报错。本质是设计上不支持const enum Num语法 - 开启
isolatedModules后, 要求所有单个ts文件是一个模块
依赖预构建
自动重构建
Vite 将预构建的依赖项缓存到 node_modules/.vite 中。一旦以下选项的内容发生变化, 就会导致缓存失效, 再次重新运行预构建
- 包管理器的锁文件内容,例如
package-lock.json,yarn.lock,pnpm-lock.yaml,或者bun.lockb - 补丁文件夹的修改时间
vite.config.js中的相关字段NODE_ENV的值
手动重构建
- 手动删除
node_modules/.vite缓存目录 - 使用
--force选项, 例如pnpm dev --force
包导入导出策略
在 Node支持ESM之后的版本(>=12.20)中, 有以下两种使用原生 ESM的方式:
- 代码文件以
.mjs结尾 package.json中声明type: "module"
当 Node 在导入导出这些ESM版本的Npm包时, 过程是较为复杂的, 其中关键字段均在package.json中定义
条件导出
在Node中, 如果使用ESM导入Npm模块, 需要看该Npm包中package.json的核心字段main和exports。且Node官方规定了在两者同时出现的场景下, exports 的优先级会比 main 更高, 最终将Npm模块的入口文件确定下来
main
{
"name": "package-a",
"main": "./dist/index.js"
}exports
在使用exports导出的场景下, 主要包含默认导出、子路径导出和条件导出
// package.json
{
"name": "package-a",
"type": "module",
"exports": {
// 默认导出,使用方式: import a from 'package-a'
".": "./dist/index.js",
}
}// package.json
{
"name": "package-a",
"type": "module",
"exports": {
// 子路径导出,使用方式: import d from 'package-a/dist'
"./dist": "./dist/index.js",
"./dist/*": "./dist/*", // 这里可以使用 `*` 导出目录下所有的文件
}
}// package.json
{
"name": "package-a",
"type": "module",
"exports": {
// 条件导出,区分 ESM 和 CommonJS 引入的情况
"./main": {
"import": "./main.js",
"require": "./main.cjs"
},
}
}其中, 条件导出的情况最为复杂, 因为它的导出key关键字可以分为以下场景
node: 在Node.js环境下适用,可以定义为嵌套条件导出import: 用于import方式导入的情况,如import("package-a")require: 用于require方式导入的情况,如require("package-a")default: 兜底方案,如果前面的条件都没命中,则使用default导出的路径types: 该导出可由typescript用来解析给定的类型定义文件。此条件应始终首先包含在内browser: 任何浏览器环境development: 可用于定义仅开发环境入口点,例如提供额外的调试上下文,例如在开发模式下运行时提供更好的错误消息。必须始终与 互斥"production"production: 可用于定义生产环境入口点。必须始终与 互斥"development"
{
"main": "./dist/node/index.js",
"types": "./dist/node/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/node/index.d.ts",
"default": "./dist/node/index.js"
},
"require": {
"types": "./index.d.cts",
"default": "./index.cjs"
}
},
"./client": {
"types": "./client.d.ts"
},
"./dist/client/*": "./dist/client/*",
"./types/*": {
"types": "./types/*"
},
"./package.json": "./package.json"
}
}{
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts",
"unpkg": "dist/vue.global.js",
"jsdelivr": "dist/vue.global.js",
"files": [
"index.js",
"index.mjs",
"dist",
"compiler-sfc",
"server-renderer",
"macros.d.ts",
"macros-global.d.ts",
"ref-macros.d.ts"
],
"exports": {
".": {
"import": {
"node": "./index.mjs",
"default": "./dist/vue.runtime.esm-bundler.js"
},
"require": "./index.js",
"types": "./dist/vue.d.ts"
},
"./server-renderer": {
"import": "./server-renderer/index.mjs",
"require": "./server-renderer/index.js"
},
"./compiler-sfc": {
"import": "./compiler-sfc/index.mjs",
"require": "./compiler-sfc/index.js"
},
"./dist/*": "./dist/*",
"./package.json": "./package.json",
"./macros": "./macros.d.ts",
"./macros-global": "./macros-global.d.ts",
"./ref-macros": "./ref-macros.d.ts"
}
}{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}条件导入
Npm包的条件导入由package.json 中的 imports字段来定义, 该字段的值是一个对象, 其中的key是导入的别名, value是导入的路径
这样相当于实现了路径别名的功能,不过与构建工具中的 alias 功能不同的是,"imports" 中声明的别名必须全量匹配
{
"imports": {
// key 一般以 # 开头
// 也可以直接赋值为一个字符串: "#dep": "lodash-es"
"#dep": {
"node": "lodash-es",
"default": "./dep-polyfill.js"
},
},
"dependencies": {
"lodash-es": "^4.17.21"
}
}// index.js
// 在执行的时候会将`#dep`定位到`lodash-es`这个第三方包,当然,你也可以将其定位到某个内部文件
import { cloneDeep } from "#dep";
const obj = { a: 1 };
// { a: 1 }
console.log(cloneDeep(obj));Vite 插件开发
Vite的插件本质上是受限制的rollup插件
插件命名
如果需要同时支持
Vite和Rollup,那么插件的命名建议以rollup-plugin-开头,例如rollup-plugin-alias否则只支持
vite,那么插件的命名必须以vite-开头,例如vite-virtual-module。 因为在Vite中有自有的钩子,将不再支持rollup
虚拟模块
作为构建工具,一般需要处理两种形式的模块,一种存在于真实的磁盘文件系统中,另一种并不在磁盘而在内存当中,也就是虚拟模块。通过虚拟模块,我们既可以把自己手写的一些代码字符串作为单独的模块内容,又可以将内存中某些经过计算得出的变量作为模块内容进行加载,非常灵活和方便
下面来实现一个简单的虚拟模块插件,首先准备一个脚手架搭建的基本项目,可以使用作者的demos(vite-vue3)
在src/plugins目录下创建一个virtual-module.ts文件, 作为虚拟模块的入口文件
// 这个模块用于来计算数组的和,
// plugins/virtual-plugin-calc.ts
import type { Plugin, ResolvedConfig } from 'vite';
// 虚拟模块名称
const virtualSumModuleId = 'virtual:calc';
// 在内部,使用了虚拟模块的插件在解析时应该将模块 ID 加上前缀 \0,这一约定来自 rollup 生态
const resolvedSumVirtualModuleId = '\0' + virtualSumModuleId;
export default function virtualSumModulePlugin(): Plugin {
let config: ResolvedConfig | null = null;
return {
name: 'vite-plugin-virtual-calc',
resolveId(id) {
if (id === virtualSumModuleId) {
return resolvedSumVirtualModuleId;
}
},
load(id) {
// 加载虚拟模块
if (id === resolvedSumVirtualModuleId) {
return 'export default function sum(arr, i = 0) { return i >= arr.length ? 0 : arr[i] + sum(arr, i + 1); }';
}
}
}
}// vite.config.ts
import virtual from './plugins/virtual-module.ts'
// 配置插件
{
plugins: [react(), virtual()]
}import { createApp } from 'vue'
import './assets/styles/style.css'
import App from './App.vue'
import sum from "virtual:calc"
console.log('%c------>[LOG:]', 'color: fuchsia', sum([1,3,5,7,9])) // 25
createApp(App).mount('#app')// 全局添加虚拟模块的类型声明
declare module 'virtual:*' {
const component: any;
export default component;
}兼容的Rollup钩子
注意
在vite.config.ts配置文件的plugins中, 传入Rollup的插件, 仅会执行下面几个兼容的钩子, 其他钩子不会执行
如果需要配置符合Rollup执行方式的插件钩子, 请单独配置到vite.config.ts配置文件的选项build.rollupOptions.plugins中, 以达到更好的兼容性
特别注意, 开发环境中moduleParsed钩子不会被调用
- 服务启动时: 仅会调用一次
options、buildStart这两个钩子, 文件变化时不会执行, 跟文件更新无关 - 每个传入模块请求时:
resolveId、load、transform三个代码完整编译的钩子 - 服务关闭时:
buildEnd、closeBundle
开发Rollup插件的过程中, 如果需要兼容Vite, 那么必须满足以下条件
- 没有使用
moduleParsed钩子 - 打包钩子和输出钩子之间没有很强的耦合(即输出阶段非常依赖
build阶段的执行), 因为Vite在开发阶段, 只会执行build阶段的钩子
插件执行时机
- pre: 在
Rollup使用alias后就会被执行 - normal: 核心插件执行之后,
build执行构建之前, 代码编译未开始 - post:
build构建之后
独有钩子
config多个插件实现这个钩子, 配置项将被深度合并到现有配置中的部分配置对象configResolved使用这个钩子能够读取和存储最终解析的配置, 即多个config合并结果configureServer配置本地开发服务器的钩子, 比如可以自定义前置中间件或者后置中间件configurePreviewServer配置本地预览服务器的钩子transformIndexHtml接收入口的HTML字符串和转换上下文的配置钩子handleHotUpdate执行自定义 HMR 更新处理
部分配置项
这块内容主要列出一些重要的配置项, 这些配置项易忘易错, 但是又很重要
resolve.dedupe
在Vite中,resolve.dedupe选项可以帮助我们消除重复的模块依赖,并提升构建性能。它可以在resolve.alias前对模块路径进行处理,将同一模块的不同版本定位到相同的地址,避免同一模块的多次加载。
使用方法:
// vite.config.ts
export default defineConfig({
// ...省略其他选项
resolve: {
// package1和package2表示需要去重的包名
dedupe: ['package1', 'package2']
}
})esbuild.jsxInject
通过 esbuild.jsxInject 来自动为每一个被 esbuild 转换的文件注入 JSX helper。这征对React项目特别有用,在React项目中很多地方都需要导入React,配置这个选项后, 会自动帮我们导入
export default defineConfig({
esbuild: {
jsxInject: `import React from 'react'`,
},
})json.namedExports
该选项默认开启,用于支持从 .json 文件中进行按名导入
import { name, version } from 'package.json'json.stringify
默认不开启为false,导入的 JSON 会被转换为export default JSON.parse("...")用来得到更好的性能。 因此无法对.json进行具名导入了。所以Vite会将json.namedExports关闭
assetsInclude
该选项指定其他文件类型作为静态资源处理, 默认只处理以下文件类型
export const KNOWN_ASSET_TYPES = [
// images
'apng',
'png',
'jpe?g',
'jfif',
'pjpeg',
'pjp',
'gif',
'svg',
'ico',
'webp',
'avif',
// media
'mp4',
'webm',
'ogg',
'mp3',
'wav',
'flac',
'aac',
'opus',
'mov',
'm4a',
'vtt',
// fonts
'woff2?',
'eot',
'ttf',
'otf',
// other
'webmanifest',
'pdf',
'txt',
]assetsInclude选项值是采用picomatch匹配规则, 具体如下
| 通配符 | 描述 |
|---|---|
* | 匹配零次或多次任何字符,不包括路径分隔符。不匹配路径分隔符或隐藏文件或目录(“点文件”),除非通过将 dot 选项设置为 true 显式启用 |
** | 匹配零次或多次任何字符,包括路径分隔符。请注意, ** 仅在路径段中仅包含路径分隔符( / 和 \\(windows下的路径分隔符))时才匹配路径分隔符。因此, foo**/bar 等效于 foo*/bar , foo/a**b/bar 等效于 foo/a*b/bar ,并且 glob 路径段中连续两个以上的星号被视为单个星号。因此, foo/***/bar 等效于 foo/*/bar |
? | 匹配一次不包括路径分隔符的任何字符。不匹配路径分隔符或前导点 |
[abc] | 匹配方括号内的任何字符。例如, [abc] 将匹配字符 a 、 b 或 c ,以及其他任何内容 |
envDir
默认目录为根目录root,这个配置用于设置.env环境变量(.env.development, .env.production, .env.test.local)的目录
envPrefix
以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。
如果需要在应用(业务)代码中暴露不含前缀VITE_的变量,只能使用define配置
export default defineConfig({
define: {
'import.meta.env.ENV_VARIABLE': JSON.stringify(process.env.ENV_VARIABLE)
}
})构建选项主要是用于配置Vite的构建行为, 包含了各种资源的打包处理
build.cssTarget
此选项允许用户为 CSS 的压缩设置一个不同的浏览器 target,此处的 target 并非是用于 JavaScript 转写目标。
应只在针对非主流浏览器时使用。 最直观的示例是当你要兼容的场景是安卓微信中的 webview 时,它支持大多数现代的 JavaScript 功能,但并不支持 CSS 中的 #RGBA 十六进制颜色符号。 这种情况下,你需要将 build.cssTarget 设置为 chrome61,以防止 vite 将 rgba() 颜色转化为 #RGBA 十六进制符号的形式
build.dynamicImportVarsOptions
Vite官网中提到这个选项是用于传递给@rollup/plugin-dynamic-import-vars插件的,那么这个插件是干什么的?
@rollup/plugin-dynamic-import-vars
这个插件用于在动态引用表达式中,例如import('src/assets/${imgName}')。该插件会将这种运行时才能处理的导入解析为一个通配符路径,然后提前预处理资源的加载
比如下面的例子:
`./locales/${locale}.js` ===转化为===> './locales/*.js'更多关于该插件的内容请查看@rollup/plugin-dynamic-import-vars
掘金也有相关的@rollup/plugin-dynamic-import-var源码分析
build.lib
将Vite修改为库模式。
entry 是必需的,因为库不能使用 HTML 作为入口。name 则是暴露的全局变量,并且在 formats 包含 'umd' 或 'iife' 时是必需的。默认 formats 是 ['es', 'umd'],如果使用了多个配置入口,则是 ['es', 'cjs']。fileName 是输出的包文件名,默认 fileName 是 package.json 的 name 选项,同时,它还可以被定义为参数为 format 和 entryAlias 的函数。
与rollup打包库的比较
总的来说,两者都可以打包库,但Vite的集成化更加好一点,配置更简单一些。
与 Vite 相比,使用 Rollup 的一个优点是工具的复杂性和依赖性可能更低。比如您不需要开发服务器并且构建过程很简单,那么使用 Rollup 更好
optimizeDeps
这块内容主要是用于配置Vite的预构建依赖项, 也就是Vite在启动时, 会将这些依赖项提前构建好, 以提升启动速度
optimizeDeps.entries
Vite会将optimizeDeps.entries中的依赖项提前构建好, 以提升启动速度
optimizeDeps.esbuildOptions
在依赖扫描和优化过程中传递给 esbuild 的选项, 与Vite冲突的external选项将被忽略, plugins与Vite的dep插件合并
optimizeDeps.force
设置为 true : 强制开启新的依赖预构建,而忽略之前已经缓存过的、已经优化过的依赖
optimizeDeps.needsInterop
Vite进行依赖性导入分析,它会对需要预编译且为 CommonJS 的依赖导入代码进行重写
因为Vite是以ESM的方式运行的,但是很多第三方库都是以CommonJS的方式导出的,所以需要对这些库进行重写,且CommonJS并不支持命名方式的导出。
举个例子,当我们在 Vite 项目中使用 react 时:
import React, { useState, createContext } from 'react'
// 此时 react 就需要配置 needsInterop 为 true,对代码进行重写以便支持ESM
import $viteCjsImport1_react from "/@modules/react.js";
const React = $viteCjsImport1_react;
const useState = $viteCjsImport1_react["useState"];
const createContext = $viteCjsImport1_react["createContext"];Rollup
Vite是基于Rollup封装的高阶构建工具, 那么本段来记录一下Rollup的各种用法及插件开发
Rollup
Rollup是以ESM标准为目标的构建工具,默认只识别ESM模块文件,对于Commonjs的模块文件,默认是不支持的,只能使用插件来提供支持
插件顺序
Rollup配置多个plugins的情况下, 插件默认是从前往后执行, 因此项目中的插件必须配置一定的顺序。如果后续的插件依赖前面的插件, 需要注意先后顺序。一般的工程中,都会使用commonjs和resolve等等插件, 所以这几个插件的顺序如下
// resolve让项目支持使用node_modules中的模块
import resolve from '@rollup/plugin-node-resolve';
// rollup使用的是es6的模块化, 插件commonjs能够让项目能够支持使用cjs源码的npm库
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'main.js',
plugins: [
resolve(),
commonjs()
],
output: {
file: 'bundle.js',
format: 'cjs'
}
}常用插件
下面罗列项目中常用的插件及配置, 更多插件请关注官方推荐插件列表
import path from 'path';
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';
import del from 'rollup-plugin-delete';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import replace from '@rollup/plugin-replace';
import legacy from '@rollup/plugin-legacy';
import json from '@rollup/plugin-json';
import eslint from '@rollup/plugin-eslint';
import image from '@rollup/plugin-image';
export default {
input: './src/main.js', // 入口文件
output: {
file: './dist/bundle.js', // 打包后的存放文件
//dir: './dist', // 多入口打包必需加上的属性
format: 'cjs', // 输出格式 amd umd es cjs iife system
name: 'bundleName', // 如果iife,umd需要指定一个全局变量
sourcemap: true, // 是否开启代码回溯
banner: "/** 文件开头的声明内容、作者等信息 **/" // 打包后的文件头部添加注释
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
__buildDate__: () => JSON.stringify(new Date()),
__buildVersion: 15
}),
// 支持从node_modules引入其他的包
nodeResolve(),
typescript({
exclude: 'node_modules/**',
include: 'src/**',
}),
// 支持common.js
// 在 rollup 中引用 commonjs 规范的包。该插件的作用是将 commonjs 模块转成 es6 模块。
commonjs({
throwOnError: true,
}),
// 支持eslint
eslint(),
// 支持加载图片
image(),
// es6语法转义
babel({
exclude: 'node_modules/**',
extensions: ['.js', '.jsx'],
presets: ['@babel/preset-env', '@babel/preset-react'],
}),
// 让项目中可以导入json文件
json(),
// 支持加载css,添加前缀等
postcss(),
// 有时,您会发现旧时代的一段有用的代码片段,在像 npm 这样的新技术出现之前。
// 这些脚本通常会将自己公开为var someLibrary = ...或window.someLibrary = ...,
// 期望其他脚本将从全局命名空间获取对库的引用。
// 将它们转换为模块通常很容易。但何苦呢?您只需添加legacy插件并进行相应配置,它就会自动变成模块。
legacy({ 'vendor/some-library.js': 'someLibrary' }),
// 打包前清空目标目录
del({ targets: 'dist/*', verbose: true }),
// 压缩js
terser(),
// 启动本地服务
serve({
contentBase: '', //服务器启动的文件夹,默认是项目根目录,需要在该文件下创建index.html
port: 8020, //端口号,默认10001
}),
// watch目录,当目录中的文件发生变化时,刷新页面
livereload('dist'),
// 使用别名
alias({
entries: [{ find: '@', replacement: path.join(__dirname, 'src') }],
}),
],
// 告诉rollup不要将此lodash打包,而作为外部依赖,在使用该库时需要先安装相关依赖
external: ['react']
};插件钩子
Rollup的插件钩子主要分为两种, 即构建钩子、输出生成钩子。 这两种钩子的时序图如下
构建钩子流程图

首先经历
options钩子进行配置的转换,得到处理后的配置对象。随之 Rollup 会调用
buildStart钩子,正式开始构建流程。Rollup 先进入到
resolveId钩子中解析文件路径。(从input配置指定的入口文件开始)。Rollup 通过调用
load钩子加载模块内容。紧接着 Rollup 执行所有的
transform钩子来对模块内容进行进行自定义的转换,比如 babel 转译。现在 Rollup 拿到最后的模块内容,进行 AST 分析,得到所有的 import 内容,调用 moduleParsed 钩子:
moduleParsed判断
- 如果是普通的 import,则执行
resolveId钩子,继续回到步骤3。 - 如果是动态 import,则执行
resolveDynamicImport钩子解析路径,如果解析成功,则回到步骤4加载模块,否则回到步骤3通过resolveId解析路径。
- 直到所有的 import 都解析完毕,Rollup 执行
buildEnd钩子,Build 阶段结束。
当然,在 Rollup 解析路径的时候,即执行resolveId或者resolveDynamicImport的时候,有些路径可能会被标记为external(翻译为排除),也就是说不参加 Rollup 打包过程,这个时候就不会进行load、transform等等后续的处理了。
在流程图最上面,watchChange和closeWatcher这两个 Hook,对应了 rollup 的watch模式。当你使用 rollup --watch 指令或者在配置文件配有watch: true的属性时,代表开启了 Rollup 的watch打包模式,这个时候 Rollup 内部会初始化一个 watcher 对象,当文件内容发生变化时,watcher 对象会自动触发watchChange钩子执行并对项目进行重新构建。在当前打包过程结束时,Rollup 会自动清除 watcher 对象调用closeWacher钩子。
输出生成钩子流程图

执行所有插件的
outputOptions钩子函数,对output配置进行转换。执行
renderStart,并发执行renderStart钩子,正式开始打包。并发执行所有插件的
banner、footer、intro、outro钩子(底层用Promise.all包裹所有的这四种钩子函数),这四个钩子功能很简单,就是往打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。从入口模块开始扫描,针对动态
import语句执行renderDynamicImport钩子,来自定义动态import的内容。对每个即将生成的
chunk,执行augmentChunkHash钩子,来决定是否更改chunk的哈希值,在watch模式下即可能会多次打包的场景下,这个钩子会比较适用。如果没有遇到
import.meta语句,则进入下一步,否则:
import.meta的处理流程
- 对于
import.meta.url语句调用resolveFileUrl来自定义 url 解析逻辑 - 对于其他
import.meta属性,则调用resolveImportMeta来进行自定义的解析。
接着
Rollup会生成所有chunk的内容,针对每个chunk会依次调用插件的renderChunk方法进行自定义操作,也就是说,在这里时候你可以直接操作打包产物了。随后会调用
generateBundle钩子,这个钩子的入参里面会包含所有的打包产物信息,包括chunk(打包后的代码)、asset(最终的静态资源文件)。你可以在这里删除一些chunk或者asset,最终这些内容将不会作为产物输出。前面提到了
rollup.rollup方法会返回一个bundle对象,这个对象是包含generate和write两个方法,两个方法唯一的区别在于后者会将代码写入到磁盘中,同时会触发writeBundle钩子,传入所有的打包产物信息,包括chunk和asset,和generateBundle钩子非常相似。不过值得注意的是,这个钩子执行的时候,产物已经输出了,而generateBundle执行的时候产物还并没有输出。顺序如下图所示:

- 当上述的
bundle的close方法被调用时,会触发closeBundle钩子,到这里 Output 阶段正式结束。
注意
当打包过程中任何阶段出现错误,会触发 renderError 钩子,然后执行closeBundle钩子结束打包。
以上就是 Rollup 中完整的插件工作流程