monorepo工程搭建和問(wèn)題解決

最近在扒拉fastgpt的前端源碼,遇到一些問(wèn)題,這里總結(jié)來(lái)說(shuō)下。

一、先了解一下兩個(gè)東西

1、monorepo是什么?

Monorepo(單一代碼倉(cāng)庫(kù)) 是一種代碼管理策略,指將多個(gè)相關(guān)項(xiàng)目或服務(wù)的代碼集中存儲(chǔ)在同一個(gè)版本控制系統(tǒng)倉(cāng)庫(kù)(如 Git)中,而非分散在多個(gè)獨(dú)立的倉(cāng)庫(kù)中。這種模式在大型技術(shù)團(tuán)隊(duì)或復(fù)雜項(xiàng)目中越來(lái)越流行,尤其受到 Google、Facebook、Microsoft 等科技巨頭的青睞。
我們就知道它的優(yōu)勢(shì)特點(diǎn):簡(jiǎn)化依賴管理、代碼和依賴復(fù)用、統(tǒng)一工具鏈等。當(dāng)然了,一般來(lái)說(shuō),很多技術(shù)都是優(yōu)劣共存的,這樣的框架也會(huì)導(dǎo)致后期依賴權(quán)限管理和測(cè)試等成本有所提高,尤其是你將來(lái)要升級(jí),影響范圍就會(huì)比較大。

2、fastgpt是什么?

FastGPT 是一個(gè)基于 LLM 大語(yǔ)言模型的知識(shí)庫(kù)問(wèn)答系統(tǒng),將智能對(duì)話與可視化編排完美結(jié)合,讓 AI 應(yīng)用開發(fā)變得簡(jiǎn)單自然。無(wú)論您是開發(fā)者還是業(yè)務(wù)人員,都能輕松打造專屬的 AI 應(yīng)用。
它幫我們實(shí)現(xiàn)了一整套的算法、存儲(chǔ)、引擎、以及前端的知識(shí)庫(kù)維護(hù)、工作流設(shè)計(jì),最終提供直接與你的 AI Agent 的對(duì)話進(jìn)行測(cè)試以及相關(guān)接口??偠灾?,F(xiàn)astGPT 極大地簡(jiǎn)化了構(gòu)建定制化、企業(yè)級(jí) AI Agent 的過(guò)程,讓你能快速將想法落地為實(shí)際可用的智能應(yīng)用。算是Agent的額開箱即用方案。

二、monorepo工程搭建

1、直接上目錄

我直接展示我的目錄吧,因?yàn)槭前窃创a,有些組件都是整個(gè)目錄拿來(lái)的,方便后面敘述。

fastgpt/
├── pnpm-workspace.yaml
├── package.json
├── tsconfig.json
├── packages/
│   ├── global/
│   │   ├── support/
│   │   ├── common/
│   │   └── package.json
│   └── web/
│       ├── common/
│       ├── components/
│       ├── hooks/
│       └── package.json
└── projects/
    └── app/
        ├── src/
        │   └── app/
        │       └── page.tsx
        ├── next.config.js
        └── package.json

大概就這樣了,fastgpt是根目錄,通過(guò) pnpm init創(chuàng)建了package.json,projects/app下是通過(guò)nextjs創(chuàng)建的,這里我在json配置里name定義為@fastgpt/app,方便后面使用。

2、開始配置

  1. 在根目錄下創(chuàng)建pnpm-workspace.yaml 文件,配置工作區(qū)域
 packages:
  - 'packages/*'
  - 'projects/*'
  1. packages/web 的 package.json 中這樣寫:
{
   "dependencies": {
     "@my-monorepo/global": "workspace:*"
   }
 }
  1. project/apppackage.json中這樣寫:
{
   "dependencies": {
     "@fastgpt/global": "workspace:*",
     "@fastgpt/web": "workspace:*",
   }
 }
// 這里@fastgpt/global這樣寫是因?yàn)楹芏嘣?yè)面和組件都用的整個(gè)命名路徑,為了減少修改,保持原樣了
  1. 配置共享tsconfig.json,尤其注意下面的paths,其他工程里可以把這個(gè)做為基礎(chǔ)配置引入
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "baseUrl": ".",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["projects/app/src/*"],
      "@fastgpt/*": ["packages/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["**/node_modules"]
}
  1. next.config.js 中添加 webpack 別名
