# todolist **Repository Path**: training-demo/todolist ## Basic Information - **Project Name**: todolist - **Description**: 学习 React 的综合小案例,使用 json-server 进行接口定义。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-11 - **Last Updated**: 2026-03-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # TodoList - React TypeScript 基于训练营 React 教程中的 TODOLIST 章节,使用 **React + TypeScript + Vite** 重构的 TodoMVC 应用。原教程使用 Class 组件,本项目全部转换为**函数式组件 + Hooks**,并注重 TypeScript 类型的复用。 ## 技术栈 - React 19 + TypeScript(函数式组件 + Hooks) - Vite(开发构建工具 + 代理配置) - Axios(HTTP 请求) - json-server(模拟 RESTful API) ## 项目结构 ``` todolist/ ├── data.json # json-server 模拟数据 ├── index.html # HTML 入口 ├── package.json ├── vite.config.ts # Vite 配置(含代理) ├── tsconfig.json └── src/ ├── main.tsx # 应用入口 ├── App.tsx # 根组件 ├── types/ │ └── todo.ts # 共享 TS 类型定义 ├── styles/ │ ├── base.css # 基础样式 │ └── index.css # TodoMVC 样式 └── components/ ├── TodoHeader.tsx # 头部 - 添加待办 ├── TodoMain.tsx # 主体 - 列表展示/删除/选中/筛选 └── TodoFooter.tsx # 底部 - 统计/筛选切换/清除已完成 ``` --- ## 完成步骤 ### 步骤一:初始化项目 使用 Vite 创建 React + TypeScript 项目: ```bash npm create vite@latest todolist -- --template react-ts cd todolist npm install ``` ### 步骤二:安装依赖 安装运行时依赖 `axios` 和开发依赖 `json-server`: ```bash npm install axios npm install -D json-server ``` 在 `package.json` 的 `scripts` 中添加 json-server 启动脚本: ```json { "scripts": { "dev": "vite", "server": "json-server data.json --port 8888", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" } } ``` ### 步骤三:模拟接口 在项目根目录创建 `data.json`,作为 json-server 的数据源: ```json { "todos": [ { "id": "1", "name": "豆豆", "done": false }, { "name": "打豆豆", "done": true, "id": "4" } ] } ``` 启动 json-server 后,自动提供以下 RESTful API: | 方法 | 路径 | 说明 | |------|------|------| | GET | `/todos` | 获取所有待办 | | GET | `/todos/:id` | 获取单个待办 | | POST | `/todos` | 添加待办 | | PUT | `/todos/:id` | 全量更新待办 | | PATCH | `/todos/:id` | 局部更新待办 | | DELETE | `/todos/:id` | 删除待办 | > **PUT vs PATCH**:PUT 是全量修改,修改数据中的某一项也要把其他数据带过去,否则其他数据会被清除;PATCH 是补丁,只修改传递的字段。 ### 步骤四:配置 Vite 代理 修改 `vite.config.ts`,将 `/api` 前缀的请求代理到 json-server,避免跨域问题: ```ts // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { proxy: { '/api': { target: 'http://localhost:8888', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, }) ``` 这样前端代码中请求 `/api/todos` 会被自动代理到 `http://localhost:8888/todos`。 ### 步骤五:定义 TypeScript 共享类型 创建 `src/types/todo.ts`,集中定义项目中复用的类型: ```ts // src/types/todo.ts export interface TodoItem { id: string name: string done: boolean } export type NewTodo = Omit export type FilterType = 'all' | 'active' | 'completed' ``` 类型复用说明: - **`TodoItem`**:完整的待办项类型,在 `App`、`TodoMain`、`TodoFooter` 组件中复用。 - **`NewTodo`**:通过 `Omit` 派生,用于新增待办时的请求参数(不含 `id`,由服务端生成),在 `TodoHeader` 组件中使用。 - **`FilterType`**:筛选类型的联合类型,在 `App`、`TodoMain`、`TodoFooter` 组件间共享,控制列表的筛选状态。 - **`Pick`**:在 `TodoMain` 组件中用于切换完成状态的参数类型。 ### 步骤六:创建样式文件 创建 `src/styles/base.css` 和 `src/styles/index.css`,包含 TodoMVC 的完整样式(`todoapp`、`todo-list`、`toggle`、`footer`、`filters` 等 class)。样式来自经典 TodoMVC 模板。 在入口文件 `src/main.tsx` 中引入: ```tsx // src/main.tsx import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './styles/base.css' import './styles/index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( , ) ``` ### 步骤七:创建根组件 App `App.tsx` 作为根组件,负责管理待办列表状态、筛选状态和清除已完成操作,将数据和方法通过 props 下发给子组件: ```tsx // src/App.tsx import { useCallback, useEffect, useState } from 'react' import axios from 'axios' import TodoHeader from './components/TodoHeader' import TodoMain from './components/TodoMain' import TodoFooter from './components/TodoFooter' import type { TodoItem, FilterType } from './types/todo' export default function App() { const [list, setList] = useState([]) const [filter, setFilter] = useState('all') const getTodoList = useCallback(async () => { const res = await axios.get('/api/todos') setList(res.data) }, []) useEffect(() => { getTodoList() }, [getTodoList]) const clearCompleted = useCallback(async () => { const completedItems = list.filter((item) => item.done) await Promise.all( completedItems.map((item) => axios.delete(`/api/todos/${item.id}`)) ) await getTodoList() }, [list, getTodoList]) return (
) } ``` 关键点: - `useState('all')` 管理当前筛选状态,默认显示全部。 - `clearCompleted` 过滤出所有已完成项,通过 `Promise.all` 并发删除后刷新列表。 - `filter` 和 `setFilter` 分别传递给 `TodoMain`(用于过滤显示)和 `TodoFooter`(用于高亮当前筛选按钮和切换筛选)。 ### 步骤八:组件拆分 — TodoHeader(添加功能) `TodoHeader` 负责头部输入框和添加新待办。通过 `interface` 定义 Props 类型,从 `types/todo.ts` 导入 `NewTodo` 类型: ```tsx // src/components/TodoHeader.tsx import { useState, type KeyboardEvent } from 'react' import axios from 'axios' import type { NewTodo } from '../types/todo' interface TodoHeaderProps { getTodoList: () => Promise } export default function TodoHeader({ getTodoList }: TodoHeaderProps) { const [todoName, setTodoName] = useState('') const addTodo = async (e: KeyboardEvent) => { if (e.key !== 'Enter') return if (!todoName.trim()) return const newTodo: NewTodo = { name: todoName, done: false } await axios.post('/api/todos', newTodo) setTodoName('') await getTodoList() } return (

