当前位置:首页 > javascript > 正文内容

微前端使用: qiankun

hxing6418个月前 (01-02)javascript3453

一、qiankun使用场景

1. 简介

qiankun是在single-spa的基础上实现的,可以保证各个项目独立使用,也可以集成使用。各系统之间不受技术栈的限制,集成使用也能保证各样式和全局变量的隔离。模块的插拔式使用,当公司项目集是一个大系统下包含多个子系统或者模块时,可以采用这种方式动态部署各个系统。亦或者是老项目技术升级和重构,可以通过qiankun按模块进行改造,避免对整个系统产生较大的影响。功能和iframe类似,但是由于iframe数据通信难度较大,且有安全和SEO的问题,所以iframe使用体验不佳。


2. 原理逻辑:

a. 需要在各个子应用的基础上新增一个主应用,通过主应用监听路由变化。

b. 当有路由切换时就会触发上述监听函数从而去匹配在主应用中注册的各个子应用路径(activeRule)是否匹配。

c. 匹配到子应用后就会加载子应用的资源到对应的容器当中去。


二、实现样例

本样例使用的是Node 16的版本,主应用采用Vue3框架,两个子应用分别使用Vue2和Vue3框架。qiankun版本是2.10.16。

1. 搭建主应用

利用脚手架创建一个qiankun-main的主应用,同时安装qiankun组件(qiankun只需要在主应用安装,子应用不需要),其中代码中标注重点的内容是配置qiankun的关键步骤

1.1 打开vue.config.js文件,添加跨域处理,避免跳转时出现跨域问题

// vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 8085,
    headers: {            // 重点1: 允许跨域访问子应用页面
        'Access-Control-Allow-Origin': '*',
    }
  }
})


1.2 主应用中设置子应用接收容器

// App.vue

<template>
  <div class="app">
    <p><router-link to="/">点击跳转到父页面</router-link></p>
    <button @click="login">登陆</button>
    <p><router-link to="/vue3">跳转到Vue3子应用</router-link></p>
    <p><router-link to="/vue2">跳转到Vue2子应用</router-link></p>
    <router-view />
    <div id="vue3"></div>    <!-- 重点2:子应用容器 id -->
    <div id="vue2"></div> <!-- 重点2:子应用容器 id -->
  </div>
</template>
<script>
import actions from '@/shared/actions';
export default {
  name: 'App',
  components: {
  },
  mounted() {
    actions.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prevState: 变更前的状态
      console.log('主应用观察者:token值改为', prevState.token);
      console.log("主应用观察者:登录状态发生改变,改变后的 token 的值为 ", state.token);
    });
  },
  methods: {
    login() {
      console.log('进入登陆事件');
      setTimeout(() => {
        const token = 'token_' + Math.floor(Math.random() * 100000);
        //登陆后随机生成token并设置
        actions.setGlobalState({ token });
        this.$router.push("/vue3");
      }, 300);
    }
  }
}
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

1.3 在src根目录下新增public-path文件;同时改造路由,设置返回的base地址

// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
// router/index.js
import {
  createRouter,
  createWebHashHistory
} from 'vue-router'
import '../public-path' // 重点3: 引入public-path文件
const router = createRouter({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/', // 重点4:qiankun进入子应用时,返回true
  history: createWebHashHistory(), // 重点5
  routes: [{
          path: '/',
          redirect: '/child'
      },
      {
          path: '/child',
          component: () => import('@/components/child')
      }
  ]
})
export default router
router/index.js

1.4 注册和引入子应用

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun'
createApp(App).use(router).mount('#app')
registerMicroApps([
  {
    name: "vue3 app",
    entry: "//localhost:8086", // 重点8:对应重点6
    container: '#vue3',         // 重点9:对应重点2
    activeRule: '/#/vue3',        // 重点10:对应重点4
    props: {
        appContent: '我是主应用传给vue的值'
    }
  },
  {
    name: "vue2 app",
    entry: "//localhost:8087", // 重点8:对应重点6
    container: '#vue2',         // 重点9:对应重点2
    activeRule: '/#/vue2',        // 重点10:对应重点4
    props: {
        appContent: '我是主应用传给Vue2的值'
    }
  }
])
setDefaultMountApp("/")  // 重点11:启动默认的子模块
// 启动
start()

