首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >全栈开发实战:从零构建一个完整的Web应用

全栈开发实战:从零构建一个完整的Web应用

原创
作者头像
闪 学it
发布2026-08-22 16:53:59
发布2026-08-22 16:53:59
590
举报

全栈开发,顾名思义,是指同时掌握前端(用户界面)与后端(服务器、数据库、业务逻辑)的开发能力。在技术快速迭代的今天,全栈开发者因其能够独立完成产品从构思到上线的全过程,成为技术团队中极具价值的角色。

本文将通过一个待办事项管理应用的完整开发过程,带你走一遍全栈开发的真实工作流。项目虽然小巧,但涵盖了前后端分离架构、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交互完成数据读写。前后端完全解耦,各自独立部署。

代码语言:javascript
复制
┌─────────────┐    HTTP请求    ┌─────────────┐    读写     ┌─────────────┐
│   前端       │ ◄────────────► │   后端       │ ◄─────────► │   数据库     │
│  React + Vite│                │Express + Node│             │  MongoDB    │
└─────────────┘                └─────────────┘             └─────────────┘

二、后端开发:搭建RESTful API

2.1 初始化项目

代码语言:javascript
复制
# 创建项目目录
mkdir todo-fullstack
cd todo-fullstack
mkdir backend frontend

# 初始化后端
cd backend
npm init -y
npm install express mongoose cors dotenv
npm install -D nodemon

2.2 连接数据库

创建backend/.env文件,存放敏感配置:

代码语言:javascript
复制
MONGODB_URI=mongodb://localhost:27017/todo_app
PORT=5000

创建backend/src/db.js,封装数据库连接逻辑:

代码语言:javascript
复制
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;

2.3 定义数据模型

Mongoose的Schema定义数据结构和验证规则。创建backend/src/models/Todo.js

代码语言:javascript
复制
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);

2.4 实现业务逻辑与路由

创建backend/src/routes/todos.js,实现完整的CRUD接口:

代码语言:javascript
复制
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;

2.5 组装主程序

创建backend/src/index.js,启动服务器:

代码语言:javascript
复制
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中添加启动脚本:

代码语言:javascript
复制
"scripts": {
  "dev": "nodemon src/index.js",
  "start": "node src/index.js"
}

运行npm run dev,后端服务就启动了。

三、前端开发:构建交互界面

3.1 初始化前端项目

代码语言:javascript
复制
cd ../frontend
npm create vite@latest . -- --template react
npm install
npm install axios react-icons

3.2 配置API代理(解决跨域)

frontend/vite.config.js中配置代理:

代码语言:javascript
复制
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true
      }
    }
  }
})

3.3 封装API请求

创建frontend/src/api/todoApi.js

代码语言:javascript
复制
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;
  }
};

3.4 构建主组件

创建frontend/src/App.jsx,实现完整的待办管理界面:

代码语言:javascript
复制
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;

3.5 添加基础样式

创建frontend/src/App.css

代码语言:javascript
复制
* { 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;
}

四、运行与联调

  1. 启动MongoDB(本地需安装): bash mongod --dbpath ./data
  2. 启动后端(在backend目录下): bash npm run dev # 输出: 🚀 服务运行在 http://localhost:5000
  3. 启动前端(在frontend目录下): bash npm run dev # 输出: ➜ Local: http://localhost:5173
  4. 浏览器访问http://localhost:5173,一个完整的待办管理应用就呈现在眼前了。

五、全栈开发的实战建议

5.1 项目结构演进

小项目可以采用上述扁平结构;项目变大后,建议按功能模块拆分:

代码语言:javascript
复制
backend/src/
├── models/          # 数据模型
├── routes/          # 路由定义
├── controllers/     # 业务控制器
├── middleware/      # 中间件(鉴权、日志等)
├── services/        # 业务逻辑层
├── utils/           # 工具函数
└── config/          # 配置文件

5.2 常见痛点与解决方案

问题

解决方案

跨域CORS

开发阶段用Vite代理;生产环境部署同源或配置CORS中间件

环境变量管理

使用dotenv,.gitignore忽略.env文件

接口变更影响前端

使用Swagger/OpenAPI生成接口文档,前后端契约测试

数据库连接断开

实现连接重试机制和健康检查端点

5.3 从Demo到产品

本教程的待办应用是MVP(最小可行产品)。要将其产品化,还需要补充:

  • 用户认证:JWT或Session登录注册
  • 输入校验:使用Joi或zod验证请求参数
  • 日志记录:集成winston或pino
  • 错误监控:接入Sentry等工具
  • 单元测试:Jest + Supertest
  • 部署配置:Docker容器化、CI/CD流水线

结语

全栈开发的价值在于端到端的掌控力。当你既能构建美观的交互界面,又能设计稳健的后端服务时,你就拥有了将一个想法变成可运行产品的完整能力。本文的待办应用只是一个起点,但其中涉及的模式——前后端分离、RESTful API、数据库建模、状态管理——是几乎所有Web应用的共同基础。

记住,全栈不是“全能”,而是“贯通”。不必追求每个领域的专家级深度,但要理解各层之间的协作方式。当你遇到问题时,知道问题出在哪个层面、该从哪里入手排查,这才是全栈思维的核心。继续迭代你的项目吧——下一个功能,就从你现在想到的那个需求开始。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、技术选型与架构设计
  • 二、后端开发:搭建RESTful API
    • 2.1 初始化项目
    • 2.2 连接数据库
    • 2.3 定义数据模型
    • 2.4 实现业务逻辑与路由
    • 2.5 组装主程序
  • 三、前端开发:构建交互界面
    • 3.1 初始化前端项目
    • 3.2 配置API代理(解决跨域)
    • 3.3 封装API请求
    • 3.4 构建主组件
    • 3.5 添加基础样式
  • 四、运行与联调
  • 五、全栈开发的实战建议
    • 5.1 项目结构演进
    • 5.2 常见痛点与解决方案
    • 5.3 从Demo到产品
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档