了解微前端的起因是因为我公司的大多数页面都是手机h5,分散且基本毫无关联,每次新页面都开一个二级域名,很难管理,所以研究了微前端,虽然很久以前就听过,拖延让我直到有需要才去自己学习
本文初探qiankun
,并且搭建一个可以跑的基础demo
,仓库地址
前言
微前端是什么呢?按照qiankun
文档中的一段摘录
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
我的理解是,微前端可以将多个关联性不强,不同项目的子应用合体在一个项目里,并且与技术栈无关,在同一个页面可以同时显示React
、Vue
、jQuery
的项目
那么qiankun
是什么呢?
qiankun
是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统
由于是国内开源的项目,文档也是中文,自然学习qiankun
也是最友好的
qiankun使用的两种方式
- 随便什么项目安装
qiankun
后使用 - 基于
umijs
的plugin-qiankun
第一种方式,对主应用和子应用都没有要求,只要安装了qiankun
并且照着文档配置好,就能跑通,但是需要配置的东西要多一点
第二种方式,主应用需要为umijs
的项目,子应用如果也为umijs
的项目,则配置非常简单,并且有额外的功能,比如跨应用的React hook
来共享数据
所以,这两种方式,都探索一番
在普通项目中使用qiankun
普通项目并不需要是框架项目,仅仅一个js
,一个html
都可以的
主应用
主应用安装qiankun
yarn add qiankun
在主应用的html
里增加一个id为root
的div
<div id="root"></div>
主应用的js
文件中写上qiankun
的配置
import { registerMicroApps } from "qiankun";// 仓库demo中有2个子项目,这里仅举例create-react-app的项目registerMicroApps([ { // 子应用唯一名称 name: "app2", // 子应用入口 entry: "//localhost:8002", // 子应用挂载的元素 container: "#root", // 子应用匹配路径 activeRule: "/app2", },]);start(); // 微前端 —— 启动
子应用
这里的子应用使用create-react-app
的项目
修改webpack
配置
由于要修改webpack
的配置,在不eject
的情况下需要安装react-app-rewired
来修改配置
yarn add react-app-rewired --dev
修改pageage.json
中的scripts
"scripts": {- "start": "react-scripts start",+ "start": "react-app-rewired start",
增加react-app-rewired配置文件
根目录增加react-app-rewired
的配置文件config-overrides.js
const { name } = require("./package");module.exports = { webpack: function override(config, env) { // 根据qiankun文档配置 config.output.library = `${name}-[name]`; config.output.libraryTarget = "umd"; config.output.jsonpFunction = `webpackJsonp_${name}`; return config; }, devServer: function (configFunction) { return function (proxy, allowedHost) { const config = configFunction(proxy, allowedHost); // 微前端项目中子项目必须支持跨域 config.headers = { "Access-Control-Allow-Origin": "*", }; return config; }; },};
修改挂载元素id
修改页面挂载元素id
,因为主应用占用了root
这个id
public/index.html
- <div id="root"></div>+ <div id="root-cra"></div>
修改子应用入口文件
src/index.jsx
增加render
函数
-ReactDOM.render(- <App />,- document.getElementById('root')-);+ const render = () => {+ ReactDOM.render(<App />, document.getElementById("root-cra")); // 修改id+ };
添加qiankun
生命周期钩子
// 在不是qiankun的情况下独立运行// qiankun会注入__POWERED_BY_QIANKUN__变量// 如果没有这个变量,表示并不是子应用,直接渲染页面节点if (!window.__POWERED_BY_QIANKUN__) { render();}/** * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 */export async function bootstrap() { console.log("app2 create-react-app bootstraped");}/** * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 */export async function mount(props) { console.log("app2 create-react-app mount", props); // 调用render,渲染子应用 render();}/** * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 */export async function unmount() { console.log("app2 create-react-app unmount"); ReactDOM.unmountComponentAtNode(document.getElementById("root-cra")!);}
访问主应用地址 localhost:8080
,是主应用,访问localhost:8080/app2
是create-react-app
的子应用,如果子应用有路由也可以直接访问,如localhost:8000/app2/page1
在umijs中使用qiankun
umijs
中,只需要主应用添加对应的插件plugin-qiankun
主应用
安装 plugin-qiankun
yarn add @umijs/plugin-qiankun@next --dev
新增 document.ejs
新建 src/pages/document.ejs
,umi 约定如果这个文件存在,会作为默认模板
<!doctype html><html><head> <meta charSet="utf-8"/> <title>micro frontend</title></head><body><div id="root-subapp"></div></body></html>
这一步主要是需要增加一个额外的div
元素来放置子应用,在plugin-qiankun
中默认子应用挂载在root-subapp
,如果没有这个元素会报错
修改配置 .umirc.ts
import { defineConfig } from 'umi';export default defineConfig({ qiankun: { master: { // 注册子应用信息 apps: [ { name: 'app1', // 唯一 id entry: '//localhost:8001', // html entry base: '/app1', // app1 的路由前缀,通过这个前缀判断是否要启动该应用,通常跟子应用的 base 保持一致 history: 'browser', // 子应用的 history 配置,默认为当前主应用 history 配置 // 子应用通过钩子函数的参数props可以拿到这里传入的值 props: {}, }, ], jsSandbox: true, // 是否启用 js 沙箱,默认为 false prefetch: true, // 是否启用 prefetch 特性,默认为 true }, },});
子应用
umijs
子应用就非常简单了,只需要修改.umirc.ts
就行了
import { defineConfig } from 'umi';export default defineConfig({ base: `/app1`, // 子应用的 base,默认为 package.json 中的 name 字段 qiankun: { slave: {} },});
全局共享数据
在普通qiankun
和umijs
中又不相同了
普通qiankun项目
普通qiankun
可以通过initGlobalState
方法来定义
主应用
import { initGlobalState } from 'qiankun';// 初始化 stateconst actions = initGlobalState(state);actions.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev);});actions.setGlobalState(state); // 修改stateactions.offGlobalStateChange(); // 移除当前应用的状态监听,微应用 umount 时会默认调用
子应用
umijs
的子应用钩子函数需要定义在src/app.js
export const qiankun = { // 从生命周期钩子函数 mount 中获取通信方法,使用方式和 master 一致 async mount(props) { props.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); props.setGlobalState(state); // 设置 }}
由于方法和事件只在钩子函数里有,我觉得可以在mount
的时候注册一个像event bus
这样的方法来供全局调用修改函数
umijs的qiankun项目
plugin-umi
提供了一个比较方便的React hook
来全局调用
主应用
约定主应用中在 src/rootExports.js
里 export
内容
let data = '';let eventList = [];export function getData() { return data;}export function bindOnChange(fn) { if (typeof fn === 'function') { eventList.push(fn); } return function unBind() { eventList = eventList.filter(v => v !== fn); };}export function setData(newData) { data = newData; eventList.forEach(cb => cb(data));}
子应用
// ...const { bindOnChange, setData } = useRootExports();useEffect(() => { const unBind = bindOnChange((data) => { console.log('root exports data change:', data); }); return () => unBind();}, []);
需要注意的是,如果这里子应用单独运行或者是主应用的qiankun
不基于umijs
,这个钩子会报错的,需要自己做好判断
直接通过配置传递
apps: [ { name: 'app1', // 唯一 id // ... // 传递给子应用 props: { username: 'zhangyu' }, },],
子应用在生命周期钩子函数中的参数可以拿到props
的内容
在qiankun
中这个配置是可以动态加载的,本文只探索了固定配置
本文完整demo仓库地址