2. 搭建子应用1

同样利用脚手架创建一个qiankun-vue3-child,项目使用Vue3作为基础框架

2.1 同样在src目录下创建public-path.js文件

// public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

2.2 改造router/index.js文件, 确认在qiankun模式下的路由基础路径

// router/index.js
import {
  createRouter,
  createWebHashHistory
} from 'vue-router'
import '../public-path' // 重点3: 引入public-path文件
const router = createRouter({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/', // 重点4:qiankun进入子应用时,返回true
  history: createWebHashHistory(), // 重点5
  routes: [
  ]
})
export default router

2.3 修改入口函数main.js,导入钩子函数

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import actions from './micros/actions'
let instance = null
function render(props = {}) {
  // qiankun模式下实现父子应用之间通信
  if (props) {
    actions.setActions(props);
  }
  const { container } = props
  // 为了避免根id#app与其他DOM冲突,需要限制查找范围
  instance = createApp(App).use(router).mount(container ? container.querySelector('#child-app') : '#child-app')
}
if (!window.__POWERED_BY_QIANKUN__) {
    render()
}
//--------- 生命周期函数------------//
export async function bootstrap() {
  console.log('[vue] vue app bootstraped')
}
export async function mount(props) {
  console.log('[vue] props from main framework', props)
  render(props)
}
export async function unmount() {
  if (instance) {
    console.log(instance, instance.unmount);
    // instance.unmount();
    instance = null
  }
}
// createApp(App).use(router).mount('#child-app')

2.4 修改打包配置文件vue.config.js,设置服务端口以及打包模式

// vue.config.js
const { defineConfig } = require('@vue/cli-service')
const { name } = require('./package');
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 8086,            // 重点6
    headers: {            // 重点7:同重点1,允许子应用跨域
        'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定义webpack配置
  configureWebpack: {
      output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd',        // 把子应用打包成 umd 库格式
          // jsonpFunction: `webpackJsonp_${name}`,
      },
  },
})

3. 搭建子应用2

步骤与第2步类似,只是使用Vue2作为基础框架


三、功能演示

2310605-20231219142600163-1643560325.gif



 四、常见问题

1. 子应用部署在同一个服务器同一个端口的不同路径下如何配置?

基本和部署在不同服务器的类似,只是将注册子应用的entry的服务器端口号换成某个路径,同时将打包的publicPath改为该路径

// 同服务器同端口部署配置
// 主应用入口文件中注册子应用
registerMicroApps([
  {
    name: "vue3_app",
    entry: "/entry_vue3", // 对应之前的 //localhost:8086
    container: '#vue3',    
    activeRule: '/#/vue3',     
    props: {
        appContent: '我是主应用传给vue的值'
    }
  }
])
// 子应用的 router/indexedDB.js
const router = createRouter({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/entry_vue3', // 设置路由路径
  history: createWebHashHistory(),
  routes: [
  ]
})
// 打包文件vue.config.js中添加默认路径
module.exports = defineConfig({
  publicPath: devFlag ? '/' : '/entry_vue3',
  transpileDependencies: true,
  devServer: {
    port: 8087,            // 重点6
    headers: {            // 重点7:同重点1,允许子应用跨域
        'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定义webpack配置  重点12
  configureWebpack: {
      output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd',        // 把子应用打包成 umd 库格式
          // jsonpFunction: `webpackJsonp_${name}`,
      },
  },
})

2. 主子应用之间通信?

2.1 使用qiankun框架提供的 initGlobalState 实现的,主要有下面三个函数:

onGlobalStateChange(callback, Immediately)在当前应用监听全局状态变化;

setGlobalState(state)按照一级属性进行状态设置,微应用只能修改一级属性;

offGlobalStateChange()移除当前的状态监听,微应用在unmount时默认调用;


2.2 使用方式,效果可见上面的案列图中对token的打印信息

a. 主应用的src目录下新增 shared/actions.js 文件。

// actions.js
import { initGlobalState } from "qiankun";
const initialState = {
  token: 'no token'
};
const actions = initGlobalState(initialState);
export default actions;

b. 比如在主应用的App.vue中使用并且实现登陆后生成token以及跳转到vue3子应用

// App.vue
import actions from '@/shared/actions';
export default {
  name: 'App',
  components: {
  },
  mounted() {
    actions.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prevState: 变更前的状态
      console.log('主应用观察者:token值改为', prevState.token);
      console.log("主应用观察者:登录状态发生改变,改变后的 token 的值为 ", state.token);
    });
  },
  methods: {
    login() {
      console.log('进入登陆事件');
      setTimeout(() => {
        const token = 'token_' + Math.floor(Math.random() * 100000);
        //登陆后随机生成token并设置
        actions.setGlobalState({ token });
        this.$router.push("/vue3");
      }, 300);
    }
  }
}

