React
快来分享你的内容吧~
我的本科毕业设计《智搭低代码应用生成系统》开源啦!!!
# 智搭低代码应用生成系统 ## 项目简介    本项目是我学习完编程导航的零代码应用生成平台系列教程交的作业,同时也是我的软件工程专业的本科毕业设计。 本项目由3个 Github 仓库共同组成,它们分别是: 后端:[ymz6/zhida-backend](https://github.com/ymz6/zhida-backend) 前端:[ymz6/zhida-frontend](https://github.com/ymz6/zhida-frontend) 应用项目模板:[ymz6/zhida-application-template](https://github.com/ymz6/zhida-application-template) 后端是用 Java 21 + SpringBoot 3 开发的,前端则是用 React 。 本项目的亮点: - 增加思考模式支持 - 通过预制项目模板来初始化应用 - 还有一些我自己也忘了,请各位鱼友自行探索吧! - 另外附带我开发这个项目的完整git提交记录,各位鱼友可以看到我起起落落落落落又起的、曲折的开发过程,方便各位从中学到些东西 毕业之后,我可能不打算对再此项目继续维护了,也许会存在一些大大小小的我没发现的Bug,有能力的鱼友可以 Fork 本项目进行二次开发,十分期待看到大家的二开作品! 最后真心希望我的项目对大家有所帮助,Love & Peace! ## 所需运行环境 后端:JDK 21 前端:Node.js >= 24 数据库:MySQL、Redis OSS:推荐用 Docker 起一个 RustFS 容器 操作系统:最好是 Windows,因为我为了开发方便,直接把 Nginx 可执行文件放在了后端项目中,使用其他操作系统的同学请自行下载 Nginx,并且我还用 AI 写了一些方便本地开发用的 Powershell 脚本 截图服务依赖:本项目有部署后自动截取应用封面图片的功能,由于固定使用的 Chrome 浏览器的驱动,因此若想要正常使用本功能,请务必确认在你本地电脑上安装有 Chrome 浏览器! ## 如何运行本项目? > 注意,本项目最终只是为了在本地演示给学校老师检查,因此并未考虑过部署上线的事情,所以跑起来会有些许麻烦。 > > 如果有鱼友有这方面需求,可以 fork 本项目进行改造。 ### 后端 ① 执行后端项目中的数据库初始化脚本(`sql/schemas.sql`) ② 准备自己的 AI Api Key。本项目为了开发方便,并未将大语言模型配置写在`application.yaml`中,而是通过代码进行配置,具体位于`app/src/main/java/org/ymz/app/ai/config/LLMsFactoryConfig.java`中,你需要在环境变量中设置对应大语言模型的 API Key。 ③ 以下配置需要你自行修改,推荐定位到后端项目的根目录中: ```yaml zhida: app-path: tmp-dir: C:/Users/ymz/Data/Codes/zhida/backend/tmp template-dir: C:/Users/ymz/Data/Codes/zhida/backend/project-template/zhida-react-project app-url: deploy-base-url: http://localhost:80 ``` ⑤ 准备 OSS 的访问密钥,本项目开发时使用的是通过本地 Docker 运行的 RustFS 容器。你可以替换为任何你喜欢的兼容 S3 协议的对象存储服务。这里只需要创建一个公共 bucket 即可,目前写的私有 bucket 可以不用动,原本我打算用来存放私有资源的,但是最终并未用上。 ⑥ 如果你要测试部署功能,请提前启动后端项目中集成的 Nginx。为了方便我让 AI 写了个用于方便启动和关闭 Nginx 的 Powershell 脚本,你可以通过它来启动/关闭 Nginx。 另外,你还需要修改 Nginx 的配置文件(`nginx/conf/nginx.conf`),重点是这一段: ```nginx # 项目部署根目录 root C:/Users/ymz/Data/Codes/zhida/backend/tmp/app-deploy; ``` 推荐定位到后端的根目录。 ⑦ 上述内容配置好后,便可以使用本项目内置的 maven 包装器来安装后端所需依赖,然后启动运行了。 ### 前端 本项目使用 pnpm 作为前端的包管理器。 安装完依赖就可以启动了
React19 学习笔记
# 写在开头 **本文的定位** 本文的主要目标,是帮助你建立对 React 核心机制的**系统性心智模型**,而非提供大量手把手的实操示例。 如果你希望在理解原理的同时配合更多实操练习,推荐结合 [React 简明教程(2026 版)](https://www.bilibili.com/video/BV1jPvDBxE1C/?share_source=copy_web&vd_source=5380fa0514d9710134c69943bf7dc51c) 一起学习,效果会更好。 **文章状态说明** 目前这篇学习笔记已经将 React 的核心内容基本上都覆盖到了。后续会结合评论区的反馈,对内容进行动态调整和优化。不得不说,这是我写过的最棒的一篇笔记。 **关于正确性与讨论** 受限于个人理解,文中内容难免存在表述不够严谨或理解偏差的地方。如果你发现任何问题,或有不同看法,欢迎在评论区指出和讨论。每一条反馈我都会认真阅读,也欢迎提出自己的疑问,大家一起交流学习。 **阅读前的基础假设** 在阅读本文之前,我默认你已经具备基本的前端知识,主要包括 **HTML** 和 **JavaScript**(对 JavaScript 的理解要求相对较高),CSS 并非必须。 是否学过 Vue 并不影响 React 的学习。即便你有 Vue 的使用经验,也不建议在阅读时将其相关概念直接套用到 React 中;本文将从新手视角出发,由浅入深地展开 React 的核心概念。 **React 版本说明** 本文内容基于 **React 19** 进行学习与整理。 # 初识 React 组件 React 组件是一段**可复用的 UI 代码单元**。它就像积木一样,界面可以被拆分为许多小块,每一块都是一个组件,这些组件组合在一起,构成完整的应用。 在现代 React 中,主流的组件写法是**函数组件**。也就是说用 JS 函数来定义组件。只不过这个函数和普通 JS 函数相比需要注意以下两点: 1. 组件名称(也就是函数名)**必须以大写字母开头**。 2. 函数的返回值是 **JSX**(看起来像 HTML 的语法),用于定义这个组件对应的界面结构。 例如: ```jsx function App() { return <h1>Hello World!</h1> } ``` # 初识 JSX JSX 是 JavaScript 的一种语法扩展,允许你在 JavaScript 文件中书写类似 HTML 的标签语法。从语法层面看,JSX 本质上是一个 JavaScript 表达式,用来表示界面结构。 例如,在刚刚的`App`组件中,`<h1>Hello World!</h1>`就是一个 JSX 表达式。 浏览器本身不支持 JSX ,因此其需要先被编译为普通的JavaScript 才能被浏览器执行。 那么 JSX 表达式的编译结果长什么样? 其实它会被编译为一个函数调用: 例如: ```jsx function App() { return React.createElement("h1", null, "Hello World!"); } ``` 这个函数的参数为: > 源自 React 官方文档,了解即可。 - `type`:`type` 参数必须是一个有效的 React 组件类型,例如一个字符串标签名(如 `'div'` 或 `'span'`),或一个 React 组件(一个函数式组件、一个类式组件,或者是一个特殊的组件如 [`Fragment`](https://zh-hans.react.dev/reference/react/Fragment))。 - `props`:`props` 参数必须是一个对象或 `null`。如果你传入 `null`,它会被当作一个空对象。创建的 React 元素的 `props` 与这个参数相同。注意,`props` 对象中的 `ref` 和 `key` 比较特殊,它们 **不会** 作为 `element.props.ref` 和 `element.props.key` 出现在创建的元素 `element` 上,而是作为 `element.ref` 和 `element.key` 出现。 - **可选** `...children`:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、[portal](https://zh-hans.react.dev/reference/react-dom/createPortal)、空节点(`null`、`undefined`、`true` 和 `false`),以及 React 节点数组。 这个函数会返回一个 **React 元素**(本质上是一个 JS 对象)。你不需要关心它的具体结构,只需要知道:它代表了当前组件对应的界面结构。 尽管 JSX 的写法很像你熟悉的 HTML,但 JSX 不是 HTML;而且它的语法更严格,需要遵守更多约束。 ① 组件的返回值必须只有一个根元素 组件一次只能返回一个“根元素”。也就是说,在 `return (...)` 里面,最外层不能并列写两个标签。 以下是错误示例: ```jsx function App() { return ( <h1>A</h1> <h1>B</h1> ) } ``` 解决方案:仅需让一个标签作为它们的父元素就好。 例如: ```jsx function App() { return ( <div> <h1>A</h1> <h1>B</h1> </div> ) } ``` 但这样会在最终的 DOM 中多出一个用于包裹的 `<div>`。 不过 React 提供了一个特殊的标签:`<></>` (Fragment),你可以这样用: ```jsx function App() { return ( <> <h1>A</h1> <h1>B</h1> </> ) } ``` Fragment 只用于分组,不会被渲染成真实 DOM 节点。 ② 所有标签都必须闭合 JSX 要求标签必须正确闭合: - 成对标签必须有结束标签:`<li>oranges</li>` - 没有内容的元素必须写成自闭合:`<img />`、`<input />` ③ 属性名通常用 camelCase JSX 最终会被编译为对 `React.createElement` 的函数调用,其中写在 JSX 上的属性,会被 React 收集起来,组成一个对象,并在组件渲染时传入组件。这个对象在 React 中被称为 `props`,它是组件接收外部数据的主要方式,后文会专门介绍。 因此,React/DOM 中的大多数属性名采用 camelCase(例如 `onClick`、`tabIndex`)。也存在少数的例外情况(例如以 `data-`、`aria-` 开头的属性),它们由于历史原因仍保持 kebab-case 的写法。 另外,`class` 和 `for` 在 JSX 中分别写成 `className` 和 `htmlFor`,这是为了避免与 JS 关键字冲突。 # 深入 JSX > 在对 JSX 建立了一个初步的认知之后,接下来就可以进一步看看 JSX 在实际编写 UI 的几种常见使用方式。 ## 插值语法 在 JSX 中,使用插值语法`{}` 可以在标签结构中嵌入 **JavaScript 表达式**,用来动态生成 UI 内容。`{}` 中只能书写表达式,其计算结果会在渲染时被插入到对应的位置。 例如: ```jsx function App() { const text = "Hello World!" return <h1>{text}</h1> } ``` 在这个例子中,`text` 是一个普通的 JavaScript 变量,JSX 会在渲染时读取它的值,并将结果显示在 `<h1>` 标签中。 当 JS 表达式的结果为 `null`、`undefined` 或 `false` 时,这些值不会被渲染到页面中。 ## 属性绑定 在 JSX 中,标签属性既可以使用字符串字面量,也可以通过 `{}` 绑定 JavaScript 表达式。 例如: ```jsx function App() { const ikunImgUrl = "https://pic4.zhimg.com/v2-d3e48d94ac7a156c058bb1ed651be1a9_1440w.jpg" return <img src={ikunImgUrl} /> } ``` ## 事件绑定 在 JSX 中,可以通过**事件处理属性**响应用户的交互操作。这些属性以 `onXxx` 的形式命名,其中 `Xxx` 表示具体的事件类型。 仅需将事件处理函数与相应的事件处理属性绑定起来,即可为元素绑定对应的事件。 例如: ```jsx function App() { return <button onClick={() => alert("恭喜你点击我了!")}>点我试试?</button> } ``` > 通过前面三个小节的内容,可以发现 `{}` 在 JSX 中具有统一的作用。 > > 无论是内容、属性还是事件处理,它们都没有语法上的差异,只是 JS 表达式生效的位置不同而已。 > > 此外,在 JSX 中编写注释时,同样需要借助 `{}` 。 > > 例如: > ```jsx > function App() { > return ( > <button onClick={() => alert("恭喜你点击我了!")}> > 点我试试? > {/* 我是一个注释 */} > </button> > ) > } > ``` ## 条件渲染 在 React 中,可以通过 JavaScript 中用于条件判断的语法,来选择性地渲染 JSX。 其中较为常见的一种方式是使用三元运算符,下面将以这种方式为例。 例如: ```jsx function App() { const isLogin = true return <>{isLogin ? <span>已登录</span> : <span>未登录</span>}</> } ``` ## 列表渲染 现在你有如下数据需要渲染到页面上: ```js const todos = [ { id: 1, content: "学React", }, { id: 2, content: "学Vue", }, { id: 3, content: "学Java", }, ]; ``` 接下来你需要将其渲染成如下结构: ```html <ul> <li>学React</li> <li>学Vue</li> <li>学Java</li> </ul> ``` 对于这类列表数据,通常会通过 `map` 方法,将每一项数据映射为一个结构相同的 JSX 元素,然后再在 JSX 中渲染。 例如: ```jsx function App() { const todos = [ { id: 1, content: "学React", }, { id: 2, content: "学Vue", }, { id: 3, content: "学Java", }, ] return ( <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.content}</li> ))} </ul> ) } ``` 上述代码将列表数据**映射**为一组 JSX 元素并渲染到页面中。 在渲染列表时,React 要求为每一项 JSX 元素提供一个唯一的 `key`,可使用数据自身的 id。 # 状态 ## 何为状态 在 React 中,组件的 UI 不是通过命令式操作来更新的,而是由数据计算得到的结果。也就是说,界面长什么样,取决于数据是什么样。这种方式正是**数据驱动视图**的设计思想的体现。 而这类由 React 管理、会影响组件的渲染结果,并在变化时自动触发组件重新渲染的数据称为**状态(State)**。 ## 定义状态 在 React 中,我们使用`useState(初始值)`这个函数来在组件中定义状态。 在函数组件中,应在**组件函数的最外层作用域**中调用 `useState` 来声明状态。 状态是**组件私有**的,只存在于定义它的组件中。 > 提示: > > 在 React 中,以 `use` 开头的函数称为 Hook 函数。 > > 关于 Hook 函数的细节将在后续内容中介绍。 `useState` 返回一个长度为 2 的数组: - 第一个元素是当前的状态值 - 第二个元素是用于更新该状态的 setter 函数 在 React 中,通常使用 JavaScript 的数组解构语法来方便地获取它们。 按照约定,状态值通常命名为 `something`,其对应的更新函数命名为 `setSomething`。 例如: ```jsx import { useState } from "react" function App() { const [count, setCount] = useState(0) return <h1>{count}</h1> } ``` ## 更新状态 在 React 中,状态只允许通过调用其配套的 setter 函数来更新。 下面以一个“功德木鱼”组件为例,说明 setter 函数的两种常见使用场景。 该组件包含以下交互和展示内容: 1. 一组按钮,分别用于“功德 +1”、“功德 -1”、“功德清零” 2. 渲染当前功德的值 ### 场景一:新状态值不依赖于旧值 当新状态值与旧值无关时,可以直接向 setter 函数传入新的值,例如: ```jsx const reset = () => setCount(0) ``` ### 场景二:新状态值依赖于旧值 当新状态值需要基于旧值计算时,应向 setter 函数传入一个**更新函数**,该函数接收旧值作为参数,并返回新的状态值。这种方式也称之为函数式更新。 > 更新函数的签名为`(oldVal) => newVal`。 例如: ```jsx const increment = () => setCount((oldVal) => oldVal + 1) const decrement = () => setCount((oldVal) => oldVal - 1) ``` 最终的“功德木鱼”组件的完整代码如下: ```jsx function App() { const [count, setCount] = useState(0) const reset = () => setCount(0) const increment = () => setCount((oldVal) => oldVal + 1) const decrement = () => setCount((oldVal) => oldVal - 1) return ( <> <h1>{count}</h1> <button onClick={decrement}>功德-1</button> <button onClick={reset}>功德清零</button> <button onClick={increment}>功德+1</button> </> ) } ``` ### setter 函数的注意事项 请看下面这个组件: ```jsx function App() { const [count, setCount] = useState(0) const reset = () => { setCount(0) console.log(count) } const increment = () => { setCount((oldVal) => oldVal + 1) console.log(count) } const decrement = () => { setCount((oldVal) => oldVal - 1) console.log(count) } return ( <> <h1>{count}</h1> <button onClick={decrement}>功德-1</button> <button onClick={reset}>功德清零</button> <button onClick={increment}>功德+1</button> </> ) } ``` 上述代码中,在调用 setter 函数后紧接着输出了状态 `count` 的值。 当你亲自调试上述代码时会发现,`console.log` 打印的仍然是更新前的状态值。 这说明,在当前执行流程中,调用 setter 并不会立即改变状态值。 调用 setter 只是告知 React 需要更新状态,而不是立刻修改当前可读取到的状态值, 因此 `console.log(count)` 读取到的仍然是旧值。 基于这一结论,可以进一步回答一个常见问题:当新状态值依赖于旧值时,是否可以直接使用 `count + 1` 的写法? 例如: ```jsx const increment = () => { setCount(count + 1) } ``` 在单次调用的场景下,这样的写法是可行的。 但如果需要在同一次执行流程中多次更新状态,例如实现“功德 +3”,则会出现问题: ```jsx const addThree = () => { setCount(count + 1) setCount(count + 1) setCount(count + 1) } ``` 原因在于,每一次更新读取到的仍然是同一个旧值。 如果使用函数式更新,则不会出现这一问题: ```jsx const addThree = () => { setCount((oldVal) => oldVal + 1) setCount((oldVal) => oldVal + 1) setCount((oldVal) => oldVal + 1) } ``` 简单来说,直接使用 `count + 1` 时,每一次更新读取到的都是同一个旧值,而函数式更新会在每一次更新时重新获取最新的状态值。 > 关于状态更新的具体细节,可以参考 React 官方文档的相关说明:[Queueing a Series of State Updates – React](https://react.dev/learn/queueing-a-series-of-state-updates) ### 不可变更新原则 在 React 中,状态应被视为**只读值**。当状态的值是对象或数组等复杂类型时,**不应直接修改它们的内容,而应创建一个新的对象或数组来替换原有的状态值**,这称为“**不可变更新原则**”。 这是因为,React 在判断状态是否发生变化时,关注的是状态值本身是否发生了变化(对于对象或数组来说,即是否是同一个引用),而不会追踪对象或数组内部字段的修改。 因此,直接修改原有对象或数组,可能导致 React 无法正确识别状态已经更新。 #### 对象更新示例 【简单对象】 考虑这样一个状态: ```jsx const [me, setMe] = useState({ name: "ikun", age: 18 }) ``` 若需要更新`me`的`age`为19,此时该如何调用`setMe`? 当状态是一个普通对象时,通过创建一个新对象来完成不可变更新。 例如: ```jsx setMe({ ...me, age: 19 }) ``` 这里通过创建一个新的对象来整体替换原有状态,而不是直接修改原有的状态对象。 展开运算符用于将原有字段拷贝到新对象中,并用新的 `age` 值覆盖对应字段,从而遵循不可变更新原则。 【嵌套对象】 考虑这样的一个状态: ```jsx const [me, setMe] = useState({ name: "ikun", age: 18, school: { name: "Guizhou University", location: "Guiyang Guizhou" } }) ``` 若需要更新`school`的`name`为"GZU",此时该如何调用`setMe`? 当状态对象存在嵌套结构时,为了遵循不可变更新原则,需要**为被修改的每一层对象创建新的副本**,例如: ```jsx setMe({ ...me, school: { ...me.school, name: "GZU" } }) ``` 可以预见的是,**随着嵌套层级的增加,为遵循不可变更新原则而编写的模板代码会迅速增多**。 #### 数组更新示例 考虑这样的状态: ```jsx const [todos, setTodos] = useState([ { id: 1, content: "学React", }, { id: 2, content: "学Vue", }, { id: 3, content: "学Java", }, ]) ``` 往`todos`中新增一个元素: ```jsx setTodos([...todos,{ id: 4, content: "学CS Go", }]) ``` 修改`id`为2这个元素的内容为"学霸题": ```jsx setTodos(todos.map((todo)=>todo.id !== 2 ? todo : { ...todo, content: "学霸题", })) ``` 删除`id`为3的元素: ```jsx setTodos(todos.filter((todo)=>todo.id !== 3)) ``` 上述示例中,每一次更新都返回了一个新的数组,并且在修改元素时为被修改的项创建了新的对象,从而遵循不可变更新原则。 #### 工程化方案:使用 `immer` 可见,当状态是对象或数组等复杂类型时,为了遵循不可变更新原则,往往需要编写较多的模板代码。 为此,React 官方推荐使用第三方库 `immer`,以简化不可变更新的编写过程。 由于这并**不属于 React 的核心内容**,这里不再展开介绍,感兴趣的读者可以参考官方文档中的相关说明: - [使用 immer 更新 state 中的对象](https://zh-hans.react.dev/learn/updating-objects-in-state#write-concise-update-logic-with-immer) - [使用 immer 更新 state 中的数组](https://zh-hans.react.dev/learn/updating-arrays-in-state#write-concise-update-logic-with-immer) # 组件之间的协作 ## 为什么需要拆分组件? 当应用规模逐渐变大时,把所有 UI 和逻辑都写在一个组件中,会迅速带来可维护性和复用性的问题。React 通过“组件拆分”的方式,将界面划分为职责明确的独立单元,从而降低复杂度,并提高复用能力。 在前面的示例中,我们刻意将所有内容写在同一个组件中,以便集中关注单个组件的基本用法。 在实际开发中,我们很少从零开始实现这些组件。更多时候,我们会直接使用一些成熟的 React 组件库,例如 [Ant Design](https://ant.design/components/overview-cn),通过组合现成的组件来快速搭建页面结构。只有在组件库未提供所需组件时,才需要自行实现。 因此,在组合多个组件之前,需要先理解组件之间的数据传递与交互方式。 接下来,我们将从最基础的方式开始,理解多个组件之间是如何协同工作的。 ## 组件的层级关系 在拆分出多个组件之后,下一步需要明确的,是这些组件在结构上的组织方式。 先定义一个简单的组件: ```jsx function Header() { return <h1>我是头部</h1> } ``` 定义好的组件,可以像普通 JSX 标签一样在其他组件中使用。 例如: ```jsx function App() { return ( <> <Header /> <p>我是页面内容</p> </> ) } ``` 这里在 `App` 组件中使用了 `Header` 组件。 当一个组件在另一个组件中被使用时,外层组件称为父组件,内层组件称为子组件。父子组件并不是组件的固定身份,而只是用于描述当前的使用关系。以刚刚的例子为例,`App` 组件是 `Header` 组件的父组件。 理解组件之间的父子关系,是后续学习组件数据传递和协作的基石。 ## 使用 props 传递数据 当组件需要在不同场景下展示不同的内容时,就不能再把数据写死在组件内部。 例如,下面是一个用于展示文章信息的 `Article` 组件,它包含文章的标题和正文: ```jsx function Article() { return ( <> <h2>标题</h2> <p>正文</p> </> ) } ``` 目前,我们将标题和正文的内容直接写在了组件内部。 在实际使用中,我们希望在不同的地方使用 `Article` 组件时,它能够展示不同的标题和正文。那么,**这个组件应该如何声明它所需要的数据呢?** 在 React 中,组件会接收一个名为 `props` 的对象,用于存放外部传入的数据。这个对象会作为组件函数的第一个参数传入,组件通过读取其中的字段来渲染对应的内容。 例如: ```jsx function Article(props) { return ( <> <h2>{props.title}</h2> <p>{props.content}</p> </> ) } ``` 由于 `props` 是一个对象,实际开发中更常见的写法是使用对象解构来简化代码: ```jsx function Article({ title, content }) { return ( <> <h2>{title}</h2> <p>{content}</p> </> ) } ``` > 在工程实践中,通常会结合 TypeScript 对 props 进行类型约束,以减少传参错误。但这些偏工程化细节,不是理解 React 核心概念所必需的内容,这里不展开;感兴趣的读者可自行查阅相关文档。 那么,这些传入组件的 `props`,究竟是从哪里来的? 在 JSX 中,自定义组件也可以像 HTML 标签一样接收属性。不同的是,这些属性不会直接作用于 DOM,而是会被 React 收集起来,作为 `props` 传入组件。 ```jsx function App() { const articles = [ { id: "l-001", title: "雨夜书简:写给未寄出的那封信", content: "窗外的雨把路灯磨成一团暖黄。我把句子写得很慢,好像每个字都要先穿过胸口再落到纸上。你不必回信——我只是想让这段沉默,有一个干净的去处。", }, { id: "l-002", title: "诗意片段:冬天的河", content: "河面薄得像一层犹豫的玻璃,风一吹就起细小的纹。岸边的芦苇把影子交给水,又把声音交给夜。你站在桥上,像站在一句未完的诗里。", }, ] return ( <> {articles.map((article) => ( <Article key={article.id} title={article.title} content={article.content} /> ))} </> ) } ``` 可见,我们通过 `title`、`content` 这两个属性把数据传入 `Article`;组件内部再从 `props` 中读取它们。 ## children 有些组件只负责提供外观或布局结构,而不关心具体显示的内容。具体内容由使用者在使用组件时,通过写在组件标签中间的内容传入。 在 React 中,写在组件标签中间的内容会被收集起来,作为 `props.children` 传入组件。组件内部只需要渲染 `children`,就可以把由使用者决定的内容显示在指定的位置。 例如: ```jsx function App() { return ( <> <Card> <p>随便什么内容</p> </Card> <Card> <div>哈哈哈</div> </Card> </> ) } function Card({ children }) { return <div className="card">{children}</div> } ``` ## 单向数据流 通过前面的示例可以发现,组件之间的数据传递始终有一个明确的方向。 在 React 中,组件之间的数据流动遵循**单向数据流的原则**:父组件通过 props 将数据传递给子组件,子组件**只能读取这些数据,而不能直接修改它们**。 为什么 React 会如此强调这个原则呢? 如果子组件可以直接修改从父组件接收到的数据,那么数据变化的来源将变得**难以追踪**,组件之间的依赖关系也会变得**难以理清**。在单向数据流原则的约束下,数据的来源始终是明确的:**由哪个组件提供数据,就由哪个组件负责维护它。** 子组件仍然可以参与数据变化,但数据的修改逻辑始终由父组件负责。子组件只是在合适的时机发出“修改请求”,由父组件决定如何更新数据。这通常通过**父组件向子组件传递回调函数,在子组件中于合适的时机调用来实现**。 输入框是一个非常典型的场景: 用户在子组件中输入内容,但输入的结果往往需要由父组件统一维护和使用。 从表现上看,输入框的值会随着用户的输入实时变化,看起来像是组件内部在不断修改数据。但在 React 中,即便是这样的交互场景,数据的流动方向依然遵循单向数据流的原则。 下面通过一个简单的示例来说明这一点: ```jsx function MyTextInput({ value, onChange }) { return ( <> <input type="text" value={value} onChange={(e) => onChange(e.target.value)} /> </> ) } ``` 使用这个组件: ```jsx function App() { const [inputText, setInputText] = useState("") return ( <> <h1>{inputText !== "" ? inputText : "请输入文本:"}</h1> <MyTextInput value={inputText} onChange={(newVal) => setInputText(newVal)} /> </> ) } ``` 效果:  在这个例子中,`MyTextInput` 组件本身并不维护任何状态。 虽然输入框的值会随着用户输入而变化,但数据的修改始终发生在父组件中,`MyTextInput` 只是通过调用`onChange` 回调,将新的输入值传递给父组件,再由父组件决定如何更新状态,并将新的值重新传递下来。 因此,输入框的交互效果看起来像是“双向绑定”,但数据的实际流动方向依然是清晰且单向的。 ## Context 在前面的示例中,我们通过 props 将数据从父组件传递给子组件。这种方式在组件层级较浅时非常直观,但**当组件嵌套层级逐渐变深时,逐层传递 props 会变得冗长且难以维护**。 假设应用中需要在多个组件中使用同一份主题配置(例如深色或浅色模式),而真正使用该配置的组件位于较深的组件层级中。 例如: ```jsx function App() { return <Layout theme="dark" /> } function Layout({ theme }) { return <Page theme={theme} /> } function Page({ theme }) { return <Button theme={theme} /> } function Button({ theme }) { return <button>当前主题: {theme}</button> } ``` 在这个例子中,`Layout` 和 `Page` 本身并不关心 `theme`, 却不得不接收并继续向下传递它。 为了解决组件层级加深时逐层传递 props 的问题,React 提供了 **Context 机制**,用于在组件树中跨层级地共享数据。 Context 的使用包含以下三个步骤: 1. 创建 Context,定义需要共享的数据 2. 提供 Context,确定数据在组件树中的作用范围 3. 使用 Context,读取当前作用域内的数据 在 React 中,通过 `createContext(默认值)`函数创建一个 Context 对象。 例如: ```jsx const ThemeContext = createContext("") ``` 创建好 Context 之后,可以通过 `Context.Provider` 组件的 `value` 属性向包裹的组件树提供数据。 其使用方式如下: ```jsx function App() { return ( <ThemeContext.Provider value="dark"> <Layout /> </ThemeContext.Provider> ) } ``` 在这个例子中,`ThemeContext.Provider` 将 `"dark"` 这个值提供给其包裹的组件树。 这里的 `theme` 不再通过 props 传递,因此 `Layout` 组件也不再需要接收这个属性,后续将给出完整的代码。 在组件中,可以通过 `useContext` 读取 Context 中提供的数据。 `useContext` 的使用方式如下: ```js const value = useContext(Context对象) ``` 例如,在 `Button` 组件中,应这样消费 `ThemeContext` 中提供的数据: ```jsx function Button() { const theme = useContext(ThemeContext) return <button>当前主题: {theme}</button> } ``` 这部分示例的完整代码为: ```jsx const ThemeContext = createContext("") function App() { return ( <ThemeContext.Provider value="dark"> <Layout /> </ThemeContext.Provider> ) } function Layout() { return <Page /> } function Page() { return <Button /> } function Button() { const theme = useContext(ThemeContext) return <button>当前主题: {theme}</button> } ``` 至此,我们已学完了 React 中的 Context 机制。 Context 并不是所有场景下都必须使用的工具,但它是 React 生态中非常基础的一部分。后续在使用各种基于 React 构建的工具库时,你很可能会频繁遇到 Context 的身影。 理解 Context 的目的,并不是记住它的具体用法,而是在看到相关代码或工具时,能够意识到:这里使用的是 React 的 Context 机制,从而自然地理解和使用它。 # 副作用 ## 何为副作用 在实际开发中,组件不仅负责渲染界面,它们还可能需要进行一些操作,如请求数据、操作 DOM、注册事件或与浏览器提供的 API 交互。 这些操作并不会直接影响组件根据当前的 `props` 和 `state` 计算出的渲染结果,但它们的执行会对组件作用域之外的环境产生影响。在 React 中,这类操作被统一称为 **副作用(Effect)**。 ## 为何需要 useEffect 在理解了什么是副作用之后,接下来需要回到 React 组件的渲染过程本身。 当 React 渲染或重新渲染组件时,会 **从上到下执行组件函数**,并根据当前的 `props` 和 `state` 计算出界面结构。React 可能会多次执行组件函数来重新计算组件的 UI,但并非每次计算都会导致真实的页面更新,页面最终只会反映其中一次计算的结果。 因此,在组件函数的渲染过程中,**不应该执行副作用**,因为这些副作用逻辑可能在多次渲染过程中被重复执行。 这也正是 **React 要求不能在组件渲染过程内执行副作用** 的原因。 然而,在实际开发中,**组件不可避免地需要执行副作用逻辑**,例如请求数据。为了避免副作用在渲染过程中执行,React 提供了 `useEffect` Hook,使得**副作用能够在组件渲染完成后执行**。 ## 初识 useEffect `useEffect` 是 React 提供的一个 Hook,用于**注册组件中需要在渲染完成之后执行的副作用逻辑**。 它使得组件函数只负责计算 UI,而副作用的执行则由 React 在渲染完成之后处理。 `useEffect` 接收两个参数: - `setup` 函数:定义副作用的具体逻辑。 - 依赖项数组(`dependencies`):可选,用来控制副作用何时重新执行。 `useEffect` 在组件渲染完成后执行 `setup` 函数,且可以返回一个 **清理函数(cleanup)**,该函数会在组件卸载或副作用重新执行之前被调用,用来清理之前的副作用,后续会具体介绍二者的实际执行顺序。 **依赖项数组**是一个由状态组成的数组。React 会比较数组中每个依赖项的前后值变化,决定副作用是否需要重新执行。 ## 副作用重新执行的时机 `useEffect` 的副作用并非每次组件渲染后都执行,而是根据传递的`dependencies` 数组的内容来决定何时重新执行。 ① 无 `dependencies` 时 如果没有传递 `dependencies`,`useEffect` 会在 **每次组件渲染后** 执行副作用逻辑。 例如: ```jsx function App() { const [count, setCount] = useState(0) useEffect(() => { console.log("副作用执行了") }) return ( <> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}>+1</button> <button onClick={() => setCount(count - 1)}>-1</button> </> ) } ``` 在这个例子中,每次 `count` 发生变化,`App` 组件就会重新渲染,副作用逻辑也会被触发。 ② `dependencies`为空数组时 如果 `dependencies` 是一个空数组 (`[]`),副作用仅在 **组件首次渲染后** 执行一次,之后的重新渲染将不会再执行。 例如: ```jsx function App() { const [show, setShow] = useState(true) return ( <> <button onClick={() => setShow((s) => !s)}> {show ? "隐藏 Component" : "显示 Component"} </button> <hr /> {show ? <Component /> : "Component 已卸载"} </> ) } function Component() { const [count, setCount] = useState(0) useEffect(() => { console.log("副作用执行了") }, []) return ( <> <h3>{count}</h3> <button onClick={() => setCount(count + 1)}>+1</button> <button onClick={() => setCount(count - 1)}>-1</button> </> ) } ``` 在这个例子中,副作用逻辑只会在 `Component` 组件 **首次渲染后** 执行一次。即使 `count` 状态发生变化导致`Component` 重新渲染,副作用也不会重新执行;副作用只会在 `Component` 组件被移除后再重新渲染时再次执行。 ③ `dependencies`中有具体依赖项时 当 `dependencies` 包含一个或多个具体的依赖项时,副作用会在**依赖项变化时**重新执行。 例如: ```jsx function App() { const [count, setCount] = useState(0) const [text, setText] = useState("") useEffect(() => { console.log("副作用执行了") }, [count]) return ( <> <h3>{count}</h3> <button onClick={() => setCount((c) => c + 1)}>+1</button> <button onClick={() => setCount((c) => c - 1)}>-1</button> <hr /> <label> text: <input value={text} onChange={(e) => { setText(e.target.value) }} /> </label> <p>当前 text:{text || "(空)"}</p> </> ) } ``` 在这个例子中,副作用只会在 `count` 状态变化时重新执行。即使 `text` 状态发生变化导致 `App`组件重新渲染,副作用也不会执行,因为 此时的`useEffect` 仅依赖于 `count`。 ## setup/cleanup 的执行顺序 在 `useEffect` 中,副作用逻辑由 **`setup`** 函数和 **`cleanup`** 函数组成。React 会在合适的时机自动调用这两个函数,确保副作用正确地清理和重新执行。 `setup` 和 `cleanup`的执行顺序如下: 1. 组件首次渲染后 React 会执行 `setup` 函数,初始化副作用。 2. 依赖项变化触发组件重新渲染时 - 在组件重新渲染之前,React 会先执行 `cleanup` 函数,清理上一次的副作用。此时`cleanup`中访问到的状态是旧值。 - 在组件重新渲染完成后,React 会重新执行 `setup` 函数,此时 `setup` 中访问到的状态是新值。 3. 组件即将卸载时 在组件卸载前,React 会最后一次执行 `cleanup` 函数,以彻底清理副作用。 例如: ```jsx function App() { const [showChild, setShowChild] = useState(true) return ( <> <button onClick={() => setShowChild((v) => !v)}> {showChild ? "卸载 Child" : "挂载 Child"} </button> <hr /> {showChild ? <Child /> : <div>Child 已卸载</div>} </> ) } function Child() { const [count, setCount] = useState(0) useEffect(() => { console.log(`[Child setup] count=${count}`) return () => { console.log(`[Child cleanup] count=${count}`) } }, [count]) return ( <> <h3>{count}</h3> <button onClick={() => setCount((c) => c + 1)}>+1</button> <button onClick={() => setCount((c) => c - 1)}>-1</button> </> ) } ``` 在这个例子中,你可以点击按钮切换 `showChild`,来观察 `setup` 和 `cleanup` 的执行顺序。每次 `count` 变化时,`Child` 组件会重新渲染,并执行相应的副作用逻辑。 ## 写到最后 有的教程可能会将上述内容描述成所谓的“生命周期”,或者将 Vue 中的生命周期概念类比到 React 中。 然而,在现代 React(尤其是函数式组件)中,**生命周期** 这一概念已经不再适用,最新的 React 文档中甚至不再提及“生命周期”这个词。 我想提醒你的是,副作用的所有相关逻辑,**不需要再依赖生命周期的概念**,而是通过 `useEffect` 来处理。`useEffect` 作为现代函数式组件中的核心 Hook,负责灵活地管理副作用。 # 自定义 Hook **自定义 Hook** 是 React 中的一种机制,它允许你将组件中**可复用的逻辑**提取到一个函数中,并将其封装,这样多个组件可以共享这段逻辑,而不需要重复代码。而这些函数被称为 自定义 Hook 函数。 React 提供了内置的 Hook(例如 `useState`、`useEffect`),自定义 Hook 可以利用这些内置的 Hook 来实现自己的功能逻辑。 自定义 Hook 必须遵循与 React 内置 Hook 相同的规范: 1. 命名规则:Hook 函数的名称必须以 `use` 开头(例如:`useState`)。 2. 调用规则:Hook 函数只能在组件的顶层作用域内调用。 为了说明如何将逻辑提取到自定义 Hook,我们先在 `App` 组件中实现一个简单的功能:追踪鼠标在屏幕上的 x 和 y 坐标。 ```jsx function App() { const [position, setPosition] = useState({ x: 0, y: 0 }) useEffect(() => { // 订阅鼠标移动事件,实时更新当前鼠标坐标 const handleMouseMove = (event) => { setPosition({ x: event.clientX, y: event.clientY }) }; // 组件挂载时绑定监听,组件卸载时移除监听,避免内存泄漏 window.addEventListener("mousemove", handleMouseMove, { passive: true }) return () => { window.removeEventListener("mousemove", handleMouseMove) }; }, []); return ( <div> x: {position.x}, y: {position.y} </div> ); } ``` 上面实现的鼠标追踪功能是一个常见的需求,假设以后我们在其他地方也需要用到这段逻辑,重复代码会增加维护成本,而且会导致逻辑分散。为了避免这种问题,我们可以将这段逻辑提取到一个自定义 Hook 中,这样以后需要使用的时候,只需简单调用该 Hook 就能复用这份逻辑。 现在,我们将追踪鼠标坐标的逻辑封装到一个自定义 Hook 中。这个 Hook 只需要返回 `x` 和 `y` 的坐标值,其它逻辑都封装在 Hook 内部,例如: ```jsx function useMousePosition() { const [position, setPosition] = useState({ x: 0, y: 0 }) useEffect(() => { const handleMouseMove = (event) => { setPosition({ x: event.clientX, y: event.clientY }) } window.addEventListener("mousemove", handleMouseMove, { passive: true }) return () => { window.removeEventListener("mousemove", handleMouseMove) }; }, []) return { x: position.x, y: position.y } } ``` 一旦我们将逻辑封装成 `useMousePosition`,在 `App` 组件中使用这个 Hook 就非常简单了: ```jsx function App() { // 调用自定义 Hook 获取鼠标坐标 const { x, y } = useMousePosition(); return ( <div> x: {x}, y: {y} </div> ); } ``` 这样,`App` 组件变得更加简洁,鼠标位置的追踪逻辑也得到了复用。如果以后需要在其他地方使用这个功能,只需要调用 `useMousePosition` 即可。 自定义 Hook 的好处在于,它能够**提高代码的复用性**,通过将公共逻辑提取到 Hook 中,我们可以在多个组件之间共享这段逻辑,避免重复编写。它还可以**简化组件的代码**,将副作用、状态管理等逻辑封装在自定义 Hook 中,使组件只专注于渲染和 UI,从而提高了代码的清晰度。此外,多个组件共享同一份逻辑,更新时只需修改自定义 Hook,避免了逻辑分散带来的维护难度,**提高了代码的可维护性**。 在实际开发过程中,我们可以使用一些成熟的 Hooks 工具库来帮助我们快速实现常见功能。例如,**Ahooks** 是一个非常优秀的 React Hooks 工具库,提供了超过 40 个常用的 Hook,包括数据请求、表单处理、缓存管理等,能够显著提高开发效率。通过学习并使用第三方库,我们不仅可以加速开发进程,还能逐步学习如何编写更加优雅、符合规范的自定义 Hook。
带你30分钟弄明白useContext的原理,教不会你随便喷!
## 前言 在React的函数组件中,`useContext`是一个非常重要的Hook,它让我们能够轻松地在组件树中共享状态。但是,你真的理解它的工作原理吗? ## 开宗明义:useContext的重新渲染原理 **useContext能重新渲染的原理,其实就是订阅发布者模式!** - **订阅阶段**:当使用`useContext`时,其实就是在订阅。订阅做的事情其实就是把这个fiber添加一些特殊的标记(dependencies)。 - **发布阶段**:等到发布者(Provider)的value发生改变时,就会执行发布这个行为。发布的行为就是给之前的特殊标记的fiber节点标记上更新。 - **渲染阶段**:其他事情交给React的渲染机制去做了。 这就是useContext重新渲染的完整流程! ## 目录 1. [订阅与发布机制](#订阅与发布机制) 2. [渲染周期详解](#渲染周期详解) 3. [Fiber树结构与依赖关系](#fiber树结构与依赖关系) 4. [基本概念澄清与常见误区](#基本概念澄清与常见误区) ## 订阅与发布机制 ### 订阅机制 当函数组件调用`useContext`时,React会在**订阅者组件**的fiber上记录依赖关系: ```javascript function useContext(Context) { const fiber = getCurrentFiber(); if (fiber) { // 在订阅者组件的 fiber 上记录依赖关系 if (!fiber.dependencies) { fiber.dependencies = []; } // 只需要注意这里,这里就添加了特殊标记 fiber.dependencies.push({ context: Context, fiber: fiber }); } return Context._currentValue; } ``` **关键点**:订阅关系记录在**订阅者组件**的fiber上,而不是Provider的fiber上。 ### 发布机制 当Provider的值发生变化时,React会遍历整个fiber树来找到所有订阅者,并且确保找到的订阅者订阅的是这个特定的发布者: ```javascript function propagateContextChange(context, fiber) { let current = fiber.child; while (current !== null) { // 检查当前 fiber 是否订阅了目标 Context if (current.dependencies) { for (let i = 0; i < current.dependencies.length; i++) { const dependency = current.dependencies[i]; if (dependency.context === context) { // 找到订阅者,标记需要更新 scheduleUpdateOnFiber(current); break; } } } // 继续遍历子节点 if (current.child !== null) { current = current.child; continue; } // 遍历兄弟节点 while (current.sibling === null) { if (current.return === null || current.return === fiber) { return; } current = current.return; } current = current.sibling; } } ``` **重要理解**: - 遍历整个fiber树是为了找到**全部**的订阅者 - 一个订阅者组件可以订阅多个发布者,比如: ```javascript const Child = () => { const user = useContext(UserContext); // 订阅 UserContext const theme = useContext(ThemeContext); // 订阅 ThemeContext return <div>{user.name}</div>; }; ``` ## 渲染周期详解 ### 完整的渲染流程 让我们通过一个具体例子来理解整个渲染周期: ```javascript const App = () => { const [count, setCount] = useState(0); return ( <CountContext.Provider value={count}> <Child /> </CountContext.Provider> ); }; const Child = () => { const count = useContext(CountContext); return <div>{count}</div>; }; ``` 当`setCount(1)`被调用时,完整的渲染流程如下: #### 1. 状态更新 ```javascript setCount(1); // 触发状态更新 ``` #### 2. React调度更新 ```javascript scheduleUpdateOnFiber(rootFiber); // 调度根fiber的更新 ``` #### 3. 开始渲染流程 ```javascript renderRootSync(root); // 开始同步渲染流程 ``` #### 4. 处理App组件 ```javascript updateFunctionComponent(appFiber); // App 重新执行,Provider 接收到新的 value ``` #### 5. 处理Provider ```javascript updateContextProvider(providerFiber); // 比较新旧值,发现不同,触发发布 ``` #### 6. 通知订阅者 ```javascript propagateContextChange(context, providerFiber); // 找到 Child 组件,标记需要更新 ``` #### 7. 处理Child组件 ```javascript updateFunctionComponent(childFiber); // Child 重新执行,useContext 返回新值 ``` #### 8. 渲染到DOM ```javascript commitRoot(root); // 将更新提交到DOM ``` ### 关键函数实现 #### updateContextProvider ```javascript function updateContextProvider(current, workInProgress, Component, nextProps) { const context = workInProgress.type._context; const newValue = nextProps.value; const oldValue = current.memoizedState; // 比较新旧值 if (is(oldValue, newValue)) { // 值相同,不更新 return; } // 值不同,更新 Context 的值 context._currentValue = newValue; // 标记需要更新 workInProgress.flags |= Update; // 通知所有订阅者 propagateContextChange(context, workInProgress); } ``` ## Fiber树结构与依赖关系 ### 为什么需要遍历整个fiber树 让我们通过一个具体的例子来说明: ```javascript const App = () => { const [count, setCount] = useState(0); return ( <CountContext.Provider value={count}> <div> <Header /> <Main> <C1 /> {/* 订阅者 1 */} </Main> <Footer> <C2 /> {/* 订阅者 2 */} </Footer> </div> </CountContext.Provider> ); }; const C1 = () => { const count = useContext(CountContext); return <div>{count}</div>; }; const C2 = () => { const count = useContext(CountContext); return <div>{count}</div>; }; ``` ### Fiber树结构 ``` // fiber 树结构: // App // └── div // ├── Header // ├── Main // │ └── C1 (订阅者) // └── Footer // └── C2 (订阅者) // 需要遍历整个树才能找到 C1 和 C2 ``` ### 依赖关系分布 ```javascript // 每个组件的 dependencies 分布: // App 组件(Provider) appFiber.dependencies = [] // 说明:App 没有使用 useContext,只是 Provider // Header 组件 headerFiber.dependencies = [] // 说明:Header 没有使用 useContext // Main 组件 mainFiber.dependencies = [] // 说明:Main 没有使用 useContext // C1 组件(订阅者) c1Fiber.dependencies = [ { context: CountContext, fiber: c1Fiber } ] // 说明:C1 订阅了 CountContext // Footer 组件 footerFiber.dependencies = [] // 说明:Footer 没有使用 useContext // C2 组件(订阅者) c2Fiber.dependencies = [ { context: CountContext, fiber: c2Fiber } ] // 说明:C2 订阅了 CountContext ``` ### 遍历查找过程 ```javascript // 当 CountContext 值变化时,遍历过程: function propagateContextChange(context, fiber) { let current = fiber.child; // 从 div 开始 while (current !== null) { // 检查当前 fiber 的 dependencies if (current.dependencies) { for (let i = 0; i < current.dependencies.length; i++) { const dependency = current.dependencies[i]; if (dependency.context === context) { // 找到订阅者! scheduleUpdateOnFiber(current); break; } } } // 继续遍历... if (current.child !== null) { current = current.child; continue; } // 遍历兄弟节点 while (current.sibling === null) { if (current.return === null || current.return === fiber) { return; } current = current.return; } current = current.sibling; } } ``` ## 基本概念澄清与常见误区 ### 重要纠正:函数组件的useContext **前提说明**:一些文章里说到通过`fiber.tag === ContextConsumer`判断是否是订阅者组件时,其实是在类组件里,在函数组件中不是这样。 在函数组件中: ```javascript // 当组件使用 useContext 时 const Child = () => { const count = useContext(CountContext); // 这里只是获取值 return <div>{count}</div>; }; // 这个组件仍然是 FunctionComponent,不是 ContextConsumer // fiber.tag === FunctionComponent ``` **关键点**:`useContext`本身**不会**创建`ContextConsumer` fiber。`ContextConsumer`实际上对应的是类组件时代的`Context.Consumer`。 ### dependencies的真正含义 每个组件的`dependencies`只记录**自己**订阅了哪些Context: ```javascript // 当组件使用 useContext 时 const C1 = () => { const count = useContext(CountContext); // C1 订阅了 CountContext return <div>{count}</div>; }; const C2 = () => { const count = useContext(CountContext); // C2 也订阅了 CountContext return <div>{count}</div>; }; // C1 的 fiber.dependencies = [ // { context: CountContext, fiber: c1Fiber } // ] // C2 的 fiber.dependencies = [ // { context: CountContext, fiber: c2Fiber } // ] // 注意:A 的 dependencies 是空的,因为 A 没有使用 useContext // A 只是 Provider,不是消费者 ``` #### 认为有专门的订阅者数组 ```javascript // ❌ 错误:认为 React 维护了订阅者数组 // CountContext.subscribers = [C1, C2] // ✅ 正确:React 通过遍历 fiber 树来找到订阅者 // 每个组件的 dependencies 只记录自己订阅了哪些 Context ``` ## 总结 通过本文的深入解析,我们了解了`useContext`的核心原理: 1. **订阅机制**:组件调用`useContext`时,在**订阅者组件**的fiber上记录依赖关系 2. **发布机制**:Provider值变化时,遍历fiber树找到所有订阅者,确保找到的订阅者订阅的是这个特定的发布者 3. **渲染流程**:从状态更新到DOM渲染的完整8步流程 4. **依赖关系**:每个组件的`dependencies`只记录自己订阅的Context 5. **常见误区**:澄清了dependencies的真正含义和遍历的必要性 `useContext`的设计体现了React的响应式理念,通过fiber树的天然结构实现了高效的跨组件状态共享。理解这些原理,不仅有助于我们更好地使用Context,也能在面试中展现出对React内部机制的深入理解。 --- **记住**:在函数组件中,`useContext`不会创建`ContextConsumer` fiber,而是通过`dependencies`记录订阅关系,通过遍历fiber树来找到所有订阅者。这是React Context机制的核心所在。
千万级科技公司高薪招聘
00后科技公司高薪招聘! 准备启动一个新的AI应用的开发项目,目前缺少前端开发工程师,欢迎来投简历,有梦想你就来,只看实力不看学历或者工作经验! 公司在合肥,来了之后包住宿,公司核心优势简单说一下,不是初创公司,已经发展两年了,现在是千万级别,还在稳定上升中 老板和公司人员基本都是00后,最大的不超过95,所以交流氛围很轻松,工作强度不高,没有条条框框的约束,只需要按照公司的开发进程做就行了 因为原先技术大多外部合作,最近几个月开始组建公司自营技术团队,求贤若渴,所以涨薪比较快,发展前景高 ps:AI应用开发的全栈工程师也可以投简历,待遇比前端开发工程师会高 
【跟着博主做】仿DeepSeek官网AI聊天网站React(Next.js)项目实战-登陆页面
个人前端小白,在跟着B站一位up @爱吃豌豆尖的Ziho做一个React(Next.js)的网站 做的时候遇到了大量的问题(有不少愚蠢问题Os...),有不少问题是在豆包的帮助下解决的,如果同样的是小白的话,可能也会遇到类似的问题,在这里小做记录,节省诸位时间。 ~~在这里发跟其他博主学习的笔记不会被鱼皮打吧˜~~ ## 登录篇 ### ziho创建完项目之后运行了npm run dev,这个命令是干嘛的? > Q:这个作者在初始化完项目之后,先运行了一个 npm run dev ,然后就把项目给起来了。但这个命令只是单单的用来起项目吗?他是有什么背后的深意吗?或者说一般开发前端项目上来都要先运行一个这个吗? A: `npm run dev`是一个常用的命令,通常用于启动开发服务器来运行前端项目。 - **启动开发服务器**:`npm run dev`的主要作用是启动一个开发服务器,它会监听项目文件的变化,并在浏览器中实时更新页面。这样,开发人员在编写代码时,只要保存文件,浏览器就会自动刷新,显示最新的页面效果,大大提高了开发效率。以 React 项目为例,当执行`npm run dev`后,开发服务器会将项目的代码进行打包和编译,然后在本地的一个指定端口(如 3000 端口)启动一个服务器,开发人员可以通过浏览器访问该端口来查看项目的运行效果。【改了立马就能看见,真好啊。。。】 - **背后的深意**:它不仅仅是简单地启动项目,还涉及到一系列的构建和编译过程。在开发过程中,前端项目通常会使用各种现代的开发工具和技术,如模块化、代码转换(如将 ES6 + 代码转换为浏览器兼容的 ES5 代码)、样式预处理等。`npm run dev`命令会触发这些工具和技术的执行,将项目的源代码转换为浏览器能够理解和运行的形式。同时,它还会提供一些开发时的辅助功能,如热模块替换(HMR),可以在不刷新整个页面的情况下更新部分代码,保持应用的状态,让开发体验更加流畅。 - **使用场景**:在前端开发中,尤其是使用像 Next.js、Create React App 等脚手架工具创建的项目,`npm run dev`是非常常见的启动开发环境的方式。不过,不同的项目可能会根据自身的需求和配置有所不同。例如,一些 Vue.js 项目可能会使用`npm run serve`来启动开发服务器,其功能与`npm run dev`类似,但具体的配置和执行的任务可能会有差异。此外,对于一些简单的静态网页项目,可能不需要这样的开发服务器,直接在浏览器中打开 HTML 文件即可查看效果,但对于使用了框架和构建工具的现代前端项目,`npm run dev`是启动项目开发环境的重要步骤。 --- ### 开发登陆功能的时候使用Clerk,这是啥?为啥要用它? >Q: 接下来作者想要实现一个登录功能,他并没有采用原本的这个,像一般人,在我看来一般人不是应该先从登录页面写起吗?但是他没有,他直接用了一个 Clerk。这是一个登录插件吗?就是说这个插件只负责登录功能,然后你就可以引入它,帮助你快速开发代码。现在都这么玩吗? A: Clerk是一个托管身份验证提供程序,它可以帮助开发者在应用程序中快速实现身份验证和用户管理功能,而不仅仅是一个简单的登录插件。使用Clerk这样的工具来实现登录功能有以下优点: - **降低开发难度**:从零开始构建安全的身份验证系统是一项复杂的任务,需要处理密码哈希和加盐、会话管理、防止恶意攻击等多个方面。而Clerk封装了这些复杂的逻辑,开发者无需自己处理底层的安全机制,大大降低了开发门槛和工作量。 - **提供多种功能**:Clerk支持多种登录方式,如电子邮件验证、无密码登录、双因素身份验证以及社交身份验证(如Google、GitHub登录)等。同时还提供了用户资料管理、密码重置等功能,能满足不同应用场景下的需求。 - **良好的开发者体验**:Clerk的API设计简洁直观,易于集成到各种框架和项目中。在Next.js项目中,只需进行一些简单的配置和安装依赖,就可以快速使用其提供的组件和功能来实现登录和用户管理界面。 - **保证安全性**:Clerk作为专业的身份验证服务提供商,会采用一系列安全措施来保护用户数据,如密码加密、认证令牌管理等,确保应用程序的安全性。 在现代前端开发中,使用类似Clerk这样的第三方身份验证服务是很常见的做法。这样可以让开发者专注于应用程序的核心功能开发,提高开发效率,同时也能保证身份验证系统的安全性和可靠性。除了Clerk,还有一些其他的身份验证服务和库,如NextAuth.js、Firebase Authentication等,也被广泛应用于不同的项目中。 --- ### 作者在引入Clerk的过程中,创建了文件".env",为啥要有这一步? >Q: 我看到引入 clerk 的一个步骤是要在我们的项目里创建了一个.env 文件,说实话,这个文件一般放一些环境变量一类的东西吗?他为什么以点开头? A: `.env`文件通常用于存放环境变量。以下是关于它以点开头及相关原因的介绍: - **以点开头的原因**:以点开头是一种常见的命名约定,用于表示该文件是一个隐藏文件。这样做的目的是为了避免在项目目录中被意外显示和误操作,同时也遵循了Unix和类Unix系统中隐藏配置文件的传统。在大多数开发环境和终端中,隐藏文件不会默认显示,只有通过特定的命令或设置才能查看,这有助于保持项目目录的整洁和有序,防止开发者不小心修改或删除重要的配置文件。 - **存放环境变量的作用**: - **区分不同环境**:在开发过程中,通常会有开发环境、测试环境和生产环境等。不同环境可能需要连接不同的数据库、使用不同的API密钥或有不同的配置参数。通过`.env`文件,可以方便地根据不同的环境设置相应的变量值,而无需在代码中硬编码这些信息。例如,在开发环境中可以使用本地的数据库连接字符串,而在生产环境中则使用正式的数据库连接字符串。 - **安全性**:对于一些敏感信息,如API密钥、数据库密码等,将它们放在`.env`文件中可以避免将这些信息直接暴露在代码仓库中。这样,即使代码仓库被公开,敏感信息也不会被轻易获取。同时,可以在`.gitignore`文件中添加`.env`,防止将环境变量文件提交到版本控制系统中,进一步提高安全性。 - **灵活性**:当需要修改某些配置参数时,只需要在`.env`文件中进行修改,而无需在代码中查找和修改多个地方。这使得项目的配置更加灵活和易于维护。 在使用`.env`文件时,需要注意确保项目的构建和运行脚本能够正确读取其中的环境变量,并在代码中通过相应的方式来访问这些变量,以实现根据不同环境进行灵活配置的目的。 > Q: 这么看来,它的作用类似于java后端开发的yml文件 A: `.env`文件在前端开发中与Java后端开发中的`.yml`(通常是指Spring Boot项目中的`application.yml`等配置文件)有一些相似之处,但也存在一些区别,具体如下: #### 相似之处 - **配置信息管理**:两者都用于存储项目的配置信息,将配置与代码分离,使项目在不同环境下的配置管理更加方便。例如,都可以在这些文件中配置数据库连接信息、服务器端口号、日志级别等。 - **环境区分**:都可以根据不同的环境(开发、测试、生产等)设置不同的配置值。以Java后端为例,通过配置不同的`application-{env}.yml`文件,在启动项目时指定不同的环境变量来加载相应的配置。前端的`.env`文件也类似,通过设置不同的`.env.development`、`.env.production`等文件,在构建或运行项目时根据环境加载对应的配置。 - **提高可维护性**:当项目的配置信息发生变化时,只需要在相应的配置文件中进行修改,而无需在大量的代码中查找和修改相关配置,提高了项目的可维护性和可扩展性。 #### 不同之处 - **语法格式**:`.env`文件的格式较为简单,通常是每行一个键值对,以`KEY=VALUE`的形式存储配置信息。而`.yml`文件采用YAML语法,具有更严格的缩进格式和层级结构,适合存储复杂的配置信息,能够以更清晰的方式表示对象、数组等数据结构。 - **功能定位**:`.env`文件主要用于存储环境相关的变量,侧重于为前端项目提供运行时的环境配置。而Java后端的`.yml`文件功能更为强大,除了配置环境变量外,还可以用于配置Spring Bean的属性、AOP切面、数据库连接池、消息队列等各种框架和中间件的复杂配置,对整个后端应用的运行和功能起着关键的作用。 - **加载方式**:前端项目中,`.env`文件的变量通常在项目构建或运行时由相关的工具(如Webpack、Vite等)进行加载和处理,并将其注入到前端代码中供使用。在Java后端中,`application.yml`等配置文件是由Spring框架在启动过程中进行加载和解析,通过配置类或注解等方式将配置信息注入到相应的Bean中。 --- ### ziho在引入Clerk的过程中创建了一个`middleware`文件,官网的解释也看不懂,这玩意有啥用? >Q: 我看到他让我们创建了一个 middleware 的文件。然后官网给出了一个的解释。The clerkMiddleware helper enables authentication and is where you'll configure your protected routes.。这句话是什么意思?根据英语来看,就是说引入的这段代码能够,帮助启用校验。这也是一个你能够保护你的routes的一个地方。这对一个新手小白来说,缺乏背景知识,比较难以理解。 A: 这句话准确的意思是:“Clerk的中间件助手能够实现身份验证功能,并且你可以在这个中间件里配置你的受保护路由。” 下面为你更详细地解释一下,帮助你理解: - **中间件(middleware)**:在Web开发中,中间件是介于请求和响应处理之间的代码层。它可以对进入应用的请求进行预处理,也可以对应用发出的响应进行后处理。就好比一个关卡,所有经过的请求和响应都要先经过它的检查和处理。 - **身份验证(authentication)**:简单来说就是确认用户的身份,判断用户是不是他所声称的那个人。比如用户登录时输入用户名和密码,系统验证用户名和密码是否正确,这就是身份验证的过程。在这个场景中,Clerk的中间件助手就负责处理这些验证逻辑,确保只有通过身份验证的用户才能访问特定的资源。 - **受保护路由(protected routes)**:路由可以理解为Web应用中不同页面或功能的地址路径。受保护路由就是指那些只有特定权限(比如已登录的用户)的用户才能访问的路由。通过配置中间件,你可以指定哪些路由是受保护的。当用户尝试访问这些受保护路由时,中间件会先检查用户是否已经通过身份验证,如果没有,就会阻止用户访问,并可能引导用户去登录页面进行登录。 举个例子,假如你开发了一个博客应用,只有注册并登录的用户才能发布新博客文章。那么“发布新博客文章”对应的页面路径就是一个受保护路由,你可以在Clerk的中间件里配置这个路由为受保护的,这样当未登录的用户尝试访问这个页面时,就会被拦截并要求先登录。 --- ### 引入过程中又改了一个layout.tsx,这个layout是干什么的? A: 在前端项目(尤其是使用 React 相关框架如 Next.js 时)中,`layout.tsx` 文件有着重要的作用,以下为你详细介绍: 在 Next.js 里,`layout.tsx` 是布局文件,它可以定义页面的整体结构和公共部分。比如网站的头部导航栏、底部的页脚、侧边栏等,这些公共的 UI 组件都可以放在 `layout.tsx` 中,这样不同的页面就可以复用这些公共部分,避免重复编写代码。 当你需要改动 `layout.tsx` 文件时,一般可能是以下几种情况: 1. **添加或修改公共组件**:比如要在网站的每个页面都添加一个新的功能按钮或者修改导航栏的样式,就需要在 `layout.tsx` 中进行操作。在你的项目中,可能是为了集成 Clerk 相关的功能,比如在布局中引入 Clerk 的提供程序(provider)组件,来确保整个应用的身份验证功能能够正常工作。例如,可能需要将 `<ClerkProvider>` 组件包裹在布局的顶层组件中,这样所有页面都能访问到 Clerk 的上下文信息,实现登录、注销等功能。 2. **处理页面级别的数据和状态**:`layout.tsx` 还可以处理一些页面级别的数据获取和状态管理。虽然它主要负责 UI 布局,但也可以在其中进行一些数据的初始化或者与后端 API 的交互,然后将数据传递给子页面组件。 3. **配置页面的元数据**:例如设置页面的标题、描述、关键字等元数据,这些信息对于搜索引擎优化(SEO)和社交媒体分享很重要。在 `layout.tsx` 中可以方便地统一配置这些元数据,确保每个页面都有合适的描述信息。 总之,`layout.tsx` 文件是定义应用整体布局和公共行为的关键部分,改动它是为了让应用的结构和功能符合项目的需求,在集成 Clerk 功能时,也是为了让身份验证相关的逻辑能够在整个应用中正确运行。如果你在改动过程中遇到具体的问题,可以随时问我。 --- ### 在引入`<SignIn />`标签的时候,这种代码结构我从来没见过,这玩意是啥呀? > Q:我对下面这段代码有点问题,我没见过这种代码形式: ``` import { SignIn } from '@clerk/nextjs' export default function Page() { return ( <div className="flex justify-center items-center h-screen w-screen"> <SignIn /> </div> ) } ``` > 前面improt我能理解,类似于python的引入是吧,后面的export,还是一个函数,函数一般来说不是处理逻辑的的吗,最后怎么返回一个`<div>`?这不是html代码么?现在框架做项目已经不用html文件来显示页面,只用其代码进行拼凑吗?,还有,className里面的这些配置都是些什么意思? A: 你观察到的是现代前端框架(如React)的典型写法,我来逐一解释: #### 1. 关于 `export default function Page()` 这是ES6模块语法,用于导出组件。在React里,页面/组件通常定义为函数,称为"函数组件"。函数返回的不是HTML,而是JSX语法(JavaScript XML)。 ```javascript // 这是一个React函数组件 export default function Page() { return <div>Hello World</div> } ``` 这种写法本质是JavaScript,JSX会在编译时被Babel转换为React.createElement()调用,最终渲染为DOM元素。 #### 2. 为什么不用HTML文件? 传统Web开发确实依赖HTML文件,但React采用"组件化"思想: - 页面由多个独立组件拼装而成 - 组件内部封装自己的结构(JSX)、样式(CSS)和逻辑(JavaScript) - 最终通过工具打包成一个或多个HTML文件 这种方式使代码更易维护和复用,例如: ```javascript // 可复用的Button组件 function Button({ text, onClick }) { return <button onClick={onClick}>{text}</button> } // 在其他组件中使用 function App() { return ( <div> <Button text="提交" onClick={() => console.log('点击')} /> </div> ) } ``` #### 3. className里的配置(如flex justify-center) 这是Tailwind CSS的类名系统。Tailwind是一个工具类优先的CSS框架,直接在HTML标签上写样式: ```html <!-- 传统CSS --> <div class="container"></div> <style> .container { display: flex; justify-content: center; align-items: center; } </style> <!-- Tailwind方式 --> <div class="flex justify-center items-center"></div> ``` 这些类名是预定义的工具类,对应特定的CSS属性,例如: - `flex` → display: flex - `justify-center` → justify-content: center - `h-screen` → height: 100vh #### 4. 实际渲染流程 虽然代码里看不到HTML文件,但最终会生成HTML。以Next.js项目为例: 1. 编写React组件(.jsx/.tsx文件) 2. 通过构建工具(如Webpack、Vite)编译 3. 生成静态HTML文件或服务器端渲染(SSR) 4. 浏览器加载HTML并执行JavaScript代码 #### 5. 推荐学习资源 - React官方文档:https://react.dev/ - Tailwind CSS文档:https://tailwindcss.com/ - Next.js教程:https://nextjs.org/learn --- > Q: 这些内容都有什么用?flex justify-center items-center h-screen w-screen A: 这些都是Tailwind CSS的工具类,用于快速设置元素的样式。我来拆解它们的作用和实际效果: ##### 1. `flex` - **作用**:设置 `display: flex` - **效果**:将元素变为弹性容器,子元素可以使用flex布局的特性 ##### 2. `justify-center` - **作用**:设置 `justify-content: center` - **效果**:在主轴(默认水平方向)上居中对齐子元素 ##### 3. `items-center` - **作用**:设置 `align-items: center` - **效果**:在交叉轴(默认垂直方向)上居中对齐子元素 ##### 4. `h-screen` - **作用**:设置 `height: 100vh` - **效果**:元素高度等于浏览器视口高度 ##### 5. `w-screen` - **作用**:设置 `width: 100vw` - **效果**:元素宽度等于浏览器视口宽度 #### 组合效果演示 当这些类一起使用时,会创建一个占满全屏且内容居中的容器: ```html <div class="flex justify-center items-center h-screen w-screen bg-gray-100"> <div class="bg-white p-8 rounded-lg shadow-lg"> <h1 class="text-2xl font-bold">居中的内容</h1> <p class="mt-2">这个元素在屏幕中水平和垂直都居中</p> </div> </div> ``` #### 常见应用场景 1. **登录/注册页面**:让表单在屏幕中央显示 2. **加载状态**:全屏加载指示器居中 3. **模态框**:确保内容在各种屏幕尺寸下都居中
AI 超级智能体笔记和作业 - AI 应用部分
# 项目代码 开源地址:https://github.com/waitlistSawana/ai-super-agent-yupi-nextjs 上线地址:https://ai-super-agent-yupi-nextjs.hsawana9.com/ 基于 Nextjs 后端实现项目课程中提到的功能,主要参考 [AI SDK](https://ai-sdk.dev/) 的实现,提供 [Shadcn Registry 服务](https://ai-super-agent-yupi-nextjs.hsawana9.com/#registry-list) 快速复制代码到本地。 欢迎 star ⭐! # 本章学习 - 提示词工程 - 生成文本 - 多轮对话 Chatbot - 拦截器(中间件) - 结构化输出 - 记忆持久化(本地文件) - 记忆持久化(数据库) ## 1. 提示词工程 多步骤提示词,xml 结构的提示词。在 Dify、阿里云百炼 迭代提示词,效果非常满意: - 角色设定 - 步骤更新 - 话题拓展 - 对话结束 - 工具调用 ```markdown <identity> <role>机器学习研究教练</role> <expertise> <area>机器学习</area> <area>数据科学</area> <area>音频处理</area> <area>计算机视觉</area> <area>自然语言处理</area> </expertise> <tone>友好、引导式、专业但平易近人</tone> </identity> <purpose> 作为您的机器学习研究伙伴,我的目标是帮助您在研究过程中取得进展。我专注于引导您完成构建基本流水线-基准模型阶段的训练前分析讨论模块,采用分步骤讨论的方式:首先认识问题,然后理解数据,接着确定研究目标,最后规划基础流水线。每个阶段我们会充分讨论并获得您的确认后,才会进入下一小节。 </purpose> <conversation_flow> <phase name="认识问题" current="true" done="false"> <focus> <point>帮助用户深入理解研究问题的本质和挑战</point> <point>分析问题的类型(分类、回归、聚类等)</point> <point>探讨问题的应用背景和实际意义</point> <point>主动提出关键问题点和可能的难点</point> </focus> <transition> <question>您对问题的理解是否清晰?我们是否可以进入下个话题,开始探索数据特征?</question> </transition> </phase> <phase name="认识数据" current="false" done="false"> <focus> <point>请求用户提供关于数据集、数据列、元数据的详细信息</point> <point>探索数据集、数据列、元数据的结构和特征,描述数据集、数据列的含义和作用</point> <point>元数据的质量、分布和潜在偏差</point> <point>元数据间的潜在关系和相互影响</point> <point>分析数据列的现实意义,预期影响以及在本课题中的意义</point> </focus> <transition> <question>您对数据的特点是否已有清晰认识?我们是否可以进入下个话题,明确研究目标?</question> </transition> </phase> <phase name="确定目标" current="false" done="false"> <focus> <point>帮助用户明确具体的研究目标</point> <point>可能的模型</point> <point>合适的评估指标</point> <point>确定表现良好和不良好的标准</point> <point>设定现实可行的阶段性目标</point> </focus> <transition> <question>您对研究目标和评估方法是否满意?我们是否可以进入下个话题,规划基础流水线?</question> </transition> </phase> <phase name="规划基准流水线" current="false" done="false"> <focus> <point>讨论最小可行的处理流程,基准模型</point> <point>建议适合的基线模型</point> <point>设计简单但完整的实验验证方案</point> </focus> <transition> <question>您对这个基础流水线的规划是否满意?是否进入下一阶段:模型训练阶段?</question> <action>结束对话,唤起结束对话工具</action> </transition> </phase> </conversation_flow> <information_verification> <action>验证用户提供的链接和资料是否可访问和充分</action> <action>检查是否能获取到研究问题和数据集的基本信息</action> <action>当信息不足时,明确向用户索要特定信息</action> <insufficient_info_response> <template> 我注意到您提供的{资源类型}似乎{问题描述}。为了更好地帮助您,我需要了解以下信息: 1. {数据集的基本结构} 2. {可用的特征、标签或元数据信息} 3. {包含数据集信息的网址链接} 4. {...具体需要的信息} 您能否提供这些信息,这样我们才能开始有效地分析研究问题? </template> </insufficient_info_response> </information_verification> <response_guidelines> <guideline>本阶段只关注最基本的流水线实现,目标是引导用户完成基准模型,不主动进行拓展和优化建议</guideline> <guideline>每次回复仅聚焦于当前阶段的内容,不要提前讨论后续小节,适当引导话题回归</guideline> <guideline>使用自然、对话式的语言,避免过于机械化的结构</guideline> <guideline>像一位经验丰富的导师一样交流,主动提出见解和分析</guideline> <guideline>在每个阶段结束时,明确询问用户是否准备进入下一阶段</guideline> <guideline>只有在用户明确确认后,才更新当前阶段并进入下一阶段</guideline> <guideline>如果用户有疑问或需要更多讨论,继续深入当前阶段的内容</guideline> <guideline>不提供任何代码实现,专注于概念和方法的讨论</guideline> <guideline>如果用户跳跃话题或提前要求代码实现,指出其在后续章节,并礼貌地引导用户回到当前小节</guideline> </response_guidelines> <phase_transition> <action>仅在用户明确确认后,将当前小节标记为完成</action> <action>将下一小节的current属性设置为"true"</action> <action>在新小节开始时,简要回顾前一小节的关键结论</action> <action>明确告知用户现在进入了新的小节</action> </phase_transition> <tools> <tool>url to markdown</tool> <tool>...orther tools</tool> </tools> <knowledge_access> <action>访问相关领域的专业知识</action> <action>提供适合用户当前阶段的信息深度</action> <action>根据研究主题提供针对性的方法和技术建议</action> </knowledge_access> ``` ## 2. 生成文本 根据aisdk文档和阿里云百炼的文档自己配置了一个 dashscope provider,用到了云百炼的 openai 兼容接口,也配置了 sealos 的接口。 主要用到了 ai 和 @ai-sdk/react 包,根据文档的示例实现的。 注意:如果想用云百炼,可以用我配置好的 ai-provider,[GIthub 代码]() 或者 用提供的 [Shadcn Registry](https://ui.shadcn.com/) 快速安装 `pnpm dlx shadcn@latest add https://ai-super-agent-yupi-nextjs.hsawana9.com/r/dashscrop-ai-provider.json` 后端采用 ai provider 和 generateText、streamText ```ts /** * generateText Route * * @author Sawana Huang * * @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text * @see https://sdk.vercel.ai/cookbook/rsc/generate-text */ import { generateText } from "ai"; import { NextResponse, type NextRequest } from "next/server"; import { sealos } from "@/lib/ai/providers/sealos"; export interface GenerateTextPostRequestBody { prompt: string; } export interface GenerateTextPostSuccessResponse { message: string; text: string; } export async function POST(request: NextRequest) { const { prompt } = (await request.json()) as GenerateTextPostRequestBody; const result = await generateText({ model: sealos("deepseek-chat-0324"), system: "You are a helpful assistant.", prompt: prompt, }); const text = result.text; return NextResponse.json( { message: "success", text: text } as GenerateTextPostSuccessResponse, { status: 200, statusText: "OK", headers: { "Content-Type": "application/json", }, }, ); } ``` 前端可以直接 fetch 后端端点 ```ts /** * generateText compoent * * @author Sawana Huang * * @see https://sdk.vercel.ai/cookbook/rsc/generate-text * * @example use it in your component * import GenerateTextDemo from "@/components/generate-text-demo"; * * <GenerateTextDemo /> */ "use client"; import { cn } from "@/lib/utils"; import { useState } from "react"; import type { GenerateTextPostSuccessResponse } from "@/app/api/v1/generate-text/route"; type GenerateTextDemoProps = { className?: string; }; export default function GenerateTextDemo({ className, ...props }: React.ComponentProps<"div"> & GenerateTextDemoProps) { const [generation, setGeneration] = useState(""); const [isLoading, setIsLoading] = useState(false); return ( <div className={cn(className)} {...props}> <div onClick={async () => { setIsLoading(true); try { const response = await fetch("/api/v1/generate-text", { method: "POST", body: JSON.stringify({ prompt: "Why is the sky blue?", }), }); const json = (await response.json()) as GenerateTextPostSuccessResponse; setGeneration(json.text); } catch (error) { console.error("Error generating text:", error); setGeneration( "Unable to generate text at the moment. Please refresh and try again.", ); } finally { setIsLoading(false); } }} > Generate </div> {isLoading ? "Loading..." : generation} </div> ); } ``` ## 3. 多轮对话 Chatbot 直接使用 ai-sdk 提供的 useChat 方法,参考 [ai-sdk Chatbot](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot)。 原理是: - 前端维护 messages,作为 body data 传送到后端,后端接收到之后传输整个 messages 给大模型。 - 后端用 generateText 或 streamText 来实现文本或者流式返回。 - 前端收集 input 组合成完整的 messages,并有完整的 hooks,如 isLoading。 - 通过 onFInish 和 onError 等调用实现更完整的业务 后端 ```ts /** * server route for comprehensive chatbot component * * @description: build server route for comprehensive chatbot component, following the document of AI SDK * * @see https://ai-sdk.dev/docs/ai-sdk-ui/chatbot * * @author Sawana Huang <hsawana9@gmail.com> * @date 2025-05-03 * */ import { dashscope } from "@/lib/ai/providers/dashscope"; import { streamText, type UIMessage } from "ai"; import { type NextRequest } from "next/server"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; export interface PostRequestBody { messages: UIMessage[]; } export interface PostSuccessResponse { message: string; } export async function POST(request: NextRequest) { const { messages, // custom fields key_use_chatbot, key_handle_submit, } = (await request.json()) as PostRequestBody & { // custom fields key_use_chatbot: string; key_handle_submit: string; }; // custom fields console.log({ key_use_chatbot, key_handle_submit }); const result = streamText({ // You can use any model you want. // see: https://ai-sdk.dev/providers/ai-sdk-providers model: dashscope("deepseek-v3"), system: "You are a helpful assistant.", messages, }); return result.toDataStreamResponse({ // Error Message: // The default error message is masked "An error occurred." // You can forward error messages or send custome messages. getErrorMessage: (error) => { if (error == null) { return "unknown error"; } if (typeof error === "string") { return error; } if (error instanceof Error) { return error.message; } return JSON.stringify(error); }, // Usage Information sendUsage: true, // Reasoning: // https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#reasoning sendReasoning: true, // Source // https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#sources // some providers response include sources // eg. Perplexity, Google generative AI sendSources: true, }); } ``` 前端: ```tsx /** * comprehensive chatbot component * * @description: comprehensive chatbot component, following the document of AI SDK * * @see https://ai-sdk.dev/docs/ai-sdk-ui/chatbot * * @author Sawana Huang <hsawana9@gmail.com> * @date 2025-05-03 * */ "use client"; import { cn } from "@/lib/utils"; import { useChat } from "@ai-sdk/react"; type ChatBotProps = { className?: string; }; export default function ChatBot({ className, ...props }: React.ComponentProps<"div"> & ChatBotProps) { const { messages, input, handleInputChange, handleSubmit, status, stop, error, reload, } = useChat({ // API endpoint Default /api/chat api: "/api/v1/chat-bot", // Throttle the messages and data updates to 50ms: experimental_throttle: 50, // Event Callbacks: // https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#event-callbacks onFinish: (message, { usage, finishReason }) => { console.log("Finished streaming message:", message); console.log("Token usage:", usage); console.log("Finish reason:", finishReason); }, onError: (error) => { console.error("An error occurred:", error); }, onResponse: (response) => { console.log("Received HTTP response from server:", response); // You can throw error here to trigger the onError callback }, // Custom headers, body, and credentials headers: { Authorization: "Bear your_token", }, body: { key_use_chatbot: "key of useChatbot", }, credentials: "same-origin", }); return ( <div className={cn("", className)} {...props}> <h2>--- Basic Messages ---</h2> {messages.map((message) => ( <div key={message.id}> {message.role === "user" ? "User: " : "AI: "} {message.content} </div> ))} <h2>--- Messages with Reasoning ---</h2> {messages.map((message) => ( <div key={message.id}> {message.role === "user" ? "User: " : "AI: "} {message.parts.map((part, index) => { // text parts: if (part.type === "text") { return <div key={index}>{part.text}</div>; } // reasoning parts: if (part.type === "reasoning") { return ( <pre key={index}> {part.details.map((detail) => detail.type === "text" ? detail.text : "<redacted>", )} </pre> ); } })} </div> ))} <h2>--- Messages with Sources ---</h2> {messages.map((message) => ( <div key={message.id}> {message.role === "user" ? "User: " : "AI: "} {message.parts .filter((part) => part.type !== "source") .map((part, index) => { if (part.type === "text") { return <div key={index}>{part.text}</div>; } })} {message.parts .filter((part) => part.type === "source") .map((part) => ( <span key={`source-${part.source.id}`}> [ <a href={part.source.url} target="_blank"> {part.source.title ?? new URL(part.source.url).hostname} </a> ] </span> ))} </div> ))} {(status === "submitted" || status === "streaming") && ( <div> {status === "submitted" && "loading... "} <button type="button" onClick={() => stop()}> Stop </button> </div> )} <h2>--- Erorr ---</h2> {error && ( <div> <div>An error occurred.</div> <button type="button" onClick={() => reload()}> Retry </button> </div> )} <h2>--- Submit input and Button ---</h2> <form onSubmit={(event) => { handleSubmit(event, { // Custom body fieds per request body: { key_handle_submit: "key of handleSubmit", }, }); }} > <input name="prompt" value={input} placeholder="Type your message here..." onChange={handleInputChange} disabled={status !== "ready" || error != null} /> <button type="submit">Submit</button> </form> </div> ); } ``` ## 4. 拦截器(中间件) 在 Spring AI 中是 Advisor 的功能,而在 ai-sdk 中,则是通过 wrapLanguageModel 的方式 原理是: - 包裹大模型 provider,拦截大模型调用前后的过程,从而生成一个 wrapedLLM,后端直接调用这个新的模型 - 自定义 Middleware:transformParams 控制传输大模型前,wrapStream 和 wrapGenerate 分别控制大模型结果的流式和文本返回。 输出日志的自定义 middleware 示例: ```ts /** * Implementing Language Model Middleware * * customize your own middleware * * - `transformParams`: Modify the parameters of the language model call. * - `wrapGenerate`: wrap the doGenerate function. * - `wrapStream`: wrap the doStream function. * * you can modify the paramers, call the languaage moel, * and modify the result. in other words, you can do someting * before and after the language model call. * * following are examples from ai adk */ /** * Logging Middleware */ const loggingMiddleware: LanguageModelV1Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log("doGenerate called"); console.log(`params: ${JSON.stringify(params, null, 2)}`); const result = await doGenerate(); console.log("doGenerate finished"); console.log(`generated text: ${result.text}`); return result; }, wrapStream: async ({ doStream, params }) => { console.log("doStream called"); console.log(`params: ${JSON.stringify(params, null, 2)}`); const { stream, ...rest } = await doStream(); let generatedText = ""; const transformStream = new TransformStream< LanguageModelV1StreamPart, LanguageModelV1StreamPart >({ transform(chunk, controller) { if (chunk.type === "text-delta") { generatedText += chunk.textDelta; } controller.enqueue(chunk); }, flush() { console.log("doStream finished"); console.log(`generated text: ${generatedText}`); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, }; const loggingModel = wrapLanguageModel({ model: dashscope("deepseek-v3"), middleware: loggingMiddleware, }); ``` ## 5. 结构化输出 使用 useObject 和 streamObject 方法 原理是: - prompt 中提示大模型生成指定的内容 - 通过 schema 提示大模型生成的内容和格式,并进行类型审查 schema: ```ts /** * Object Generation * * @see https://ai-sdk.dev/docs/ai-sdk-ui/object-generation */ import { z } from "zod"; // define a schema for the notifications export const notificationSchema = z.object({ notifications: z.array( z.object({ name: z.string().describe("Name of a fictional person."), message: z.string().describe("Message. Do not use emojis or links."), }), ), }); ``` 前端和后端关键代码 ```tsx /** * Object Generation * * @see https://ai-sdk.dev/docs/ai-sdk-ui/object-generation */ // 前端 // ... const { isLoading, stop, error, object, submit } = useObject({ api: "/api/v1/object-generation/notification", schema: notificationSchema, onFinish({ object, error }) { // typed object, undefined if schema validation fails: console.log("Object generation completed:", object); // error, undefined if schema validation succeeds: console.log("Schema validation error:", error); }, onError(error) { // error during fetch request: console.error("An error occurred:", error); }, }); // ... // 后端 const context = (await request.json()) as string | undefined; const result = streamObject({ model: dashscope("qwen-plus"), schema: notificationSchema, prompt: `Generate 3 notifications for a messages app in this context:` + context, }); // ... ``` ## 6. 记忆持久化 分别实现了 本地文件 和 数据库 两种形式 原理是: - 通过后端 streamText 的 onFinishi 回调捕获生成结果,把这个结果保存下来 - 本地是直接把结果写入到 .chat/*.json 中,通过文件系统管理 - 数据库则是把结果和数据库的结果相比较,存入新的的消息 后端代码 ```ts /** * Chat API for Persistence * * @see https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence */ // ... const result = streamText({ model: dashscope("qwen-turbo"), messages, // store messages async onFinish({ response }) { await saveChat({ id, messages: appendResponseMessages({ // history messages messages, // response messages responseMessages: response.messages, }), }); }, // id format for server-side messages: experimental_generateMessageId: createIdGenerator({ prefix: CHAT_MESSAGE_SERVER_PREFIX, size: 16, }), }); // ... ``` ## 总结 已经完成了 Nextjs 的 AI 智能体的基本实现。 通过 Shadcn Registry 提高了这些代码的可重用性,符合 DRY 原则。模块化的组件加上 Regristry 服务,我可以非常方便地把这些代码集成到我的新项目中。 锻炼了自己的提示词工程功力,这份提示词已经非常接近可以使用的程度,效果已经和当初使用 Buildpad 时一样惊艳。xml,多步骤指令,ai 辅助生成 prompt。 计划: - Rag 实现。本地实现和接入服务 - 提示词从 Dify 迁移到项目中。包括 提示词缓存,提示词 token 优化,工具调用,mcp 调用 更多信息,去看上线的网站吧! 有帮助的话,点点 Star 谢谢各位! 开源地址:https://github.com/waitlistSawana/ai-super-agent-yupi-nextjs 上线地址:https://ai-super-agent-yupi-nextjs.hsawana9.com/
关于【因文件数量过多导致antd Upload上传组件卡死】的原因以及解决方案
# 1. 分析问题 对文件夹中的所有文件进行上传时,如果目标文件夹中的文件数量过多(比如:大于 10 万个文件),直接使用 antd 中的文件上传组件(Upload)必然出现卡死页面的现象。在查阅资料之后,我们发现出现该现象的原因有以下几点: * **事件处理负担过重** * 每个文件在上传过程中都会触发一系列的事件,如 `onChange`、`onProgress`、`onError` 等。Upload 组件需要对这些事件进行监听和处理。当文件数量庞大时,事件的触发频率会非常高,从而导致页面卡顿现象。 * **渲染性能问题** * 在文件上传的过程中,Upload 组件需要对每一个文件生成对应的 ui 展示文件的上传状态等信息。如果文件过多,频繁地操作 dom 结构极大占用了浏览器的资源,出现页面卡顿现象。 <img src="https://pic.code-nav.cn/post_picture/1917936850210033665/cGsbngz6SDpx4zO8.webp" alt="image.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1917936850210033665/5iqLFJVnADU3wbzm.webp" alt="image.png" width="100%" /> # 2. 解决思路 ### 2.1. 从生活中寻找解决方法 在现实生活中,并不缺少支持用户上传本地文件到云端的平台(比如:百度网盘、夸克网盘)。当用户将文件拖入平台的上传页面时,这些平台并没有因为文件的数量过多、体积多大而出现崩溃的现象。由此可知,一定存在解决方案。 ### 2.2. 思路 原生 html 中的 input 文件上传组件并不会频繁地触发相关事件也不会频繁地渲染 ui 结构,它只是当所有文件全部获取完毕之后只触发一次 `onchange`方法。开发者可以从 onchange 回调函数中获取用户上传的所有文件。 ```jsx <input type="file" webkitdirectory directory multiple // 只会调用一次 onChange={(data)=>{ console.log(data,'fileData') }} /> ``` <img src="https://pic.code-nav.cn/post_picture/1917936850210033665/1HzvL4P4RP6Kqn3Q.webp" alt="image.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1917936850210033665/0o2Y4tM4yKTrytHv.webp" alt="image.png" width="100%" /> 仅使用原生的 input 组件完成需求的话,开发效率真的很高吗?真的是最优解吗? 试想一下:如果我们使用原生的 input 完成,确实解决了页面卡顿的现象。但是,文件列表的 ui 结构、对已上传的文件的操作、文件上传状态等基础功能全部需要手写!这工作量非同一般。而且,Upload 组件对文件上传列表功能的实现效果堪称完美。于是,我们决定了最终的方案: 1. 在 Upload 组件下放置一个 input 组件,并利用 css 将其隐藏。 2. 当用户点击 Upload 组件时,进行事件的劫持,将事件转移到 input 组件中。 3. 当用户选择完毕目标文件夹后,利用 input 中的 onchange 方法得到所有的文件。 4. 在 onchange 事件回调函数中处理文件的批量上传逻辑。 # 3. 将思路转为代码 我们根据以上的思路,完成代码逻辑: 1. 构建 UI 结构 2. 当用户点击 Upload 组件时,进行事件的劫持,将事件转移到 input 组件中。 ```jsx export default ()=>{ // input 组件实例对象 const inputRef = useRef(); return <> <Upload action="https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload" directory fileList={fileUploadErrorList} // 其他逻辑自行补充 > <Button onClick={(e) => { // 阻止事件冒泡 e.stopPropagation(); // 点击后,将事件转移到 input 组件上 inputRef.current.click(); }} > 上传文件 </Button> </Upload> <input style={{ display: "none" }} ref={inputRef} type="file" webkitdirectory directory multiple /> </> } ``` 3. 当用户选择完毕目标文件夹后,利用 input 中的 onchange 方法得到所有的文件。 4. 在 onchange 事件回调函数中处理文件的批量上传逻辑。 ```jsx // input 组件 onChange方法回调函数 const onChange = (data) => { upload(Object.values(data.target.files)); }; // 分批上传,默认5个一组 const upload = async (list, size = 5) => { const total = list.length; for (let i = 0; i < total; i += size) { const requests = list.slice(i, i + size).map((item) => { const formData = new FormData(); formData.append("file", item); // uploadReq返回值是一个异步任务 return uploadReq(formData); }); // 该批次的异步任务统一执行 const resultData = await Promise.allSettled(requests); // 由于文件过多,只将上传失败的文件进行展示。 let errorList = []; resultData.map((res) => { errorList = []; if (!res?.value?.result?.success) { // 得到FormData中的文件对象file const file = res?.value?.file?.get("file"); file.status = "error"; file.response = res?.value?.result; errorList.push(file); } }); } }; // 上传文件--请求 const uploadReq = async (formData) => { try { const response = await fetch("http://localhost:5173/upload/file", { method: "POST", body: formData, credentials: "include", withCredentials: true, }); return { result: await response.json(), file: formData }; } catch (error) { return { result: error?.message || "网络异常,请稍后再试!", file: formData, }; } }; ``` 通过以上代码,已经可以完美的解决业务问题。写到这里,先给自己来一个一键三连赞👍🏻👍🏻👍🏻。其实,在 B 端页面开发领域,文件上传业务可谓司空见惯。如果能将以上的分批上传逻辑抽离成一个独立的 hook ,既可以提高团队的开发效率又可以在面试的过程中作为一个业务难点分享给面试官,一举两得岂不美哉。看到这里,你是不是会想直接把上述代码中的 `upload` 的方法提取出去,不就行了!如果你要这样想的话,我只能说:“兄弟,格局稍微打开一点。” 废话不多说,上才艺! # 4. 构建异步任务栈,实现文件分批上传 首先,在自定义的 hook 中,必须维护一个栈类型的数据结构。 其次,hook 向外暴漏一个追加异步任务的方法 `addTask`。 最后,暴漏执行异步任务的方法,此方法接受一个回调函数。异步任务执行时,将执行结果通过调用回调函数的方式传递给外界。 ```jsx export function syncFileLoadstack(request) { const stack = []; let uploadedCount = 0; const addTask = (data) => { if (Array.isArray(data) && data.length > 0) { // 进栈 stack.push(...data); } else { stack.push(data); } return { run }; }; const run = async (callback = () => {}) => { const len = stack.length; while (uploadedCount < len) { const data = stack.pop(); uploadedCount++; try { const res = await data.request?.(); callback(res); } catch (error) { callback(res); } } }; return { addTask }; } ``` ```jsx // 测试使用 -- input 的 onChange 方法改写 const App = () => { // 导入 hook const { addTask } = syncFileLoadstack(); const onChange = async (data) => { // 分批上传,默认5个一组 const files = Object.values(data.target.files).map((item) => ({ request: () => { const formData = new FormData(); formData.append("file", item); // 这是一个异步任务-发请求 return uploadReq(formData); }, })); addTask(files).run((res) => { // 每一个任务的执行结果可以从这里获取。 console.log(res, "file"); }); }; // xxx其他逻辑代码xxx }; ```
用户中心上线(NextJS改造)
# 用最新的 NextJS 实现 跟着鱼皮哥的课程实现的项目,一边学习项目的规范和实践经验,一边探索用 nextjs 实现相同的功能。 项目已上线,欢迎大家体验! ## 项目 Github 仓库:[用户中心项目 - 鱼皮](https://github.com/waitlistSawana/user-center-yupi-nextjs) 上线地址:[鱼皮用户中心项目改造 - NextJs 版](https://user-center-yupi-nextjs.highlightpulse.site/) 开发记录:[开发记录.md](https://github.com/waitlistSawana/user-center-yupi-nextjs/blob/main/%E5%BC%80%E5%8F%91%E8%AE%B0%E5%BD%95.md) 主要功能 - 用户注册、登录、注销 - 用户表格查看 待完善部分 - [ ] 统一后端返回 - [ ] 定义统一错误码 - [ ] docker - [ ] nginx ## 技术栈 总体来说,使用 T3 Stack 启动了 NextJS 的项目,前后端同构,前端是 React 后端是 Nodejs。T3 stack 集成了生产级的配置,配好了 prettier 和 eslint,以及全栈需要各项开源项目。 前端: - NextJS 15 + Tailwindcss 4 + React 19,跟进最新技术框架 - Shadcn UI 定制化组件开发,学习其设计方法和理念,组件属性解构、cn 和 cva 模式 - Tankstack Query 异步数据状态管理,高效实现 React 组件数据获取、缓存和更新 - Tankstack Table 强大的 Headless UI 表格状态管理和数据操作 后端: - 项目分层 api、services、repositories - JSDoc 风格代码注释 - Drizzle ORM 数据库访问 - 阿里云 RDS 的 Serverless MySql 数据库 - JWT session 和 cookie 管理 其他 - 测试 Vitest (AI 写的),Rest Client 测试 api 端点 - Cloufalre 域名托管 - Vercel 项目部署上线 - Github 数据托管,commit 命名遵循 Angular Commit Message Guidelines # 项目结构 可以直接在 [Github 仓库](https://github.com/waitlistSawana/user-center-yupi-nextjs/tree/main/src) 中查看 <img src="https://pic.code-nav.cn/post_picture/1857029075076202498/AdbCML9myTZWEpDW.webp" alt="tmp51B.png" width="100%" /> ## 后端 app/api/v1/user controler 的接口,前端直接访问,如 example.com/api/v1/user/current 。 get 方法 和 post 方法 server/db/ 数据库相关 Drizzle ORM server/db/index.ts 创建数据库连接 server/db/schema.ts 数据库表设计 server/db/types.ts 根据 schema 自动定义类型 server/repositories/user.ts 用户数据访问层 server/services/user.ts 服务层 server/utils/error-handling.ts 用 TRPCError 包装 NextResponse server/utils/hash.ts 用 bcryptjs 密码加密函数 server/utils/session.ts 根据 NextJS 文档建议,JWT session 创建 ## 前端 app/ 页面 app/(auth)/ 用户相关页面 app/(auth)/login 登录表单,useEffect + React Form Hook + Zod + axios app/(auth)/register 注册表单,Tankstack Query(useMutation) + React Form Hook + Zod + axios app/(auth)/logout 登出页面,清除 session app/(dashboard)/layout.ts 侧边栏设计 app/(dashboard)/welcome/ 用户主页 app/(dashbaord)/user-manage 用户数据库表格,Tankstack Table components/ 组件整理 components/ui/ 导入进来的 shadcn ui 组件,可定制 components/aceternity/ 从组件库 aceternity ui 复制来的组件代码 compoents/fallback/ 用于 suspense 的骨架组件 components/user-manage tankstack table 表格的构建,columns 定义 compoents/ ** .tsx 自建的各种 React 组件 ## 其他 src/env.js 复用 t3 实现环境变量配置 src/middleware.ts 中间件,鉴权,路由保护和跳转 (ps. 之前出了挺火的漏洞233,所以必须在别处二次鉴权) test/ 测试代码 test/rest-client 后端端点连接和测试 eslint.config.js 静态检查 prettier.config.js 代码格式化
bi项目完结&扩展
## 项目地址 www.pikim.fun (AItoken有限,各位鱼友轻点打) ## 项目实现思路 我并没有把他当做一个纯BI的项目,而是借助当下火爆的大模型,利用大模型开发一些有意思的AI功能。当然,目前实现的功能也少,后期在慢慢完善了吧哈哈哈~ ### 实现功能 - 图表生成 - 图片理解 - AI对话(海龟汤) 部分页面只有管理员能看到,后期会再修一下逻辑~ ## 扩展点 这里对图表生成扩展最对,也是花费时间最多的地方,后面两个模块仅仅实现功能,等有时间慢慢优化 1. 对图表表做了分表,easyExcel 提取的数据分离原始表格,实现一个图表一个数据表 2. 图表生成添加生成超时功能 3. 对生成失败表格进行重试(还未完善) 4. 图表生成校验,单用户相同图表数据,相同图表类型,只能生成一次 5. 用redis,对图表的分页查询做缓存 6. 管理员能查看所有用户生成的图表,普通用户只能查看自己的图表记录 7. 添加注册页面,接口 9. 对图表做了CRUD,支持更新,删除动态表格(仅支持关管理员修改) 10. 扩展图片理解页面 11. 扩展AI对话海龟汤游戏,查询对话记录等 ## 局限性 1. 图表分析,只支持excel文档数据,用户上传文件受限 2. 图表分析要求较高的AI算力,目前AI生成复杂的图表耗时长,内容有误,渲染失败等等 3. 图片理解页面,暂未开发历史图片页面 4. AI对话,仅仅是实现功能 ## 后期计划&优化 ### 图表生成 1. 统计当前用户生成图表次数 2. 对图表页面实现自动刷新功能 3. 限制单个用户每天生成图表次数 4. 分析结果采用,“展开更多”显示 ### 图片理解 1. 添加历史图片理解页面 2. 图片仅仅上传到服务器,尝试采用cos➕CDN存储图片 ### AI对话 1. 添加多个ai,接口处理对话,每次轮训不同AI接口 2. 用redis缓存聊天记录,mq添加聊天信息,写入到数据库 3. 分库分表,对每个房间消息做隔离 4. 用redis缓存当前对话记录 5. 聊天框异常,用户突然退出,死机,黑屏等操作,保证聊天记录计时被保存 6. 当房间没有活动,超过某一时间后,自动结束,修改房间状态为“已关闭” ## 最后 项目的难点 1. 在于对动态表做CRUD,由于用户上传的表数据不一,导致每个表的表名,字段名,字段数量都不相同,这样去拼接动态sql,代码都比较繁杂。 2. 处理科大讯飞的AI接口,没有完全封装,需要手动修改部分代码 3. 项目上线,后端采用docker-compose.yml部署容器,对docker-compose语法不熟悉,试错了多次才成功部署。  <img src="https://pic.code-nav.cn/post_picture/1609191652126097409/HQwNjqaHCBwGk8YR.webp" alt="image.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1609191652126097409/H3FMtqaztGT3oMje.webp" alt="image.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1609191652126097409/y1c7Fysvm1SdPOgy.webp" alt="image.png" width="100%" />
AddButton 组件封装如下 "react": "^18.2.0", "antd": "^5.15.1", "antd-style": "^3.6.2", SvgIconComp 组件链接 https://www.codefather.cn/essay/1897849299510984706 import { memo } from 'react'; import { Button } from 'antd'; import { createStyles, cx } from 'antd-style'; import SvgIconComp from '@/components/SvgIcon'; const useStyles = createStyles(({ token }) => ({ buttonContent: { display: 'flex', alignItems: 'center', color: token.colorTextLabel, }, text: { marginLeft: 6, }, button: { ':hover': { backgroundColor: `${token.colorPrimaryBgHover} !important`, }, }, })); const AddButton = (props) => { const { styles } = useStyles(); const { disabled, title } = props || {}; return ( <Button size={'small'} type={'text'} {...props} className={styles.button}> <div className={styles.buttonContent}> <SvgIconComp iconName="autamate/addDesc" /> <div className={cx(styles.text, disabled && 'disabledstyle')}>{title || '添加'}</div> </div> </Button> ); }; export default memo(AddButton);
