前言
-
本章主要分享微前端源码,对理论仅简单描述,如还未了解什么是微前端,或理解微前端基本组成等,可移步笔者上篇: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;
}
}
结语
匆忙的文章与案例,有不理解的地方,欢迎留言!
github地址:github.com/zhuangweizh…