vue-cli+qiankun微前端架构应用

198 阅读2分钟

前言

架构使用场景:

为了实现模块功能划分通常项目会以目录作为模块划分依据,但是对于一个巨无霸级别的项目,这种模块划分方式依然显得尤为冗余,目录结构也尤为复杂,同时对于不同项目组开发的功能将会耦合在一个项目中,导致项目模块功能部署不能灵活独立的上线,为了解决这个问题,所以要把巨无霸项目按业务应用划分成多个项目,此处整合项目称为主应用,被整合应用称为子应用,这样就能实现巨无霸项目的代码按业务划分并由负责团队独立开发,独立部署和管理,传统常用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\"`);
}