使用 grunt 合并子应用 manifest 并生成统一路由
解读
在国内微前端、多页应用、SSR 同构等场景下,主应用需要动态感知各子应用(独立仓库、独立部署)的入口与路由信息。
子应用各自维护一份 manifest.json(含 name、entry、publicPath、routes 等字段),
CI 阶段把这些 manifest 上传到统一制品库(Nexus/阿里云 OSS/企业内网静态服务器)。
Grunt 负责在构建主应用时,把所有子应用的 manifest 拉取、校验、合并、去重,最终输出一份 routes.json 供运行时加载器(import-map、qiankun、自研路由框架)消费。
面试时,考官想确认:
- 你是否理解“非源码资源聚合”这一构建环节;
- 能否用 Grunt 插件体系低成本落地,而不引入 webpack/vite 额外编译;
- 是否考虑国内网络稳定性、缓存策略、灰度发布、回滚等工程化细节。
知识点
- Grunt 多任务并发:
grunt-concurrent或this.async()避免顺序拉取耗时。 - HTTP 拉取:
grunt-http或自定义grunt.task.runXHR,需加 retry + 超时(国内机房跨区慢)。 - JSON Schema 校验:
ajv在 Grunt 插件里做 manifest 字段强校验,防止子应用写错字段导致主应用白屏。 - 去重与冲突解决:同一子应用多版本并存时,按 semver 最大兼容版本 或 灰度权重 保留一条记录。
- 路由扁平化:把嵌套路由拍平,生成 path -> entry 的 Map,减少运行时递归。
- 缓存与增量:利用
grunt-newer+ ETag,只有子应用 manifest 的 etag 变化才重新合并,节省流水线时间。 - 输出格式兼容:同时生成 TypeScript 声明文件(routes.d.ts),方便主应用代码提示。
- 回滚策略:在
grunt-contrib-copy之前先备份旧 routes.json,一旦校验失败可 自动回滚。
答案
- 初始化任务
在项目根目录安装依赖:
npm i -D grunt grunt-contrib-clean grunt-contrib-copy grunt-http ajv semver
- 编写自定义合并任务
tasks/merge-manifest.js
const http = require('axios');
const semver = require('semver');
const Ajv = require('ajv');
const ajv = new Ajv();
module.exports = function(grunt) {
grunt.registerMultiTask('mergeManifest', '合并子应用 manifest', function() {
const done = this.async();
const options = this.options({
output: 'dist/routes.json',
schema: {
type: 'object',
properties: {
name: {type: 'string'},
entry: {type: 'string'},
publicPath: {type: 'string'},
routes: {
type: 'array',
items: {
type: 'object',
properties: {
path: {type: 'string'},
exact: {type: 'boolean'}
},
required: ['path']
}
}
},
required: ['name', 'entry', 'routes']
}
});
const validate = ajv.compile(options.schema);
const merged = {};
Promise.all(options.urls.map(async url => {
const res = await http.get(url, {timeout: 5000});
const data = res.data;
if (!validate(data)) throw new Error(`invalid manifest ${url}`);
const key = `${data.name}@${semver.clean(data.version || '0.0.0')}`;
if (!merged[key] || semver.gt(data.version, merged[key].version)) {
merged[key] = data;
}
}))
.then(() => {
const routes = [];
Object.values(merged).forEach(m => {
m.routes.forEach(r => routes.push({
...r,
entry: m.entry,
publicPath: m.publicPath
}));
});
grunt.file.write(options.output, JSON.stringify({routes}, null, 2));
grunt.log.ok(`合并完成,共 ${routes.length} 条路由`);
done();
})
.catch(done);
});
};
- Gruntfile.js 配置
module.exports = function(grunt) {
grunt.initConfig({
clean: { dist: 'dist' },
mergeManifest: {
prod: {
options: {
urls: [
'https://cdn1.xxx.com/app1/manifest.json',
'https://cdn2.xxx.com/app2/manifest.json'
],
output: 'dist/routes.json'
}
}
},
copy: {
main: {
files: [{ src: 'dist/routes.json', dest: 'public/routes.json' }]
}
}
});
grunt.loadTasks('tasks');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['clean', 'mergeManifest', 'copy']);
};
- 在 CI 中调用
grunt default --env=prod
产物 public/routes.json 即为统一路由表,主应用通过 fetch('/routes.json') 动态注册子应用。
拓展思考
- 灰度发布:在 manifest 里增加
weight字段,合并任务按权重随机抽样,实现子应用灰度。 - 增量更新:把 etag 记录到
.grunt-manifest-cache,下次构建只拉取变化的子应用,节省 80% 网络耗时。 - 安全校验:在 CI 里用企业内网私钥对 routes.json 做签名,运行时公钥验签,防止运营商劫持插入恶意路由。
- 可视化看板:把合并后的路由表同步到 内网低代码平台,运维同学可一键下线故障子应用,无需重新打包主应用。