nuxt3和qiankun上线落地:nuxt3主应用配置篇

1,181 阅读6分钟

背景

我们项目有时会有网页中嵌入其他网页的需求,或者拆分巨石应用的需求,以前都是使用iframe,现在更高级的是使用微前端框架。

iframe的好处是天然的沙箱隔离、元素隔离、样式隔离,坏处是加载慢、性能差、无法全局弹窗、通信难等。

微前端的好处是解决拆分巨石应用、缩小代码主项目代码提高首页加载、可路由匹配页面、可预请求文件、允许不同语言前端项目等,坏处是主项目和子项目接入麻烦,部署麻烦,打包配置要求更高,微前端框架可能造成性能加载瓶颈等

虽然iframe有好处也有坏处,同理微前端框架也同样有好处也有坏处,建议根据团队前端实力,前端运维能力合理选择,并不是说iframe和微前端一定谁最优,脱离场景都是耍流氓。

系列其他篇文章:

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

# nuxt3和qiankun上线落地:应用部署篇

推荐众所周知:

我们项目是后台管理系统,是常规vue3单页应用,改造成nuxt3服务端渲染以及qiankun微前端,对于首屏首页第一次渲染来说,基本秒级出来。

对比

qiankun目前版本 v2.10.16,周下载量17921,micro-app目前版本 v1.0.0-rc.5,周下载量1644

使用量都不多,相比下qiankun版本号更稳定,所以选择qiankun。

image.png

image.png

附录micro-app原理

想了解原理的,可以看github上官方给出的原理解释

Nuxt3 主应用

中文官网 nuxtjs.org.cn/

做服务端渲染有两种方式:

手动增加 server.jsentry-client.jsentry-server.js,把原来单一模式的Vue实例,store实例,router实例改成每个请求独立这些实例等,参考模版项目 github.com/bluwy/creat…

  • 第二种推荐是使用成熟服务端框架 Nuxt、Quasar 等,根据框架规范写组件、服务接口请求、中间件、模板页、插件等

初始化 nuxt3 项目

npx nuxi@latest init <project-name>

安装上 pinia,pwa,antdv 包

pnpm add pinia @pinia/nuxt @pinia-plugin-persistedstate/nuxt @nuxt/image ant-design-vue qiankun lodash-es
pnpm add -D @ant-design-vue/nuxt @vite-pwa/nuxt less

nuxt.config.ts引入模块

import { fileURLToPath, URL } from "node:url";
import { resolve } from "path";
const { VITE_PROXY } = process.env; // 环境变量在package.json的scripts命令里面nuxt dev --dotenv .env.development增加的开发服务器地址
export default defineNuxtConfig({
  devtools: { enabled: false }, // 关闭页面的开发工具
  ssr: true, // 默认启动服务端渲染
  routeRules: {
    "/qkpage/**": { ssr: false }, // qiankun子应用页面不用服务端渲染
  },
  modules: [
    "@pinia/nuxt", // nuxt的pinia模块
    "@pinia-plugin-persistedstate/nuxt",  // pinia的持久化
    "@nuxt/image", // NuxtImg图片组件
    "@ant-design-vue/nuxt", // nuxt的antdv组件库自动按需加载
    "@vite-pwa/nuxt", // pwa模块
  ],
  piniaPersistedstate: {
    storage: "localStorage", // pinia持久化到localStorage
  },
  imports: {  // 根据实际需要做文件自动import导入
    dirs: [
      "store/**/*.{ts,js,mjs,mts}",
      "composables/base/**/*.{ts,js,mjs,mts}",
    ],
  },
  nitro: { // 开发模式接口代理转发
    compressPublicAssets: true,
    devProxy:
      VITE_PROXY &&
      JSON.parse(VITE_PROXY!).reduce((p, c) => {
        p[c[0]] = {
          target: c[1],
          changeOrigin: true,
          prependPath: true,
        };
        return p;
      }, {}),
  },
  vite: { // 增加 /@/开头的前缀可识别,默认已经有@/前缀可识别了
    resolve: {
      alias: [
        // [/]@/xxxx => xxxx
        {
          find: /[\/]@\//,
          replacement: fileURLToPath(new URL("./", import.meta.url)),
        },
      ],
    },
  },
  hooks: {
    "pages:extend": (pages) => {
      // qiankun微前端需要的追加自定义的路由
      pages.push({
        path: "/qkpage",
        file: resolve(__dirname, "components/QianKunContent.vue"),
        children: [
          {
            path: "/:slug(.*)*", // 一定要加上这段兜底,不然qiankun匹配不到子应用的路由
            file: resolve(__dirname, "components/QianKunContent.vue"),
          },
        ],
      });
    },
  },
});

pinia 配置和持久化

根目录新建store目录,新建user.ts文件,做持久化

export const useUserStore = defineStore({
  id: "app-user",
  state: () => ({
    userInfo: null,
  }),
  actions: {
    async login(params) {
      try {
        const data: any = await fetch("/sys/login", {
          method: "POST",
          body: params,
        });
        const { userInfo } = data;
        this.userInfo = userInfo;
      } catch (error) {
        return Promise.reject(error);
      }
    },
  },
  persist: true, // 持久化
});

