前言
架构使用场景:
为了实现模块功能划分通常项目会以目录作为模块划分依据,但是对于一个巨无霸级别的项目,这种模块划分方式依然显得尤为冗余,目录结构也尤为复杂,同时对于不同项目组开发的功能将会耦合在一个项目中,导致项目模块功能部署不能灵活独立的上线,为了解决这个问题,所以要把巨无霸项目按业务应用划分成多个项目,此处整合项目称为主应用,被整合应用称为子应用,这样就能实现巨无霸项目的代码按业务划分并由负责团队独立开发,独立部署和管理,传统常用iframe解决父子应用的整合,但是大部分浏览器不能支持iframe的cookie共享,导致项目sso失效,并且iframe还不能很好的解决父子应用业务信息的传递,所以选用qiankun来解决父子应用的整合。
架构原理
通过将子应用的router和vuex注册到主应用的router和vuex便可实现在主应用中切换到子应用页面和管理子应用vuex数据,主应用index.html只建立单一子应用容器id设为container,结合qiankun功能地址改变即可触发不同子应用资源的加载,此处切换加载特指样式图片等静态资源,子应用的router和vuex配置主要在第一次加载时注册使用,由于生产环境浏览器具有静态资源缓存功能,所以二次切换加载时间可忽略不计,仅在第一次加载时需等待片刻。
router注册原理
const createRouter = () => new Router({
scrollBehavior: () => ({ y: 0 })
})
const router = createRouter();
router.addRoute(constantRoute);
vuex注册原理
const store = new Vuex.Store({
modules: {},
getters
})
store.registerModule(appName, Object.assign({
namespaced: true,
modules: {}
}, constantStores));
源码解析
主应用配置
// qiankun.js
import 'whatwg-fetch';
import 'custom-event-polyfill';
import 'core-js/stable/promise';
import 'core-js/stable/symbol';
import 'core-js/stable/string/starts-with';
import 'core-js/web/url';
import Vue from 'vue';
import { TokenKey } from '@/utils/auth';
import { registerMicroApps, start, initGlobalState } from 'qiankun';
import store from '@/store';
import router from '@/router';
registerMicroApps([
{
name: 'myapp',
entry: 'http://122.0.0.0:8081,
container: '#container',
activeRule: '/myapp/#/tfp/microapp',
props: {
Vue,
store,
router,
token: sessionStorage.getItem([TokenKey])
}
}
],
{
beforeLoad: [
app => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name)
}
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
}
],
afterUnmount: [
app => {
console.log('[LifeCycle] before unmount %c%s', 'color: green;', app.name)
}
]
})
start({ prefetch: 'all' });
let action = initGlobalState({
appName: '',
constantRoutes: null,
constantStores: null
});
action.onGlobalStateChange((state, prev) => {
let { appName, constantRoutes, constantStores } = state;
let constantRoute = {
path: '/tfp',
name: 'Tfp',
component: () => import('@/layout'),
children: constasntRoutes.map(route => {
return Object.assign(route, {
path: `${appName}${route.path}`,
name: route.name?route.name.split('_')[0]: ''
})
})
}
})
router.addRoute(constantRoute);
if(!store.state[appName]) {
store.registerModule(appName, Object.assign({
namespaced: true,
modules: {}
}, constantStores));
Object.keys(constantStores.modules).forEach(key => {
store.registerModule([appName, key], constantStores.modules[key]);
})
}
子应用配置
// router/index.js
import Vue from 'vue';
import Router from 'vue-router';
export const constantRoutes = [
homeRouter,
...otherRouter
]
const createRouter = () => new Router({
mode: 'hash',
scrollBehavior: () => ({
y: 0
})
})
const router = createRouter();
export default router;
// store/index.js
const modulesFiles = require.context('./modules', true, /\.js$/);
const modlues = modulesFiles.keys().reduce((module, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1');
const value = modulesFiles(modulePath);
modules[moduleName] = value.default;
return modules;
}, {})
export const constantStores = {
state,
mutations: {},
getters,
modules
}
const store = new Vuex.Store(constantStores);
export default store;
import './public-path';
import { constantStores, default as store } from '@/store';
import { cosntantRoutes, default as router } from '@/router';
import '@/public/styles/app.scss';
import '@/public/styles/index.scss
function render() {
Promise.all([
import('vue'),
import('@/App'),
import('element-ui'),
import('vuex-i18n'),
import('vue-clipboard2'),
import('@/permission')
]).then(([Vue, App, ElementUI, VuexI18n, VueClipboard]) => {
[Vue, App, ElementUI, VuexI18n, VueClipboard] = [Vue.default, App.default, VuexI18n.default, VueClipboard.default];
Vue.use(VueClipboard);
Vue.use(ElementUI);
Vue.use(VuexI18n.plugin, store);
Vue.config.production = false;
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
})
}
if(!window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
}
export async function mount(props) {
props.Vue.prototype.constant = Constant;
<!-- custom code --!>
props.setGlobalState({
appName: 'maicorApp',
constantRoutes,
constantStores
})
props.onGlobalStateChange((state, prev) => {
console.log(state)
})
}
export async function unmount(){
}
'
样式隔离问题解决方案
样式文件处理
npm install postcss-prefix-selector
// postcss.config.js
let appName = require("./package.json").name;
module.exports = {
'plugins': {
'postcss-prefix-selector': {
prefix: `.${appName}`,
transform(prefix, selector, prefixedSelector, filePath, rule) {
if(filePath.incudes("app.scss")) {
return selector;
} else {
return `${prefix} ${selector}, ${prefix}${selector}`;
}
}
},
'autoprefixed': {}
}
}
vue文件处理
// ClassPrfixLoader.js
let appName = require("../../package.json").name;
module.exports = function(content, map, meta) {
return content.replace(/class=\"(.*)\"/, `class=\"${appName} $1\"`);
}