使用 grunt-webpack 生成 client 与 server 两份 bundle

解读

面试官真正想考察的是:

  1. 你是否理解 Grunt 的任务编排哲学(配置驱动、插件化、串行/并行控制)。
  2. 能否把 webpack 的多入口、多配置 无缝嫁接到 Grunt 流程里,而不是“用 webpack 命令行硬跑”。
  3. 国内主流场景(SSR、同构、微前端)是否做过真实落地:client 包供浏览器拉取,server 包供 Node 渲染,两份产物路径、publicPath、target、externals 完全不同。
  4. 是否知道 grunt-webpack 3.x 与 4.x 的差异(缓存、watch、error callback)以及 Grunt 1.5 之后对并行任务的原生支持,避免“跑两次 webpack 耗时翻倍”这类低级坑。
  5. 有没有踩过 Windows 中文路径、公司私有 Nexus 源、CI 内存溢出 这些国内特色坑,并给出兜底方案。

知识点

  • grunt-webpack 插件的两种调用方式webpack 单配置 vs webpack-dev-server 调试模式。
  • webpack 多配置数组(multi-compiler):返回 [clientConfig, serverConfig],grunt-webpack 会一次性跑完,输出两份 stats。
  • Grunt 任务并行:使用 grunt-concurrent 或 Grunt 1.5 的 grunt.registerTask('parallel', ['webpack:client', 'webpack:server']) 配合 grunt.util.spawn 实现并行,缩短 40%+ 构建时间。
  • 路径隔离规范:client 产物放在 dist/public,server 产物放在 dist/server,并在 serverConfig.externals = [webpackNodeExternals()] 避免把 node_modules 打进去。
  • 国内 CI 限速:在 clientConfig.optimization.splitChunks.cacheGroups.vendor.test 里把公司私库 @xxx 统一拆包,配合 hard-source-webpack-plugin 做二级缓存,解决“每次 npm ci 后全量重编译”问题。
  • 错误码收敛:grunt-webpack 默认把 webpack 错误吞掉,需在 options.stats 显式配 'errors-warnings',并在 done callbackgrunt.fail.fatal 让 Jenkins 红灯,防止“构建失败却绿屏上线”。
  • 环境区分:借助 process.env.BUILD_ENV === 'prod' 动态切换 mode、devtool、sourceMap,避免测试环境生成慢速的 source-map 而拖慢流水线。

答案

  1. 安装依赖

    npm i -D grunt-webpack webpack webpack-cli webpack-node-externals grunt-concurrent
    
  2. 在项目根新建 webpack.config.js 导出多配置数组

    const nodeExternals = require('webpack-node-externals');
    const path = require('path');
    
    const client = {
      name: 'client',
      mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
      entry: './src/client/index.js',
      output: {
        path: path.resolve('dist/public'),
        filename: '[name].[contenthash].js',
        publicPath: '/static/'
      },
      optimization: {
        splitChunks: { chunks: 'all' }
      }
    };
    
    const server = {
      name: 'server',
      target: 'node',
      mode: 'development', // 服务端不需要压缩,降低 CI 耗时
      entry: './src/server/index.js',
      output: {
        path: path.resolve('dist/server'),
        filename: 'server.js',
        libraryTarget: 'commonjs2'
      },
      externals: [nodeExternals({
        allowlist: [/^@company/] // 公司内网私有包仍需打包
      })]
    };
    
    module.exports = [client, server];
    
  3. Gruntfile.js 里注册 grunt-webpack 任务

    module.exports = function(grunt) {
      grunt.initConfig({
        webpack: {
          options: require('./webpack.config.js'), // 直接引用数组
          stats: 'errors-warnings'
        }
      });
    
      grunt.loadNpmTasks('grunt-webpack');
    
      // 串行方案(本地开发)
      grunt.registerTask('build', ['webpack']);
    
      // 并行方案(CI 提速)
      grunt.registerTask('build:parallel', function() {
        const done = this.async();
        grunt.util.spawn({ grunt: true, args: ['webpack:client'] }, () => {});
        grunt.util.spawn({ grunt: true, args: ['webpack:server'] }, () => {
          done();
        });
      });
    };
    
  4. 运行

    # 本地
    npx grunt build
    # CI
    NODE_ENV=production npx grunt build:parallel
    

    产物结构

    dist/
      public/
        main.abc123.js
        vendor.def456.js
      server/
        server.js
    
  5. 验证

    • 浏览器访问 http://localhost/static/main.abc123.js 返回 200。
    • Node 端 node dist/server/server.js 正常拉起 Express,无 Cannot find module 'react' 报错,证明 externals 生效。

拓展思考

  • 如何做到“一次 watch,两份热更新”
    webpack.config.js 里给 client 配 devServer.hot=true,给 server 配 webpack/hot/poll?300,再用 grunt-concurrent 同时起 webpack-dev-servernodemon dist/server/server.js,实现 “浏览器热替换 + Node 端自动重启” 同构开发,解决国内面试常问的“ssr 热更新卡顿”问题。

  • 微前端场景下子应用双 bundle
    client.output.library='subApp' 设为 umd,把 server.output.libraryTarget='commonjs2' 保持不动,主应用通过 require('subapp/server') 做 SSR,浏览器通过 script src="/static/subApp.js" 做 hydrate,实现 “子应用独立构建、主应用零改造” 的国内微前端落地范式。