微前端架构的几种技术选型随着SPA大规模的应用,紧接着就带来一个新问题:一个规模化应用需要拆分。一方面功能快速增加导致打包时间成比例上升,而紧急发布时要求是越短越好,这是矛盾的。另一方面当一个代码库集成了所有功能时,日常协作绝对是非常困难的。而且最近十多年,前端技术的发展是非常快的,每隔两年就是一个时代,导致同志们必须升级项目甚至于换一个框架。但如果大家想在一个规模化应用中一个版本做好这件事,基本上是不可能的。最早的解决方案是采用iframe的方法,根据功能主要模块拆分规模化应用,子应用之间使用跳转。但这个方案最大问题是导致页面重新加载和白屏。那有什么好的解决方案呢?微前端这样具有跨应用的解决方案在此背景下应运而生了!微前端的概念微前端是什么:微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。有一个基座应用(主应用),来管理各个子应用的加载和卸载。
f135ab0912746bd6.png所以微前端不是指具体的库,不是指具体的框架,不是指具体的工具,而是一种理想与架构模式。微前端的核心三大原则就是:独立运行、独立部署、独立开发微前端的优势采用微前端架构的好处就是,将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性。实现微前端的几种方式· 从single-spa到qiankun· 基于WebComponent的micro-app· webpack5实现的Module Federation微前端框架的分类Single-spasingle-spa是一个很好的微前端基础框架,而qiankun框架就是基于single-spa来实现的,在single-spa的基础上做了一层封装,也解决了single-spa的一些缺陷。首先我们先来了解该如何使用single-spa来完成微前端的搭建。
single-spa.jpgSingle-spa实现原理首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler,。URL响应时,匹配子应用路由并加载渲染子应用。上图便是对single-spa完整的描述。有了理论基础,接下来,我们来看看代码层面时如何使用的。以下以Vue工程为例基座构建single-spa,在Vue工程入口文件main.js完成基座的配置。基座配置//main.jsimport Vue from 'vue'import App from './App.vue'import router from './router'import { registerApplication, start } from 'single-spa'Vue.config.productionTip = falseconst mountApp = (url) => { return new Promise((resolve, reject) => { const script = document.createElement('script') script.src = url script.onload = resolve script.onerror = reject // 通过插入script标签的方式挂载子应用 const firstScript = document.getElementsByTagName('script')[0] // 挂载子应用 firstScript.parentNode.insertBefore(script, firstScript) })}const loadApp = (appRouter, appName) => { // 远程加载子应用 return async () => { //手动挂载子应用 await mountApp(appRouter + '/js/chunk-vendors.js') await mountApp(appRouter + '/js/app.js') // 获取子应用生命周期函数 return window[appName] }}// 子应用列表const appList = [ { // 子应用名称 name: 'app1', // 挂载子应用 app: loadApp('http://localhost:8083', 'app1'), // 匹配该子路由的条件 activeWhen: location => location.pathname.startsWith('/app1'), // 传递给子应用的对象 customProps: {} }, { name: 'app2', app: loadApp('http://localhost:8082', 'app2'), activeWhen: location => location.pathname.startsWith('/app2'), customProps: {} }]// 注册子应用appList.map(item => { registerApplication(item)}) // 注册路由并启动基座new Vue({ router, mounted() { start() }, render: h => h(App)}).$mount('#app')复制代码构建基座的核心是:配置子应用信息,通过registerApplication注册子应用,在基座工程挂载阶段start启动基座。子应用配置import Vue from 'vue'import App from './App.vue'import router from './router'import singleSpaVue from 'single-spa-vue'Vue.config.productionTip = falseconst appOptions = { el: '#microApp', router, render: h => h(App)}// 支持应用独立运行、部署,不依赖于基座应用// 如果不是微应用环境,即启动自身挂载的方式if (!process.env.isMicro) { delete appOptions.el new Vue(appOptions).$mount('#app')}// 基于基座应用,导出生命周期函数const appLifecycle = singleSpaVue({ Vue, appOptions})// 抛出子应用生命周期// 启动生命周期函数export const bootstrap = (props) => { console.log('app2 bootstrap') return appLifecycle.bootstrap(() => { })}// 挂载生命周期函数export const mount = (props) => { console.log('app2 mount') return appLifecycle.mount(() => { })}// 卸载生命周期函数export const unmount = (props) => { console.log('app2 unmount') return appLifecycle.unmount(() => { })}复制代码配置子应用为umd打包方式//vue.config.jsconst package = require('./package.json')module.exports = { // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载 publicPath: '//localhost:8082', // 开发服务器 devServer: { port: 8082 }, configureWebpack: { // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个 // 全局对象获取一些信息,比如子应用导出的生命周期函数 output: { // library的值在所有子应用中需要唯一 library: package.name, libraryTarget: 'umd' } }复制代码配置子应用环境变量// .env.micro NODE_ENV=developmentVUE_APP_BASE_URL=/app2isMicro=true复制代码子应用配置的核心是用singleSpaVue生成子路由配置后,必须要抛出其生命周期函数。用以上方式便可轻松实现一个简单的微前端应用了。那么我们有single-spa这种微前端解决方案,为什么还需要qiankun呢?相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。QiankunQiankun的优势· 基于 single-spa[1] 封装,提供了更加开箱即用的 API。· · 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。· · HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。· · 样式隔离,确保微应用之间样式互相不干扰。· · JS 沙箱,确保微应用之间 全局变量/事件 不冲突。· · 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。· 基座配置import { registerMicroApps, start } from 'qiankun';registerMicroApps([ { name: 'reactApp', entry: '//localhost:3000', container: '#container', activeRule: '/app-react', }, { name: 'vueApp', entry: '//localhost:8080', container: '#container', activeRule: '/app-vue', }, { name: 'angularApp', entry: '//localhost:4200', container: '#container', activeRule: '/app-angular', },]);// 启动 qiankunstart();复制代码子应用配置以 create react app 生成的 react 16 项目为例,搭配 react-router-dom 5.x。1.在 src 目录新增 public-path.js,解决子应用挂载时,访问静态资源冲突 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }复制代码2.设置 history 模式路由的 base: ${name}-[name]
; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = webpackJsonp_${name}
; config.output.globalObject = 'window'; return config; }, devServer: (_) => { const config = _; config.headers = { 'Access-Control-Allow-Origin': '*', }; config.historyApiFallback = true; config.hot = false; config.watchContentBase = false; config.liveReload = false; return config; },};复制代码以上对Qiankun的使用可以看出,与single-spa使用过程很相似。不同的是,Qiankun的使用过程更简便了。一些内置的操作交由给Qiankun内部实现。这是一种IOC思想的实现,我们只管面向容器化开发,其他操作交给Qiankun框架管理。Micro-appmicro-app并没有沿袭single-spa的思路,而是借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom的隔离特性,micro-app不需要像single-spa和qiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。WebComponent的概念WebComponent[2]是HTML5提供的一套自定义元素的接口,WebComponent[3]是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。以上是MDN社区对WebComponent的解释。· Custom elements(自定义元素): 一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。· Shadow DOM(影子 DOM) :一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。· HTML templates(HTML 模板): 和 #counter-increment { width: 60px; height: 30px; margin: 20px; background: none; border: 1px solid black; }
; // 定义组件HTMl结构 shadowRoot.innerHTML =
; // 获取+号按钮及数值内容 this.incrementButton = this.shadowRoot.querySelector('#counter-increment'); this.counterValue = this.shadowRoot.querySelector('#counter-value'); // 实现点击组件内事件驱动 this.incrementButton.addEventListener("click", this.decrement.bind(this)); } increment() { this.counter++ this.updateValue(); } // 替换counter节点内容,达到更新数值的效果 updateValue() { this.counterValue.innerHTML = this.counter; }}// 在真实dom上,生成自定义组件元素customElements.define('counter-element', CounterElement);复制代码有了对WebComponent的理解,接下来,我们更明白了Micro-app的优势。micro-app的优势Counter
d879637b4bb34253.png· 使用简单· 我们将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。· 同时micro-app还提供了js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等一系列完善的功能。· · 零依赖· micro-app没有任何依赖,这赋予它小巧的体积和更高的扩展性。· · 兼容所有框架· 为了保证各个业务之间独立开发、独立部署的能力,micro-app做了诸多兼容,在任何技术框架中都可以正常运行。· 基座的简易配置基座存在预加载子应用、父子应用通信、公共文件共享等等// index.jsimport React from "react"import ReactDOM from "react-dom"import App from './App'import microApp from '@micro-zoe/micro-app'const appName = 'my-app'// 预加载microApp.preFetch([ { name: appName, url: 'xxx' }])// 基座向子应用数据通信microApp.setData(appName, { type: '新的数据' })// 获取指定子应用数据const childData = microApp.getData(appName)microApp.start({ // 公共文件共享 globalAssets: { js: ['js地址1', 'js地址2', ...], // js地址 css: ['css地址1', 'css地址2', ...], // css地址 }})复制代码分配一个路由给子应用// router.jsimport { BrowserRouter, Switch, Route } from 'react-router-dom'export default function AppRoute () { return ( micro-app-${window.__MICRO_APP_NAME__}
] = { mount, unmount }} else { // 非微前端环境直接渲染 mount()}复制代码设置子应用路由import { BrowserRouter, Switch, Route } from 'react-router-dom'export default function AppRoute () { return ( // 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute, // 如果没有设置baseroute属性,则此值默认为空字符串
微信图片_20220626184254.png
微信图片_20220626184305.png以上是webpack5与之前版本的模块管理对比图微应用配置通过webpack5的配置达成微应用的效果// 配置webpack.config.jsconst { ModuleFederationPlugin } = require("webpack").container;new ModuleFederationPlugin({ name: "appA", //出口文件 filename: "remoteEntry.js", //暴露可访问的组件 exposes: { "./input": "./src/input", }, //或者其他模块的组件 //如果把这一模块当作基座模块的话, //这里应该配置其他子应用模块的入口文件 remotes: { appB: "appB@http://localhost:3002/remoteEntry.js", }, //共享依赖,其他模块不需要再次下载,便可使用 shared: ['react', 'react-dom'],})复制代码以上便是我对微应用架构的理解,以及微应用架构技术的演变过程。不难看出,这些技术的演变都朝着易用性和可拓展性的方向演进。其中技术也有其时代的局限性,不过思想和技术总是在不断进步的。这几类技术选型都有其优缺点,各有千秋,我们可以根据不同的需要选择不同的技术来构建应用。下列是本文写作时的参考资料:single-spa: zh-hans.single-spa.js.org/docs/gettin…[4]qiankun: qiankun.umijs.org/zh/guide[5]WebComponent: developer.mozilla.org/zh-CN/docs/…[6]micro-app: cangdu.org/micro-app/d…[7]参考资料[1]https://github.com/CanopyTax/single-spa[2]https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%...[3]https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%...[4]https://zh-hans.single-spa.js.org/docs/getting-started-overview[5]https://qiankun.umijs.org/zh/guide[6]https://developer.mozilla.org/zh-CN/docs/Web/Web_Components[7]http://cangdu.org/micro-app/docs.html#/