
文章目录
本文将详细阐述如何使用 React 框架一步步开发一个功能完整的任务记录网站(Todo List App)。我们将从需求分析、技术选型、架构设计开始,深入到具体实现、状态管理、数据持久化,最后讨论优化与部署。文章包含清晰的实现思路、代码示例和系统流程图,旨在为你提供一个可复用的开发模板。
一、 需求分析与功能规划
在开始编码之前,我们必须明确我们要构建什么。一个基本的任务记录网站应包含以下核心功能:
- 任务增删改查 (CRUD)
- 添加任务: 输入任务描述,创建新任务。
- 查看任务: 以列表形式展示所有任务。
- 编辑任务: 修改已有任务的内容。
- 删除任务: 将任务从列表中移除。
- 任务状态管理
- 标记完成/未完成: 切换任务的完成状态。
- 筛选任务: 查看“所有”、“进行中”或“已完成”的任务。
- 数据持久化
- 将任务数据保存在浏览器的
localStorage中,防止页面刷新后数据丢失。
- 将任务数据保存在浏览器的
- 用户体验 (UX)
- 清晰的交互反馈、键盘支持(如回车添加任务)。
- 简洁美观的UI。
进阶功能(可选):
- 任务分类或标签。
- 任务优先级(高、中、低)。
- 任务截止日期。
- 动画效果(如添加、删除任务时的过渡动画)。
- 拖拽排序。
二、 技术选型与项目结构
1. 技术栈
- 前端框架: React (使用
Create React App或Vite搭建) - 语言: JavaScript (或 TypeScript,强烈推荐用于更好的开发体验)
- 状态管理: React
useState,useReducer(对于此规模项目,Redux 是过度设计) - 样式方案: CSS Modules / Styled-Components / 或简单的 CSS 文件
- 构建工具: Create React App 或 Vite 内置的构建工具
- 数据持久化: 浏览器
localStorage
2. 项目目录结构
一个清晰的结构是项目可维护性的基石。
my-todo-app/
├── public/
│ └── index.html
├── src/
│ ├── components/ # 可复用的UI组件
│ │ ├── TodoItem/ # 单个任务组件
│ │ │ ├── index.jsx
│ │ │ └── TodoItem.module.css
│ │ ├── Button/ # 按钮组件
│ │ ├── Input/ # 输入框组件
│ │ └── Filter/ # 筛选器组件
│ ├── hooks/ # 自定义Hooks
│ │ └── useLocalStorage.js # 持久化数据的Hook
│ ├── contexts/ # React Contexts (可选)
│ │ └── TodoContext.js # 任务状态的全局管理
│ ├── utils/ # 工具函数
│ │ └── helpers.js
│ ├── App.jsx # 主组件
│ ├── App.css
│ └── index.js # 应用入口
└── package.json
三、 核心实现思路与流程图
在开始编码之前,理解数据的流动至关重要。下图描绘了本应用的核心状态管理流程:
flowchart TD
A[用户操作<br>添加/删除/切换任务/编辑] --> B[UI事件触发<br>如onClick, onSubmit]
B --> C[调用Dispatch函数<br>派发对应Action<br>如<br>ADD_TODO, REMOVE_TODO]
C --> D{Reducer函数<br>基于Action类型<br>处理状态逻辑}
D -- 生成新状态 --> E[State状态树更新<br>todos: 任务数组<br>filter: 当前筛选器]
E -- 状态向下传递 --> F[UI组件重新渲染<br>App → TodoList → TodoItem]
F -- 直观显示变化 --> G[用户看到更新后的界面]
H[副作用: 数据持久化] --> I[监听State变化<br>自动同步到localStorage]
E --> H
这个单向数据流确保了应用行为的可预测性和可调试性。
四、 分步代码实现
1. 状态建模
首先,我们需要定义任务的数据结构。
// 一个任务对象的结构
{
id: 1, // 唯一标识,通常用Date.now()或库生成
text: 'Learn React', // 任务文本
completed: false // 完成状态
// createdAt: ... // 可选: 创建时间
}
2. 使用 useReducer 管理状态
对于涉及多个子状态和复杂更新的场景,useReducer 比多个 useState 更合适。
// 在 App.jsx 中
import React, { useReducer } from 'react';
// 1. 定义初始状态
const initialState = {
todos: [],
filter: 'all' // 'all', 'active', 'completed'
};
// 2. 定义Reducer函数
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
case 'EDIT_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.newText }
: todo
)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload.filter
};
default:
return state;
}
}
function App() {
// 3. 在组件中使用useReducer
const [state, dispatch] = useReducer(todoReducer, initialState);
const { todos, filter } = state;
// ... 后续会将dispatch函数传递给子组件
}
3. 实现自定义Hook进行数据持久化
我们需要一个Hook来将状态同步到 localStorage。
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 从localStorage获取初始值,如果没有则使用传入的initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// 返回一个包装过的setter,这个setter会在更新状态的同时持久化到localStorage
const setValue = (value) => {
try {
// 允许value是一个函数,就像useState一样
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;
在 App.js 中集成持久化Hook:
// 修改App.js中的useReducer一行
const [state, dispatch] = useReducer(todoReducer, initialState);
// 改为 -->
const [state, dispatch] = useReducer(todoReducer, initialState);
const [persistedState, setPersistedState] = useLocalStorage('todos-app-state', initialState);
// useEffect来同步state到persistedState
useEffect(() => {
setPersistedState(state);
}, [state, setPersistedState]);
// 注意: 更优雅的做法是直接在todoReducer初始化时从useLocalStorage读取初始值
4. 构建核心组件
Input 组件 (AddTodo.jsx)
// src/components/AddTodo/AddTodo.jsx
import React, { useState } from 'react';
import styles from './AddTodo.module.css';
const AddTodo = ({ onAddTodo }) => {
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (inputValue.trim()) {
onAddTodo(inputValue.trim());
setInputValue('');
}
};
return (
<form onSubmit={handleSubmit} className={styles.form}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="添加一个新任务..."
className={styles.input}
/>
<button type="submit" className={styles.button}>添加</button>
</form>
);
};
export default AddTodo;
TodoItem 组件
// src/components/TodoItem/TodoItem.jsx
import React, { useState } from 'react';
import styles from './TodoItem.module.css';
const TodoItem = ({ todo, onToggle, onDelete, onEdit }) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
if (editText.trim()) {
onEdit(todo.id, editText.trim());
setIsEditing(false);
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleEdit();
} else if (e.key === 'Escape') {
setEditText(todo.text);
setIsEditing(false);
}
};
return (
<li className={`${styles.todoItem} ${todo.completed ? styles.completed : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className={styles.checkbox}
/>
{isEditing ? (
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={handleEdit}
onKeyDown={handleKeyDown}
autoFocus
className={styles.editInput}
/>
) : (
<>
<span
onDoubleClick={() => setIsEditing(true)}
className={styles.text}
>
{todo.text}
</span>
<button
onClick={() => onDelete(todo.id)}
className={styles.deleteButton}
>
删除
</button>
</>
)}
</li>
);
};
export default TodoItem;
Filter 组件
// src/components/Filter/Filter.jsx
import styles from './Filter.module.css';
const FILTERS = {
ALL: 'all',
ACTIVE: 'active',
COMPLETED: 'completed'
};
const Filter = ({ currentFilter, onFilterChange }) => {
return (
<div className={styles.filter}>
{Object.entries(FILTERS).map(([key, value]) => (
<button
key={value}
onClick={() => onFilterChange(value)}
className={`${styles.filterButton} ${currentFilter === value ? styles.active : ''}`}
>
{key}
</button>
))}
</div>
);
};
export default Filter;
5. 在主组件 App.jsx 中组合一切
// src/App.jsx
import React, { useReducer } from 'react';
import useLocalStorage from './hooks/useLocalStorage';
import AddTodo from './components/AddTodo/AddTodo';
import TodoItem from './components/TodoItem/TodoItem';
import Filter from './components/Filter/Filter';
import { todoReducer, initialState } from './todoReducer';
import styles from './App.module.css';
function App() {
const [persistedState, setPersistedState] = useLocalStorage('todos-app-state', initialState);
const [state, dispatch] = useReducer(todoReducer, persistedState);
const { todos, filter } = state;
// 派生状态:根据筛选器过滤任务
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// Action Creators (可选,使代码更清晰)
const addTodo = (text) => dispatch({ type: 'ADD_TODO', payload: { text } });
const toggleTodo = (id) => dispatch({ type: 'TOGGLE_TODO', payload: { id } });
const deleteTodo = (id) => dispatch({ type: 'DELETE_TODO', payload: { id } });
const editTodo = (id, newText) => dispatch({ type: 'EDIT_TODO', payload: { id, newText } });
const setFilter = (filter) => dispatch({ type: 'SET_FILTER', payload: { filter } });
// 同步状态到localStorage
React.useEffect(() => {
setPersistedState(state);
}, [state, setPersistedState]);
return (
<div className={styles.app}>
<h1>我的任务清单</h1>
<AddTodo onAddTodo={addTodo} />
<Filter currentFilter={filter} onFilterChange={setFilter} />
<ul className={styles.todoList}>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
))}
</ul>
<div className={styles.stats}>
总计: {todos.length} | 已完成: {todos.filter(t => t.completed).length} | 未完成: {todos.filter(t => !t.completed).length}
</div>
</div>
);
}
export default App;
五、 样式与优化
- CSS Modules: 为每个组件添加样式文件(如
App.module.css),实现样式的模块化,避免冲突。 - Keyboard Accessibility: 像在
TodoItem组件中一样,为输入框添加键盘事件(Enter 保存,Escape 取消)。 - 性能优化: 使用
React.memo包装TodoItem组件,避免不必要的重渲染。对于函数Props(如onToggle),使用useCallback进行缓存。
// 优化TodoItem
const MemoizedTodoItem = React.memo(TodoItem);
// 在App.js中使用useCallback缓存函数
const toggleTodo = useCallback((id) => dispatch({ type: 'TOGGLE_TODO', payload: { id } }), []);
六、 部署上线
- 构建: 运行
npm run build生成优化的静态文件(在build或dist文件夹)。 - 部署: 可以将
build文件夹部署到任何静态网站托管服务,如:- Vercel: 与GitHub集成,自动化部署最简单。
- Netlify: 同样简单强大,支持拖拽部署。
- GitHub Pages: 免费,适合个人项目。
总结与展望
通过以上步骤,我们成功地使用 React 构建了一个功能完备、架构清晰的任务记录网站。我们利用了 React 的组件化、状态管理、Hooks 等核心概念,并实现了数据的本地持久化。
下一步可以如何增强它?
- 后端集成: 使用
axios或fetch与真正的后端API(如Node.js + Express, Firebase, Supabase)交互,实现多设备同步。 - 路由: 使用
React Router为不同的筛选状态(/active,/completed)添加路由。 - 状态管理: 如果功能变得极其复杂,可以考虑引入
Zustand或Redux Toolkit。 - UI库: 引入像
Ant Design或Material-UI这样的组件库,快速打造专业界面。 - 测试: 为Reducer和组件添加单元测试(使用Jest和React Testing Library)。
这个项目是学习 React 的绝佳实践,它涵盖了现代前端开发的核心流程和思想。祝你编码愉快!


被折叠的 条评论
为什么被折叠?



