引言

全栈开发是当今IT行业最受欢迎的技能之一,它要求开发者同时掌握前端和后端技术。Node.js作为基于Chrome V8引擎的JavaScript运行时,使得使用JavaScript进行服务器端编程成为可能,极大地简化了全栈开发的学习曲线。本文将通过一个实际项目案例,手把手教你从零开始构建一个完整的Node.js全栈应用,帮助你掌握全栈开发的核心技能。

1. 环境搭建与准备工作

在开始我们的项目之前,需要确保你的开发环境已经准备就绪。

1.1 安装Node.js和npm

首先,你需要在你的计算机上安装Node.js和npm(Node包管理器)。你可以访问Node.js官网下载最新的LTS(长期支持)版本。

安装完成后,打开终端或命令提示符,运行以下命令验证安装:

node -v npm -v 

如果安装成功,你将看到已安装的Node.js和npm的版本号。

1.2 安装代码编辑器

选择一个合适的代码编辑器对于开发体验至关重要。Visual Studio Code(VS Code)是一个免费且功能强大的选择,它对JavaScript和Node.js有很好的支持。

你可以从VS Code官网下载并安装它。安装后,建议安装以下扩展以提升开发体验:

  • ESLint:JavaScript代码检查工具
  • Prettier:代码格式化工具
  • Node.js Extension Pack:Node.js开发扩展包

1.3 安装MongoDB

我们的项目将使用MongoDB作为数据库。你可以从MongoDB官网下载并安装MongoDB Community Server。

安装完成后,确保MongoDB服务正在运行。在Windows上,你可以通过服务管理器检查;在macOS或Linux上,可以使用以下命令:

sudo systemctl status mongod # 对于使用systemd的系统 sudo service mongod status # 对于使用init.d的系统 

2. 项目初始化与基础架构设计

2.1 创建项目目录

首先,让我们创建一个项目目录并初始化npm项目:

mkdir nodejs-fullstack-project cd nodejs-fullstack-project npm init -y 

这将创建一个名为nodejs-fullstack-project的目录,并在其中生成一个package.json文件。

2.2 项目结构设计

一个好的项目结构能够使代码更易于维护和扩展。我们的项目将采用以下结构:

nodejs-fullstack-project/ ├── client/ # 前端React应用 ├── server/ # 后端Node.js应用 │ ├── config/ # 配置文件 │ ├── controllers/ # 控制器 │ ├── models/ # 数据模型 │ ├── routes/ # 路由定义 │ ├── middleware/ # 中间件 │ └── utils/ # 工具函数 ├── .env # 环境变量 ├── .gitignore # Git忽略文件 └── package.json # 项目依赖和脚本 

让我们创建这个结构:

mkdir -p client server/{config,controllers,models,routes,middleware,utils} touch .env .gitignore 

2.3 安装项目依赖

我们需要安装一些核心依赖来构建我们的应用。首先,安装后端依赖:

npm install express mongoose cors dotenv bcryptjs jsonwebtoken npm install --save-dev nodemon 

这些依赖的作用如下:

  • express:Node.js的Web应用框架
  • mongoose:MongoDB对象建模工具
  • cors:用于处理跨域资源共享
  • dotenv:用于加载环境变量
  • bcryptjs:用于密码哈希
  • jsonwebtoken:用于生成和验证JWT令牌
  • nodemon:开发时自动重启服务器

接下来,创建前端应用并安装依赖:

npx create-react-app client cd client npm install axios react-router-dom 
  • axios:用于发送HTTP请求
  • react-router-dom:React的路由库

2.4 配置package.json脚本

打开package.json文件,添加以下脚本:

"scripts": { "start": "node server/index.js", "server": "nodemon server/index.js", "client": "npm start --prefix client", "dev": "concurrently "npm run server" "npm run client"", "build": "npm run build --prefix client" } 

你还需要安装concurrently来同时运行前端和后端:

npm install --save-dev concurrently 

3. 后端API开发

现在,让我们开始构建后端API。我们将创建一个简单的任务管理应用,它将包括用户认证和任务管理功能。

3.1 创建Express服务器

