一、遗留系统:路由分发
- 路由分发式微前端
- 适用场景
- 不同技术栈之间差异较大,难以兼容、迁移、改造
- 项目不想花费大量的时间在这个系统的改造上
- 现在的系统在未来将会被取代
- 系统功能已经很完善,基本不会有新需求
- 基于路由分发的Nginx配置示例
http { server { listen 80; server_name www.demo.com; location /api/ { proxy_pass http://www.test1.com/api; } location /web/admin { proxy_pass http://www.test2.com/web/admin; } location /web/notifications { proxy_pass http://www.test3.com/web/notifications; } location / { proxy_pass /; } }}
路由分发的测试
- 单元测试:URL验证
- 集成测试:URL重定向测试
二、遗留系统微前端:使用iframe作为容器
- 设计应用管理机制
- 应用加载、卸载时机
- 动画过渡
- 设计应用通信机制
- 通过iframeEl.contentWindow获取iframe元素的window对象
- 定义通信规范:事件名格式、何时开始监听事件、何时解绑监听事件
三、微应用化
- 形式和原理
- 构建时以单体应用的形式构建
- 运行时以应用模块的形式存在
- 原理:从多个项目中复制出代码,合并到一个项目中
- 架构实施
- 持续集成设计
- 子应用代码更新,触发对应模块的持续构建
- 主应用代码更新,触发整个系统的持续构建
- 一旦子应用持续构建成功,就会触发整个系统的持续构建
- 设计占位方式
- 子应用中指向其他子应用的路由
- 主应用中的子应用模块
- 测试策略
- 依赖一致性测试
- 比对子应用中package.json的依赖版本
- 功能模块生成测试
- 测试复制的模块能否复制到对应的目录上:编写脚本,在持续集成的过程中运行测试脚本,如果没有检测到exit(-1),则持续集成构建失败
- 测试生成的模块代码大小是否正常:正常情况下每个chunk.js文件要大于空白的模块;同时在持续集成中运行脚本,并执行exit(-1)
- E2E测试
四、前端微服务化
- 微服务化设计方案
- 基座控制系统
- 主工程在运行的时候,会去服务器获取最新的应用配置
- 主工程在获取配置后,将逐一创建应用,并为应用绑定生命周期
- 当主工程监测到路由变化时,将寻找是否有对应的路由匹配到应用
- 当匹配到对应的应用时,则加载相应的应用
- 应用间事件
- 避免应用间事件名冲突
- 可以将应用名作为应用内事件前缀
- 设计路由响应机制
- 基座应用只负责自己相应的路由,剩下的路由用一个统一的模块来处理,如Angular中的入口模块AppModule
- 当基座应用的路由发生变化后,会发出对应的路由变化的全局事件
- 在子应用启动后,它们将监听是否有相应的路由变化
- 通用型前端微服务化:Single-SPA
- 基座部分
import * as singleSpa from 'single-spa';sinleSpa.registerApplication( 'app-1', () => import('app1/app1.js'), pathPrefix('/app1'));singleSpa.registerApplication( 'app2', () => import('app2/app2.js'), pathPrefix('/app2'));singleSpa.start()
- 子应用部分
- React例子
import React from 'react';import ReactDOM from 'react-dom';import singleSpaReact from 'single-spa-react';import Root from './root.component.js';const reactLifecycles = singleSpaReact({ React, ReactDOM, rootComponent: Root, domElementGetter});export function bootstrap(props) { return reactLifecycles.bootstrap(props);}export function mount(props) { return reactLifecycles.mount(props);}export function unmount(props) { return reactLifecycles.unmount(props);}
- Angular例子
const ngLifecycles = singleSpaAngular({ domElementGetter, mainModule, angularPlatform: platformBrowserDynamic(), template: ``, Router,})
- 优缺点
- 优势
- 能支持大部分主流的前端框架,也能支持传统的前端框架
- 提供更好的用户体验,即不需要页面跳转,直接在当前页面载入
- 方便迁移旧的遗留系统
- 缺陷
- 系统构建复杂,应用需要集成在一起进行构建
- 不支持不同应用的部署分离
- 代码结构复杂
- 有额外的大量学习成本
- 定制型前端微服务化:Mooa
- 主工程部分
- 初始化微前端框架相关的配置
- 从远程获取所有子应用的配置
- 配置所有的子应用
- 进行主工程和子应用的路由配置
- 监测路由变化,一旦匹配到对应的路由,则加载对应的应用,并卸载旧的应用
- 子应用部分
- 路由更新
- 适配框架的基准URL
- 解决依赖冲突
- iframe隔离
- 为不同的子应用分配ID
- 在子应用中进行hook,以通知主应用:子应用已加载
- 在子应用中创建对应的事件监听,来响应主应用的URL变化事件
- 在主应用中监听子程序的路由跳转等需求
五、组件化微前端:微件化
- 运行时编译微件化:动态组件渲染
- Vue微件化
- 编写应用容器模板
"app-2"> "message"> 鼠标悬停几秒钟查看此处动态绑定的提示信息!
- 编写实例化应用脚本
var app2 = new Vue({ el: '#app-2', data: { message: '页面加载于' + new Date().toLocaleString() }}
- 将模板和脚本放置在远程,在需要调用的地方创建与容器id对应的元素,接着运行相应脚本即可
- Angular微件化
- 在核心工程中提供微件化所需要的环境
SystemJs.set('@angular/core', SystemJs.newModule(angularCore));SystemJs.set('@angular/common', SystemJs.newModule(angularCommon));SystemJs.set('@angular/common/http', SystemJs.newModule(angularCommonHttp));
- 在微件工程里,构建出能运行在核心工程的代码
// 导入微件const module = await SystemJs.import(widget.moduleBundlePath);// 编译模块const moduleFactory = await this.compiler.compileModuleAsync(module[widget.moduleName]);// 解析 ComponentFactoryconst moduleRef = moduleFactory.create(this.injector);const componentProvider = moduleRef.injector.get(widget.name);const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(componentProvider);// 编译组件this.content.createComponent(componentFactory);
- 预编译微件化
- React微件化
- 编写统一的API,封装React的根组件
export const init = (config) => { ReactDOM.render(<App/>, document.getElementById('root')); serviceWorker.unregister();}
- 修改构建系统的配置,让其输出可在浏览器上直接运行的版本
var config = { // ... output: { // ... library: 'MyApp', libraryTarget: 'umd', umdNameDefine: true, }}
- 加载对应的JavaScript脚本,并调用组件的初始化方法