MongoDB + Express + React + Node.js全栈项目
本文最后更新于221 天前,其中的信息可能已经过时,如有错误请发送邮件到1986413837@qq.com

MERN (MongoDB, Express, React, Node.js) 技术栈是一个非常棒的选择

Todo List 是学习这个技术栈的经典入门项目

分步骤完成这个项目

项目结构

创建一个总的项目文件夹,然后在里面分别创建 server(后端)和 client(前端)两个文件夹

数据库设置(MongoDB Atlas)

MongoDB Atlas提供了一个免费云数据库服务

具体步骤如下

具体界面大概长这样

后端(Express)

进入server目录 开始搭建后端

A.后端项目初始化

1.初始化Node.js项目

npm init -y //创建一个package.json文件

2.安装依赖

express:我们的 Node.js web 框架

mongoose:一个 ODM (Object Data Modeling) 库 它是我们与 MongoDB 数据库沟通的桥梁 它能让我们用 JavaScript 对象来定义数据结构(Schema)

cors:一个中间件,用于解决跨域资源共享 (CORS) 问题。因为我们的 React 前端 (运行在 localhost:3000) 需要调用后端 (运行在 localhost:5000),它们在不同的”域”(端口不同),浏览器默认会阻止这种请求

dotenv:用于管理环境变量 我们会把敏感的数据库连接字符串放在一个 .env 文件中,而不是硬编码在代码里

nodemon (开发依赖):一个工具,它会监视你的文件改动,并自动重启服务器,极大提升开发效率

# 安装生产依赖
npm install express mongoose cors dotenv

# 安装开发依赖
npm install -D nodemon

3.配置package.json

打开 package.json 文件,在 scripts 部分添加 “start” 和 “dev” 命令:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

npm start:在生产环境运行服务器

npm run dev:在开发环境运行服务器(使用 nodemon)

4.创建.env文件

server 根目录创建 .env 文件 这是存放秘密信息的地方

# .env 文件

# 把你在 MongoDB Atlas 复制的连接字符串粘贴到这里
# 把 <username> 替换为你的数据库用户名 (例如 todoUser)
# 把 <password> 替换为你的数据库密码 (例如 yourStrongPassword123)
# 你可能还需要在 .net/ 后面指定一个数据库名,例如 .net/todolist?
MONGO_URI=mongodb+srv://todoUser:yourStrongPassword123@cluster0.xxxxx.mongodb.net/todolist?retryWrites=true&w=majority

# 我们让服务器运行在 5000 端口
PORT=5000

5.创建.gitignore文件

server 根目录创建 .gitignore 文件,防止把 node_modules.env 文件提交到 Git

# .gitignore 文件
node_modules
.env

B.后端文件结构

为了保持代码整洁,我们采用 MVC (Model-View-Controller) 的变体结构:

server/
├── config/
│ └── db.js # 数据库连接逻辑
├── models/
│ └── Todo.js # 数据库模型 (Schema)
├── controllers/
│ └── todoController.js # API 的业务逻辑
├── routes/
│ └── todos.js # API 的路由定义
├── node_modules/
├── .env # 环境变量
├── .gitignore
├── package.json
└── server.js # 服务器入口文件

C.详细的后端代码

1.数据库连接(config/db.js)

这是连接到你 MongoDB Atlas 数据库的核心文件

// config/db.js
const mongoose = require('mongoose');
const dotenv = require('dotenv');

// 加载 .env 文件中的环境变量
dotenv.config();

const connectDB = async () => {
  try {
    // mongoose.connect 返回一个 promise
    await mongoose.connect(process.env.MONGO_URI, {
      // 这些是 Mongoose 6.x 之后的推荐选项,可以不写,Mongoose 默认会处理
      // useNewUrlParser: true, 
      // useUnifiedTopology: true,
    });
    console.log('MongoDB 连接成功...');
  } catch (err) {
    console.error('MongoDB 连接失败:', err.message);
    // 如果连接失败,退出 Node.js 进程
    process.exit(1);
  }
};

module.exports = connectDB;

2.数据模型(models/Todo.js)

Mongoose Schema 用于定义 “Todo” 事项在数据库中长什么样

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

// 定义 Schema
const TodoSchema = new mongoose.Schema({
  task: {
    type: String,
    required: [true, '任务内容不能为空'], // 必填项
  },
  completed: {
    type: Boolean,
    default: false, // 默认值为 false (未完成)
  },
  createdAt: {
    type: Date,
    default: Date.now, // 默认值为当前时间
  },
});

