微前端之RuoYi-Vue接入qiankun实战

介绍

项目地址ruoyi-vue-qiankun

基于RuoYi-Vue,使用 qiankun 实现微前端;主应用基于 vue2,目前接入 vue2 子应用。

环境:

  • JDK >= 1.8
  • MySQL >= 5.7
  • Maven >= 3.0
  • Node >= 12
  • Redis >= 3

技术框架:

  • vue:2.6.12
  • qiankun:2.8.4
  • spring-boot:2.5.14

文档参考:

项目运行

1. 前端

启动主应用,启动端口:9001

cd app-main
npm i
npm run dev

image.png

启动子应用,启动端口:9002

cd app-vue2 
npm i
npm run dev

image.png

2. 后端

启动端口:9000

使用 IDEA 运行,具体可参考 RuoYi 后端运行

接入问题

1. 打包部署

使用 nginx 进行部署,主应用(main)、子应用(vue2)部署在同一个目录

├── main
└── vue2

修改 nginx 配置,允许子应用跨域访问:

http {
  # 主应用
  server {
    listen 9001;
    location / {
      root D:\\develop\\nginx-1.23.0\\html\\main;
      index index.html index.html;
      try_files $uri $uri/ /index.html;

      if ($request_filename ~ .*\.(htm|html)$) {
        add_header Cache-Control no-store;
      }
    }
  }

  # 子应用
  server {
    listen 9002;
    location / {
      root D:\\develop\\nginx-1.23.0\\html\\vue2;
      index index.html index.html;
      try_files $uri $uri/ /index.html;
        
      # 跨域配置
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      add_header Access-Control-Allow-Credentials true;
    }
  }
}

完整 nginx 配置

2. 权限改造

由主应用控制子应用的路由、权限

image.png

①. 主应用

Ⅰ. 新建 micro 目录,存放子应用相关代码

# @/src/micro/index.js
import { registerMicroApps, start } from 'qiankun';

import { filterMicroApps, microAppHooks } from './micro';

// 注册微应用
export const registerApps = () => {
  const microApps = filterMicroApps();
  registerMicroApps(microApps, microAppHooks);
  start({
    prefetch: 'all', // 全量
    sandbox: {
      // strictStyleIsolation: true, // 严格的样式隔离模式 不推荐,大量bug
      experimentalStyleIsolation: true, // 实验性的样式隔离特性
    },
  });
};

从 vuex 中拿到所有路由、菜单,根据 MICRO_APPS进行过滤并传入对应的子应用中

# @/src/micro/micro.js
import store from '@/store';

// 所有子应用
const MICRO_APPS = [
  {
    name: 'app-vue2',
    entry:
      process.env.NODE_ENV === 'production'
        ? '//localhost:9002' // 实际部署的地址
        : '//localhost:9002',
    activeRule: '/app/vue2',
  },
];

// 根据所配置权限,得到可注册的微应用及其路由结构
export const filterMicroApps = () => {
  const regMicroApps = MICRO_APPS.filter(app => {
    const appRoute = store.getters.appRoutes.find(r => r.remark === app.name);
    if (appRoute?.remark === app.name) {
      app.routes = appRoute;
      return true;
    }
    return false;
  });
  return regMicroApps.map(app => {
    return {
      name: app.name,
      entry: app.entry,
      activeRule: app.activeRule,
      container: '#subapp-viewport',
      props: {
        routerBase: app.activeRule,
        routes: app.routes,
        permissions: store.getters.permissions,
        roles: store.getters.roles,
      },
    };
  });
};

完整代码

Ⅱ. 主应用中,创建子应用入口容器 subapp-viewport,并在 mounted 钩子调用 registerApps注册微应用

<template>
  <section class="app-main">
    # 子应用容器
    <div id="subapp-viewport" />
    
    # 主应用路由入口
    <transition
      name="fade-transform"
      mode="out-in"
    >
      <keep-alive :include="cachedViews">
        <router-view
          v-if="!$route.meta.link"
          :key="key"
        />
      </keep-alive>
    </transition>
  </section>
</template>

<script>
import { registerApps } from '@/micro';

import iframeToggle from './IframeToggle/index';

export default {
  name: 'AppMain',
  mounted() {
    if (!window.qiankunStarted) {
      window.qiankunStarted = true;
      registerApps();
    }
  },
};
</script>

完整代码

②. 子应用

Ⅰ. 配置 public-path

// /src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

Ⅱ. 修改入口文件 main.js

// /src/main.js
import Vue from 'vue';
import VueRouter from 'vue-router';

import App from './App';
import store from './store';
import { constantRoutes } from './router';
import { initRouter } from './router/initRouter';

import actions from './actions';
import './public-path';

let instance = null;
let router = null;
function render(props = {}) {
  const { container, routerBase } = props;
  
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : '/',
    mode: 'history',
    routes: constantRoutes,
  });
  initRouter(props, router);

  instance = new Vue({
    router,
    store,
    render: h => h(App),
  });

  instance.$mount(container ? container.querySelector('#sub-app') : '#sub-app');
}

// 子应用独立运行的环境
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
}
export async function mount(props) {

  actions.setActions(props);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}

完整代码

Ⅲ. 修改 vue.config.js 配置文件

  • 打包为 umd 格式
  • 配置 publicPath 修复图片资源404问题
const port = 9002;
const publicPath =
  process.env.NODE_ENV === 'production'
    ? `http://localhost:${port}`
    : `http://localhost:${port}`;

module.exports = {
  configureWebpack: {
    output: {
      // 把子应用打包成 umd 库格式
      library: `${name}`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
  chainWebpack(config) {
    // 修复图片打包404问题
    config.module
      .rule('fonts')
      .use('url-loader')
      .loader('url-loader')
      .options({
        limit: 4096, // 小于4kb将会被打包成 base64
        fallback: {
          loader: 'file-loader',
          options: {
            name: 'fonts/[name].[hash:8].[ext]',
            publicPath,
          },
        },
      })
      .end();
    config.module
      .rule('images')
      .use('url-loader')
      .loader('url-loader')
      .options({
        limit: 4096, // 小于4kb将会被打包成 base64
        fallback: {
          loader: 'file-loader',
          options: {
            name: 'img/[name].[hash:8].[ext]',
            publicPath,
          },
        },
      });
  },
};

完整代码