逻辑隔离
单体应用软隔离
与硬隔离相对的,笔者称为单体应用软隔离,其更多地依赖于应用框架或者开发构建流程,来实现容错与样式、DOM 等隔离。单体应用软隔离又可以从应用的组合时机与技术栈的支持情况这两个维度,划分不同的解决方案。对于需要支持不同技术栈(React, Angular, Vue.js, etc.)的场景,我们往往需要彻底的类后端微服务化,每个前端应用都是独立的服务化应用,而宿主应用则提供统一的应用管理和启动机制;此时若需要解决资源重复加载、冗余的问题,则需要依赖统一构建或者由宿主应用提供公共依赖库,子应用打包时仅打包自身或非公用库代码。如果是相同技术栈的场景,那么我们可以方便地利用框架本身的懒加载能力,在开发阶段以模块划分为微应用进行开发,构建时以单体应用的形式构建,在运行时是以应用模块的形式存在。
Application Composition | 应用组合
典型的应用组合方式分为构建时(Build Time)组合与运行时(Runtime)组合,如下图所示即是典型的构建时组合方案:
构建时组合的优势在于能够进行较好地依赖管理,抽取公共模块,减少最终的包体大小,不过其最终的产出仍是单体应用,各个应用模块无法进行独立部署。与之相对的,运行时组合能够保障真正地独立开发与独立部署:
运行时组合中,我们可以选择在使用 Tailor 这样的工具进行服务端组合(SSI),也可以使用 JSPM, SystemJS 这样的动态导入工具,进行客户端组合。运行时组合同时能提供按需加载的特性,优化首页的加载速度。不过运行时组合可能重复加载依赖项(通过浏览器缓存或 HTTP2 适度解决),并且不同于 iFrame 的硬隔离,运行时组合仍可能面临难以预料的第三方依赖冲突。
React 这样的声明式组件框架,天然就支持应用的组合,我们可以传入渲染锚点以进行应用组合,也可以将不同框架的应用封装为 Web Components。首先我们可以将 React 应用定义为自定义元素:
📎 完整代码参考 fe-boilerplate/micro-frontend
window.customElements.define(
'react-app',
class ReactApp extends HTMLElement {
...
render() {
render(<App title={this.title} />, this);
}
...
}
);
然后在前端中直接使用该自定义元素:
<react-app title="React Separate Running App" />
在单体应用中,框架将路由指定到对应的组件或者内部服务中;而微前端中,我们需要将应用内的组件调用变成了更细粒度的应用间组件调用,即原先我们只是将路由分发到应用的组件执行,现在则需要根据路由来找到对应的应用,再由应用分发到对应的组件上。具体的实践中,可能宿主应用使用 Hash Router 已经占用了 Hash 标记位,那么就需要为子应用提供专属的查询键,来进行子应用内跳转。
应用隔离与治理
在 React 中可以使用 ErrorBoundary,来限制应用崩溃的影响;如果是自定义的应用加载器,也可以实现 Promise 容错方案。Redux 可以考虑在宿主应用创建统一的 Store,每个应用中按照命名空间划分使用子状态空间:
const subConnect = (subAppName) => (mapStateToProps, mapDispatchToProps) =>
connect(
(state) => mapStateToProps({ ...state[subAppName] }, state),
mapDispatchToProps
);
对于 Action 可以使用命名空间形式:
`app/service-name/action`;
而对于应用治理方面,single-spa 或者 ueact-component 都定义了跨框架的组件生命周期,譬如在 single-spa 中,可以将 React 生命周期归一化:
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
domElementGetter: () => document.getElementById("main-content"),
});
export const bootstrap = [reactLifecycles.bootstrap];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];
然后将其导出为单一应用并且异步加载:
// src/index.js
import { registerApplication, start } from "single-spa";
registerApplication(
// Name of our single-spa application
"root",
// Our loading function
() => import("./root.app.js"),
// Our activity function
() => true
);
start();