RAGFlow 前端项目:从 Vue2 到 React 的对照指南
本文档面向有 Vue2 经验的开发者,帮助你快速理解 RAGFlow Web 前端项目的 React 技术栈。
一、技术栈总览对照
| 维度 | Vue2 项目(你熟悉的) | RAGFlow React 项目 |
|---|---|---|
| 框架 | Vue 2 | React 18 |
| 语言 | JavaScript 为主 | TypeScript(必须) |
| 构建工具 | Webpack / Vue CLI | Vite |
| 路由 | Vue Router | react-router v7 |
| 状态管理 | Vuex | TanStack React Query(服务端状态)+ useState(客户端状态) |
| UI 组件库 | Element UI | shadcn/ui(基于 Radix UI + Tailwind CSS) |
| 样式方案 | <style scoped> / SCSS / Less |
Tailwind CSS(原子类)+ Less(全局变量) |
| 表单 | v-model 双向绑定 | react-hook-form + zod 校验 |
| HTTP 请求 | axios 封装 | axios 封装 + React Query 缓存层 |
| 国际化 | vue-i18n | i18next + react-i18next |
| 组件形式 | SFC 单文件组件(.vue) | TSX 函数式组件(.tsx) |
二、核心概念对照
2.1 组件定义
Vue2 --- 单文件组件(SFC)
vue
<template>
<div class="user-card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>
<script>
export default {
name: 'UserCard',
props: {
user: { type: Object, required: true }
},
data() {
return { isExpanded: false }
},
methods: {
toggle() { this.isExpanded = !this.isExpanded }
}
}
</script>
<style scoped>
.user-card { padding: 16px; }
</style>
React --- TSX 函数式组件
tsx
// 模板、逻辑、样式全在一个函数中
interface UserCardProps {
user: { name: string; email: string };
}
export default function UserCard({ user }: UserCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="p-4"> {/* Tailwind CSS 原子类 */}
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
关键区别:
- React 没有
template/script/style分离,一切都在函数中 - 用
useState替代data(),用props替代 Vue 的props选项 - 用
className替代class(因为class是 JS 保留字) - 用
{}替代{``{}}插值,用onClick替代@click
2.2 响应式数据
Vue2 --- 自动响应式
js
data() {
return { count: 0, name: '' }
},
// 直接赋值即可触发更新
methods: {
increment() { this.count++ },
setName(val) { this.name = val }
}
React --- 手动触发更新
tsx
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 必须调用 setter 才能触发更新
const increment = () => setCount(count + 1);
// 或者用函数式更新
const increment = () => setCount(prev => prev + 1);
关键区别:
- Vue2 的
data是自动响应式的,直接修改就能更新视图 - React 的
useState返回[值, 设值函数],必须调用设值函数才能更新 - React 没有 Vue 的"修改对象属性自动触发更新",需要整体替换对象
2.3 计算属性 vs useMemo
Vue2
js
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
React
tsx
const fullName = useMemo(() => {
return firstName + ' ' + lastName;
}, [firstName, lastName]); // 依赖数组,类似 Vue 的自动依赖收集
2.4 侦听器 vs useEffect
Vue2
js
watch: {
keyword(newVal) {
this.search(newVal);
}
}
React
tsx
useEffect(() => {
search(keyword);
}, [keyword]); // 依赖数组决定何时执行
注意 :useEffect 比 watch 更强大也更复杂:
- 空依赖
[]=mounted时执行一次 - 有依赖 = 依赖变化时执行
- 返回函数 =
beforeDestroy时执行(清理副作用)
2.5 条件渲染与列表渲染
Vue2
vue
<!-- 条件 -->
<div v-if="isLoading">加载中...</div>
<div v-else>内容</div>
<!-- 列表 -->
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
React
tsx
{/* 条件:三元表达式 */}
{isLoading ? <div>加载中...</div> : <div>内容</div>}
{/* 条件:逻辑与 */}
{isLoading && <div>加载中...</div>}
{/* 列表:map */}
{list.map(item => (
<div key={item.id}>{item.name}</div>
))}
2.6 事件处理
Vue2
vue
<button @click="handleClick">点击</button>
<input @input="onInput" />
<form @submit.prevent="onSubmit">...</form>
React
tsx
<button onClick={handleClick}>点击</button>
<input onInput={onInput} />
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>...</form>
关键区别:
@click→onClick,@input→onInput(驼峰命名)- 没有事件修饰符(
.prevent、.stop),需要手动调用e.preventDefault()等 - 事件处理函数默认接收事件对象
e
2.7 表单双向绑定
Vue2 --- v-model
vue
<el-input v-model="form.name" />
<el-select v-model="form.type">...</el-select>
v-model 是语法糖,等价于下面完整代码:
vue
//双向绑定 = 赋值 + 监听修改,Vue 把这一套封装成 v-model,一行搞定。
<el-input
:value="form.name"
@input="form.name = $event.target.value"
/>
React 原生手动双向绑定(最基础对照)
const [name, setName] = useState('')
<Input
value={name}
onChange={(e) => setName(e.target.value)}
/>
和 Vue 底层一模一样,只是没有语法糖,必须手动写 value + onChange。
如果表单字段多(20 个输入框),每个都写一遍,代码巨冗余,还得自己写校验、错误提示。
React --- react-hook-form(专门解决「多表单、校验、类型」的库)
tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. 定义校验 schema
// zod 校验规则(替代 Vue 模板里写的校验)
const FormSchema = z.object({
name: z.string().min(1, '名称不能为空'),//min(1) = 字符串最少要有1个字符,校验不通过,抛出提示文字
type: z.string().min(1, '请选择类型'),
});
// 2. 初始化表单
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),// 绑定上面的校验规则
defaultValues: { name: '', type: '' },// 表单默认值,等价Vue form初始数据
});
// 3. 渲染表单
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name" // 和schema字段对应,等同于v-model绑定的form.name
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormControl>
{/* 核心:{...field} 等价Vue v-model */}
<Input {...field} />
</FormControl>
<FormMessage /> {/* 自动显示校验错误,不用手动写v-if错误 */}
</FormItem>
)}
/>
</form>
</Form>
关键区别:
- 没有
v-model,react-hook-form 通过field对象展开绑定({...field}自动包含 value + onChange) - 校验逻辑从模板移到了
zodschema 中,类型安全 FormMessage自动显示校验错误,不需要手动写错误提示
重点讲解:
-
{...field}到底是什么?field对象自动包含两个属性,和原生双向绑定完全对应:field = { value: form的值, onChange: 自动更新表单值的函数 }<Input {...field} />等价展开成:<Input value={field.value} onChange={field.onChange} />等同于 Vue 的
v-model="form.name",只是 React 没有语法糖,靠解构field实现一键绑定。 -
表单提交
<form onSubmit={form.handleSubmit(onSubmit)}>form.handleSubmit:提交时自动执行 zod 校验 ,校验通过才执行你的提交函数
onSubmit;校验失败自动生成错误信息,交给FormMessage展示。Vue 需要手动调用
form.validate(),React 这里内置了。
2.8 组件通信
| 场景 | Vue2 | React |
|---|---|---|
| 父→子 | props |
props(一样) |
| 子→父 | $emit |
回调函数 props(onXxx) |
| 跨层级 | provide/inject |
React Context 或直接 props 逐层传递 |
| 全局状态 | Vuex | React Query(服务端)+ useState(客户端) |
Vue2 子→父
vue
<!-- 子组件 -->
<button @click="$emit('confirm', data)">确认</button>
<!-- 父组件 -->
<Child @confirm="handleConfirm" />
React 子→父
tsx
// 子组件
<button onClick={() => onConfirm(data)}>确认</button>
// 父组件
<Child onConfirm={handleConfirm} />
三、项目架构对照
3.1 目录结构
Vue2 典型项目 RAGFlow React 项目
├── src/ ├── src/
│ ├── views/ │ ├── pages/ ← 页面(同 views)
│ ├── components/ │ ├── components/ ← 公共组件
│ │ └── xxx.vue │ │ └── ui/ ← shadcn/ui 基础组件
│ ├── store/ │ ├── hooks/ ← 自定义 hooks(替代 store)
│ │ ├── index.js │ │ ├── use-knowledge-request.ts
│ │ └── modules/ │ │ ├── use-llm-request.tsx
│ ├── router/ │ │ └── use-document-request.ts
│ │ └── index.js │ ├── services/ ← API 服务层
│ ├── api/ │ │ ├── knowledge-service.ts
│ │ └── xxx.js │ │ └── user-service.ts
│ ├── utils/ │ ├── utils/ ← 工具函数
│ │ └── request.js │ │ ├── next-request.ts ← axios 封装
│ └── assets/ │ │ └── api.ts ← API 端点常量
│ ├── interfaces/ ← TypeScript 类型定义
│ │ ├── database/ ← 数据模型
│ │ └── request/ ← 请求参数
│ └── locales/ ← 国际化翻译文件
3.2 状态管理:Vuex vs React Query
这是最大的思维转变。Vue2 中 Vuex 管理所有状态,React 项目中服务端状态 和客户端状态分开管理。
Vuex 模式(你熟悉的)
js
// store/modules/knowledge.js
const state = { list: [], total: 0, loading: false };
const mutations = {
SET_LIST(state, data) { state.list = data; }
};
const actions = {
async fetchList({ commit }, params) {
commit('SET_LOADING', true);
const data = await api.getList(params);
commit('SET_LIST', data);
commit('SET_LOADING', false);
}
};
React Query 模式(本项目使用)
tsx
// hooks/use-knowledge-request.ts
export const useFetchKnowledgeList = () => {
// useQuery 专门处理「查询类接口」(查列表、查详情)
const { data, isFetching: loading } = useQuery({
//缓存 key,相当于数据的唯一身份证
//参数不变:再次调用不会重复发请求,直接拿缓存
//参数变了:自动重新请求新数据
queryKey: ['knowledgeList', params], // 缓存键
queryFn: async () => { // 请求函数
//自动生成内置状态,不用自己定义:
//data:后端返回的列表数据(等价 Vuex state.list)
//isFetching:请求加载中(等价 Vuex loading)
//isError:请求失败标记、error 错误信息
const { data } = await kbService.getList(params);
return data;
},
});
return { data, loading };
};
// 变更操作
export const useCreateKnowledge = () => {
const queryClient = useQueryClient();
// useMutation:专门处理「修改类接口」新增 / 编辑 / 删除
const { mutateAsync } = useMutation({
mutationFn: kbService.create,//// 新增接口
onSuccess: () => {
// 自动刷新列表,不需要手动 commit
// 把缓存里的列表数据标记过期,组件会自动重新请求列表
queryClient.invalidateQueries({ queryKey: ['knowledgeList'] });
},
});
return { createKnowledge: mutateAsync };
};
核心差异:
- Vuex:手动管理 loading/error/data 状态,手动触发刷新
- React Query:自动管理 loading/error/data,自动缓存和去重,
invalidateQueries自动刷新 - 不需要写
mutation/action,useQuery和useMutation一行搞定
3.3 路由对照
Vue2 --- Vue Router
js
const routes = [
{ path: '/login', component: Login },
{ path: '/home', component: Home, meta: { requiresAuth: true } },
{ path: '/dataset/:id', component: Dataset },
];
const router = new VueRouter({ routes });
// 路由守卫
router.beforeEach((to, from, next) => { ... });
React --- react-router v7
tsx
// routes.tsx --- 声明式配置
const routeConfig = [
{ path: '/login', Component: lazy(() => import('@/pages/login')) },
{ path: '/home', Component: lazy(() => import('@/pages/home')) },
{ path: '/dataset/:id', Component: lazy(() => import('@/pages/dataset')) },
];
const router = createBrowserRouter(routeConfig);
// 路由参数获取
const { id } = useParams(); // 对应 this.$route.params.id
const [searchParams] = useSearchParams(); // 对应 this.$route.query
const navigate = useNavigate(); // 对应 this.$router.push
关键区别:
- 没有全局路由守卫,用
loader函数或组件内useEffect实现类似功能 - 懒加载用
React.lazy()+Suspense,而非 Vue 的() => import() - 路由跳转用
navigate('/path'),而非this.$router.push('/path')
3.4 样式对照
Vue2 --- Scoped Style + SCSS
vue
<template>
<div class="card">
<h3 class="card__title">{{ title }}</h3>
</div>
</template>
<style lang="scss" scoped>
.card {
padding: 16px;
&__title {
font-size: 18px;
font-weight: bold;
}
}
</style>
React --- Tailwind CSS 原子类
tsx
export default function Card({ title }: { title: string }) {
return (
<div className="p-4">
<h3 className="text-lg font-bold">{title}</h3>
</div>
);
}
Tailwind 常用类名速查:
| CSS 属性 | Tailwind 类名 |
|---|---|
padding: 16px |
p-4 |
margin: 8px 0 |
my-2 |
display: flex |
flex |
align-items: center |
items-center |
justify-content: space-between |
justify-between |
font-size: 14px |
text-sm |
font-weight: bold |
font-bold |
color: #333 |
text-gray-800 |
background: white |
bg-white |
border-radius: 8px |
rounded-lg |
width: 100% |
w-full |
display: none |
hidden |
overflow: hidden |
overflow-hidden |
position: relative |
relative |
条件样式合并 (替代 Vue 的 :class):
tsx
import { cn } from '@/lib/utils';
<div className={cn(
'p-4 rounded-lg',
isActive && 'bg-blue-500 text-white', // 条件类名
isDisabled && 'opacity-50 cursor-not-allowed'
)} />
3.5 API 请求对照
Vue2 --- 直接调用 API + Vuex
js
// api/knowledge.js
export function getList(params) {
return request.get('/api/v1/datasets', { params });
}
// 在组件或 Vuex action 中
async fetchList() {
this.loading = true;
try {
const { data } = await getList(this.params);
this.list = data;
} finally {
this.loading = false;
}
}
React --- 三层架构:Service → Hook → Component
tsx
// 1. Service 层:纯 API 调用
// services/knowledge-service.ts
const list = (params: Params) => nextRequest.get('/api/v1/datasets', { params });
// 2. Hook 层:React Query 封装
// hooks/use-knowledge-request.ts
export const useFetchKnowledgeList = () => {
const { data, isFetching } = useQuery({
queryKey: ['knowledgeList', params],
queryFn: () => knowledgeService.list(params),
});
return { data, loading: isFetching };
};
// 3. Component 层:使用 hook
// pages/dataset/index.tsx
export default function DatasetPage() {
const { data, loading } = useFetchKnowledgeList();
if (loading) return <Spinner />;
return <DataTable data={data} />;
}
3.6 国际化对照
Vue2 --- vue-i18n
vue
<template>
<p>{{ $t('knowledge.name') }}</p>
</template>
<script>
this.$t('knowledge.name')
</script>
React --- react-i18next
tsx
const { t } = useTranslation();
<p>{t('knowledge.name')}</p>
几乎一样,只是获取 t 函数的方式不同。
四、生命周期对照
| Vue2 生命周期 | React Hook | 说明 |
|---|---|---|
created |
useState 初始化 + 函数体 |
组件函数体本身就在创建时执行 |
mounted |
useEffect(() => {}, []) |
空依赖 = 只在挂载后执行 |
updated |
useEffect(() => {}, [dep]) |
依赖变化时执行 |
beforeDestroy |
useEffect(() => () => cleanup, []) |
返回清理函数 |
watch |
useEffect(() => {}, [dep]) |
依赖变化时执行 |
五、本项目特殊模式
5.1 自定义 Hooks 拆分
本项目大量使用自定义 hooks 来拆分逻辑,这是 React 的核心模式:
pages/dataset/dataset/
├── index.tsx ← 页面主组件(组合 hooks)
├── use-dataset-table-columns.tsx ← 表格列定义
├── use-run-document.ts ← 运行文档逻辑
├── use-upload-document.ts ← 上传文档逻辑
└── use-bulk-operate-dataset.tsx ← 批量操作逻辑
类比 Vue2 :相当于把 methods 中的逻辑拆到单独文件,通过 import 引入。Vue2 的 mixins 也可以实现,但有命名冲突问题;React hooks 通过参数显式传递,更安全。
5.2 shadcn/ui 组件模式
本项目使用 shadcn/ui 模式,这不是一个 npm 包,而是可复制的组件代码 放在 components/ui/ 下:
- 基于 Radix UI 无障碍原语
- 使用 Tailwind CSS 样式
- 使用
cva(class-variance-authority)管理变体 - 完全可控,可以直接修改源码
类比 Vue2:相当于 Element UI,但组件代码在你的项目中,你可以随意修改。
5.3 SSE 流式请求
本项目聊天功能使用 Server-Sent Events(SSE)实现流式响应:
tsx
// hooks/logic-hooks.ts 中的 useSendMessageWithSse
// 使用原生 fetch + EventSourceParserStream 处理流式数据
const response = await fetch(url, { method: 'POST', body, headers });
const reader = response.body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream());
这在 Vue2 项目中通常也需要类似处理,没有框架差异。
六、快速上手建议
6.1 思维转换清单
- 不再有 this:所有数据都是函数内的变量,通过参数和返回值传递
- 手动管理更新:修改状态必须调用 setter,不能直接赋值
- JSX 就是 JS :模板就是 JavaScript,可以用任何 JS 语法(
map、三元、逻辑与) - 一切皆函数 :没有
.vue文件,组件就是返回 JSX 的函数 - hooks 就是逻辑复用:替代 mixins、替代 Vuex action 中的逻辑
6.2 推荐学习路径
- 先学 React 基础 :
useState、useEffect、useRef、条件渲染、列表渲染 - 再学 TypeScript 基础:接口定义、泛型、类型推断(本项目重度使用)
- 然后学 React Query :理解
useQuery/useMutation的缓存机制 - 最后看项目代码 :从简单页面开始(如
pages/files/),逐步理解复杂页面
6.3 推荐阅读
- React 官方文档 --- 交互式教程,非常适合入门
- TanStack React Query 文档 --- 理解服务端状态管理
- Tailwind CSS 文档 --- 查找原子类名
- shadcn/ui 文档 --- 理解 UI 组件用法