描述在 grunt 中实现 Tree-Shaking 兼容

解读

面试官抛出“Tree-Shaking 兼容”并不是让你现场手写 Rollup,而是考察三点:

  1. 是否理解 Tree-Shaking 的前置条件(ES Module 静态结构、副作用标记、DCE 原理);
  2. 是否知道 Grunt 本身只做任务调度,不具备模块依赖分析能力,必须嫁接具备 Tree-Shaking 能力的工具链
  3. 能否给出可落地的 Gruntfile 集成方案,并解释如何验证效果、规避国产组件库常见副作用陷阱。
    在国内实际项目中,Webpack 与 Rollup 并存,很多老系统仍用 Grunt 做“壳”,因此“用 Grunt 驱动 Webpack5 或 Rollup2+ 完成 Tree-Shaking”是高频改造场景,答出这一思路即可命中面试官痛点。

知识点

  • ESM 静态依赖图:只有 import/export 才能被静态分析,require/exports 无法摇树。
  • sideEffects 字段package.json 中显式标记“无副作用”文件,给打包器白名单;国内组件库(如 AntD 3.x)常因 import './style' 导致整包引入。
  • DCE(Dead Code Elimination)与 Tree-Shaking 区别:DCE 基于代码可达性,Tree-Shaking 基于模块依赖图,两者互补。
  • Grunt 生态关键插件
    grunt-webpack:将 Webpack5 嵌入 Grunt 任务,支持 optimization.usedExportssideEffects
    grunt-rollup:直接调用 Rollup,天然 Tree-Shaking,适合库构建。
    grunt-contrib-uglify:仅做压缩,无摇树能力,不能单独完成 Tree-Shaking
  • 国产兼容坑
    – Babel 转码后默认转成 CommonJS,需用 @babel/preset-env{ modules: false } 保持 ESM。
    – 老项目存在 jquery 全局依赖,需用 externals 剥离,否则会被误打包。
  • 验证方法
    webpack-bundle-analyzer 生成可视化报告,对比摇树前后模块体积。
    – 在产物中全局搜索 /* unused harmony export */ 注释,确认标记未被清除。
    – 使用 ag -c "function.*unused" 统计未引用函数残留数,国内 CI 常用此脚本做门禁。

答案

在 Grunt 中实现 Tree-Shaking 兼容的核心思路是**“任务外包”**:让 Grunt 做生命周期管理,把真正的模块分析与摇树交给 Webpack5 或 Rollup。下面给出国内团队落地最多的 Grunt + Webpack5 方案,三步即可跑通:

  1. 安装依赖

    npm i -D grunt-webpack webpack webpack-cli webpack-bundle-analyzer \
             @babel/core @babel/preset-env babel-loader
    
  2. 配置 Gruntfile.js

    module.exports = function(grunt) {
      const webpackConfig = {
        mode: 'production',
        devtool: false,
        entry: './src/index.js',
        output: { path: __dirname + '/dist', filename: 'app.[contenthash].js' },
        module: {
          rules: [{
            test: /\.m?js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: [
                  ['@babel/preset-env', { modules: false }] // **关键:保持 ESM**
                ]
              }
            }
          }]
        },
        optimization: {
          usedExports: true,              // **标记未使用导出**
          sideEffects: false,             // **强制识别 package.json 中的 sideEffects 字段**
          providedExports: true,
          concatenateModules: true
        },
        externals: { jquery: 'jQuery' },  // **国产老项目常见全局变量**
        plugins: [
          new (require('webpack-bundle-analyzer')).BundleAnalyzerPlugin({
            analyzerMode: 'static',
            openAnalyzer: false,
            reportFilename: '../reports/webpack.html'
          })
        ]
      };
    
      grunt.initConfig({
        webpack: {
          prod: webpackConfig
        },
        clean: { dist: ['dist'] }
      });
    
      grunt.loadNpmTasks('grunt-webpack');
      grunt.loadNpmTasks('grunt-contrib-clean');
      grunt.registerTask('build', ['clean', 'webpack']);
    };
    
  3. 在业务库中声明无副作用
    项目根目录 package.json 加入:

    "sideEffects": ["*.css", "*.less", "src/polyfill.js"]
    

    若依赖的国产组件库未标记,可在 webpackConfig.module.rules 追加 side-effects-loader 或手动 alias 到 ESM 版本。

执行 npx grunt build 后,查看 reports/webpack.html 即可看到被灰色标记的未引用模块,体积下降 30%~60% 即证明 Tree-Shaking 生效。若需库构建,可再建一个 grunt-rollup 子任务,把同样源码打出 ESM + UMD 双格式,供外部按需加载。

拓展思考

  1. 双构建管线:国内大型中台普遍要求“Grunt 老任务线不动,新增 Tree-Shaking 产物线”,可通过 grunt-concurrent 并行跑传统 grunt-contrib-uglify 与新 grunt-webpack,灰度切换,零回归风险。
  2. 微前端场景:子应用独立构建,主应用用 import() 动态加载。若子应用仍用 Grunt,可在 CDN 上传前增加 “二次摇树” 步骤:用 Rollup 把子应用 ESM 入口再跑一遍,剔除主应用未用到的 reducer,常再省 15% 体积。
  3. 合规审计:国内金融项目要求可溯源构建,需在 Gruntfile 中把 webpackConfig.stats 设为 'verbose',并把生成的 webpack-stats.json 一并归档,审计人员可用 webpack-bundle-diff 工具对比版本差异,确保没有“暗桩”代码被摇掉后又被重新引入。