server目录下创建index.js文件:

const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const dotenv = require('dotenv'); // 加载环境变量 dotenv.config(); const app = express(); // 中间件 app.use(cors()); app.use(express.json()); // 数据库连接 mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }) .then(() => console.log('MongoDB connected')) .catch(err => console.log(err)); // 路由 app.get('/', (req, res) => res.send('API Running')); // 定义端口 const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); 

3.2 配置环境变量

打开.env文件,添加以下内容:

MONGO_URI=mongodb://localhost:27017/taskmanager JWT_SECRET=your_jwt_secret PORT=5000 

3.3 创建数据模型

server/models目录下,我们将创建两个模型:User和Task。

User模型

创建server/models/User.js

const mongoose = require('mongoose'); const UserSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, date: { type: Date, default: Date.now } }); module.exports = mongoose.model('user', UserSchema); 

Task模型

创建server/models/Task.js

const mongoose = require('mongoose'); const TaskSchema = new mongoose.Schema({ user: { type: mongoose.Schema.Types.ObjectId, ref: 'user' }, title: { type: String, required: true }, description: { type: String, required: true }, status: { type: String, enum: ['To Do', 'In Progress', 'Completed'], default: 'To Do' }, date: { type: Date, default: Date.now } }); module.exports = mongoose.model('task', TaskSchema); 

3.4 创建认证中间件

server/middleware目录下创建auth.js文件:

const jwt = require('jsonwebtoken'); const config = require('config'); module.exports = function(req, res, next) { // 从请求头获取token const token = req.header('x-auth-token'); // 检查是否有token if (!token) { return res.status(401).json({ msg: 'No token, authorization denied' }); } try { // 验证token const decoded = jwt.verify(token, process.env.JWT_SECRET); // 将用户信息添加到请求对象 req.user = decoded.user; next(); } catch (err) { res.status(401).json({ msg: 'Token is not valid' }); } }; 

3.5 创建控制器

server/controllers目录下,我们将创建用户和任务的控制器。

用户控制器

创建server/controllers/userController.js

const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const User = require('../models/User'); // 注册用户 exports.registerUser = async (req, res) => { const { name, email, password } = req.body; try { // 检查用户是否已存在 let user = await User.findOne({ email }); if (user) { return res.status(400).json({ msg: 'User already exists' }); } // 创建新用户 user = new User({ name, email, password }); // 加密密码 const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(password, salt); // 保存用户到数据库 await user.save(); // 创建JWT const payload = { user: { id: user.id } }; jwt.sign( payload, process.env.JWT_SECRET, { expiresIn: '5h' }, (err, token) => { if (err) throw err; res.json({ token }); } ); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; // 用户登录 exports.loginUser = async (req, res) => { const { email, password } = req.body; try { // 检查用户是否存在 let user = await User.findOne({ email }); if (!user) { return res.status(400).json({ msg: 'Invalid credentials' }); } // 验证密码 const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ msg: 'Invalid credentials' }); } // 创建JWT const payload = { user: { id: user.id } }; jwt.sign( payload, process.env.JWT_SECRET, { expiresIn: '5h' }, (err, token) => { if (err) throw err; res.json({ token }); } ); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; // 获取当前用户信息 exports.getCurrentUser = async (req, res) => { try { const user = await User.findById(req.user.id).select('-password'); res.json(user); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; 

任务控制器

创建server/controllers/taskController.js

const Task = require('../models/Task'); // 获取所有任务 exports.getTasks = async (req, res) => { try { const tasks = await Task.find({ user: req.user.id }).sort({ date: -1 }); res.json(tasks); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; // 添加新任务 exports.addTask = async (req, res) => { const { title, description, status } = req.body; try { const newTask = new Task({ title, description, status, user: req.user.id }); const task = await newTask.save(); res.json(task); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; // 更新任务 exports.updateTask = async (req, res) => { const { title, description, status } = req.body; // 构建任务对象 const taskFields = {}; if (title) taskFields.title = title; if (description) taskFields.description = description; if (status) taskFields.status = status; try { let task = await Task.findById(req.params.id); if (!task) return res.status(404).json({ msg: 'Task not found' }); // 确保用户拥有任务 if (task.user.toString() !== req.user.id) { return res.status(401).json({ msg: 'Not authorized' }); } task = await Task.findByIdAndUpdate( req.params.id, { $set: taskFields }, { new: true } ); res.json(task); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; // 删除任务 exports.deleteTask = async (req, res) => { try { let task = await Task.findById(req.params.id); if (!task) return res.status(404).json({ msg: 'Task not found' }); // 确保用户拥有任务 if (task.user.toString() !== req.user.id) { return res.status(401).json({ msg: 'Not authorized' }); } await Task.findByIdAndRemove(req.params.id); res.json({ msg: 'Task removed' }); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }; 

3.6 创建路由

server/routes目录下,我们将创建用户和任务的路由。

用户路由

创建server/routes/users.js

const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); const auth = require('../middleware/auth'); // @route POST api/users/register // @desc 注册用户 // @access 公开 router.post('/register', userController.registerUser); // @route POST api/users/login // @desc 用户登录 // @access 公开 router.post('/login', userController.loginUser); // @route GET api/users/current // @desc 获取当前用户 // @access 私有 router.get('/current', auth, userController.getCurrentUser); module.exports = router; 

