qiankun的使用方法及为何使用乾坤请看 react hooks/vue2/vue3 + qiankun微服务踩坑记
这篇文章所表述内容主要是qiankun的实现方式,说得高级一点就是
原理
niuer使用
- 主应用
- 安装
pnpm add niuer
- 注册
import { registerMicroApps, GlobalAction } from 'niuer'; const initState = { loading: true, text: '初始化...' }; const globalAction = new GlobalAction(initState); const appList = [ { key: '1', name: 'react-app', entry: '//172.16.2.240:9001', activeRule: '/microservice/react-app', container: '#microAppContainer', link: '/react-app', props: { basename: '/microservice/react-app', globalAction } } ]; registerMicroApps(appList);
- 启动
import { start } from 'niuer'; start();
- 安装
- 微应用
- 暴露生命周期
export async function beforeMount() { console.log('%c react app beforeMounted', 'color: red;'); } export async function mount(props: Props) { console.log('React 子应用', props); render(props); } export async function unmount({ container }) { ReactDOM.unmountComponentAtNode((container?.querySelector('#reactApp') ?? document.querySelector('#reactApp'))); }
- 设置静态资源路径
if (window.__POWERED_IS_NIUER__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_IS_NIUER__; }
- 设置output打包libraryTarget为umd
const {name} = require("../package.json"); ... output: { library: `${name}`, libraryTarget: 'umd', chunkLoadingGlobal: `webpackJsonp_${name}-[name]`, ... }
- 配置webpack dev环境允许跨域
devServer: { ... historyApiFallback: { rewrites: [{ from: /./, to: '/' }] } ... }
页面架构
实现思路
- 注册子应用,注册信息必须包含子应用入口地址、激活路径、挂载点
- 路由劫持,监听路由变化,匹配路由名字加载、渲染对应子应用
- 规定CommonJs规范获取子应用生命周期钩子,执行mount、unmount
- 设置沙箱,隔离运行环境
- js:Proxy
- css:ShadowDom/css属性选择器/Scoped
- 通过数据劫持实现数据共享(基于Proxy)
注册
- 先看一个子应用item
- key 唯一标识符
- name 子应用名字,这里会用在子应用挂载的div上
required
- entry 子应用入口地址
required
- activeRule 子应用激活路由
required
- container 子应用挂载的dom id
required
- link 主应用点击菜单触发跳转的地址
- props 初始化传给子应用的参数
- globalAction 全局数据监听函数
其中标有
required
的是必须参数,key、link是配置给主应用使用的
监听路由
- 触发浏览器路由变化有添加、替换、前进/后退,其中
添加
和替换
同归属于history
事件下,前进/后退
归属于window
事件下 - 浏览器监听路由变化分为
hash
和history
两种模式,本次只讨论history
模式。 - 在生命周期钩子里,我们暴露了一个
unmout
来卸载子应用,到我们从一个子应用切换到另一个子应用是需要调用此方法,所以我们需要记录当前路由prevRoute
(prevRoute是跳转之前的pathname)和下一个路由nextRoute
(nextRoute是路由跳转完成后的pathname),目的是为了在切换应用时根据prevRoute去找到上一个子应用的item - 每当添加和替换路由时,需要先备份history的
pushState
和replaceState
状态,如果不备份在执行pushState
和replaceState
完成后就没法设置prevRoute
和nextRoute
- 初始化设置prevRoute、nextRoute
let prevRoute = '',
nextRoute = window.location.pathname;
- 实现代码
- 添加路由(pushState)
const rawPushState = window.history.pushState; window.history.pushState = (...args) => { prevRoute = window.location.pathname; rawPushState.apply(window.history, args); nextRoute = window.location.pathname; PathnameChange(); };
- 替换路由(replaceState)
const rawReplaceState = window.history.replaceState; window.history.replaceState = (...args) => { prevRoute = window.location.pathname; rawReplaceState.apply(window.history, args); nextRoute = window.location.pathname; PathnameChange(); };
- 前进后退 (popstate)
window.addEventListener('popstate', () => { prevRoute = nextRoute; nextRoute = window.location.pathname; PathnameChange(); });
注意:这里为什么设置
prevRoute = nextRoute
,因为浏览器前进后退按钮执行完成后永远拿到的是nextRoute
,无法在跳转之前进行操作 - 添加路由(pushState)
- 路由变化处理
- 渲染子应用的核心操作在
PathnameChange
里
- 渲染子应用的核心操作在
匹配子应用
-
当页面路由发生改变之后会触发以上三个事件的其中一个,此时获取当前路由名字
window.location.pathname
,然后去匹配item
中的activeRule
,从而拿到当前路由对应的app item -
通过
entry
获取当前页面的html
,这里使用fetch
,这里通过fetch获取到的文本就是一个普通的html文档 -
获取到的html文本长这样
加载子应用
以下是
PathnameChange
所做的事,另外在渲染当前子应用之前首先的拿到上一个子应用,执行unmout
-
创建一个div,设置div的
innerHTML
为上述html文本, 并将div添加到item中的container
,这里的html文本会自定自行css -
获取div中的所有
script
,得到js文本- 如果script有src属性,通过fetch发送http请求
- 如果script没有src,获取当前script的
textContent
-
执行js文本的方法有两种,
eval
和new Function
- 至此页面虽然执行了
js
但页面并不会看到任何内容,因为还没有执行mount方法(render)
-
构造CommonJs模块环境
-
首先看看webpack打包后输出的代码
这里我们采用CommonJs规范,所以在执行环境中手动构造了一个module和exports
-
获取
instance
中的三个钩子
-
-
执行子应用render,执行mount需要把item的props传给子应用,应为子应用需要
basename
和container
css隔离
- 本文使用ShadowDom,设置div的attachShadow({ mode: open }),html页面会开启css隔离,但低版本浏览器不支持
- shadowDom长这样👇
js沙箱、全局通信 本次暂不讨论
本篇幅并没有写得很详细,只罗列了一个大概,有关项目源码可查看niuer源码