在 Vue.js 源码中,
pnpm run build reactivity这个命令背后究竟发生了什么?为什么 Vue3 选择Rollup作为构建工具?Vite和Rollup又是什么关系?本文将深入理解Rollup的核心配置,探索 Vue3 的构建体系,并理清Vite与Rollup的渊源。
Rollup 基础配置解析
什么是 Rollup?
Rollup 是一个 JavaScript 模块打包器,它可以将多个模块打包成一个单独的文件。与 Webpack 不同,Rollup 专注于 ES 模块的静态分析,以生成更小、更高效的代码。
Rollup 的核心优势
treeShaking:基于 ES 模块的静态分析,自动移除未使用的代码- 支持输出多种模块格式(ESM、CJS、UMD、IIFE)
- 配置文件简洁直观,学习成本低
- 插件体系完善,可以处理各种场景
核心配置:input 与 output
Rollup 的配置文件通常是 rollup.config.js,它导出一个配置对象或数组:
input:入口文件配置
javascript
// rollup.config.js
export default {
// 单入口(最常见)
input: 'src/index.js',
// 多入口(对象形式)
input: {
main: 'src/main.js',
admin: 'src/admin.js',
utils: 'src/utils.js'
},
// 多入口(数组形式)
input: ['src/index.js', 'src/cli.js']
};
output:输出配置
output 配置决定了打包产物的形式和位置:
javascript
export default {
input: 'src/index.js',
// 单输出配置
output: {
file: 'dist/bundle.js', // 输出文件
format: 'esm', // 输出格式
name: 'MyLibrary', // UMD/IIFE 模式下的全局变量名
sourcemap: true, // 生成 sourcemap
banner: '/*! MyLibrary v1.0.0 */' // 文件头注释
},
// 多输出配置(数组形式,输出多种格式)
output: [
{
file: 'dist/my-lib.cjs.js',
format: 'cjs' // CommonJS,适用于 Node.js
},
{
file: 'dist/my-lib.esm.js',
format: 'es' // ES Module,适用于现代浏览器/打包工具
},
{
file: 'dist/my-lib.umd.js',
format: 'umd', // UMD,适用于所有场景
name: 'MyLibrary'
},
{
file: 'dist/my-lib.iife.js',
format: 'iife', // IIFE,直接用于浏览器 script 标签
name: 'MyLibrary'
}
]
};
输出格式详解
| 格式 | 全称 | 适用场景 | 特点 |
|---|---|---|---|
| es / esm | ES Module | 现代浏览器、打包工具 | 保留 import/export,支持 Tree Shaking |
| cjs | CommonJS | Node.js 环境 | 使用 require/module.exports |
| umd | Universal Module Definition | 通用(浏览器、Node.js) | 兼容 AMD、CommonJS 和全局变量 |
| iife | Immediately Invoked Function Expression | 直接在浏览器用 script 脚本引入 | 自执行函数,避免全局污染 |
| amd | Asynchronous Module Definition | RequireJS 等 | 异步模块加载 |
插件系统:扩展 Rollup 的能力
Rollup 的核心功能很精简,大多数能力需要通过插件来扩展。插件通过 plugins 数组配置,可以是单个插件实例或包含多个插件的数组:
javascript
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import babel from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'umd',
name: 'MyLibrary'
},
plugins: [
// 解析 node_modules 中的第三方模块[citation:1]
nodeResolve(),
// 将 CommonJS 模块转换为 ES 模块[citation:10]
commonjs(),
// 支持导入 JSON 文件
json(),
// 替换代码中的字符串(常用于环境变量)
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
// 使用 Babel 进行代码转换
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**'
}),
// 压缩代码(生产环境)
terser()
]
};
external:排除外部依赖
当构建一个库时,我们通常不希望将第三方依赖(如 React、Vue、lodash)打包进最终的产物,而是将其声明为外部依赖:
javascript
export default {
input: 'src/index.js',
output: {
file: 'dist/my-lib.js',
format: 'umd',
name: 'MyLibrary',
// 为 UMD 模式提供全局变量名映射
globals: {
'react': 'React',
'react-dom': 'ReactDOM',
'lodash': '_'
}
},
// 排除外部依赖
external: [
'react',
'react-dom',
'lodash',
// 也可以使用正则表达式
/^lodash\// // 排除 lodash 的所有子模块
]
};
Tree Shaking
Rollup 最令人津津乐道的就是其 Tree Shaking 功能,它通过静态分析移除未使用的代码,减小打包体积:
javascript
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
treeshake: {
// 模块级别的副作用分析
moduleSideEffects: false,
// 属性访问分析(更精确的 Tree Shaking)
propertyReadSideEffects: false,
// 尝试合并模块
tryCatchDeoptimization: false,
// 未知全局变量分析
unknownGlobalSideEffects: false
}
};
// 更简单的用法:直接使用布尔值
treeshake: true // 开启默认的摇树优化[citation:1]
watch:监听模式
在开发过程中,我们可以开启监听模式,当文件变化时自动重新打包:
javascript
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
watch: {
include: 'src/**', // 监听的文件
exclude: 'node_modules/**', // 排除的文件
clearScreen: false // 不清除屏幕
}
};
// 或者在命令行中开启
// rollup -c --watch
// rollup -c -w (简写)
Vue3 使用的关键 Rollup 插件
Vue3 的源码采用 monorepo 管理,使用 Rollup 进行构建。让我们看看 Vue3 在构建过程中使用了哪些关键插件:
@rollup/plugin-node-resolve
作用:允许 Rollup 从 node_modules 中导入第三方模块。
javascript
// 为什么需要这个插件?
import { reactive } from '@vue/reactivity'; // 这个模块在 node_modules 中
// 没有插件时,Rollup 无法解析这个路径
// Vue3 中的使用
import nodeResolve from '@rollup/plugin-node-resolve';
export default {
plugins: [
nodeResolve({
// 指定解析的模块类型
mainFields: ['module', 'main'], // 优先使用 module 字段[citation:5]
extensions: ['.js', '.json', '.ts'], // 支持的文件扩展名
preferBuiltins: false // 不优先使用 Node 内置模块
})
]
};
@rollup/plugin-commonjs
作用:将 CommonJS 模块转换为 ES 模块,使得 Rollup 可以处理那些尚未提供 ES 模块版本的依赖:
javascript
import commonjs from '@rollup/plugin-commonjs';
export default {
plugins: [
commonjs({
// 指定哪些文件需要转换
include: 'node_modules/**',
// 扩展名
extensions: ['.js', '.cjs'],
// 忽略某些模块的转换
ignore: ['conditional-runtime-dependency']
})
]
};
@rollup/plugin-replace
作用:在打包时替换代码中的字符串,常用于注入环境变量或特性开关(Feature Flags):
javascript
// Vue3 中的特性开关示例[citation:2]
// packages/compiler-core/src/errors.ts
export function createCompilerError(code, loc, messages, additionalMessage) {
// __DEV__ 在构建时被替换为 true 或 false
if (__DEV__) {
// 开发环境才执行的代码
}
}
// rollup 配置
import replace from '@rollup/plugin-replace';
export default {
plugins: [
replace({
// 防止被 JSON.stringify 转义
preventAssignment: true,
// 定义环境变量
__DEV__: process.env.NODE_ENV !== 'production',
__VERSION__: JSON.stringify('3.2.0'),
// 特性开关
__FEATURE_OPTIONS_API__: true,
__FEATURE_PROD_DEVTOOLS__: false
})
]
};
@rollup/plugin-json
作用:支持从 JSON 文件导入数据:
javascript
import json from '@rollup/plugin-json';
export default {
plugins: [
json({
// 指定 JSON 文件的大小限制,超过限制则作为单独文件引入
preferConst: true,
indent: ' '
})
]
};
// 使用时
import pkg from './package.json';
console.log(pkg.version);
rollup-plugin-terser
作用:压缩代码,减小生产环境的包体积:
javascript
import { terser } from 'rollup-plugin-terser';
export default {
plugins: [
// 只在生产环境使用
process.env.NODE_ENV === 'production' && terser({
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
pure_funcs: ['console.log'] // 移除特定的函数调用
},
output: {
comments: false // 移除注释
}
})
]
};
@rollup/plugin-babel
作用:使用 Babel 进行代码转换,处理语法兼容性问题:
javascript
import babel from '@rollup/plugin-babel';
export default {
plugins: [
babel({
// 排除 node_modules
exclude: 'node_modules/**',
// 包含的文件
include: ['src/**/*.js', 'src/**/*.ts'],
// Babel helpers 的处理方式
babelHelpers: 'bundled', // 或 'runtime'
// 扩展名
extensions: ['.js', '.jsx', '.ts', '.tsx']
})
]
};
@rollup/plugin-typescript
作用:支持 TypeScript 编译:
javascript
import typescript from '@rollup/plugin-typescript';
export default {
plugins: [
typescript({
tsconfig: './tsconfig.json',
declaration: true, // 生成 .d.ts 文件
declarationDir: 'dist/types'
})
]
};
如何构建指定包(以 pnpm run build reactivity 为例)
Vue3 采用 monorepo 管理多个包,使用 pnpm 作为包管理器。理解 pnpm run build reactivity 背后的机制,能帮助我们更好地理解现代构建流程:
项目结构
text
vue-next/
├── packages/ # 所有子包
│ ├── reactivity/ # 响应式系统
│ │ ├── src/
│ │ ├── package.json # 包级配置
│ │ └── ...
│ ├── runtime-core/ # 运行时核心
│ ├── runtime-dom/ # 浏览器运行时
│ ├── compiler-core/ # 编译器核心
│ ├── vue/ # 完整版本
│ └── ...
├── package.json # 根配置
├── pnpm-workspace.yaml # pnpm 工作区配置
└── rollup.config.js # Rollup 配置文件
pnpm-workspace.yaml 配置
yml
# pnpm-workspace.yaml
packages:
- 'packages/*' # 声明 packages 下的所有目录都是工作区的一部分
这个配置告诉 pnpm:packages 目录下的每个子目录都是一个独立的包,它们之间可以互相引用而不需要发布到 npm。
根 package.json 的脚本配置
json
// 根目录 package.json
{
"private": true,
"scripts": {
"build": "node scripts/build.js", // 构建所有包
"build:reactivity": "pnpm run build reactivity", // 只构建 reactivity 包
"dev": "node scripts/dev.js", // 开发模式
"test": "jest" // 运行测试
}
}
pnpm run 的底层原理
当我们在命令行执行 pnpm run build reactivity 时,背后发生了以下步骤:
- 解析命令:
pnpm run build reactivity - 读取根目录
package.json中的scripts - 找到 "build":
node scripts/build.js - 将参数 "reactivity" 传递给脚本
- 在 PATH 环境变量中查找 node
- 执行
node scripts/build.js reactivity - 脚本根据参数决定构建哪个包
build.js 脚本分析
Vue3 的构建脚本会解析命令行参数,决定构建哪些包:
javascript
// scripts/build.js (简化版)
const fs = require('fs');
const path = require('path');
const execa = require('execa');
const { targets: allTargets } = require('./utils');
// 获取命令行参数
const args = require('minimist')(process.argv.slice(2));
const targets = args._; // 获取到的参数数组
async function build() {
// 如果没有指定目标,构建所有包
if (!targets.length) {
await buildAll(allTargets);
} else {
// 只构建指定的包
await buildSelected(targets);
}
}
async function buildSelected(targets) {
for (const target of targets) {
await buildPackage(target);
}
}
async function buildPackage(packageName) {
console.log(`开始构建: @vue/${packageName}`);
// 切换到包目录
const pkgDir = path.resolve(__dirname, '../packages', packageName);
// 使用 rollup 构建该包
await execa(
'rollup',
[
'-c', // 使用配置文件
'--environment', // 设置环境变量
`TARGET:${packageName}`, // 告诉 rollup 要构建哪个包
'--watch' // 开发模式时可能开启
],
{
stdio: 'inherit', // 继承输入输出
cwd: pkgDir // 在包目录执行
}
);
}
build();
Rollup 配置如何区分不同的包
javascript
// rollup.config.js (简化版)
import { createRequire } from 'module';
import path from 'path';
import fs from 'fs';
// 获取所有包
const packagesDir = path.resolve(__dirname, 'packages');
const packages = fs.readdirSync(packagesDir)
.filter(f => fs.statSync(path.join(packagesDir, f)).isDirectory());
// 根据环境变量决定构建哪个包
const target = process.env.TARGET;
function createConfig(packageName) {
const pkgDir = path.resolve(packagesDir, packageName);
const pkg = require(path.join(pkgDir, 'package.json'));
// 为每个包生成不同的配置
return {
input: path.resolve(pkgDir, 'src/index.ts'),
output: [
{
file: path.resolve(pkgDir, pkg.main),
format: 'cjs',
sourcemap: true
},
{
file: path.resolve(pkgDir, pkg.module),
format: 'es',
sourcemap: true
}
],
plugins: [
// 共用插件
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {})
]
};
}
// 如果指定了 target,只构建那个包
if (target) {
module.exports = createConfig(target);
} else {
// 否则构建所有包
module.exports = packages.map(createConfig);
}
包级 package.json 的配置
每个包都有自己的 package.json,定义了该包的元信息和构建产物的入口:
json
// packages/reactivity/package.json
{
"name": "@vue/reactivity",
"version": "3.2.0",
"main": "dist/reactivity.cjs.js", // CommonJS 入口
"module": "dist/reactivity.esm.js", // ES Module 入口
"unpkg": "dist/reactivity.global.js", // 直接引入的 UMD 版本
"types": "dist/reactivity.d.ts", // TypeScript 类型定义
"dependencies": {
"@vue/shared": "3.2.0"
}
}
Vite 与 Rollup 的关系
为什么需要 Vite?
虽然 Rollup 很优秀,但在开发大型应用时,它和 Webpack 一样面临着性能瓶颈:随着项目变大,启动开发服务器的时间越来越长。
Vite 的双引擎架构
Vite 在开发环境和生产环境使用不同的引擎:
- 开发环境:利用浏览器原生 ES 模块 + esbuild 预构建
- 生产环境:使用 Rollup 进行深度优化打包
开发环境:利用原生 ES 模块
html
<!-- Vite 开发服务器的原理 -->
<script type="module">
// 浏览器直接请求模块,服务器实时编译返回
import { createApp } from '/node_modules/.vite/vue.js'
import App from '/src/App.vue'
createApp(App).mount('#app')
</script>
esbuild 使用 Go 编写,比 JS 编写的打包器快 10-100 倍,可以预构建依赖,并转换 TypeScript/JSX。
生产环境:使用 Rollup 打包
Vite 在生产环境构建时,会使用 Rollup 进行打包。Vite 的插件系统也是与 Rollup 兼容的,这意味着绝大多数 Rollup 插件也可以在 Vite 中使用:
javascript
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [
vue() // 这个插件同时支持开发环境和生产环境
],
// 构建配置
build: {
// 底层是 Rollup 配置
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
nested: resolve(__dirname, 'nested/index.html')
},
output: {
// 代码分割配置
manualChunks: {
vendor: ['vue', 'vue-router']
}
}
},
// 输出目录
outDir: 'dist',
// 生成 sourcemap
sourcemap: true,
// 压缩配置
minify: 'terser' // 或 'esbuild'
}
});
Vite 与 Rollup 的配置对比
| 配置项 | Rollup | Vite |
|---|---|---|
| 入口文件 | input | build.rollupOptions.input |
| 输出目录 | output.file / output.dir | build.outDir |
| 输出格式 | output.format | build.rollupOptions.output.format |
| 外部依赖 | external | build.rollupOptions.external |
| 插件 | plugins | plugins (同时支持 Vite 和 Rollup 插件) |
| 开发服务器 | 无(需配合 rollup -w) | 内置,支持 HMR |
何时选择 Vite,何时选择 Rollup?
使用 Rollup
- 开发 JavaScript/TypeScript 库
- 需要精细控制打包过程
- 项目不复杂,不需要开发服务器
- 已有基于 Rollup 的构建流程
使用 Vite
- 开发应用(Vue/React 项目)
- 需要快速启动的开发服务器
- 需要 HMR 热更新
- 希望简化配置
两者结合
- 库开发时使用 Rollup
- 应用开发时使用 Vite
- Vite 内部使用 Rollup 构建生产环境
总结
Rollup 的核心优势
- 简洁性: 配置直观,学习成本低
- TreeShaking: 基于ES模块的静态分析,产出代码极小
- 多格式输出: 支持输出多种模块格式,适用于不同环境
- 插件生态: 丰富的插件,可以处理各种场景
- 源码可读性: 打包后的代码保持较好的可读性
Vite 的创新之处
- 开发体验: 利用原生ES模块,实现极速启动和热更新
- 双引擎架构: 开发用 esbuild,生产用 Rollup,各取所长
- 配置简化: 内置常用配置,开箱即用
- 插件兼容: 兼容 Rollup 插件生态
构建工具是现代前端开发的基石,深入理解它们不仅能帮助我们写出更高效的代码,还能在遇到问题时快速定位和解决。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!