一、背景
商家后台前端代码目前代码量达到十万级,每个迭代团队需要在同一仓库中迭代几十个需求,在日渐庞大的巨石应用下如此活跃的迭代,开发效率与构建效率上给我们带来了一些挑战,我们需要优化以下几点:
- 代码构建体量大,随着时间推移,构建速度的优化空间较少。
- 巨石应用下各个业务模块没有做物理拆分,管理与维护难度提升。
- 应用粒度较粗,在发布节点上需要对应用做进一步拆分以优化发布粒度。
- 巨石应用下,组件与业务的关系需要梳理,避免出现重复开发的情况。
微前端是目前解决应用拆分的主要解决方案,但是由于其隔离性的机制使得各个子应用间完全隔离,使得用户在开发子应用时无法访问其他子应用页面,这对于各子应用存在关联关系需要同时访问开发的场景开发效率较低,并且目前市面上已经完全封装的主流微前端框架对我们来说是黑盒,无法做到高度自定义,无法满足特定拆分需求,因此我们决定采用模块联邦与大仓模式结合的方式解决以上问题。
二、目标
提升构建与维护效率
将巨石应用进行拆分,独立开发与部署、提升构建部署效率与代码维护效率。
虽是单个子应用开发,但可全量访问巨石应用模块
由于商家内部各应用间在业务上存在上下游的关系,我们在开发D子应用时,需要在其上游的C、B子应用中进行相关配置,它们之间是强绑定的关系,结论就是:需要在本地开发时,能够同时使用其他子应用的页面模块的功能。
三、技术方案
以上提到的提升构建与维护效率,目前微前端与MF方案都是支持的,关键点在于如何在本地开发能够访问到平台的全量功能。本地代码已经有了,启动本地进程即可,那么,其他子应用如何访问?
微应用代理
如果是从本地再启动其他应用,这其实就背离了应用拆分的初衷,同时也降低了本地开发的效率,同时随着子应用越来越多,本地进程也会越来越多,这可能会大大影响本地开发效率。看来本地启动行不通,那么从哪里访问呢?对了,线上不是可以访问到其他模块吗?但是怎么将本地开发与线上的模块结合呢?由于目前商家后台使用了MF方式在路由层或vue文件内远程引入MF模块,那么是否可以通过MF模块转发的方式将远程组件代理至本地组件呢?
以上的组件代理插件实质上是用于做MF组件加载管理的chrome插件,可以控制基座应用加载的模块是线上的还是本地。
代理方案如下
- 用户首次进入线上测试环境,会加载远程应用入口文件,代理插件会将入口文件根据用户配置转发至对应的本地地址。
- 进入路由访问,由于上一步对远程应用入口做了代理,加载的模块被代理至本地,这样用户就可以在线上访问到本地页面,实现本地开发。
- 在线上基座应用与本地应用间建立websocket链接,本地应用代码更改后通知线上基座应用刷新页面。
下面是具体实现:
generateRedirectUrl = (details: UrlDetailsType) => {
if (details.url.includes(MICRO_ONLINE_LOAD_PATH)) {
const redirectUrl = this.generateDefaultProxyUrl({ originUrl: details.url })
if (redirectUrl) {
console.log('触发代理', `${details.url}代理至${redirectUrl}`)
this.checkMicroAppStatus({ originUrl: details.url, redirectUrl })
return {
redirectUrl,
}
}
}
if (details.url.includes('t1-dev.dewu.net:98')) {
console.log("details.url.includes('t1-dev.dewu.net:98')", details.url);
const redirectUrl = this.generateOnlineUrlByLocal({ originUrl: details.url })
if (redirectUrl) {
console.log('触发代理', `${details.url}代理至${redirectUrl}`)
this.checkMicroAppStatus({ originUrl: details.url, redirectUrl })
return {
redirectUrl,
}
}
}
}
- 文件内容代理为本地文件后,此时对应的模块加载path还是会加载线上路由,这里同样需要做内容代理。
- 由于是基于线上测试环境开发,本地开发的页面不仅需要在线上展示,并且本地代码更新后需要触发线上页面更新,这是必不可少的步骤,我们基于websocket将本地与线上进行连接。
- 不同子应用动态设置socketUrl与PingUrl。
function getHost() {
if (process.env.SOCKET_SERVER) {
return new URL(process.env.SOCKET_SERVER);
}
return location;
}
function getSocketUrl() {
let h = getHost();
let host = h.host;
host = `localhost:${PORT}`;
const isHttps = h.protocol === 'https:';
return `ws://${host}`;
}
function getPingUrl() {
const h = getHost();
return `${h.protocol}//${h.host}/__umi_ping`;
}
- 建立websocket连接,并定时触发连接检测。
let pingTimer = null;
let isFirstCompilation = true;
let mostRecentCompilationHash = null;
let hasCompileErrors = false;
let hadRuntimeError = false;
const pingUrl = getPingUrl();
if (!window[`${APP_NAME}UmiEntry`]) {
const socket = new WebSocket(getSocketUrl(), 'webpack-hmr');
socket.addEventListener('message', ({ data }) =>
__awaiter(void 0, void 0, void 0, function* () {
data = JSON.parse(data);
if (data.type === 'connected') {
console.log(`[webpack] connected.`);
// proxy(nginx, docker) hmr ws maybe caused timeout,
// so send ping package let ws keep alive.
pingTimer = setInterval(() => socket.send('ping'), 30000);
} else {
handleMessage(data).catch(console.error);
}
}),
);
}
- 本地需求开发共享部署态代码的store与路由跳转。
- 需求开发完成后进行单个应用部署,由于是本地代理,不影响测试访问。
基于模块联邦
前面的代理机制依赖于MF的远程加载,模块联邦加载机制可参考「掘金」平台中题目为“最详细的Module Federation的实现原理讲解” 这篇文章。基于模块联邦的微前端落地方案可以参考之前的一篇文章 基于Module Federation的模块化跨栈方案探索。
本地与部署态基座应用通过MF方案加载子应用,同时部署态新增动态加载保证远程组件的实时性,在加载入口文件处进行监控告警。
基座应用为部署态,在进行MF加载时,通过chrome插件动态控制加载子文件路径,开发态子应用共享部署态代码的store,路由注册等基础配置。
加载态依赖chrome插件做动态代理,实现本地与其他测试环境构建代码的动态切换,同时子应用与部署态代码建立websocket代码更新链接,在子应用更新代码时,实时刷新线上页面。同时支持端口的动态配置,一键关闭。
效果
以上介绍加载链路保证了构建部署提速与功能的完整,较好的解决了应用拆分功能不完备问题。本次架构优化将构建由15s减少至2.0s。业务需求部署速度由8min减少至2min。
四、应用拆分
大仓模式
应用拆分只是目的,要实现这个目标不仅仅要做拆分,对于商家后台来说各个应用间的复用同等重要,由于是业务解耦,这意味着各应用间存在更多可复用的功能与模块。
同时不仅是商家后台的部分模块也会在交易后台使用,既要保证应用业务的解耦,同时要保证组件充分复用,大仓模式是目前最合适的方案。
大仓模块化共享
由于商家后台各个子应用由于同属商家整条业务链路,存在众多可共用的组件和模块,而npm发布模式本身给业务组件与业务项目带来了一定隔离性,同时因为各子应用业务上存在关联,很多大型模块需要被多个子应用引入,而这些大型模块的迭代通常比较频繁,同时需要对业务请求进行封装。这里我们使用了基于大仓模式的源码引入以达到代码共用的目的。组件开发链路如下:
这里体现的是源码引入的方式,在构建态进行通用模块的打包构建,这一点目前能跑通的背景是商家后台本身是一个完整的应用,现有的模式同样是一个组件被多个模块所使用,同时测试阶段也是全量回归。以下是大仓组件基础链路:
- 组件构建发布使用标准的cli规范。
- 在提交MR节点与发布节点新增自动化卡口。
- 通过依赖分析自动化检测单测运行范围。
- 组件发布时发布通知,提醒组件使用者,并运行业务单测。
之后会对该部分做详细介绍。
五、总结与思考
单应用构建->单页面构建?
以上主要讲述了MF方案如何将本地结合线上开发,这里仅对微应用级别做了解耦,基于MF的模块化实现,由于remoteMicro实质上是创建了一个引用路径到require函数的映射然后代理至本地,那么对于不同模块,在能力上是具备模块化代理的能力的,基于目前MF按需构建(仅构建暴露出去的组件模块)的规则,我们可以对某个模块的映射对象里的xxx.async.js做代理。这样就可以实现页面粒度的按需构建,在部署构建提速上有很大潜力。
本篇文章主要介绍了如何对商家巨石应用做拆分,包括拆分方案的介绍,如何同时保证单个构建与功能完整性,并且针对微应用代理加载进行了进一步探索,接着介绍了大仓模式下需要遵循的规范以及未来的规划。大仓模式目前在前端平台已经持续不断地完善,将来应该会针对此模式做更详细的介绍,在拆分这件事情上,对于构建本身或许能被更加细粒度化,构建文件的代理本质上减少了代码的构建量,目前是通过人为控制的方式,此次验证了模块联邦支持可代理与动态更改expose。基于这两个特性,是否能将构建做到更加局部化,这可能会成为构建优化的方向。应用拆分一方面提升了开发人员的开发与部署效率,同时也对业务迭代流程做了业务解耦,明确了责任边界,更有利于后台应用的开发需求管理,降低需求代码维护成本。