React实战:构建全栈待办事项应用

React框架实战:构建一个全栈待办事项应用

在现代Web开发中,React凭借其组件化、声明式和高效的虚拟DOM特性,已成为构建用户界面的首选框架。本文将通过一个完整的实战项目,带你从零开始构建一个全栈待办事项应用,涵盖前端React开发、后端API设计以及数据持久化方案。

项目规划与技术栈选择

在开始编码前,明确项目需求和技术选型至关重要。这个待办事项应用需要实现以下核心功能:

  • 创建、读取、更新和删除待办事项
  • 标记待办事项为完成/未完成状态
  • 按优先级和截止日期排序
  • 用户认证与数据隔离

技术栈选择如下:

  • 前端:React 18 + TypeScript + Redux Toolkit
  • 后端:Node.js + Express + MongoDB
  • 认证:JWT (JSON Web Tokens)
  • 部署:Docker + AWS EC2

后端开发:构建RESTful API

1. 项目初始化与依赖安装

首先创建Express项目并安装必要依赖:

mkdir todo-backend && cd todo-backend
npm init -y
npm install express mongoose jsonwebtoken bcryptjs cors dotenv
npm install -D nodemon

2. 设计数据模型

使用Mongoose定义待办事项和用户的数据模型:

// models/Todo.js
const mongoose = require(\'mongoose\');

const todoSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    trim: true
  },
  description: String,
  completed: {
    type: Boolean,
    default: false
  },
  priority: {
    type: String,
    enum: [\'low\', \'medium\', \'high\'],
    default: \'medium\'
  },
  dueDate: Date,
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: \'User\',
    required: true
  }
}, {
  timestamps: true
});