// 导出模型
// mongoose.model('模型名', Schema)
// 'Todo' 是模型的名字,MongoDB 会自动将其转为复数形式 'todos' 作为集合(collection)的名字
module.exports = mongoose.model('Todo', TodoSchema);

3.路由(routes/todos.js)

路由文件只负责定义 API 的 URL 路径 (endpoints) 和它们使用的 HTTP 方法 (GET, POST, PUT, DELETE),并指定由哪个控制器函数来处理

// routes/todos.js
const express = require('express');
const router = express.Router();

// 引入我们的控制器
const {
  getTodos,
  createTodo,
  updateTodo,
  deleteTodo,
} = require('../controllers/todoController');

// 定义路由
// GET /api/todos - 获取所有 todo
router.get('/', getTodos);

// POST /api/todos - 创建一个新的 todo
router.post('/', createTodo);

// PUT /api/todos/:id - 更新一个 todo (例如标记为完成)
// :id 是一个动态参数,表示 todo 的唯一 ID
router.put('/:id', updateTodo);

// DELETE /api/todos/:id - 删除一个 todo
router.delete('/:id', deleteTodo);

module.exports = router;

4.控制器(controllers/todoController.js)

这是后端的核心业务逻辑

控制器函数负责处理请求、与数据库交互(通过 Model)、并返回响应

我们使用 async/await 来处理 Mongoose 的异步操作

// controllers/todoController.js
const Todo = require('../models/Todo'); // 引入 Todo 模型

// @desc    获取所有 Todos
// @route   GET /api/todos
exports.getTodos = async (req, res) => {
  try {
    // Todo.find() 会查找这个 collection 中的所有文档
    // .sort({ createdAt: -1 }) 按创建时间倒序排列
    const todos = await Todo.find().sort({ createdAt: -1 });

    // res.json() 发送 JSON 响应
    res.status(200).json(todos);
  } catch (err) {
    res.status(500).json({ message: '服务器错误', error: err.message });
  }
};

// @desc    创建一个新的 Todo
// @route   POST /api/todos
exports.createTodo = async (req, res) => {
  try {
    // req.body 包含了从前端发送过来的数据 (例如: { "task": "学习 Node.js" })
    const { task } = req.body;

    if (!task) {
      return res.status(400).json({ message: '任务内容不能为空' });
    }

    // 使用 Todo 模型创建一个新的文档实例
    const newTodo = new Todo({
      task: task,
      // 'completed' 和 'createdAt' 会使用 Schema 中的默认值
    });

    // .save() 将新文档保存到数据库
    const savedTodo = await newTodo.save();

    res.status(201).json(savedTodo); // 201 表示 "Created"
  } catch (err) {
    res.status(500).json({ message: '服务器错误', error: err.message });
  }
};

// @desc    更新一个 Todo (切换 completed 状态)
// @route   PUT /api/todos/:id
exports.updateTodo = async (req, res) => {
  try {
    // req.params.id 从 URL 中获取 ID
    const todo = await Todo.findById(req.params.id);

    if (!todo) {
      return res.status(404).json({ message: '未找到该 Todo' });
    }

    // 切换 completed 状态
    // 我们也可以通过 req.body 来获取更新的内容,但这里只做切换
    todo.completed = !todo.completed;

    const updatedTodo = await todo.save();
    // 或者使用 findByIdAndUpdate
    // const updatedTodo = await Todo.findByIdAndUpdate(
    //   req.params.id,
    //   { completed: !todo.completed },
    //   { new: true } // {new: true} 会返回更新后的文档
    // );

    res.status(200).json(updatedTodo);
  } catch (err) {
    res.status(500).json({ message: '服务器错误', error: err.message });
  }
};

// @desc    删除一个 Todo
// @route   DELETE /api/todos/:id
exports.deleteTodo = async (req, res) => {
  try {
    const todo = await Todo.findById(req.params.id);

    if (!todo) {
      return res.status(404).json({ message: '未找到该 Todo' });
    }

    // await todo.remove(); // Mongoose 6.x 之后不推荐
    await Todo.findByIdAndDelete(req.params.id);

    res.status(200).json({ message: 'Todo 删除成功' });
  } catch (err) {
    res.status(500).json({ message: '服务器错误', error: err.message });
  }
};

5.服务器入口(server.js)

这是我们服务器的”启动文件” 负责:

  1. 加载环境变量
  2. 连接数据库
  3. 初始化 Express
  4. 加载中间件 (cors, express.json)
  5. 挂载路由
  6. 启动服务器监听
// server.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const connectDB = require('./config/db'); // 引入数据库连接函数

// 加载环境变量 (必须在最前面)
dotenv.config();

// 引入路由文件
const todoRoutes = require('./routes/todos');

