描述多站点代理路由的 middleware 注入方法
解读
在国内前端工程化面试中,**“多站点代理路由”**通常指:
- 本地开发阶段用 Grunt 起静态服务(grunt-contrib-connect);
- 需要把
/api/*打到后端 A 站,/b2b/*打到后端 B 站,同时把/打到本地 dist; - 还要支持 路径重写、Cookie 域转换、HTTPS 降级、WebSocket 转发 等“中国特色”需求(如微信 JSSDK 校验、企微网关)。
面试官真正想听的是:
- 你能否在 Grunt 插件体系内 优雅地注入中间件,而不是手写 dev-server;
- 是否了解 connect-http-proxy / grunt-connect-proxy 的底层原理;
- 能否把“注入时机、顺序、参数”讲清楚,并给出可落地的代码片段。
知识点
- Grunt 任务生命周期:init → registerTask → task.run → 插件内部
this.async()。 - grunt-contrib-connect 暴露的 middlewares 钩子:
middlewares: function(connect, options, middlewares),必须 return 修改后的数组。 - http-proxy-middleware(v2 以后)与 grunt-connect-proxy 的异同:
- 前者是 Express 通用中间件,后者是 Grunt 专用封装;
- 两者都依赖 node-http-proxy,但 grunt-connect-proxy 在 target、context、rewrite、headers 四个字段上做了 Gruntfile 友好封装。
- 多站点规则冲突解决:
- 按 最长 context 优先 排序;
- 使用 pathRewrite 正则 去掉公共前缀,避免 404;
- 对 WebSocket upgrade 事件单独监听,防止握手被静态中间件吃掉。
- 国内常见坑:
- 公司网关把
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'
]);
});
};
使用流程:
- 终端执行
grunt serve; - 浏览器访问
https://localhost:9000/api/user→ 实际打到https://gateway.a.com/user; - 访问
https://localhost:9000/b2b/order→ 实际打到http://b2b.internal.cn/prefix/b2b/order; - 静态资源走本地 dist,LiveReload 通过 WebSocket 正常升级。
拓展思考
- 如果公司强制 Nginx 统一入口,可以把 Grunt 仅当“本地静态编译+热更新”,代理逻辑下沉到 dev.conf;此时 Grunt 侧只需保留 livereload 脚本注入,中间件注入逻辑可降级为 空函数,减少记忆负担。
- 当项目升级到 Vite/Webpack5 后,同样的多站点需求可用 vite-plugin-proxy 或 webpack-dev-server 的
setupMiddlewares钩子,但“最长匹配 + rewrite + changeOrigin” 思想完全一致,面试时可横向对比,体现你对“配置即代码”理念的贯通。 - 大型 Monorepo 里,不同子包需要不同后端,可在 Gruntfile 里读取 lerna.json 动态生成 proxies 数组,实现“零硬编码”;把这段思路讲出来,能让面试官直接给你升一级。