任务路由

创建server/routes/tasks.js

const express = require('express'); const router = express.Router(); const taskController = require('../controllers/taskController'); const auth = require('../middleware/auth'); // @route GET api/tasks // @desc 获取所有任务 // @access 私有 router.get('/', auth, taskController.getTasks); // @route POST api/tasks // @desc 添加新任务 // @access 私有 router.post('/', auth, taskController.addTask); // @route PUT api/tasks/:id // @desc 更新任务 // @access 私有 router.put('/:id', auth, taskController.updateTask); // @route DELETE api/tasks/:id // @desc 删除任务 // @access 私有 router.delete('/:id', auth, taskController.deleteTask); module.exports = router; 

3.7 更新服务器文件

现在,让我们更新server/index.js文件,添加我们创建的路由:

const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const dotenv = require('dotenv'); // 加载环境变量 dotenv.config(); const app = express(); // 中间件 app.use(cors()); app.use(express.json()); // 数据库连接 mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }) .then(() => console.log('MongoDB connected')) .catch(err => console.log(err)); // 路由 app.get('/', (req, res) => res.send('API Running')); // 定义路由 app.use('/api/users', require('./routes/users')); app.use('/api/tasks', require('./routes/tasks')); // 定义端口 const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); 

4. 前端界面开发

现在,让我们开始构建前端界面。我们将使用React来创建一个单页应用。

4.1 设置React应用

我们已经使用create-react-app创建了React应用。现在,让我们清理并设置基本结构。

client/src目录下,创建以下文件夹结构:

client/src/ ├── components/ # React组件 ├── context/ # React Context ├── services/ # API服务 └── utils/ # 工具函数 

4.2 创建Context

我们将使用React Context来管理全局状态,如用户认证状态。

创建client/src/context/AuthContext.js

import React, { createContext, useState, useEffect } from 'react'; import authService from '../services/authService'; export const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); useEffect(() => { const loadUser = async () => { const token = localStorage.getItem('token'); if (token) { authService.setAuthToken(token); try { const res = await authService.getCurrentUser(); setUser(res.data); setIsAuthenticated(true); } catch (err) { localStorage.removeItem('token'); setUser(null); setIsAuthenticated(false); } } setLoading(false); }; loadUser(); }, []); // 注册用户 const register = async (userData) => { try { const res = await authService.register(userData); localStorage.setItem('token', res.data.token); authService.setAuthToken(res.data.token); const userRes = await authService.getCurrentUser(); setUser(userRes.data); setIsAuthenticated(true); return { success: true }; } catch (err) { return { success: false, error: err.response.data.msg || 'Registration failed' }; } }; // 用户登录 const login = async (userData) => { try { const res = await authService.login(userData); localStorage.setItem('token', res.data.token); authService.setAuthToken(res.data.token); const userRes = await authService.getCurrentUser(); setUser(userRes.data); setIsAuthenticated(true); return { success: true }; } catch (err) { return { success: false, error: err.response.data.msg || 'Login failed' }; } }; // 用户登出 const logout = () => { localStorage.removeItem('token'); setUser(null); setIsAuthenticated(false); }; return ( <AuthContext.Provider value={{ user, isAuthenticated, loading, register, login, logout }} > {children} </AuthContext.Provider> ); }; 