// 连接数据库
connectDB();

// 初始化 Express app
const app = express();

// 中间件
// 1. 启用 CORS
app.use(cors());
// 2. 启用 Express 内置的 JSON 解析器
// (这样 req.body 才能解析 application/json 格式的请求体)
app.use(express.json());

// 挂载路由
// 当访问 /api/todos 时,使用 todoRoutes 来处理
app.use('/api/todos', todoRoutes);

// 定义根路由 (可选, 用于测试服务器是否运行)
app.get('/', (req, res) => {
  res.send('Todo List API 正在运行...');
});

// 从 .env 文件获取端口号,如果没有定义,则使用 5000
const PORT = process.env.PORT || 5000;

// 启动服务器
app.listen(PORT, () => {
  console.log(`服务器正在 http://localhost:${PORT} 上运行`);
});

D.运行和测试后端

npm run dev (server目录下)

在终端显示:

MongoDB 连接成功…
服务器正在 http://localhost:5000 上运行

前端(React)

切换到client目录

A.React 项目初始化

1.使用 Create React App (CRA) 创建项目

npx create-react-app . 
# 注意后面的 "." 表示在当前 (client) 目录创建

2.安装 axios

我们将使用 axios 来发起 HTTP 请求(调用后端 API),它比原生的 fetch 更好用

npm install axios

3.配置代理(Proxy)

为了解决开发环境下的 CORS 跨域问题(React:3000 -> Express:5000),我们可以在 client/package.json 中添加一个 proxy 字段

打开 client/package.json,在末尾(与 dependencies 同级)添加

"proxy": "http://localhost:5000"

这样设置后,React 在开发中发起 /api/todos 请求时,会自动将其代理到 http://localhost:5000/api/todos,避免了跨域

4.清理src目录

删除 src 目录下的 App.test.js, logo.svg, setupTests.js 等文件,保持 App.js, index.js, App.css, index.css 即可

B.React完整代码

src/App.js

// src/App.js
import React, { useState, useEffect }
  from 'react';
import axios from 'axios';
import './App.css'; // 引入 CSS

