很多团队在业务系统越做越大的时候都会撞上同一堵墙:代码仓库膨胀、前端模块互相纠缠、发版要全员陪着熬夜、想给某个子系统单独扩容却找不到边界。我去年带着团队把一个单体后台管理系统拆成了基于Java后端与Vue前端的微前端架构,并在此基础上搭建了业务系统拆分与集成平台。整个过程弯路走了一些,但沉淀下来的设计思路和代码骨架,对正在做同样改造的团队应该挺有参考价值。这篇文章就把模型设计、拆分思路、集成方案和部分核心代码一次性讲透。
1. 不拆不行:单体系统在业务膨胀后暴露出的真实痛点
先说背景。我们原先的系统的前端是一个Vue 2的单体工程,后端是Spring Boot的单体服务。这个架构在业务初期非常舒服,一个人能从头撸到尾,联调成本低,部署也简单。但等业务线从两条变成六条,前端代码超过20万行,后端模块互相调用形成网状依赖之后,问题就变成了一颗定时炸弹。
最典型的表现是发版效率直线下滑。前端任何一个小模块改了样式,整个系统都要重新构建,构建一次要跑十几分钟;发布窗口从下午两点开始,搞到晚上八九点是常态。后端更麻烦,订单模块一个慢SQL拖垮了全站的接口,但那个出问题的查询是三个月前别的小组写的,现任负责人都说不清那段逻辑。还有一个隐性成本是技术升级被锁死,我们想给某个报表模块引入Web Worker优化大数据量渲染,但因为模块之间共享了全局状态和大量全局组件,谁都不敢轻举妄动。
这些问题单靠管理手段解决不了,纯粹是架构层面把不该耦合的东西耦合在一起了。我当时在技术方案评审时给管理层算了一笔账:系统每延迟发布一次,业务方就少一周时间做市场活动;每一次全量回归测试,光人工成本就在四位数以上。结论很明确,系统必须拆。
但拆分也有两种路线。一种是后端做微服务,前端继续一坨;另一种是前后端一起拆,前端走微前端。只拆后端不拆前端的问题在于,前端还是会因为业务模块太多而持续膨胀,而且后端拆成微服务之后,前端的调用关系更复杂了,单体前端反而成了新的瓶颈。所以团队最终拍板的是前后端联动拆分,前端用微前端架构,后端按业务域拆服务,中间通过统一的网关和注册中心打通。
这里需要澄清一个容易混淆的概念:微前端拆的不是页面,而是业务模块的边界。如果你只是把路由拆成几个懒加载的组件,那不叫微前端,那叫代码分割。真正意义上的微前端,是让每个业务子应用拥有独立的开发、构建、部署生命周期,子应用之间在运行时互不阻塞。这也是我们选择qiankun作为基座框架的核心原因,它把JS沙箱、样式隔离、生命周期管理这些脏活累活都封装好了,团队上手成本低。
2. 架构设计:平台由哪些核心模块组成,各自承担什么职责
拆分与集成平台的整体架构我画过不下十版,最终落地的方案是四个核心模块加一条贯穿始终的链路。四个模块分别是基座主应用、业务子应用、后端拆分服务和集成编排层。这条链路就是用户从浏览器发起的每一次请求,在主应用、子应用、后端服务之间的流转路径。
2.1 基座主应用:负责承载和调度,不负责具体业务
基座主应用的定位是壳,它只做三件事:加载子应用、管理公共登录态、维护全局导航骨架。我见过不少团队把公共业务逻辑也塞进基座,比如把订单列表页直接写在主应用里,这是非常糟糕的做法。基座一旦膨胀,就会重新变成一个大泥球。
我们在基座里预留了路由注册表,每个子应用通过registerMicroApps注册之后,主应用会把子应用的activeRule与导航菜单做映射。这样用户从菜单点击进入子应用时,实际上触发了主应用的路由变化,进而唤起了子应用的挂载。这里还要注意一个细节,基座不能依赖任何子应用的私有依赖,它的package.json要尽量干净。我们基座只引入了Vue、qiankun和一套公共UI组件库,其他东西全都不装。
2.2 业务子应用:按业务域切分,独立仓库独立部署
子应用的划分遵循高内聚低耦合原则。我们把订单、商品、用户、营销、报表、权限这六个大域拆成了六个Vue子应用,每个子应用拥有独立仓库、独立package.json、独立的构建产物。子应用之间不允许直接互相引用组件或工具函数,如果确有需要,必须通过主应用提供的公共依赖进行。
子应用在qiankun架构里有两种运行模式,一种是构建时打包成umd格式的单一bundle,还有一种是用webpack的externals配合运行时加载。我们用的是前者,简单直接,每个子应用build之后产出一个入口JS文件和一个CSS文件,基座通过import-html-entry去拉取并执行。实际测试下来首屏性能比原先的单体应用反而好,因为子应用只在路由命中时加载,天然做了按需加载,代价是第一次进入某个子应用时有一小段白屏时间,这个可以通过骨架屏优化。
2.3 后端拆分服务与集成编排层:Java端如何配合前端拆分
后端不能假装看不见前端的拆分,否则前后端边界就错位了。我们沿着前端的六个业务域,把原有的Spring Boot单体服务拆成了六个独立部署的Java服务,服务之间通过OpenFeign做内部调用,所有外部请求统一经过Spring Cloud Gateway网关进行路由转发。
集成编排层是整个平台设计里最容易被忽视的部分。它的职责是处理跨域查询和数据聚合。比如用户管理页面需要展示用户的订单数量,这个数据在订单服务里,但页面挂在用户子应用下。我们没有让前端跨两个服务去请求,而是在网关后面加了一层BFF层,用Java的CompletableFuture并发调用两个服务,聚合成一个完整的响应返回给前端。BFF层同时承担了响应裁剪和字段映射的工作,前端拿到的是刚刚好够用的数据,不多不少。
2.4 数据模型与接口模型的设计思路
拆分后的接口设计要有一套统一的规范来兜底。我们约定所有子应用的接口路径统一以业务域开头,比如/order/、/user/、/product/,网关层按前缀做路由转发。参数对象和返回结构也做了统一封装,返回体固定为code、message、data三段式结构,分页参数固定为pageNum和pageSize,时间字段统一用时间戳传输,由前端统一格式化。
数据库层面的拆分没有一步到位,而是分了三期。第一期只拆分逻辑边界,物理上还是共库;第二期按核心交易域和支撑域分库,订单、商品独立成库;第三期才把报表类的大查询迁到独立只读从库。这样分期的好处是控制风险,每一期都能独立上线验证。现在回头看,如果一开始强拆物理库,光是数据迁移和双写就够我们喝一壶的。
3. 微前端集成的核心实现:qiankun基座下的主应用与子应用落地代码
理论说完了,直接进入代码。这部分我尽量还原真实项目里的写法,去掉花里胡哨的封装,保留最关键的骨架。你能跑通这一套,就已经具备了独立搭建微前端平台的基本盘。
3.1 主应用注册子应用的完整配置
先从主应用侧开始。安装依赖这一步不过多赘述,直接看main.js里的初始化逻辑。我们用的是qiankun 2.x版本,注册和启动的API非常稳定。
// src/main.js import { registerMicroApps, start, initGlobalState } from 'qiankun'; // 注册表:所有子应用的元信息都写在这里 const apps = [ { name: 'order-app', entry: process.env.VUE_APP_ORDER_ENTRY || '//localhost:5001', container: '#subapp-viewport', activeRule: '/order', }, { name: 'user-app', entry: process.env.VUE_APP_USER_ENTRY || '//localhost:5002', container: '#subapp-viewport', activeRule: '/user', }, { name: 'product-app', entry: process.env.VUE_APP_PRODUCT_ENTRY || '//localhost:5003', container: '#subapp-viewport', activeRule: '/product', }, ]; // 全局状态:登录态、用户信息、全局配置 const actions = initGlobalState({ token: localStorage.getItem('token'), userInfo: null, }); // 监听主应用状态变更,通知所有子应用 actions.onGlobalStateChange((state, prev) => { console.log('主应用状态变更:', state); }); // 注册并启动 registerMicroApps(apps, { beforeLoad: [app => { console.log('加载前:', app.name); }], beforeMount: [app => { console.log('挂载前:', app.name); }], afterUnmount: [app => { console.log('卸载后:', app.name); }], }); start({ sandbox: { experimentalStyleIsolation: true } });几个关键点给你们划一下。容器id是#subapp-viewport,每个子应用挂载前会替换这个容器的内容,所以这个div不要放任何静态内容。activeRule要跟主应用的路由前缀对齐,主应用用history路由模式时,activeRule匹配的是路径前缀。如果子应用挂在二级路径下,activeRule要把完整前缀写上,比如/crm/order。
3.2 子应用改造:Vue工程如何暴露微前端生命周期
子应用侧最关键的是改造入口文件main.js,让它暴露qiankun要求的三个生命周期:bootstrap、mount、unmount。我们用的是vue-cli创建的标准工程,改造并不复杂。
// 子应用入口 src/main.js import Vue from 'vue'; import App from './App.vue'; import router from './router'; import store from './store'; Vue.config.productionTip = false; let instance = null; // 渲染函数:负责用当前路由创建Vue实例 function render(props = {}) { const { container } = props; instance = new Vue({ router, store, render: h => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } // 独立运行时的引导逻辑,用于本地开发 if (!window.__POWERED_BY_QIANKUN__) { render(); } // 导出微前端生命周期 export async function bootstrap() { console.log('子应用启动'); } export async function mount(props) { render(props); } export async function unmount() { instance.$destroy(); instance = null; }这里有一个非常容易踩的坑是container参数。子应用被基座加载时,Vue实例不能再挂到自己的#app上,必须挂到基座传入的container.querySelector('#app')上。如果漏了这个,子应用会挂载到主应用的body上,轻则DOM错乱,重则样式污染全站。
3.3 子应用路由与打包配置的联动
路由和打包配置需要成对改。vue-router要设置base,而且必须读取基座传入的前缀,否则跳转会迷路。webpack打包要开启umd格式和publicPath动态设置。
// 子应用 src/router/index.js import Vue from 'vue'; import Router from 'vue-router'; Vue.use(Router); // 关键:base要取主应用路径前缀,qiankun会通过window.__routerBase__注入 const router = new Router({ mode: 'history', base: window.__POWERED_BY_QIANKUN__ ? window.__routerBase__ : process.env.BASE_URL, routes: [ { path: '/', name: 'Home', component: () => import('../views/Home.vue') }, { path: '/list', name: 'List', component: () => import('../views/List.vue') }, ], }); export default router;打包配置在vue.config.js里改,重点是output.library和libraryTarget。libraryTarget必须是umd,library这个值最好是全局唯一,避免主应用同时加载多个子应用时变量名冲突。
// 子应用 vue.config.js const { name } = require('./package.json'); module.exports = { publicPath: '/', // 服务器相对路径 devServer: { port: 5001, headers: { 'Access-Control-Allow-Origin': '*', // 开发环境必须允许跨域 }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${name}`, }, }, };配置文件里有不少细节值得单独说明。devServer的跨域头必须配,否则基座所在的端口跟子应用端口不一致时,浏览器会直接拦截样式文件和脚本文件的加载。jsonpFunction这行是为了防止多个webpack应用同时运行时,异步加载chunk的jsonp函数重名。生产环境的服务器也要给子应用入口文件配好CORS头,NGINX里加三行配置就能搞定,别等到上线了才想起来,排查一个跨域问题能磨掉你大半天。
3.4 主应用与子应用之间的通信机制
微前端通信是集成平台设计里最容易被问到的环节。qiankun官方提供的方案是initGlobalState,它实现了一套简易的全局状态总线。我们的用法是:主应用统一管理token和用户信息,任何子应用需要登录态时,从globalState读取;子应用要跳转其他域页面时,通过主应用下发跳转指令。
// 子应用内使用全局通信 // 1. 在mount生命周期里接收actions let globalActions = null; export async function mount(props) { render(props); globalActions = props.onGlobalStateChange || null; // 监听主应用状态变化 if (globalActions) { globalActions((state, prev) => { if (state.token !== prev.token) { // token刷新后同步更新业务里的请求头 localStorage.setItem('token', state.token); } }, true); } } // 2. 向主应用发起状态变更 function updateToken(token) { if (globalActions) { globalActions.setGlobalState({ token }); } }注意setGlobalState可以传部分字段,qiankun会用浅合并的方式更新state,所以不需要每次把整个state都传一遍。我实际开发中遇到的一个问题,是初始化时序:基座先执行start,子应用后mount,如果子应用在mount时立即setGlobalState,可能主应用还没来得及initGlobalState。稳妥的做法是子应用先读取props里的字段,再考虑主动更新。
4. 业务系统拆分的方法论:六个域怎么切、边界怎么定、公共代码怎么处理
架构代码只是骨架,真正的难点在怎么确定拆分边界。这块的决策直接决定后续半年你会不会天天救火。我在方案阶段做了三轮评审,中途推翻过两版方案,最后沉淀下来的方法论可以浓缩成三个词:业务驱动、数据反推、依赖解耦。
4.1 先画业务流程图,再定服务边界
拆解的第一步不是画架构图,而是把公司的核心业务流程一条条列出来。我们从订单创建这条主链路开始画泳道图,把涉及的角色、动作、数据实体全部标出来。画完之后发现订单流程跨越了库存、支付、优惠券、物流四个领域,这说明订单这个域天然是聚合根,它必须通过调用其他领域的服务来凑齐整个流程。
边界确定的一个硬性标准是:一个业务对象的一生只能由一个服务负责。比如商品的生命周期包括创建、上下架、编辑、删除、审核,这些操作全部由product-service承担,其他服务不能直接改商品表,只能调商品服务暴露的接口。这条规则执行起来要很坚决,否则拆完跟没拆没区别。
4.2 依赖方向:单向依赖是红线
我们内部有一个依赖图审查环节,每个服务只能依赖下游服务,不能出现循环依赖。比如订单服务依赖用户服务和商品服务,但用户服务不能反过来依赖订单服务。如果用户服务需要展示订单数量,方案是通过事件订阅或者BFF聚合,在接口层把两边数据拼装起来,而不是让user-service直接调order-service。
这个红线一旦破掉,微前端拆分就名存实亡了。因为前端的子应用划分和后端服务划分是对齐的,后端互相循环依赖,前端必然也要互相跳转、互相共享数据,最后变成一场灾难。我们在集成平台上加了一个依赖检查脚本,每次代码提交时自动分析Java代码里的import关系,发现跨越服务层的依赖直接拦截CI。
4.3 公共模块的三种归宿
拆分中最棘手的问题是公共代码。原来的单体工程里有一个common模块,里面什么都有,工具类、注解、枚举、DTO、常量,甚至连几个HttpUtil都有。这类代码的归属我们定了三条规则:
第一,纯工具类下沉到独立的基础库,比如DateUtils、StringUtils这种,打包成独立jar,所有服务引用。第二,业务相关的枚举和常量,必须在对应的服务里定义,不允许放在公共库里。比如订单状态枚举OrderStatus必须放order-service,如果user-service需要用到这个枚举值做判断,那就在参数上传递字符串,不依赖对方包。
第三,实体类和DTO一概不共享。服务之间通信通过自定义的APIRequest和APIResponse对象进行,这些对象挂在服务对外暴露的feign-client包里。为了让这条规则落地,我们的Maven私服上禁用了对老common包的发布功能,物理上堵住这条路。
4.4 数据库拆分时如何保证数据一致性
数据库拆分到物理分库后,最大隐患是分布式事务。我们没有引入Seata这类重量级框架,而是采用了一种比较务实的两阶段补偿策略。核心交易链路里,订单创建和库存扣减这两个操作,我们使用本地消息表加定时任务扫描的方式保证最终一致性。
本地消息表的原理不复杂:order-service在本地事务里同时写订单表消息表和业务表,然后异步把消息推给库存服务。如果库存服务处理失败,消息表里的记录会保持待重试状态,定时任务每十秒拉一次未处理消息重新投递。库存服务的消费逻辑要做幂等处理,我们用的是业务唯一键加消费记录表双重校验,确保重复消息不会重复减库存。
这种方案的优点是不用引入额外的基础设施,适合中小团队。缺点是需要业务侧配合做幂等设计。如果你们公司有比较成熟的消息中间件团队,也可以直接用RocketMQ的事务消息,效果更好。
5. 集成平台在重构中的关键作用:版本管理、灰度发布与自动化部署
微前端改造不只是技术重构,更是一次交付方式的变革。集成平台这个模块承载的正是这一部分能力,它让六个子应用、六个Java服务在持续交付上能够互不阻塞。
5.1 统一版本管理:Model模型如何映射到发布
我们设计了一个发布模型,把用户可见的版本号定义为平台版本,每个平台版本对应一组子应用版本和一个后端服务版本。发布记录表有三个关键字段:platformVersion、appVersionMap、serviceVersionMap。appVersionMap是一个JSON结构,记录order-app=1.2.3、user-app=2.0.1这样的映射关系。
有了这个模型之后,回滚变得很干净。如果线上出问题,只需要把platformVersion切到上一个版本的映射关系,基座加载子应用时就会指向历史构建产物。这里的关键是子应用的构建产物不能覆盖式部署,要在对象存储里保留历史版本,NGINX上按版本号作为目录区分。
5.2 灰度发布落地:先内部子应用后全量
微前端架构的灰度发布比单体要灵活。我们在基座层做了一个开关系统,通过query参数或者cookie把用户流量分桶。比如新上线的user-app 3.0版本需要灰度,我们把user-app的entry地址改成灰度地址,同时设置灰度规则白名单里包含某些测试账号和内部员工账号,命中灰度规则的就走新版本入口,其他用户继续走稳定版。
灰度到全量切换的操作非常简单,因为子应用entry只是改了一个URL字符串。但要注意样式文件的加载问题,灰度子应用和稳定版子应用如果并存,可能会出现样式文件被浏览器缓存串掉的偶发问题。解决方式是在子应用入口URL上额外挂一个版本号参数,每次发布版本号变化,浏览器会强制拉取新版本资源。
5.3 CI/CD流水线的模板化
我们为每个子应用和后端服务编写了统一的流水线模板。前端流水线的步骤是:代码拉取、依赖安装、单元测试、项目构建、产物上传对象存储、更新版本映射表。后面两步是关键,产物一旦上传就不可变,发布时只需要改映射表。后端流水线增加了镜像构建和Kubernetes滚动更新,步骤跟单体时的发布流程基本一致。
这套流水线跑起来之后,最直观的变化是发版时间从半天变成半小时以内。业务方提一个小需求,当天就能走完测试和发布,这在以前是不可想象的。而且由于每个子应用独立发版,某个模块临时上线修改也不会影响其他模块,产品经理再也不用等一个统一发版窗口了。
6. 沙箱隔离与样式冲突:微前端架构最容易翻车的地方
微前端平台上线三个月之后,我们统计了工单系统里反馈的前端问题,发现样式错乱和JS冲突占了将近六成。这两个问题几乎是每个微前端团队都会遭遇的坎,处理不好整个平台都会被钉在耻辱柱上。我单独开一节把我们的排查思路和最终方案讲清楚。
6.1 默认沙箱的边界与局限
qiankun的沙箱分为JS沙箱和样式沙箱。JS沙箱默认开启,原理是用ES6的Proxy对window对象做代理,子应用运行时的所有全局变量写入和读取都被拦截,应用卸载时再把变更的属性清除干净。这套机制在处理Vue这类框架时效果很好,因为Vue本身不太依赖真正的全局污染。
但有一种情况JS沙箱会失效:子应用通过document写入了一个script标签指向外部脚本,这个外部脚本直接操作window,不受Proxy约束。我们的营销子应用当时引入了一款第三方数据可视化SDK,就是这种玩法,上线后频繁出现页面卡死。最后我们把SDK的加载方式从动态注入改成了npm包引用,问题才解决。如果你也遇到类似情况,优先排查动态script标签。
样式隔离我实际跑下来,experimentalStyleIsolation的效果是给子应用的样式选择器加上scope属性,把它限制在子应用挂载的DOM容器里。但这种隔离不彻底,因为子应用的全局样式里如果定义了body或html标签的样式,仍然会穿透到主应用。我们最终的兜底方案是:强制约定子应用不得使用元素选择器写全局样式,所有组件样式必须带scoped。这一个约定解决了我90%的样式冲突。
6.2 弹窗和浮层挂载位置的坑
样式隔离还带出一个隐藏问题:子应用里的弹窗组件默认是挂载到body下面的,因为Element UI这些组件库的弹层是append到body的。一旦挂到body,就脱离了子应用的样式作用域,隔离效果直接失效,而且弹窗的z-index还会跟主应用的全局弹窗打架。
处理方案有两种。第一种是给组件库的弹层挂载点重新指定,Element UI的Dialog可以传append-to-body=false,并把弹层绑定到子应用根节点上。第二种是主应用和子应用统一约定z-index的管理规则,主应用的全局弹窗限制在z-index 1000以内,子应用弹窗往上叠加以100递增。我们采用了第一种为主,第二种为辅,目前弹窗类问题基本绝迹。
6.3 公共组件与依赖的共享策略再梳理
样式问题解决之后,我们把公共依赖的共享方式重新捋了一遍。基于qiankun自带的externa机制,我们指定了Vue、Vue Router、Element UI这三个包在子应用构建时不打包,运行时统一从主应用引入。这个做法看似简单,实际带来三个好处:子应用构建体积直接砍掉一半,构建速度从五分钟降到一分半;内存里只有一份Vue实例,沙箱隔离的压力变小;子应用之间跳转时,公共库的初始化时间不再重复计算。
这里有个前提要注意,externa共享依赖要求主应用和子应用的公共库版本必须完全一致。我们为此加了一条CI检查:子应用构建时对比主应用package.json里的依赖版本,不一致直接构建失败。版本冲突是微前端共享公共库时最容易爆雷的地方,把这个检查做成自动化之后,省了大量排障时间。
7. 系统验证与效果:拆分前后的性能、效率、稳定性对比
讲完实现,最后放一组我们平台上线跑通后的真实数据。拆分的价值最终要体现在这些可量化的指标上。
构建效率上,单模块平均构建时间从20分钟下降到1到3分钟,因为每个子应用只构建自己的代码。全量发版部署时间从一个下午压缩到40分钟以内,紧急修复一个子应用bug可以在15分钟内完成从提交到生效的全过程。
运行时性能上,首屏加载时间平均优化了约40%。原因很简单,单体应用初始化时要加载全量路由和全量组件代码,首屏一次加载三四兆JS;微前端架构下,初始加载只有主应用的壳子和当前命中的子应用资源,后续切到其他子应用时再动态加载。当然子应用切换时会有一段额外的下载和初始化开销,实测每个子应用首次加载约1到3秒,后续切换由于浏览器缓存存在,基本在500毫秒以内。
稳定性上,前后对比更加明显。单体架构时每个月平均一两次重大线上事故,往往因为一个报表查询拖垮了核心交易链路。拆分之后服务之间物理隔离,报表服务的CPU被打满,订单服务完全不受影响。过去三个月,核心链路零P0级故障。
团队协作效率的变化虽然没有硬数据,但感受很直接。以前一个前端仓库五十多个分支来回合,光解决冲突就要半天。现在六个子应用六个仓库,每个团队在自己的一亩三分地里干活,合并冲突几乎没有了。新同事入职后的上手速度也快得多,只需要关心自己负责的那个子应用和后端服务。
8. 复盘与建议:这套方案适合谁、哪里还可以做得更好
做了这么多改造,如果让我一句线概括微前端架构这件事,那就是:它不是银弹,但当你真的遭遇了多业务线并行开发和频繁发版的痛,它可能是目前工程实践里最成熟的那条路。最后给正在评估这套方案的团队三条建议。
第一,拆分前一定要想清楚自己的动机。如果你的系统只有两条业务线,团队只有几个人,一个月才发一次版,那完全没有必要上微前端。维护基座、处理沙箱问题、管理版本映射这些复杂度是实打实的新增成本。微前端解决的主要是组织级协作效率问题,不是代码级的技术炫技问题。
第二,拆分的过程要分步走,先做前端还是先做后端要根据团队情况来。我们是前端先行,因为前端的构建和发布瓶颈最痛。但如果你后端接口混乱、数据库耦合严重,那建议后端先做模块化的逻辑梳理,否则前端拆完发现后端还是一坨,联调时会非常痛苦。
第三,尽量在一开始就把自动化质量门禁做好。我们的单元测试覆盖率、代码扫描规则、静态依赖检查,都是在拆分后一个月内补齐的。这些门禁保证了六个子应用在高速迭代的时候不会往整体架构里注水。如果等代码已经写乱了再补基建,付出的代价要翻几倍。
另外有件事我一直想做但还没做完:多环境联调的统一入口。目前每个子应用在本地开发时要各自启动一个devServer,新同学上手要开六个终端窗口,非常劝退。后续的计划是做一个CLI工具,读取版本映射表一键拉起所有依赖服务的本地环境,让整个平台的开发体验回到单体时代那么清爽。这个工具做完再来分享。