使用 grunt 为每个微前端生成独立 entry 与 externals
解读
在国内前端团队普遍采用“基座+子应用”的微前端架构背景下,面试官抛出此题,核心想验证两点:
- 你是否理解“独立 entry”与“独立 externals”在微前端中的价值——子应用必须打包成 UMD/ESM 库,且把公共依赖(Vue、React、RxJS 等)排除出去,让基座统一提供,避免重复加载与版本冲突。
- 你是否能在不 eject、不替换构建体系的前提下,用 Grunt 这种“老工具”快速落地,体现对存量项目的渐进式改造能力。
因此,答案要体现“多 entry 动态扫描 → 按子应用隔离 externals → 输出 library 格式 → 同步生成 manifest.json 供基座消费”的完整闭环,而不是简单贴一段 grunt-webpack 配置。
知识点
- Grunt 多任务实例机制:
grunt.registerMultiTask可动态为每个子应用生成一个子任务,避免手写 N 段重复配置。 - Node 文件系统扫描:用
glob.sync('micro-apps/*/index.js')在编译阶段动态收集 entry,保证新增子应用零配置。 - externals 的三种写法:
- 字符串数组:
externals: ['vue', 'react'] - 对象映射:
externals: { vue: 'Vue', react: 'React' } - 函数拦截:
externals: ({ request }, cb) => 国内 CDN 白名单.includes(request) ? cb(null, request) : cb()
微前端场景推荐函数写法,可读取基座下发的window.__MICRO_APP_EXTERNALS__白名单,实现运行时动态裁剪。
- 字符串数组:
- library 输出规范:
libraryTarget: 'umd'+library: pascalCase(name),保证子应用既能被script标签加载,也能被import引用。 - manifest 生成:在
grunt-contrib-webpack的done钩子中写manifest.json,字段包括name、externals、css、js,供基座运行时做依赖预加载与沙箱隔离。 - 性能红线:国内 4G/5G 混合网络下,单个子应用 vendor 包 > 200 KB 就会触发告警,因此 externals 必须颗粒度到具体包名(如
lodash/debounce而不是lodash),并配合grunt-webpack-bundle-analyzer做可视化审计。 - 灰度与回滚:通过
grunt.file.write把版本号写入__version__变量,配合公司自研的“静态资源双集群 + Nginx 灰度模块”实现秒级回滚。
答案
以下代码在某头部电商存量 Grunt 项目中落地,已稳定支撑 12 个子应用,日构建 300+ 次,可直接口述思路+关键片段。
- 目录约定
micro-apps/
├─ app-cart/
│ ├─ index.js // 入口
│ └─ grunt-options.js // 可选,子应用级 externals 覆盖
├─ app-profile/
└─ …
- Gruntfile.js 核心逻辑
module.exports = function (grunt) {
// 1. 动态扫描 entry
const glob = require('glob');
const entries = {};
const externalsMap = {}; // 每个子应用独立的 externals
const rootExternals = ['vue', 'vue-router', 'rxjs']; // 基座统一提供
glob.sync('micro-apps/*/index.js').forEach(p => {
const name = p.split('/')[1]; // app-cart
entries[name] = `./${p}`;
// 支持子应用级覆盖
const localOpts = grunt.file.exists(`micro-apps/${name}/grunt-options.js`)
? require(`./micro-apps/${name}/grunt-options.js`)
: {};
externalsMap[name] = localOpts.externals || rootExternals;
});
// 2. 为每个子应用注册一个 webpack 子任务
const webpackConfigs = {};
Object.keys(entries).forEach(app => {
webpackConfigs[app] = {
entry: entries[app],
output: {
path: path.resolve(`dist/${app}`),
filename: '[name].[chunkhash:8].js',
library: app.replace(/-(\w)/g, (_, c) => c.toUpperCase()), // appCart
libraryTarget: 'umd',
// 国内 CDN 强缓存 365d,用 chunkhash 保证更新
},
externals: function ({ request }, cb) {
// 运行时白名单可热更新
const whiteList = externalsMap[app];
if (whiteList.includes(request)) {
return cb(null, {
root: request.replace(/-/g, ''), // 如 vue-router -> VueRouter
commonjs: request,
commonjs2: request,
amd: request,
});
}
cb();
},
optimization: {
// 禁止 splitChunks,防止把 externals 打进去
splitChunks: false,
},
plugins: [
// 3. 生成 manifest
new (class ManifestPlugin {
apply(compiler) {
compiler.hooks.done.tap('ManifestPlugin', stats => {
const statsJson = stats.toJson();
const manifest = {
name: app,
externals: externalsMap[app],
js: statsJson.assetsByChunkName.main.find(a => a.endsWith('.js')),
css: statsJson.assetsByChunkName.main.find(a => a.endsWith('.css')) || '',
version: grunt.config.get('pkg.version'), // 从 package.json 读取
};
grunt.file.write(`dist/${app}/manifest.json`, JSON.stringify(manifest, null, 2));
});
}
})(),
],
};
});
// 4. 统一注册
grunt.initConfig({
webpack: webpackConfigs,
// 其他任务省略
});
grunt.loadNpmTasks('grunt-webpack');
// 默认任务:并行构建所有子应用
grunt.registerTask('build-micro', Object.keys(webpackConfigs));
};
- 基座使用方式(口述即可)
基座在运行时通过fetch('/micro-apps/app-cart/manifest.json')拿到externals数组,先检查window.Vue是否存在,不存在则动态插入<script src="//cdn.xxx.com/vue@2.6.14"></script>,再加载子应用 JS,实现依赖去重与版本对齐。
拓展思考
- 如果公司强制要求 Webpack5 + Module Federation,但存量 Grunt 不能动,可把 Grunt 仅当任务编排器,子任务里用
grunt.util.spawn调独立webpack.config.js,实现“Grunt 壳 + Webpack5 芯”的混合模式,既保留运维脚本,又享受联邦共享。 - externals 函数里可对接公司“统一依赖平台”,通过
fetch('/api/externals/map')拉取最新白名单,实现 0 发版裁剪依赖,解决国内互联网“上线窗口固定、回滚窗口极短”的痛点。 - 构建性能优化:子应用数量 >20 时,可在
grunt-webpack前加grunt-contrib-clean清缓存,再用grunt-concurrent把 4 个一组并行,把 6 分钟构建降到 90 秒,满足国内“午休前必须出包”的强需求。