描述在 grunt 中实现灰度发布到指定租户

解读

面试官想确认三件事:

  1. 你是否理解“灰度发布”在国内的真实含义——按租户维度逐步放量,而非简单按 IP 或 UID 切流;
  2. 你是否能把“灰度”逻辑与 Grunt 的“构建-上传-刷新 CDN”自动化流程打通,让同一份源码产出两套(或多套)差异化产物
  3. 你是否具备“配置即代码”思维,用 Gruntfile 把租户名单、比例、版本号、回滚策略全部显性化,而不是写死在脚本里。

回答时要突出“构建侧打标 + 发布侧切流”两步走,并给出可落地的 Grunt 任务链,避免空谈 Jenkins 或 K8s。

知识点

  • 多环境产物隔离:利用 grunt.file.write 动态生成 gray-config.json,把租户白名单、灰度版本号、CDN 路径注入到构建产物里。
  • 任务编排grunt.registerTask('gray', ['clean:gray', 'webpack:gray', 'string-replace:grayTag', 'upload:gray', 'cdn:refreshGray'])保证 gray 任务与 prod 任务完全隔离
  • 租户维度打标:在 HTML/JS 入口里插入 window.__GRAY_TENANTS__ = <%= grayTenants %>由 Grunt 模板引擎一次性替换,前端运行时根据当前登录租户 ID 匹配是否走灰度 CDN。
  • 上传与刷新:国内云厂商(阿里云 OSS+CDN、腾讯云 COS+ECDN)均提供 OpenAPI 密钥对,用 grunt-httpgrunt-exec 调用官方 CLI,实现“上传后立刻按目录刷新”,避免老资源缓存。
  • 回滚能力:Gruntfile 里维护 grayHistory 数组,记录每次灰度的 buildNoCDN 目录回滚任务直接复用旧目录做刷新,10 秒内完成。

答案

  1. 目录约定

    dist/prod/        // 全量包
    dist/gray/{version}/{tenant}/ // 按版本+租户隔离
    
  2. 安装依赖

    npm i -D grunt-contrib-clean grunt-string-replace grunt-webpack grunt-http
    
  3. Gruntfile 核心片段

    module.exports = function(grunt) {
      const grayTenants = grunt.option('tenants')?.split(',') || ['tenantA', 'tenantB'];
      const grayVersion = grunt.option('ver') || Date.now();
    
      grunt.initConfig({
        clean: {
          gray: ['dist/gray/<%= grayVersion %>']
        },
        webpack: {
          gray: {
            entry: './src/main.js',
            output: {
              path: `dist/gray/${grayVersion}/common/`,
              filename: 'app.[contenthash].js'
            }
          }
        },
        'string-replace': {
          grayTag: {
            files: [{
              expand: true,
              cwd: `dist/gray/${grayVersion}/common/`,
              src: '*.js',
              dest: `dist/gray/${grayVersion}/common/`
            }],
            options: {
              replacements: [{
                pattern: /\/\*GRAY_TAG\*\//,
                replacement: `window.__GRAY_TENANTS__=${JSON.stringify(grayTenants)};window.__GRAY_VER__='${grayVersion}';`
              }]
            }
          }
        },
        http: {
          uploadGray: {
            options: {
              url: `https://oss-cn-shanghai.aliyuncs.com/${bucket}/gray/${grayVersion}/`,
              method: 'PUT',
              headers: { Authorization: generateOSSAuth() },
              src: `dist/gray/${grayVersion}/**`,
              dest: '/'
            }
          },
          refreshCDN: {
            options: {
              url: 'https://cdn.aliyuncs.com/',
              method: 'POST',
              form: {
                ObjectPath: `https://cdn.xxx.com/gray/${grayVersion}/`,
                Action: 'RefreshObjectCaches'
              },
              headers: { Authorization: generateCDNAuth() }
            }
          }
        }
      });
    
      grunt.registerTask('gray', function() {
        if (!grayTenants.length) grunt.fail.warn('必须指定 --tenants');
        grunt.task.run(['clean:gray', 'webpack:gray', 'string-replace:grayTag', 'http:uploadGray', 'http:refreshCDN']);
        grunt.log.ok(`灰度版本 ${grayVersion} 已发布至租户 ${grayTenants.join(',')}`);
      });
    };
    
  4. 运行命令

    grunt gray --tenants=tenantA,tenantB --ver=20250618
    
  5. 前端运行时

    if (window.__GRAY_TENANTS__?.includes(currentTenantId)) {
        __webpack_public_path__ = `https://cdn.xxx.com/gray/${window.__GRAY_VER__}/common/`;
    }
    
  6. 回滚

    grunt cdnRollback --ver=20250615
    

    任务内部直接调用 CDN 刷新接口,把 20250615 目录重新刷为最新即可。

拓展思考

  • 动态租户列表:把租户白名单放到 Redis 或配置中心,Grunt 任务里先调用接口拉取,实现“零重启”增删灰度租户
  • 比例灰度:如果业务要求“按租户+流量百分比”双层灰度,可在构建阶段只打标版本,真正的切流逻辑下沉到网关层(如阿里云 MSE、腾讯云 CLB),Grunt 负责把灰度版本推送至“灰度池”目录,网关按租户 ID 做一致性哈希。
  • 安全合规:国内金融、政企项目要求“灰度包可审计”,可在 Grunt 任务链尾部追加 grunt-zipdist/gray/${version} 打成 tar.gz自动上传至公司内部的 Artifactory 并记录 SHA256,满足监管留痕。