4.3 创建API服务

client/src/services目录下,我们将创建API服务文件。

认证服务

创建client/src/services/authService.js

import axios from 'axios'; const API_URL = '/api/users'; // 设置axios默认headers const setAuthToken = token => { if (token) { axios.defaults.headers.common['x-auth-token'] = token; } else { delete axios.defaults.headers.common['x-auth-token']; } }; // 注册用户 const register = async userData => { const response = await axios.post(`${API_URL}/register`, userData); return response; }; // 用户登录 const login = async userData => { const response = await axios.post(`${API_URL}/login`, userData); return response; }; // 获取当前用户 const getCurrentUser = async () => { const response = await axios.get(`${API_URL}/current`); return response; }; export default { setAuthToken, register, login, getCurrentUser }; 

任务服务

创建client/src/services/taskService.js

import axios from 'axios'; const API_URL = '/api/tasks'; // 获取所有任务 const getTasks = async () => { const response = await axios.get(API_URL); return response; }; // 添加新任务 const addTask = async taskData => { const response = await axios.post(API_URL, taskData); return response; }; // 更新任务 const updateTask = async (id, taskData) => { const response = await axios.put(`${API_URL}/${id}`, taskData); return response; }; // 删除任务 const deleteTask = async id => { const response = await axios.delete(`${API_URL}/${id}`); return response; }; export default { getTasks, addTask, updateTask, deleteTask }; 

4.4 创建React组件

现在,让我们创建React组件来构建我们的用户界面。

认证组件

创建client/src/components/auth/Register.js

