Turborepo + Qiankun + pnpm 实践方案(二)

292 阅读3分钟

Turborepo + Qiankun + pnpm 实践方案(二)

背景

在前一篇文章中,我们介绍了使用 Turborepo、Qiankun 和 pnpm 构建一个 Monorepo 项目的基础方案。将进一步探讨如何搭建主应用与子应用的跳转配置,下一步实现统一的 token 鉴权与应用鉴权及用户无感知的 token 自动延时。

项目场景

我们有一个主体的多应用页面,每个应用对应企业的不同平台或数据可视化项目。为了实现统一的 token 鉴权,主应用将使用 Qiankun 框架管理子应用。主应用提供统一的布局(如 layout 和 sidebar),而子应用可以使用各自熟悉的技术栈并专注于业务代码。

image.png

image.png

主应用配置

主应用将通过 main.ts 进行基础配置,并在主应用挂载完毕后再启动 Qiankun。这样做可以避免主应用未完全挂载导致加载子应用时报错的问题。

主应用 main.ts 配置


import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import { APP_CONF } from "./appConf";
import { start, registerMicroApps } from "qiankun";
import "ant-design-vue/dist/reset.css";

registerMicroApps(APP_CONF, {
  beforeLoad: [async (app) => console.log("before load", app.name)],
  beforeMount: [async (app) => console.log("before mount", app.name)],
  afterMount: [async (app) => console.log("after mount", app.name)],
});

// 挂载主应用后再启动 Qiankun
const app = createApp(App).use(store).use(router);
app.mount("#app");

router.isReady().then(() => {
  start();
});

提取子应用的配置文件 appConf.ts


/**
 * @template qiankun配置
 * 子应用配置
 *
 * @param { name } 子应用名称
 * @param { entry } 对应子应用的路径地址
 * @param { activeRule } 需要匹配的路由
 * @param { container } 要渲染的容器
 */

export const APP_CONF = [
  {
    name: "sub-mis", // 子应用名称
    entry: "//localhost:5173", // 默认会加载这个路径下的html,解析里面的js
    activeRule: "/sassAdmin/subMis", // 匹配的路由
    container: "#frame", // 渲染的容器
  },
];

主页面布局示例

在主页面中,我们可以实现子应用与主应用路由页面的跳转,并进行一些基础布局的配置。


<template>
  <div style="height: 100vh">
    <Layout style="height: 100%">
      <LayoutSider v-model:collapsed="collapsed" :trigger="null" collapsible>
        <div class="logo" />
        <Menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">
          <MenuItem key="1" @click="handleSubAppMis">
            <span>菜单1</span>
          </MenuItem>
          <MenuItem key="2" @click="handleToTest">
            <span>测试文件</span>
          </MenuItem>
          <MenuItem key="3" @click="handleSubAppMisJs">
            <span>菜单mis-js</span>
          </MenuItem>
        </Menu>
      </LayoutSider>
      <Layout>
        <LayoutHeader style="background: #fff; padding: 0">
          <menu-unfold-outlined
            v-if="collapsed"
            class="trigger"
            @click="() => (collapsed = !collapsed)"
          />
          <menu-fold-outlined
            v-else
            class="trigger"
            @click="() => (collapsed = !collapsed)"
          />
        </LayoutHeader>
        <LayoutContent
          :style="{
            margin: '24px 16px',
            padding: '24px',
            background: '#fff',
            minHeight: '280px',
          }"
        >
          <MainContainer />
        </LayoutContent>
      </Layout>
    </Layout>
  </div>
</template>

<script lang="ts" setup>
import {
  Layout,
  LayoutSider,
  Menu,
  MenuItem,
  LayoutHeader,
  LayoutContent,
} from "ant-design-vue";
import { MenuUnfoldOutlined, MenuFoldOutlined } from "@ant-design/icons-vue";
import { ref } from "vue";
import { useRouter } from "vue-router";
import MainContainer from "@/components/layout/layoutView.vue";

const router = useRouter();
const selectedKeys = ref<string[]>(["1"]);
const collapsed = ref<boolean>(false);

const handleSubAppMis = () => {
  selectedKeys.value = ["1"];
  router.push({ path: "/sassAdmin/subMis" });
};

const handleToTest = () => {
  selectedKeys.value = ["2"];
  router.push({ path: "/sassAdmin/test" });
};

const handleSubAppMisJs = () => {
  selectedKeys.value = ["3"];
  router.push({ path: "/sassAdmin/subMis/js" });
};
</script>

<style lang="less">
.logo {
  height: 32px;
  background: rgba(255, 255, 255, 0.3);
  margin: 16px;
}

.site-layout .site-layout-background {
  background: #fff;
}
</style>

处理子应用渲染逻辑

使用 keep-alivev-show 区分主应用和子应用的渲染逻辑。


<template>
  <div>
    <keep-alive v-show="route.name">
      <router-view :key="key" />
    </keep-alive>
    <section v-show="!route.name" id="frame"></section>
  </div>
</template>

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

const route = useRoute();
const key = computed(() => route.path);
</script>

子应用配置

子应用的配置需要考虑是否独立部署。如果是作为 Qiankun 子应用运行,则需要特殊处理。

子应用挂载处理

在子应用的 main.ts 中,我们需要判断当前应用是否作为 Qiankun 子应用运行。如果是,则使用 Qiankun 的 renderWithQiankun 进行挂载。



import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import Router from "./router";
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/dist/helper";

let app: any;

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  createApp(App).use(Router).mount("#app");
} else {
  renderWithQiankun({
    mount(props: any) {
      app = createApp(App);
      app.use(Router);
      app.mount(props.container.querySelector("#app"));
    },
    bootstrap() {
      console.log("vue app bootstrap");
    },
    update() {
      console.log("vue app update");
    },
    unmount() {
      console.log("vue app unmount");
      app && app.unmount();
    },
  });
}

子应用路由配置

在配置子应用路由时,需要注意 createWebHistory 的根路径需要与主应用的路由匹配。


import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [
  { path: "/", name: "Root", component: () => import("../view/home/home.vue") },
  { path: "/home", name: "Home", component: () => import("../view/home/home.vue") },
  { path: "/subMis", name: "SubMis", component: () => import("../view/home/home.vue") },
  { path: "/subMis/js", name: "SubMisJs", component: () => import("../view/question/jsQuestion.vue") },
];

const router = createRouter({
  history: createWebHistory("/sassAdmin"), // 注意这里需要与主应用的路由匹配
  routes,
});

export default router;

结语

到此为止,我们已经完成了主应用和子应用的配置。主应用通过 Qiankun 管理子应用,子应用则根据 Qiankun 的要求进行配置。这种结构允许我们灵活地将不同的技术栈和业务逻辑集成在一个统一的管理平台中,同时确保了应用的可扩展性和维护性。