Qiankun微前端 + Turborepo + pnpm 实践

2,561 阅读13分钟

前言

小编在实习过程中接触了 Qiankun 微前端的项目,个人还是比较感兴趣的,所以参考公司的项目搭建一个 Qiankun 微前端 + Turborepo + pnpm 实践的 demo 作为演示和学习,以进一步去了解这一套体系结构......

在现代前端开发中,微前端架构已经成为一种流行的解决方案,它允许我们将一个大型应用拆分为多个独立的子应用,每个子应用可以独立开发、测试和部署。这种架构不仅提高了开发效率,还增强了系统的可维护性和扩展性。

接下来小编将介绍如何使用 qiankun 微前端框架、Turborepo 构建系统和 pnpm 包管理器来实现一个基于 Vue 的主应用和子应用的微前端项目。我们将详细探讨如何配置和组织项目结构,确保主应用和子应用能够无缝集成,并利用 Turborepo 和 pnpm 的优势来提高开发和构建效率。

开始之前,可以推荐下掘友们先看一下小编之前写过的一篇关于 如何使用 vue3 + pnpm 搭建 monorepo 项目的一篇文章,然后再来看 monorepo 架构是如何结合微前端搭配使用到项当中的,小编觉得这个恰好是一个循序渐进的过程哈,理解起来比较容易一点,链接如下:

教你如何使用 vue3 + pnpm 搭建 monorepo 项目

闲话不多赘述,我们步入正题吧

技术栈介绍、调研与选型

微前端介绍: 微前端(Micro Frontends)是一种架构风格,旨在将前端应用拆分为多个独立的、可独立开发和部署的子应用。每个子应用可以由不同的团队开发,使用不同的技术栈,并且可以独立部署。微前端的核心思想是将前端应用拆分为多个小的、独立的模块,每个模块负责特定的功能或页面,从而提高开发效率、可维护性和可扩展性。

微前端的应用场景

  1. 大型企业应用:将大型企业应用拆分为多个独立的子应用,每个子应用由不同的团队开发和维护。
  2. 多技术栈项目:在同一个项目中使用不同的技术栈,每个子应用可以使用不同的技术栈。
  3. 渐进式升级:逐步升级和替换旧的前端应用,每个子应用可以独立升级和替换。

常见微前端实现方案:

技术方案核心特点支持框架集成难度性能社区支持适用场景
iframe简单易用,完全隔离任意简单较差一般简单应用、需要完全隔离的场景
Web Components标准化的 Web API,完全隔离原生 JavaScript简单较好一般组件化开发、多技术栈项目
single-spa提供生命周期管理,支持多种前端框架React, Vue, Angular 等较高较好活跃大型企业应用、多技术栈项目
qiankun基于 single-spa 封装,提供更简洁的 APIReact, Vue, Angular 等中等较好活跃大型企业应用、多技术栈项目
Module Federation基于 Webpack 5,提供模块共享机制React, Vue, Angular 等较高较好活跃大型企业应用、多技术栈项目
wujie基于 Web Components,提供组件化开发原生 JavaScript简单较好一般组件化开发、多技术栈项目
micro-app基于 Web Components,提供组件化开发原生 JavaScript简单较好一般组件化开发、多技术栈项目

本文小编选择的是 qiankun 微前端的方案,因为小编所在的实习公司也是使用的这一套方案,所以想进一步加深一下对这套方案的了解,并且 qiankun 基于 single-spa 封装,提供了更简洁的 API,使得集成和使用更加简单。你不需要深入了解 single-spa 的复杂性,就可以快速上手

Turborepo 介绍: Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。它旨在通过并行构建、缓存和增量构建等技术,显著提高构建速度,从而提升开发效率。Turborepo 特别适用于大型 Monorepo(单一代码库)项目,其中包含多个子项目或包。

