接手一个已经跑了一年多的 qiankun 微前端项目,第一件让我头疼的事不是某个子应用挂了,而是基座(主应用)越来越像一个“业务应用”,而不是一个“容器”。路由表堆了两百多条,导航菜单在基座里写死,每个子应用接入都是“量身定制”——A 项目要传 token 对象、B 项目要监听自定义事件、C 项目直接改基座全局变量。结果就是:每次接一个新子应用,都要动基座代码;每次动基座代码,全量回归;基座发布频率比子应用还高。
这次改造的目标很明确:把主应用真正改造成一个通用容器,让子应用按统一契约接入,基座不再感知具体业务。下文就是我在这次“微前端容器标准化改造”里的完整思路、实操过程和踩坑记录,希望对正在做微前端治理的团队有帮助。
1. 基座失控的五个信号:先认清问题再谈标准化
做容器标准化之前,我花了两周时间把现有代码翻了一遍,把“失控”的具体表现整理成了五类信号。这五类信号基本是所有微前端项目从“能跑”走向“难维护”的共同路径,如果你的项目已经出现其中两三条,说明容器层该动刀了。
1.1 信号一:基座路由表无限膨胀
很多团队把微前端当成“路由分发器”来用:主应用里维护了所有子应用的路由映射,再叠加子应用内部路由,路由表直接失去层次。我这边的情况是主应用 router 配置里混着三类东西:纯基座页面路由、子应用挂载路由、以及为了兼容旧链接而写的重定向规则。每次新增子应用,第一件事就是往路由表里塞几十行,第二件事是检查有没有路由前缀冲突。
路由表膨胀的根因,是主应用承担了“全局路由中心”的职责。容器标准化之后,这个职责应该下沉:基座只维护“哪个前缀路由对应哪个子应用”的映射表,子应用内部路由完全交给子应用自身。
1.2 信号二:导航菜单在基座里写死
我们基座的侧边栏菜单是按角色硬编码的,每个菜单项对应一个子应用路由。业务侧说要调整菜单顺序,得改基座代码;新增一个业务模块,得改基座代码;子应用改名,还得改基座代码。菜单变来变去,基座的发布频率就这么被拖上去了。
标准化改造里,菜单和路由都应该是“数据”,而不是“代码”。容器提供菜单渲染框架,子应用或配置中心下发菜单数据,基座只负责按权限渲染。
1.3 信号三:全局样式互相污染
微前端最常见的样式事故:子应用 A 在全局写了body { font-family: ... },子应用 B 的页面字体被带偏;或者两个子应用都用 antd,版本还不一样,样式互相覆盖导致按钮忽大忽小。qiankun 默认的样式隔离机制只在加载时生效,并不能完全杜绝运行时的动态插入样式污染全局。
这个问题的本质是:子应用把“局部样式”写成了“全局样式”,而容器没有强制约束。标准化之后,每个子应用必须明确自己的“样式边界”——要么使用 CSS Modules / CSS-in-JS 局部注入,要么通过构建工具统一加前缀,禁止裸写全局选择器。
1.4 信号四:子应用通信没有任何协议
我们当时的通信机制是“自由市场”:基座往子应用传 props,子应用在 window 上挂全局事件,另一个子应用再监听。查一个数据流问题,经常要顺着事件名在代码里搜半天;更恶心的是没人记得哪个事件是在哪里被清理的,页面切走之后,事件监听还挂在 window 上。
通信标准化不是要规定“只能用某一种通信方式”,而是要规定“什么时候能通、怎么通、怎么断”。后面我会详细说我们定下的通信契约。
1.5 信号五:生命周期管理名存实亡
qiankun 要求子应用导出bootstrap、mount、unmount三个生命周期,但很多子应用只是敷衍地导出空函数,真正的资源清理(定时器、全局监听、WebSocket 连接)全靠浏览器自动回收。结果就是:子应用切走之后,定时器还在跑,全局监听还在收,内存和 CPU 被白白占用。
容器标准化的核心,就是要让生命周期从“形式校验”变成“实质约束”——unmount 时必须把接管的东西全部交还。
这五个信号指向同一个根因:主应用和子应用之间没有清晰的职责边界,也没有一份“接入契约”。所以改造的第一步不是写代码,而是定契约。
2. 先定契约再写代码:子应用接入容器的六项契约
容器标准化最关键的不是技术选型,而是“约定”。我参考了 qiankun 官方文档、single-spa 的生命周期模型,结合我们自己项目的历史包袱,整理出了一份“子应用接入契约清单”。这份契约既是文档,也是校验逻辑,最终用 JSON Schema 固化到了配置里。
2.1 生命周期契约
每个子应用必须在入口文件导出bootstrap、mount、unmount三个函数,格式如下:
export async function bootstrap() { // 只做一次性初始化,比如建立全局配置 } export async function mount(props) { // props 包含 container、setLoadingState、globalState 等容器注入的能力 // 在这里挂载应用、注册路由、启动业务逻辑 } export async function unmount() { // 清理:卸载 React/Vue 实例、清除定时器、解绑全局事件、断开 WebSocket }契约要求里明确几条硬性规则:
- mount 必须返回一个“清理函数列表”,或者把需要清理的资源注册到容器提供的
registerCleanupTask里,容器在 unmount 阶段统一执行。 - unmount 必须是幂等的,重复调用不能报错。
- bootstrap 里禁止做 DOM 操作和业务初始化,因为它可能在应用挂载前就被调用。
我们发现,如果只靠“约定”没有“强制”,很多子应用还是会漏。所以容器层规定:子应用unmount后,容器会做一个“全局痕迹巡检”——检查 window 上新增的属性是否被清理,检查定时器数量是否回落,检查 DOM 中是否残留子应用的根节点。巡检不通过就上报告警,这个机制后面还会细说。
2.2 路由契约
路由契约解决的是“这个子应用管哪些 URL”的问题。标准化的约定是:
- 每个子应用独占一个一级路由前缀,比如
/finance/*、/hr/*。 - 子应用只能在自身前缀下注册路由,不允许跨前缀操作。
- 基座只在配置表里维护“前缀 -> 子应用 name”的映射,不关心子应用内部路由结构。
- 子应用内部路由跳转必须使用相对路径,避免写死完整 URL。
qiankun 在activeRule里匹配路由前缀,我们把它提到了配置层:
// 子应用配置示例 { name: 'finance', entry: '//localhost:7101', container: '#subapp-container', activeRule: '/finance' }路由契约带来的直接好处是:新增子应用不再需要动基座路由代码,改配置即可。
2.3 数据通信契约
我们最终采用了“受控通信”模式:容器通过 props 向子应用注入一个channel对象,子应用只能通过这个 channel 发布和订阅事件,禁止直接往 window 上挂全局变量。
// 容器注入的 channel props.channel = { publish: (topic, payload) => { // 统一事件中心:带命名空间、日志、权限校验 }, subscribe: (topic, handler) => { // 返回取消订阅函数 }, // 支持跨子应用同步状态 shareState: (state) => { /* ... */ } }通信契约里的关键规则:
- 事件主题必须带命名空间前缀(例如
finance:refresh、base:user-updated),防止冲突。 - 发布和订阅都要走容器提供的 channel,不允许子应用之间直接引用对方实例。
- 所有订阅在 unmount 时由容器统一清理,子应用不需要(也不允许)手动挨个解绑。
这样一改,再查数据流问题就简单了:打开容器的事件日志,看 topic 和 payload 一目了然。
2.4 样式空间契约
样式隔离我们分了三个层面,契约里要求子应用必须同时满足:
| 层面 | 要求 | 实现方式 |
|---|---|---|
| 开发期 | 全局选择器必须带子应用前缀 | CSS Modules、BEM 命名、样式文件内约束 |
| 构建期 | 样式文件自动加前缀或局部注入 | PostCSS 插件、webpack 配置 |
| 运行期 | 容器启动严格隔离模式 | qiankun 样式隔离 + 关键场景 Shadow DOM |
契约中明确禁止的事项:禁止修改body、html的样式;禁止在mount之外动态向document.head插入全局style标签(组件库弹窗的场景有专门方案,后面讲)。
2.5 运行时基线契约
微前端最隐蔽的问题就是“运行时版本冲突”。A 子应用用的是 React 16,B 子应用用的是 React 18;基座自己还可能注册了全局 React。表面看不出来,一旦两个子应用同时挂载,hooks 顺序、事件系统、上下文就会互相干扰。
我们定的运行时基线是:
- 基座统一加载 React、ReactDOM、antd 等基础库,通过 webpack 的
externals暴露给子应用。 - 子应用构建时将这些依赖声明为 external,不打包进产物。
- 子应用里的基础库版本必须和基线版本兼容,否则容器在加载时直接拦截并提示升级。
这套“运行时基线契约”大大降低了多实例冲突的概率,代价是子应用升级基础库时会受限制。实际执行中,我们每半年评估一次基线版本是否升级。
2.6 异常与日志契约
最后一个契约是关于“异常怎么上报”的。以前子应用报错只会打到自己的控制台,基座完全不知道某个页面白屏了。标准化之后,容器会在子应用 mount 时注入一个reportError函数,子应用全局异常、接口异常、资源加载失败都要通过它上报。
// 容器提供的异常上报 props.reportError({ source: 'finance', type: 'javascript-error', message: 'xxx is not a function', stack: '...', meta: { url: location.href, errorTime: Date.now() } });异常日志同样带命名空间和采样率,避免上报风暴。
这六项契约定义清楚之后,容器的代码实现就有了依据。下一步就是把“契约”变成“容器能力”。
3. 容器改造实操:从业务主应用到通用容器
契约定完,我开始动手重构基座。整个过程拆成五步,每一步都有明确的产出物和验收标准。这一节会把核心代码和设计思路讲透,包括为什么这样做。
3.1 第一步:盘清家底,建立子应用资产清单
改造之前,我先做了一个子应用资产盘点,信息放进一个 Markdown 表格里,随后迁移到配置中心。表格长这样:
| 子应用名 | 路由前缀 | 技术栈 | 基础库版本 | 当前接入方式 | 负责人 | 依赖基座的特殊逻辑 |
|---|---|---|---|---|---|---|
| finance | /finance | React 17 | antd 4.x | props 传 token,window 事件 | 张三 | 基座侧写死了菜单数据 |
| hr | /hr | Vue 2 | element-ui | 直接调用基座全局函数 | 李四 | 需要基座提供上传地址 |
| portal | /portal | React 18 | antd 5.x | 独立运行 | 王五 | 无 |
这张表和之后的 JSON 配置是一一对应的。盘点过程中你会发现很多“隐藏依赖”,比如某个子应用偷偷在window.__POWERED_BY_QIANKUN__下做了分支逻辑,或者从基座 import 了一个工具函数。这些都是标准化要清理的对象。
3.2 第二步:注册机制——配置驱动,代码零侵入
容器标准化的第一步是实现“子应用注册表”。我直接把所有子应用配置抽到一个micro-apps.config.json里,通过配置中心下发,基座启动时拉取并注册。
[ { "name": "finance", "entry": "https://micro.example.com/finance/index.html", "container": "#subapp-container", "activeRule": "/finance", "props": { "runtime": "react17", "theme": "dark" }, "sandbox": true, "prefetch": false }, { "name": "hr", "entry": "https://micro.example.com/hr/index.html", "container": "#subapp-container", "activeRule": "/hr", "props": { "runtime": "vue2", "theme": "light" } } ]对应在基座里的加载代码变成:
// app-container.js import { registerMicroApps, start, initGlobalState } from 'qiankun'; export function setupMicroFrontend(configList) { const apps = configList.map((cfg) => ({ ...cfg, props: { ...cfg.props, // 容器统一注入能力 channel: createChannel(cfg.name), reportError: createErrorReporter(cfg.name), registerCleanupTask: createCleanupRegistry(cfg.name), globalState: getGlobalState() } })); registerMicroApps(apps, { beforeLoad: [/* 全局 loading */], beforeMount: [/* 权限校验 */], afterUnmount: [runContainerCleanupCheck], }); start({ prefetch: false, sandbox: true, singular: false }); }配置驱动的核心好处,是“新增子应用不再发布基座”。运营侧要把一个新系统接进来,只需要在配置中心加一段 JSON,基座自动感知并注册路由。我们甚至做了一个管理后台,非技术人员也能完成接入申请。
3.3 第三步:加载器与生命周期托管
qiankun 本身提供了loadMicroApp和registerMicroApps,但直接使用还是有不少重复劳动:加载失败重试、资源预加载、超时处理、错误边界。我封装了一个loadAppWithPolicy函数,把这些策略统一收敛。
// loader.js const LOAD_TIMEOUT = 5000; const RETRY_TIMES = 2; export async function loadAppWithPolicy(appConfig) { let retries = 0; while (retries <= RETRY_TIMES) { try { const app = await loadMicroApp(appConfig, { sandbox: { strictStyleIsolation: process.env.NODE_ENV === 'production' }, singular: false, }); return app; } catch (err) { retries += 1; console.warn(`[container] load ${appConfig.name} failed, retry ${retries}`, err); } } // 触发全局错误上报 reportFatalError(appConfig.name, 'LOAD_FAILED'); renderLoadFailedPage(appConfig.name); return null; }生命周期托管方面,容器维护了一个WeakMap来跟踪每个子应用的清理任务:
// cleanup-registry.js const cleanupRegistry = new Map(); // name -> Set<Function> export function createCleanupRegistry(appName) { return function registerCleanupTask(task) { if (!cleanupRegistry.has(appName)) { cleanupRegistry.set(appName, new Set()); } cleanupRegistry.get(appName).add(task); }; } export async function runCleanupTasks(appName) { const tasks = cleanupRegistry.get(appName) || new Set(); for (const task of tasks) { try { await task(); } catch (e) { console.error(`[container] cleanup task of ${appName} failed`, e); } } cleanupRegistry.delete(appName); }然后在afterUnmount钩子里调用:
afterUnmount: [ async (app) => { await runCleanupTasks(app.name); runContainerCleanupCheck(app.name); } ]runContainerCleanupCheck就是我们前面说的“痕迹巡检”,它会在子应用卸载后 1 秒检查:
- window 上是否多出了该子应用之前没有的属性;
document.head里是否残留该子应用注入的 style、script;- 定时器数量是否异常增长;
- DOM 节点是否还有子应用的根元素。
巡检发现异常不阻断业务,但会上报到日志平台,形成“待优化清单”,再反向推动子应用修复。这个机制非常有效——子应用原来一直以为自己的清理逻辑没问题,巡检数据一出,全都老实了。
3.4 第四步:容器自身的 UI 隔离
容器不只是逻辑层,还有 UI 层。传统基座里的侧边栏、顶栏、面包屑、用户信息区,都属于“容器 UI”。这些 UI 在微前端里有个特殊性:它要跨子应用保持稳定,不能被子应用样式污染,也不能污染子应用。
我之前见过一个案例:子应用里用了position: fixed的抽屉弹窗,结果弹窗跑到了基座导航栏底下,整个层级乱掉。后来统一方案是:容器 UI 全部使用 Shadow DOM 包裹,基座的样式和子应用彻底隔离。
// container-ui.js const host = document.getElementById('container-shell'); const shadowRoot = host.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = ` <style>/* 容器自己的样式,全部放进 Shadow DOM */</style> <div id="sidebar"></div> <div id="topbar"></div> <main id="subapp-container"></main> `;Shadow DOM 方案的副作用是:基座 UI 和子应用的“全局弹窗”如果涉及跨层级交互(比如子应用弹窗要显示在导航栏之上),会变得很麻烦。所以我们最终只对“纯容器 UI”用 Shadow DOM,子应用挂载区保持普通 DOM 结构,弹窗问题通过子应用侧的挂载节点配置解决。
3.5 第五步:基座瘦身与业务下沉
这一步属于“组织架构调整”了。容器改造前的基座代码里混着大量业务组件:登录页、个人中心、审批流、报表查看器……这些其实都不应该住在基座里。我把它们逐一拆出来变成独立子应用,或者下沉到公共组件库。
瘦身之后的基座只保留四类代码:
- 微前端容器加载与生命周期管理;
- 全局用户态、权限态、主题态的管理;
- 容器 UI(导航、页头、页脚);
- 公共基础库(React、antd 等)的暴露与版本管理。
基座瘦身直接带来两个直观变化:一是构建体积从 8MB 降到 1.2MB,二是发布频率从每周三次降到两周一次。
4. 沙箱与样式隔离的边界控制:最容易翻车的两个点
容器标准化改造中,真正技术难度最高的不是加载器,而是“边界控制”——JS 沙箱的边界,和样式隔离的边界。这两个问题如果处理不好,上面所有契约都会变成空谈。
4.1 JS 沙箱:qiankun 的沙箱机制和我们的取舍
qiankun 的沙箱是它的核心能力,按场景分为三种:快照沙箱(LegacySandbox)、代理沙箱(ProxySandbox)、多实例沙箱。简单说明一下:
- 快照沙箱:进入子应用时把 window 上的属性快照保存,卸载时恢复。适合不支持 Proxy 的老浏览器,但只能同时运行一个实例,而且性能开销随属性数量增长。
- 代理沙箱:基于 Proxy 实现,子应用对 window 的访问都会走代理层,不会真正污染全局 window。可以多实例共存,是默认且推荐的方式。
- 多实例沙箱:在代理沙箱基础上支持多个子应用同时挂载。
标准化改造里,我选定的是 ProxySandbox +singular: false(允许多实例并行)。要注意的是,沙箱并不等于“完全隔离”,它在两个场景下会漏:
- 子应用里使用了
eval、new Function动态执行代码,这些代码可能绕过沙箱代理; - 子应用创建了 Worker、WebSocket、IndexedDB 等不在 window 作用域内的全局资源,沙箱管不到。
针对第一类,我们在 CSP(内容安全策略)里禁止了unsafe-eval,并在加载时扫描产物里的动态执行代码,发现就打回。针对第二类,只能在卸载巡检里检查 Worker 和网络连接是否清理。
从我们线上实践看,沙箱的概率性“漏”主要出现在第三方 SDK 上,比如埋点 SDK 会在 window 上注册一堆东西,卸载不干净。所以容器层专门维护了一份“已知问题 SDK 清单”,遇到就提示子应用避开或升级。
4.2 样式隔离:从“约定”到“强制”的三层防御
样式隔离在 qiankun 里有两种内置方案:
experimentalStyleIsolation: true:qiankun 会在装载时给子应用根容器加一个><Modal getContainer={() => document.querySelector('#finance-root')} // ... >这个改动之后,弹窗渲染在拥有 data 属性的容器内,样式作用域就匹配上了。如果你的子应用用了多个组件库,每个组件库的弹窗类组件都要过一遍这个配置。
第五步,验证。逐个子应用打开“弹窗类”功能做回归:Modal、Drawer、Dropdown、Tooltip、Select 的远程搜索下拉、DatePicker 的浮层。我把这些列成了一个标准回归清单,每次容器发布前都跑一遍。
这条链路走完,你会发现样式隔离不是“一个开关”能解决的事,它需要在构建、运行时、组件层三个层面同时做约束。这也是容器标准化比单纯搭一个 qiankun demo 复杂得多的地方。
4.4 全局样式漏出的另一个隐蔽案例:字体加载
还有一个隐蔽的样式污染:子应用里通过
@font-face注册了自定义字体,字体加载会注入style标签并修改全局的font-family。表面看只是字体变了,实际上会导致所有子应用的文字、图标渲染异常,特别是使用 iconfont 的项目,图标全部变成方块。这个问题我们在“运行期样式隔离”里拦不住,因为
@font-face是动态插入的全局样式。解决方案分两步:第一步是让子应用把字体资源也纳入“资源契约”,优先使用基座提供的统一字体方案;第二步是容器开启strictStyleIsolation时,通过 MutationObserver 监听 style 标签的插入,遇到非白名单的字体声明就拦截并上报。5. 存量应用平滑迁移与灰度验证
容器标准化改造最怕的一件事,就是“一夜之间把 8 个子应用全部切到新容器”。万一出事,回滚都来不及。我的做法是“试点先行、逐批迁移、灰度放量”,每一批都有明确的双跑和回滚策略。
5.1 迁移顺序怎么定:先软后硬
第一批迁移的子应用应该满足三个条件:功能简单、用户量小、依赖的基座特殊逻辑最少。我当时选了内部的工作台子应用,因为它只有几个查询页面、几乎没有弹窗、用户是内部的几个团队。这个选择让问题暴露范围最小。
迁移前我会做一次“依赖审计”,把子应用对基座的所有“特殊依赖”列出来,比如直接引用基座全局函数、依赖基座提供的上传地址、甚至是从基座 import 组件。列出来之后逐一替换为“契约方式”:
- 基座全局函数 -> 通过 props 注入
- 基座提供的地址 -> 通过配置下发
- 从基座 import 组件 -> 抽到公共组件库,或改为子应用自身依赖
这个审计表也是后续每个新子应用接入的必填材料。
5.2 双跑与比对:新老链路并行验证
为了让迁移风险可控,我们保留了老基座(旧容器)和新基座(标准化容器)并行运行,通过 nginx 按路由前缀分流。同一时间只有部分子应用切到新容器,其余还在老容器。
双跑阶段我会重点比对四类数据:
- 接口请求是否一致(尤其是请求头、鉴权参数);
- 登录态是否同步(老容器下登录的子应用,切到新容器后能不能保持登录);
- 跳转路径是否一致(子应用内部跳转、子应用之间跳转、以及浏览器刷新后的路由恢复);
- 业务埋点上报是否正常(切到新容器后,埋点是否还能正确标识来源)。
在这个阶段我踩过最大的坑是:登录态在双跑时出现“两边不同步”。原因是老容器传 props 时把 token 直接放进了子应用的 state,新容器改成通过 channel 订阅用户状态,而子应用内部还是从 state 里读 token。排查后发现子应用根本没有重新 mount,只是状态没更新。后来在契约里加了一条:子应用 mount 时必须以 props 里的用户态为准初始化,不能用缓存。
5.3 灰度放量:白名单 -> 10% -> 50% -> 100%
双跑比对通过后,才开始灰度放量。灰度的策略是:
- 第一批:内部白名单账号(产品、研发、测试)先进新容器,观察一周,收集反馈;
- 第二批:切 10% 的真实用户流量,持续观察错误率和加载耗时;
- 第三批:逐步放量到 50%,关注接口 5xx 比例和页面崩溃率;
- 第四批:100% 放量,同时保留老容器的入口作为紧急回滚目标。
灰度期间的监控指标要落实到位:子应用加载成功率、平均加载耗时、JS 错误率、资源加载失败率、白屏率。容器把这些指标全部接到监控平台,并且设了告警阈值——任何一项指标超过阈值,就暂停放量并回滚到上一批次。
5.4 回滚预案:不赌“一定不会出事”
即使做了这么多准备,我还是建议把回滚预案写死。我们的方案是:
- 老容器构建产物保留,nginx 配置随时可以一键把流量切回老容器;
- 新容器的所有配置和代码打 tag,问题定位时能快速查出具体版本;
- 灰度期间每天写回滚演练脚本,确保 5 分钟内能完成全局回滚。
实际迁移过程中,我们还是发生了一次回滚:第二个子应用(hr)切到新容器后,它的旧版 Vue 实例在两小时后突然上报大量内存异常。原因很快定位到,但为了稳,仍然按预案把它对应的流量切回了老容器。修复后重新走灰度流程。这件事的教训是:回滚不是“失败了才用”,而是“不符合预期就要用”,等找到了根因再重新上线。
6. 说说这次改造的几个心得
容器标准化改造做完到现在,最大的感受是:微前端的复杂度并没有消失,它只是从“藏在基座代码里”变成了“显式的契约和配置”。基座稳定了,子应用解放了,但代价是整个团队必须共同遵守一套规则。
几个实操心得分享给大家:
第一,契约一定要用代码强制,不能只靠 README。我们一开始写了很完整的设计文档,结果子应用接入时照样各有各的写法。后来把契约做成了校验工具,接入时跑一遍扫描,不通过就不给注册权限,效果立竿见影。
第二,unmount 质量要作为子应用发布门禁。很多子应用的页面在浏览器里关掉,状态本来就该销毁,所以没人重视 unmount。但微前端里子应用是“活着”的,切走了只是隐藏,随时会再回来。泄漏一次不致命,泄漏多了就崩。把“卸载巡检”接入 CI 之后,团队对 unmount 的重视程度明显提高。
第三,公共依赖版本要快刀斩乱麻地统一。运行时基线契约刚开始推行时阻力很大,子应用维护者担心升级基础库会影响业务。后来我们出了一个“兼容矩阵”对比文档,明确哪些版本是安全的、哪些必须升级,配合容器层的版本拦截,半年内所有子应用都统一到了基线版本。
如果后续还想扩展,可以把这个容器做成 npm 包,新项目开箱即用;也可以考虑接多个基座(比如管理端容器、C 端容器),子应用一次接入、多端复用。容器只要保持“薄”、保持“通用”,它就永远只会是地基,而不是一个新的”大泥球“。