基于qiankun微前端,分享如何将vue2过渡到vue3的实战篇(附github源码)
前言
本章主要分享微前端源码,对理论仅简单描述,如还未了解什么是微前端,或理解微前端基本组成等,可移步笔者上篇:juejin.cn/post/702545…
该实战案例,适合中台管理系统,想将vue2升级到vue3的小伙伴们。
1)项目概况
本项目案例,主要使用微前端qiankun框架,打通vue2.6 + vue3.0 + vue3.2(vite)。包含子父通信,
应用名称 | 应用级别 | 使用框架 | 端口 |
---|---|---|---|
main | 主 | vue2.6 | 8080 |
crm | 子 | vue3.2 | 8081 |
sale | 子 | vue3.0 | 8082 |
2)应用基本搭建
vue2: vue create main
{ "name": "main", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "ant-design-vue": "^1.7.8", "core-js": "^3.6.5", "qiankun": "^2.5.1", "register-service-worker": "^1.7.2", "vue": "^2.6.11", "vue-router": "^3.2.0", "js-cookie": "^2.2.1", "vuex": "^3.4.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-vuex": "~4.5.0", "@vue/cli-service": "~4.5.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "vue-template-compiler": "^2.6.11" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] }复制代码
package.json
vue3: create-vite-app crm
package.json
{ "name": "crm", "version": "0.0.0", "scripts": { "serve": "vite", "build": "vite build" }, "dependencies": { "path": "^0.12.7", "sass": "^1.43.2", "vite-plugin-style-import": "^1.4.0", "vue": "^3.2.16", "vue-router": "^4.0.12", "vuex": "^4.0.0-0", "vite-plugin-qiankun": "1.0.10", "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@types/js-cookie": "^3.0.0", "@types/node": "^16.11.1", "@vitejs/plugin-vue": "^1.9.3", "ant-design-vue": "^2.2.8", "typescript": "^4.4.3", "vite": "^2.6.4-beta", "vue-tsc": "^0.3.0" } }复制代码
3)重置子应用模式
主应用与子应用分别注册后,即可完成数据通信。此时,修改子应用的启动方式:
import { setupAntd } from "@/plugins/antd" import { routes } from "@/router" import { setupStore } from "@/store" import { qiankunWindow, renderWithQiankun } from "vite-plugin-qiankun/dist/helper" import { createApp } from "vue" import { createRouter, createWebHistory } from "vue-router" import registerMainStore from '../../main/src/globalStore/register' import App from "./App.vue" import store from "./store" let instance: any = null const history: any = null function render(props: any = {}) { const { container } = props instance = createApp(App) setupAntd(instance) // 引入antd setupStore(instance) // 引入store const history = createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? "/crm" : "/") const router = createRouter({ history, routes }) instance.use(router) instance.mount(container ? container.querySelector("#app") : document.getElementById("app")) if (qiankunWindow.__POWERED_BY_QIANKUN__) { console.log("crm正在作为子应用运行") } } function storeMonitor(props: any) { if (props.onGlobalStateChange) { props.onGlobalStateChange((value: any, prev: any) => { console.log(`[子应用crm接受数据成功]:`, value) store.dispatch("syncMainProject", value) }, true) } } renderWithQiankun({ bootstrap() { console.log("crm,vue3启动成功") }, mount(props) { store.dispatch("initMainProject", props) storeMonitor(props) render(props) registerMainStore(store, props) }, unmount(props) { console.log("crm已卸载") instance.unmount() instance._container.innerHTML = "" history.destroy() // 不卸载 router 会导致其他应用路由失败 instance = null } }) if (!qiankunWindow.__POWERED_BY_QIANKUN__) { console.log(`主应用crm启动`) render() }复制代码
4)应用路由配置
qiankun使用微前端有两个思路:
1)registerMicroApps
2)loadMicroApp
笔者的观点,registerMicroApps比较适合商城页面的衔接,而中台管理系统,因为有需要共享的菜单栏,头部等,loadMicroApp更适合。
先查看最简单的挂载:
const app = loadMicroApp({ name: 'crm', entry: 'http://localhost:8081', // 对应的路由地址 container: '#crm_Container', // 挂载的id activeRule: '/crm',, // 转发的地址 props: { // ...附带参数 } }); start();复制代码
此时,还需要考虑,主应用与子应用的区别,不同应用的切换(采用方案为同事挂载多个,不在当前子项目将display:none隐藏)等。可直接看调试后代码:
export default { data() { return { loadedApp: {}, microApps: [ { name: 'crm', entry: 'http://localhost:8081', container: '#crm_Container', activeRule: '/crm', }, { name: 'sale', entry: 'http://localhost:8082', container: '#appChild2', activeRule: '/sale', }, ], }; }, computed: { ...mapGetters(['getToken']), }, methods: { isQianKun( routePath = this.$route.path ){ const microApp = this.microApps.find(item => routePath.includes(item.activeRule)); return microApp; }, goQiankun( routePath = this.$route.path ) { const loadedApp = this.loadedApp; const microApp = this.microApps.find(item => routePath.includes(item.activeRule)); // 如果是子应用 if (microApp) { // 将主应用的路由转化为子路由URL const childRoutePath = routePath.replace(microApp.activeRule, ''); // 如果没有加载当前子应用 if (!loadedApp[microApp.name]) { // 开始加载 const app = loadMicroApp({ ...microApp, props: { token: this.getToken, getGlobalState: actions.getGlobalState // 下发getGlobalState方法 } }); // 加载子应用 // 开始完成 app.loadPromise.then(() => {}); loadedApp[microApp.name] = { // 将当前子应用存入loadedApp缓存 app, subRoutes: [childRoutePath], }; } else { // 如果已加载子应用,将子应用的路由记录到数组中 const subRoutes = loadedApp[microApp.name].subRoutes; if (!subRoutes.includes(childRoutePath)) { subRoutes.push(childRoutePath); } } // 通知子应用增加 keep-alive 的 include actions.setGlobalState(loadedApp); } this.loadedApp = loadedApp; start(); }, }, };复制代码
这样,即可控制不同的路由,进入到不同的应用。再由不同的应用显示对应的页面。
参考链接:qiankun.umijs.org/zh/api#regi…
5)启动公用配置
同时存在多个项目的情况下,每次运行将要逐个npm , npm run serve等。如果此时,外边能直接启动所有的项目的话就方便许多。那么,安排。
新建package.json:
{ "name": "qiankun", "version": "0.0.1", "description": "来自稀土掘金,我叫逐步前行", "main": "index.js", "devDependencies": { "npm-run-all": "^4.1.5" }, "scripts": { "install": "npm-run-all --serial install:*", "install:main": "cd main && npm install", "install:crm": "cd platform && npm install", "install:sale": "cd platform && npm install", "serve": "npm-run-all --parallel serve:*", "serve:main": "cd main && npm run serve", "serve:crm": "cd crm && npm run serve", "serve:sale": "cd sale && npm run serve" }, "keywords": [ "main", "platform" ], "author": "逐步前行", "license": "MIT", "__npminstall_done": false }复制代码
6)应用样式隔离
不同应用在同一浏览器窗口同时显示,如果不处理,将互相影响。
这里快速分享几个方案:
不同项目内部组件库,可以使用bem直接区分。可以保证不会有重叠。
如果同一页面,同时显示ant-design-vue 1.0版本,与ant-design-vue 2.0版本,可以使用重命名组件库的思维。
我们可以把ant-design-vue 2.0的前缀修改成 ant2-, 这样就不会与原来的ant- 冲突。
export default defineConfig( ..., css: { preprocessorOptions: { less: { modifyVars: { "ant-prefix": "ant2" }, javascriptEnabled: true } } } }复制代码
APP.vue
<div class="app"> <a-config-provider :locale="locale" prefix-cls="ant2"> <router-view /> </a-config-provider> </div>复制代码
记得,把对应的ant样式文件,ant-替换为ant2-
7)应用状态共享
此时需要考虑不同项目通信的问题,我们先引入qiankun自带的initGlobalState。 直接看代码。
主应用注册实例:
import { initGlobalState } from 'qiankun'; import Vue from 'vue'; import utils from "../utils/utils"; // 父应用的初始state const initialState = Vue.observable({ type: "", }); const actions = initGlobalState(initialState); actions.onGlobalStateChange((state, prev) => { console.log('主应用监听变化', state, prev); const newState = JSON.parse( JSON.stringify(state)); console.log('newState', newState); for (const key in newState) { initialState[key] = newState[key] } }); // 定义一个获取state的方法下发到子应用 actions.getGlobalState = key => { // 有key,表示取globalState下的某个子级对象 // 无key,表示取全部 console.log('主应用监听store获取', key); return key ? initialState[key] : initialState; }; export default actions;复制代码
子应用接受实例:
/** * @param {vuex实例} store * @param {qiankun下发的props} props */ function registerMainStore(store, props = {}) { if (!store || !store.hasModule) { return } // 获取初始化的state const initState = props.getGlobalState && props.getGlobalState() || { } // 将父应用的数据存储到子应用中,命名空间固定为global if (!store.hasModule('global')) { // 这里是全局的store const globalModule = { namespaced: true, state: initState, actions: { // 子应用改变state并通知父应用 setGlobalState({ commit }, payload) { commit('setGlobalState', payload) commit('emitGlobalState', payload) }, // 初始化,只用于mount时同步父应用的数据 initGlobalState({ commit }, payload) { commit('setGlobalState', payload) }, }, mutations: { setGlobalState(state, payload) { // eslint-disable-next-line state = Object.assign(state, payload) }, // 通知父应用 emitGlobalState(state) { console.log(`通知父应用成功,参数为:`, state); if (props.setGlobalState) { props.setGlobalState(state) } }, }, } store.registerModule('global', globalModule) } else { // 每次mount时,都同步一次父应用数据 store.dispatch('global/initGlobalState', initState) } } export default registerMainStore复制代码
此时,我们只需要在子应用mount生命周期,添加监听,即可实时接收到主应用的通信:
function storeMonitor(props: any) { if (props.onGlobalStateChange) { props.onGlobalStateChange((value: any, prev: any) => { console.log(`[子应用crm接受数据成功]:`, value) store.dispatch("syncMainProject", value) }, true) } }复制代码
8)用户权限打通
用户信息与权限等,笔者的设计是由主应用统一维护。子应用需要,我们可以利用上述"应用状态共享"的方案,同步到所有子应用:
我们在主应用登录时,同步子应用:
// 假设setToken, 为登录方法,需redirectToken同步子应用。 setToken(state , token){ state.token = token; Cookies.set('token', token); setTimeout(() => { globalStore.setGlobalState({ type: 'redirectToken', token }); }, 100); }复制代码
子应用接受消息:
function storeMonitor(props: any) { if (props.onGlobalStateChange) { props.onGlobalStateChange((value: any, prev: any) => { console.log(`[子应用crm接受数据成功]:`, value) store.dispatch("syncMainProject", value) }, true) } } // 同步到store async syncMainProject({ commit, dispatch, getters }: ActionContext<IQianKunState, IStore>, obj: any) { switch (obj.type) { case "redirectToken": commit("setToken", obj?.token) break; } } 复制代码
此时,子应用就可以实时同步用户状态。至于用户状态获取后,怎么显示,那属于各个应用自治的问题了。
9)tab切换
tab的切换,涉及到两个痛点,一个是缓存(下述会单独分析)。还有另外一个就是项目之间的控制:
需要每次走一遍主应用逻辑,也要同时检查是否唤起子应用。
<template> <div> <a-tabs v-model="tabActive" type="editable-card" @change="onChange" @edit="onDel"> <a-tab-pane v-for="(item, index) in tabList" :key="index" :tab="item.name" :closable="true" > </a-tab-pane> </a-tabs> </div> </template> <script> import { mapGetters } from "vuex"; import qiankun from "../views/qiankun.js"; export default { mixins: [qiankun], data(){ return{ tabActive: Number(this.$store.getters.getActiveTabs ) } }, computed: { tabList(){ return this.$store.getters.getTabItems; }, }, watch:{ '$store.getters.getActiveTabs': function(val){ this.tabActive = val; } }, methods: { onDel(targetKey, action) { this.$store.dispatch("delTabs", targetKey); }, onChange(targetKey, action) { this.tabActive = targetKey; this.$router.push({ path: this.tabList[targetKey].path }); this.$store.commit("setActiveTabs", targetKey); this.isQianKun() && this.goQiankun(); // 走子项目路由 }, }, }; </script>复制代码
10)打通keep-alive
首先,keep-alive由各个项目自治。我们只需要维护好,哪一些改缓存,哪一些不该缓存。这些应该在主项目维护好:
主应用:
<div v-show="$route.path.startsWith('/main')"> <keep-alive :include="getCacheTabs> <router-view></router-view> </keep-alive> </div> <div v-for="o in microApps" v-show="$route.path.startsWith(o.activeRule)" :key="o.name"> <KeepAlive> <div :id="o.container.slice(1)"></div> </KeepAlive> </div> 复制代码
所有页面跳转,均由主应用处理。如有子应用需要页面跳转,也经过主应用的形式。
/* url: 跳转的路由路径 delTab: 是否删除当前标签 name: 调整的tab名称,不传将会获取配置中的名称 obj.isRouterName: 是否打开router的name模式, meta路由额外参数 */ redirectPage(url, delTab = false, name, obj = {}) { const mainRoutes = vue.$router.options.routes[1].children; console.log(`mainRoutes`, mainRoutes); const path = url.indexOf("?") ? url.split("?")[0] : url; const nowIndex = mainRoutes.findIndex(item => { return item.path === path }) const isExistMain = nowIndex !== -1 if( isExistMain ){ //主应用跳转 const meta = obj.meta ? obj.meta : mainRoutes[nowIndex].meta; const routerName = name || meta.title; vue.$store.dispatch("setTabs", { path: url, name: routerName, delTab }) vue.$router.push({ path: url }); } else { //子应用跳转 vue.$store.dispatch("setTabs", { path: url, name, delTab }) vue.$router.push({ path: url }); } }复制代码
11)公用抽离
案例demo太小,本案例暂为提供抽离。
但是想要实现也比较简单,新建common项目。如登录页面等需要复用,可以统一到common项目应用。
12)qiankun部署
直接给nginx配置,
server { listen 80; listen 443 ssl; server_name ****; include conf.d/ssl/ssl.conf; include conf.d/oss/oss.conf; location /crmMicro/ { proxy_pass http://**.**.**.**:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /saleMicro { proxy_pass http://**.**.**.**:8082/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { proxy_pass http://**.**.**.**:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
作者:逐步前行
链接:https://juejin.cn/post/7038236737444773924
伪原创工具 SEO网站优化 https://www.237it.com/