背景
我们项目有时会有网页中嵌入其他网页的需求,或者拆分巨石应用的需求,以前都是使用iframe,现在更高级的是使用微前端框架。
iframe的好处是天然的沙箱隔离、元素隔离、样式隔离,坏处是加载慢、性能差、无法全局弹窗、通信难等。
微前端的好处是解决拆分巨石应用、缩小代码主项目代码提高首页加载、可路由匹配页面、可预请求文件、允许不同语言前端项目等,坏处是主项目和子项目接入麻烦,部署麻烦,打包配置要求更高,微前端框架可能造成性能加载瓶颈等
虽然iframe有好处也有坏处,同理微前端框架也同样有好处也有坏处,建议根据团队前端实力,前端运维能力合理选择,并不是说iframe和微前端一定谁最优,脱离场景都是耍流氓。
系列其他篇文章:
推荐众所周知:
-
nuxt3 服务端渲染 nuxtjs.org.cn/
-
阿里
qiankun
qiankun.umijs.org/zh -
和京东
micro-app
micro-zoe.github.io/micro-app/
我们项目是后台管理系统,是常规vue3单页应用,改造成nuxt3服务端渲染以及qiankun微前端,对于首屏首页第一次渲染来说,基本秒级出来。
对比
qiankun目前版本 v2.10.16,周下载量17921,micro-app目前版本 v1.0.0-rc.5,周下载量1644
使用量都不多,相比下qiankun版本号更稳定,所以选择qiankun。
附录micro-app原理
想了解原理的,可以看github上官方给出的原理解释
-
micro-app介绍 github.com/micro-zoe/m…
-
从零开始写一个微前端框架-沙箱篇 github.com/micro-zoe/m…
-
从零开始写一个微前端框架-渲染篇 github.com/micro-zoe/m…
-
从零开始写一个微前端框架-样式隔离篇 github.com/micro-zoe/m…
-
从零开始写一个微前端框架-数据通信篇 github.com/micro-zoe/m…
Nuxt3 主应用
中文官网 nuxtjs.org.cn/
做服务端渲染有两种方式:
- 第一种是根据vue官网介绍 cn.vuejs.org/guide/scali… 和 vite官网介绍 cn.vitejs.dev/guide/ssr.h…
手动增加 server.js
、entry-client.js
、entry-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,推荐就用按需加载
源码
首页是主应用页面
路由qkpage开头的是加载的子应用
主应用源码,源码中服务器地址改为了 xxxxx
后记感悟
- 预渲染页面中不要使用 useFetch 等
在 nuxt.config.ts 中配置 routeRules prerender 可以让某些页面预渲染,但是useFetch会在打包时请求,如果是变化的数据则不在变化
一种是改为客户端请求数据,二种是去掉预渲染
- app.vue中不用使用客户注册的组件
因为 antd 组件是在 plugins 的 antd.client.ts 中,标志是客户端在注册,所以服务端一进来 app.vue 无法识别 a-config-provider 导致 所有页面都没有服务端渲染了
- 拆包方式要标志是客户端才有效不然会报错 extenals ...