Appearance
使用
qiankun有自动加载和手动加载。自动预先注册微应用,当微应用的activeRule和浏览器url匹配时(可以同时匹配多个),执行挂载逻辑。手动加载,按需的将某个子应用挂载到特定容器上。
qiankun的entry可以是html(会按照下文import-html-entry解析),也能一个是style、js和html 字符串地址组成的对象。
注意事项
- 子应用需要将publicPath动态的设置为全局变量 window.INJECTED_PUBLIC_PATH_BY_QIANKUN,webpack的动态配置需要在入口文件中配置,而且如果esm,还需要新建一个文件来配置
- 子应用挂载时,需要避免挂载到主应用的root元素上。使用props.container.querySelector('#root')
- 通过window.POWERED_BY_QIANKUN 来判断当前应用是否被当做了子应用
- 子应用需要配置跨域,因为主应用加载资源都是通过fetch函数进行加载(fetch 有跨域问题)
- entryjs需要放在html中的最后一个脚本,或者通过script entry属性标记出来
- 如果子应用是在懒挂载在路由页面,主应用的路由配置需要在子路由之前,因为主应用的路由也是监听的popState的变化来渲染对应的页面,qiankun也是监听的同样,按照先注册监听函数先执行。
- 只有手动挂载的子应用才有update声明周期
- 如果资源的请求需要额外的自定义属性,需要自定义fetch方法,比如静态资源请求需要cookie等情况
- 最好给入口文件的文件类型修改成text/plain,避免运营商搞操作
- 不要qiankun处理的js,添加ignore属性即可
import-html-entry
微服务实现的核心,通过解析入口文件获取子应用的html、css和js。同时对解析出的部分进行处理以方便加载
获取html并处理
通过fetch加载入口对应的html文件字符串(fetch 下来都是字符串)。然后对html字符串进行正则匹配处理,大致包括以下内容。
- 去掉所有注释
- 注释所有的js引用语句,注释所有的内联js代码。(当然是先保存了)
- 注释掉所有的css引用语句,但是保留了内联css语句
处理css引用
在上一步中也不是完全是注释所有的css引用语句,而是在同一个地方使用fetch语句拉取对应的外部css,处理后再通过style标签在原地方引用。
路由挂载子应用
如果部分子应用是挂载到部分路由页面的,有可能出现子应用挂载时找不到容器的情况,原因是这个容器还在主应用中懒加载。有两个解决方法,第一个是在主应用路由页面手动loadApp。需要保证加载子应用时,存在container容器
子应用卸载
在自动模式下,切换路由时,qiankun会自动调用子应用的unmount方法将子应用卸载,但是在手动模式下,loadMicroApp()会返回子应用实例,实例上有unmount方法。
还有个问题,子应用的副作用需要自行清理吗?比如settimeout setinteral等副作用。答案是否,当然也可以自行清理。qiankun在挂载子应用时通过沙箱的形式重写了全局变量,并且劫持了setInterval 等副作用函数。对于addeventlistener,也是重写了函数,额外添加了收集逻辑,在卸载时会一一取消绑定
新的问题是,子应用卸载后再次挂载时会默认加载之前的状态,子应用修改的全局变量会加载,子应用动态添加的style会恢复。但是样式的解析始终是根据style在文档的位置来决定,如果再次挂载的子应用style添加在第一次后面,可能会出现一些意外情况。
- 子应用卸载时,子应用的fakeWindow不会消失,再次挂载继续使用。
- 基于上面的理论,因为子应用通过umd打包,导出了一个挂载的window上面的全局变量。再次加载子应用时,重复的__webpack_require__.d 不会重复执行(参考cjs或则esm),也会导致动态创建的style不会再次挂载。
- 针对上面的情况,qiankun在自用调用documen.head.appendchild时会劫持该方法。把动态创建的style保存到起来,当子应用再次挂载时会添加到子应用之前对应的位置中,所以需要保证子应用的style是通过该方法添加的才能劫持。
子应用动态的style是怎样添加的,为什么没有添加到主应用上的head中
上文了解到,在html entry中style和css link能够被收集处理,但是webpack打包后的css基本都是动态加载,webpack本身是无法判断该style插入到子应用还是主应用。qiankun通过fakeWindow实现了对appenchild,insertBefore方法的劫持,在调用时判断是主应用还是子应用调用,从而插入到相应位置,同理对应的svg处理逻辑叶鸿昌
样式沙箱
样式沙箱包括上面的动态加载style,link和对样式的隔离。如果开启了严格沙箱模式,在静态的css字符串被fetch回来时,出对每一个样式添加添加唯一前缀,对于动态加载的样式,在拦截中添加唯一前缀
子应用资源加载路径问题
之前我们一直推崇是publicPath为相对路径,通过文档的base来查找资源(为了方便多级部署),在qiankun中,因为一个文档只有拥有一个base,子应用会使用主应用的base导致出错,所以子应用必须要使用绝对路径,这个路劲要么是在编译时配置,要么是在设置为动态地址。不过有一点css中对资源的引用是在编译时确定的,而且不管相对绝对都是根据主应用来确定的
如果说非要用动态地址,那么子应用的中css的资源必须是完整路径,或者将对应的资源编译成base 64,改行内应用。
js沙箱
qiankun又名sandbox + html entry + single-spa。核心就是沙箱模式,通过沙箱默认子应用可以随意的造,不用担心影响主应用或者其他子应用 qiankun目前有三种沙箱模式,快照沙箱(snapshot),高级沙箱(legacy)和代理沙箱(proxy)。
快照沙箱,当沙箱激活时给当前的window拍摄一个照片,把当前window的拷贝给快照。然后将改动的沙箱赋值给window,当沙箱取消时,将快照还原,保存沙箱激活期间所有的操作,带下次激活时恢复。实现起来比较简单,就是深拷贝window即可。缺点是window属性众多,激活和失活时都要做全量拷贝,同时是在windows变量本身做操作会污染window变量。
高级沙箱,也叫单例代理沙箱,快照沙箱的升级版本,通过this.fakeWindow代理Proxy记录属性的删除新增和更新。只记录变化的属性,在激活和失活时也只用操作部分属性,但是属性的还是会操作到window上。性能变高,但是还是没有解决单例问题,只能在一个微应用的场景使用
代理沙箱,在高级沙箱的基础上,升级了到多例模式,而且不会对window进行操作。所有操作都代理到了fakeWindow上。
应用间通信
qiankun自带应用通信方案,主应用会暴露出一个发布订阅的组件。子应用之间、主应用相互订阅发布。如果主应用声明了全局变量,子应用会默认在mount的props添加对应的方法。
需要注意的是,初始化全局state需要在注册子应用之后,调用start之前。
不过官方提供的应用间通信属实捡漏,基本只适用于demo,在开发中需要通过注册组件时提供props的形式注入。vue中可以使用ref和reactive来当做发布订阅,前提条件是主子应用同一个Vue(vue依赖是在另一个dep存放的,不在变量上)。
社区中有shared方案,大致逻辑是主应用和子应用同时维护一份store,子应用初始化时监听主应用的变化,并触发自己的action。同时子应用也有主应用的dispatch。也能触发主应用store。这个方案有如下问题未解决
- 主应用中对于怎么样按照模块划分,如何结合现有状态管理技术栈
- 子应用会无脑监听主应用状态变化,出现回调地狱,需要增加子应用的判断指定监听的逻辑
还有种方案是单例全局状态,将主应用的状态管理通过props传递到子应用。
应用间共享依赖
qiankun不推荐运行时共享,不过在生产中势必会出现共享代码的情况。比如公共的vue,react,react-dom等依赖。qiankun不推崇原因是应用应该被视为一个独立的个体,如果有运行时依赖,应用单独运行会有问题 我目前只推荐使用externals的形式共享,将externals的变量挂载到全局中实现。
js
// webpack.config.js
export default {
externals: {
'vue': 'vue' // 编译后代码是 export default = vue 相当于在全局空间查找vue
}
}还有种方案是通过props共享,这种方法的弊端是子应用没法获得补全等功能,同时子应用也独立运行时
js
import { component, utils, config } from './lib'
export default [{
name: 'app1',
entry: 'http://localhost:8081',
container: '#app-container',
activeRule: '#/app1',
props: {
component, // 共享主应用的组件
utils, // 共享主应用的方法
config // 主应用的配置等
}
}]应用之前还存在共享依赖,比如应用A,应用B都打包了某个依赖,如果应用A先加载了依赖,应用B就直接使用依赖,不需要再加载自身打包的依赖。 也是通过全局的变量共享实现,如果在全局空间找到有相同的依赖,则不加载。其实这个也能作为externals
路由规划指南
保持一致是最佳选择。主应用切记不要*管理路由404路由,通过添加全局路由钩子(beforeEach,判断当前跳转的路径是否是404), 子应用需要配置base前缀来保持路由。
子应用通过history.pushState来实现对主应用的跳转或则通过主应用提供的方法来实现。
动态添加子应用
除了手动的加载子应用外,qiankun还支持动态添加,重复执行registerMircoApp()会动态的添加, 相同name的忽略。
publicPath
qiankun中子应用的publicPath自动设置为entry的根路径,通过start的参数getPublicPath设置 INJECTED_PUBLIC_PATH_BY_QIANKUN 常见于子应用是二级部署的情况。
工程化探索实践
- 主应用通过全局的生命周期钩子,beforeLoad和afterLoad添加加载进度条。
- 请务必将配置集中化,比如子应用的base交给主应用管理。子应用不要写死。
参考文章
