qiankun是基于 single-spa 做的二次封装,主要解决了single-spa 的一些痛点和不足。
single-spa存在的问题?
- 1、对微应用侵入性太强
微应用的改造步骤:- 微应用路由改造,添加一个特定的前缀
- 微应用入口改造,挂载点变更和生命周期函数导出
- 打包工具配置更改
single-spa 采用JS Entry 的方式接入微应用。也就是说single-spa 接入微应用需要将微应用整个打包成一个JS文件,发布到静态资源服务器,然后再主应用中配置该JS文件的地址告诉 single-spa 去这个地址加载微应用。问题出现了,如按需加载、首屏资源加载优化、css独立打包等优化没有了。
2、样式隔离
single-spa 没有做。怎么做到主应用和微应用之间的样式,微应用和微应用的样式互不影响?这个只能通过约定命名规范来实现,比如应用样式以自己的应用名称开头。3、JS隔离
single-spa 没有做。JS全局对象污染,A应用在window上加一个自己的属性window.A,微应用B 也能访问到。4、资源预加载
single-spa 没有做。例如怎么实现在第一个微应用加载完后,后台悄悄加载其他微应用。5、应用间通信
single-spa 没有做。它只在注册微应用时给微应用注入一些状态信息,后续就不管了,没有任何通信的手段。
qiankun 如何解决以上问题
1、HTML Entry
qiankun 通过HTML Entry 的方式来解决JS Entry带来的问题2、样式隔离
采用shadow dom 包裹没一个微应用,从而确保微应用的样式互不干扰
采用css scoped 方式(实验性)动态改写 css 选择器来实现3、运行时沙箱
qiankun的运行时沙箱分为 JS 沙箱和样式沙箱4、资源预加载
qiankun 实现预加载的思路有两种,一种是当主应用执行 start 方法启动 qiankun 以后立即去预加载微应用的静态资源,另一种是在第一个微应用挂载以后预加载其它微应用的静态资源,这个是利用 single-spa 提供的 single-spa:first-mount 事件来实现的5、应用间通信
qiankun 通过发布订阅模式来实现应用间通信
示例项目
yarn examples:install
yarn examples:start
qiankun 提供了6种实例,vue、vue3、react15、react16、angular9、purehtml。
主应用在 examples/main 目录下,提供了两种实现方式,基于路由配置的 registerMicroApps 和 手动加载微应用的loadMicroApp。通过 webpak.config.js 的 entry 可以知道有两个入口文件 multiple.js 和 index.js。
- 1、基于路由配置
在 examples/main/index.js 中,将微应用关联到一些 url 规则,实现当浏览器 url 发生变化时,自动加载相应的微应用。主应用可以使用react进行运行,也可以使用vue进行运行。
registerMicroApps(
[
{
name: 'vue',
entry: '//localhost:7101',
container: '#subapp-viewport',
loader,
activeRule: '/vue',
},
],
{
beforeLoad: [
app => {
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);
},
],
},
);
- 2、手动加载微应用
在 examples/main/multiple.js 中有loadMicroApp实现的例子
function mount() {
app = loadMicroApp(
{ name: 'react15', entry: '//localhost:7102', container: '#react15' },
{ sandbox: { experimentalStyleIsolation: true } },
);
}
vue微应用引入,需要修改 vue.config.js 和 mian.js 、public-path.js
{
...
// 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}`,
}
...
}
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
运行时沙箱
运行时沙箱包括 JS 沙箱 和 样式沙箱
JS 沙箱
JS 沙箱是通过 proxy 代理 window 对象,记录window对象上属性的增删改查
- 单例模式
直接代理了原生 window 对象,记录原生 window 对象的增删改查,当 window 对象激活时恢复 window 对象到上次即将失活时的状态,失活时恢复 window 对象到初始初始状态 - 多例模式
代理了一个全新的对象,这个对象是复制的 window 对象的一部分不可配置属性,所有的更改都是基于这个 fakeWindow 对象,从而保证多个实例之间属性互不影响
样式沙箱
样式沙箱实际做的事情其实很简单,就是将动态添加的 script、link、style 这三个元素插入到对的位置,属于主应用的插入主应用,属于微应用的插入到对应的微应用中,方便微应用卸载的时候一起删除,当然样式沙箱还额外做了两件事:
(1)在卸载之前为动态添加样式做缓存,在微应用重新挂载时再插入到微应用内
(2)将 proxy 对象传递给 execScripts 函数,将其设置为微应用的执行上下文
-
样式隔离
qiankun 的样式隔离有两种方式,一种是严格样式隔离,通过 shadow dom 来实现,另一种是实验性的样式隔离,就是 scoped css,两种方式不可共存。在 qiankun 中的严格样式隔离,就是在这个 createElement 方法中做的,通过 shadow dom 来实现, shadow dom 是浏览器原生提供的一种能力,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的 <video> 元素为例,实际上,在它的 Shadow DOM 中,包含来一系列的按钮和其他控制器
实验性样式隔离
实验性样式的隔离方式其实就是 scoped css,qiankun 会通过动态改写一个特殊的选择器约束来限制 css 的生效范围
HTML Entry
HTML Entry 是由 import-html-entry 库实现的,通过 http 请求加载指定地址的首屏内容即 html 页面,然后解析这个 html 模版得到 template, scripts , entry, styles。
{
template: 经过处理的脚本,link、script 标签都被注释掉了,
scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块],
styles: [样式的http地址],
entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
}
然后远程加载 styles 中的样式内容,将 template 模版中注释掉的 link 标签替换为相应的 style 元素。然后向外暴露一个 Promise 对象。
{
// template 是 link 替换为 style 后的 template
template: embedHTML,
// 静态资源地址
assetPublicPath,
// 获取外部脚本,最终得到所有脚本的代码内容
getExternalScripts: () => getExternalScripts(scripts, fetch),
// 获取外部样式文件的内容
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
// 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
execScripts: (proxy, strictGlobal) => {
if (!scripts.length) {
return Promise.resolve();
}
return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
}
}
HTML Entry 最终会返回一个 Promise 对象,qiankun 就用了这个对象中的 template、assetPublicPath 和 execScripts 三项,将 template 通过 DOM 操作添加到主应用中,执行 execScripts 方法得到微应用导出的生命周期方法,并且还顺便解决了 JS 全局污染的问题,因为执行 execScripts 方法的时候可以通过 proxy 参数指定 JS 的执行上下文。
内容来源
微前端框架 之 qiankun 从入门到源码分析
qiankun 2.x 运行时沙箱 源码分析
HTML Entry 源码分析