Turborepo 的核心特点

  1. 并行构建:Turborepo 可以并行构建多个包,充分利用多核处理器的性能。
  2. 缓存:Turborepo 会缓存构建结果,避免重复构建相同的代码,从而提高构建速度。
  3. 增量构建:Turborepo 支持增量构建,只构建发生变化的部分,而不是整个代码库。
  4. 依赖图:Turborepo 会自动分析包之间的依赖关系,确保构建顺序正确。
  5. Monorepo 支持:Turborepo 特别适用于 Monorepo 项目,可以管理多个包和子项目。

Turborepo 的使用场景

  1. 大型 Monorepo 项目:Turborepo 特别适用于包含多个包和子项目的大型 Monorepo 项目。
  2. 多团队协作:在多团队协作的项目中,Turborepo 可以提高构建速度,减少等待时间。
  3. 持续集成/持续部署(CI/CD) :Turborepo 可以显著提高 CI/CD 管道的构建速度,减少构建时间。

Turborepo VS Lerna:由于小编借助 Turborepo 主要是想实现 Monorepo 架构的思想,涉及到多包管理的时候也还有 Lerna 方案,以下简要对比分析一下这两者:

特性TurborepoLerna
核心功能高性能构建系统,支持并行构建、缓存和增量构建用于管理 JavaScript 项目的工具,支持版本管理、发布和依赖管理
适用场景大型 Monorepo 项目,特别适用于需要高性能构建的场景大型 Monorepo 项目,特别适用于需要版本管理和发布的场景
并行构建支持并行构建,充分利用多核处理器的性能支持并行构建,但需要手动配置
缓存支持缓存构建结果,避免重复构建相同的代码支持缓存,但需要手动配置
增量构建支持增量构建,只构建发生变化的部分支持增量构建,但需要手动配置
依赖图自动分析包之间的依赖关系,确保构建顺序正确自动分析包之间的依赖关系,确保构建顺序正确
版本管理不支持版本管理支持版本管理,可以自动更新依赖包的版本
发布不支持发布支持发布,可以自动发布包到 npm
依赖管理支持依赖管理,可以自动安装和更新依赖支持依赖管理,可以自动安装和更新依赖
配置配置简单,使用 turbo.json 文件进行配置配置相对复杂,使用 lerna.json 文件进行配置
社区支持社区相对较小,但正在快速发展社区较大,有丰富的文档和示例代码
性能高性能,特别适用于需要快速构建的场景性能较好,但需要手动配置以提高性能
集成可以与其他工具集成,如 Webpack、Babel 等可以与其他工具集成,如 Webpack、Babel 等

总的来说,Lerna 是较早出现的工具,主要用于管理 JavaScript 项目的版本管理、发布和依赖管理。Turborepo 是相对较新的工具,主要用于高性能构建,特别适用于需要快速构建的大型 Monorepo 项目。

pnpm: 小编在开头介绍的上一篇博客,也就是如何使用 vue3 + pnpm 搭建 monorepo 项目中有对于 pnpm 构建 monorepo 项目的优势进行介绍,这里就不再赘述...

初始化项目:

首先,我们需要初始化一个新的项目,并配置 Turborepo 和 pnpm。

mkdir qiankun-turborepo-pnpm-demo
cd qiankun-turborepo-pnpm-demo
pnpm init

配置 pnpm 工作区

在项目根目录下创建 pnpm-workspace.yaml 文件,定义工作区。

packages:
  - 'apps/*'
  - 'packages/*'

 配置 Turborepo

在项目根目录下创建 turbo.json 文件,配置 Turborepo,该文件用于配置 Turborepo 的构建管道和任务,以下是一个示例配置,实际开发看具体需求而定

{
  "$schema": "https://turborepo.org/schema.json",
  "tasks": {
    "build": {
      "dependsOn": [
        "^build"
      ],
      "outputs": [
        "dist/**"
      ]
    },
    "test": {
      "dependsOn": [
        "build"
      ],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false
    }
  }
}

