作者:戴荣兴
-
项目背景
图书集成信息平台目前数十个子系统,目前的实现方式是在一个子系统选择页跳转对应的子系统,如果用户需要在不同子系统操作,得重复切换子系统。此方式就有点繁琐,所以将多个子系统资源整合在一个统一的系统中就很有必要。
-
实现方案
-
具体需求
- 实现后需要满足如下效果:
-
- 页面的功能和展示效果需要保持和原来一致。
- 改造的子系统的资源需要在一个菜单中显示。
- 新旧版能够随时切换。
- 如果用户不包含改造了的子系统资源,不应该跳转新界面。
- 已经接入了qiankun的子应用需要不受影响。
-
技术选型
vue3+qiankun+antd-vue
-
具体实现
-
-
系统资源处理
-
- 原有系统的菜单路由名称和层级关系是由前端维护的,统一工作项目中就需要把这部分关系在运维中心的资源管理处维护。在
运维系统-资源管理增加了父级菜单和菜单是否隐藏2个字段,并且维护了排序值和页面名称这2个原有字段。后续资源需要按照前端菜单层级关系来新增。 -
{ "id": 1, "name": "主系统", "children": [ { "id": 2, "name": "产品开发管理系统", "path": "pdm", "children": [ { "id": 21, "name": "B单管理", "path": "/module/pdm/sampleManage" "children": [ { "id": 211, "name": "打样任务查询", "path": "/module/pdm/sampleManage/sampleSearch" } ] }, { "id": 22, "name": "A单管理", "path": "/module/pdm/royaltyReview" // ... 其他子级资源 } ] }, { "id": 3, "name": "销售BI系统", "children": [ // ... 其他子级资源 ] } ] } -
-
主应用搭建
-
- 使用vue-cli搭建项目,生成下图所示项目:
- api:系统http请求相关
- assets:静态资源相关(图片、字体、全局样式等)
- components:公共组件
- global:引入全局方法、组件、样式等
- hooks:自定义的hooks
- layout:布局组件,包含左侧菜单和页面顶部组件
- micro:子系统相关配置,及注册微前端方法
- router:路由相关,其中router-guards.js是用来处理路由拦截的
- store:状态管理,本次使用pinia
- utils:公共方法
- views:页面组件
- 代码的重点是在micro和store目录:micro注册微前端相关的代码,store/app/index里处理子应用资源相关的代码。
-
// micro/index.js export function createMicroApps() { // 只能注册一次 if ( isRegister) return ; isRegister = true ; const appStore = useAppStoreWithOut(); // 根据配置生成子应用列表 let apps = microName.map((el) => { return { name: el.micro, // entry: el.micro == 'sell' ? 'http://localhost:8086/' : el.path + '/', entry: el.path + '/', container: '#microContainer', activeRule: getActiveRule(`#${el.modules}`), props: { container: '#microContainer', token: getToken(), }, }; }); // 数据字典系统另做处理 apps = apps.concat({ name: 'dataDic', entry: isProduct ? 'https://*********/dataDic/' : 'http://test******/dataDic/', container: '#microContainer', activeRule: getActiveRule('#/dataDic'), props: { container: '#microContainer', token: getToken(), }, }); registerMicroApps(apps, { beforeLoad: [ (app) => { appStore.setQiankunLoading(true ) ; // 资源加载时的loading }, ], beforeMount: [ (app) => { appStore.setQiankunLoading(false ) ; }, ], afterUnmount: [ (app) => { appStore.setQiankunLoading(false ) ; }, ], }); } -
// store/modules/app/index.js // 登录后根据当前是否新老系统,做不同的处理 // 首次登录根据当前角色所拥有的的资源来判断是否跳转新的界面:如果其资源不包含在本次改造的子系统中,则跳转manage页面,反之直接跳转新界面 // 下次登录则取本地存储的跳转方式 // systemType: '1' 新的界面 // systemType: '2' 旧的manage界面 // isAuto用来控制是否需要同时加载4级资源。 getResource(isAuto) { return new Promise(async (resolve, reject) => { let systemType = getSystemType(); if (systemType === '2') { return resolve(systemType); } const { micro } = await this.getSystemsList(); if (isUndef(systemType) && micro.length === 0) { setSystemType('2'); return resolve('2'); } // 获取子系统资源 const resources = await getMicroResources(micro); if (resources?.length === 0) { setSystemType('2'); return resolve('2'); } let asyncResources = resources.map((el, index) => { let routes = []; let system = micro[index]; if (el?.data?.status === 200) { routes = el.data.items; } return { routes: routes, system: system, }; }); if (isAuto) { await this.getResourceLevel4(micro, asyncResources); } else { this.getResourceLevel4(micro, asyncResources); } this.microList = createTreeResources(asyncResources); setResources(this.microList); return resolve('1'); }); }, - 在layout/microContainer.vue文件中增加子应用容器,在组件挂载之后加载子应用。这个组件需要一直存在,所以在代码中用v-show来控制显示隐藏。相关问题
-
// prefetch: false,去掉预加载 // sandbox开启样式隔离,加上改配置后,会在子系统的css选择器前面加上 // div[data-qiankun="子应用name"] start({ prefetch: false, sandbox: { experimentalStyleIsolation: true } }); -
-
子系统处理
-
- 在主应用中,给资源统一加上了系统前缀来区分各个子系统,为了是子系统的路由匹配生效,在各个子系统中需要加上路由前缀。以下用基础数据项目为例:
- 资源处理
-
// router/index.js // 给路由增加前缀的方法 export function routerBaseUrl(list) { if(!isQiankun) return; list.forEach(item=>{ let path = item.path; let redirect = item.redirect; if(path && path.indexOf('/')===0){ item.oldPath = path; item.path = '/module/baseData' + path; } if(redirect && redirect.indexOf('/')===0){ item.oldRedirect = redirect; item.redirect = '/module/baseData' + redirect; } if(item.children && item.children.length){ routerBaseUrl(item.children) } }) } // 重新设置子系统layout组件,作为子应用时不需要左侧菜单和顶部header function setLayout(list) { if (!isQiankun) { return; } list.forEach((item) => { if (item?.component?.name === 'layout') { item.component = microLayout; } if (item?.children?.length) { setLayout(item.children); } }); } - 跳转处理
-
// 需要处理三种情况的跳转 // 1. vue-router跳转 function nextPath(next, path) { if (isQiankun) { if (typeof path === 'object' && !path.path.includes('/module/baseData')) { path.path = `/module/baseData${path.path}`; next(path); return; } if (typeof path === 'string' && !path.includes('/module/baseData')) { next(`/module/baseData${path}`); return; } } next(); } // 2. window.open跳转 (function () { if (isQiankun) { let winOpen = window.open; window.open = function () { let arg = [...arguments]; let url = arg[0]; let isRouterPath = arg[1]; if (isRouterPath == 'micro' && url?.startsWith('#')) { url = `#${url.replace('#', '/module/baseData')}`; winOpen(url); } else { winOpen(...arg); } }; } })(); // 3. a标签获取vue-link标签打开新界面跳转 Vue.prototype.$_qiankun = function(link) { if (!isQiankun || !link || link.includes('/module/baseData')) return link; let str = '/module/baseData'; if (link.includes('#')) { str = '/#/module/baseData'; } return link.replace(/#?/, str); }; - 导出子应用钩子函数
-
let instance = null ; const render = (e) => { if ( isQiankun) { const { container } = e; instance = new Vue({ el: container ? container.querySelector('#app') : '#app', router, store, template: '<App/>', components: { App }, }); } else { const { appContent, loading } = e; if ( !instance) { instance = new Vue({ el: '#app', router, store, template: '<App/>', components: { App }, }); } else { // if (!appContent) return; store.commit('SET_APP_CONTENT', appContent); store.commit('SET_APP_LOADING', loading); } } }; if ( !isQiankun) { //第一次调用初始主应用 render({}); const genActiveRule = (routerPrefix) => { return ( location) => location.hash.startsWith(routerPrefix); }; let apps = [ { name: 'dataDic', entry: process.env.VUE_APP_MICRO_URL, render: render, activeRule: genActiveRule('#/dataDic'), props: { token: getToken(), }, }, ]; //注册的子应用 参数为数组 registerMicroApps(apps, { beforeLoad: [(app) => {}], beforeMount: [(app) => {}], afterUnmount: [(app) => {}], }); start(); } export async function bootstrap() { console.log('baseData app bootstraped'); } export async function mount(props) { render(props); setToken(props.token); } export async function unmount(props) { if ( instance) { instance.$destroy(); instance.$el.innerHTML = ''; instance = null ; } }
-