使用 grunt 将依赖打包为单一文件并剔除未使用代码

解读

面试官真正想考察的是:

  1. 你是否理解 “打包为单一文件”“剔除未使用代码(Tree-Shaking)” 在 Grunt 时代的技术限制与可行方案;
  2. 能否在 国内真实企业环境(IE 兼容、老旧 jQuery 插件、无 ESModule 历史包袱)下给出落地步骤;
  3. 是否具备 “渐进升级” 思维,即在不推翻现有 Grunt 流水线的前提下,引入现代工具链完成 Tree-Shaking,并保证上线后 可回滚、可调试、可缓存

如果直接回答“Grunt 做不到,换 Webpack”会被认为 逃避问题;若只说“grunt-contrib-concat + uglify”会被认为 不懂 Tree-Shaking。正确姿势是:以 Grunt 为调度器,集成 Rollup 做 Tree-Shaking,再回落到 UglifyJS 做压缩,最终产出单一 bundle,并给出 源码映射、差异化缓存、灰度发布 等国内厂子必考细节。

知识点

  1. Grunt 插件生态:grunt-rollup、grunt-contrib-uglify、grunt-contrib-clean、grunt-filerev、grunt-contrib-copy。
  2. Tree-Shaking 前提:依赖包必须 提供 ESModule 入口(package.json 中 "module" 字段),否则 Rollup 无法静态分析;国内常见坑:lodash 需用 lodash-es,moment 需用 dayjs 替代或配置 babel-plugin-import
  3. 多格式输出:Rollup 输出 IIFE 格式,可直接塞到页面,兼顾旧项目;同时生成 sourcemap 文件,方便线上 Sentry 定位行列号。
  4. 差异化缓存:文件名加入 contenthash(grunt-filerev),配合 CDN 回源策略,强制缓存 1 年,更新时只改 hash。
  5. 灰度发布:利用 Nginx split_clients 模块,按 Cookie 或 IP 段灰度 5% 流量到新 bundle,回滚时只需切回旧文件。
  6. 合规审计:国内金融、政务项目要求 第三方库许可证白名单,Rollup 打包后需用 rollup-plugin-license 生成依赖清单,提交法务审核。

答案

  1. 安装依赖
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
  1. 改造目录结构
src/
  main.js          // 入口,只引用真正用到的 API
  utils/
    index.js       // 自己写的工具函数,统一 export
dist/
  js/
    vendor.${hash}.js   // 最终单一文件
  1. 配置 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']);
};
  1. 入口文件 src/main.js 示例
// 只引入用到的模块,Tree-Shaking 生效
import { debounce } from 'lodash-es';
import dayjs from 'dayjs';

window.MyBundle = { debounce, dayjs };
  1. 上线流程
  • 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 内刷新完成。

拓展思考

  1. 渐进到 Webpack/Rspack
    当团队后续需要 Code-Splitting、CSS in JS 时,可保留 Grunt 做 任务编排,把复杂打包逻辑迁移到 Webpack 子进程,通过 grunt-webpack 调用,实现 “老任务照跑,新需求新管道”,避免一次性重构带来的排期风险。

  2. Monorepo 场景
    国内很多公司把 组件库、业务库、营销页 放在同一仓库。可在 Grunt 中循环调用 Rollup,利用 rollup-plugin-multi-entry 把每个子包入口分别打包,再统一上传到 私有 CDN(阿里云 OSS + 函数计算刷新),实现 “一键发布 20+ 业务线”

  3. 合规白名单自动化
    rollup 插件链末尾追加 rollup-plugin-license,输出 dependencies-license.json,再写 Grunt 自定义任务 校验许可证是否在公司白名单(MIT、BSD、Apache-2.0 允许,GPL 系列禁止),阻断 CI 流水线,防止法务风险。