描述多站点代理路由的 middleware 注入方法

解读

在国内前端工程化面试中,**“多站点代理路由”**通常指:

  1. 本地开发阶段用 Grunt 起静态服务(grunt-contrib-connect);
  2. 需要把 /api/* 打到后端 A 站,/b2b/* 打到后端 B 站,同时把 / 打到本地 dist;
  3. 还要支持 路径重写、Cookie 域转换、HTTPS 降级、WebSocket 转发 等“中国特色”需求(如微信 JSSDK 校验、企微网关)。
    面试官真正想听的是:
  • 你能否在 Grunt 插件体系内 优雅地注入中间件,而不是手写 dev-server;
  • 是否了解 connect-http-proxy / grunt-connect-proxy 的底层原理;
  • 能否把“注入时机、顺序、参数”讲清楚,并给出可落地的代码片段。

知识点

  1. Grunt 任务生命周期:init → registerTask → task.run → 插件内部 this.async()
  2. grunt-contrib-connect 暴露的 middlewares 钩子:
    middlewares: function(connect, options, middlewares),必须 return 修改后的数组
  3. http-proxy-middleware(v2 以后)与 grunt-connect-proxy 的异同:
    • 前者是 Express 通用中间件,后者是 Grunt 专用封装;
    • 两者都依赖 node-http-proxy,但 grunt-connect-proxy 在 target、context、rewrite、headers 四个字段上做了 Gruntfile 友好封装。
  4. 多站点规则冲突解决:
    • 最长 context 优先 排序;
    • 使用 pathRewrite 正则 去掉公共前缀,避免 404;
    • WebSocket upgrade 事件单独监听,防止握手被静态中间件吃掉。
  5. 国内常见坑:
    • 公司网关把 location 头写成内网 IP,需 changeOrigin: true + cookieDomainRewrite
    • 微信开发票接口要求 Referer 白名单,必须在 headers 里强制写死;
    • 高版本 node 下 keepAlive 超时,需要 agentOptions 设 maxSockets: 64

答案

下面给出一份 可直接拷贝到 Gruntfile.js 的完整示例,覆盖“本地 dist + 两个后端站点 + WebSocket”场景,关键步骤已加粗

module.exports = function(grunt) {
  // 1. 载入专用代理插件
  grunt.loadNpmTasks('grunt-connect-proxy');

  grunt.initConfig({
    // 2. 先声明代理列表
    connect: {
      options: {
        port: 9000,
        hostname: '0.0.0.0',
        // 3. **注入中间件的唯一官方入口**
        middlewares: function(connect, options, middlewares) {
          // 3-1. 把代理中间件插到最前面,保证优先匹配
          middlewares.unshift(
            require('grunt-connect-proxy/lib/utils').proxyRequest
          );
          // 3-2. 继续返回数组,grunt-contrib-connect 会把后续静态中间件追加
          return middlewares;
        }
      },
      proxies: [
        {
          context: '/api',          // 最长匹配
          host: 'gateway.a.com',
          port: 443,
          https: true,
          changeOrigin: true,
          // **路径重写:把 /api/user -> /user**
          rewrite: { '^/api': '' },
          // **解决国内网关常见的 cookie 域冲突**
          cookieDomainRewrite: 'localhost',
          headers: {
            'x-forwarded-prefix': '/api'
          }
        },
        {
          context: '/b2b',
          host: 'b2b.internal.cn',
          port: 80,
          changeOrigin: true,
          rewrite: { '^/b2b': '/prefix/b2b' }
        }
      ],
      livereload: {
        options: {
          // 4. 本地 dist 目录
          base: '<%= config.dist %>',
          // 5. **开启 WebSocket 升级监听**
          middlewares: function(connect, options, middlewares) {
            // 复用上面的代理注入逻辑
            middlewares.unshift(
              require('grunt-connect-proxy/lib/utils').proxyRequest
            );
            return middlewares;
          }
        }
      }
    },

    // 6. 注册任务:先起代理,再起 connect
    concurrent: {
      server: ['connect:livereload', 'watch']
    }
  });

  grunt.registerTask('serve', function(target) {
    // **关键顺序:先初始化代理,再启动静态服务**
    grunt.task.run([
      'configureProxies:livereload',
      'concurrent:server'
    ]);
  });
};

使用流程:

  1. 终端执行 grunt serve
  2. 浏览器访问 https://localhost:9000/api/user → 实际打到 https://gateway.a.com/user
  3. 访问 https://localhost:9000/b2b/order → 实际打到 http://b2b.internal.cn/prefix/b2b/order
  4. 静态资源走本地 dist,LiveReload 通过 WebSocket 正常升级

拓展思考

  1. 如果公司强制 Nginx 统一入口,可以把 Grunt 仅当“本地静态编译+热更新”,代理逻辑下沉到 dev.conf;此时 Grunt 侧只需保留 livereload 脚本注入,中间件注入逻辑可降级为 空函数,减少记忆负担。
  2. 当项目升级到 Vite/Webpack5 后,同样的多站点需求可用 vite-plugin-proxywebpack-dev-serversetupMiddlewares 钩子,但“最长匹配 + rewrite + changeOrigin” 思想完全一致,面试时可横向对比,体现你对“配置即代码”理念的贯通。
  3. 大型 Monorepo 里,不同子包需要不同后端,可在 Gruntfile 里读取 lerna.json 动态生成 proxies 数组,实现“零硬编码”;把这段思路讲出来,能让面试官直接给你升一级