todos

setTodoName(e.target.value)} onKeyUp={addTodo} />
) } ``` 关键点: - `KeyboardEvent` 为键盘事件提供类型约束。 - `NewTodo` 类型复用自 `TodoItem`,通过 `Omit` 排除 `id` 字段。 - 按下 Enter 后校验非空、发送 POST 请求、清空输入框、刷新列表。 ### 步骤九:组件拆分 — TodoMain(列表渲染 / 删除 / 选中 / 筛选显示) `TodoMain` 负责渲染待办列表,处理删除和切换完成状态,并根据 `filter` 过滤显示内容: ```tsx // src/components/TodoMain.tsx import { useMemo } from 'react' import axios from 'axios' import type { TodoItem, FilterType } from '../types/todo' interface TodoMainProps { list: TodoItem[] filter: FilterType getTodoList: () => Promise } export default function TodoMain({ list, filter, getTodoList }: TodoMainProps) { const filteredList = useMemo(() => { switch (filter) { case 'active': return list.filter((item) => !item.done) case 'completed': return list.filter((item) => item.done) default: return list } }, [list, filter]) const delTodo = async (id: string) => { await axios.delete(`/api/todos/${id}`) await getTodoList() } const changeDone = async ({ id, done }: Pick) => { await axios.patch(`/api/todos/${id}`, { done: !done }) await getTodoList() } return (
    {filteredList.map((item) => (
  • changeDone(item)} />
  • ))}
) } ``` 关键点: - **筛选功能**:通过 `useMemo` 根据 `filter` 计算 `filteredList`,`all` 显示全部、`active` 只显示未完成、`completed` 只显示已完成。 - **删除功能**:点击 `destroy` 按钮,调用 `DELETE /api/todos/:id` 后刷新列表。 - **选中功能**:切换 checkbox 时调用 `PATCH /api/todos/:id`,将 `done` 取反。 - `Pick` 从完整类型中精确提取需要的字段,保证类型安全。 - `item.done ? 'completed' : ''` 动态设置 `className`,控制已完成项的样式(删除线)。 ### 步骤十:组件拆分 — TodoFooter(统计 / 筛选切换 / 清除已完成) `TodoFooter` 接收完整列表,计算未完成项数量,处理筛选切换和清除已完成项: ```tsx // src/components/TodoFooter.tsx import type { TodoItem, FilterType } from '../types/todo' interface TodoFooterProps { list: TodoItem[] filter: FilterType onFilterChange: (filter: FilterType) => void onClearCompleted: () => Promise } const FILTERS: { key: FilterType; label: string }[] = [ { key: 'all', label: 'All' }, { key: 'active', label: 'Active' }, { key: 'completed', label: 'Completed' }, ] export default function TodoFooter({ list, filter, onFilterChange, onClearCompleted, }: TodoFooterProps) { const leftCount = list.filter((item) => !item.done).length return ( ) } ``` 关键点: - **筛选切换**:`FILTERS` 常量数组使用 `FilterType` 约束 `key`,通过 `map` 渲染筛选按钮,点击时调用 `onFilterChange` 更新父组件的筛选状态。当前激活的筛选项通过 `filter === key` 判断并添加 `selected` class 高亮显示。 - **清除已完成**:点击 "Clear completed" 按钮时调用 `onClearCompleted`,由父组件 `App` 并发删除所有已完成项。 - `e.preventDefault()` 阻止 `` 标签默认跳转行为,保持 SPA 体验。 - 复用 `TodoItem` 类型定义 Props,与 `App` 和 `TodoMain` 共享同一类型。 --- ## TypeScript 类型复用总结 所有类型集中定义在 `src/types/todo.ts`,各组件按需导入: | 类型 | 定义方式 | 使用位置 | |------|---------|---------| | `TodoItem` | 直接定义 `interface` | `App.tsx`(状态类型)、`TodoMain.tsx`(Props + 事件参数)、`TodoFooter.tsx`(Props + 计算) | | `NewTodo` | `Omit` | `TodoHeader.tsx`(新增待办参数) | | `FilterType` | 联合类型 `'all' \| 'active' \| 'completed'` | `App.tsx`(筛选状态)、`TodoMain.tsx`(过滤列表)、`TodoFooter.tsx`(筛选切换) | | `Pick` | 内联使用 | `TodoMain.tsx`(切换完成状态参数) | ## 启动方式 ```bash # 1. 安装依赖 npm install # 2. 启动 json-server 模拟接口(终端 1) npm run server # 3. 启动 Vite 开发服务器(终端 2) npm run dev ``` 然后在浏览器中打开 Vite 输出的地址(默认 http://localhost:5173)。