2022-03-08-微前端qiankun实践

微前端方案

1、什么是微前端?为什么微前端方案?

2016年底在ThoughtWorks 技术雷达中提出,将多个独立的前端应用聚合在一起组成一个全新的应用,各个应用之间彼此独立,互不干扰,应用内高内聚,应用间松耦合;对于用户来说就是一个完整的应用,但是在技术角度上是由一个个独立的应用组合组合而成;

  • 拆分、聚合
  • 源码交付,自由组合 – 拆
  • 应用接入 – 合
  • 需求体量较大,避免巨石应用 – 拆

2、什么是qiankun、为什么qiankun?

  • qiankun 是一个基于 single-spa 的微前端实现库,解决了应用加载,以及js/css污染问题,更简单好用。
  • 方案成熟可靠、口碑较好,部门内部也有不少项目生产实践
  • 接入简单,不会对原有工程造成很大“破坏”。
  • 避免巨石应用,方便应用接入
  • 与emp 相比更适合当前技术底座

3、带来怎样的收益,价值在哪里?

  • 技术栈无关、易扩展:主框架不限制接入应用的技术栈,项目无限扩展
  • 独立开发、独立部署:微应用仓库独立,前后端可独立开发,独立部署互不干扰
  • 增量升级:微前端是一种非常好的实施渐进式重构的手段和策略
  • 对团队来说,快速实践产出文档,形成组织过程资产

4、缺点

  • 依赖不共享,重复加载资源
  • 通过主应用共享依赖,子应用独立运行时需要启动主应用

1. qiankun本地实践

1.1 SwitchyOmega+whistle代理

前言:前端开发过程中经常会遇到需要对请求做拦截,然后通过代理工具或者其他方式转发到指定的服务器上。最常见的场景就是在单点登录的情况下,需要用域名来访问,带上登录token,后端才能识别成合法的用户请求。下文介绍一下chrome插件SwitchyOmega和基于nodejs的代理工具whistle的使用方法

  • whistle

    whistle(读音[ˈwɪsəl],拼音[wēisǒu])基于Node实现的跨平台web调试代理工具,类似的工具有Windows平台上的Fiddler,主要用于查看、修改HTTP、HTTPS、Websocket的请求、响应,也可以作为HTTP代理服务器使用,不同于Fiddler通过断点修改请求响应的方式,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能。

    # 安装
    npm install -g whistle
    # 启动
    w2 start
    
  • SwitchyOmega

作为chrome的浏览器插件,安装后可以随时方便地使用。支持设置不同的场景模式,并且可以按照预定规则进行自动切换场景。直接在chrome网上应用店搜索就可以安装:

项目分为三大层,底层调度层,核心层和业务层。

  • 底层调度层:主要借助single-spa做应用的调度,加载,卸载;qiankun主要提供js隔离,样式隔离,以及应用注册和应用通信功能等上层功能。和业务无关
  • 核心层:主要接入qiankun框架,抽离出系统中的公共模块,例如登录注册,头部header、侧边菜单sidebar等功能,负责鉴权、全局事件处理,应用分发等
  • 业务层:主要负责业务,根据主系统的要求,做系统的改造,接入微前端

1.2 项目配置

将普通的项目改造成 qiankun 主应用基座,需要进行四步操作:

  • 当前qiankun版本 “qiankun”: “^2.4.10”,
    1. 创建微应用容器 - 用于承载微应用,渲染显示微应用;
    2. 注册微应用 - 设置微应用激活条件,微应用地址等等;
    3. 创建子应用 - 微前端接入配置;
    4. 启动 qiankun - 主应用基座中启动。

1.2.1 创建微应用容器

app.vue 中通过$route.name判断是否为项目路由

    <section v-show="!$route.name" id="qianKunBox" class="in-qiankun"></section>
    <router-view v-show="$route.name"></router-view>

1.2.2 注册微应用

  • vue.config.js 改造配置文件 用于给主应用接入时进行注册,对应主应用注册子应用时的 registerMicroApps 中的某一个自入口的 name ``` javascript const packageName = require(‘./package.json’).name

module.exports = { // … other configs configureWebpack: { // 让主应用能正确识别微应用暴露出来的一些信息 output: { library: ${packageName}, libraryTarget: ‘umd’, jsonpFunction: webpackJsonp_${packageName} } }, // … other configs }


* src/qiankun/config.js 子应用注册信息
``` javascript
const { VUE_APP_ROUTER_BASE } = process.env;

