解释在 grunt 中实现 webpack 外部化 node_modules

解读

面试官真正想考察的是:

  1. 你是否理解 外部化(externals) 的本质——把运行时依赖从 bundle 中剔除,让体积与构建时间双降;
  2. 你是否能在 Grunt 生态 里把 webpack 的 externals 配置落地,而不是只会裸跑 webpack-cli;
  3. 你是否具备 “配置即代码” 的维护意识,能把重复逻辑抽象成可复用的 Grunt 任务,兼顾团队后续迭代。

国内项目常见痛点:

  • 后端渲染或微前端场景下,node_modules 统一走 CDN,包体积直接关乎首屏秒开率
  • 私有源不稳定,每次 npm install 全量拉包容易超时,externals 能显著降低网络压力;
  • 很多候选人说得出“webpack 配 externals”,却说不清 Grunt 怎么把这份配置喂给 webpack,导致现场翻车。

知识点

  1. Grunt 任务运行模型:initConfig → loadNpmTasks → registerTask,配置是“JSON 友好”的扁平对象。
  2. grunt-webpack 插件:官方维护,把 webpack 的 Compiler 实例包装成 Grunt 任务,支持多目标(target)并行。
  3. webpack externals 语法
    • 字符串模板:externals: { vue: 'Vue' }
    • 函数模式:(ctx, req, cb) => req.includes('lodash') ? cb(null, 'commonjs ' + req) : cb()
    • 正则批量:externals: [/^(?!\.|\/)/] 把非相对引用全部外置。
  4. Grunt 模板字符串<%= %> 可动态注入文件列表、环境变量,实现“同一份配置,dev/prod 两套 externals”。
  5. 性能红线:externals 仅解决“打出来的包”体积,不解决 node_modules 目录物理存在;若需彻底免装依赖,需结合 Yarn PnP 或 pnpm 虚拟存储,但面试阶段点到为止即可。

答案

现场回答采用“总-分-总”结构,先给结论,再贴关键代码,最后加一句可落地的工程化建议。

结论先行
借助 grunt-webpack 插件,在 Gruntfile 里把 webpack 的 externals 字段写成多目标配置,即可把 node_modules 统一外部化,让业务 bundle 体积下降 60% 以上

关键代码(可直接抄到白板):

module.exports = function(grunt) {
  // 1. 依赖声明
  grunt.loadNpmTasks('grunt-webpack');

  // 2. 配置区
  grunt.initConfig({
    // 环境嗅探,国内镜像源常挂,加一行保底
    env: process.env.NODE_ENV || 'development',

    webpack: {
      options: {
        // 共享配置
        resolve: { modules: ['node_modules'] },
        externals: function(ctx, req, cb) {
          // 核心逻辑:只要请求路径里包含 node_modules,就外置
          const isExternal =
            /^[a-zA-Z0-9-@/]+$/.test(req) && // 排除相对路径
            !req.startsWith('.') &&
            !req.startsWith('/');          // 绝对路径也保留
          if (isExternal) {
            // 统一挂到全局 window.__external 命名空间,方便 CDN 引入
            return cb(null, `root __external['${req}']`);
          }
          cb();
        }
      },
      dev: {
        mode: 'development',
        entry: './src/main.js',
        output: { path: '<%= grunt.template.today("yyyymmdd") %>/dist', filename: 'app.dev.js' }
      },
      prod: {
        mode: 'production',
        entry: './src/main.js',
        output: { path: 'dist', filename: 'app.[contenthash:8].js' },
        // 生产环境再加一层白名单,只外置稳定三件套
        externals: { vue: 'Vue', vuex: 'Vuex, 'vue-router': 'VueRouter' }
      }
    }
  });

  // 3. 任务编排
  grunt.registerTask('default', ['webpack:prod']);
};

落地补充(体现工程化深度):

  • 在 CI 里加一行 grunt webpack:prod --env=cdn把 externals 清单同步到后端模板,实现“构建-发布-注入”闭环;
  • monorepo 子包,用 externalsType: 'module' 结合 import-map,避免全局变量污染,面试时抛出这句话可瞬间加分。

拓展思考

  1. externals 与分包(splitChunks)混用
    若把 lodash 拆成独立 chunk 又同时 externals,webpack 会优先 externals,导致 splitChunks 失效;国内大型中台项目需提前约定“基础库走 CDN,业务库走 split”,否则测试环境能跑,线上 404。

  2. vite / esbuild 时代 Grunt 还有价值吗?
    面试官常反向提问。标准答案:

    • 存量项目(jQuery/Backbone)仍需 Grunt 插件链做图片压缩、雪碧图、时间戳加戳,一刀切的迁移 ROI 太低;
    • 新基建可 用 Grunt 做胶水层,把 vite 的 build 结果再 post-process(加广告指纹、注入公司 Sentry 上报),老工具新用法才是资深工程师的护城河。
  3. 安全底线
    externals 后 CDN 文件必须加 SRI(integrity 哈希),否则第三方 CDN 被投毒直接 RCE;国内银行项目已把 SRI 写进合规基线,面试提到这一点可展示安全意识。