什么是微前端
微前端是指存在于浏览器中的微服务,其借鉴了微服务的架构理念,将微服务的概念扩展到了前端。
如果对微服务的概念比较陌生的话,可以简单的理解为微前端就是将一个大型的前端应用拆分成多个模块,每个微前端模块可以由不同的团队进行管理,并可以自主选择框架,并且有自己的仓库,可以独立部署上线。
微前端的好处
1.团队自治
2.兼容老项目
3.跨技术栈
现有的微前端方案
1.iframe
通过iframe标签来嵌入到父应用中,iframe具有天然的隔离属性,各个子应用之间以及子应用和父应用之间都可以做到互不影响。
iframe的缺点:
- url不同步,如果刷新页面,iframe中的页面的路由会丢失。
- 全局上下文完全隔离,内存变量不共享。
- UI不同步,比如iframe中的页面如果有带遮罩层的弹窗组件,则遮罩就不能覆盖整个浏览器,只能在iframe中生效。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
2.single-spa
官网:https://zh-hans.single-spa.js.org/docs/getting-started-overviewsingle-spa是最早的微前端框架,可以兼容很多技术栈。
single-spa的缺点:
- 没有实现js隔离和css隔离
- 需要修改大量的配置,包括基座和子应用的,不能开箱即用
3.qiankun
qiankun是阿里开源的一个微前端的框架qiankun的优点:
- 基于single-spa封装的,提供了更加开箱即用的API
- 技术栈无关,任意技术栈的应用均可使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- HTML Entry的方式接入,像使用iframe一样简单
- 实现了single-spa不具备的样式隔离和js隔离
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 基座(主应用):主要负责集成所有的子应用,提供一个入口能够访问你所需要的子应用的展示,尽量不写复杂的业务逻辑
- 子应用:根据不同业务划分的模块,每个子应用都打包成
umd
模块的形式供基座(主应用)来加载
基座改造
基座用的是create-react-app
脚手架加上antd
组件库搭建的项目,也可以选择vue或者其他框架,一般来说,基座只提供加载子应用的容器,尽量不写复杂的业务逻辑。
- 安装qiankun
// 安装qiankun
npm i qiankun // 或者 yarn add qiankun
- 修改入口文件
// 在src/index.tsx中增加如下代码
import { start, registerMicroApps} from 'qiankun'
// 1.要加载的子应用列表
const apps = [{name: 'sub-react', // 子应用的名称entry: '//localhost:3001', // 默认会加载这个路径下的html,解析里面的jsactiveRule: '/sub-react',// 匹配的路由container: '#sub-app' // 子应用加载的容器},
] // 2. 注册子应用
registerMicroApps(apps, {beforeLoad: [async app => console.log('before load', app.name)],beforeMount: [async app => console.log('before mount', app.name)],afterMount: [async app => console.log('after mount', app.name)],afterUnmount: [async app => console.log('after unmount', app.name)]
})// 3. 启动微服务
start()
react子应用
使用create-react-app
脚手架创建,webpack
进行配置,为了不eject所有的webpack配置,我们选择用react-app-rewired
工具来改造webpack配置。
2. 修改入口文件
// 在src/index.tsx中增加如下代码
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom'
import './public-path'let root: any;
// 将render方法用函数包裹,供后续主应用与独立运行调用
function render(props: any) {const { container } = props// 1.拿到root的divconst dom = container ? container.querySelector('#root') : document.getElementById('root')root = createRoot(dom)// 2.把app渲染到root上面// basename对应的是基座里面子应用列表的路由// 因为基座加载子应用的时候是匹配路由的root.render(<BrowserRouter basename='/sub-react'><App/></BrowserRouter>)
}// 判断是否在qiankun环境下,非qiankun环境下独立运行
if(!(window as any).__POWERED_BY_QIANKUN__) {render({})
}// 各个生命周期
// bootstrap 置灰在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用mount钩子,不会再重复触发 bootstrap
export async function bootstrap() {console.log('react app bootstrap');
}// 应用每次进入都会调用mount方法,通常我们在这里触发应用的渲染方法
export async function mount(props: any) {console.log('props==', props);render(props)
} // 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props: any) {root.unmount()
}
- 新增public-path.js
if (window.__POWERED_BY_QIANKUN__) {// 动态设置 webpack publicPath,防止资源加载出错// eslint-disable-next-line no-undef__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
- 修改webpack配置文件
// 在根目录下新增config-overrides.js文件并新增如下配置
const { name } = require("./package");module.exports = {webpack: (config) => {config.output.library = `${name}-[name]`;// 把项目打包成umd模块,方便qiankun去读取我们暴露出来的生命周期(bootstrap、mount、unmount)config.output.libraryTarget = "umd";config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;return config;}
};
vue子应用
# 创建子应用,选择vue3+vite
npm create vite@latest
改造子应用
- 安装
vite-plugin-qiankun
依赖包
npm i vite-plugin-qiankun # yarn add vite-plugin-qiankun
- 修改vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun';export default defineConfig({base: '/sub-vue', // 和基座中配置的activeRule一致server: {port: 3002, // 端口cors: true, // 允许跨域origin: 'http://localhost:3002' // 指定允许跨域请求的来源地址},plugins: [vue(),// 加一个qiankun,写子应用的名称,需要开发模式的需要配置useDevModeqiankun('sub-vue', { // 配置qiankun插件// 是否运行在开发模式下useDevMode: true})]
})
- 修改main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'let app: any;
// 判断是不是在qiankun环境下
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {createApp(App).use(router).mount('#app');
} else {renderWithQiankun({// 子应用挂载(来回切换的时候)mount (props){ // 挂载的时候app = createApp(App);app.use(router).mount(props.container.querySelector('#app'))},// 只有子应用第一次加载会触发bootstrap () { // 应用刚加载的时候console.log('vue app bootstrap');},// 更新update () { // 更新console.log('vue app update');},// 卸载unmount () { // 卸载console.log('vue app unmount');app?.unmount();}})
}
umi子应用
使用umi4去创建子应用,创建好后只需要简单的配置就可以跑起来
- 安装插件
npm i @umijs/plugins
- 配置.umirc.ts
export default {base: '/sub-umi',npmClient: 'npm',plugins: ['@umijs/plugins/dist/qiankun'],qiankun: {slave: {},}
};
// 在入口文件导出qiankun的生命周期
export const qiankun = {async mount(props:any) {console.log(props);},async bootstrap () {console.log('umi app bootstraped');},async afterMount(props: any) {console.log('umi app afterMount', props);},
}
补充
1.样式隔离
qiankun内部实现的是子应用之间的样式隔离,基座和子应用之间的样式么没有进行隔离
应用间的样式隔离原因:子应用之间的样式隔离很简单,加载子应用的样式,卸载的时候把子应用的样式进行卸载,加载下一个子应用的时候加载下一个子应用的样式
1.样式隔离1.1 每个应用的样式使用固定的格式1.2 通过css-module的方式给每个应用自动添加上前缀修改基座应用公共样式的时候还是会影响子应用的样式,这时候可以把子应用的样式优先级提高一点 (样式隔离)
2.子应用间的跳转
2.1 主应用和微应用都是hash模式,主应用根据hash来判断微应用,则不用考虑这个问题
通过location.hash直接修改hash值
http://localhost:3001/#/react-app/list
修改为
http://localhost:3001/#/vue-app/list
2.2 history模式下应用之间的跳转或者微应用跳主应用页面,直接使用微应用的路由实例是不行的,原因是微应用的路由实例跳转都基于路由的base。
2.2.1 history.pushState()
2.2.2 将主应用的路由实例通过props传给微应用,微应用这个路由实例跳转
// 基座和子应用用的都是一个windows对象,可以在基座中复写并监听history.pushState()方法并做相应的跳转逻辑// 在app.tsx重写pushState
// 重写函数// 重写函数const _wr = function (type: string) {// 拿到windos 的history对象传过来的参数const org = (window as any).history[type]return function() {// 拿到org后对他重新调用下const rv = org.apply(this, arguments);const e: any = new Event(type)// 返回发布一个自定义事件e.arguments = argumentswindow.dispatchEvent(e)return rv}}// 把重写的函数赋值给window上pushStatewindow.history.pushState = _wr('pushState')// 在这个函数中做跳转后的逻辑const bindHistory = () => {const currentPath = window.location.pathname;setSelectedPath(routes.find(item => currentPath.includes(item.key))?.key || '')}// 绑定事件window.addEventListener('pushState', bindHistory)
公共依赖加载
3.1 场景:如果主应用和子应用都使用了相同的库或者包(antd, axios等),就可以用externals(外部扩展)的方式来引入,减少加载重复报导致资源浪费,就是一个 项目使用后另一个项目不必再重复加载。
3.2.1 主应用:将所有公共依赖配置webpack的externals,并且在index.html使用外链引入这些公共依赖
3.2.2 子应用:和主应用一样配置webpack的externals,并且在index.html使用外链引入这些公共依赖,注意,还需要给子应用的公共依赖加上ignore属性(这是自定义的属性, 非标准属性),
qiankun在解析时如果发现ignore属性就会自动忽略
以axios为例:
基座的配置
// 修改config-overrides.js
const { override, addWebpackExternals } = require('customize-cra')// 这个配置就是通过外链的方式去引入这个包
module.exports = override(addWebpackExternals({axios: "axios"})
)// 在publi目录下的index.html添加外链
<!-- 注意:这里的公共依赖的版本必须和子应用一致 -->
<script src="https://unpkg.com/axios@1.1.2/dist/axios.min.js"></script>
子应用的配置
// 在umi-app子应用中
// 修改.umirc.ts配置文件
export default {base: '/sub-umi',npmClient: 'npm',plugins: ['@umijs/plugins/dist/qiankun'],qiankun: {slave: {},},headScripts: [{ // 配置外链地址,和设置忽略,qiankun在解析时如果发现ignore属性就会自动忽略src: 'https://unpkg.com/axios@1.1.2/dist/axios.min.js', ignore: true}]
};
全局状态管理
一般来说,各个子应用是通过业务来划分的,不同业务线应该降低耦合度,尽量去避免通信,但是如果涉及到一些公共的状态或者操作,qiankun也是支持的。
qiankun提供了一个全局的GLobalState来共享数据,基座初始化之后,子应用可以监听到这个数据的变化,也能提交这个数据
// 基座的配置
// 在src/index.tsx中增加如下代码
import { initGlobalState } from 'qiankun'
// 基座初始化
const state = { count: 1 }
const actions = initGlobalState(state);
// 基座项目监听和修改
actions.onGlobalStateChange((state, prev) => {// state: 变更后的状态; prev 变更前的状态console.log(state, prev);
})
actions.setGlobalState(state)
// 子应用的配置
// 在src/index.tsx中增加如下代码
// 在子应用的mount生命周期监听
export async function mount(props: any) {console.log('props==', props);// 子项目监听和修改// 然后在子应用中拿到这两个函数,然后给他设置一个count:2props.onGlobalStateChange((state,prev) => {// state: 变更后的状态, prev 变更前的状态console.log('子应用state===',state,prev)// 一般是将这个state存储到我们子应用的store,然后在其他组件中去用// 这样就是实现了一个简单基座和子应用之间的通信// 同样在其他子应用中想要用到基座传过来的状态也是这样用的,// 修改的话也是调用这个setGlobalState// 监听变化也是调用onGlobalStateChange})props.setGlobalState({ count: 2 })render(props)
}