qiankun默认空组件

根目录新建 components 目录,新建 QianKunContent.vue,里面什么都不用做,qiankun的容器节点放到layouts布局里面了

<template>
  <div></div>
</template>
<style>
#qiankun-content,
#qiankun-content > div {
  height: 100%;
}
</style>

layouts默认布局

根目录新增layouts目录,新建default.vue,在slot位置会填充微前端页面,一般管理系统其他部分比如顶部和左边都是固定的功能菜单等

注意:项目用的loadMicroApp加载子应用,所有子应用的多页签缓存会保留,卸载子应用缓存按钮,作用清理是子应用多页签缓存

<template>
  <a-layout class="layouts-default">
    <a-layout-header class="header">
      顶部logo区域 <a-button @click="onCleanMicroApps">卸载子应用缓存</a-button>
    </a-layout-header>
    <a-layout>
      <a-layout-sider class="left-slider">
        <ScrollContainer> <LayoutMenu></LayoutMenu></ScrollContainer>
      </a-layout-sider>
      <a-layout>
        <!-- 中间区域默认插槽 -->
        <slot />
        <div id="qiankun-content"></div>
      </a-layout>
    </a-layout>
  </a-layout>
</template>

<script setup lang="ts">
import { cleanMicroApps } from "@/store/microApp";
const onCleanMicroApps = () => {
  cleanMicroApps();
  navigateTo("/");
};
</script>

<style scoped lang="less">
.layouts-default {
  min-height: 100%;
}
.header,
.left-slider {
  color: #fff;
}
</style>

注册qiankun主应用配置

根目录新增plugins目录,新建 qiankun.client.ts,其中 xxxx.client.ts 或者 xxxx.server.ts 分别表示只在客户端或者服务端使用

import { prefetchApps, registerMicroApps, start, loadMicroApp } from "qiankun";
import { debounce } from "lodash-es";
import { microApps, setPrefetchBundleJson } from "@/store/microApp";

export default defineNuxtPlugin((nuxtApp) => {
  // registerApps();
  const router = useRouter();
  nuxtApp.vueApp.mixin({
    mounted: () => {
      watch(
        () => router.currentRoute.value.path,
        (toPath) => {
          const microItem = microApps.find((it) =>
            toPath.startsWith(it.activeRule)
          );
          if (microItem && !microItem.microIns) {
            const domContainer = document.querySelector(microItem.container);
            if (domContainer) {
              microItem.microIns = loadMicroApp(microItem);
            }
          }
        },
        { immediate: true, deep: true }
      );
    },
  });
  // 手动预请求子应用js和css文件
  microApps.forEach((app) => {
    setPrefetchBundleJson(app);
  });
});

子应用缓存方案

qiankun加载子应用有两种模式:

  • 第一种是registerMicroApps注册,然后start,这是自动挂载,路由改变后,重新mount子应用,路由切换时,每次都会重新执行子应用的mount方法和旧的unmounted方法

  • 第二种是loadMicroApp,每次路由匹配上后手动加载子应用

自动档下,是这样运作的:

1.首次load应用,创建子应用实例,渲染。

2.切到其他子应用后切回,会重新创建新的子应用实例并渲染。是的,之前的子应用实例qiankun直接不要了,即使你没有手动销毁实例。所以说,采用这种模式的话一定要在子应用暴露的unmount钩子里手动销毁实例,不然就内存泄漏了。

qiankun并没有提供配置项来修正这种行为,旧的实例直接被弃置不用了,而你keep-alive的仅仅是子应用下的某个组件实例,而整个子应用实例都被弃之不用了,keep-alive某个组件自然是没有任何用处的。 而loadMicroApp则不然,loadMicroApp的策略是每个子应用都有一个唯一的实例ID,reload时会复用之前的实例

注册antdv组件库按需加载

在 nuxt.config.ts 中已经使用 @ant-design-vue/nuxt,会自动按需加载

如果不用按需加载就去掉@ant-design-vue/nuxt,则要在plugins中新增antd.client.ts,推荐就用按需加载

image.png

源码

首页是主应用页面

image.png

路由qkpage开头的是加载的子应用

image.png

主应用源码,源码中服务器地址改为了 xxxxx

gitee.com/rootegg/my-…

后记感悟

  • 预渲染页面中不要使用 useFetch 等

在 nuxt.config.ts 中配置 routeRules prerender 可以让某些页面预渲染,但是useFetch会在打包时请求,如果是变化的数据则不在变化

一种是改为客户端请求数据,二种是去掉预渲染

image.png

image.png

  • app.vue中不用使用客户注册的组件

因为 antd 组件是在 plugins 的 antd.client.ts 中,标志是客户端在注册,所以服务端一进来 app.vue 无法识别 a-config-provider 导致 所有页面都没有服务端渲染了

image.png

image.png

  • 拆包方式要标志是客户端才有效不然会报错 extenals ...

image.png