最近由于项目需要,有幸接触到微前端MicroApp,下面是我对微前端的一些理解给大家做一个分享
微前端是什么
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。
为什么要用微前端
1、拆分和细化
当下前端领域,单页面应用(SPA)是非常流行的项目形态之一,而随着时间的推移以及应用功能的丰富,单页应用变得不再单一而是越来越庞大也越来越难以维护,往往是改一处而动全身,由此带来的发版成本也越来越高。微前端的意义就是将这些庞大应用进行拆分,并随之解耦,每个部分可以单独进行维护和部署,提升效率。
2、整合历史系统进行迭代
在不少的业务中,存在很多历史项目,这些项目大多以采用老框架类似的B端管理系统为主,介于日常运营,这些系统需要结合到新框架中来使用还不能抛弃,对此我们也没有理由浪费时间和精力重写旧的逻辑。而微前端可以将这些系统进行整合,在基本不修改来逻辑的同时来同时兼容新老两套系统并行运行。
3、对于项目迭代式重构
对于比较多的项目,或许因为以前的技术规划或者技术选型,导致现今比较落后,对一个项目健康发展起到阻碍作用,项目维护成本过高但是一次性重构成本过大,我们选择迭代式重构,在业务迭代中对项目进行重构,对业务也会理解更加透彻。这样微前端就能很好的嵌入进来;
4、多个项目组业务耦合
面对多个产品线,经常出现业务交叉,不同的同时会在例外一个项目组的项目中开发,使得手上的项目使得项目管理混乱,尤其面对一些库的依赖升级,如果其他组成员未知晓,可能导致一些未知问题;
微前端实现方案
iframe嵌套:
众所周知,iframe
是html
提供的标签,能加载其他web应用的内容,并且它能兼容所有的浏览器,因此,你可以用它来加载任何你想要加载的web应用。iframe最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。 但是,最大的问题就是在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来开发体验、产品体验的问题。
组合式应用路由分发
每个子应用独立构建和部署,运行时由父应用进行路由管理,应用加载,启动,卸载,以及通信机制。
该方案的核心是“主从”思想,即包括一个基座(MainApp)应用和若干个微(MicroApp)应用,基座应用大多数是一个前端SPA项目,主要负责应用注册,路由映射,消息下发等,而微应用是独立前端项目,这些项目不限于采用React,Vue,Angular或者JQuery开发,每个微应用注册到基座应用中,由基座进行管理,但是如果脱离基座也是可以单独访问。
Web Components
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。主要包含三大属性:
Custom elements(自定义元素):一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。
Shadow DOM(影子 DOM)、:一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
HTML templates(HTML 模板):
[<template>](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/template)
和[<slot>](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/slot)
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用
基于webpack5 module Federation
多个独立的构建可以形成一个应用程序。这些独立的构建不会相互依赖,因此可以单独开发和部署它们。 这通常被称为微前端,但并不仅限于此。 它的主要功能是我们可以将项目中的部分组件或全部组件暴露给外侧使用。我们也可以引用其他项目中暴露的组件,从而实现模块的复用。
其他:Nginx路由转发
single-spa
提供生命周期概念,并负责调度子应用的生命周期 挟持 url 变化事件和函数,url 变化时匹配对应子应用,并执行生命周期流程
首先我们需要用importmap
在根项目中引入所有的模块文件和子项目,从而在其余项目中可以进行模块的引用。因为浏览器的不支持import,需要引入system.js
注册子应用
single-spa 里最重要的 API:registerApplication
singleSpa.registerApplication({ name:'navbar', // 子应用名app: () => System.import('navbar'), // 如何加载你的子应用activeWhen: location => true, // url 匹配规则,表示啥时候开始走这个子应用的生命周期//activeWhen: location => location.pathname.startsWith('/app1') customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到 authToken: 'something'}}) singleSpa.start() // 启动主应用
执行生命周期
const vueLifecycles = singleSpaVue({Vue, appOptions: {render: (h) => h(App), router,},});export const bootstrap = vueLifecycles.bootstrap;export const mount = vueLifecycles.mount;export const unmount = vueLifecycles.unmount;
qiankun
相比single-spa
,qiankun
他解决了JS沙盒环境,不需要手动的去写调用子应用JS,加载采用的HTML Entry方式
JS Entry 的方式通常是子应用将资源打成一个 entry script,但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。
HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。
基座
注册
import { registerMicroApps, start } from "qiankun";const apps = [{ name: "vueApp", // 应用的名字 entry: "//localhost:10000",//加载的html路径 container: "#vue", // 容器名 activeRule: "/vue", // 激活的路径},{ name: "reactApp", entry: "//localhost:20000", container: "#react", activeRule: "/react",},];registerMicroApps(apps); // 注册应用start(); // 开启
子项目
import Vue from 'vue'import App from './App.vue'import router from './router'// Vue.config.productionTip = falselet instance = nullfunction render(props) { instance = new Vue({ router,render: h => h(App)}).destroy();}
子项目打包配置
module.exports = { devServer:{ port:10000,// 需要允许跨域 headers:{'Access-Control-Allow-Origin':'*'}}, configureWebpack:{ output:{ library:'vueApp', libraryTarget:'umd'}}}
css隔离方案
子应用之间样式隔离:
- Dynamic Stylesheet 动态样式表,当应用切换时移除老应用样式,添加新应用样式
主应用和子应用之间的样式隔离:
BEM (Block Element Modifier) 约定项目前缀
CSS-Modules 打包时生成不冲突的选择器名
Shadow Dom 真正意义上的隔离
js沙箱机制
- 快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实例)
class SnapshotSandbox{constructor(){this.proxy = window; this.modifyPropsMap = {}; this.active();}active(){ // 激活this.windowSnapshot = {}; // 拍照 for(const prop in window){if(window.hasOwnProperty(prop)){this.windowSnapshot[prop] = window[prop];}}Object.keys(this.modifyPropsMap).forEach(p=>{window[p] = this.modifyPropsMap[p];})}inactive(){ // 失活for(const prop in window){if(window.hasOwnProperty(prop)){if(window[prop] !== this.windowSnapshot[prop]){this.modifyPropsMap[prop] = window[prop];window[prop] = this.windowSnapshot[prop]}}}}}
- Proxy 代理沙箱,不影响全局环境
class ProxySandbox {constructor() {const rawWindow = window;const fakeWindow = {}const proxy = new Proxy(fakeWindow, {set(target, p, value) { target[p] = value; return true},get(target, p) {return target[p] || rawWindow[p];}});this.proxy = proxy }}