nuxt3和qiankun上线落地:vue子应用篇

466 阅读4分钟

上篇 nuxt3和qiankun上线落地:nuxt3主应用配置篇 已经配置好主应用,这篇讲解配置子应用过程。

vue3 子应用

npm create vue@latest

装包 qiankun

pnpm add vite-plugin-qiankun

增加 vite.config.ts 配置

主要是增加 vite-plugin-qiankun 插件

import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import qiankun from "vite-plugin-qiankun";
import { configBundleSpiderPlugin } from "./build/bundleSpider";

const isBuild = process.env.NODE_ENV === "production";
// https://vitejs.dev/config/
export default defineConfig({
  base: "/cy-jeecg-ui/",
  plugins: [
    vue(),
    vueJsx(),
    qiankun("cy-jeecg-ui", {
      // 第一个参数 qiankunName 测试了可以任意,不用跟 package.json name 一致
      useDevMode: true,
    }),
    configBundleSpiderPlugin(isBuild),
  ],
  server: {
    host: true,
    port: 5173,
    cors: true, // 主应用获取子应用时跨域响应头
  },
  preview: {
    host: true,
    port: 5173,
    cors: true, // 主应用获取子应用时跨域响应头
  },
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

ts.config.node.json 增加配置

多增加了两点:一个是我们添加了vite插件目录 build,二是增加 noImplicitAny

{
  "extends": "@tsconfig/node20/tsconfig.json",
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "nightwatch.conf.*",
    "playwright.config.*",
    "build/**/*"   // 这句是增加的
  ],
  "compilerOptions": {
    "noImplicitAny": false,  // 这句是增加的
    "composite": true,
    "noEmit": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",

    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"]
  }
}

main.ts 配置qiankun渲染

特别注意是getNewRouter这里获取路由对象,因为路由的前缀要通过主应用传递过来

import "./assets/main.css";

import { createApp } from "vue";
import { createPinia } from "pinia";

import App from "./App.vue";
import { getNewRouter } from "./router";

import {
  renderWithQiankun,
  qiankunWindow,
  type QiankunProps,
} from "vite-plugin-qiankun/dist/helper";

let app: any = null;

const initQianKun = () => {
  renderWithQiankun({
    // bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap
    // 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等
    bootstrap() {
      console.log("bootstrap");
    },
    // 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法,也可以接受主应用传来的参数
    mount(props: any) {
      render(props.container, props);
    },
    // 应用每次 切出/卸载 会调用的unmount方法,通常在这里我们会卸载微应用的应用实例
    unmount(props: any) {
      console.log("unmount", props);
      // 主应用registerMicroApps+start,这是挂自动档,.切到其他子应用后切回,会重新创建新的子应用实例并渲染。
      // 是的,之前的子应用实例qiankun直接不要了,即使你没有手动销毁实例。
      // 所以说,采用这种模式的话一定要在子应用暴露的unmount钩子里手动销毁实例,不然就内存泄漏了。
      app.unmount();
    },
    update: function (props: QiankunProps): void | Promise<void> {
      console.log("update");
    },
  });
};

const render = (container: any, props: any) => {
  // 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
  app = createApp(App);
  app.use(createPinia());
  app.use(getNewRouter(props));
  app.mount(container ? container.querySelector("#app") : "#app");
};
// 判断是否为乾坤环境,否则会报错iqiankun]: Target container with #subAppContainerVue3 not existed while subAppVue3 mounting!
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render(null, undefined);

子应用增加keep-alive

注意vue3的路由缓存和vue2不一样,不用通过include判定,在路由配置想里面写keepAlive:true就行,另外:key="$route.fullPath"设置的目的是,嵌套子路由时通配符也可识别换路由页面

<script setup lang="ts">
import { RouterView } from "vue-router";
</script>

<template>
  <!-- vue3.0 keep-alive配置 -->
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component
        :is="Component"
        v-if="$route.meta.keepAlive"
        :key="$route.fullPath"
      />
    </keep-alive>
    <component
      :is="Component"
      v-if="!$route.meta.keepAlive"
      :key="$route.fullPath"
    />
  </router-view>
</template>

路由配置

这里就契合了上面两点,一是通过主应用传递activeRule生成路由匹配前缀,而是在meta中添加keepAlive标记缓存

import { createRouter, createWebHistory } from "vue-router";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
import HomeView from "../views/HomeView.vue";

export const getNewRouter = (props: any) => {
  const router = createRouter({
    history: createWebHistory(
      qiankunWindow.__POWERED_BY_QIANKUN__
        ? props.activeRule
        : import.meta.env.BASE_URL
    ), // activeRule 是  /qkpage
    routes: [
      {
        path: "/home",
        name: "home",
        component: HomeView,
        meta: {
          keepAlive: true,
        },
      },
      {
        path: "/about",
        name: "about",
        // route level code-splitting
        // this generates a separate chunk (About.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import("../views/AboutView.vue"),
      },
    ],
  });

  return router;
};

增加优化主应用提前预先请求文件

这个插件的目的是在子应用打包后,生成bundle.json文件,里面有所有js和css文件地址,主应用可以提前请求这些文件,就可以达到尽快渲染页面的目的

import fs from "fs";
import path from "path";

export function configBundleSpiderPlugin(isBuild: boolean) {
  return {
    name: "build-js-spider",
    writeBundle() {
      if (!isBuild) return;
      const srcPath = path.resolve(__dirname, "../", "dist");
      const destPath = path.resolve(srcPath, "assets", "bundle.json");
      const jsArr = getAllFile(srcPath, {
        nodir: true,
      })
        .filter((it) => it.endsWith(".js") || it.endsWith(".css"))
        .map((it) => it.replace(srcPath, "").replace(/\\/g, "/"));
      fs.writeFileSync(
        destPath,
        JSON.stringify({
          manifest: jsArr,
        })
      );
    },
  };
}

/**
 * 遍历指定目录下的所有文件
 * @param {*} dir
 */
const getAllFile = function (dir, option) {
  const res: string[] = [];
  function traverse(dir) {
    fs.readdirSync(dir).forEach((file) => {
      const pathname = path.join(dir, file);
      if (fs.statSync(pathname).isDirectory()) {
        traverse(pathname);
      } else {
        res.push(pathname);
      }
    });
  }
  traverse(dir);
  return res;
};

源码

主应用源码

gitee.com/rootegg/my-…

子应用源码

gitee.com/rootegg/my-…

image.png

问题

开发模式 pnpm dev 子应用启动后,会报错,因为子应用是开发模式时,主应用entry要写完整url地址,如果是生产模式可以只写后面路径

image.png

image.png