前言
我们公司是做hrsaas的,前几个月和别的公司有合作,他们要求我们使用 qiankun 的方案作为子应用接入他们的系统。我:啥是qiankun啊? 微前端是啥,没接触过啊。没辙看看文档,硬着头皮接吧。跟着文档接入难度不大,开个页面一看惨不忍睹啊,发生了下面一系列问题,记录一下,省的忘了。
问题一:子应用的字体静态资源404问题
独立运行好好的,接入qiankun之后字体图标/图片都加载不出来了。就像这样:
导致这个问题的原因大概率是因为作为子应用运行时请求的 url 发生了变化,打开控制台可以发现路径是省略域名的,这就是导致错误的原因。
独立运行的项目的ip是: http:// localhost:7777
省略域名请求的路径是 :
http:// localhost:7777/static/img/src/assets/images/loginBackground.png,可以拿到资源。
而主应用运行地址是 http:// localhost:8080 ,请求的是
http:// localhost:8080/static/img/src/assets/images/loginBackground.png,自然就拿不到资源了。
所以我们只需要把路径修改正确即可,方法有很多种
- 我们可以在打包配置里面进行配置。例如 webpack 的 publicPath
设置完之后可以看到路径已经变成了一个完整的url,资源可以正常加载。
- 可以通过设置limit的大小把资源作为base64引入,缺点就是会增大打包后的体积。
- 直接把资源的路径写死,例如引入阿里云播放器插件时,通过网络远程下载 css 是相对路径。
在主应用时会造成播放图标加载失败,我就直接把 css 复制到本地一份把路径写死解决了。
问题二:You need to export lifecycle functions in sso entry
这个错误就是说没有找到子应用导出的生命周期,如果你确定已经在入口文件声明了并且配置都正确。那就重点检查一下打包后的html文件,检查的生命周期所在的 js 是不是最后一个加载的脚本。如果不是,需要移动顺序将其变成最后一个加载的 js,或者在 html 中将入口 js 手动标记为 entry。
我是因为分包配置,导致生命周期文件不是位于最后导致的报错
问题三:子应用如何跳转到主应用/其他子应用
- 通过主应用的$router跳转
主应用内跳转其他子应用直接带上子应用的路由前缀跳转即可,如果我们可以把主应用的路由实例传递给子应用,子应用就可以采用相同的方式进行路由跳转了。
当然也可以封装一个路由方法来限制跳转行为,再传递给子应用。
主应用将router实例通过props传递给子应用,子应用在render中接收。
// 主应用的main.js
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import router from './routers'
import './index.css'
Vue.use(VueRouter)
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'training-front',
entry: '//localhost:8081/training-front',
container: '#container',
activeRule: '/training-front',
props: {}
},
{
name: 'sso',
entry: '//localhost:7777/sso',
container: '#container',
activeRule: '/sso',
props: {}
},
{
name: 'reactApp',
entry: '//localhost:5173',
container: '#container',
activeRule: '/reactApp',
props: {}
}
],{
// qiankun 生命周期钩子 - 加载前
beforeLoad: (app) => {
console.log('加载子应用前,加载进度条=',app, app.name)
app.props.mainRouter = router
return Promise.resolve()
},
});
// 主应用跳转子应用 this.$router.push({ path: '/sso/home' }); sso是路由base,home为具体路径。
// 如果sso项目独立运行内部跳转 this.$router.push({ path: '/home' })
子应用在render中接收props
function render(props={}) {
// 主应用传的的props 可以在这里接收
const { container,mainRouter} = props;
// 这个是项目自己封装的 等同于new Vue()
createDesktopApp(
{
basePath: router.base,
mixins:[commons],
menus: [],
store,
routes: [],
modules: router.options.routes,
routerModule: {
router: Router,
beforeHooks: [beforeRoute, ...router.beforeHooks]
}
},
container ? container.querySelector('#app') : '#app'
);
// 重点是这里,mainRouter即为主应用的路由实例,全局存储之后就可以用于跳转了
Vue.prototype.$parentRouter = mainRouter
}
// 在业务中
// <button @click="$parentRouter.push('/sso/home')">子应用中跳转</button>
pushState跳转
在子应用跳转主应用或其他子应用也可以使用 pushState 方法,pushState(state, unused, url)
div onClick={()=>window.history.pushState(null,'','/sso/home')}>通过pushState跳转到sso</div>
- window.location.href 直接跳转
这种跳转是最直接的,刷新页面跳转不容易出问题,但是体验相对不好。
window.location.href = '主应用的域名' + '/sso/home' //
问题四:主应用全局样式污染
主应用和子应用都是用element的组件库,主应用全局对element的样式进行了定制化样式覆盖。然后发现主应用的全局样式影响了子应用。
可行方案:使用postcss 插件替换css前缀
需要下载 postcss-change-css-prefix借助该插件把主应用的公共类名替换掉。
// postcss.config.js
const addCssPrefix = require('postcss-change-css-prefix')
module.exports = {
plugins: [
addCssPrefix({
prefix: 'el-',
replace: 'gp-',
}),
],
}
使用前:
使用后的效果:
qiankun提供了两种样式隔离方案,但是都不理想。
start({
// sandbox : { strictStyleIsolation: true } // 严格隔离
// sandbox : { experimentalStyleIsolation: true } // 试验性隔离
});
strictStyleIsolation 严格隔离,开启后子应用会被放入Shadow DOM 从而达到样式隔离的目的。
它的隔离效果是非常好的,问题在于它会使子应用挂载到主应用 body 元素上的弹窗(element/antd的组件)找不到挂载对象。
放弃该方案
experimentalStyleIsolation试验性隔离
开启该属性后,它会为子应用的每个样式都加上属性选择器,类似 Vue 的 scoped 。
使用发现有如下问题:
个人认为它只能达到子应用的样式不影响主应用,不能隔离主应用的全局样式。
挂载到body的组件样式会失效,如弹窗,抽屉等。
当引入一个el-dialog弹窗时,弹窗的model(遮罩层)默认是挂载到body上面的,而这个body是主应用的body。由于目前开启实验性隔离,子应用的每个样式都加上属性选择器,导致原本model的样式无法作用于主应用的model。
弹窗就是常规的写法
<el-dialog
title="添加提醒事项"
:visible.sync="dialogAddRemind"
width="500px"
@close="closeAddRemind"
>
dom层级是这样的:
遮罩层是拿不到样式的
解决方法一:
:modal-append-to-body="false" 不把弹窗挂到body上了。
<el-dialog
title="添加提醒事项"
:visible.sync="dialogAddRemind"
width="500px"
:modal-append-to-body="false"
@close="closeAddRemind"
>
加完之后dom层级是这样的:
样式自然就可以获取到了
解决方法二:
整个项目里面有这么多的弹窗,总不能把所有的弹窗都加一遍吧。所以需要让原本挂载到body的dom还是挂载到body上,但是不能挂载到主应用了,需要挂载到子应用的dom上。方法就是重写appendChild
function redirectPopup(container) {
// 子应用中需要挂载到子应用的弹窗className。样式class白名单,用子应用的样式。
const whiteList = ['el-select-dropdown', 'el-popper', 'el-popover', 'el-dialog__wrapper','v-modal']
// 保存原有document.body.appendChild方法
const originFn = document.body.appendChild.bind(document.body)
// 重写appendChild方法
document.body.appendChild = (dom) => {
// 根据标记,来区分是否用新的挂载方式
let count = 0
whiteList.forEach((x) => {
if (dom.className.includes(x)) count++
})
if (count > 0 && container.container) {
// 有弹出框的时候,挂载的元素挂载到子应用上,而不是主应用的body上 这个.app可以修改成子应用的标识
container.container.querySelector('.app').appendChild(dom)
} else {
originFn(dom)
}
}
}
function render(props = {}) {
const { container } = props;
if(container){
redirectPopup(props)
//...new vue()
}
// ...
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
dom层级为:
原本挂载到主应用的dom,通过重写appendChild后,挂载到了类名为app的元素下。目前dom和样式都在子应用中,自然也就可以正常的展示样式了。如果还有别的组件挂载到了主应用的body上,可以检查下 whiteList 是否包含类名。
这样就可以保证子应用的样式,不会影响主应用。
问题五:各个应用之间如何通信
- props
适合初始化时进行传值,问题三有具体用法。
- 消息订阅监听 推荐这种方式,好理解
新建EventBus.js文件
class EventBus{
constructor(){
this.events = {}
}
on(eventName, callBack){
if(!this.events[eventName]){
this.events[eventName] = []
}
// 防止重复订阅同一个事件
const has = this.events[eventName].some(fn => fn === callBack)
if(!has){
this.events[eventName].push(callBack)
}
}
emit(eventName, ...args){
if(this.events[eventName]){
this.events[eventName].forEach(fn => fn(...args))
}
}
off(eventName, callBack){
if(this.events[eventName]){
this.events[eventName] = this.events[eventName].filter(fn => fn !== callBack)
}
}
}
window.$eventBus = new EventBus()
在主应用的main.js 中引入一下
// main.js
import './utils/EventBus'
// ...registerMicroApps([])
主应用更改业务逻辑
<button @click="changeTheme">更换主题颜色</button>
changeTheme(){
window.$eventBus.emit('changeColor','black')
}
子应用监听主应用的消息 main.js
function render(props = {}) {
const { container } = props;
if(container){
window.$eventBus.on('changeColor', (color) => {
console.log('color is '+ color)
})
}
// ... new Vue()
}
- onGlobalStateChange setGlobalState initGlobalState
这是官方提供的方法 官方说明 ,它可以动态的监听值的变化。
适用场景:
主应用设置换肤主题值发生变化,同步给子应用,子应用接收值之后进行换肤逻辑。
主应用进行退出登录发送消息给子应用,子应用进行注销token。
子应用和主应用都可以订阅或发送消息
主应用
// actions.js
import {
initGlobalState
} from 'qiankun';
// 注册微应用通信示例
const initialState = {};
const actions = initGlobalState(initialState);
export {
actions, // 导出通信示例
}
更换主题的业务逻辑
<button @click="changeTheme">更换主题颜色</button>
// 触发事件就发送消息
changeTheme(){
if(this.theme === 'dark'){
this.theme = 'light'
actions.setGlobalState({theme: 'light'})
}else{
this.theme = 'dark'
actions.setGlobalState({theme: 'dark'})
}
},
子应用
封装的目的是为了让整个项目只要import,就可以直接使用。
//actions.js
function emptyAction(...args) {
// 警告:提示当前使用的是空 Action
console.warn("Current execute action is empty!");
}
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction,
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions;
}
/**
* 映射
*/
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args);
}
/**
* 映射
*/
setGlobalState(...args) {
return this.actions.setGlobalState(...args);
}
}
const actions = new Actions();
export default actions;
子应用 main.js
function render(props = {}) {
const { container } = props;
if(container){
// 订阅消息
actions.setActions(props)
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log("state, prev", state, prev)
if(state.theme !== prev.theme){
// 换肤逻辑
}
})
}
// ... new Vue()
}
render初始化的时候,props的值
console.log("state, prev", state, prev) 输出结果如下:
问题六:js隔离问题
子应用在window添加全局变量,其他应用无法直接通过window获取,因为qiankun的js隔离机制。需要通过window.proxy.xxx 去获取。
问题七:vite + react 怎么接入qiankun
qiankun 不支持vite,大部分的解决方案都是引入 vite-plugin-qiankun
改造方案不困难 vite-plugin-qiankun 接入文档
按照文档对 react 配置完之后,在开发环境发现有下面的报错。
vite接入qiankun react需要额外处理 因为
vite.config.ts 注释掉 react()
export default defineConfig({
plugins: [
// react(), 注释掉
qiankun('reactApp', {
useDevMode: true
}),
...(
useDevMode ? [] : [
reactRefresh()
]
),
],
base: '/reactApp',
})
然后发现报错。
这是因为 react() 引入了@vitejs/plugin-react 它可以让我们在tsx中不必声明React。去掉之后我们还要在每个jsx文件都引入 import React from 'react' 。才能正常运行
还有个解决方案我没尝试但是可以参考下: blog.cchealthier.com/2023/10/15/…
结尾
真是踩了不少的坑才把这些项目接完,后续遇到问题再补充,希望能对大家有所帮助。