• 作者:老汪软件技巧
  • 发表时间:2024-09-23 00:01
  • 浏览量:

前言

我们公司是做hrsaas的,前几个月和别的公司有合作,他们要求我们使用 qiankun 的方案作为子应用接入他们的系统。我:啥是qiankun啊? 微前端是啥,没接触过啊。没辙看看文档,硬着头皮接吧。跟着文档接入难度不大,开个页面一看惨不忍睹啊,发生了下面一系列问题,记录一下,省的忘了。

问题一:子应用的字体静态资源404问题

独立运行好好的,接入qiankun之后字体图标/图片都加载不出来了。就像这样:

导致这个问题的原因大概率是因为作为子应用运行时请求的 url 发生了变化,打开控制台可以发现路径是省略域名的,这就是导致错误的原因。

独立运行的项目的ip是 :7777/ ,省略域名请求的路径是 :7777/static/img/src/assets/images/loginBackground.png,可以拿到资源。

而主应用运行地址是 :8080/ ,请求的是 :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
}
// 在业务中
// 

pushState跳转

在子应用跳转主应用或其他子应用也可以使用 pushState 方法,pushState(state, unused, url)

div onClick={()=>window.history.pushState(null,'','/sso/home')}>通过pushState跳转到sso

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 从而达到样式隔离的目的。

菜鸟网络即将面临的挑战在哪里_对接菜鸟物流接口_

Shadow DOM 说明

它的隔离效果是非常好的,问题在于它会使子应用挂载到主应用 body 元素上的弹窗(element/antd的组件)找不到挂载对象。

放弃该方案

experimentalStyleIsolation 试验性隔离

开启该属性后,它会为子应用的每个样式都加上属性选择器,类似 Vue 的 scoped 。

使用发现有如下问题:

个人认为它只能达到子应用的样式不影响主应用,不能隔离主应用的全局样式。

如果弹窗挂载到了主应用的body上,还会导致样式丢失。

可能有兼容性问题,如果使用了类似:root的伪类,还会导致样式混乱。

坑有点多故放弃该方案

问题五:各个应用之间如何通信props

适合初始化时进行传值,问题三有具体用法。

onGlobalStateChange setGlobalState initGlobalState

可以动态的监听值的变化。

适用场景:

主应用设置换肤主题值发生变化,同步给子应用,子应用接收值之后进行换肤逻辑。

主应用进行退出登录发送消息给子应用,子应用进行注销token。

子应用和主应用都可以订阅或发送消息

主应用

// actions.js
import {
    initGlobalState
  } from 'qiankun';
   
   
  // 注册微应用通信示例
  const initialState = {};
  const actions = initGlobalState(initialState);
   
  export {
    actions, // 导出通信示例
  }

更换主题的业务逻辑

   
    // 触发事件就发送消息
    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' 。才能正常运行

还有个解决方案我没尝试但是可以参考下: /2023/10/15/…

结尾

真是踩了不少的坑才把这些项目接完,后续遇到问题再补充,希望能对大家有所帮助。