微前端之qiankun上手实践指南
前言
本文主要记录使用qiankun + Vue2
搭建微前端的过程,主应用、子应用均采用Vue2,由于笔者工作项目中使用的路由模式都是hash
模式,所以文中的主应用和子应用也都采用hash
模式。
一、主应用
1、创建主应用
通过vue-cli
创建一个Vue2项目,创建主应用路由和其对应的页面。
// /src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login.vue')
},
{
path: '/',
redirect: '/home',
component: () => import('@/views/layout.vue'),
children: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/home.vue')
}
]
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router
接着创建主应用layout
布局,左侧菜单栏,右侧用于挂载微应用。
<template>
<div style="height: 100%;">
<el-container style="height: 100%;">
<el-aside style="height: 100%;">
<el-menu
router
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
style="height: 100%;"
:default-active="$route.path"
>
<el-menu-item index="/home">
<i class="el-icon-menu"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-menu-item index="/micro-app-goods/list">
<i class="el-icon-menu"></i>
<span slot="title">商品列表</span>
</el-menu-item>
<el-menu-item index="/micro-app-order/list">
<i class="el-icon-menu"></i>
<span slot="title">订单列表</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容 -->
<el-main>
<!-- 主应用路由 -->
<router-view />
<!-- 挂载子应用的节点 -->
<div id="micro-app"></div>
</el-main>
</el-container>
</div>
</template>
菜单栏的商品列表和订单列表分别为【商品子应用】和【订单子应用】的页面。
2、主应用接入qiankun
安装qiankun:
yarn add qiankun
接入子应用:
// layout.vue
mounted() {
const apps = [
{
name: 'micro-app-goods', // 微应用名称,确保唯一
entry: '//localhost:3002', // 微应用入口
container: '#micro-app', // 微应用挂载的节点
activeRule: '#/micro-app-goods' // 微应用激活规则,由于是用的hash模式,所以前面记得加#
},
{
name: 'micro-app-order',
entry: '//localhost:3003',
container: '#micro-app',
activeRule: '#/micro-app-order'
}
]
registerMicroApps(apps) // 注册
start() // 启动
}
为了确保qiankun
启动时能获取到子应用挂载节点,所以在mounted
进行上述操作。
此时点击菜单的商品列表或订单列表,页面还是空白的,因为是在主应用的layout
路由里加载子应用,此时主应用并没有/micro-app-goods/list
这个路由,所以会404。
所以还需要在主应用的路由文件里增加一个路由,通过通配符去匹配子应用的路径并将其指向layout
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login.vue')
},
{
path: '/',
redirect: '/home',
component: () => import('@/views/layout.vue'),
children: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/home.vue')
}
]
},
// 匹配子应用
{
path: '/micro-app-*', // 以`/micro-app-`开头
component: () => import('@/views/layout.vue')
}
]
再次点击跳转子应用的菜单可以发现qiankun
已经开始工作了,但此时我们还没有创建对应的子应用,所以控制台会报错。
二、子应用
1、创建子应用
创建一个商品子应用micro-app-goods
并创建路由。
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/micro-app-goods/list',
name: 'GoodsList',
component: () => import('@/views/goods/list.vue')
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router
<!-- src/views/goods/list.vue -->
<template>
<div>
<p>商品列表页</p>
</div>
</template>
2、增加public-path.js
// src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line camelcase, no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
3、改造main.js
import './public-path' // 引入前面增加的publi-path文件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'
import store from './store/index'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
Vue.use(ElementUI)
let instance = null
function render(props = {}) {
const { container } = props
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() {}
export async function mount(props) {
render(props)
}
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
}
子应用不需要安装qiankun
,只需在main.js
暴露出qiankun
所需要的生命周期函数即可。
4、改造vue.config.js
- 子应用需要打包成
umd
格式。 - 子应用需要支持跨域。
const { defineConfig } = require('@vue/cli-service')
const { name } = require('./package')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: './',
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`
// chunkLoadingGlobal: `webpackJsonp_${name}` // webpack5使用chunkLoadingGlobal
}
},
devServer: {
port: 3002,
headers: {
// 支持跨域
'Access-Control-Allow-Origin': '*'
}
}
})
到这里子应用的改造便完成了,重新启动子应用后便可以在主应用里加载子应用了。
三、应用间跳转
因为主应用、子应用的路由模式都是hash
模式,所以比较省事,平时怎么跳的就怎么跳。
// 【商品子应用】跳转【订单子应用】
this.$router.push({
path: '/micro-app-order/list'
})
四、样式隔离
qiankun
提供了两种样式隔离方式:strictStyleIsolation
和experimentalStyleIsolation
。
start({
sandbox: {
strictStyleIsolation: true,
// or
// experimentalStyleIsolation: true
}
})
使用strictStyleIsolation
开启严格的隔离模式后,qiankun
会为每个微应用包裹上一层shadow dom
来达到隔离的效果。
experimentalStyleIsolation
则是为微应用的样式增加【应用名前缀】。
qiankun3.0版本计划移除strictStyleIsolation,推荐使用experimentalStyleIsolation。
五、应用通信
1、props
主应用通过props传递:
const token = 'xxx'
const apps = [
{
name: 'micro-app-goods',
entry: '//localhost:3002',
container: '#micro-app',
activeRule: '#/micro-app-goods',
props: {
token
}
},
{
name: 'micro-app-order',
entry: '//localhost:3003',
container: '#micro-app',
activeRule: '#/micro-app-order',
props: {
token
}
}
]
registerMicroApps(apps)
start({
sandbox: {
experimentalStyleIsolation: true
}
})
子应用接收:
// src/main.js
function render(props = {}) {
const { container, token } = props
console.log('token', token)
instance = new Vue({
router,
store,
render: (h) => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
当然也可以传递一些方法:
// 主应用
const actions = {
getName: () => {
return '张三'
}
}
const apps = [
{
name: 'micro-app-goods',
entry: '//localhost:3002',
container: '#micro-app',
activeRule: '#/micro-app-goods',
props: {
actions
}
}
]
// 微应用
function render(props = {}) {
const { container, actions } = props
Vue.prototype.$actions = actions // 挂到全局
instance = new Vue({
router,
store,
render: (h) => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
// 在其他地方调用
mounted() {
const name = this.$actions.getName()
}
2、initGlobalState
主应用:
// src/qiankun/actions.js
import { initGlobalState } from 'qiankun'
const actions = initGlobalState({})
export default actions
主应用使用:
import actions from '@/qiankun/actions'
// 改变状态
actions.setGlobalState({
name: '李四'
})
// 监听状态改变
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('主应用onGlobalStateChange', state, prev)
})
微应用的props参数里默认就有setGlobalState
和onGlobalStateChange
。
微应用可以通过onGlobalStateChange
监听状态改变。
// 微应用
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log(state, prev)
}, true) // onGlobalStateChange 第二个参数为 true,表示立即执行一次观察者函数
render(props)
}
initGlobalState使用起来挺鸡肋的,qiankun3.0版本也计划移除掉移除掉它了,不推荐使用。
除此之外,也可以使用状态管理(如vuex)或发布/订阅模式(EventBus)等方式,通过主应用下发,实现应用间通信。
六、路由缓存keep-alive
使用registerMicroApps
时,微应用在切出后会卸载掉,切入后再重新加载,所以在这种方式下,即使我们的Vue应用使用了keep-alive
也无法实现缓存。
1、方案一:loadMicroApp + 多应用节点
qiankun
提供了loadMicroApp
API用于手动加载微应用,使用loadMicroApp
,切换微应用不会被卸载,除非自己手动进行卸载。
同时创建多个微应用容器节点,使每个微应用都单独挂载在一个DOM节点上,用v-show
控制对应的微应用容器显示或隐藏,隐藏只是display:none
而没有真正销毁DOM,从而达到keep-alive
的作用。
2、方案二:手动缓存实例
还是通过registerMicroApps
注册微应用,只挂载一个微应用节点,使用类似于keep-alive
的实现方式,微应用自己缓存本身实例的vnode,下次重新进入时微应用直接使用上一次缓存的vnode直接渲染为真实DOM。
3、方案一实现过程
- 方案一的优点是:实现起来较为简单,应用间切换更流畅一些。缺点是如果应用过多,DOM节点也更多,内存负担会更大。
- 方案二的优点是:DOM节点少,内存也会降低些。缺点是需要自己实现,实现方式较为复杂,而且多了虚拟DOM转换真实DOM的过程,渲染时间更多一些。
笔者的项目中采用的是方案一,对方案二感兴趣的同学可自行查阅其他文章了解。下面是改造过程:
- 定义多个微应用节点,使用
v-show
控制显示隐藏。
<!-- <div id="micro-app"></div> -->
<div id="micro-app-goods" v-show="$route.path.startsWith('/micro-app-goods')"></div>
<div id="micro-app-order" v-show="$route.path.startsWith('/micro-app-order')"></div>
- loadMicroApp
mounted() {
const apps = [
{
name: 'micro-app-goods',
entry: '//localhost:3002',
// container: '#micro-app',
container: '#micro-app-goods', // 修改container
activeRule: '#/micro-app-goods'
},
{
name: 'micro-app-order',
entry: '//localhost:3003',
// container: '#micro-app',
container: '#micro-app-order', // 修改container
activeRule: '#/micro-app-order'
}
]
this.appInstances = apps.map(app => {
const appInstance = loadMicroApp(app, {
sandbox: {
experimentalStyleIsolation: true
}
})
return appInstance
})
// 销毁时调用loadMicroApp返回值里的unmount进行卸载
this.$once('hook:beforeDestroy', () => {
this.appInstances.forEach((app) => {
app.unmount()
})
})
}
这里只是做一个示例,实际项目中也可以通过路由判断去加载对应子应用,而不是一次性加载全部子应用。
loadMicroApp
返回一个对象,可以通过它里面的方法来进行卸载操作,例如在多标签页的后台管理系统中,关闭后如果不存在子应用的页签了可以手动进行卸载。
- 子应用里增加
keep-alive
<template>
<div id="app">
<keep-alive>
<router-view />
</keep-alive>
</div>
</template>
关于keep-alive改造就完成了,接下来在商品列表页和订单列表页的created
输出内容。
created() {
console.log('商品列表===')
}
可以看到,重新切回商品列表和订单列表,控制台不会触发created
,keep-alive
缓存已经生效。
最后
感谢您的阅读,如果本文对你有什么帮助的话,别忘了动动手指点个赞❤❤❤!
转载自:https://juejin.cn/post/7269740596335591458