现在再简单介绍一下 turbo.json 文件的作用,方便理解

在 Turborepo 项目中,turbo.json 文件是一个重要的配置文件,它用于定义和管理项目的构建、测试、运行等任务的执行流程。以下是 turbo.json 文件的主要作用:

  1. 定义任务管道(Pipeline) turbo.json 文件的核心功能是定义任务管道(Pipeline)。任务管道是一系列任务的执行顺序和依赖关系。通过定义任务管道,我们可以控制任务的执行顺序、并行执行的任务、以及任务之间的依赖关系。

例如:

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}
  • build: 表示构建任务。dependsOn: ["^build"] 表示当前包的构建任务依赖于所有依赖包的构建任务完成。
  • test: 表示测试任务。dependsOn: ["build"] 表示测试任务依赖于构建任务完成。
  • lint: 表示代码检查任务。outputs: [] 表示该任务没有输出文件。
  1. 配置缓存行为 turbo.json 文件还可以配置缓存行为,以提高任务的执行效率。Turborepo 会自动缓存任务的输出结果,并在后续执行时复用缓存结果,从而减少重复工作。

例如:

{
  "pipeline": {
    "build": {
      "cache": true,
      "outputs": ["dist/**"]
    }
  }
}
  • cache: true: 表示启用缓存。Turborepo 会缓存 build 任务的输出结果,并在下次执行时检查是否可以复用缓存。
  • outputs: 指定任务的输出文件路径,Turborepo 会根据这些路径来判断缓存是否有效。
  1. 定义全局配置 turbo.json 文件还可以包含一些全局配置,例如环境变量、全局任务等。

例如:

{
  "globalDependencies": ["package.json", "tsconfig.json"],
  "globalEnv": ["NODE_ENV", "API_URL"]
}
  • globalDependencies: 指定全局依赖文件,这些文件的变化会影响所有任务的执行。
  • globalEnv: 指定全局环境变量,这些变量会在所有任务中可用。
  1. 自定义任务 你可以通过 turbo.json 文件定义自定义任务,并指定它们的执行顺序和依赖关系。

例如:

{
  "pipeline": {
    "customTask": {
      "dependsOn": ["build"],
      "outputs": ["custom-output/**"]
    }
  }
}
  • customTask: 定义一个自定义任务,该任务依赖于 build 任务完成。
  • outputs: 指定自定义任务的输出文件路径。
  1. 集成其他工具 turbo.json 文件还可以与其他工具集成,例如 ESLint、TypeScript 等。你可以通过配置文件来指定这些工具的执行方式和参数。

例如:

{
  "pipeline": {
    "lint": {
      "outputs": [],
      "command": "eslint src/**/*.js"
    }
  }
}
  • command: 指定执行 lint 任务时运行的命令。

总结: turbo.json 文件在 Turborepo 项目中起到了核心配置的作用,它定义了任务的执行流程、缓存行为、全局配置等。通过合理配置 turbo.json,我们可以优化项目的构建、测试、运行等任务的执行效率,提高开发效率。

创建主应用

在 apps/ 目录下创建主应用 main-app

mkdir apps
cd apps
npm init vite@latest main-app --template vue

创建子应用

npm init vite@latest sub-app-1 --template vue
npm init vite@latest sub-app-2 --template vue

当前目录结构

