前沿:
1.微前端优缺点,引入的问题?
2.qiankun和iframe对比?
一、主应用改造
1.安装乾坤
npm i qiankun -S
2.菜单改造
为实现微前端菜单部分必须改造,前端需要了解有几个子应用,子应用及其地址,菜单又属于哪个子应用。
主要分为两种实现方式:
无需中台:
从path地址栏添加标识进行区分,如:
原:/traffic/ai -> /qiankun/sub-app1/traffic/ai
- 以qiankun开头即认为是子应用,sub-app1为应用名称
配置:
改造前:```
[ { "redirect": null, "path": "/simple", "component": "layouts/RouteView", "route": "1", "children": [ { "path": "/simple/UIexample", "component": "simple/UIexample", "route": "1", "meta": { "keepAlive": true, "internalOrExternal": false, "title": "UI组件示例" }, "name": "simple-UIexample", "id": "702cd68f0bc72e4bddda437180c10872" } ], "meta": { "keepAlive": true, "internalOrExternal": false, "icon": "file", "title": "简版示例" }, "name": "simple", "id": "7cc903fdc3836b9ed98226f6f1337fdc" } ]
#### 改造后: 所有属于子应用的项目都需要携带- qiankunApp : 子应用名称
- qiankunAppUrl :子应用地址```
[ { "redirect": null, "path": "/simple", "component": "layouts/RouteView", "route": "1", "children": [ { "path": "/simple/UIexample", "component": "simple/UIexample", "route": "1", "meta": { "keepAlive": true, "internalOrExternal": false, "title": "UI组件示例" }, "name": "simple-UIexample", "id": "702cd68f0bc72e4bddda437180c10872", "qiankunApp": "subapp1", //子应用名称 "qiankunAppUrl": "http://localhost:3001" //子应用地址 } ], "meta": { "keepAlive": true, "internalOrExternal": false, "icon": "file", "title": "简版示例" }, "name": "simple", "id": "7cc903fdc3836b9ed98226f6f1337fdc" } ]
3.注册子应用
指明子应用名称、入口地址、渲染目标、及匹配规则activeRule
excludeAssetFilter 包含第三方script文件,需要在白名单过滤,或者中台直接放开跨域限制
import { registerMicroApps, start } from "qiankun";
import { USER_AUTH, SYS_BUTTON_AUTH } from "@/store/mutation-types";
import store from "@/store";
import { filterAppMenu } from "@/utils/menu";
import "./globalState";
const menus = store.state.user.permissionList;
const microApps = [
{
name: "subapp1",
entry: "//test.umetrip.com/saas/ume-tiny-manager-subapp/",
container: "#subapp1",
activeRule: "/saas/umeapp-tiny-manager/#/subapp1",
props: {
auth: sessionStorage.getItem(USER_AUTH),
allAuth: sessionStorage.getItem(SYS_BUTTON_AUTH),
},
},
];
// 本地调试时的映射关系
const mapping = {
subapp1: {
entry: "//localhost:3001/",
activeRule: "/#/subapp1",
},
};
// 替换接口获取到的菜单信息
microApps.forEach((ele) => {
// ele.entry = mapping[ele.name].entry;
// ele.activeRule = mapping[ele.name].activeRule;
});
registerMicroApps(microApps);
function isCrossOriginRequest(url) {
var parser = document.createElement("a");
parser.href = url;
return parser.origin !== window.location.origin;
}
const config = {
sandbox: {
strictStyleIsolation: true, // 严格沙箱
experimentalStyleIsolation: true, // 实验性沙箱
},
excludeAssetFilter: (assetUrl) => {
// 如果是跨域请求就放开qiankun劫持
if (!assetUrl.includes("localhost") && isCrossOriginRequest(assetUrl)) {
return true;
}
},
};
start(config);
4.文件加载改造
原本的路由有自己匹配的页面
let menu = {
path: item.path,
name: item.name,
redirect: item.redirect,
hidden: item.hidden,
meta: {
title: item.meta.title,
icon: item.meta.icon,
url: item.meta.url,
component: (resolve)=>require(['@/' + component + '.vue'], resolve),
permissionList: item.meta.permissionList,
keepAlive: item.meta.keepAlive,
/*update_begin author:wuxianquan date:20190908 for:赋值 */
internalOrExternal: item.meta.internalOrExternal,
/*update_end author:wuxianquan date:20190908 for:赋值 */
},
}
由于qiankun路由不属于主应用,不需要执行component: (resolve)=>require(['@/' + component + '.vue'], resolve),,做如下改动
let menu = {
path: item.path,
name: item.name,
redirect: item.redirect,
hidden: item.hidden,
meta: {
title: item.meta.title,
icon: item.meta.icon,
url: item.meta.url,
// component: (resolve)=>require(['@/' + component + '.vue'], resolve),
permissionList: item.meta.permissionList,
keepAlive: item.meta.keepAlive,
/*update_begin author:wuxianquan date:20190908 for:赋值 */
internalOrExternal: item.meta.internalOrExternal,
/*update_end author:wuxianquan date:20190908 for:赋值 */
},
}
// 非乾坤应用加载,乾坤不执行
if (!item.qiankunApp) {
menu.component = (resolve) => {
require(['@/' + component + '.vue'], resolve)
}
}
二、子应用改造
1. vue2-webpack 项目改造
使用Vue-cli在项目根目录新建一个sub-vue的子应用,子应用的名称最好与父应用加载子应用时时的名称一致(这样可以直接使用package.json中的name作为output)。
-
vue.config.js- configureWebpack打包配置,用以输出乾坤子应用
- devServer的端口改为与主应用配置的一致,且加上跨域
headers和output配置。
// package.json的name需注意与主应用一致
const { name } = require('../package.json')
module.exports = {
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
// jsonpFunction: `webpackJsonp_${name}`, webpack5时开启
}
},
devServer: {
port: 3000, // 配置子应用端口
headers: {
'Access-Control-Allow-Origin': '*' // 调用子应用接口时跨域处理
}
}
}
2. ### 新增src/public-path.js
/* eslint-disable */
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3. ### src/router/index.js 兼容乾坤base地址(hash不需要)
import Vue from 'vue'
import Router from 'vue-router'
import { constantRouterMap } from '@/config/router.config'
//update-begin-author:taoyan date:20191011 for:TASK #3214 【优化】访问online功能测试 浏览器控制台抛出异常
try {
const originalPush = Router.prototype.push
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err)
}
} catch (e) {}
//update-end-author:taoyan date:20191011 for:TASK #3214 【优化】访问online功能测试 浏览器控制台抛出异常
Vue.use(Router)
//这里是history的写法
export default new Router({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? '/subapp1' : process.env.BASE_URL,
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap,
})
//这里是hash的写法
const createRouter = () =>
new Router({
mode: "hash",
// base: window.__POWERED_BY_QIANKUN__ ? "/#/app-vue/" : "/#/",
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes,
});
4. ### 改造main.js,引入上面的public-path.js,改写render,添加生命周期函数等,最终如下(按实际情况改造):
import "./public-path";
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App.vue";
import router from "./router";
import store from "./store";
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 以下为乾坤必要导出函数
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
router = null;
}
5.子应用菜单改造
通过判断当前项目window.POWERED_BY_QIANKUN,区分是单独运行还是qiankun中作为子应用,作为乾坤子应用时菜单直接从父应用获取。
从main.js 获取props
import './public-path'
import Vue from 'vue'
import App from './App.vue'
import Storage from 'vue-ls'
import router from './router'
import store from './store/'
import ENV from '../config'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.less' // or 'ant-design-vue/dist/antd.less'
import '@umetrip/ume-pc-header/dist/ume-pc-header.css'
import '@/permission' // permission control
import iViews from 'view-design/dist/iview.min.js'
import { USER_AUTH, SYS_BUTTON_AUTH } from '@/store/mutation-types'
import 'view-design/dist/styles/iview.css'
import {
ACCESS_TOKEN,
DEFAULT_COLOR,
DEFAULT_THEME,
DEFAULT_LAYOUT_MODE,
DEFAULT_COLOR_WEAK,
SIDEBAR_TYPE,
DEFAULT_FIXED_HEADER,
DEFAULT_FIXED_HEADER_HIDDEN,
DEFAULT_FIXED_SIDEMENU,
DEFAULT_CONTENT_WIDTH_TYPE,
DEFAULT_MULTI_PAGE,
} from '@/store/mutation-types'
import config from '@/defaultSettings'
import hasPermission from '@/utils/hasPermission'
Vue.config.productionTip = false
Vue.use(Storage, config.storageOptions)
Vue.use(Antd)
Vue.use(iViews)
Vue.use(hasPermission)
window._CONFIG['UmeCdnUrl'] = process.env.UME_CDN_URL || 'http://172.24.86.84/umePortalInternal/umeBlocks-sdk/'
window._FRAME_ENV = ENV === 'development' ? 'develop' : 'pro'
if (process.env.API_URL) {
window._CONFIG['BaseUrl'] = process.env.API_URL
window._CONFIG['Env'] = process.env.Env
window._CONFIG['AppName'] = config.app
}
let instance = null
function render(props = {}) {
const { container } = props
instance = new Vue({
router,
store,
mounted() {
store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar))
store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak))
store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
store.commit('SET_TOKEN', 'testToken')
store.commit('SET_MULTI_PAGE', Vue.ls.get(DEFAULT_MULTI_PAGE, config.multipage))
},
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app')
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped')
}
export async function mount(props) {
sessionStorage.setItem(USER_AUTH, props.auth)
sessionStorage.setItem(SYS_BUTTON_AUTH, props.allAuth)
store.commit('SET_QIANKUNMENU', props.menu)
render(props)
}
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
}
// 设置子应用的路由前缀
const routerBase = '/changLongApp'; // 与主应用中 activeRule 一致
router.beforeEach((to, from, next) => {
console.log('是的是的', routerBase, to.path)
// let path = `${routerBase}/${to.path}`
// to.path = path
next();
});
router.afterEach(to => {
// 根据路由的 meta.menu 设置子应用的菜单信息
const menu = to.meta.menu;
setGlobalState({ subappMenu: menu });
});
修改permisssion.js
/*
* @Creator: gpf
* @Date: 2021-08-06 13:56:07
* @LastEditors: gpf
* @LastEditTime: 2022-04-03 15:20:40
* @Description: file content
*/
import Vue from 'vue'
import router from './router'
import store from './store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { generateIndexRouter } from '@/utils/util'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
router.beforeEach(async (to, from, next) => {
NProgress.start() // start progress bar
if (store.getters.permissionList.length === 0) {
let res = null
if (window.__POWERED_BY_QIANKUN__) {
res = store.state.qiankunApp.qiankunMenu
store.commit('SET_PERMISSIONLIST', res)
} else {
const getPermissionList = await store.dispatch('GetPermissionList')
res = getPermissionList.menu || []
}
let constRoutes = []
constRoutes = generateIndexRouter(res)
// 添加主界面路由
store.dispatch('UpdateAppRouter', { constRoutes }).then(() => {
// 根据roles权限生成可访问的路由表
// 动态添加可访问路由表
router.addRoutes(store.getters.addRouters)
const redirect = decodeURIComponent(from.query.redirect || to.path)
if (to.path === redirect) {
next({ ...to, replace: true })
} else {
// 跳转到目的路由
next({ path: redirect })
}
})
} else {
next()
}
})
router.afterEach(() => {
NProgress.done() // finish progress bar
})
补充性vuex
// qiankunApp.js
export default {
namespace: true,
state: {
qiankunMenu: [], //从主应用继承的qiankun菜单
},
mutations: {
// 更新当前显示的qiankunApp
SET_QIANKUNMENU(state, data) {
state.qiankunMenu = data
},
},
}
三、qiankun:主应用和子应用之间的通信方式(五种)
1.initGlobalState (最常用)
参数:传入你维护的一个 state,类似 store 中的 state
返回:action 实例,并挂载了三个函数
1)、onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
2)、setGlobalState: (state: Record<string, any>) => boolean, 可以在应用中任何地方调用来修改全局状态,子应用想使用的话可以通过 props 把 action 传给子应用使用
3)、offGlobalStateChange: () => boolean,移除当前应用的状态监听,微应用 umount 时会默认调用
参考链接: blog.csdn.net/weixin_4405…
参考实例:www.jianshu.com/p/b352b62df…
2.localStorage/sessionStorage
3.通过路由参数共享
4.官方提供的 props
5.shared 方案: 就是父应用通过 vuex 或者 redux 正常使用维护一个 state,然后创建一个 shared 实例,这个实例提供对 state 的增删改查,然后通过 props 把这个 shared 实例传给子应用,子应用使用就行
四、qiankun的跳转方式
1.子应用内部的页面跳转(子应用独立开发和维护):子应用内部使用正常的vue-router或者react-router的路由跳转方式即可
// Vue 示例: this.$router.push('/new-path');
// React 示例: this.props.history.push('/new-path');
2.父应用控制子应用跳转:在qiankun中,父应用可以通过 prefetch 和 load 这两个生命周期钩子控制子应用的挂载和展示,进而控制子应用页面的跳转。
注意:父应用监听了popstate事件来捕捉浏览器的前进后退操作,并通过setGlobalState方法更新全局状态
子应用通过onGlobalStateChange钩子监听这些变化,从而实现父应用控制子应用页面跳转的目的
import { registerMicroApps, start } from 'qiankun';
// 注册子应用
registerMicroApps([
{
name: '子应用名称',
entry: '//localhost:子应用端口',
activeRule: '/parent-path',
container: '#yourContainer',
props: {
onGlobalStateChange: (state, prevState) => {
// 监听全局状态变化,根据变化控制子应用跳转
if (state.path === '/new-path') {
history.pushState(null, '', '/new-path'); // 触发子应用的onGlobalStateChange
}
},
},
},
// ... 其他子应用配置
]);
start(); // 启动qiankun
// 父应用中监听路由变化,并通过setGlobalState触发子应用的状态变化
window.addEventListener('popstate', () => {
// 获取当前路由或者状态
const currentPath = window.location.pathname;
// 使用setGlobalState通知子应用
microApp.setGlobalState({ path: currentPath });
});
注意:qiankun乾坤框架父子服务之间的跳转出现undefined路由:
五、qiankun的部署
场景 1:主应用和微应用部署到同一个服务器(同一个 IP 和端口)
场景 2:主应用和微应用部署在不同的服务器,使用 Nginx 代理访问
参考链接:qiankun.umijs.org/zh/cookbook
六、项目实战
1.常见错误问题: qiankun.umijs.org/zh/faq#%E5%…
注意:
2. vue3-vite 项目改造
参考网址:blog.csdn.net/qq_44278289…
qiankun父子应用改造: www.jianshu.com/p/4dbc491f5…
3.项目实战(gitee实例):
4.当前项目git地址:
git1.local.umetrip.com/Cooperation…
其中zuhu_gjx 为简单的demo
其中umebot_gjx为 标准平台为基准,舱音umeboot为子应用的项目
5.租户系统方案
以umeboot为基准(权限和路由/租户系统都走这个),标准平台为子应用(菜单都嵌入)
====》 用umeboot_v3doing 分支
umeboot 为主应用,vue3+vite+ts
项目登录密码: admin 123456
changLongSystem 为子应用, vue2+webpack
项目登录密码:wuyue001 1qaz@WSX
gwyong Asdf1234!!
guojunxiao Umetrip123!
所遇问题点:
一、解决微前端qiankun中子应用弹窗样式丢失的问题
function redirectPopup(container) {
// 子应用中需要挂载到子应用的弹窗className。样式class白名单,用子应用的样式。
const whiteList = ['ant-calendar-panel']
// 保存原有document.body.appendChild方法
const originFn = document.body.appendChild.bind(document.body)
// 重写appendChild方法
document.body.appendChild = (dom) => {
// 根据标记,来区分是否用新的挂载方式
let count = 0
whiteList.forEach((x) => {
const flag = dom.getElementsByClassName(x)
if (flag) count++
})
console.log(count)
if (count > 0 && container.container) {
// 有弹出框的时候,挂载的元素挂载到子应用上,而不是主应用的body上
container.container.querySelector('#app').appendChild(dom)
} else {
originFn(dom)
}
}
}
七、项目部署
建议:主应用和微应用都是独立开发和部署,即它们都属于不同的仓库和服务。
场景 1:主应用和微应用部署到同一个服务器(同一个 IP 和端口)
如果服务器数量有限,或不能跨域等原因需要把主应用和微应用部署到一起。
通常的做法是主应用部署在一级目录,微应用部署在二/三级目录。
微应用想部署在非根目录,在微应用打包之前需要做两件事:
- 必须配置
webpack构建时的publicPath为目录名称,更多信息请看 webpack 官方说明 和 vue-cli3 的官方说明 history路由的微应用需要设置base,值为目录名称,用于独立访问时使用。
部署之后注意三点:
activeRule不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。- 微应用的真实访问路径就是微应用的
entry,entry可以为相对路径。 - 微应用的
entry路径最后面的/不可省略,否则publicPath会设置错误,例如子项的访问路径是http://localhost:8080/app1,那么entry就是http://localhost:8080/app1/。
具体的部署有以下两种方式,选择其一即可。
场景 2:主应用和微应用部署在不同的服务器,使用 Nginx 代理访问
一般这么做是因为不允许主应用跨域访问微应用,做法就是将主应用服务器上一个特殊路径的请求全部转发到微应用的服务器上,即通过代理实现“微应用部署在主应用服务器上”的效果。
例如,主应用在 A 服务器,微应用在 B 服务器,使用路径 /app1 来区分微应用,即 A 服务器上所有 /app1 开头的请求都转发到 B 服务器上。
此时主应用的 Nginx 代理配置为:
/app1/ {
proxy_pass www.b.com/app1/;
proxy_set_header Host server_port;
}
具体网址见:qiankun.umijs.org/zh/cookbook…