微前端 + Monorepo 架构:记录 Turborepo + Next.js + React + qiankun 的实践

384 阅读5分钟

微前端 + Monorepo 架构实践:基于 Turborepo + Next.js + React + qiankun

在当今的前端开发领域,微前端结合 Monorepo 架构的应用愈发广泛,它能有效地解决大型项目的模块拆分、团队协作以及独立部署等诸多问题。略微记录基于 Turborepo、Next.js、React 和 qiankun 的实践过程

一、使用 Turborepo 脚手架搭建项目

(一)安装方式

首先,我们通过 Turborepo 脚手架来生成对应的项目,具体的安装命令如下:

链接直达:turbo.build/repo/docs/g…

pnpm dlx create-turbo@latest --example with-tailwind

(二)项目初始化情况

项目初始化完成后,会呈现出特定的结构,大家可以参考如下的初始化截图):

Image_20241219101508.png

注意!!!:qiankun 是不支持 next 加载 next 应用的,因为这样会导致互相访问资源,进而产生循环问题。所以在我们的示例中,采用的是 next 加载一个用 vite 创建的 react 微应用这种方式来构建整个微前端架构。

二、Next.js 作为主应用加载微应用的相关实现

(一)定义相关接口与类型

1. types/global.ts

在这个文件中,定义了一些和用户认证相关的数据接口,用于在主应用与微应用之间传递必要的用户信息,代码如下:

export interface AuthUser {
  avatar: string; // 头像
  user_email: string; // 邮箱
  user_id: string; // 用户id
  user_name: string; // 用户名
}

export interface AuthUserData {
  user?: AuthUser;
}
2. common/sub-apps.ts

此文件主要用于定义微应用相关的配置信息,包括微应用的名称以及入口地址等内容,示例代码如下:

import { MERCHANT } from '../constants/sub-apps';

export interface SubApp {
  name: string;
  entry: string;
}

export const merchantApp: SubApp = {
  name: MERCHANT,
  entry: '//localhost:9528'
};

export const subApps: SubApp[] = [merchantApp];

(二)编写加载微应用的 Hook:use-qiankun.ts

这个 Hook 在整个微前端架构中起着关键作用,它负责加载具体的微应用,并处理加载过程中的各种状态以及与微应用实例的交互等逻辑,以下是详细代码及关键注释:

import { loadMicroApp, type MicroApp } from 'qiankun';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { SubApp } from '../common/sub-apps';
import type { AuthUserData } from '../types/global';

// 定义该 Hook 返回的数据结构类型,包含加载状态、微应用实例引用以及容器元素引用
interface QiankunReturn {
  loading: boolean;
  appInstance: React.RefObject<MicroApp | null>;
  containerRef: React.RefObject<HTMLDivElement>;
}

const useQiankun = (app: SubApp): QiankunReturn => {
  const [loading, setLoading] = useState(true);
  const appInstance = useRef<MicroApp | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  const loadFn = useCallback(async (): Promise<void> => {
    // 这里是一个非常关键的判断逻辑,在 Next.js 环境下,需要这样判断 window(next会预先服务端执行一次)
    // 千万不能简单地写成 typeof window === 'undefined' 然后直接 return,
    // 否则会出现问题(详细可参考:https://github.com/umijs/qiankun/issues/2037)
    if (typeof window!== 'undefined' && containerRef.current) {
      try {
        // 调用 qiankun 的 loadMicroApp 方法来加载微应用,传入相应的配置参数,包括微应用的基本信息、承载容器以及要传递给微应用的用户数据等
        const microApp = loadMicroApp<AuthUserData>({
         ...app,
          container: containerRef.current,
          props: {
            user: {
              user_id: '123456',
              user_name: 'zifer',
              user_email: 'super-test@test.com',
              avatar: ''
            }
          }
        });
        appInstance.current = microApp;
        // 等待微应用挂载完成,确保在挂载成功后再进行后续操作
        await microApp.mountPromise;
      } catch (error) {
        console.error('Failed to load micro app:', error);
      } finally {
        // 无论加载成功与否,最终都将加载状态设置为 false,表示加载过程结束
        setLoading(false);
      }
    }
  }, [app, containerRef]);

  useEffect(() => {
    void loadFn();

    return (): void => {
      if (appInstance.current) {
        // 卸载对应的微应用,不用特别判断微应用是否加载完成,否则会卸载失败
        // 如果判断了微应用加载完成再去卸载,会发现当路由切换再回来时,微应用加载失败,一片空白
        void appInstance.current.unmount().catch(() => {
          console.error(`Failed to unmount ${app.name} micro app`);
        });
      }
    };
  }, [loadFn, app]);

  return {
    loading,
    appInstance,
    containerRef
  };
};