├── qiankun-turborepo-pnpm-demo
   ├── apps
   |   ├── main-app // 主应用
   |   |   ├── .vscode
   |   |   ├── public
   |   |   ├── src
   |   |   |    ├── assets
   |   |   |    ├── components
   |   |   |    ├── router
   |   |   |    ├── stores
   |   |   |    ├── views
   |   |   |    ├── App.vue
   |   |   |    └── main.js
   |   |   ├── .gitignore
   |   |   ├── .prettierrc.json
   |   |   ├── eslint.config.js
   |   |   ├── index.html
   |   |   ├── package.json
   |   |   ├── README.md
   |   |   └── vite.config.js
   |   ├── sub-app-1 // 第一个子应用
   |   |   ├── .vscode
   |   |   ├── public
   |   |   ├── src
   |   |   |    ├── assets
   |   |   |    ├── components
   |   |   |    ├── router
   |   |   |    ├── stores
   |   |   |    ├── views
   |   |   |    ├── App.vue
   |   |   |    └── main.js
   |   |   ├── .gitignore
   |   |   ├── .prettierrc.json
   |   |   ├── eslint.config.js
   |   |   ├── index.html
   |   |   ├── package.json
   |   |   ├── README.md
   |   |   └── vite.config.js
   |   └── sub-app-2 // 第二个子应用
   |       ├── .vscode
   |       ├── public
   |       ├── src
   |       |    ├── assets
   |       |    ├── components
   |       |    ├── router
   |       |    ├── stores
   |       |    ├── views
   |       |    ├── App.vue
   |       |    └── main.js
   |       ├── .gitignore
   |       ├── .prettierrc.json
   |       ├── eslint.config.js
   |       ├── index.html
   |       ├── package.json
   |       ├── README.md
   |       └── vite.config.js
   ├── package.json // 公共库声明文件
   ├── pnpm-workspace.yaml // pnpm 管理的 workspace
   └── turbo.json // Turborepo 的配置文件


依赖安装

共享依赖安装:由于我们主应用和子应用都需要使用到 qiankun 这个库,所以我们就可以把共同的依赖安装在根目录下面,这很好地体现 Monorepo 架构的思想 ==》实现包的复用。

// 安装 qiankun 项目依赖

pnpm i qiankun -S -w


// vite-plugin-qiankun 提供了简洁的 API,使得在 Vite 项目中集成 qiankun 变得更加容易。

pnpm i vite-plugin-qiankun --save-dev -w

安装各个项目的依赖:在 Monorepo 架构中,如果我们在根目录执行 pnpm i,pnpm 会根据 pnpm-workspace.yaml 文件中的配置,自动安装所有子项目的依赖。这样可以确保所有子项目的依赖都被正确安装,而无需在每个子项目中单独执行 pnpm i

// 在项目根目录执行
 pnpm i

配置主应用

在主应用的 src/main.js 文件中配置 qiankun

import { createApp } from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
import router from './router';
import { createPinia } from 'pinia';

const app = createApp(App);
registerMicroApps([
  {
    name: 'sub-app-1',
    entry: '//localhost:8091',
    container: '#subapp-container-1',
    activeRule: '/sub-app-1',
  },
  {
    name: 'sub-app-2',
    entry: '//localhost:8092',
    container: '#subapp-container-2',
    activeRule: '/sub-app-2',
  },
]);

start();
app.use(router);
app.use(createPinia());
app.mount('#app');

在主应用的 src/App.vue 文件中配置子应用挂载点

<template>
  <div id="app">
    <router-view />
    // 注意这里的subapp-container-1要与上面main.js中配置的container属性值一致
    <div id="subapp-container-1"></div>
    // 与subapp-container-1同理
    <div id="subapp-container-2"></div>
  </div>
</template>

<script>
export default {
  name: 'App',
};
</script>

新建组件,用于加载子应用

<template>
  <div>
    <h1>Sub App 1</h1>
  </div>
</template>

<script>
export default {
  name: 'SubApp1View',
};
</script>

子应用2同理,不再赘述,如图:

微信图片_20241016212720.png

接下来在组件 HomeView.vue 中设置简单的跳转逻辑,分别展示子应用

<template>
  <div>
    <h1>Home Page</h1>
    <button @click="goToSubApp1">Go to Sub App 1</button>
    <button @click="goToSubApp2">Go to Sub App 2</button>
  </div>
</template>

