为了业务在原有系统中,集成新项目,我们经常会使用qiankun这套微前端解决方案,来解决集成诉求。
vue2,vue3在qiankun中的使用实际差别不大,主要路由处理上有些区别,也会在文中指出,会以【vue3】作为标识。
通过这边文章,可以了解到,如何在vue(2/3)技术栈里使用微前端集成新老项目,主要内容包括:
- 不同的路由模式(history, hash),主、子应用分别在开发、生产环境的代码改造、部署;
- 在进入子应用前,如何处理鉴权逻辑;
- 主、子应用样式隔离的处理;
- 主、子应用间的通信;
- 在应用某个路由下加载微应用;
- QA,开发过程中遇到的问题汇总。
常用API
下面是常用的两个qiankun API,已给出链接,可以在官网查看具体用法:
1、 不同的路由模式,主、子应用分别在开发、生产环境的代码改造、部署;
a. 主、子应用路由都为hash模式的改造
开发环境,主应用改造:
import { registerMicroApps, start } from 'qiankun';
registerMicroApps(
[
{
// 微前端应用名
name: 'app1',
// 微前端启动地址
entry: '//localhost:8100/',
// 微前端挂载dom
container: '#app',
// 微前端触发路由
activeRule: '#/app1',
// 主应用向子应用传递的静态值
props: {
name: 'yuxiaoyu',
},
},
],
);
start();
开发环境,子应用改造:
// 入口文件main.js
// 子应用并不用引入qiankun,只要暴露响应的声明周期钩子给主应用使用就ok
// 挂载实例
function render(props: any = {}) {
const { container } = props;
app = createApp(App);
app.use(router);
app.use(store);
router.isReady().then(() => {
app.mount(container ? container.querySelector('#container') : '#container');
});
}
// 微应用在主应用运行时,主应用会在微应用中挂载window.__POWERED_BY_QIANKUN__,可以用于判断环境
// 官方提供了下面这个webpack注入publicPath的方法, 开发环境我们这么使用,生产改到vue.config.js中,后面再介绍。
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 如果是独立运行 window.__POWERED_BY_QIANKUN__=undefined 直接render
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 最后暴露的三个方法是固定的,加载渲染以及销毁
export async function bootstrap() { }
export async function mount(props: any) {
render(props);
}
export async function unmount() {
app.unmount();
app._container.innerHTML = "";
app = null;
// 这里reload的原因: 因为在项目中,微应用和主应用没有共用导航等信息
// 相当于两个独立的页面,所以就共用了<div id="app">
// 在卸载微应用后,为了再把主应用渲染出来,就重新reload了一遍。
location.reload();
}
// router改造
const router = createRouter({
history: createWebHashHistory(),
routes,
});
// 因为主应用在激活子应用时,有一个activeRule前缀,所以在hash模式下,我们需要给每个一级路由都添加activeRule的前缀,主应用为'#/app1',那么子应用前我们就加'/app1'就可以了。
export default [
{
path: '/app1/fujidaohang',
redirect: '/app1/fujidaohang/zijidaohang',
component: BothLayout,
name: 'fujidaohang',
meta: {
title: '父级导航',
navPosition: 'top',
},
children: [
{
path: 'zijidaohang',
component: () => import('@/apps/fujidaohang/views/zijidaohang.vue'),
name: 'zijidaohang',
meta: {
title: '子级导航',
navPosition: '',
},
},
],
},
{
path: '/app1/course',
component: Course,
name: 'course',
children: [],
}
];
// vue.config.js改造
const packageName = require('./package.json').name;
module.exports = {
...
// 用于主应用识别子应用,固定写法
configureWebpack: {
output: {
library: 'app1',
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
}
}
生产构建部署:
构建部署可以选择两种方式:
同域名、不同域名部署。
同域名部署:
1.可以考虑将子应用打包放在static路径下,作为主应用的静态资源引入。这种处理方式的适用场景包含:子应用不需要独立访问、子应用不需要独立部署。这种方式的好处在于,不用运维配合去Nginx配置,本地打包,并使用原有的部署方式。当然,它同时也失去了微前端,主、子独立部署的优点。
2.还有一种方式,可以将打包后的静态资源在同一台服务器进行部署,然后针对于项目新增部署和Nginx相关配置。在主应用访问的时候可以通过域名加载,也可以通过相对路径进行加载(因为我们同台机器部署)。这个相对于上面的方案,增加了运维成本,但保留了主子独立发布的特性。
不用域名部署
这里的方案和上面的2差不多,区别在于资源可能放在不同的服务器上,所以只能通过域名进行资源的加载(也就是registerMicroApps中配置的entry需要是域名,不能是相对路径了,和开发环境差不多),记得处理跨域。
当然也可以根据需要选用相应的部署方式,官方都有给出详细的介绍: 如何部署。
生产构建,主应用改造:
// main.js
// 注册微应用
registerMicroApps(
[
{
name: 'app1',
// 路径改为部署后,微应用要存放在主应用的目录,其余不变
entry: '/static/index.html',
container: '#app',
activeRule: '#/app1',
props: {
name: 'kuitos',
},
},
],
);
生产构建,微应用改造:
// main.js
// 去掉qiankun的__webpack_public_path__注入
// if (window.__POWERED_BY_QIANKUN__) {
// __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }
// vue.config.js
module.exports = {
...
// 这里新增打包资源存放路径,与上面主应用相对应
publicPath: '/static'
}
b. 主、子应用路由都为history模式的改造
开发模式:
这里我们和hash-dev模式进行对比,只列举差异的部分。
// 主应用
// main.js, 注册微应用有变化
registerMicroApps(
[
{
name: 'app1',
entry: '//localhost:8101',
container: '#app',
// 改变点:激活路由由hash模式变为history模式
activeRule: '/app1',
props: {
name: 'yuxiaoyu',
},
},
],
);
// router.js
const router = new VueRouter({
// 由hash改为history模式
mode: 'history',
base: process.env.BASE_URL,
routes,
});
// 子应用
// router/index.js
// 【vue3】因为子应用为vue3版本,这里是vue2和vue3本身路由处理上的区别,我们只是使用了vue3的写法加上了需要的前缀app1
const router = createRouter({
// 改为history模式,并且加activeRule前缀
history: createWebHistory('/app1'),
routes,
});
// router.js
// 去掉这里的app1前缀
export default [
{
path: '/fujidaohang',
redirect: '/fujidaohang/zijidaohang',
component: BothLayout,
name: 'fujidaohang',
meta: {
title: '父级导航',
navPosition: 'top',
},
children: [
{
path: 'zijidaohang',
component: () => import('@/apps/fujidaohang/views/zijidaohang.vue'),
name: 'zijidaohang',
meta: {
title: '子级导航',
navPosition: '',
},
},
],
},
{
path: '/course',
component: Course,
name: 'course',
children: [],
}
];
生产构建部署:
这里与hash模式改动点是一致的,只描述不同点。
主应用改造:
// main.js
// 注册微应用
registerMicroApps(
[
{
// 路径改为部署后,微应用要存放在主应用的目录,其余不变
entry: '/static/index.html',
},
],
);
微应用改造:
// main.js
// 去掉qiankun的__webpack_public_path__注入
// if (window.__POWERED_BY_QIANKUN__) {
// __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }
// vue.config.js
module.exports = {
...
// 这里新增打包资源存放路径,与上面主应用相对应
publicPath: '/static'
}
2、在进入子应用前,如何处理鉴权逻辑
当进入微应用时,我们鉴定微应用是否登录时,我们可以考虑在微应用做鉴权,也可以在主应用做鉴权。
这里介绍一种在主应用内鉴权的方案。
qiankun里在注册微应用时,registerMicroApps提供了第二个参数,lifeCycles- 可选,全局的微应用生命周期钩子
registerMicroApps(
[{...}], {
// 这里采用在进入微前端之前进行鉴权,确保进入微前端时,已经登录。
beforeLoad: [
() => {
if (!auth.check()) {
vm.$router.push(loginPath);
}
},
],
},);
3、主、子应用样式隔离的处理
a. 主、子应用都使用antd
这里官方提供了修改antd前缀的方法,如将ant改为dida-ant, 如何确保主应用跟微应用之间的样式隔离。
这里ant-design-vue文档里虽然没有提供prefixCls的参数,但是可以使用的,源码里相应的处理。
b. 主应用自定义样式与子应用冲突
这里我们可以采用官方提供的start(options?),将微应用放入浏览器所支持的shadow dom中。
start({
sandbox: {
// 主应用 & 子应用样式隔离
strictStyleIsolation: true, // 放入shadow dom中
}
});
这里我们可以看到微前端被放入了shadow-root里,对于shadow dom可以通过这里进行了解。
这样隔离后,在我们使用ant design这种外部库时会有一些问题,例如popup组件,原本实现是挂在document.body中的,我们将子应用放到了shadow dom中,那就需要将popup也挂进去。ant design官方提供了方法,搜索getPopupContainer;
4、 主、子应用间的通信
a. 静态值传递
我们可以通过注册微应用时,通过props参数进行数据的传递,在mounted和render生命周期中,可以拿到props数据。
registerMicroApps(
[
{
name: 'app1',
entry: '//localhost:8100',
container: '#app',
activeRule: '/app1',
props: {
id: 1,
},
},
],
);
b. 通信机制
qiankun官方提供了创建主、子应用通信的定义方法。initGlobalState(state),文档链接,初始化后会返回三个方法,onGlobalStateChange,setGlobalState,offGlobalStateChange,分别是监听,设置和移除。
// 主应用
// 初始化 state
// 调用initGlobalState后,会挂在props中进行传递
const initialState = {
userInfo: {}, // 用户信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((newState, oldState) => {
// newState: 变更后的状态; oldState 变更前的状态
console.log('mainapp: global state changed', newState, oldState);
});
actions.setGlobalState({
userInfo: {
name: 'Zhangsan',
},
});
actions.offGlobalStateChange();
export async function mount(props: any) {
render(props);
// props 会注入onGlobalStateChange、setGlobalState方法。
console.log('props :>> ', props);
app.config.globalProperties.$onGlobalStateChange =props.onGlobalStateChange;
app.config.globalProperties.$setGlobalState = props.setGlobalState;
props.onGlobalStateChange((newState: any, oldState: any) => {
// newState: 变更后的状态; oldState 变更前的状态
console.log('microapp: global state changed', newState, oldState);
});
window.document.addEventListener('click', () => {
props.setGlobalState({
userInfo: {
name: Math.random(),
},
});
});
}
5、在应用某个路由下加载微应用
背景:
vue3作为主应用, 原系统作为子应用(vue2)。也就是vue3(主)接vue2(子)。在已有路由或组件中引入子应用。 如图:
需要将子应用嵌套在layout组件内
官方提供了一种在应用某个路由下面接入方法。
官方提供的方法是在vue2为主应用时接入。所以这里写法上有会一些区别。
{
path: '/huodongguanli',
redirect: '/trace_micro/project/projectList',
component: SideLayout,
name: 'huodongguanli',
meta: {
title: '活动管理',
navPosition: 'side',
icon: () => {
return h(ContactsOutlined);
},
},
children: [
{
path: '/trace_micro/project/projectList', // 需要嵌套在二级菜单,且路径不需要一级的path拼接的,这里写绝对路径(这里为业务逻辑,不用关注)
component: { default: '' }, // 因为这里只是为了声明一个menu项,并不需要组件,(组件内容在子应用中),这里给个含有default的空对象
name: 'lianluliebiao',
meta: {
title: '链路列表',
navPosition: 'side',
},
},
{
path: 'chuangjianjiangzuo', // 主应用中的路由 正常写就可以
name: 'chuangjianjiangzuo',
meta: {
title: '创建讲座',
navPosition: 'side',
},
},
],
},
// 针对于不用在主应用里菜单显示出来的路由,都进行模糊匹配
// 【vue3】
// 因为vue3中,vue-router4.x[取消了通配符的写法](https://next.router.vuejs.org/zh/guide/migration/index.html#%E5%88%A0%E9%99%A4%E4%BA%86-%EF%BC%88%E6%98%9F%E6%A0%87%E6%88%96%E9%80%9A%E9%85%8D%E7%AC%A6%EF%BC%89%E8%B7%AF%E7%94%B1),所以在匹配的`path`的写法上会有一些区别:
{
path: '/trace_micro/:pathMatch(.*)*', // 【重要】这里我们采用router4.x中提供的写法去匹配子应用的路由
hidden: true,
component: SideLayout,
name: 'projectOperate',
meta: {
title: '创建链路',
navPosition: 'side',
},
}
其余的使用方法(start和挂载节点等)与官方提供的一致。
6、常见问题
官方有提供常见问题文档,遇到问题时,可以先去这里查看一下是否有相应的匹配解决方案。
这里也列举几个,我在接入项目时候遇到的其它的坑:
1、与主应用共用同一节点,如#app。在返回主应用时,主应用不显示。
这里因为我们在初始化微前端时,把#app中的内容替换成了子应用的数据,在返回时,并没有重新挂载,目前可以通过reload简单处理。
2、与主应用共用同一节点,如#app。dom看起来挂载成功了,但是样式都没有。
这个和第一个又是不同的情况,主要是因为我们把子应用的dom结构直接挂载到了主应用上,微前端这一套都没有生效。在mount生命周期中,我们可以拿到props,这里面挂载了一个container属性,就是挂载微前端的dom节点,可以通过props.container.querySelector('#app')去挂载子应用的vue。
3、子应用跳转主应用,$router.push不生效
是因为我们主、子应用使用的router并不是一个实例,注册的路由也不一样,所以子应用并不能匹配到主应用的路由,可以使用history.push或者location.href进行跳转。
4、在打开主应用打开子应用时,会报js加载不到,或者<等语法不对的问题
一般由于主应用引入子应用的路径不对,检查路径是否能引到子应用资源。
5、主应用引入babel-polyfill时,有时候会babel-polyfill引入两次的问题。
这个先检查一下是否子应用也单独引入babel-polyfill了,如果引入了就都统一放到一处去引用。
现在大部分新的应用都是用vue-cli进行生成,并不会单独引入babel-polyfill,那如果报这个错了,就检查一下是不是,子应用的路径有问题,引入的资源不对。报了引两次的问题,可能也是由于子应用没有引到的问题。和babel-polyfill本身引入没有关系。
6、关于样式隔离
上面也有给出样式隔离的方案,但方案本身还是有一些问题的。比如我们提到的popup挂在document.body上的问题,需要处理。可能还会有一些其它的问题要处理。
最简单有效的方法还是在主、子应用中,尽量使用style scoped进行组件级别的样式隔离。对于使用同一组件库,不同配置,可以通过修改组件、样式前缀解决。
以上就是微前端(qiankun)在vue2、3中的实战开发,希望对各位有所帮助。