const nextConfig:NextConfig = {
  webpack: (config, { isServer }) => {
    // 添加別名解析
    config.resolve.alias = {
      ...config.resolve.alias,
      '@fastgpt/web': path.resolve(__dirname, '../../packages/web'),
      '@fastgpt/global': path.resolve(__dirname, '../../packages/global'),
    };
    // 重要:禁用 symlinks 解析
    config.resolve.symlinks = false;    
    return config;
  }
};

3、運(yùn)行

按照正常的配置下來(lái),這樣應(yīng)該是可以了

pnpm run --filter @fastgpt/app
//或進(jìn)入project/app下運(yùn)行

三、問(wèn)題分析

1、問(wèn)題描述

這里只提一個(gè)典型的問(wèn)題來(lái)檢驗(yàn)工程的效果吧。(其實(shí)你們也可以不看,因?yàn)榇a確實(shí)沒(méi)問(wèn)題,上下文,路徑等,都正常,只不過(guò)剛開始不了解問(wèn)題原因的時(shí)候,可能需要逐一排查)

  1. _app.js中引入
import ChakraUIContext from '@/web/context/ChakraUI';
return (
  <ChakraUIContext>
   <Layout>{setLayout(<Component {...pageProps} />)}</Layout>
</ChakraUIContext>
);
  1. \web\context\ChakraUI.tsx的代碼如下:
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
return (
  <ChakraProvider theme={theme}>
    <ColorModeScript initialColorMode={theme.config.initialColorMode} />
    {children}
  </ChakraProvider>
);
  1. 以上就是chakra-ui的上下文引用關(guān)系,確認(rèn)是沒(méi)有問(wèn)題的。在app中的頁(yè)面引入了公共hook
import { useToast } from '@fastgpt/web/hooks/useToast'; // 引入
const { toast } = useToast(); // 實(shí)例
toast({title:'hello'}) // 在適當(dāng)?shù)奈恢谜{(diào)用
  1. 最后看一下整個(gè)hook的源碼
import { useToast as uToast, type UseToastOptions } from '@chakra-ui/react';
import { type CSSProperties, useCallback } from 'react';
import { useTranslation } from 'next-i18next';

export const useToast = (props?: UseToastOptions & { containerStyle?: CSSProperties }) => {
  const { containerStyle, ...toastProps } = props || {};
  const { t } = useTranslation();

  const toast = uToast({
    position: 'top',
    duration: 2000,
    containerStyle: {
      fontSize: 'sm',
      ...containerStyle
    },
    ...toastProps
  });

  const myToast = useCallback(
    (options?: UseToastOptions) => {
      if (options?.title || options?.description) {
        toast({
          ...(options.title && { title: t(options.title as any) }),
          ...(options.description && { description: t(options.description as any) }),
          ...options
        });
      }
    },
    [props]
  );

  return {
    toast: myToast
  };
};

鑒于是copy過(guò)來(lái)的,所以這里所有的代碼都是沒(méi)問(wèn)題的,但是結(jié)果確實(shí),toast調(diào)用的時(shí)候,頁(yè)面沒(méi)有交互,盡管我打印了toast,方法存在,但是,它在頁(yè)面上卻始終不反應(yīng)。

2、嘗試解決辦法

  1. 檢查項(xiàng)目名稱:各個(gè)項(xiàng)目的命名不要重復(fù),查看package.json中的name;
  2. 依賴安裝問(wèn)題:雖然配置了工作區(qū),但可能某個(gè)依賴沒(méi)有正確安裝;
  3. Hooks 使用條件useToast 可能依賴于某個(gè)上下文(Context),而該上下文在 monorepo 結(jié)構(gòu)中沒(méi)有正確提供;
  4. 檢查路徑別名:在 Next.js 項(xiàng)目中,需要在 tsconfig.jsonnext.config.js 中配置路徑別名;
  5. 檢查主題配置:在 @fastgpt/web/styles/theme 中導(dǎo)出的主題是否包含了 toast 的樣式?如果沒(méi)有特別配置,Chakra UI 的默認(rèn)主題應(yīng)該包含 toast;
  6. 根目錄install:切記要在根目錄install,如果在子包操作,很有可能把重復(fù)的依賴下載下來(lái);
  7. SSR 相關(guān)的問(wèn)題:如果 toast 在服務(wù)端被調(diào)用,或者在服務(wù)端渲染時(shí)調(diào)用了 toast,可能導(dǎo)致問(wèn)題(這個(gè)我沒(méi)有驗(yàn)證,但是nextJs不應(yīng)該有這個(gè)問(wèn)題,否則頁(yè)面就不能用了);
  8. 多個(gè) React 實(shí)例:在 monorepo 中,如果多個(gè)項(xiàng)目或包安裝了 React,可能會(huì)導(dǎo)致多個(gè) React 實(shí)例。這會(huì)破壞 Context 的工作機(jī)制,因?yàn)?Context 依賴于同一個(gè) React 實(shí)例;
