最初接触微前端有种高大上的感觉,细细研究了一下,其实也不难,本文就不介绍iframe,single-spa之类的东西了,直奔主题。
当前系统有横向一级顶部路由和竖向二级侧面路由,点击一级路由会触发二级路由的切换,从而实现两级路由的切换。
之所以采用微前端,是因为我们的工程有太多冗余第三方代码,因此为第三方用户开辟一个单独的工程势在必行,所以说微前端也是为了我们主体仓库代码的干净。
主应用路由(router.js)配置,增加sub项
{
path: "/micro/*",
name: "micro",
component: Home,// Home一般为包含上部和左侧路由的layout组件
redirect: "/",
meta: {
auth: false, // 是否需要登录
keepAlive: false // 是否缓存组件
},
children: [{
path: "/",
name: "sub",
component: () => import('@/views/sub.vue'),// 当路由点击时,链接到sub组件
meta: {
auth: true,
keepAlive: false
}
}]
},
主应用sub.vue(当路由点击时,打开的子应用页面)
<template>
<div class="layout">
<Layout class="main_view">
<!-- micros为定义的微应用挂载dom的id -->
<div id="micros" class="views"></div>
</Layout>
</div>
</template>
<script>
import '@/micros' // 触发微应用的注册和主应用的数据监听等逻辑
import {start} from "qiankun";
import API from '@/api/apv';
import {
mapActions
} from "vuex";
export default {
name: "micros",
created() {
start()
},
mounted() {
this.signingReminder()
this.$nextTick(()=>{
// 此逻辑为了解决主应用在子应用路由下直接刷新导致的路由不展示的问题;
// 当前子页面的mounted周期,对左侧路由进行store数值更新,并进行refreshList刷新操作(这里的逻辑视自己工程具体情况而定,核心思路是一致的)。
this.comeSetListArrQiankun(
[
{
"name": "仓库管理",
"path": "/micro/thirdPartyStore",
"showChild": 0,
"tabType": 600,
"type": "third_party_supplier",
"child": [
{
"name": "入库管理sub",
"path": "/micro/thirdPartyStore/stock",
"signColor": 6000,
"type": "third_party_supplier",
"child": 1
},
{
"name": "库存管理sub",
"path": "/micro/thirdPartyStore/inventory",
"signColor": 6001,
"type": "third_party_supplier",
"child": 1
},
{
"name": "出库管理sub",
"path": "/micro/thirdPartyStore/outbound",
"signColor": 6002,
"type": "third_party_supplier",
"child": 1
},
]
}
]
)
// 路由更新需要主动触发refreshList,刷新视图
this.$parent.$parent.$refs.PageHeader.$emit('refreshList')
})
},
methods: {
...mapActions("index",['comeSetListArrQiankun']),
signingReminder() {
// 当前页面的其他逻辑
},
}
}
</script>
micros中的apps.js
import store from '@/store/index'
import {registerMicroApps} from "qiankun";
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "micro",
entry: "http://127.0.0.1:8889/sub/micro/",
container: "#micros",
activeRule: "/mono/micro/",
props: {
}
},
];
export default apps;
micros中的index.js
import { registerMicroApps, initGlobalState, addGlobalUncaughtErrorHandler, start } from "qiankun";
import apps from "@/micros/apps";
import store from '../store/index'
/**
* @description: 注册微应用
* 第一个参数 - 微应用的注册信息
* 第二个参数 - 全局生命周期钩子
*/
registerMicroApps(apps,
{
beforeLoad: [
async (app) => {
}
],
beforeMount: [
app => {
//console.log("subdata",app)
//console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterMount: [
app => {
//console.log("subdata",window)
//console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterUnmount: [
app => {
//console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
//location.reload(true)
},
],
}
);
// 初始化 state
const actions = initGlobalState({menus: []});
// 主应用监听路由列表传递,一旦传递过来通过主应用自身的store进行路由设置
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
store.dispatch('index/comeSetListArrQiankun', state.menus);
});
addGlobalUncaughtErrorHandler((event) => {
const { msg } = event;
if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
console.log('加载失败');
}
});
// start();
子应用main.js
import './public-path';
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
import store from './store/index'
import ViewUI from 'view-design';
import VueClipboard from 'vue-clipboard2';
import VueJsonp from 'vue-jsonp';
import debounce from './mixins/debounce'; // 全局防抖
import 'view-design/dist/styles/iview.css';
import installTrimDirective from '@/utils/trimDirective'
import * as _ from 'lodash'
import PerfectScrollbar from 'vue2-perfect-scrollbar'
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
import menus from '@/store/modules/index/_types_thirdPartyStore'
Vue.prototype.$_ = _
window.sharedData = 'Hello from subapp';
Vue.use(PerfectScrollbar);
Vue.use(VueJsonp)
Vue.use(ViewUI);
Vue.use(VueClipboard)
Vue.mixin(debounce)
Vue.prototype.$bus = new Vue()
Vue.use(installTrimDirective) // * 全局去空格 v-trim
// 阻止启动生产消息
Vue.config.productionTip = false
let instance = null;
function render(props = {}) {
instance = new Vue({
el: '#app', // 这里应该替换为你实际的挂载点
router,
render: (h) => h(App),
created() {
}
})
}
if (!window.__POWERED_BY_QIANKUN__) {
render()
} else {
}
export async function bootstrap(props) {
}
export async function mount(props) {
props.onGlobalStateChange((state) => {
console.log('子应用接收的参数', state)
}, true)
props.setGlobalState({menus}) // 当子应用挂载完毕,子应用设置左侧路由数据给主应用
if (!instance) {
render();
}
}
export async function unmount() {
if (instance) {
// 卸载 Vue 实例
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
}
vue.config.js(主要将webpack的out,library和libraryTarget进行设置,防止报错 Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry)
const path = require("path");
const packageName = require("./package.json").name;
const node_env = process.env.NODE_ENV === "production";
// const baseUrl = process.env.VUE_APP_BASE_URL;
const baseUrl = "/";
const resolve = (dir) => path.join(__dirname, dir);
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
outputDir: `../dist/${packageName}`,
publicPath: node_env ? baseUrl : "/",
assetsDir: "static",
configureWebpack: {
resolve: {
alias: {
"@": resolve("src"),
},
},
output: {
library: `${packageName}-[name]`,
libraryTarget: "umd", // 把微应用打包成 umd 库格式
chunkLoadingGlobal: `webpackJsonp_${packageName}`,
},
},
devServer: {
hot: true,
// disableHostCheck: true,
host: process.env.VUE_APP_HOST,
port: process.env.VUE_APP_PORT, // 在.env中VUE_APP_PORT=7788,与父应用的配置一致
headers: {
"Access-Control-Allow-Origin": "*", // 主应用获取子应用时跨域响应头
},
},
})
public-path.js(防止页面404,否则会找不到子应用对应资源,此文件要在main.js的顶部引用)
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
注意:
- 工程中的token传递也可以通过initGlobalState时,传递给子应用,也可以通过挂载到localstorage上,由工程总体共用。
- 暂时未进行样式隔离等需求,待后续验证
如果是vite项目,子应用配置稍有不同
vite.config.js
import {fileURLToPath, URL} from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun';
import pkg from './package.json';
// https://vitejs.dev/config/
export default (({mode}) => {
return defineConfig({
plugins: [
vue(),
qiankun('vue3',{
useDevMode: true
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
target: 'esnext',
format: 'UMD', // 输出 UMD 格式的代码
globalName: '@nahui/merchant-vue3', // 子应用的全局名称
outDir: `../../dist/${pkg.name}`,
},
base: '/',
server: {
headers: {
'Access-Control-Allow-Origin': '*',
},
host: loadEnv(mode, process.cwd()).VITE_APP_HOST, // VITE_APP_HOST = 127.0.0.1
cors: true,
port: loadEnv(mode, process.cwd()).VITE_APP_PORT, // VITE_APP_PORT = 8030
open: true
},
})
})
main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// import './style.css'
import App from './App.vue'
import router from './router'
import { renderWithQiankun, qiankunWindow, QiankunProps } from 'vite-plugin-qiankun/dist/helper'
import { useRoutesStore } from '@/stores/routes.js';
import { useGlobalStore } from '@/stores/global.js';
import { initGlobalState } from './utils'
let app = null
const initQianKun = () => {
renderWithQiankun({
// bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap
// 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等
bootstrap() {
console.log('bootstrap');
},
// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法,也可以接受主应用传来的参数
mount(_props) {
app = createApp(App)
app.use(createPinia())
app.use(router)
const routesStore = useRoutesStore()
const globalStore = useGlobalStore()
initGlobalState(routesStore, _props) // 处理路由渲染等逻辑
globalStore.initGlobalState(_props)
render(_props.container)
},
// 应用每次 切出/卸载 会调用的unmount方法,通常在这里我们会卸载微应用的应用实例
unmount(_props) {
console.log('unmount', _props);
app.unmount()
},
update: function (props) {
console.log('update');
}
});
}
const render = (container) => {
if (!container) {
app = createApp(App)
app.use(createPinia())
app.use(router)
}
// 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
const appDom = container ? container : "#app"
app.mount(appDom)
}
// 判断是否为乾坤环境,否则会报错[qiankun]: Target container with #subAppContainerVue3 not existed while subAppVue3 mounting!
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render(null)