最近在扒拉fastgpt的前端源码,遇到一些问题,这里总结来说下。
一、先了解一下两个东西
1、monorepo是什么?
Monorepo(单一代码仓库) 是一种代码管理策略,指将多个相关项目或服务的代码集中存储在同一个版本控制系统仓库(如 Git)中,而非分散在多个独立的仓库中。这种模式在大型技术团队或复杂项目中越来越流行,尤其受到 Google、Facebook、Microsoft 等科技巨头的青睐。
我们就知道它的优势特点:简化依赖管理、代码和依赖复用、统一工具链等。当然了,一般来说,很多技术都是优劣共存的,这样的框架也会导致后期依赖权限管理和测试等成本有所提高,尤其是你将来要升级,影响范围就会比较大。
2、fastgpt是什么?
FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,将智能对话与可视化编排完美结合,让 AI 应用开发变得简单自然。无论您是开发者还是业务人员,都能轻松打造专属的 AI 应用。
它帮我们实现了一整套的算法、存储、引擎、以及前端的知识库维护、工作流设计,最终提供直接与你的 AI Agent 的对话进行测试以及相关接口。总而言之,FastGPT 极大地简化了构建定制化、企业级 AI Agent 的过程,让你能快速将想法落地为实际可用的智能应用。算是Agent的额开箱即用方案。
二、monorepo工程搭建
1、直接上目录
我直接展示我的目录吧,因为是扒源码,有些组件都是整个目录拿来的,方便后面叙述。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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是根目录,通过 pnpm init创建了package.json,projects/app下是通过nextjs创建的,这里我在json配置里name定义为@fastgpt/app,方便后面使用。
2、开始配置
- 在根目录下创建
pnpm-workspace.yaml文件,配置工作区域
|
1 2 3 |
packages: - 'packages/*' - 'projects/*' |
- packages/web 的
package.json中这样写:
|
1 2 3 4 5 |
{ "dependencies": { "@my-monorepo/global": "workspace:*" } } |
- 在
project/app的package.json中这样写:
|
1 2 3 4 5 6 7 |
{ "dependencies": { "@fastgpt/global": "workspace:*", "@fastgpt/web": "workspace:*", } } // 这里@fastgpt/global这样写是因为很多原页面和组件都用的整个命名路径,为了减少修改,保持原样了 |
- 配置共享
tsconfig.json,尤其注意下面的paths,其他工程里可以把这个做为基础配置引入。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ "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 别名
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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、运行
按照正常的配置下来,这样应该是可以了
|
1 2 |
pnpm run --filter @fastgpt/app //或进入project/app下运行 |
三、问题分析
1、问题描述
这里只提一个典型的问题来检验工程的效果吧。(其实你们也可以不看,因为代码确实没问题,上下文,路径等,都正常,只不过刚开始不了解问题原因的时候,可能需要逐一排查)
- 在
_app.js中引入
|
1 2 3 4 5 6 |
import ChakraUIContext from '@/web/context/ChakraUI'; return ( <ChakraUIContext> <Layout>{setLayout(<Component {...pageProps} />)}</Layout> </ChakraUIContext> ); |
\web\context\ChakraUI.tsx的代码如下:
|
1 2 3 4 5 6 7 |
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'; return ( <ChakraProvider theme={theme}> <ColorModeScript initialColorMode={theme.config.initialColorMode} /> {children} </ChakraProvider> ); |
- 以上就是chakra-ui的上下文引用关系,确认是没有问题的。在app中的页面引入了公共hook
|
1 2 3 |
import { useToast } from '@fastgpt/web/hooks/useToast'; // 引入 const { toast } = useToast(); // 实例 toast({title:'hello'}) // 在适当的位置调用 |
- 最后看一下整个hook的源码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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过来的,所以这里所有的代码都是没问题的,但是结果确实,toast调用的时候,页面没有交互,尽管我打印了toast,方法存在,但是,它在页面上却始终不反应。
2、尝试解决办法
- 检查项目名称:各个项目的命名不要重复,查看package.json中的name;
- 依赖安装问题:虽然配置了工作区,但可能某个依赖没有正确安装;
- Hooks 使用条件:
useToast可能依赖于某个上下文(Context),而该上下文在 monorepo 结构中没有正确提供; - 检查路径别名:在 Next.js 项目中,需要在
tsconfig.json和next.config.js中配置路径别名; - 检查主题配置:在
@fastgpt/web/styles/theme中导出的主题是否包含了 toast 的样式?如果没有特别配置,Chakra UI 的默认主题应该包含 toast; - 根目录install:切记要在根目录install,如果在子包操作,很有可能把重复的依赖下载下来;
- SSR 相关的问题:如果 toast 在服务端被调用,或者在服务端渲染时调用了 toast,可能导致问题(这个我没有验证,但是nextJs不应该有这个问题,否则页面就不能用了);
- 多个 React 实例:在 monorepo 中,如果多个项目或包安装了 React,可能会导致多个 React 实例。这会破坏 Context 的工作机制,因为 Context 依赖于同一个 React 实例;
|
1 2 3 4 5 6 7 8 9 10 11 |
//在`projects/app/src/pages/_app.tsx`中添加以下代码,注意window的使用时机 import React from 'react'; window.__APP_REACT__ = React; // 同样在toast组件里也这样设置.__WEB_REACT__ // 在弹出toast的地方判断 useEffect(() => { console.log('App React:', window.__APP_REACT__); console.log('Web React:', window.__WEB_REACT__); console.log('是否同一个实例:', window.__APP_REACT__ === window.__WEB_REACT__);}, []); // 发现是同一个 |
- transpilePackages:确保中包含了需要转译的包,增加
next.config.js中配置
|
1 2 3 4 5 6 7 |
const nextConfig = { // 移除 experimental.esmExternals 配置 experimental: { externalDir: true, // 保留这个 // esmExternals: 'loose' // 删除这一行 }, } |
- 确保单例模式:增加
next.config.js中webpack的配置(这个不需要)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
const nextConfig:NextConfig = { webpack: (config, { isServer }) => { // 添加别名解析 config.resolve.alias = { // 强制单例 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 |
{"pnpm": {"overrides": {"react": "^18.2.0","react-dom": "^18.2.0"}}} |
- 提升公共依赖:在
.npmrc中配置
|
1 2 3 4 |
# 提升所有依赖 shamefully-hoist=true # 或仅提升react public-hoist-pattern[]=react*public-hoist-pattern[]=react-dom* |
- 手动指定解析路径(这个不需要)
|
1 2 3 4 5 6 7 8 |
// 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、正确办法
我真是尝试太多方法了,脑子都浆糊了。但是我们知道,往往越是复杂的问题,解决办法就越简单!
问题解决:packages/web/node_modules和根目录下都存在react、@chakra-ui等依赖,删除子包中的依赖就可以了!!!
- 规范
package.json,删除子包的冗余依赖设置,手动提取公共依赖到根目录; - 删除所有node_modules重新
install,这样子包就不会再重复下载依赖了; - 前面的解决方法尝试有一些是无效的,可以先进行这个操作如果还有问题再去尝试;
四、遗留问题
1、fastgpt源码中,在packages/web/node_modules和根目录中,同样存在react、@chakra-ui等依赖,但是它并没有出现我这样的问题,检查了相关配置,也没有特殊处理,还需要再研究一下。
2、在 monorepo 中,我们通常会在根目录运行 pnpm install 来安装所有工作区项目的依赖。pnpm 的工作区特性会尽量将依赖提升到根目录的 node_modules 中,除非有版本冲突。但是我在根目录和子包都使用了同版本react的依赖,它并没有忽略掉子包的依赖下载,可能还有特殊配置吧。
3、在前面排查问题的时候,我曾经排查是否有多个react实例的问题,事实证明是同一个,但是从解决方法:删除了子包里的共同依赖,又不清楚到底是不是同实例的问题了。