使用 grunt 将依赖打包为单一文件并剔除未使用代码
解读
面试官真正想考察的是:
- 你是否理解 “打包为单一文件” 与 “剔除未使用代码(Tree-Shaking)” 在 Grunt 时代的技术限制与可行方案;
- 能否在 国内真实企业环境(IE 兼容、老旧 jQuery 插件、无 ESModule 历史包袱)下给出落地步骤;
- 是否具备 “渐进升级” 思维,即在不推翻现有 Grunt 流水线的前提下,引入现代工具链完成 Tree-Shaking,并保证上线后 可回滚、可调试、可缓存。
如果直接回答“Grunt 做不到,换 Webpack”会被认为 逃避问题;若只说“grunt-contrib-concat + uglify”会被认为 不懂 Tree-Shaking。正确姿势是:以 Grunt 为调度器,集成 Rollup 做 Tree-Shaking,再回落到 UglifyJS 做压缩,最终产出单一 bundle,并给出 源码映射、差异化缓存、灰度发布 等国内厂子必考细节。
知识点
- Grunt 插件生态:grunt-rollup、grunt-contrib-uglify、grunt-contrib-clean、grunt-filerev、grunt-contrib-copy。
- Tree-Shaking 前提:依赖包必须 提供 ESModule 入口(package.json 中 "module" 字段),否则 Rollup 无法静态分析;国内常见坑:lodash 需用 lodash-es,moment 需用 dayjs 替代或配置 babel-plugin-import。
- 多格式输出:Rollup 输出 IIFE 格式,可直接塞到页面,兼顾旧项目;同时生成 sourcemap 文件,方便线上 Sentry 定位行列号。
- 差异化缓存:文件名加入 contenthash(grunt-filerev),配合 CDN 回源策略,强制缓存 1 年,更新时只改 hash。
- 灰度发布:利用 Nginx split_clients 模块,按 Cookie 或 IP 段灰度 5% 流量到新 bundle,回滚时只需切回旧文件。
- 合规审计:国内金融、政务项目要求 第三方库许可证白名单,Rollup 打包后需用 rollup-plugin-license 生成依赖清单,提交法务审核。
答案
- 安装依赖
npm i -D grunt grunt-cli grunt-rollup rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-terser grunt-contrib-clean grunt-contrib-copy grunt-filerev
- 改造目录结构
src/
main.js // 入口,只引用真正用到的 API
utils/
index.js // 自己写的工具函数,统一 export
dist/
js/
vendor.${hash}.js // 最终单一文件
- 配置 Gruntfile.js(核心片段)
module.exports = function(grunt) {
grunt.initConfig({
clean: ['dist'],
rollup: {
options: {
format: 'iife',
name: 'MyBundle',
plugins: [
require('@rollup/plugin-node-resolve')({ browser: true }),
require('@rollup/plugin-commonjs')(),
require('@rollup/plugin-terser')({
compress: { drop_console: true, pure_funcs: ['console.log'] },
mangle: { reserved: ['$', 'jQuery'] } // 保护全局变量
})
],
sourcemap: true
},
vendor: {
files: { 'dist/js/vendor.js': 'src/main.js' }
}
},
filerev: {
js: { src: 'dist/js/vendor.js', dest: 'dist/js' }
},
copy: {
map: {
expand: true,
cwd: 'dist/js',
src: '*.map',
dest: 'dist/js'
}
}
});
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-rollup');
grunt.loadNpmTasks('grunt-filerev');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['clean', 'rollup', 'filerev', 'copy']);
};
- 入口文件 src/main.js 示例
// 只引入用到的模块,Tree-Shaking 生效
import { debounce } from 'lodash-es';
import dayjs from 'dayjs';
window.MyBundle = { debounce, dayjs };
- 上线流程
- Jenkins 执行
grunt default后,产出vendor.${hash}.js与.map; - 将
${hash}写入manifest.json,供 Java 模板引擎 自动注入; - Nginx 配置
add_header Cache-Control "public, max-age=31536000, immutable"; - Sentry 上传 sourcemap,隐藏服务器真实路径,仅保留
~/js前缀; - 若出现事故,回滚脚本只需 还原 manifest 文件,CDN 边缘节点 30s 内刷新完成。
拓展思考
-
渐进到 Webpack/Rspack:
当团队后续需要 Code-Splitting、CSS in JS 时,可保留 Grunt 做 任务编排,把复杂打包逻辑迁移到 Webpack 子进程,通过grunt-webpack调用,实现 “老任务照跑,新需求新管道”,避免一次性重构带来的排期风险。 -
Monorepo 场景:
国内很多公司把 组件库、业务库、营销页 放在同一仓库。可在 Grunt 中循环调用 Rollup,利用 rollup-plugin-multi-entry 把每个子包入口分别打包,再统一上传到 私有 CDN(阿里云 OSS + 函数计算刷新),实现 “一键发布 20+ 业务线”。 -
合规白名单自动化:
在rollup插件链末尾追加 rollup-plugin-license,输出dependencies-license.json,再写 Grunt 自定义任务 校验许可证是否在公司白名单(MIT、BSD、Apache-2.0 允许,GPL 系列禁止),阻断 CI 流水线,防止法务风险。