export default useQiankun;

重点强调!!!:(强调三遍)上述代码中,在 Next.js 环境里对 window 的判断方式是经过实践验证且容易出现问题的地方,大家一定要严格按照这种嵌套判断的形式来处理,避免采用其他看似等效但实际会引发故障的写法。

(三)在组件中使用 use-qiankun Hook 加载子应用:MerchantApp.ts

下面这个组件展示了如何在 Next.js 的组件中实际运用前面定义的 use-qiankun Hook 来加载具体的微应用,通过结合 Spin 组件(这里假设 @repo/ui/Spin 是一个用于展示加载状态的组件),为用户呈现出一个比较友好的加载微应用的界面效果,代码如下:

'use client';

import React from 'react';
import Spin from '@repo/ui/Spin';
import useQiankun from '../../hooks/use-qiankun';
import { merchantApp } from '../../common/sub-apps';

const MerchantApp: React.FC = () => {
  const { loading, containerRef } = useQiankun(merchantApp);
  return (
    <div>
      <Spin spinning={loading}>
        <div ref={containerRef} />
      </Spin>
    </div>
  );
};

export default MerchantApp;

三、React 子应用的相关配置

(一)安装 qiankun 插件

在 React 子应用项目中,我们需要安装对应的 qiankun 插件来支持微应用与主应用之间的交互以及相关功能,在项目根目录执行如下命令(这里以名为 merchant 的子应用为例,对应 apps 目录下的 merchant 子目录):

# 在项目根目录执行如下,这里merchant是子应用的名称,也就是apps的merchant目录
pnpm add vite-plugin-qiankun-lite -D --filter merchant 

(二)修改 vite.config.ts 文件

为了让子应用能正确地被主应用加载并运行在微前端架构下,我们需要对 vite.config.ts 文件进行相应的配置修改,主要是添加 qiankun 相关的插件配置以及服务器相关的配置,代码修改示例如下:

+ import qiankun from 'vite-plugin-qiankun-lite'
+ import { name } from './package.json'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
    const env = loadEnv(mode, 'env', ENV_PREFIX)

    return {
        plugins: [
+           qiankun({ name, sandbox: true }),
        ],
        server: {
+           port: 9528,
            open: env.SERVER_OPEN_BROWSER === 'true',
+           cors: true,
+           origin: '*'
        },
       ...
    }
})

(三)修改入口文件 index.ts

入口文件是子应用启动和与主应用交互的关键所在,我们需要在这里定义好子应用的启动、挂载、卸载以及更新等逻辑,以便在微前端架构下能正确地响应主应用的各种操作,代码如下:

import React from 'react'
import { createRoot, Root } from 'react-dom/client'
import App from './App'
import { QiankunProps } from './types/qiankun'

let root: Root
const render = (props: any = {}) => {
    const container = props?.container? props.container.querySelector('#root') : document.getElementById('root')
    root = createRoot(container)
    root.render(
        <React.StrictMode>
            <App />
        </React.StrictMode>
    )
}

console.log('window.__POWERED_BY_QIANKUN__', window.__POWERED_BY_QIANKUN__)
if (!window.__POWERED_BY_QIANKUN__) {
    render()
}

export async function bootstrap() {
    console.log('🚀 ~ bootstrap ~ bootstrap:', bootstrap)
}

export async function mount(props: QiankunProps) {
    console.log('🚀 ~ mount ~ mount:', props)
    console.log('mount root', root)
    render(props)
}

export async function unmount(props: QiankunProps) {
    console.log('🚀 ~ unmount ~ unmount:', props)
    root.unmount()
}

export async function update(props: QiankunProps) {
    console.log('🚀 ~ unmount ~ unmount:', props)
    console.log('update root', root)
}

通过以上步骤,大致完成了基于 Turborepo + Next.js + React + qiankun 的微前端 + Monorepo 架构的基本实践搭建,大家可以根据实际项目需求在此基础上进一步扩展和优化相关功能与配置,比如主子应用通信机制