共计 38635 个字符,预计需要花费 97 分钟才能阅读完成。
简介
从 single-spa 的缺点讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qiankun 框架。
介绍
qiankun 是基于 single-spa 做了二次封装的微前端框架,通过解决了 single-spa 的一些弊病和有余,来帮忙大家实现更简略、无痛的构建一个生产可用的微前端架构零碎。
微前端框架 之 single-spa 从入门到精通 通过从 根本应用 -> 部署 -> 框架源码剖析 -> 手写框架,带你全方位刨析 single-spa 框架。
因为 qiankun 是基于 single-spa 做的二次封装,次要解决了 single-spa 的一些痛点和有余,所以最好对 single-spa 有一个全面的理解和意识,明确其原理、理解它的有余和缺点,而后带着问题和目标去浏览 qiankun 源码,能够达到事倍功半的成果,整个浏览过程的思路也会更加清晰明了。
为什么不是 single-spa
如果你很理解 single-spa 或者浏览过 微前端框架 之 single-spa 从入门到精通,你会发现 single-spa 就做了两件事,加载微利用(加载办法还是用户本人提供的)、保护微利用状态(初始化、挂载、卸载)。理解多了会发现 single-spa 虽好,然而却存在一些比较严重的问题
-
对微利用的侵入性太强
single-spa 采纳 JS Entry 的形式接入微利用。微利用革新个别分为三步:
- 微利用路由革新,增加一个特定的前缀
- 微利用入口革新,挂载点变更和生命周期函数导出
- 打包工具配置更改
侵入型强其实说的就是第三点,更改打包工具的配置,应用 single-spa 接入微利用须要将微利用整个打包成一个 JS 文件,公布到动态资源服务器,而后在主利用中配置该 JS 文件的地址通知 single-spa 去这个地址加载微利用。
不说其它的,就当初这个改变就存在很大的问题,将整个微利用打包成一个 JS 文件,常见的打包优化基本上都没了,比方:按需加载、首屏资源加载优化、css 独立打包等优化措施。
我的项目公布当前呈现了 bug,修复之后须要更新上线,为了革除浏览器缓存带来的影响,个别文件名会带上 chunkcontent,微利用公布之后文件名都会发生变化,这时候还须要更新主利用中微利用配置,而后从新编译主利用而后公布,这套操作几乎是不能忍耐的,这也是 微前端框架 之 single-spa 从入门到精通 这篇文章中示例我的项目中微利用公布时的环境配置抉择 development 的起因。
-
款式隔离问题
single-spa 没有做这部分的工作。一个大型的零碎会有很的微利用组成,怎么保障这些微利用之间的款式互不影响?微利用和主利用之间的款式互不影响?这时只能通过约定命名标准来实现,比方利用款式以本人的利用名称结尾,以利用名结构一个独立的命名空间,这个形式新零碎还好说,如果是一个已有的零碎,这个革新工作量可不小。
-
JS 隔离
这部分工作 single-spa 也没有做。JS 全局对象净化是一个很常见的景象,比方:微利用 A 在全局对象上增加了一个本人特有的属性,
window.A
,这时候切换到微利用 B,这时候如何保障window
对象是洁净的呢? -
资源预加载
这部分的工作 single-spa 更没做了,毕竟将微利用整个打包成一个 js 文件。当初有个需要,比方为了进步零碎的用户体验,在第一个微利用挂载实现后,须要让浏览器在后盾悄悄的加载其它微利用的动态资源,这个怎么实现呢?
-
利用间通信
这部分工作 single-spa 没做,它只在注册微利用时给微利用注入一些状态信息,后续就不论了,没有任何通信的伎俩,只能用户本人去实现
以上 5 个问题中第 2、3、5 还好说,能够通过一些形式来解决,比方采纳命名空间的形式解决款式隔离问题,通过备份全局对象,每次微利用切换时初始化全局对象的形式来解决 JS 隔离的问题,通信问题能够通过传递一些通信办法,这点依赖了 JS 对象自身的个性(传递的是援用)来实现;然而第一个和第四个就不好解决了,这是 JS Entry 形式带来的问题,要解决这个问题,难度绝对就会大很多,工作量也会更大。况且这些通用的脏活累活就不应该由用户(框架使用者)来解决,而是由框架来解决。
为什么是 qiankun
下面说到,通用的脏活累活应该在框架层面去做,qiankun 基于 single-spa 做了二次封装,很好的解决了下面提到的几个问题。
-
HTML Entry
qiankun 通过 HTML Entry 的形式来解决 JS Entry 带来的问题,让你接入微利用像应用 iframe 一样简略。
-
款式隔离
qiankun 实现了两种款式隔离
- 严格的款式隔离模式,为每个微利用的容器包裹上一个
shadow dom
节点,从而确保微利用的款式不会对全局造成影响 - 实验性的形式,通过动静改写
css
选择器来实现,能够了解为css scoped
的形式
- 严格的款式隔离模式,为每个微利用的容器包裹上一个
-
运行时沙箱
qiankun 的运行时沙箱分为
JS
沙箱和款式沙箱
JS 沙箱
为每个微利用生成独自的window proxy
对象,配合HTML Entry
提供的 JS 脚本执行器 (execScripts) 来实现 JS 隔离;款式沙箱
通过重写DOM
操作方法,来劫持动静款式和 JS 脚本的增加,让款式和脚本增加到正确的中央,即主利用的插入到主利用模版内,微利用的插入到微利用模版,并且为劫持的动静款式做了scoped css
的解决,为劫持的脚本做了 JS 隔离的解决,更加具体的内容可持续往下浏览或者间接浏览 微前端专栏 中的qiankun 2.x 运行时沙箱 源码剖析
-
资源预加载
qiankun 实现预加载的思路有两种,一种是当主利用执行
start
办法启动 qiankun 当前立刻去预加载微利用的动态资源,另一种是在第一个微利用挂载当前预加载其它微利用的动态资源,这个是利用 single-spa 提供的single-spa:first-mount
事件来实现的 -
利用间通信
qiankun 通过公布订阅模式来实现利用间通信,状态由框架来对立保护,每个利用在初始化时由框架生成一套通信办法,利用通过这些办法来更改全局状态和注册回调函数,全局状态产生扭转时触发各个利用注册的回调函数执行,将新旧状态传递到所有利用
阐明
文章基于 qiankun 2.0.26
版本做了残缺的源码剖析,目前网上如同还没有 qiankun 2.x
版本的残缺源码剖析,简略搜了下如同都是 1.x 版本的
因为框架代码比拟多的,博客有字数限度,所以将全部内容拆成了三篇文章,每一篇都可独立浏览:
-
微前端框架 之 qiankun 从入门到精通
,文章由以下三局部组成
为什么不是 single-spa
,具体介绍了 single-spa 存在的问题为什么是 qiankun
,具体介绍了 qiankun 是怎么从框架层面解决 single-spa 存在的问题的源码解读
,残缺解读了 qiankun 2.x 版本的源码
- qiankun 2.x 运行时沙箱 源码剖析,具体解读了 qiankun 2.x 版本的沙箱实现
- HTML Entry 源码剖析,具体解读了 HTML Entry 的原理以及在 qiankun 中的利用
源码解读
这里没有独自编写示例代码,因为 qiankun
源码中提供了残缺的示例我的项目,这也是 qiankun
做的很好的一个中央,提供残缺的示例,防止大家在应用时反复踩坑。
微前端实现和革新时面临的第一个艰难就是主利用的设置、微利用的接入,single-spa
官网没有提供一个很好的示例我的项目,所以大家在应用 single-spa
接入微利用时还是须要踩不少坑的,甚至有些问题须要去浏览源码能力解决
框架目录构造
从 github 克隆我的项目当前,执行一下命令:
-
装置
qiankun
框架所需的包yarn install
-
装置示例我的项目的包
yarn examples:install
以上命令执行完结当前:
有料的 package.json
-
npm-run-all
一个 CLI 工具,用于并行或程序执行多个 npm 脚本
-
father-build
基于 rollup 的库构建工具,father 更加弱小
- 多我的项目的目录组织以及 scripts 局部的编写
-
main 和 module 字段
标识组件库的入口,当两者同时存在时,module 字段的优先级高于 main
示例我的项目中的主利用
这里须要更改一下示例我的项目中主利用的 webpack
配置
{ | |
... | |
devServer: {// 从 package.json 中能够看出,启动示例我的项目时,主利用执行了两条命令,其实就是启动了两个主利用,然而却只配置了一个端口,浏览器关上 localhost:7099 和你料想的有一些出入,这时显示的是 loadMicroApp(手动加载微利用) 形式的主利用,基于路由配置的主利用没起来,因为端口被占用了 | |
// port: '7099' | |
// 这样配置,手动加载微利用的主利用在 7099 端口,基于路由配置的主利用在 7088 端口 | |
port: process.env.MODE === 'multiple' ? '7099' : '7088' | |
} | |
... | |
} |
启动示例我的项目
yarn examples:start
命令执行完结当前,拜访 localhost:7099
和 localhost:7088
两个地址,能够看到如下内容:
到这一步,就证实我的项目正式跑起来了,所有筹备工作就绪
示例我的项目
官网为咱们筹备了两种主利用的实现形式,五种微利用的接入示例,覆盖面能够说是比拟广了,足以满足大家的广泛须要了
主利用
主利用在 examples/main
目录下,提供了两种实现形式,基于路由配置的 registerMicroApps
和 手动加载微利用的 loadMicroApp
。主利用很简略,就是一个从 0 通过 webpack 配置的一个同时反对 react 和 vue 的我的项目,至于为什么同时反对 react 和 vue,持续往下看
webpack.config.js
就是一个一般的 webpack
配置,配置了一个开发服务器 devServer
、两个 loader
(babel-loader、css loader)、一个插件 HtmlWebpackPlugin
(通知 webpack html 模版文件是哪个)
通过 webpack
配置文件的 entry
字段得悉入口文件别离为 index.js
和 multiple.js
基于路由配置
通用将微利用关联到一些 url
规定的形式,实现当浏览器 url
发生变化时,主动加载相应的微利用的性能
index.js
// qiankun api 引入 | |
import {registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState} from '../../es'; | |
// 全局款式 | |
import './index.less'; | |
// 专门针对 angular 微利用引入的一个库 | |
import 'zone.js'; | |
/** | |
* 主利用能够应用任何技术栈,这里提供了 react 和 vue 两种,能够随便切换 | |
* 最终都导出了一个 render 函数,负责渲染主利用 | |
*/ | |
// import render from './render/ReactRender'; | |
import render from './render/VueRender'; | |
// 初始化主利用,其实就是渲染主利用 | |
render({loading: true}); | |
// 定义 loader 函数,切换微利用时由 qiankun 框架负责调用显示一个 loading 状态 | |
const loader = loading => render({loading}); | |
// 注册微利用 | |
registerMicroApps( | |
// 微利用配置列表 | |
[ | |
{ | |
// 利用名称 | |
name: 'react16', | |
// 利用的入口地址 | |
entry: '//localhost:7100', | |
// 利用的挂载点,这个挂载点在下面渲染函数中的模版外面提供的 | |
container: '#subapp-viewport', | |
// 微利用切换时调用的办法,显示一个 loading 状态 | |
loader, | |
// 当路由前缀为 /react16 时激活以后利用 | |
activeRule: '/react16', | |
}, | |
{ | |
name: 'react15', | |
entry: '//localhost:7102', | |
container: '#subapp-viewport', | |
loader, | |
activeRule: '/react15', | |
}, | |
{ | |
name: 'vue', | |
entry: '//localhost:7101', | |
container: '#subapp-viewport', | |
loader, | |
activeRule: '/vue', | |
}, | |
{ | |
name: 'angular9', | |
entry: '//localhost:7103', | |
container: '#subapp-viewport', | |
loader, | |
activeRule: '/angular9', | |
}, | |
{ | |
name: 'purehtml', | |
entry: '//localhost:7104', | |
container: '#subapp-viewport', | |
loader, | |
activeRule: '/purehtml', | |
}, | |
], | |
// 全局生命周期钩子,切换微利用时框架负责调用 | |
{ | |
beforeLoad: [ | |
app => { | |
// 这个打印日志的办法能够学习一下,第三个参数会替换掉第一个参数中的 %c%s,并且第三个参数的色彩由第二个参数决定 | |
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name); | |
}, | |
], | |
beforeMount: [ | |
app => {console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name); | |
}, | |
], | |
afterUnmount: [ | |
app => {console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name); | |
}, | |
], | |
}, | |
); | |
// 定义全局状态,并返回两个通信办法 | |
const {onGlobalStateChange, setGlobalState} = initGlobalState({user: 'qiankun',}); | |
// 监听全局状态的更改,当状态产生扭转时执行回调函数 | |
onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev)); | |
// 设置新的全局状态,只能设置一级属性,微利用只能批改已存在的一级属性 | |
setGlobalState({ | |
ignore: 'master', | |
user: {name: 'master',}, | |
}); | |
// 设置默认进入的子利用,当主利用启动当前默认进入指定微利用 | |
setDefaultMountApp('/react16'); | |
// 启动利用 | |
start(); | |
// 当第一个微利用挂载当前,执行回调函数,在这里能够做一些非凡的事件,比方开启一监控或者买点脚本 | |
runAfterFirstMounted(() => {console.log('[MainApp] first app mounted'); | |
}); |
VueRender.js
/** | |
* 导出一个由 vue 实现的渲染函数,渲染了一个模版,模版外面蕴含一个 loading 状态节点和微利用容器节点 | |
*/ | |
import Vue from 'vue/dist/vue.esm'; | |
// 返回一个 vue 实例 | |
function vueRender({loading}) { | |
return new Vue({ | |
template: ` | |
<div id="subapp-container"> | |
<h4 v-if="loading" class="subapp-loading">Loading...</h4> | |
<div id="subapp-viewport"></div> | |
</div> | |
`, | |
el: '#subapp-container', | |
data() { | |
return {loading,}; | |
}, | |
}); | |
} | |
// vue 实例 | |
let app = null; | |
// 渲染函数 | |
export default function render({loading}) { | |
// 单例,如果 vue 实例不存在则实例化主利用,存在则阐明主利用曾经渲染,须要更新主营利用的 loading 状态 | |
if (!app) {app = vueRender({ loading}); | |
} else {app.loading = loading;} | |
} |
ReactRender.js
/** | |
* 同 vue 实现的渲染函数,这里通过 react 实现了一个一样的渲染函数 | |
*/ | |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
// 渲染主利用 | |
function Render(props) {const { loading} = props; | |
return ( | |
<> | |
{loading && <h4 className="subapp-loading">Loading...</h4>} | |
<div id="subapp-viewport" /> | |
</> | |
); | |
} | |
// 将主利用渲染到指定节点下 | |
export default function render({loading}) {const container = document.getElementById('subapp-container'); | |
ReactDOM.render(<Render loading={loading} />, container); | |
} |
手动加载微利用
通常这种场景下的微利用是一个不领路由的可独立运行的业务组件,这种应用形式的状况比拟少见
multiple.js
/** | |
* 调用 loadMicroApp 办法注册了两个微利用 | |
*/ | |
import {loadMicroApp} from '../../es'; | |
const app1 = loadMicroApp( | |
// 利用配置,名称、入口地址、容器节点 | |
{name: 'react15', entry: '//localhost:7102', container: '#react15'}, | |
// 能够增加一些其它的配置,比方:沙箱、款式隔离等 | |
{ | |
sandbox: {// strictStyleIsolation: true,}, | |
}, | |
); | |
const app2 = loadMicroApp({ name: 'vue', entry: '//localhost:7101', container: '#vue'}, | |
{ | |
sandbox: {// strictStyleIsolation: true,}, | |
}, | |
); |
vue
vue 微利用在 examples/vue
目录下,就是一个通过 vue-cli 创立的 vue demo 利用,而后对 vue.config.js
和 main.js
做了一些更改
vue.config.js
一个一般的 webpack
配置,须要留神的中央就三点
{ | |
... | |
// publicPath 没在这里设置,是通过 webpack 提供的全局变量 __webpack_public_path__ 来即时设置的,webpackjs.com/guides/public-path/ | |
devServer: { | |
... | |
// 设置跨域,因为主利用须要通过 fetch 去获取微利用引入的动态资源的,所以必须要求这些动态资源反对跨域 | |
headers: {'Access-Control-Allow-Origin': '*',}, | |
}, | |
output: { | |
// 把子利用打包成 umd 库格局 | |
library: `${name}-[name]`, // 库名称,惟一 | |
libraryTarget: 'umd', | |
jsonpFunction: `webpackJsonp_${name}`, | |
} | |
... | |
} |
main.js
// 动静设置 __webpack_public_path__ | |
import './public-path'; | |
import ElementUI from 'element-ui'; | |
import 'element-ui/lib/theme-chalk/index.css'; | |
import Vue from 'vue'; | |
import VueRouter from 'vue-router'; | |
import App from './App.vue'; | |
// 路由配置 | |
import routes from './router'; | |
import store from './store'; | |
Vue.config.productionTip = false; | |
Vue.use(ElementUI); | |
let router = null; | |
let instance = null; | |
// 利用渲染函数 | |
function render(props = {}) {const { container} = props; | |
// 实例化 router,依据利用运行环境设置路由前缀 | |
router = new VueRouter({ | |
// 作为微利用运行,则设置 /vue 为前缀,否则设置 / | |
base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/', | |
mode: 'history', | |
routes, | |
}); | |
// 实例化 vue 实例 | |
instance = new Vue({ | |
router, | |
store, | |
render: h => h(App), | |
}).$mount(container ? container.querySelector('#app') : '#app'); | |
} | |
// 反对利用独立运行 | |
if (!window.__POWERED_BY_QIANKUN__) {render(); | |
} | |
/** | |
* 从 props 中获取通信办法,监听全局状态的更改和设置全局状态,只能操作一级属性 | |
* @param {*} props | |
*/ | |
function storeTest(props) { | |
props.onGlobalStateChange && | |
props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), | |
true, | |
); | |
props.setGlobalState && | |
props.setGlobalState({ | |
ignore: props.name, | |
user: {name: props.name,}, | |
}); | |
} | |
/** | |
* 导出的三个生命周期函数 | |
*/ | |
// 初始化 | |
export async function bootstrap() {console.log('[vue] vue app bootstraped'); | |
} | |
// 挂载微利用 | |
export async function mount(props) {console.log('[vue] props from main framework', props); | |
storeTest(props); | |
render(props); | |
} | |
// 卸载、销毁微利用 | |
export async function unmount() {instance.$destroy(); | |
instance.$el.innerHTML = ''; | |
instance = null; | |
router = null; | |
} |
public-path.js
/** | |
* 在入口文件中应用 ES6 模块导入,则在导入后对 __webpack_public_path__ 进行赋值。* 在这种状况下,必须将公共门路 (public path) 赋值移至专属模块,而后将其在最后面导入 | |
*/ | |
// qiankun 设置的全局变量,示意利用作为微利用在运行 | |
if (window.__POWERED_BY_QIANKUN__) { | |
// eslint-disable-next-line no-undef | |
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; | |
} |
jQuery
这是一个应用了 jQuery 的我的项目,在 examples/purehtml
目录下,展现了如何接入应用 jQuery 开发的利用
package.json
为了达到演示成果,应用 http-server
在起了一个本地服务器,并且反对跨域
{ | |
... | |
"scripts": { | |
"start": "cross-env PORT=7104 http-server . --cors", | |
"test": "echo \"Error: no test specified\"&& exit 1" | |
}, | |
... | |
} |
entry.js
// 渲染函数 | |
const render = $ => {$('#purehtml-container').html('Hello, render with jQuery'); | |
return Promise.resolve();}; | |
// 在全局对象上导出三个生命周期函数 | |
(global => {global['purehtml'] = {bootstrap: () => {console.log('purehtml bootstrap'); | |
return Promise.resolve();}, | |
mount: () => {console.log('purehtml mount'); | |
// 调用渲染函数 | |
return render($); | |
}, | |
unmount: () => {console.log('purehtml unmount'); | |
return Promise.resolve();}, | |
}; | |
})(window); |
index.html
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Purehtml Example</title> | |
<script src="//cdn.bootcss.com/jquery/3.4.1/jquery.min.js"> | |
</script> | |
</head> | |
<body> | |
<div style="display: flex; justify-content: center; align-items: center; height: 200px;"> | |
Purehtml Example | |
</div> | |
<div id="purehtml-container" style="text-align:center"></div> | |
<!-- 引入 entry.js,相当于 vue 我的项目的 publicPath 配置 --> | |
<script src="//localhost:7104/entry.js" entry></script> | |
</body> | |
</html> |
angular 9、react 15、react 16
这三个实例我的项目就不一一剖析了,和 vue 我的项目相似,都是配置打包工具将微利用打包成一个 umd 格局,而后配置利用入口文件 和 路由前缀
小结
好了,读到这里,零碎革新(能够开始干活了)基本上就曾经能够顺利进行了,从主利用的开发到微利用接入,应该是不会有什么问题了。
当然如果你想持续深刻理解,比方:
- 下面用到那些 API 的原理是什么?
- qiankun 是怎么解决咱们之前提到的 single-spa 未解决的问题的?
- …
接下来就带着咱们的疑难和目标去全面深刻的理解 qiankun
框架的外部实现
框架源码
整个框架的源码目录是 src
,入口文件是 src/index.ts
入口 src/index.ts
/** | |
* 在示例或者官网提到的所有 API 都在这里对立导出 | |
*/ | |
// 最要害的三个,手动加载微利用、基于路由配置、启动 qiankun | |
export {loadMicroApp, registerMicroApps, start} from './apis'; | |
// 全局状态 | |
export {initGlobalState} from './globalState'; | |
// 全局的未捕捉异样处理器 | |
export * from './errorHandler'; | |
// setDefaultMountApp 设置主利用启动后默认进入哪个微利用、runAfterFirstMounted 设置当第一个微利用挂载当前须要调用的一些办法 | |
export * from './effects'; | |
// 类型定义 | |
export * from './interfaces'; | |
// prefetch | |
export {prefetchImmediately as prefetchApps} from './prefetch'; |
registerMicroApps
/** | |
* 注册微利用,基于路由配置 | |
* @param apps = [ | |
* { | |
* name: 'react16', | |
* entry: '//localhost:7100', | |
* container: '#subapp-viewport', | |
* loader, | |
* activeRule: '/react16' | |
* }, | |
* ... | |
* ] | |
* @param lifeCycles = {... 各个生命周期办法对象} | |
*/ | |
export function registerMicroApps<T extends object = {}>( | |
apps: Array<RegistrableApp<T>>, | |
lifeCycles?: FrameworkLifeCycles<T>, | |
) { | |
// 避免微利用反复注册,失去所有没有被注册的微利用列表 | |
const unregisteredApps = apps.filter(app => !microApps.some(registeredApp => registeredApp.name === app.name)); | |
// 所有的微利用 = 已注册 + 未注册的(将要被注册的) | |
microApps = [...microApps, ...unregisteredApps]; | |
// 注册每一个微利用 | |
unregisteredApps.forEach(app => { | |
// 注册时提供的微利用根本信息 | |
const {name, activeRule, loader = noop, props, ...appConfig} = app; | |
// 调用 single-spa 的 registerApplication 办法注册微利用 | |
registerApplication({ | |
// 微利用名称 | |
name, | |
// 微利用的加载办法,Promise< 生命周期办法组成的对象 > | |
app: async () => { | |
// 加载微利用时主利用显示 loading 状态 | |
loader(true); | |
// 这句能够疏忽,目标是在 single-spa 执行这个加载办法时让出线程,让其它微利用的加载办法都开始执行 | |
await frameworkStartedDefer.promise; | |
// 外围、精华、难点所在,负责加载微利用,而后一大堆解决,返回 bootstrap、mount、unmount、update 这个几个生命周期 | |
const {mount, ...otherMicroAppConfigs} = await loadApp( | |
// 微利用的配置信息 | |
{name, props, ...appConfig}, | |
// start 办法执行时设置的配置对象 | |
frameworkConfiguration, | |
// 注册微利用时提供的全局生命周期对象 | |
lifeCycles, | |
); | |
return {mount: [async () => loader(true), ...toArray(mount), async () => loader(false)], | |
...otherMicroAppConfigs, | |
}; | |
}, | |
// 微利用的激活条件 | |
activeWhen: activeRule, | |
// 传递给微利用的 props | |
customProps: props, | |
}); | |
}); | |
} |
start
/** | |
* 启动 qiankun | |
* @param opts start 办法的配置对象 | |
*/ | |
export function start(opts: FrameworkConfiguration = {}) { | |
// qiankun 框架默认开启预加载、单例模式、款式沙箱 | |
frameworkConfiguration = {prefetch: true, singular: true, sandbox: true, ...opts}; | |
// 从这里能够看出 start 办法反对的参数不止官网文档说的那些,比方 urlRerouteOnly,这个是 single-spa 的 start 办法反对的 | |
const {prefetch, sandbox, singular, urlRerouteOnly, ...importEntryOpts} = frameworkConfiguration; | |
// 预加载 | |
if (prefetch) {// 执行预加载策略,参数别离为微利用列表、预加载策略、{ fetch、getPublicPath、getTemplate} | |
doPrefetchStrategy(microApps, prefetch, importEntryOpts); | |
} | |
// 款式沙箱 | |
if (sandbox) {if (!window.Proxy) {console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox'); | |
// 快照沙箱不反对非 singular 模式 | |
if (!singular) {console.error('[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable'); | |
// 如果开启沙箱,会强制应用单例模式 | |
frameworkConfiguration.singular = true; | |
} | |
} | |
} | |
// 执行 single-spa 的 start 办法,启动 single-spa | |
startSingleSpa({urlRerouteOnly}); | |
frameworkStartedDefer.resolve();} |
预加载 – doPrefetchStrategy
/** | |
* 执行预加载策略,qiankun 反对四种 | |
* @param apps 所有的微利用 | |
* @param prefetchStrategy 预加载策略,四种 =》* 1、true,第一个微利用挂载当前加载其它微利用的动态资源,利用的是 single-spa 提供的 single-spa:first-mount 事件来实现的 | |
* 2、string[],微利用名称数组,在第一个微利用挂载当前加载指定的微利用的动态资源 | |
* 3、all,主利用执行 start 当前就间接开始预加载所有微利用的动态资源 | |
* 4、自定义函数,返回两个微利用组成的数组,一个是要害微利用组成的数组,须要马上就执行预加载的微利用,一个是一般的微利用组成的数组,在第一个微利用挂载当前预加载这些微利用的动态资源 | |
* @param importEntryOpts = {fetch, getPublicPath, getTemplate} | |
*/ | |
export function doPrefetchStrategy(apps: AppMetadata[], | |
prefetchStrategy: PrefetchStrategy, | |
importEntryOpts?: ImportEntryOpts, | |
) {// 定义函数,函数接管一个微利用名称组成的数组,而后从微利用列表中返回这些名称所对应的微利用,最初失去一个数组[{name, entry}, ...] | |
const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter(app => names.includes(app.name)); | |
if (Array.isArray(prefetchStrategy)) { | |
// 阐明加载策略是一个数组,当第一个微利用挂载之后开始加载数组内由用户指定的微利用资源,数组内的每一项示意一个微利用的名称 | |
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts); | |
} else if (isFunction(prefetchStrategy)) {// 加载策略是一个自定义的函数,可齐全自定义利用资源的加载机会(首屏利用、次屏利用) | |
(async () => { | |
// critical rendering apps would be prefetch as earlier as possible,要害的应用程序应该尽可能早的预取 | |
// 执行加载策略函数,函数会返回两个数组,一个要害的应用程序数组,会立刻执行预加载动作,另一个是在第一个微利用挂载当前执行微利用动态资源的预加载 | |
const {criticalAppNames = [], minorAppsName = []} = await prefetchStrategy(apps); | |
// 立刻预加载这些要害微应用程序的动态资源 | |
prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts); | |
// 当第一个微利用挂载当前预加载这些微利用的动态资源 | |
prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts); | |
})();} else { | |
// 加载策略是默认的 true 或者 all | |
switch (prefetchStrategy) { | |
case true: | |
// 第一个微利用挂载之后开始加载其它微利用的动态资源 | |
prefetchAfterFirstMounted(apps, importEntryOpts); | |
break; | |
case 'all': | |
// 在主利用执行 start 当前就开始加载所有微利用的动态资源 | |
prefetchImmediately(apps, importEntryOpts); | |
break; | |
default: | |
break; | |
} | |
} | |
} | |
// 判断是否为弱网环境 | |
const isSlowNetwork = navigator.connection | |
? navigator.connection.saveData || | |
(navigator.connection.type !== 'wifi' && | |
navigator.connection.type !== 'ethernet' && | |
/(2|3)g/.test(navigator.connection.effectiveType)) | |
: false; | |
/** | |
* prefetch assets, do nothing while in mobile network | |
* 预加载动态资源,在挪动网络下什么都不做 | |
* @param entry | |
* @param opts | |
*/ | |
function prefetch(entry: Entry, opts?: ImportEntryOpts): void { | |
// 弱网环境下不执行预加载 | |
if (!navigator.onLine || isSlowNetwork) { | |
// Don't prefetch if in a slow network or offline | |
return; | |
} | |
// 通过工夫切片的形式去加载动态资源,在浏览器闲暇时去执行回调函数,防止浏览器卡顿 | |
requestIdleCallback(async () => { | |
// 失去加载动态资源的函数 | |
const {getExternalScripts, getExternalStyleSheets} = await importEntry(entry, opts); | |
// 款式 | |
requestIdleCallback(getExternalStyleSheets); | |
// js 脚本 | |
requestIdleCallback(getExternalScripts); | |
}); | |
} | |
/** | |
* 在第一个微利用挂载之后开始加载 apps 中指定的微利用的动态资源 | |
* 通过监听 single-spa 提供的 single-spa:first-mount 事件来实现,该事件在第一个微利用挂载当前会被触发 | |
* @param apps 须要被预加载动态资源的微利用列表,[{name, entry}, ...] | |
* @param opts = {fetch , getPublicPath, getTemplate} | |
*/ | |
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void { | |
// 监听 single-spa:first-mount 事件 | |
window.addEventListener('single-spa:first-mount', function listener() { | |
// 已挂载的微利用 | |
const mountedApps = getMountedApps(); | |
// 从预加载的微利用列表中过滤出未挂载的微利用 | |
const notMountedApps = apps.filter(app => mountedApps.indexOf(app.name) === -1); | |
// 开发环境打印日志,已挂载的微利用和未挂载的微利用别离有哪些 | |
if (process.env.NODE_ENV === 'development') {console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notMountedApps); | |
} | |
// 循环加载微利用的动态资源 | |
notMountedApps.forEach(({entry}) => prefetch(entry, opts)); | |
// 移除 single-spa:first-mount 事件 | |
window.removeEventListener('single-spa:first-mount', listener); | |
}); | |
} | |
/** | |
* 在执行 start 启动 qiankun 之后立刻预加载所有微利用的动态资源 | |
* @param apps 须要被预加载动态资源的微利用列表,[{name, entry}, ...] | |
* @param opts = {fetch , getPublicPath, getTemplate} | |
*/ | |
export function prefetchImmediately(apps: AppMetadata[], opts?: ImportEntryOpts): void { | |
// 开发环境打印日志 | |
if (process.env.NODE_ENV === 'development') {console.log('[qiankun] prefetch starting for apps...', apps); | |
} | |
// 加载所有微利用的动态资源 | |
apps.forEach(({entry}) => prefetch(entry, opts)); | |
} |
利用间通信 initGlobalState
// 触发全局监听,执行所有利用注册的回调函数 | |
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) { | |
// 循环遍历,执行所有利用注册的回调函数 | |
Object.keys(deps).forEach((id: string) => {if (deps[id] instanceof Function) {deps[id](cloneDeep(state), cloneDeep(prevState)); | |
} | |
}); | |
} | |
/** | |
* 定义全局状态,并返回通信办法,个别由主利用调用,微利用通过 props 获取通信办法。* @param state 全局状态,{key: value} | |
*/ | |
export function initGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[qiankun] state has not changed!'); | |
} else { | |
// 办法有可能被反复调用,将已有的全局状态克隆一份,为空则是第一次调用 initGlobalState 办法,不为空则非第一次次调用 | |
const prevGlobalState = cloneDeep(globalState); | |
// 将传递的状态克隆一份赋值为 globalState | |
globalState = cloneDeep(state); | |
// 触发全局监听,当然在这个地位调用,失常状况下没啥反馈,因为当初还没有利用注册回调函数 | |
emitGlobal(globalState, prevGlobalState); | |
} | |
// 返回通信办法,参数示意利用 id,true 示意本人是主利用调用 | |
return getMicroAppStateActions(`global-${+new Date()}`, true); | |
} | |
/** | |
* 返回通信办法 | |
* @param id 利用 id | |
* @param isMaster 表明调用的利用是否为主利用,在主利用初始化全局状态时,initGlobalState 外部调用该办法时会传递 true,其它都为 false | |
*/ | |
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions { | |
return { | |
/** | |
* 全局依赖监听,为指定利用(id = 利用 id)注册回调函数 | |
* 依赖数据结构为:* {* {id}: callback | |
* } | |
* | |
* @param callback 注册的回调函数 | |
* @param fireImmediately 是否立刻执行回调 | |
*/ | |
onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) { | |
// 回调函数必须为 function | |
if (!(callback instanceof Function)) {console.error('[qiankun] callback must be function!'); | |
return; | |
} | |
// 如果回调函数曾经存在,反复注册时给出笼罩提示信息 | |
if (deps[id]) {console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`); | |
} | |
// id 为一个利用 id,一个利用对应一个回调 | |
deps[id] = callback; | |
// 克隆全局状态 | |
const cloneState = cloneDeep(globalState); | |
// 如果须要,立刻登程回调执行 | |
if (fireImmediately) {callback(cloneState, cloneState); | |
} | |
}, | |
/** | |
* setGlobalState 更新 store 数据 | |
* | |
* 1. 对新输出 state 的第一层属性做校验,如果是主利用则能够增加新的一级属性进来,也能够更新已存在的一级属性,* 如果是微利用,则只能更新已存在的一级属性,不能够新增一级属性 | |
* 2. 触发全局监听,执行所有利用注册的回调函数,以达到利用间通信的目标 | |
* | |
* @param state 新的全局状态 | |
*/ | |
setGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[qiankun] state has not changed!'); | |
return false; | |
} | |
// 记录旧的全局状态中被扭转的 key | |
const changeKeys: string[] = []; | |
// 旧的全局状态 | |
const prevGlobalState = cloneDeep(globalState); | |
globalState = cloneDeep( | |
// 循环遍历新状态中的所有 key | |
Object.keys(state).reduce((_globalState, changeKey) => {if (isMaster || _globalState.hasOwnProperty(changeKey)) { | |
// 主利用 或者 旧的全局状态存在该 key 时才进来,阐明只有主利用才能够新增属性,微利用只能够更新已存在的属性值,且不论主利用微利用只能更新一级属性 | |
// 记录被扭转的 key | |
changeKeys.push(changeKey); | |
// 更新旧状态中对应的 key value | |
return Object.assign(_globalState, { [changeKey]: state[changeKey] }); | |
} | |
console.warn(`[qiankun] '${changeKey}' not declared when init state!`); | |
return _globalState; | |
}, globalState), | |
); | |
if (changeKeys.length === 0) {console.warn('[qiankun] state has not changed!'); | |
return false; | |
} | |
// 触发全局监听 | |
emitGlobal(globalState, prevGlobalState); | |
return true; | |
}, | |
// 登记该利用下的依赖 | |
offGlobalStateChange() {delete deps[id]; | |
return true; | |
}, | |
}; | |
} |
全局未捕捉异样处理器
/** | |
* 整个文件的逻辑一眼明了,整个框架提供了两种全局异样捕捉,一个是 single-spa 提供的,另一个是 qiankun 本人的,你只需提供相应的回调函数即可 | |
*/ | |
// single-spa 的异样捕捉 | |
export {addErrorHandler, removeErrorHandler} from 'single-spa'; | |
// qiankun 的异样捕捉 | |
// 监听了 error 和 unhandlerejection 事件 | |
export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {window.addEventListener('error', errorHandler); | |
window.addEventListener('unhandledrejection', errorHandler); | |
} | |
// 移除 error 和 unhandlerejection 事件监听 | |
export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {window.removeEventListener('error', errorHandler); | |
window.removeEventListener('unhandledrejection', errorHandler); | |
} |
setDefaultMountApp
/** | |
* 设置主利用启动后默认进入的微利用,其实是规定了第一个微利用挂载实现后决定默认进入哪个微利用 | |
* 利用的是 single-spa 的 single-spa:no-app-change 事件,该事件在所有微利用状态扭转完结后(即产生路由切换且新的微利用曾经被挂载实现)触发 | |
* @param defaultAppLink 微利用的链接,比方 /react16 | |
*/ | |
export function setDefaultMountApp(defaultAppLink: string) { | |
// 当事件触发时就阐明微利用曾经挂载实现,但这里只监听了一次,因为事件被触发当前就移除了监听,所以说是主利用启动后默认进入的微利用,且只执行了一次的起因 | |
window.addEventListener('single-spa:no-app-change', function listener() { | |
// 阐明微利用曾经挂载实现,获取挂载的微利用列表,再次确认的确有微利用挂载了,其实这个确认没啥必要 | |
const mountedApps = getMountedApps(); | |
if (!mountedApps.length) { | |
// 这个是 single-spa 提供的一个 api,通过触发 window.location.hash 或者 pushState 更改路由,切换微利用 | |
navigateToUrl(defaultAppLink); | |
} | |
// 触发一次当前,就移除该事件的监听函数,后续的路由切换(事件触发)时就不再响应 | |
window.removeEventListener('single-spa:no-app-change', listener); | |
}); | |
} | |
// 这个 api 和 setDefaultMountApp 作用统一,官网也提到,兼容老版本的一个 api | |
export function runDefaultMountEffects(defaultAppLink: string) { | |
console.warn('[qiankun] runDefaultMountEffects will be removed in next version, please use setDefaultMountApp instead', | |
); | |
setDefaultMountApp(defaultAppLink); | |
} |
runAfterFirstMounted
/** | |
* 第一个微利用 mount 后须要调用的办法,比方开启一些监控或者埋点脚本 | |
* 同样利用的 single-spa 的 single-spa:first-mount 事件,当第一个微利用挂载当前会触发 | |
* @param effect 回调函数,当第一个微利用挂载当前要做的事件 | |
*/ | |
export function runAfterFirstMounted(effect: () => void) { | |
// can not use addEventListener once option for ie support | |
window.addEventListener('single-spa:first-mount', function listener() {if (process.env.NODE_ENV === 'development') {console.timeEnd(firstMountLogLabel); | |
} | |
effect(); | |
// 这里不移除也没事,因为这个事件后续不会再被触发了 | |
window.removeEventListener('single-spa:first-mount', listener); | |
}); | |
} |
手动加载微利用 loadMicroApp
/** | |
* 手动加载一个微利用,是通过 single-spa 的 mountRootParcel api 实现的,返回微利用实例 | |
* @param app = {name, entry, container, props} | |
* @param configuration 配置对象 | |
* @param lifeCycles 还反对一个全局生命周期配置对象,这个参数官网文档没提到 | |
*/ | |
export function loadMicroApp<T extends object = {}>( | |
app: LoadableApp<T>, | |
configuration?: FrameworkConfiguration, | |
lifeCycles?: FrameworkLifeCycles<T>, | |
): MicroApp {const { props} = app; | |
// single-spa 的 mountRootParcel api | |
return mountRootParcel(() => loadApp(app, configuration ?? frameworkConfiguration, lifeCycles), {domElement: document.createElement('div'), | |
...props, | |
}); | |
} |
qiankun 的外围 loadApp
接下来介绍
loadApp
办法,集体认为qiankun
的外围代码能够说大部分都在这里,当然这也是整个框架的精华和难点所在
/** | |
* 实现了以下几件事:* 1、通过 HTML Entry 的形式近程加载微利用,失去微利用的 html 模版(首屏内容)、JS 脚本执行器、动态经资源门路 | |
* 2、款式隔离,shadow DOM 或者 scoped css 两种形式 | |
* 3、渲染微利用 | |
* 4、运行时沙箱,JS 沙箱、款式沙箱 | |
* 5、合并沙箱传递进去的 生命周期办法、用户传递的生命周期办法、框架内置的生命周期办法,将这些生命周期办法对立整顿,导出一个生命周期对象,* 供 single-spa 的 registerApplication 办法应用,这个对象就相当于应用 single-spa 时你的微利用导出的那些生命周期办法,只不过 qiankun | |
* 额定填了一些生命周期办法,做了一些事件 | |
* 6、给微利用注册通信办法并返回通信办法,而后会将通信办法通过 props 注入到微利用 | |
* @param app 微利用配置对象 | |
* @param configuration start 办法执行时设置的配置对象 | |
* @param lifeCycles 注册微利用时提供的全局生命周期对象 | |
*/ | |
export async function loadApp<T extends object>( | |
app: LoadableApp<T>, | |
configuration: FrameworkConfiguration = {}, | |
lifeCycles?: FrameworkLifeCycles<T>, | |
): Promise<ParcelConfigObject> { | |
// 微利用的入口和名称 | |
const {entry, name: appName} = app; | |
// 实例 id | |
const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`; | |
// 上面这个不必管,就是生成一个标记名称,而后应用该名称在浏览器性能缓冲器中设置一个工夫戳,能够用来度量程序的执行工夫,performance.mark、performance.measure | |
const markName = `[qiankun] App ${appInstanceId} Loading`; | |
if (process.env.NODE_ENV === 'development') {performanceMark(markName); | |
} | |
// 配置信息 | |
const {singular = false, sandbox = true, excludeAssetFilter, ...importEntryOpts} = configuration; | |
/** | |
* 获取微利用的入口 html 内容和脚本执行器 | |
* template 是 link 替换为 style 后的 template | |
* execScript 是 让 JS 代码 (scripts) 在指定 上下文 中运行 | |
* assetPublicPath 是动态资源地址 | |
*/ | |
const {template, execScripts, assetPublicPath} = await importEntry(entry, importEntryOpts); | |
// single-spa 的限度,加载、初始化和卸载不能同时进行,必须等卸载实现当前才能够进行加载,这个 promise 会在微利用卸载实现后被 resolve,在前面能够看到 | |
if (await validateSingularMode(singular, app)) {await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise); | |
} | |
// --------------- 款式隔离 --------------- | |
// 是否严格款式隔离 | |
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation; | |
// 实验性的款式隔离,前面就叫 scoped css,和严格款式隔离不能同时开启,如果开启了严格款式隔离,则 scoped css 就为 false,强制敞开 | |
const enableScopedCSS = isEnableScopedCSS(configuration); | |
// 用一个容器元素包裹微利用入口 html 模版, appContent = `<div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>` | |
const appContent = getDefaultTplWrapper(appInstanceId, appName)(template); | |
// 将 appContent 有字符串模版转换为 html dom 元素,如果须要开启款式严格隔离,则将 appContent 的子元素即微利用入口模版用 shadow dom 包裹起来,以达到款式严格隔离的目标 | |
let element: HTMLElement | null = createElement(appContent, strictStyleIsolation); | |
// 通过 scoped css 的形式隔离款式,从这里也就能看出官网为什么说:// 在目前的阶段,该性能还不反对动静的、应用 <link /> 标签来插入外联的款式,但思考在将来反对这部分场景 | |
// 在现阶段只解决 style 这种内联标签的状况 | |
if (element && isEnableScopedCSS(configuration)) {const styleNodes = element.querySelectorAll('style') || []; | |
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {css.process(element!, stylesheetElement, appName); | |
}); | |
} | |
// --------------- 渲染微利用 --------------- | |
// 主利用装载微利用的容器节点 | |
const container = 'container' in app ? app.container : undefined; | |
// 这个是 1.x 版本遗留下来的实现,如果提供了 render 函数,当微利用须要被激活时就执行 render 函数渲染微利用,新版本用的 container,弃了 render | |
// 而且 legacyRender 和 strictStyleIsolation、scoped css 不兼容 | |
const legacyRender = 'render' in app ? app.render : undefined; | |
// 返回一个 render 函数,这个 render 函数要不应用用户传递的 render 函数,要不将 element 插入到 container | |
const render = getRender(appName, appContent, container, legacyRender); | |
// 渲染微利用到容器节点,并显示 loading 状态 | |
render({element, loading: true}, 'loading'); | |
// 失去一个 getter 函数,通过该函数能够获取 <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div> | |
const containerGetter = getAppWrapperGetter( | |
appName, | |
appInstanceId, | |
!!legacyRender, | |
strictStyleIsolation, | |
enableScopedCSS, | |
() => element,); | |
// --------------- 运行时沙箱 --------------- | |
// 保障每一个微利用运行在一个洁净的环境中(JS 执行上下文独立、利用间不会产生款式净化)let global = window; | |
let mountSandbox = () => Promise.resolve(); | |
let unmountSandbox = () => Promise.resolve(); | |
if (sandbox) { | |
/** | |
* 生成运行时沙箱,这个沙箱其实由两局部组成 => JS 沙箱(执行上下文)、款式沙箱 | |
* | |
* 沙箱返回 window 的代理对象 proxy 和 mount、unmount 两个办法 | |
* unmount 办法会让微利用失活,复原被加强的原生办法,并记录一堆 rebuild 函数,这个函数是微利用卸载时心愿本人被从新挂载时要做的一些事件,比方动静样式表重建(卸载时会缓存)* mount 办法会执行一些一些 patch 动作,复原原生办法的加强性能,并执行 rebuild 函数,将微利用复原到卸载时的状态,当然从初始化状态进入挂载状态就没有复原一说了 | |
*/ | |
const sandboxInstance = createSandbox( | |
appName, | |
containerGetter, | |
Boolean(singular), | |
enableScopedCSS, | |
excludeAssetFilter, | |
); | |
// 用沙箱的代理对象作为接下来应用的全局对象 | |
global = sandboxInstance.proxy as typeof window; | |
mountSandbox = sandboxInstance.mount; | |
unmountSandbox = sandboxInstance.unmount; | |
} | |
// 合并用户传递的生命周期对象和 qiankun 框架内置的生命周期对象 | |
const {beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = mergeWith({}, | |
// 返回内置生命周期对象,global.__POWERED_BY_QIANKUN__ 和 global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的设置就是在内置的生命周期对象中设置的 | |
getAddOns(global, assetPublicPath), | |
lifeCycles, | |
(v1, v2) => concat(v1 ?? [], v2 ?? []), | |
); | |
await execHooksChain(toArray(beforeLoad), app, global); | |
// get the lifecycle hooks from module exports,获取微利用裸露进去的生命周期函数 | |
const scriptExports: any = await execScripts(global, !singular); | |
const {bootstrap, mount, unmount, update} = getLifecyclesFromExports(scriptExports, appName, global); | |
// 给微利用注册通信办法并返回通信办法,而后会将通信办法通过 props 注入到微利用 | |
const { | |
onGlobalStateChange, | |
setGlobalState, | |
offGlobalStateChange, | |
}: Record<string, Function> = getMicroAppStateActions(appInstanceId); | |
const parcelConfig: ParcelConfigObject = { | |
name: appInstanceId, | |
bootstrap, | |
// 挂载阶段须要执行的一系列办法 | |
mount: [ | |
// 性能度量,不必管 | |
async () => {if (process.env.NODE_ENV === 'development') {const marks = performance.getEntriesByName(markName, 'mark'); | |
// mark length is zero means the app is remounting | |
if (!marks.length) {performanceMark(markName); | |
} | |
} | |
}, | |
// 单例模式须要等微利用卸载实现当前能力执行挂载工作,promise 会在微利用卸载完当前 resolve | |
async () => {if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {return prevAppUnmountedDeferred.promise;} | |
return undefined; | |
}, | |
// 增加 mount hook, 确保每次利用加载前容器 dom 构造曾经设置结束 | |
async () => { | |
// element would be destroyed after unmounted, we need to recreate it if it not exist | |
// unmount 阶段会置空,这里从新生成 | |
element = element || createElement(appContent, strictStyleIsolation); | |
// 渲染微利用到容器节点,并显示 loading 状态 | |
render({element, loading: true}, 'mounting'); | |
}, | |
// 运行时沙箱导出的 mount | |
mountSandbox, | |
// exec the chain after rendering to keep the behavior with beforeLoad | |
async () => execHooksChain(toArray(beforeMount), app, global), | |
// 向微利用的 mount 生命周期函数传递参数,比方微利用中应用的 props.onGlobalStateChange 办法 | |
async props => mount({...props, container: containerGetter(), setGlobalState, onGlobalStateChange }), | |
// 利用 mount 实现后完结 loading | |
async () => render({ element, loading: false}, 'mounted'), | |
async () => execHooksChain(toArray(afterMount), app, global), | |
// initialize the unmount defer after app mounted and resolve the defer after it unmounted | |
// 微利用挂载实现当前初始化这个 promise,并且在微利用卸载当前 resolve 这个 promise | |
async () => {if (await validateSingularMode(singular, app)) {prevAppUnmountedDeferred = new Deferred<void>(); | |
} | |
}, | |
// 性能度量,不必管 | |
async () => {if (process.env.NODE_ENV === 'development') {const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`; | |
performanceMeasure(measureName, markName); | |
} | |
}, | |
], | |
// 卸载微利用 | |
unmount: [async () => execHooksChain(toArray(beforeUnmount), app, global), | |
// 执行微利用的 unmount 生命周期函数 | |
async props => unmount({...props, container: containerGetter() }), | |
// 沙箱导出的 unmount 办法 | |
unmountSandbox, | |
async () => execHooksChain(toArray(afterUnmount), app, global), | |
// 显示 loading 状态、移除微利用的状态监听、置空 element | |
async () => {render({ element: null, loading: false}, 'unmounted'); | |
offGlobalStateChange(appInstanceId); | |
// for gc | |
element = null; | |
}, | |
// 微利用卸载当前 resolve 这个 promise,框架就能够进行后续的工作,比方加载或者挂载其它微利用 | |
async () => {if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {prevAppUnmountedDeferred.resolve(); | |
} | |
}, | |
], | |
}; | |
// 微利用有可能定义 update 办法 | |
if (typeof update === 'function') {parcelConfig.update = update;} | |
return parcelConfig; | |
} |
款式隔离
qiankun
的款式隔离有两种形式,一种是严格款式隔离,通过 shadow dom
来实现,另一种是实验性的款式隔离,就是 scoped css
,两种形式不可共存
严格款式隔离
在 qiankun
中的严格款式隔离,就是在这个 createElement
办法中做的,通过 shadow dom
来实现,shadow dom
是浏览器原生提供的一种能力,在过来的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放管制按钮的 <video>
元素为例,实际上,在它的 Shadow DOM 中,蕴含来一系列的按钮和其余控制器。Shadow DOM 规范容许你为你本人的元素(custom element)保护一组 Shadow DOM。具体内容可查看 shadow DOM
/** | |
* 做了两件事 | |
* 1、将 appContent 由字符串模版转换成 html dom 元素 | |
* 2、如果须要开启严格款式隔离,则将 appContent 的子元素即微利用的入口模版用 shadow dom 包裹起来,达到款式严格隔离的目标 | |
* @param appContent = `<div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>` | |
* @param strictStyleIsolation 是否开启严格款式隔离 | |
*/ | |
function createElement(appContent: string, strictStyleIsolation: boolean): HTMLElement { | |
// 创立一个 div 元素 | |
const containerElement = document.createElement('div'); | |
// 将字符串模版 appContent 设置为 div 的子与阿苏 | |
containerElement.innerHTML = appContent; | |
// appContent always wrapped with a singular div,appContent 由模版字符串变成了 DOM 元素 | |
const appElement = containerElement.firstChild as HTMLElement; | |
// 如果开启了严格的款式隔离,则将 appContent 的子元素(微利用的入口模版)用 shadow dom 包裹,以达到微利用之间款式严格隔离的目标 | |
if (strictStyleIsolation) {if (!supportShadowDOM) { | |
console.warn('[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!', | |
); | |
} else {const { innerHTML} = appElement; | |
appElement.innerHTML = ''; | |
let shadow: ShadowRoot; | |
if (appElement.attachShadow) {shadow = appElement.attachShadow({ mode: 'open'}); | |
} else { | |
// createShadowRoot was proposed in initial spec, which has then been deprecated | |
shadow = (appElement as any).createShadowRoot();} | |
shadow.innerHTML = innerHTML; | |
} | |
} | |
return appElement; | |
} |
实验性款式隔离
实验性款式的隔离形式其实就是 scoped css
,qiankun
会通过动静改写一个非凡的选择器束缚来限度 css
的失效范畴,利用的款式会依照如下模式改写:
// 假如利用名是 react16 | |
.app-main {font-size: 14px;} | |
div[data-qiankun-react16] .app-main {font-size: 14px;} |
process
/** | |
* 做了两件事:* 实例化 processor = new ScopedCss(),真正解决款式选择器的中央 | |
* 生成款式前缀 `div[data-qiankun]=${appName}` | |
* @param appWrapper = <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div> | |
* @param stylesheetElement = <style>xx</style> | |
* @param appName 微利用名称 | |
*/ | |
export const process = ( | |
appWrapper: HTMLElement, | |
stylesheetElement: HTMLStyleElement | HTMLLinkElement, | |
appName: string, | |
) => { | |
// lazy singleton pattern,单例模式 | |
if (!processor) {processor = new ScopedCSS(); | |
} | |
// 目前反对 style 标签 | |
if (stylesheetElement.tagName === 'LINK') {console.warn('Feature: sandbox.experimentalStyleIsolation is not support for link element yet.'); | |
} | |
// 微利用模版 | |
const mountDOM = appWrapper; | |
if (!mountDOM) {return;} | |
// div | |
const tag = (mountDOM.tagName || '').toLowerCase(); | |
if (tag && stylesheetElement.tagName === 'STYLE') {// 生成前缀 `div[data-qiankun]=${appName}` | |
const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`; | |
/** | |
* 理论解决款式的中央 | |
* 拿到款式节点中的所有款式规定,而后重写款式选择器 | |
* 含有根元素选择器的状况:用前缀替换掉选择器中的根元素选择器局部,* 一般选择器:将前缀插到第一个选择器的前面 | |
*/ | |
processor.process(stylesheetElement, prefix); | |
} | |
} | |
export const QiankunCSSRewriteAttr = 'data-qiankun'; |
ScopedCSS
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule | |
enum RuleType { | |
// type: rule will be rewrote | |
STYLE = 1, | |
MEDIA = 4, | |
SUPPORTS = 12, | |
// type: value will be kept | |
IMPORT = 3, | |
FONT_FACE = 5, | |
PAGE = 6, | |
KEYFRAMES = 7, | |
KEYFRAME = 8, | |
} | |
const arrayify = <T>(list: CSSRuleList | any[]) => {return [].slice.call(list, 0) as T[];}; | |
export class ScopedCSS {private static ModifiedTag = 'Symbol(style-modified-qiankun)'; | |
private sheet: StyleSheet; | |
private swapNode: HTMLStyleElement; | |
constructor() {const styleNode = document.createElement('style'); | |
document.body.appendChild(styleNode); | |
this.swapNode = styleNode; | |
this.sheet = styleNode.sheet!; | |
this.sheet.disabled = true; | |
} | |
/** | |
* 拿到款式节点中的所有款式规定,而后重写款式选择器 | |
* 含有根元素选择器的状况:用前缀替换掉选择器中的根元素选择器局部,* 一般选择器:将前缀插到第一个选择器的前面 | |
* | |
* 如果发现一个款式节点为空,则该节点的款式内容可能会被动静插入,qiankun 监控了该动静插入的款式,并做了同样的解决 | |
* | |
* @param styleNode 款式节点 | |
* @param prefix 前缀 `div[data-qiankun]=${appName}` | |
*/ | |
process(styleNode: HTMLStyleElement, prefix: string = '') { | |
// 款式节点不为空,即 <style>xx</style> | |
if (styleNode.textContent !== '') { | |
// 创立一个文本节点,内容为 style 节点内的款式内容 | |
const textNode = document.createTextNode(styleNode.textContent || ''); | |
// swapNode 是 ScopedCss 类实例化时创立的一个空 style 节点,将款式内容增加到这个节点下 | |
this.swapNode.appendChild(textNode); | |
/** | |
* {* cssRules: CSSRuleList {0: CSSStyleRule, 1: CSSStyleRule, 2: CSSStyleRule, 3: CSSStyleRule, length: 4} | |
* disabled: false | |
* href: null | |
* media: MediaList {length: 0, mediaText: ""} | |
* ownerNode: style | |
* ownerRule: null | |
* parentStyleSheet: null | |
* rules: CSSRuleList {0: CSSStyleRule, 1: CSSStyleRule, 2: CSSStyleRule, 3: CSSStyleRule, length: 4} | |
* title: null | |
* type: "text/css" | |
* } | |
*/ | |
const sheet = this.swapNode.sheet as any; // type is missing | |
/** | |
* 失去所有的款式规定,比方 | |
* [* {selectorText: "body", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "body { background: rgb(255, 255, 255); margin: 0px; }", …} | |
* {selectorText: "#oneGoogleBar", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "#oneGoogleBar { height: 56px;}", …} | |
* {selectorText: "#backgroundImage", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "#backgroundImage { border: none; height: 100%; poi…xed; top: 0px; visibility: hidden; width: 100%;}", …} | |
* {selectorText: "[show-background-image] #backgroundImage {xx}" | |
* ] | |
*/ | |
const rules = arrayify<CSSRule>(sheet?.cssRules ?? []); | |
/** | |
* 重写款式选择器 | |
* 含有根元素选择器的状况:用前缀替换掉选择器中的根元素选择器局部,* 一般选择器:将前缀插到第一个选择器的前面 | |
*/ | |
const css = this.rewrite(rules, prefix); | |
// 用重写后的款式替换原来的款式 | |
// eslint-disable-next-line no-param-reassign | |
styleNode.textContent = css; | |
// cleanup | |
this.swapNode.removeChild(textNode); | |
return; | |
} | |
/** | |
* | |
* 走到这里阐明款式节点为空 | |
*/ | |
// 创立并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用 | |
const mutator = new MutationObserver(mutations => {for (let i = 0; i < mutations.length; i += 1) {const mutation = mutations[i]; | |
// 示意该节点曾经被 qiankun 解决过,前面就不会再被反复解决 | |
if (ScopedCSS.ModifiedTag in styleNode) {return;} | |
// 如果是子节点列表发生变化 | |
if (mutation.type === 'childList') { | |
// 拿到 styleNode 下的所有款式规定,并重写其款式选择器,而后用重写后的款式替换原有款式 | |
const sheet = styleNode.sheet as any; | |
const rules = arrayify<CSSRule>(sheet?.cssRules ?? []); | |
const css = this.rewrite(rules, prefix); | |
// eslint-disable-next-line no-param-reassign | |
styleNode.textContent = css; | |
// 给 styleNode 增加一个 ScopedCss.ModifiedTag 属性,示意曾经被 qiankun 解决过,前面就不会再被解决了 | |
// eslint-disable-next-line no-param-reassign | |
(styleNode as any)[ScopedCSS.ModifiedTag] = true; | |
} | |
} | |
}); | |
// since observer will be deleted when node be removed | |
// we dont need create a cleanup function manually | |
// see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/disconnect | |
// 察看 styleNode 节点,当其子节点发生变化时调用 callback 即 实例化时传递的函数 | |
mutator.observe(styleNode, { childList: true}); | |
} | |
/** | |
* 重写款式选择器,都是在 ruleStyle 中解决的:* 含有根元素选择器的状况:用前缀替换掉选择器中的根元素选择器局部,* 一般选择器:将前缀插到第一个选择器的前面 | |
* | |
* @param rules 款式规定 | |
* @param prefix 前缀 `div[data-qiankun]=${appName}` | |
*/ | |
private rewrite(rules: CSSRule[], prefix: string = '') { | |
let css = ''; | |
rules.forEach(rule => { | |
// 几种类型的款式规定,所有类型查看 https://developer.mozilla.org/zh-CN/docs/Web/API/CSSRule#%E7%B1%BB%E5%9E%8B%E5%B8%B8%E9%87%8F | |
switch (rule.type) {// 最常见的 selector { prop: val} | |
case RuleType.STYLE: | |
/** | |
* 含有根元素选择器的状况:用前缀替换掉选择器中的根元素选择器局部,* 一般选择器:将前缀插到第一个选择器的前面 | |
*/ | |
css += this.ruleStyle(rule as CSSStyleRule, prefix); | |
break; | |
// 媒体 @media screen and (max-width: 300px) {prop: val} | |
case RuleType.MEDIA: | |
// 拿到其中的具体款式规定,而后调用 rewrite 通过 ruleStyle 去解决 | |
css += this.ruleMedia(rule as CSSMediaRule, prefix); | |
break; | |
// @supports (display: grid) {} | |
case RuleType.SUPPORTS: | |
// 拿到其中的具体款式规定,而后调用 rewrite 通过 ruleStyle 去解决 | |
css += this.ruleSupport(rule as CSSSupportsRule, prefix); | |
break; | |
// 其它,间接返回款式内容 | |
default: | |
css += `${rule.cssText}`; | |
break; | |
} | |
}); | |
return css; | |
} | |
/** | |
* 一般的根抉择器用前缀代替 | |
* 根组合选择器置空,疏忽非标准模式的兄弟选择器,比方 html + body {...} | |
* 针对一般选择器则是在第一个选择器前面插入前缀,比方 .xx 变成 .xxprefix | |
* | |
* 总结就是:* 含有根元素选择器的状况:用前缀替换掉选择器中的根元素选择器局部,* 一般选择器:将前缀插到第一个选择器的前面 | |
* | |
* handle case: | |
* .app-main {} | |
* html, body {} | |
* | |
* @param rule 比方:.app-main {} 或者 html, body {} | |
* @param prefix `div[data-qiankun]=${appName}` | |
*/ | |
// eslint-disable-next-line class-methods-use-this | |
private ruleStyle(rule: CSSStyleRule, prefix: string) { | |
// 根抉择,比方 html、body、:root | |
const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm; | |
// 根组合选择器,比方 html body {...}、html > body {...} | |
const rootCombinationRE = /(html[^\w{[]+)/gm; | |
// 选择器 | |
const selector = rule.selectorText.trim(); | |
// 款式文本 | |
let {cssText} = rule; | |
// 如果选择器为根选择器,则间接用前缀将根选择器替换掉 | |
// handle html {...} | |
// handle body {...} | |
// handle :root {...} | |
if (selector === 'html' || selector === 'body' || selector === ':root') {return cssText.replace(rootSelectorRE, prefix); | |
} | |
// 根组合选择器 | |
// handle html body {...} | |
// handle html > body {...} | |
if (rootCombinationRE.test(rule.selectorText)) { | |
// 兄弟选择器 html + body,非标准选择器,有效,转换时疏忽 | |
const siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm; | |
// since html + body is a non-standard rule for html | |
// transformer will ignore it | |
if (!siblingSelectorRE.test(rule.selectorText)) { | |
// 阐明时 html + body 这种非标准模式,则将根组合器置空 | |
cssText = cssText.replace(rootCombinationRE, ''); | |
} | |
} | |
// 其它个别选择器,比方 类选择器、id 选择器、元素选择器、组合选择器等 | |
// handle grouping selector, a,span,p,div {...} | |
cssText = cssText.replace(/^[\s\S]+{/, selectors => | |
// item 是匹配的字串,p 是第一个分组匹配的内容,s 是第二个分组匹配的内容 | |
selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {// handle div,body,span { ...} | |
if (rootSelectorRE.test(item)) { | |
// 阐明选择器中含有根元素选择器 | |
return item.replace(rootSelectorRE, m => {// do not discard valid previous character, such as body,html or *:not(:root) | |
const whitePrevChars = [',', '(']; | |
// 将其中的根元素替换为前缀 | |
if (m && whitePrevChars.includes(m[0])) {return `${m[0]}${prefix}`; | |
} | |
// replace root selector with prefix | |
return prefix; | |
}); | |
} | |
// selector1 selector2 =》selector1prefix selector2 | |
return `${p}${prefix} ${s.replace(/^ */, '')}`; | |
}), | |
); | |
return cssText; | |
} | |
// 拿到其中的具体款式规定,而后调用 rewrite 通过 ruleStyle 去解决 | |
// handle case: | |
// @media screen and (max-width: 300px) {} | |
private ruleMedia(rule: CSSMediaRule, prefix: string) {const css = this.rewrite(arrayify(rule.cssRules), prefix); | |
return `@media ${rule.conditionText} {${css}}`; | |
} | |
// 拿到其中的具体款式规定,而后调用 rewrite 通过 ruleStyle 去解决 | |
// handle case: | |
// @supports (display: grid) {} | |
private ruleSupport(rule: CSSSupportsRule, prefix: string) {const css = this.rewrite(arrayify(rule.cssRules), prefix); | |
return `@supports ${rule.conditionText} {${css}}`; | |
} | |
} |
结语
以上内容就是对 qiankun 框架的残缺解读了,置信你在浏览完这篇文章当前会有不错的播种,源码在 github
浏览 qiankun 时的感触就是 书读百变其义自现
,qiankun 框架有些中央实现还是比拟难了解的,置信大家浏览源码时也会有这个感触,那就多读几遍吧,当然也能够来评论区交换,独特学习,共同进步!!
链接
- 微前端专栏
- github
感激各位的:点赞 、 珍藏 和评论,咱们下期见。
当学习成为了习惯,常识也就变成了常识,扫码关注微信公众号,独特学习、提高。文章已收录到 github,欢送 Watch 和 Star。