从零到一:使用 React 构建一个功能完整的任务记录网站

在这里插入图片描述

本文将详细阐述如何使用 React 框架一步步开发一个功能完整的任务记录网站(Todo List App)。我们将从需求分析、技术选型、架构设计开始,深入到具体实现、状态管理、数据持久化,最后讨论优化与部署。文章包含清晰的实现思路、代码示例和系统流程图,旨在为你提供一个可复用的开发模板。


一、 需求分析与功能规划

在开始编码之前,我们必须明确我们要构建什么。一个基本的任务记录网站应包含以下核心功能:

  1. 任务增删改查 (CRUD)
    • 添加任务: 输入任务描述,创建新任务。
    • 查看任务: 以列表形式展示所有任务。
    • 编辑任务: 修改已有任务的内容。
    • 删除任务: 将任务从列表中移除。
  2. 任务状态管理
    • 标记完成/未完成: 切换任务的完成状态。
    • 筛选任务: 查看“所有”、“进行中”或“已完成”的任务。
  3. 数据持久化
    • 将任务数据保存在浏览器的 localStorage 中,防止页面刷新后数据丢失。
  4. 用户体验 (UX)
    • 清晰的交互反馈、键盘支持(如回车添加任务)。
    • 简洁美观的UI。

进阶功能(可选)

  • 任务分类或标签。
  • 任务优先级(高、中、低)。
  • 任务截止日期。
  • 动画效果(如添加、删除任务时的过渡动画)。
  • 拖拽排序。

二、 技术选型与项目结构

1. 技术栈

  • 前端框架: React (使用 Create React AppVite 搭建)
  • 语言: 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;

五、 样式与优化

  1. CSS Modules: 为每个组件添加样式文件(如 App.module.css),实现样式的模块化,避免冲突。
  2. Keyboard Accessibility: 像在 TodoItem 组件中一样,为输入框添加键盘事件(Enter 保存,Escape 取消)。
  3. 性能优化: 使用 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 } }), []);

六、 部署上线

  1. 构建: 运行 npm run build 生成优化的静态文件(在 builddist 文件夹)。
  2. 部署: 可以将 build 文件夹部署到任何静态网站托管服务,如:
    • Vercel: 与GitHub集成,自动化部署最简单。
    • Netlify: 同样简单强大,支持拖拽部署。
    • GitHub Pages: 免费,适合个人项目。

总结与展望

通过以上步骤,我们成功地使用 React 构建了一个功能完备、架构清晰的任务记录网站。我们利用了 React 的组件化、状态管理、Hooks 等核心概念,并实现了数据的本地持久化。

下一步可以如何增强它?

  • 后端集成: 使用 axiosfetch 与真正的后端API(如Node.js + Express, Firebase, Supabase)交互,实现多设备同步。
  • 路由: 使用 React Router 为不同的筛选状态(/active, /completed)添加路由。
  • 状态管理: 如果功能变得极其复杂,可以考虑引入 ZustandRedux Toolkit
  • UI库: 引入像 Ant DesignMaterial-UI 这样的组件库,快速打造专业界面。
  • 测试: 为Reducer和组件添加单元测试(使用Jest和React Testing Library)。

这个项目是学习 React 的绝佳实践,它涵盖了现代前端开发的核心流程和思想。祝你编码愉快!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北辰alk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值