const props = {
  header: false,
  sidebar: false,
  isParentSlider: true,
};
export const subAppRouter = [
  {
    name: 'hrss-web-portal',
    entry: `${VUE_APP_ROUTER_BASE}/sub/portal/?t=${Date.now()}`,
    container: '#qianKunBox',
    activeRule: `${VUE_APP_ROUTER_BASE}/portal`,
    props: {
      ...props,
      baseRouter: `${VUE_APP_ROUTER_BASE}/portal`,
      isParentSlider: false,
    },
  },
];
  • 注册子应用信息 ``` javascript import ‘core-js/stable’; import ‘regenerator-runtime/runtime’; import ‘@/core’; import Vue from ‘vue’; import { registerMicroApps, setDefaultMountApp, start } from ‘qiankun’; import App from ‘./app.vue’; import router from ‘./router’; import store from ‘./store’; import { topbarConfig, sideBarConfig } from ‘./config’; import { dealApps } from ‘@/qiankun’; import MUTATIONTYPES from ‘./store/mutations-type’;

const { VUE_APP_ROUTER_BASE } = process.env; const storeGlobalTypes = Vue.prototype.$storeGlobalTypes;

let appMain = null; // 初始化应用渲染 const initRender = (props = {}) => { const { userInfo } = props; if (!appMain) { appMain = new Vue({ router, store, render: (h) => h(App), }).$mount(‘#app’); } // 此处省略大段设置 用户信息、菜单代码… return dealApps(appMain); }; // 注册微前端 function register(apps) { registerMicroApps(apps, { 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); setTimeout(() => { store.commit(index/${MUTATIONTYPES.SET_APP_LOAD}, true); }, 1); console.log(‘main beforeMount ‘, app); store.commit(global/${storeGlobalTypes.SET_SIDEBAR_STATUS}, app.props.isParentSlider); // 设置顶栏 }, ], afterUnmount: [ (app) => { console.log(‘[LifeCycle] after unmount %c%s’, ‘color: green;’, app.name); }, ], }); // 设置默认打开的微前端 应用 TODO 如果路径存在则跳转,否则走默认路径 setDefaultMountApp(${VUE_APP_ROUTER_BASE}/portal);

// 启动qiankun start({ sandbox: { strictStyleIsolation: false }, }); }

const apps = initRender({ userInfo: { isLogin: true, info: { name: ‘nickname’, }, // 用户中心可操作列表 menu: [], }, }); // 微前端主应用,微应用注册 register(apps);

#### 1.2.3 创建子应用

* 子应用导出钩子函数
``` javascript
// eslint-disable-next-line require-await
export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

// eslint-disable-next-line require-await
export async function mount(props) {
  console.log('[vue] vue app mount', props);
  initRender(props);
}

// eslint-disable-next-line require-await
export async function unmount() {
  instance.$destroy();
  instance = null;
}

2. 依赖共享优化

虽然qiankun微前端方案,能带来应用独立部署独立开发的好处,但是独立部署时,各子应用将重复引入相同依赖包,使项目整体包体积。这里可以将chunk-vendor中的依赖包通过webpack的externals忽略,再通过外链方式引入html模板中,依赖包可通过copy-webpack-plugin从node_modules取出。

  1. 复用主应用公共依赖
  2. 子应用间共享依赖

具体实现

1. 主应用提取”公共“依赖

    externals: {
      vue: 'Vue',
      vuex: 'Vuex',
      axios: 'axios',
      'tdesign-vue': 'TDesign',
      'vue-router': 'VueRouter',
      // vue: {
      //   commonjs: 'vue',
      //   commonjs2: 'vue',
      //   amd: 'vue',
      //   root: 'Vue', // 外链cdn加载挂载到window上的变量名
      // },
    plugins: [
      new CopyWebpackPlugin({
        patterns: [
          { from: path.join(__dirname, '/node_modules/vue/dist/vue.min.js'), to: './lib/vue.min.js' },
          {
            from: path.join(__dirname, '/node_modules/vue-router/dist/vue-router.min.js'),
            to: './lib/vue-router.min.js',
          },
          { from: path.join(__dirname, '/node_modules/vuex/dist/vuex.min.js'), to: './lib/vuex.min.js' },
          { from: path.join(__dirname, '/node_modules/axios/dist/axios.min.js'), to: './lib/axios.min.js' },
          { from: path.join(__dirname, '/node_modules/tdesign-vue/dist/tdesign.min.js'), to: './lib/tdesign.min.js' },
          { from: path.join(__dirname, '/node_modules/tdesign-vue/dist/tdesign.min.css'), to: './lib/tdesign.min.css' },
        ],
      }),

2. 主应用外链引入

    <script ignore src="<%= VUE_APP_CDN_PATH%>/lib/vue.min.js"></script>
    <script ignore src="<%= VUE_APP_CDN_PATH%>/lib/vue-router.min.js"></script>
    <script ignore src="<%= VUE_APP_CDN_PATH%>/lib/vuex.min.js"></script>
    <script ignore src="<%= VUE_APP_CDN_PATH%>/lib/axios.min.js"></script>
    <script ignore src="<%= VUE_APP_CDN_PATH%>/lib/tdesign.min.js"></script>

3. main.js 加载子应用前注入依赖

const commonCssLibs = ['tdesign.min.css'];
const commonJsLibs = ['vue.min.js', 'vue-router.min.js', 'vuex.min.js', 'axios.min.js', 'tdesign.min.js'];
const loadScript = (url) => {
  const script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = url;
  script.setAttribute('ignore', 'true'); // 避免重复加载
  script.onerror = () => {
    console.error(`加载失败${url},请刷新重试`);
  };
  document.head.appendChild(script);
};
const loadLink = (url) => {
  const script = document.createElement('link');
  script.rel = 'stylesheet';
  script.type = 'text/css';
  script.href = url;
  script.setAttribute('ignore', 'true'); // 避免重复加载
  script.onerror = () => {
    console.error(`加载失败${url},请刷新重试`);
  };
  document.head.appendChild(script);
};

// 注册子应用,加载之前注入依赖
registerMicroApps(apps, {
    beforeLoad: [
      (app) => {
        commonJsLibs.forEach((libName) => {
          const libUrl = `${VUE_APP_ROUTER_BASE}/lib/${libName}`;
          loadScript(libUrl);
        });
        commonCssLibs.forEach((libName) => {
          const libUrl = `${VUE_APP_ROUTER_BASE}/lib/${libName}`;
          loadLink(libUrl);
        });
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
}

4. 子应用打包忽略公共依赖

    externals: {
      vue: 'Vue',
      vuex: 'Vuex',
      axios: 'axios',
      'tdesign-vue': 'TDesign',
      'vue-router': 'VueRouter',
      // vue: {
      //   commonjs: 'vue',
      //   commonjs2: 'vue',
      //   amd: 'vue',
      //   root: 'Vue', // 外链cdn加载挂载到window上的变量名
      // },

3. 线上部署