使用Vue3从0到1搭建qiankun项目

2,213 阅读1分钟

前言:本文章主要记载作者从0到1搭建qiankun项目的过程以及中间遇到的一些问题,也给想接触qiankun的朋友一个学习的Demo

1. 项目技术栈介绍及搭建

主应用

   // 1. 使用vite创建项目,并命名为dashboard
   npm init vite@latest dashboard -- --template vue
   // 因为使用vite创建的项目是个空项目,需要我们自己安装所需的库
   // 2. 安装Vue-router和vuex
   npm i vue-router -S
   npm i vuex -S

路由配置

// src目录新建router文件夹以及index.js文件
// index.js
import { createRouter, createWebHistory } from "vue-router";
import { BaseRoute, errorRouter } from "./baseRouter";
/**
* Vite 实现从文件系统中导入多个模块
* docs: https://vitejs.cn/guide/features.html#glob-import
*
* 以下代码等价于webpack的:
* const req = require.context('./modules',true,/\.js$/)
* req.keys().forEach(path => asyncRoutes.push(req(path).default))
* 
* 读取./modules文件夹中的js文件
*/
let asyncRoutes = [];
const modules = import.meta.globEager("./modules/*.js");
for (const path in modules) {
  asyncRoutes.push(modules[path].default);
}
let routes = asyncRoutes.concat([...BaseRoute, ...errorRouter]);
// 创建路由
const router = createRouter({
  history: createWebHistory(),
  routes: routes,
});    
export default router


// ./baseRouter.js
export const BaseRoute = [
  {
    path: "/",
    component: Layout
  },
];

export const errorRouter = [
  {
    path: "/error",
    component: Layout,
    children: [
      {
        path: "/403",
        name: "403",
        component: NotRights,
      },
      {
        /*
        * 匹配所有路由
        * 详情请见官方文档:
        * https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route
        */
        path: "/:pathMatch(.*)*",
        name: "404",
        component: NotFound,
      },
    ],
  },
];

vuex 配置

// src目录新建store文件夹以及index.js文件
// index.js
import { createStore } from "vuex";

const modules = {};
const req = import.meta.globEager("./modules/*.js");
for (const path in req) {
  // 用文件名代表模块名
  // /modules/user.js ==> user
  let pathName = path.split("modules/")[1].split(".")[0];
  if (!modules[pathName]) modules[pathName] = req[path].default;
}

const store = createStore({
   state() {
     return {};
   },
   modules,
});
export default store

子应用

// 1. 使用Vue Cli创建项目,并命名micro-user和micro-salary
vue create micro-user
vue create micro-salary
// 因为Vue Cli创建项目根据需求自动安装Vue相关库,所以无需在安装

2. qiankun配置

先贴官方文档地址:qiankun

主应用

// 1. 安装qiankun
npm i qiankun -S

// 2. 通过registerMicroApps函数注册微应用
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
   {
       name: "micro-user", // 必选,微应用的名称,微应用之间必须确保唯一。
       entry: "//localhost:8001", // 必选,微应用的入口,需要与微应用的地址保持一致
       container: "#view-main", // 必选,微应用的容器节点的选择器或者 Element 实例。
       activeRule: "/micro-user", // 必选,微应用的激活规则
   },
   {
       name: "micro-salary", 
       entry: "//localhost:8002",
       container: "#view-main", 
       activeRule: "/micro-salary",
   },
])
// 更多参数和使用方法见上方官方文档

// 3. 需要让主应用的路由可以识别微应用的路由
// router/baseRouter.js
export const BaseRoute = [
  {
    path: "/",
    redirect: "/system",
  },
  {
    path: "/micro-salary/:morePath*",
    component: Layout
  },
  {
    path: "/micro-user/:morePath*",
    component: Layout
  },
];



// 4. 启动qiankun
start()

子应用

  1. 修改vue.config.js配置
 const { defineConfig } = require("@vue/cli-service");
 const { name } = require("./package.json");
 const { resolve } = require("path");
 module.exports = defineConfig({
   /*
   重点!!!
   项目打包部署的基本路径,一定要写且需要和项目运行地址保持一致,不然会出现无法读取到资源情况
   */
   publicPath: "http://localhost:8001/", 
   devServer: {
    port: "8001", // 需要与主应用注册时的入口保持一致
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
   },
   configureWebpack: {
    resolve: {
      alias: {
        "@": resolve(__dirname, "src"),
      },
    },
    output: {
      library: `${name}-[name]`,
      libraryTarget: "umd", // 把微应用打包成 umd 库格式
      /**
       * Webpack 5 会从 package.json name 中自动推断出一个唯一的构建名称,并将其作为 output.uniqueName 的默认值。
       * 由于 package.json 中有唯一的名称,可将 output.jsonpFunction 删除。
       */
      // jsonpFunction: `webpackJsonp_${name}`,
    },
  },
 })
 
  1. 修改路由配置