c. 子应用中使用时首先在根目录下创建一个micros/actions.js文件

// actions.js
function emptyAction() {
  // 确保单独部署时不会报错
  console.warn('当前无可执行的Action');
}
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;

d. 子应用的APP.vue页面中监听主应用中数据的变化以及子应用主动修改数据观察主应用是否能接收到

 // App.vue
import actions from '@/micros/actions.js';
export default {
  name: 'App',
  components: {
  },
  mounted() {
    actions.onGlobalStateChange(state => {
      console.log('子应用Vue的观察函数:', state);
    }, true)
  },
  methods: {
    changeToken() {
      actions.setGlobalState({ token: 'Vue3_' + Math.floor(Math.random() * 100000) })
    }
  }
}

3. 各个应用之间如何提取一些公共的资源或者模块?

可以将公共模块提取成一个公共组件发布到npm,然后由各个应用按需安装。


4. 各个系统如何做到只登陆一次?

可以参考单点登陆实现,简单逻辑就是比如:

a. 有一个地址sso.com做为控制中心,然后a.com、b.com为子模块。

b. 当访问a.com时无权限时路由会携带参数“a.com”自动跳转到登陆页面,输入用户名密码信息后,经过sso.com验证通过生成ticket并返回给页面同时跳转到a.com并下发ticket。

c. a.com请求获取到ticket后访问sso.com的服务器进行验证是否有效,有效则允许登陆,这样就完成了一次登陆。

d. 如果在已登录的状态下跳转到b.com,则省略第二步的登陆验证直接将ticket携带到b.com,然后再访问sso.com进行有消息验证。


注意:主应用注册的activeRule为/vue3时跳转到子应用不生效可能是因为浏览器路由跳转时自动加上/#/,所以在activeRule也需要修改为/#/vue3才可以跳转

扫描二维码推送至手机访问。

版权声明:本文由星星博客发布,如需转载请注明出处。

本文链接:http://xingxinghan.cn/?id=498

分享给朋友:

“微前端使用: qiankun” 的相关文章

JS逆向实战20——深入解析JavaScript虚拟机逆向工程

在Web开发领域中,JavaScript(JS)是一门广泛使用的编程语言。然而,由于其脚本性质和开放性,JavaScript代码的安全性一直备受关注。为了保护知识产权和提高代码的安全性,一些开发者会将JavaScript代码进行加密和混淆。这就需要逆向工程师掌握JS逆向技术来还原加密和混淆后的代码。...

JavaScript中的call、bind和apply方法深度解析

JavaScript是一种动态的、强大的编程语言,它的灵活性源于其独特的函数调用方式。在JavaScript中,我们常常会遇到三个非常重要的函数方法:call、bind和apply。这些方法都是用来改变函数运行时this的指向的。理解它们的工作原理和使用场景,对于我们编写高质量的Java...

7个Js async/await高级用法

JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/awai...

JS数组常用方法

JS数组常用方法

JS数组常用方法:1.数组的复制和填充批量复制方法 copyWithin(),以及填充数组方法fill()。这两个方法的函数签名类似,都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小。 1.1.fill()方法使用fill()方法可以向一个已...

typescript 中 omit 的理解

在 TypeScript 中,Omit 是一种非常有用的工具类型,它可以用于创建一个新的类型,这个新类型是从现有类型中排除了指定的属性后得到的。如果你想要处理一个对象但又不需要包含某些属性,那么 Omit 可以帮助你快速地创建一个新的类型。 比如说,...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。