module.exports = mongoose.model(\'Todo\', todoSchema);

3. 实现认证中间件

创建JWT认证中间件保护API端点:

middleware/auth.js
const jwt = require(\'jsonwebtoken\');
require(\'dotenv\').config();

const auth = (req, res, next) => {
  const token = req.header(\'Authorization\')?.replace(\'Bearer \', \'\');
  
  if (!token) {
    return res.status(401).json({ message: \'No token, authorization denied\' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded.user;
    next();
  } catch (error) {
    res.status(401).json({ message: \'Token is not valid\' });
  }
};

module.exports = auth;

4. 编写API路由

实现完整的CRUD操作路由:

// routes/todos.js
const express = require(\'express\');
const router = express.Router();
const auth = require(\'../middleware/auth\');
const Todo = require(\'../models/Todo\');

// @route   POST api/todos
// @desc    Create a todo
// @access  Private
router.post(\'/\', auth, async (req, res) => {
  const { title, description, priority, dueDate } = req.body;
  
  try {
    const newTodo = new Todo({
      title,
      description,
      priority,
      dueDate,
      user: req.user.id
    });

    const todo = await newTodo.save();
    res.json(todo);
  } catch (error) {
    res.status(500).json({ message: \'Server error\' });
  }
});

// @route   GET api/todos
// @desc    Get all todos for a user
// @access  Private
router.get(\'/\', auth, async (req, res) => {
  try {
    const todos = await Todo.find({ user: req.user.id })
      .sort({ dueDate: 1, priority: -1 });
    res.json(todos);
  } catch (error) {
    res.status(500).json({ message: \'Server error\' });
  }
});

// @route   PUT api/todos/:id
// @desc    Update a todo
// @access  Private
router.put(\'/:id\', auth, async (req, res) => {
  const { title, description, completed, priority, dueDate } = req.body;
  
  const todoFields = {};
  if (title) todoFields.title = title;
  if (description) todoFields.description = description;
  if (completed !== undefined) todoFields.completed = completed;
  if (priority) todoFields.priority = priority;
  if (dueDate) todoFields.dueDate = dueDate;

  try {
    let todo = await Todo.findById(req.params.id);
    
    if (!todo) return res.status(404).json({ message: \'Todo not found\' });
    
    if (todo.user.toString() !== req.user.id) {
      return res.status(401).json({ message: \'Not authorized\' });
    }

    todo = await Todo.findByIdAndUpdate(
      req.params.id,
      { $set: todoFields },
      { new: true }
    );

    res.json(todo);
  } catch (error) {
    res.status(500).json({ message: \'Server error\' });
  }
});

// @route   DELETE api/todos/:id
// @desc    Delete a todo
// @access  Private
router.delete(\'/:id\', auth, async (req, res) => {
  try {
    let todo = await Todo.findById(req.params.id);
    
    if (!todo) return res.status(404).json({ message: \'Todo not found\' });
    
    if (todo.user.toString() !== req.user.id) {
      return res.status(401).json({ message: \'Not authorized\' });
    }

    await Todo.findByIdAndRemove(req.params.id);
    res.json({ message: \'Todo removed\' });
  } catch (error) {
    res.status(500).json({ message: \'Server error\' });
  }
});

module.exports = router;

前端开发:构建React应用

1. 项目初始化与配置

使用Create React App初始化项目并配置Redux:

npx create-react-app todo-frontend
cd todo-frontend
npm install @reduxjs/toolkit react-redux axios
npm install -D @types/react-router-dom

2. 设计Redux状态管理

使用Redux Toolkit创建状态切片:

// features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from \'@reduxjs/toolkit\';
import axios from \'axios\';

const API_URL = \'/api/todos\';

// Get todos
export const getTodos = createAsyncThunk(\'todos/getTodos\', async (token) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  };
  
  const response = await axios.get(API_URL, config);
  return response.data;
});

// Add todo
export const addTodo = createAsyncThunk(\'todos/addTodo\', async ({ todo, token }) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  };
  
  const response = await axios.post(API_URL, todo, config);
  return response.data;
});

// Update todo
export const updateTodo = createAsyncThunk(\'todos/updateTodo\', async ({ id, todo, token }) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  };
  
  const response = await axios.put(`${API_URL}/${id}`, todo, config);
  return response.data;
});

// Delete todo
export const deleteTodo = createAsyncThunk(\'todos/deleteTodo\', async ({ id, token }) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  };
  
  await axios.delete(`${API_URL}/${id}`, config);
  return id;
});

const initialState = {
  todos: [],
  isLoading: false,
  error: null
};

const todoSlice = createSlice({
  name: \'todos\',
  initialState,
  reducers: {
    reset: (state) => {
      state.todos = [];
      state.isLoading = false;
      state.error = null;
    }
  },
  extraReducers: (builder) => {
    builder
      // Get todos
      .addCase(getTodos.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(getTodos.fulfilled, (state, action) => {
        state.isLoading = false;
        state.todos = action.payload;
      })
      .addCase(getTodos.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.error.message;
      })
      // Add todo
      .addCase(addTodo.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(addTodo.fulfilled, (state, action) => {
        state.isLoading = false;
        state.todos.push(action.payload);
      })
      .addCase(addTodo.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.error.message;
      })
      // Update todo
      .addCase(updateTodo.fulfilled, (state, action) => {
        const index = state.todos.findIndex(todo => todo._id === action.payload._id);
        if (index !== -1) {
          state.todos[index] = action.payload;
        }
      })
      // Delete todo
      .addCase(deleteTodo.fulfilled, (state, action) => {
        state.todos = state.todos.filter(todo => todo._id !== action.payload);
      });
  }
});

export const { reset } = todoSlice.actions;
export default todoSlice.reducer;

3. 创建React组件

构建核心UI组件:

// components/TodoList.js
import React from \'react\';
import { useSelector, useDispatch } from \'react-redux\';
import { deleteTodo, updateTodo } from \'../features/todos/todosSlice\';

const TodoList = () => {
  const { todos } = useSelector((state) => state.todos);
  const dispatch = useDispatch();

  const handleDelete = (id) => {
    dispatch(deleteTodo({ id, token: localStorage.getItem(\'token\') }));
  };

  const handleToggle = (todo) => {
    dispatch(updateTodo({
      id: todo._id,
      todo: { completed: !todo.completed },
      token: localStorage.getItem(\'token\')
    }));
  };

  return (
    
{todos.map((todo) => (

{todo.title}

{todo.description}

Priority: {todo.priority} Due: {new Date(todo.dueDate).toLocaleDateString()}
handleToggle(todo)} />
))}
); }; export default TodoList;

4. 实现路由与布局

配置应用路由和主布局组件:

// App.js
import React from \'react\';
import { BrowserRouter as Router, Routes, Route } from \'react-router-dom\';
import { Provider } from \'react-redux\';
import store from \'./app/store\';
import Login from \'./components/Login\';
import Register from \'./components/Register\';
import Dashboard from \'./components/Dashboard\';
import PrivateRoute from \'./components/PrivateRoute\';

function App() {
  return (
    
      
        
          <Route path=\"/login\" element={} />
          <Route path=\"/register\" element={} />
          <Route
            path=\"/\"
            element={
              
                
              
            }
          />
        
      
    
  );
}

export default App;

部署与优化

1. Docker化应用

创建Dockerfile实现容器化部署:

# Dockerfile for backend
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD [\"npm\", \"start\"]

# Dockerfile for frontend
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD [\"npm\", \"start\"]

2. 性能优化策略

提升应用性能的关键措施:

  • 使用React.memo和useMemo优化组件渲染
  • 实现虚拟滚动处理大量待办事项
  • 启用Redux的持久化存储保存用户状态
  • 添加错误边界组件捕获渲染错误
  • 实现离线功能使用Service Worker

总结

通过这个全栈待办事项应用的开发实践,我们深入掌握了React生态系统的高级用法,包括Redux状态管理、JWT认证、RESTful API设计以及现代部署流程。这个项目展示了如何将前端框架与后端服务完美结合,构建出功能完善、性能优秀的Web应用。

在实际开发中,还可以进一步扩展功能,如添加文件上传、实时通知、数据可视化等特性。通过不断实践和优化,React开发者能够构建出更加复杂和强大的应用,满足现代Web应用的各种需求。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...