/*
因为qiankun是通过监听路由结合注册微应用时填写的激活规则来进行确定渲染的微应用
所以需要给路由增加基础路径,基础路径需要和微应用的激活规则相匹配
*/
const router = createRouter({
  history: createWebHistory(
    window.__POWERED_BY_QIANKUN__ ? "/micro-user/" : "/"
  ),
  routes,
});
  1. 修改入口文件(main.js)
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
let app = null;

function render(props = {}) {
  const { container } = props;
  app = createApp(App);
  app
    .use(store)
    .use(router)
    .mount(container ? container.querySelector("#app") : "#app");
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
  console.log("[vue] props from main framework", props);
  render(props);
}
export async function unmount() {
  app.unmount();
}

至此微应用的配置就完成了

补上项目结构

// layout.vue
<template>
  <el-container>
    <el-header>
      <HeaderWrap />
    </el-header>
    <el-container class="layout-main">
      <el-aside class="nav-bar">
        <NavBar />
      </el-aside>
      <!-- 微应用容器 -->
      <div id="view-main" />
      <!-- 主应用自己的路由渲染 -->
      <router-view />
      <!--
          该处请注意,请不要写成以下结构
          <div id="view-main">
              <router-view/>
          </div>
          这种结构会导致加载完微应用后,无法在使用主应用的路由
      -->
    </el-container>
  </el-container>
</template>

// NavBar.vue
<template>
  <el-menu
    active-text-color="#ffd04b"
    background-color="#545c64"
    class="el-menu-vertical-demo"
    :default-active="activeMenu"
    text-color="#fff"
    router
    @select="menuSelect"
  >
    <el-menu-item index="/micro-user/">
      <span>用户微应用</span>
    </el-menu-item>
    <el-menu-item index="/micro-salary/">
      <span>薪资微应用</span>
    </el-menu-item>
  </el-menu>
</template>

这样的项目就搭建好了,预览效果见下图 20220302_174600.gif

3. 注册微应用优化

上面我们所做的加载一些固定的微应用,但如果说微应用的数量不固定呢? 比如现在只有薪资和用户两个微应用,过一段时间又增加了一个系统设置微应用,难道我们还要去修改主应用已有的代码吗?

以下解决方案属于作者的想法:

因为我们知道注册微应用的时候,只需要固定的一些配置,比如:name,entry,container等,那么我们就可以把所有的微应用配置项存储到数据库中,每有一个新的微应用就往数据库中插入一条数据,然后通过接口的形式获取到所有的微应用,完成批量注册

下面进行实现前端部分:

// 1. 主应用store下创建micro模块
import { router } from "@/router";
import Layout from "@/layout/index.vue";
import { prefetchApps, registerMicroApps, start } from "qiankun";

export default {
  namespaced: true,
  state() {
    return {
      isLoadMicro: false, // 微应用路由是否加载完成
    };
  },
  mutations: {
    changeLoadStatus(state, payload) {
      state.isLoadMicro = payload;
    },
    startMicroApps(state, payload) {
      // 注册微应用
      registerMicroApps(payload);
      // 启动微服务
      start({ sandbox: { strictStyleIsolation: true } });
    },
  },
  actions: {
    // 加载微应用路由
    loadMicroRouter({ commit }) {
      return new Promise((res) => {
        // 模拟异步请求
        setTimeout(() => {
          const microRouter = [
            {
              name: "micro-salary",
              entry: "//localhost:8001",
              container: "#view-main",
              activeRule: "/micro-salary",
            },
            {
              name: "micro-user",
              entry: "//localhost:8002",
              container: "#view-main",
              activeRule: "/micro-user",
            },
          ];
          
          microRouter.forEach((micro) => {
            /*
             给每一个微应用都添加一个可以与之相匹配的路由,防止渲染404页面
             通过addRoute添加的路由不会立马生效,需要触发一次新的导航,才会生效
             所以还需要更改路由拦截代码
            */
            router.addRoute({
              path: `${micro.activeRule}/:morePath*`,
              component: Layout,
            });
          });
          
          commit("changeLoadStatus", true);
          commit("startMicroApps", microRouter);
          res();
        }, 1000);
      });
    },
  },
};
import { store } from "@/store";
router.beforeEach(async (to) => {
    if (!store.state.micro.isLoadMicro) {
      await store.dispatch("micro/loadMicroRouter");
      /**
       * 不能写成 return { ...to, replace:true }
       * 因为路由是动态添加的,导致第一次没有匹配的路由,则会匹配到404路由
       * 且当有path和name时,跳转的路径以name为主
       * 所以只能以路径作为跳转依据
       */
      return { path: to.path, replace: true };
    } else {
      return true;
    }
 });

qiankun所提供的Api,本项目只用到了最基本的两个,更多的API还需要去查看官网

到这里,整个项目就大功告成了,感谢阅读,最后附上仓库地址