1.发现问题
在生产大型前端项目时候,随着需求、模块的不断增加。项目的打包维护效率越来越低。
项目中部分稳定的模块,在项目迭代中不希望打包这部分模块。
不同团队间开发同一个应用技术栈不同。
开发过程中,因为一些需求要升级技术栈,但是因为项目太大,升级相对复杂,容易出问题。
-
现行iframe解决方案弊端:
-
刷新问题
iframe
页面没有自己的历史记录,使用的是基座(父页面)的浏览历史。 所以,当iframe
页在内部进行跳转时,浏览器地址栏无变化,基座中加载的src
资源也无变化,当浏览器刷新时,无法停留在iframe
内部跳转后的页面上,需要用户重新走一遍操作,用户产生痛点。 -
遮罩问题
UI 不同步,DOM 结构不共享,iframe 页产生的带遮罩的弹窗,只能遮罩 iframe 区域。
-
通信问题
全局上下文完全隔离,内存变量不共享。与基座非同源下,
iframe
无法直接获取基座url
的参数。 -
加载问题
影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。
类似问题还有很多,从业务价值上看,损失了用户体验;从开发角度,有必要通过技术来提升业务体验的同时,提高产品后期更新迭代维护成本。
-
2.关于微前端qiankun
微前端:将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。这些子应用可以独立打包,独立部署,可以分别使用不同技术栈如Vue、React。当路径切换时加载不同的子应用。
Single-SPA:前端微服务化的 JavaScript 前端解决方案,实现了路由劫持和应用加载(即根据不同的路由加载不同的应用)。css和js需要进行隔离。否则容易造成全局污染。
qiankun :基于Single-SPA,,解决了Single-SPA需要自行处理Css样式隔离,Js执行隔离问题。提供多个API,接入简单。基座不受限子应用的技术栈,子应用独立部署维护,接入时基座同步更新;又可独立运行
3. qiankun API 说明
registerMicroApps(apps, lifeCycles?)
-
参数
- apps -
Array<RegistrableApp>
- 必选,微应用的一些注册信息 - lifeCycles -
LifeCycles
- 可选,全局的微应用生命周期钩子
- apps -
-
类型
-
RegistrableApp
name -
string
- 必选,微应用的名称,微应用之间必须确保唯一。entry -
string | html
- 必选,微应用的入口。- 配置为字符串时,表示微应用的访问地址,例如
https://qiankun.umijs.org/guide/
。 - 配置为对象时,
html
的值是微应用的 html 内容字符串如 '<span>子应用<span>',而不是微应用的访问地址。微应用的publicPath
将会被设置为/
。
container -
string | HTMLElement
- 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root'
或container: document.querySelector('#root')
。activeRule -
string |function (location)
-必选,浏览器 url 发生变化会调用 activeRule 里的规则,当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活
为函数时,函数会传入当前location作为参数,包含当前的请求路径等信息,根据函数返回值判断是否激活,如location.pathname.startsWith('/app1')。 - 配置为字符串时,表示微应用的访问地址,例如
-
LifeCycles
- beforeLoad -
Lifecycle | Array<Lifecycle>
- 可选 - beforeMount -
Lifecycle | Array<Lifecycle>
- 可选 - afterMount -
Lifecycle | Array<Lifecycle>
- 可选 - beforeUnmount -
Lifecycle | Array<Lifecycle>
- 可选 - afterUnmount -
Lifecycle | Array<Lifecycle>
- 可选
- beforeLoad -
-
start
参数
- opts -
Options
可选
类型
-
Options
-
prefetch -
boolean | 'all' | string[] | function
- 可选,是否开启预加载,默认为true
。配置为
true
则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源配置为
'all'
则主应用start
后即开始预加载所有微应用静态资源配置为
string[]
则会在第一个微应用 mounted 后开始加载数组内的微应用资源配置为
function
则可完全自定义应用的资源加载时机 (首屏应用及次屏应用) -
sandbox -
boolean
|{ strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean }
- 可选,是否开启沙箱,默认为 `true。默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。当配置为
{ strictStyleIsolation: true }
时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。当 配置为
{ strictStyleIsolation: true }
时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围 singular -
boolean | ((app: RegistrableApp<any>) => Promise<boolean>);
- 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为true
。
-
setDefaultMountApp(appLink)
-
参数
- appLink -
string
- 必选
- appLink -
-
用法
设置主应用启动后默认进入的微应用。
-
示例
import { setDefaultMountApp } from 'qiankun'; setDefaultMountApp('/vueApp');
4.qiankun案例实现
首先我们创建两个应用;一个子应用,一个父应用;(我们需要父应用加载子应用)
1.主应用安装qiankun
npm i qiankun -S
2.主应用页面中通过router路由把子页面挂载到 id="vue" 的标签上
<div>
<el-menu router mode="horizontal" >
<!--基座中可以放自己的路由-->
<el-menu-item index="/">基座</el-menu-item>
<!--引用其他子应用-->
<el-menu-item index="/vue">vue应用01</el-menu-item>
<el-menu-item index="/react">vue应用02</el-menu-item>
</el-menu>
<div><router-view></router-view></div>
<div id="vue"></div>
<div id="react"></div>
</div>
3.主应用入口js文件中注册子应用、配置路由指向
import { registerMicroApps, start } from 'qiankun'
import actions from './actions'
const apps=[
{
name: 'vueApp', // 应用的名字
entry: 'http://localhost:10002/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
container: '#vue', // 容器名
activeRule: '/vue', // 触发的路径
props: { //传递参数数据,
getGlobalState:actions.getGlobalState, //下发获取初始全局数据方法
username:"吴亦凡"
},
}
{
name: 'reactApp',
entry: '//localhost:10003',
container: '#react',
activeRule: '/react',
}
]
registerMicroApps(apps); //注册子应用
start({
prefetch: false // 可选,关闭预加载
singular: true, // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
});
4.子应用配置(不需要额外安装任何其他依赖即可接入 qiankun 主应用)
在入口js中导出 bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
import actions from './actions'
let instance = null
function render(props) {
instance = new Vue({
router,
render: h => h(App)
}).$mount('#app') // 这里是挂载到自己的html中 基座会拿到这个挂载后的html 将其插入进去
}
if (window.__POWERED_BY_QIANKUN__) { // (判断是自启动还是父应用调用)动态添加publicPath,主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
} else render() // 默认独立运行
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap(props) {
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
actions.setActions(props) //子项目的入口文件中设置子应用的全局state
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
instance.$destroy() //页面卸载
}
在webpack打包增加如下配置使主应用能正确识别微应用暴露出来的一些信息
module.exports = {
devServer: {
port: 10002,
headers: {
'Access-Control-Allow-Origin': '*' //子应用开启跨域
}
},
configureWebpack: {
output: {
library: 'vueApp', //库名,与主应用注册的微应用name保持一致
libraryTarget: 'umd' //UMD,一种javascript打包模式,让你打包好的代码模块能被主应用访问
}
}
}
运行展示
上图中 在子应用在进入时调用mount方法,同时通过props将数据传给子应用。
由上图可以看出子应用1被挂载主应用中id=“vue”的标签中
5.常见问题
1.样式隔离问题
qiankun 将会自动隔离子应用之间的样式(开启沙箱的情况下),沙箱模式下 qiankun 会为每个子应用的容器包裹上一个 shadow dom 节点,从而确保子应用的样式不会对全局造成影响。
主应用子应用之间样式隔离可以给主应用所有样式添加一个前缀。
start({
sandbox :{strictStyleIsolation: true} //沙箱开启 默认为true
})
2.Js隔离问题
qiankun框架通过沙箱(可以理解为一个黑盒,用于隔离当前执行的环境作用域和外部的其他作用域)实现js隔离(无需手动配置)
legacySandBox //基于 Proxy API 来实现
proxySandBox //基于 Proxy API 来实现
snapshotSandBox。 //不支持 Proxy API 的低版本浏览器中,会降级为 snapshotSandBox
legacySandBox
本质上还是操作 window 对象,存在三个应用池,分别用于子应用卸载时还原主应用的状态和子应用加载时还原子应用的状态。
通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离。
proxySandBox
用于多实例场景,不会直接操作 window 对象。并且为了避免子应用操作或者修改主应用上诸如 window、document、location 这些重要的属性,会遍历这些属性到子应用 window 副本(fakeWindow)上。所以在激活和卸载的时候也不需要操作状态池更新 / 还原主子应用的状态了。相比较看来,proxySandBox 是现阶段 qiankun 中最完备的沙箱模式,完全隔离了主子应用的状态,不会像 legacySandBox 模式下在运行时期间仍然会污染 window。
snapshotSandBox
在不支持 Proxy 的场景下会降级为 snapshotSandBox。
原理即子应用激活 / 卸载时分别去通过快照的形式记录/还原状态来实现沙箱的。
3.路由跳转问题
子项目跳转到另一个子项目不能直接使用<router-link>或者router.push/router.replace,因为子项目的路由跳转只会机遇子项目的base。<a> 链接可以跳转但是会刷新。
解决方案:主项目的路由实例对象挂载到全局状态传过去,使用父项目的这个 router
跳转
this.initialState.router.push("/"); //父应用
// this.initialState.router.push('/vue/about') //跳转子应用
4.通信问题
- 主应用与微应用的通信
- 微应用之间的通信
通信方式:
1.qiankun
官方提供的通信方式 - Actions
通信(详情见全局状态通信)。
优点:
使用简单;
官方支持性高;
-
适合通信较少的业务场景;
缺点:
子应用独立运行时,需要额外配置无
Actions
时的逻辑;子应用需要先了解状态池的细节,再进行通信;
由于状态池无法跟踪,通信场景较多时,容易出现状态混乱、维护困难等问题;
2.使用Vuex进行状态管理
优点:
- 子应用无法随意污染主应用的状态池,只能通过主应用暴露的
shared
实例的特定方法操作状态池,从而避免状态池污染产生的问题。 - 子应用将具备独立运行的能力
具体实现
1.微应用原本就没有使用Vuex
进行状态管理
主应用向微应用传递store
实例
props:{store}, //共享主应用的store实例
2.微应用本身就有自己store
实例
此时要考虑父子应用store分离
需要在入口文件中添加如下一行代码:
Vue.prototype.microStore = microStore
此时在子应用的各个页面都能够通过this.microStore
访问自身的store
。(this.microStore.state)
子应用之间传值:子应用的Vue
实例属于同级实例,不能做到响应式,需要通过Vue现有的API方法Vue.observable(store)
将共享的store实例进行响应式设置
5.主项目路由模式如何选择
由于 `qiankun` 是通过 `location.pathname` 值来判断当前应该加载哪个子项目的,所以需要给每个子项目注入不同的路由 `path`,而 `hash` 模式子项目路由跳转不改变 `path`,所以无影响,`history` 模式子项目路由设置 `base` 属性即可。
如果主项目使用 hash
模式,那么得用 location.hash
值来判断当前应该加载哪个子项目,并且子项目都得是 hash
模式,还需要给子项目所有的路由都添加一个前缀,子项目的路由跳转如果之前使用的是 path
也需要修改,用 name
跳转则不用。
如果主项目是 hash
模式,子项目为 history
模式,那么跳转到子项目之后,无法跳转到另一个 history
模式的子项目,也无法回到主项目的页面。
6.微应用打包之后 css 中的字体文件和图片加载 404
原因是 qiankun
将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。
而 css
文件一旦打包完成,就无法通过动态修改 publicPath
来修正其中的字体文件和背景图片的路径。
解决方案:
1.所有图片等静态资源上传至 `cdn`,`css` 中直接引用 `cdn` 地址
2.借助 `webpack` 的 `url-loader` 将字体文件和图片打包成 `base64`(适用于字体文件和图片体积小的项目)
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,
use: [
{
loader: 'url-loader',
options: {},
},
],
},
],
},
};
3.借助 webpack 的 file-loader ,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目)
const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`;
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]',
publicPath,
},
},
],
},
],
},
}
7.qiankun 是否兼容 ie
兼容.但是 IE 环境下(不支持 Proxy 的浏览器)只能使用单实例模式,qiankun 会自动将 singular
配置为 true
。
8.全局状态管理
qiankun通过initGlobalState, onGlobalStateChange, setGlobalState实现主应用的全局状态管理,然后默认会通过props将通信方法传递给子应用。
案例实现:
主应用:
import { initGlobalState } from 'qiankun'
export const initialState = {
globalLocation: {
id: 1234,
station: '北京'
}
}
const actions = initGlobalState(initialState) //初始化全局数据
// 自定义一个获取state的方法下发到子应用(通过props)
actions.getGlobalState = (key) => {
// 有key,表示取globalState下的某个子级对象
// 无key,表示取全部
return key ? initialState[key] : initialState
}
actions.onGlobalStateChange((newState, prev) => { //监听全局状态
for (let key in newState) {
initialState[key] = newState[key]
}
});
export default actions
子应用:
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: function () { },
setGlobalState: function () { },
getGlobalState: function () { },
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions
}
//拿到入口文件中设置子应用的全局方法
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args)
}
setGlobalState(...args) {
return this.actions.setGlobalState(...args)
}
getGlobalState(...args) {
return this.actions.getGlobalState(...args)
}
}
const actions = new Actions()
export default actions
子应用在mount函数调用时设置子应用全局状态
import actions from './actions'
export async function mount(props) {
actions.setActions(props) //子项目的入口文件中设置子应用的全局state
render(props)
}
此时在 主应用/子应用 即可拿到调用setGlobalState更新数据通信