import React, { useState, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../../context/AuthContext'; const Register = () => { const [formData, setFormData] = useState({ name: '', email: '', password: '', password2: '' }); const [alert, setAlert] = useState(null); const { name, email, password, password2 } = formData; const { register } = useContext(AuthContext); const navigate = useNavigate(); const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value }); const onSubmit = async e => { e.preventDefault(); if (password !== password2) { setAlert({ type: 'danger', message: 'Passwords do not match' }); return; } const result = await register({ name, email, password }); if (result.success) { navigate('/dashboard'); } else { setAlert({ type: 'danger', message: result.error }); } }; return ( <div className="register-container"> <h1>Sign Up</h1> {alert && <div className={`alert alert-${alert.type}`}>{alert.message}</div>} <form onSubmit={onSubmit}> <div className="form-group"> <label htmlFor="name">Name</label> <input type="text" name="name" value={name} onChange={onChange} required /> </div> <div className="form-group"> <label htmlFor="email">Email Address</label> <input type="email" name="email" value={email} onChange={onChange} required /> </div> <div className="form-group"> <label htmlFor="password">Password</label> <input type="password" name="password" value={password} onChange={onChange} required minLength="6" /> </div> <div className="form-group"> <label htmlFor="password2">Confirm Password</label> <input type="password" name="password2" value={password2} onChange={onChange} required minLength="6" /> </div> <button type="submit" className="btn btn-primary">Register</button> </form> </div> ); }; export default Register; 

创建client/src/components/auth/Login.js

import React, { useState, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../../context/AuthContext'; const Login = () => { const [formData, setFormData] = useState({ email: '', password: '' }); const [alert, setAlert] = useState(null); const { email, password } = formData; const { login } = useContext(AuthContext); const navigate = useNavigate(); const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value }); const onSubmit = async e => { e.preventDefault(); const result = await login({ email, password }); if (result.success) { navigate('/dashboard'); } else { setAlert({ type: 'danger', message: result.error }); } }; return ( <div className="login-container"> <h1>Login</h1> {alert && <div className={`alert alert-${alert.type}`}>{alert.message}</div>} <form onSubmit={onSubmit}> <div className="form-group"> <label htmlFor="email">Email Address</label> <input type="email" name="email" value={email} onChange={onChange} required /> </div> <div className="form-group"> <label htmlFor="password">Password</label> <input type="password" name="password" value={password} onChange={onChange} required /> </div> <button type="submit" className="btn btn-primary">Login</button> </form> </div> ); }; export default Login; 

任务组件

创建client/src/components/tasks/TaskForm.js

import React, { useState, useContext, useEffect } from 'react'; import { AuthContext } from '../../context/AuthContext'; import taskService from '../../services/taskService'; const TaskForm = ({ currentTask, clearCurrent }) => { const { user } = useContext(AuthContext); const [task, setTask] = useState({ title: '', description: '', status: 'To Do' }); const [alert, setAlert] = useState(null); useEffect(() => { if (currentTask !== null) { setTask(currentTask); } else { setTask({ title: '', description: '', status: 'To Do' }); } }, [currentTask]); const { title, description, status } = task; const onChange = e => setTask({ ...task, [e.target.name]: e.target.value }); const onSubmit = async e => { e.preventDefault(); if (title === '' || description === '') { setAlert({ type: 'danger', message: 'Please enter title and description' }); return; } try { if (currentTask === null) { await taskService.addTask({ ...task, user: user._id }); setAlert({ type: 'success', message: 'Task added successfully' }); } else { await taskService.updateTask(currentTask._id, task); setAlert({ type: 'success', message: 'Task updated successfully' }); } // 清空表单 setTask({ title: '', description: '', status: 'To Do' }); clearCurrent(); } catch (err) { setAlert({ type: 'danger', message: 'Error saving task' }); } }; return ( <div className="task-form-container"> <h2>{currentTask ? 'Edit Task' : 'Add Task'}</h2> {alert && <div className={`alert alert-${alert.type}`}>{alert.message}</div>} <form onSubmit={onSubmit}> <div className="form-group"> <label htmlFor="title">Title</label> <input type="text" name="title" value={title} onChange={onChange} required /> </div> <div className="form-group"> <label htmlFor="description">Description</label> <textarea name="description" value={description} onChange={onChange} required /> </div> <div className="form-group"> <label htmlFor="status">Status</label> <select name="status" value={status} onChange={onChange} > <option value="To Do">To Do</option> <option value="In Progress">In Progress</option> <option value="Completed">Completed</option> </select> </div> <div className="form-group"> <button type="submit" className="btn btn-primary"> {currentTask ? 'Update Task' : 'Add Task'} </button> {currentTask && ( <button type="button" className="btn btn-light" onClick={clearCurrent} > Clear </button> )} </div> </form> </div> ); }; export default TaskForm; 

创建client/src/components/tasks/TaskItem.js

import React, { useContext } from 'react'; import { AuthContext } from '../../context/AuthContext'; import taskService from '../../services/taskService'; const TaskItem = ({ task, setCurrent, clearCurrent }) => { const { user } = useContext(AuthContext); const onDelete = async id => { if (window.confirm('Are you sure you want to delete this task?')) { try { await taskService.deleteTask(id); clearCurrent(); } catch (err) { console.error(err); } } }; return ( <div className="task-item"> <h3>{task.title}</h3> <p>{task.description}</p> <span className={`status status-${task.status.replace(' ', '-').toLowerCase()}`}> {task.status} </span> <div className="task-actions"> <button onClick={() => setCurrent(task)} className="btn btn-sm btn-primary" > Edit </button> <button onClick={() => onDelete(task._id)} className="btn btn-sm btn-danger" > Delete </button> </div> </div> ); }; export default TaskItem; 

创建client/src/components/tasks/TaskList.js

import React, { useState, useEffect, useContext } from 'react'; import { AuthContext } from '../../context/AuthContext'; import taskService from '../../services/taskService'; import TaskItem from './TaskItem'; const TaskList = () => { const { user } = useContext(AuthContext); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState('All'); useEffect(() => { const getTasks = async () => { try { const res = await taskService.getTasks(); setTasks(res.data); setLoading(false); } catch (err) { console.error(err); setLoading(false); } }; getTasks(); }, [user]); const filteredTasks = filter === 'All' ? tasks : tasks.filter(task => task.status === filter); if (loading) { return <div>Loading tasks...</div>; } return ( <div className="task-list-container"> <h2>My Tasks</h2> <div className="filter-container"> <button className={`btn ${filter === 'All' ? 'btn-primary' : 'btn-light'}`} onClick={() => setFilter('All')} > All </button> <button className={`btn ${filter === 'To Do' ? 'btn-primary' : 'btn-light'}`} onClick={() => setFilter('To Do')} > To Do </button> <button className={`btn ${filter === 'In Progress' ? 'btn-primary' : 'btn-light'}`} onClick={() => setFilter('In Progress')} > In Progress </button> <button className={`btn ${filter === 'Completed' ? 'btn-primary' : 'btn-light'}`} onClick={() => setFilter('Completed')} > Completed </button> </div> {filteredTasks.length === 0 ? ( <div className="no-tasks">No tasks found</div> ) : ( <div className="task-list"> {filteredTasks.map(task => ( <TaskItem key={task._id} task={task} /> ))} </div> )} </div> ); }; export default TaskList; 

布局组件

创建client/src/components/layout/Navbar.js

import React, { useContext } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { AuthContext } from '../../context/AuthContext'; const Navbar = () => { const { isAuthenticated, user, logout } = useContext(AuthContext); const navigate = useNavigate(); const onLogout = () => { logout(); navigate('/login'); }; const authLinks = ( <> <li>Hello, {user && user.name}</li> <li> <a onClick={onLogout} href="#!"> Logout </a> </li> </> ); const guestLinks = ( <> <li> <Link to="/register">Register</Link> </li> <li> <Link to="/login">Login</Link> </li> </> ); return ( <nav className="navbar"> <h1> <Link to="/">Task Manager</Link> </h1> <ul>{isAuthenticated ? authLinks : guestLinks}</ul> </nav> ); }; export default Navbar; 

4.5 创建页面组件

client/src目录下,创建pages文件夹,并在其中创建以下页面组件。

创建client/src/pages/Home.js

import React from 'react'; import { Link } from 'react-router-dom'; const Home = () => { return ( <div className="home-container"> <h1>Welcome to Task Manager</h1> <p>Please log in or register to manage your tasks</p> <div className="buttons"> <Link to="/register" className="btn btn-primary"> Register </Link> <Link to="/login" className="btn btn-light"> Login </Link> </div> </div> ); }; export default Home; 

创建client/src/pages/Dashboard.js

import React, { useState, useContext } from 'react'; import { AuthContext } from '../context/AuthContext'; import TaskForm from '../components/tasks/TaskForm'; import TaskList from '../components/tasks/TaskList'; const Dashboard = () => { const { isAuthenticated } = useContext(AuthContext); const [currentTask, setCurrent] = useState(null); const clearCurrent = () => { setCurrent(null); }; if (!isAuthenticated) { // 这里应该重定向到登录页面,但我们将在路由中处理 return null; } return ( <div className="dashboard-container"> <div className="dashboard-content"> <div className="task-form-section"> <TaskForm currentTask={currentTask} clearCurrent={clearCurrent} /> </div> <div className="task-list-section"> <TaskList setCurrent={setCurrent} clearCurrent={clearCurrent} /> </div> </div> </div> ); }; export default Dashboard; 

4.6 更新App.js

现在,让我们更新client/src/App.js文件,设置路由和上下文提供者:

import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import Navbar from './components/layout/Navbar'; import Home from './pages/Home'; import Register from './components/auth/Register'; import Login from './components/auth/Login'; import Dashboard from './pages/Dashboard'; import './App.css'; function App() { return ( <AuthProvider> <Router> <div className="App"> <Navbar /> <div className="container"> <Routes> <Route path="/" element={<Home />} /> <Route path="/register" element={<Register />} /> <Route path="/login" element={<Login />} /> <Route path="/dashboard" element={ <PrivateRoute> <Dashboard /> </PrivateRoute> } /> </Routes> </div> </div> </Router> </AuthProvider> ); } // 私有路由组件 const PrivateRoute = ({ children }) => { const { isAuthenticated, loading } = React.useContext(AuthContext); if (loading) { return <div>Loading...</div>; } return isAuthenticated ? children : <Navigate to="/login" />; }; export default App; 

4.7 添加基本样式

创建client/src/App.css文件,添加一些基本样式:

/* 全局样式 */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Arial', sans-serif; line-height: 1.6; background-color: #f4f4f4; color: #333; } .container { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; } /* 导航栏 */ .navbar { display: flex; justify-content: space-between; align-items: center; background: #333; color: #fff; padding: 1rem; } .navbar h1 a { color: #fff; text-decoration: none; } .navbar ul { display: flex; list-style: none; } .navbar ul li { margin-left: 1rem; } .navbar ul li a { color: #fff; text-decoration: none; } /* 按钮样式 */ .btn { display: inline-block; background: #333; color: #fff; padding: 0.5rem 1rem; border: none; cursor: pointer; font-size: 1rem; border-radius: 5px; margin-right: 0.5rem; text-decoration: none; } .btn:hover { background: #555; } .btn-primary { background: #007bff; } .btn-primary:hover { background: #0069d9; } .btn-danger { background: #dc3545; } .btn-danger:hover { background: #c82333; } .btn-light { background: #f8f9fa; color: #333; } .btn-light:hover { background: #e2e6ea; } .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; } /* 表单样式 */ .form-group { margin-bottom: 1rem; } .form-group label { display: block; margin-bottom: 0.5rem; } .form-group input, .form-group textarea, .form-group select { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 5px; } .form-group textarea { height: 100px; } /* 警告框样式 */ .alert { padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 5px; } .alert-danger { color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; } .alert-success { color: #155724; background-color: #d4edda; border-color: #c3e6cb; } /* 首页样式 */ .home-container { text-align: center; padding: 2rem; } .home-container h1 { margin-bottom: 1rem; } .home-container p { margin-bottom: 2rem; } /* 认证页面样式 */ .register-container, .login-container { max-width: 500px; margin: 2rem auto; padding: 2rem; background: #fff; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } /* 仪表板样式 */ .dashboard-container { display: flex; flex-direction: column; } .dashboard-content { display: grid; grid-template-columns: 1fr 2fr; gap: 2rem; } /* 任务表单样式 */ .task-form-container { background: #fff; padding: 1.5rem; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } /* 任务列表样式 */ .task-list-container { background: #fff; padding: 1.5rem; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } .filter-container { margin-bottom: 1rem; } .filter-container .btn { margin-right: 0.5rem; } .task-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } .task-item { background: #f9f9f9; padding: 1rem; border-radius: 5px; border-left: 4px solid #007bff; } .task-item h3 { margin-bottom: 0.5rem; } .task-item p { margin-bottom: 1rem; } .status { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.8rem; font-weight: bold; } .status-to-do { background: #f8d7da; color: #721c24; } .status-in-progress { background: #fff3cd; color: #856404; } .status-completed { background: #d4edda; color: #155724; } .task-actions { margin-top: 1rem; } .no-tasks { text-align: center; padding: 2rem; color: #666; } /* 响应式设计 */ @media (max-width: 768px) { .dashboard-content { grid-template-columns: 1fr; } .navbar { flex-direction: column; text-align: center; } .navbar ul { margin-top: 1rem; } .navbar ul li { margin: 0 0.5rem; } } 

5. 前后端连接

现在,我们需要确保前端和后端能够正确通信。为此,我们需要设置代理,以便前端可以向后端API发送请求。

5.1 设置代理

client/package.json文件中,添加以下代理配置:

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

这将允许前端应用将API请求代理到运行在端口5000上的后端服务器。

5.2 添加CORS支持

我们已经安装了cors中间件并在server/index.js中使用了它,这应该已经处理了跨域资源共享问题。但让我们确保它正确配置:

server/index.js中,更新CORS配置:

// 中间件 app.use(cors({ origin: 'http://localhost:3000', // 允许来自前端的请求 credentials: true })); app.use(express.json()); 

6. 测试应用

现在,我们已经完成了前后端的开发,让我们测试一下应用是否正常工作。

6.1 启动应用

在项目根目录下,运行以下命令启动应用:

npm run dev 

这将同时启动前端(在端口3000)和后端(在端口5000)。

6.2 测试功能

打开浏览器,访问http://localhost:3000,你应该能够看到首页。尝试以下操作:

  1. 注册一个新用户
  2. 使用新注册的用户登录
  3. 添加一些任务
  4. 编辑任务
  5. 删除任务
  6. 按状态筛选任务
  7. 登出并重新登录

如果所有功能都正常工作,那么恭喜你,你已经成功构建了一个全栈应用!

7. 部署应用

开发完成后,你可能希望将应用部署到生产环境。以下是部署应用的基本步骤。

7.1 构建前端应用

首先,构建前端应用:

npm run build 

这将在client/build目录中创建一个优化过的生产版本的前端应用。

7.2 设置Express服务静态文件

更新server/index.js,添加以下代码来服务静态文件:

const path = require('path'); // 在其他中间件之后,路由之前添加 // 静态文件服务 if (process.env.NODE_ENV === 'production') { // 设置静态文件夹 app.use(express.static('client/build')); // 任何不是API路由的请求都将被重定向到index.html app.get('*', (req, res) => { res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html')); }); } 

7.3 部署到云服务

你可以选择将应用部署到各种云服务,如Heroku、AWS、Google Cloud等。以下是部署到Heroku的基本步骤:

  1. 创建Heroku账户并安装Heroku CLI
  2. 在项目根目录下,创建Procfile文件,内容如下:
web: npm start 
  1. 初始化Git仓库并提交代码:
git init git add . git commit -m "Initial commit" 
  1. 创建Heroku应用并推送代码:
heroku create git push heroku master 
  1. 设置环境变量:
heroku config:set MONGO_URI=your_mongodb_connection_string heroku config:set JWT_SECRET=your_jwt_secret 
  1. 打开应用:
heroku open 

8. 总结与扩展

通过本文,你已经学会了如何从零开始构建一个完整的Node.js全栈应用。我们涵盖了以下内容:

  1. 环境搭建与准备工作
  2. 项目初始化与基础架构设计
  3. 后端API开发
  4. 前端界面开发
  5. 前后端连接
  6. 测试应用
  7. 部署应用

8.1 可能的扩展

这个项目还有很多可以扩展的地方,例如:

  1. 添加更多功能

    • 任务分类和标签
    • 任务截止日期和提醒
    • 任务优先级
    • 任务评论和附件
  2. 改进用户界面

    • 添加更多动画和过渡效果
    • 实现拖放功能来更改任务状态
    • 添加深色模式
  3. 性能优化

    • 实现分页加载任务
    • 添加缓存机制
    • 优化数据库查询
  4. 添加测试

    • 单元测试
    • 集成测试
    • 端到端测试
  5. 安全性增强

    • 添加速率限制
    • 实现更严格的输入验证
    • 添加CSRF保护

8.2 学习资源

如果你想进一步学习全栈开发,以下资源可能会有所帮助:

  1. 官方文档

    • Node.js文档
    • Express文档
    • React文档
    • MongoDB文档
  2. 在线课程

    • The Web Developer Bootcamp by Colt Steele
    • MERN Stack Front To Back by Brad Traversy
    • Node.js, Express, MongoDB & More: The Complete Bootcamp by Jonas Schmedtmann
  3. 书籍

    • “Node.js Design Patterns” by Mario Casciaro
    • “Learning React” by Alex Banks and Eve Porcello
    • “MongoDB: The Definitive Guide” by Shannon Bradshaw, Eoin Brazil, and Kristina Chodorow

结语

全栈开发是一项复杂但非常有价值的技能。通过构建实际项目,你不仅学习了技术,还了解了如何将这些技术组合在一起创建完整的应用。希望本文能够帮助你开始你的全栈开发之旅,并激发你进一步探索和学习的兴趣。记住,实践是最好的老师,继续构建项目,不断学习新技术,你将成为一名优秀的全栈开发者。