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
中完整的插件工作流程