//在`projects/app/src/pages/_app.tsx`中添加以下代碼,注意window的使用時(shí)機(jī)
import React from 'react';
window.__APP_REACT__ = React;
// 同樣在toast組件里也這樣設(shè)置.__WEB_REACT__
// 在彈出toast的地方判斷
useEffect(() => {
    console.log('App React:', window.__APP_REACT__);
    console.log('Web React:', window.__WEB_REACT__);
    console.log('是否同一個(gè)實(shí)例:', window.__APP_REACT__ === window.__WEB_REACT__);}, 
[]);
// 發(fā)現(xiàn)是同一個(gè)
  1. transpilePackages:確保中包含了需要轉(zhuǎn)譯的包,增加 next.config.js 中配置
const nextConfig = {
  // 移除 experimental.esmExternals 配置
  experimental: {
    externalDir: true, // 保留這個(gè)
    // esmExternals: 'loose' // 刪除這一行
  },
}
  1. 確保單例模式:增加 next.config.js 中webpack的配置(這個(gè)不需要)
const nextConfig:NextConfig = {
  webpack: (config, { isServer }) => {
      // 添加別名解析
      config.resolve.alias = {
        // 強(qiáng)制單例 React
        'react': path.resolve(__dirname, 'node_modules/react'),
        'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
        '@chakra-ui/react': path.resolve(__dirname, 'node_modules/@chakra-ui/react'),
      };
      return config;
    }
};
  1. 強(qiáng)制統(tǒng)一版本
{"pnpm": {"overrides": {"react": "^18.2.0","react-dom": "^18.2.0"}}}
  1. 提升公共依賴:在.npmrc中配置
# 提升所有依賴
shamefully-hoist=true
# 或僅提升react
public-hoist-pattern[]=react*public-hoist-pattern[]=react-dom*
  1. 手動(dòng)指定解析路徑(這個(gè)不需要)
// next.config.js
webpack: (config) => {
  config.resolve.alias = {
    ...config.resolve.alias,
    react: path.resolve(__dirname, '../../node_modules/react'),
    'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
  };
}

3、正確辦法

我真是嘗試太多方法了,腦子都漿糊了。但是我們知道,往往越是復(fù)雜的問(wèn)題,解決辦法就越簡(jiǎn)單!

問(wèn)題解決packages/web/node_modules和根目錄下都存在react、@chakra-ui等依賴,刪除子包中的依賴就可以了?。。?/p>

  1. 規(guī)范package.json,刪除子包的冗余依賴設(shè)置,手動(dòng)提取公共依賴到根目錄;
  2. 刪除所有node_modules重新install,這樣子包就不會(huì)再重復(fù)下載依賴了;
  3. 前面的解決方法嘗試有一些是無(wú)效的,可以先進(jìn)行這個(gè)操作如果還有問(wèn)題再去嘗試;

四、遺留問(wèn)題

1、fastgpt源碼中,在packages/web/node_modules和根目錄中,同樣存在react、@chakra-ui等依賴,但是它并沒(méi)有出現(xiàn)我這樣的問(wèn)題,檢查了相關(guān)配置,也沒(méi)有特殊處理,還需要再研究一下。

2、在 monorepo 中,我們通常會(huì)在根目錄運(yùn)行 pnpm install 來(lái)安裝所有工作區(qū)項(xiàng)目的依賴。pnpm 的工作區(qū)特性會(huì)盡量將依賴提升到根目錄的 node_modules 中,除非有版本沖突。但是我在根目錄和子包都使用了同版本react的依賴,它并沒(méi)有忽略掉子包的依賴下載,可能還有特殊配置吧。

3、在前面排查問(wèn)題的時(shí)候,我曾經(jīng)排查是否有多個(gè)react實(shí)例的問(wèn)題,事實(shí)證明是同一個(gè),但是從解決方法:刪除了子包里的共同依賴,又不清楚到底是不是同實(shí)例的問(wèn)題了。

原文鏈接:monorepo工程搭建和問(wèn)題解決 | 1Z5K

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容