function App() {
  const [todos, setTodos] = useState([]);
  const [newTask, setNewTask] = useState('');

  // 1. 获取所有 Todos (GET)
  const fetchTodos = async () => {
    try {
      // 注意:因为设置了 proxy, 我们只需要写 /api/todos
      const res = await axios.get('/api/todos');
      setTodos(res.data);
    } catch (err) {
      console.error('获取 Todos 失败:', err);
    }
  };

  // 首次加载时获取数据
  useEffect(() => {
    fetchTodos();
  }, []); // 空依赖数组表示只在组件挂载时运行一次

  // 2. 添加新 Todo (POST)
  const handleAddTodo = async (e) => {
    e.preventDefault(); // 阻止表单默认提交行为
    if (!newTask.trim()) return; // 忽略空任务

    try {
      const res = await axios.post('/api/todos', { task: newTask });
      // 将新返回的 todo 添加到列表的最前面
      setTodos([res.data, ...todos]);
      setNewTask(''); // 清空输入框
    } catch (err) {
      console.error('添加 Todo 失败:', err);
    }
  };

  // 3. 切换 Todo 完成状态 (PUT)
  const handleToggleComplete = async (id, currentCompleted) => {
    try {
      const res = await axios.put(`/api/todos/${id}`, {
        // 注意:我们的后端逻辑是自动切换状态,所以 body 不是必须的
        // 但如果后端需要,你可以发送 { completed: !currentCompleted }
      });

      // 更新前端 state
      setTodos(
        todos.map((todo) =>
          todo._id === id ? { ...todo, completed: res.data.completed } : todo
        )
      );
    } catch (err) {
      console.error('更新 Todo 失败:', err);
    }
  };

  // 4. 删除 Todo (DELETE)
  const handleDeleteTodo = async (id) => {
    // *** 新增的删除确认逻辑 ***
    if (!window.confirm('您确定要删除这个任务吗?')) {
      return; // 如果用户点击“取消”,则停止执行后续操作
    }
    // *** 结束新增逻辑 ***

    try {
      await axios.delete(`/api/todos/${id}`);
      // 从前端 state 中移除
      setTodos(todos.filter((todo) => todo._id !== id));
    } catch (err) {
      console.error('删除 Todo 失败:', err);
    }
  };

  return (
    <div className="App">
      <header>
        <h1>Todo List</h1>
      </header>

      {/* 添加 Todo 的表单 */}
      <form onSubmit={handleAddTodo} className="add-todo-form">
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="添加新任务..."
        />
        <button type="submit">添加</button>
      </form>

      {/* Todo 列表 */}
      <ul className="todo-list">
        {todos.map((todo) => (
          <li
            key={todo._id}
            className={`todo-item ${todo.completed ? 'completed' : ''}`}
          >
            <span
              onClick={() => handleToggleComplete(todo._id, todo.completed)}
              className="task-text"
            >
              {todo.task}
            </span>
            <button
              onClick={() => handleDeleteTodo(todo._id)}
              className="delete-btn"
            >
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

src/App.css

/* src/App.css */

/* 通用样式和布局 */
body {
  margin: 0;
  padding: 0;
  font-family: 'Arial', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* 活泼的浅蓝色渐变背景 */
  background: linear-gradient(135deg, #f0f8ff 0%, #e6e6fa 100%); 
  display: flex;
  justify-content: center;
  align-items: flex-start; /* 从顶部开始布局 */
  padding: 50px 20px;
  min-height: 100vh;
}

.App {
  width: 100%;
  max-width: 550px; /* 稍微放大一点 */
  background-color: #ffffff; 
  border-radius: 20px; /* 更圆润的边角 */
  /* 更明显的阴影,带一点色彩 */
  box-shadow: 0 10px 30px rgba(108, 92, 231, 0.2); 
  padding: 30px;
  min-height: 500px;
}

header {
  text-align: center;
  margin-bottom: 30px;
}

h1 {
  /* 使用紫色作为标题强调色 */
  color: #6c5ce7; 
  font-size: 2.5em;
  font-weight: 700;
  /* 标题下方增加一个圆润的强调 */
  display: inline-block;
  padding-bottom: 10px;
  position: relative;
}

h1::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 60%;
  height: 4px;
  background-color: #a29bfe;
  border-radius: 2px;
}

/* 添加 Todo 表单样式 */
.add-todo-form {
  display: flex;
  margin-bottom: 35px;
  gap: 12px; 
}

.add-todo-form input {
  flex-grow: 1;
  padding: 14px 18px;
  border: 2px solid #ddd;
  border-radius: 10px; /* 保持圆润 */
  font-size: 1.05em;
  transition: all 0.3s ease;
}

.add-todo-form input:focus {
  outline: none;
  /* 聚焦时使用鲜明的青色 */
  border-color: #00cec9; 
  box-shadow: 0 0 10px rgba(0, 206, 201, 0.3);
}

.add-todo-form button {
  padding: 14px 25px;
  /* 按钮使用紫色 */
  background-color: #6c5ce7; 
  color: white;
  border: none;
  border-radius: 10px;
  font-size: 1.05em;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.3s, transform 0.1s;
}

.add-todo-form button:hover {
  background-color: #584bda;
}

/* Todo 列表样式 */
.todo-list {
  list-style: none;
  padding: 0;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 18px;
  margin-bottom: 15px;
  background-color: #f8f9fa; 
  border-radius: 10px;
  transition: all 0.3s ease;
  /* 列表项的柔和阴影 */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 
  border-left: 6px solid #a29bfe; /* 左侧紫色装饰条 */
}

.todo-item:hover {
  background-color: #ffffff;
  /* 鼠标悬停时边框颜色更深 */
  border-left-color: #6c5ce7; 
}

.task-text {
  flex-grow: 1;
  margin-right: 15px;
  font-size: 1.1em;
  cursor: pointer;
  color: #2d3436; /* 较深的文字颜色 */
}

/* 完成状态 */
.todo-item.completed {
  opacity: 0.9;
  /* 完成项使用鲜明的青色装饰条 */
  border-left-color: #00b894; 
  background-color: #e8f7f5; /* 略带青色的背景 */
}

.todo-item.completed .task-text {
  text-decoration: line-through; 
  color: #95a5a6; /* 灰色文字 */
}

/* 删除按钮样式 */
.delete-btn {
  /* 醒目的红色 */
  background-color: #ff6b6b; 
  color: white;
  border: none;
  padding: 10px 18px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9em;
  font-weight: 600;
  transition: background-color 0.3s, transform 0.1s;
}

.delete-btn:hover {
  background-color: #ee5253;
}

C.运行项目

现在你可以同时运行后端和前端了

运行后端: 打开第一个终端窗口,进入 server 目录 npm run dev

运行前端: 打开第二个终端窗口,进入 client 目录 npm start

现在,就可以在浏览器中添加、查看、切换完成状态和删除 Todo 事项了!所有数据都会持久化存储在你的 MongoDB Atlas 云数据库中

总结

最后的文件结构like this

后端控制台

前端控制台

最终效果

Network

Life's a struggle, I'll conquer it.
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