最近在扒拉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、開始配置
- 在根目錄下創(chuàng)建
pnpm-workspace.yaml文件,配置工作區(qū)域
packages:
- 'packages/*'
- 'projects/*'
- packages/web 的
package.json中這樣寫:
{
"dependencies": {
"@my-monorepo/global": "workspace:*"
}
}
- 在
project/app的package.json中這樣寫:
{
"dependencies": {
"@fastgpt/global": "workspace:*",
"@fastgpt/web": "workspace:*",
}
}
// 這里@fastgpt/global這樣寫是因?yàn)楹芏嘣?yè)面和組件都用的整個(gè)命名路徑,為了減少修改,保持原樣了
- 配置共享
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"]
}
- 在
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í)候,可能需要逐一排查)
- 在
_app.js中引入
import ChakraUIContext from '@/web/context/ChakraUI';
return (
<ChakraUIContext>
<Layout>{setLayout(<Component {...pageProps} />)}</Layout>
</ChakraUIContext>
);
-
\web\context\ChakraUI.tsx的代碼如下:
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
return (
<ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
{children}
</ChakraProvider>
);
- 以上就是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)用
- 最后看一下整個(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、嘗試解決辦法
- 檢查項(xiàng)目名稱:各個(gè)項(xiàng)目的命名不要重復(fù),查看package.json中的name;
- 依賴安裝問(wèn)題:雖然配置了工作區(qū),但可能某個(gè)依賴沒(méi)有正確安裝;
-
Hooks 使用條件:
useToast可能依賴于某個(gè)上下文(Context),而該上下文在 monorepo 結(jié)構(gòu)中沒(méi)有正確提供; -
檢查路徑別名:在 Next.js 項(xiàng)目中,需要在
tsconfig.json和next.config.js中配置路徑別名; -
檢查主題配置:在
@fastgpt/web/styles/theme中導(dǎo)出的主題是否包含了 toast 的樣式?如果沒(méi)有特別配置,Chakra UI 的默認(rèn)主題應(yīng)該包含 toast; - 根目錄install:切記要在根目錄install,如果在子包操作,很有可能把重復(fù)的依賴下載下來(lái);
- 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è)面就不能用了);
- 多個(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è)
-
transpilePackages:確保中包含了需要轉(zhuǎn)譯的包,增加
next.config.js中配置
const nextConfig = {
// 移除 experimental.esmExternals 配置
experimental: {
externalDir: true, // 保留這個(gè)
// esmExternals: 'loose' // 刪除這一行
},
}
-
確保單例模式:增加
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;
}
};
- 強(qiáng)制統(tǒng)一版本
{"pnpm": {"overrides": {"react": "^18.2.0","react-dom": "^18.2.0"}}}
-
提升公共依賴:在
.npmrc中配置
# 提升所有依賴
shamefully-hoist=true
# 或僅提升react
public-hoist-pattern[]=react*public-hoist-pattern[]=react-dom*
- 手動(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>
- 規(guī)范
package.json,刪除子包的冗余依賴設(shè)置,手動(dòng)提取公共依賴到根目錄; - 刪除所有node_modules重新
install,這樣子包就不會(huì)再重復(fù)下載依賴了; - 前面的解決方法嘗試有一些是無(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)題了。