<script>
export default {
  methods: {
    goToSubApp1() {
      this.$router.push({ name: 'SubApp1' });
    },
    goToSubApp2() {
      this.$router.push({ name: 'SubApp2' });
    },
  },
};
</script>

微信图片_20241016212952.png

接下来调整主应用的路由文件 src/router/index.js 设置路由关系

import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/HomeView.vue'),
  },
  {
    path: '/sub-app-1',
    name: 'SubApp1',
    component: () => import('../views/SubApp1View.vue'),
  },
  {
    path: '/sub-app-2',
    name: 'SubApp2',
    component: () => import('../views/SubApp2View.vue'),
  },
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
});

export default router;

微信图片_20241016213415.png

配置子应用

修改第一个子应用 sub-app-1 的 vite.config.js

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { name } from './package.json';
import qiankun from 'vite-plugin-qiankun'

export default defineConfig({
  plugins: [
    vue(),
    qiankun('sub-app-1', { // 微应用名字,与主应用注册的微应用名字保持一致
      useDevMode: true
    })
  ],
  server: {
    port: 8091,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  build: {
    rollupOptions: {
      input: 'src/main.js', // 或者 'src/main.ts'
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
        format: 'umd',
        name: `${name}-[name]`,
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
});

注意:由于路由模式为history,需要匹配子应用的入口规则,修改src/router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/HomeView.vue'),
  },
];

const router = createRouter({
  history: createWebHistory(
  qiankunWindow.__POWERED_BY_QIANKUN__
      ? '/sub-app-1/'
      : '/'
  ),
  routes,
});

export default router;

微信图片_20241016214206.png

在子应用的main.js里添加生命周期等相关配置

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import {
  renderWithQiankun,
  qiankunWindow
} from 'vite-plugin-qiankun/dist/helper'

let app

const render = (container) => {
  app = createApp(App)
  app
    .use(router)
    .mount(container ? container.querySelector('#app') : '#app')
}

const initQianKun = () => {
  renderWithQiankun({
    mount(props) {
      const { container } = props
      render(container)
    },
    bootstrap() {},
    unmount() {
      app.unmount()
    }
  })
}

qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()

微信图片_20241016214500.png

第二个子应用 sub-app-2 的配置基本一样,只需要注意 sub-app-1 中写为 sub-app-1 的地方替换为 sub-app-2就好了,还有就是端口号改一下,避免冲突

简单修改一下子应用的App.vue的内容,如下,如果子应用加载成功,页面将会显示应用的 hello from micro app 1

<template>
  <div>hello from micro app 1</div>
</template>

<script setup>
</script>


<style scoped>
</style>

 运行项目

修改根目录下的脚本配置以启动项目:

{
  "name": "qiankun-turborepo-pnpm-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "pnpm run --parallel dev",
    "build": "pnpm run --parallel build",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "qiankun": "^2.10.16"
  },
  "devDependencies": {
    "vite-plugin-qiankun": "^1.0.15"
  }
}

接着执行 pnpm dev, 项目就可以跑起来了

效果展示:

微信图片_20241016220502.png

微信图片_20241016220947.png

微信图片_20241016220906.png

结语

最后,如果需要有一些公共的UI组件库和工具的配置的话可以在 apps 同级目录下新建一个 packages 文件夹,里面存放共享包,实现过程可以参考小编开头提到的那篇文章

到这里小编的分享就结束了,感谢阅读,有不足的地方还请在评论区留下您的高见,谢谢!!!

仓库地址以及参考文献

本 demo 的仓库地址: Qiankun微前端 + Turborepo + pnpm 实践

小编参考了一下文档或者博主的文章,一一列举出来,并表示我衷心的感谢!!!

  1. qiankun 微前端官网
  2. Turborepo + Qiankun + pnpm 实践方案(一)
  3. 从零到一使用 turborepo + pnpm 搭建企业级 Monorepo 项目
  4. qiankun:vue3 + vite从开发到部署实现微前端