全栈开发,顾名思义,是指同时掌握前端(用户界面)与后端(服务器、数据库、业务逻辑)的开发能力。在技术快速迭代的今天,全栈开发者因其能够独立完成产品从构思到上线的全过程,成为技术团队中极具价值的角色。
本文将通过一个待办事项管理应用的完整开发过程,带你走一遍全栈开发的真实工作流。项目虽然小巧,但涵盖了前后端分离架构、RESTful API设计、数据库交互、前端交互与状态管理等企业级应用的核心要素。
你将学到:如何使用React构建交互式界面,如何用Node.js+Express搭建API服务,如何用MongoDB存储数据,以及如何让这三者协同工作。
在动手之前,先明确技术栈:
层级 | 技术选型 | 理由 |
|---|---|---|
前端框架 | React 18 + Vite | 组件化开发,Vite构建速度快 |
后端框架 | Node.js + Express | JavaScript全栈统一,生态丰富 |
数据库 | MongoDB + Mongoose ODM | 文档型数据库,与JavaScript对象天然契合 |
接口风格 | RESTful API | 简单直观,易于理解和调试 |
开发环境 | 前后端分离,通过代理解决跨域 | 符合现代开发模式 |
架构概览:客户端(React)通过HTTP请求访问服务端(Express API),服务端与MongoDB交互完成数据读写。前后端完全解耦,各自独立部署。
┌─────────────┐ HTTP请求 ┌─────────────┐ 读写 ┌─────────────┐
│ 前端 │ ◄────────────► │ 后端 │ ◄─────────► │ 数据库 │
│ React + Vite│ │Express + Node│ │ MongoDB │
└─────────────┘ └─────────────┘ └─────────────┘# 创建项目目录
mkdir todo-fullstack
cd todo-fullstack
mkdir backend frontend
# 初始化后端
cd backend
npm init -y
npm install express mongoose cors dotenv
npm install -D nodemon创建backend/.env文件,存放敏感配置:
MONGODB_URI=mongodb://localhost:27017/todo_app
PORT=5000创建backend/src/db.js,封装数据库连接逻辑:
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log('✅ MongoDB连接成功');
} catch (error) {
console.error('❌ MongoDB连接失败:', error.message);
process.exit(1);
}
};
export default connectDB;Mongoose的Schema定义数据结构和验证规则。创建backend/src/models/Todo.js:
import mongoose from 'mongoose';
const todoSchema = new mongoose.Schema(
{
title: {
type: String,
required: [true, '标题不能为空'],
trim: true,
maxlength: [100, '标题不能超过100个字符']
},
description: {
type: String,
trim: true,
maxlength: [500, '描述不能超过500个字符']
},
completed: {
type: Boolean,
default: false
},
priority: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium'
},
dueDate: {
type: Date,
default: null
}
},
{
timestamps: true // 自动添加 createdAt 和 updatedAt
}
);
export default mongoose.model('Todo', todoSchema);创建backend/src/routes/todos.js,实现完整的CRUD接口:
import express from 'express';
import Todo from '../models/Todo.js';
const router = express.Router();
// GET /api/todos - 获取所有待办(支持分页和筛选)
router.get('/', async (req, res) => {
try {
const { completed, priority, page = 1, limit = 10 } = req.query;
const filter = {};
if (completed !== undefined) filter.completed = completed === 'true';
if (priority) filter.priority = priority;
const skip = (parseInt(page) - 1) * parseInt(limit);
const todos = await Todo.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit));
const total = await Todo.countDocuments(filter);
res.json({
success: true,
data: todos,
pagination: { page: parseInt(page), limit: parseInt(limit), total }
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// POST /api/todos - 创建新待办
router.post('/', async (req, res) => {
try {
const todo = new Todo(req.body);
await todo.save();
res.status(201).json({ success: true, data: todo });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// GET /api/todos/:id - 获取单个待办
router.get('/:id', async (req, res) => {
try {
const todo = await Todo.findById(req.params.id);
if (!todo) {
return res.status(404).json({ success: false, message: '待办不存在' });
}
res.json({ success: true, data: todo });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// PUT /api/todos/:id - 更新待办
router.put('/:id', async (req, res) => {
try {
const todo = await Todo.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!todo) {
return res.status(404).json({ success: false, message: '待办不存在' });
}
res.json({ success: true, data: todo });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// DELETE /api/todos/:id - 删除待办
router.delete('/:id', async (req, res) => {
try {
const todo = await Todo.findByIdAndDelete(req.params.id);
if (!todo) {
return res.status(404).json({ success: false, message: '待办不存在' });
}
res.json({ success: true, message: '删除成功' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
export default router;创建backend/src/index.js,启动服务器:
import express from 'express';
import cors from 'cors';
import connectDB from './db.js';
import todoRoutes from './routes/todos.js';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 连接数据库
await connectDB();
// 路由
app.use('/api/todos', todoRoutes);
// 健康检查
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 全局错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ success: false, message: '服务器内部错误' });
});
app.listen(PORT, () => {
console.log(`🚀 服务运行在 http://localhost:${PORT}`);
});在package.json中添加启动脚本:
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
}运行npm run dev,后端服务就启动了。
cd ../frontend
npm create vite@latest . -- --template react
npm install
npm install axios react-icons在frontend/vite.config.js中配置代理:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})创建frontend/src/api/todoApi.js:
import axios from 'axios';
const API_BASE = '/api/todos';
export const todoApi = {
// 获取所有待办
getAll: async (params = {}) => {
const response = await axios.get(API_BASE, { params });
return response.data;
},
// 创建待办
create: async (data) => {
const response = await axios.post(API_BASE, data);
return response.data;
},
// 更新待办
update: async (id, data) => {
const response = await axios.put(`${API_BASE}/${id}`, data);
return response.data;
},
// 删除待办
delete: async (id) => {
const response = await axios.delete(`${API_BASE}/${id}`);
return response.data;
}
};创建frontend/src/App.jsx,实现完整的待办管理界面:
import React, { useState, useEffect } from 'react';
import { todoApi } from './api/todoApi';
import { FaTrash, FaCheck, FaUndo, FaPlus } from 'react-icons/fa';
import './App.css';
function App() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [newTodo, setNewTodo] = useState({ title: '', description: '', priority: 'medium' });
const [filter, setFilter] = useState({ completed: undefined });
// 加载待办列表
const loadTodos = async () => {
setLoading(true);
try {
const result = await todoApi.getAll(filter);
setTodos(result.data);
} catch (error) {
console.error('加载失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadTodos();
}, [filter]);
// 创建新待办
const handleCreate = async (e) => {
e.preventDefault();
if (!newTodo.title.trim()) return;
try {
await todoApi.create(newTodo);
setNewTodo({ title: '', description: '', priority: 'medium' });
loadTodos();
} catch (error) {
alert('创建失败: ' + error.message);
}
};
// 切换完成状态
const handleToggle = async (id, completed) => {
try {
await todoApi.update(id, { completed: !completed });
loadTodos();
} catch (error) {
alert('更新失败');
}
};
// 删除待办
const handleDelete = async (id) => {
if (!confirm('确定要删除吗?')) return;
try {
await todoApi.delete(id);
loadTodos();
} catch (error) {
alert('删除失败');
}
};
// 获取优先级标签颜色
const getPriorityColor = (priority) => {
const colors = { low: '🟢', medium: '🟡', high: '🔴' };
return colors[priority] || '⚪';
};
return (
<div className="app-container">
<h1>📋 待办事项</h1>
{/* 筛选器 */}
<div className="filter-bar">
<button onClick={() => setFilter({ completed: undefined })}>全部</button>
<button onClick={() => setFilter({ completed: false })}>进行中</button>
<button onClick={() => setFilter({ completed: true })}>已完成</button>
</div>
{/* 创建表单 */}
<form className="create-form" onSubmit={handleCreate}>
<input
type="text"
placeholder="标题*"
value={newTodo.title}
onChange={(e) => setNewTodo({ ...newTodo, title: e.target.value })}
required
/>
<input
type="text"
placeholder="描述"
value={newTodo.description}
onChange={(e) => setNewTodo({ ...newTodo, description: e.target.value })}
/>
<select
value={newTodo.priority}
onChange={(e) => setNewTodo({ ...newTodo, priority: e.target.value })}
>
<option value="low">低优先级</option>
<option value="medium">中优先级</option>
<option value="high">高优先级</option>
</select>
<button type="submit"><FaPlus /> 添加</button>
</form>
{/* 待办列表 */}
{loading ? (
<p>加载中...</p>
) : (
<ul className="todo-list">
{todos.length === 0 ? (
<li className="empty">🎉 暂无待办,添加一条吧!</li>
) : (
todos.map((todo) => (
<li key={todo._id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<span className="priority-badge">{getPriorityColor(todo.priority)}</span>
<div className="todo-content">
<strong>{todo.title}</strong>
{todo.description && <p>{todo.description}</p>}
<small>{new Date(todo.createdAt).toLocaleDateString()}</small>
</div>
<div className="todo-actions">
<button onClick={() => handleToggle(todo._id, todo.completed)}>
{todo.completed ? <FaUndo /> : <FaCheck />}
</button>
<button className="delete" onClick={() => handleDelete(todo._id)}>
<FaTrash />
</button>
</div>
</li>
))
)}
</ul>
)}
</div>
);
}
export default App;创建frontend/src/App.css:
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f0f2f5;
padding: 20px;
}
.app-container {
max-width: 700px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
h1 { margin-bottom: 20px; color: #1a1a2e; }
.filter-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filter-bar button {
padding: 6px 16px;
border: 1px solid #ddd;
border-radius: 20px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.filter-bar button:hover { background: #e6f0ff; }
.create-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 25px;
}
.create-form input,
.create-form select {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
flex: 1;
min-width: 120px;
}
.create-form button {
padding: 10px 24px;
background: #4a6cf7;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.create-form button:hover { background: #3a5cd5; }
.todo-list {
list-style: none;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
}
.todo-item:hover { background: #f8f9fa; }
.todo-item.completed .todo-content strong {
text-decoration: line-through;
color: #999;
}
.priority-badge { font-size: 18px; }
.todo-content {
flex: 1;
}
.todo-content strong { display: block; }
.todo-content p { color: #666; font-size: 14px; margin: 2px 0; }
.todo-content small { color: #aaa; font-size: 12px; }
.todo-actions {
display: flex;
gap: 8px;
}
.todo-actions button {
padding: 6px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #e8e8e8;
transition: all 0.2s;
}
.todo-actions button:hover { background: #d0d0d0; }
.todo-actions button.delete:hover { background: #ff4444; color: white; }
.empty {
text-align: center;
color: #999;
padding: 40px 0;
font-size: 18px;
}http://localhost:5173,一个完整的待办管理应用就呈现在眼前了。小项目可以采用上述扁平结构;项目变大后,建议按功能模块拆分:
backend/src/
├── models/ # 数据模型
├── routes/ # 路由定义
├── controllers/ # 业务控制器
├── middleware/ # 中间件(鉴权、日志等)
├── services/ # 业务逻辑层
├── utils/ # 工具函数
└── config/ # 配置文件问题 | 解决方案 |
|---|---|
跨域CORS | 开发阶段用Vite代理;生产环境部署同源或配置CORS中间件 |
环境变量管理 | 使用dotenv,.gitignore忽略.env文件 |
接口变更影响前端 | 使用Swagger/OpenAPI生成接口文档,前后端契约测试 |
数据库连接断开 | 实现连接重试机制和健康检查端点 |
本教程的待办应用是MVP(最小可行产品)。要将其产品化,还需要补充:
全栈开发的价值在于端到端的掌控力。当你既能构建美观的交互界面,又能设计稳健的后端服务时,你就拥有了将一个想法变成可运行产品的完整能力。本文的待办应用只是一个起点,但其中涉及的模式——前后端分离、RESTful API、数据库建模、状态管理——是几乎所有Web应用的共同基础。
记住,全栈不是“全能”,而是“贯通”。不必追求每个领域的专家级深度,但要理解各层之间的协作方式。当你遇到问题时,知道问题出在哪个层面、该从哪里入手排查,这才是全栈思维的核心。继续迭代你的项目吧——下一个功能,就从你现在想到的那个需求开始。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。