各位观众老爷,晚上好!我是你们的老朋友,今晚咱们聊聊如何用Vite和Rollup这对“双剑合璧”,打造一个极致优化的生产环境打包方案。别怕,我尽量把这个过程说得像在家门口下馆子一样轻松愉快。
开场白:Vite和Rollup,哥俩好,效率高
首先,咱们要明白Vite和Rollup各自的优势。Vite就像一个急性子的厨师,开发阶段讲究“快”,利用浏览器原生ESM,按需编译,闪电般的速度让你爱不释手。Rollup则像一个精益求精的大厨,生产环境打包时,专注于“精”,通过各种优化手段,让打包后的代码体积更小,性能更强。
简单来说:
工具 | 优势 | 适用场景 |
---|---|---|
Vite | 开发阶段,启动速度快,热更新迅速,基于ESM | 大型项目,需要快速迭代,注重开发效率 |
Rollup | 生产环境打包,Tree Shaking 强大,插件生态丰富,可定制性高,产物体积小,性能优化空间大 | 需要极致优化的生产环境代码,对代码体积和性能有较高要求的项目,例如:库、组件库、框架等 |
所以,理想的方案是:开发阶段用Vite,享受丝滑的开发体验;生产环境用Rollup,榨干代码的每一滴性能。
第一步:Vite构建,Rollup接棒
最基本的操作流程是:先用Vite构建出用于生产环境的中间产物,然后用Rollup对这些中间产物进行进一步的优化和打包。
-
Vite 构建生产环境代码
在
package.json
中定义 Vite 的 build 命令:{ "scripts": { "build:vite": "vite build" } }
运行
npm run build:vite
或yarn build:vite
,Vite 会将你的代码打包到dist
目录(默认)。当然,你可以通过vite.config.js
文件自定义构建配置,例如指定输出目录:// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], build: { outDir: 'dist_vite' // 指定输出目录 } })
-
Rollup 接棒优化打包
接下来,我们需要安装 Rollup 及相关插件:
npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-terser --save-dev
rollup
: Rollup 的核心库。@rollup/plugin-node-resolve
: 用于解析 Node.js 模块。@rollup/plugin-commonjs
: 用于将 CommonJS 模块转换为 ES 模块。rollup-plugin-terser
: 用于代码压缩和混淆。
创建一个
rollup.config.js
文件,配置 Rollup:// rollup.config.js import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; export default { input: 'dist_vite/index.js', // Vite 构建的入口文件 output: { file: 'dist/bundle.js', // Rollup 打包后的输出文件 format: 'es', // 输出格式,这里使用 ES 模块 sourcemap: true, // 生成 Source Map,方便调试 }, plugins: [ nodeResolve(), // 解析 Node.js 模块 commonjs(), // 将 CommonJS 模块转换为 ES 模块 terser(), // 压缩代码 ], };
在
package.json
中添加 Rollup 的构建命令:{ "scripts": { "build:rollup": "rollup -c rollup.config.js" } }
运行
npm run build:rollup
或yarn build:rollup
,Rollup 就会根据你的配置,对 Vite 构建的中间产物进行优化和打包。
第二步:Rollup配置进阶,让优化更上一层楼
仅仅是上面的基础配置,还远远不够。 Rollup 的强大之处在于其高度可定制性,我们可以通过各种插件和配置项,对打包过程进行更精细的控制。
-
Tree Shaking,摇掉无用代码
Tree Shaking 是一种移除 JavaScript 代码中未引用代码的技术。 Rollup 对 Tree Shaking 的支持非常强大,能够有效地减小打包后的代码体积。默认情况下,Rollup 会自动进行 Tree Shaking,但为了确保其正常工作,你需要注意以下几点:
- 使用 ES 模块语法 (
import
和export
)。CommonJS 模块 (require
和module.exports
) 对 Tree Shaking 不友好。 - 避免副作用代码。副作用代码指的是那些在模块加载时会产生副作用的代码,例如修改全局变量。 Rollup 很难判断副作用代码是否被使用,因此可能会保留它们。
- 使用 ES 模块语法 (
-
代码分割 (Code Splitting),按需加载
代码分割是将你的代码分割成多个小的 chunk,然后按需加载这些 chunk。这可以减少初始加载时间,提高应用性能。 Rollup 支持多种代码分割策略,常用的有以下几种:
- 基于入口点的代码分割: Rollup 会将每个入口点及其依赖的代码打包成一个 chunk。
- 基于动态导入的代码分割: 使用
import()
语法动态导入模块时, Rollup 会将动态导入的模块及其依赖的代码打包成一个 chunk。
要启用代码分割,你需要在 Rollup 的
output
配置中设置format: 'es'
,并使用多个入口点或动态导入。例如,有多个入口点:
// rollup.config.js export default { input: { main: 'src/main.js', about: 'src/about.js', }, output: { dir: 'dist', // 输出目录 format: 'es', // 输出格式 entryFileNames: '[name].js', // 入口文件名称 chunkFileNames: 'chunks/[name].js', // chunk 文件名称 sourcemap: true, }, };
或者使用动态导入:
// src/main.js async function loadAbout() { const { default: about } = await import('./about.js'); console.log(about); } loadAbout();
-
插件生态,武装到牙齿
Rollup 拥有丰富的插件生态,你可以利用这些插件来扩展 Rollup 的功能,例如:
@rollup/plugin-typescript
: 用于支持 TypeScript。rollup-plugin-vue
: 用于支持 Vue 单文件组件。rollup-plugin-postcss
: 用于处理 CSS。rollup-plugin-image
: 用于处理图片。
使用插件非常简单,只需要安装插件,然后在
rollup.config.js
的plugins
数组中添加插件即可。// rollup.config.js import typescript from '@rollup/plugin-typescript'; export default { // ... plugins: [ typescript(), // 添加 TypeScript 插件 ], };
-
代码压缩和混淆,让代码更苗条
代码压缩和混淆是减小代码体积和提高代码安全性的重要手段。 Rollup 提供了
rollup-plugin-terser
插件,可以方便地进行代码压缩和混淆。// rollup.config.js import { terser } from 'rollup-plugin-terser'; export default { // ... plugins: [ terser({ compress: { drop_console: true, // 移除 console.log 语句 }, mangle: true, // 混淆变量名 }), ], };
-
Scope Hoisting (实验性),提升运行性能
Scope Hoisting 是一种将多个模块的代码合并到一个作用域中的技术。这可以减少函数调用次数,提高代码的运行性能。 Rollup 对 Scope Hoisting 的支持还处于实验性阶段,需要手动开启。
// rollup.config.js export default { // ... treeshake: { moduleSideEffects: 'no-external', // 允许移除没有副作用的模块 propertyReadSideEffects: false, // 允许移除对对象属性的无副作用的读取 tryCatchDeoptimization: false, // 允许移除 try...catch 语句 }, context: 'this', // 确保 `this` 指向全局对象 moduleContext: 'this', // 确保模块中的 `this` 指向全局对象 output: { // ... generatedCode: { preset: 'es2015', // 使用 ES2015 代码风格 symbols: true, // 保留模块的符号信息 }, }, };
注意: Scope Hoisting 可能会导致一些兼容性问题,需要谨慎使用。
第三步:实例演练,手把手教你配置
咱们来一个实际的例子,假设我们有一个基于 Vue 3 的项目,使用 TypeScript 编写,需要打包成一个库。
-
项目结构
my-library/ ├── src/ │ ├── components/ │ │ ├── MyComponent.vue │ │ └── index.ts │ ├── index.ts ├── vite.config.ts ├── rollup.config.js ├── package.json └── tsconfig.json
-
src/components/MyComponent.vue
<template> <div> <h1>{{ message }}</h1> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; const message = ref('Hello from MyComponent!'); </script>
-
src/components/index.ts
import MyComponent from './MyComponent.vue'; export { MyComponent };
-
src/index.ts
import { MyComponent } from './components'; export { MyComponent };
-
vite.config.ts
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import dts from 'vite-plugin-dts'; // 用于生成 .d.ts 文件 export default defineConfig({ plugins: [ vue(), dts({ insertTypesEntry: true, // 自动插入 types 入口 }), ], build: { lib: { entry: './src/index.ts', // 入口文件 name: 'MyLibrary', // 库的名称 fileName: (format) => `my-library.${format}.js`, // 输出文件名 }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: ['vue'], output: { globals: { vue: 'Vue', // 在 UMD 构建模式下,vue 将被视为全局变量 Vue }, }, }, }, });
-
rollup.config.js
import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import vue from 'rollup-plugin-vue'; import { terser } from 'rollup-plugin-terser'; import postcss from 'rollup-plugin-postcss'; export default { input: 'dist/my-library.es.js', // Vite 构建的 ES 模块入口文件 output: [ { file: 'dist/my-library.cjs.js', // CommonJS 格式 format: 'cjs', sourcemap: true, }, { file: 'dist/my-library.umd.js', // UMD 格式 format: 'umd', name: 'MyLibrary', // 库的名称 sourcemap: true, globals: { vue: 'Vue', // 在 UMD 构建模式下,vue 将被视为全局变量 Vue }, }, { file: 'dist/my-library.es.js', // ES 模块格式 format: 'es', sourcemap: true, }, ], external: ['vue'], // 声明外部依赖,不打包进库 plugins: [ nodeResolve(), // 解析 Node.js 模块 commonjs(), // 将 CommonJS 模块转换为 ES 模块 typescript({ tsconfig: './tsconfig.json', // 指定 tsconfig 文件 declaration: false, // 不生成 .d.ts 文件,Vite 已经生成 }), vue({ preprocessStyles: true, // 预处理样式 }), postcss({ modules: true, // 启用 CSS Modules extract: true, // 提取 CSS 到单独的文件 }), terser(), // 压缩代码 ], };
-
package.json
{ "name": "my-library", "version": "1.0.0", "main": "dist/my-library.cjs.js", "module": "dist/my-library.es.js", "unpkg": "dist/my-library.umd.js", "types": "dist/index.d.ts", "scripts": { "dev": "vite", "build:vite": "vite build", "build:rollup": "rollup -c rollup.config.js", "build": "npm run build:vite && npm run build:rollup" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", "@vitejs/plugin-vue": "^4.5.2", "postcss": "^8.4.32", "rollup": "^2.79.1", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-vue": "^6.0.0", "typescript": "^5.3.3", "vite": "^5.0.10", "vite-plugin-dts": "^3.7.0", "vue": "^3.3.11" } }
-
运行构建命令
运行
npm run build
或yarn build
,Vite 会先构建出 ES 模块格式的库,然后 Rollup 会对这个 ES 模块进行进一步的优化和打包,生成 CommonJS、UMD 和 ES 模块格式的库。
第四步:注意事项,避开那些坑
- 外部依赖 (External Dependencies): 使用
external
配置项声明你的库的外部依赖,例如vue
。这可以避免将这些依赖打包进你的库,减小库的体积,并允许用户使用他们自己的版本的依赖。 - 输出格式 (Output Format): 根据你的库的使用场景,选择合适的输出格式。常用的输出格式有 CommonJS (
cjs
)、UMD (umd
) 和 ES 模块 (es
)。 - Source Map: 生成 Source Map 可以方便用户调试你的库的代码。建议在生产环境也生成 Source Map,但不要将 Source Map 文件发布到生产环境,而是将它们存储在私有服务器上。
- 错误处理: 在 Rollup 的配置中添加错误处理机制,可以帮助你及时发现和解决构建问题。
总结:Vite + Rollup,打造你的专属优化方案
通过 Vite 和 Rollup 的巧妙结合,我们可以打造一个高度优化的生产环境打包方案,让你的代码体积更小,性能更强,用户体验更好。 记住,没有一成不变的配置,你需要根据你的项目的具体情况,不断调整和优化你的配置,才能达到最佳效果。
希望今天的分享对你有所帮助,祝你打包愉快!下次再见!