RAGFlow 前端项目:从 Vue2 到 React 的对照指南

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]); // 依赖数组决定何时执行

注意useEffectwatch 更强大也更复杂:

  • 空依赖 [] = 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>

关键区别

  • @clickonClick@inputonInput(驼峰命名)
  • 没有事件修饰符(.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)
  • 校验逻辑从模板移到了 zod schema 中,类型安全
  • 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/actionuseQueryuseMutation 一行搞定

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 思维转换清单

  1. 不再有 this:所有数据都是函数内的变量,通过参数和返回值传递
  2. 手动管理更新:修改状态必须调用 setter,不能直接赋值
  3. JSX 就是 JS :模板就是 JavaScript,可以用任何 JS 语法(map、三元、逻辑与)
  4. 一切皆函数 :没有 .vue 文件,组件就是返回 JSX 的函数
  5. hooks 就是逻辑复用:替代 mixins、替代 Vuex action 中的逻辑

6.2 推荐学习路径

  1. 先学 React 基础useStateuseEffectuseRef、条件渲染、列表渲染
  2. 再学 TypeScript 基础:接口定义、泛型、类型推断(本项目重度使用)
  3. 然后学 React Query :理解 useQuery/useMutation 的缓存机制
  4. 最后看项目代码 :从简单页面开始(如 pages/files/),逐步理解复杂页面

6.3 推荐阅读