从零开始构建Node.js实际项目案例详解手把手教你掌握全栈开发技能
引言
全栈开发是当今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
,你应该能够看到首页。尝试以下操作:
- 注册一个新用户
- 使用新注册的用户登录
- 添加一些任务
- 编辑任务
- 删除任务
- 按状态筛选任务
- 登出并重新登录
如果所有功能都正常工作,那么恭喜你,你已经成功构建了一个全栈应用!
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的基本步骤:
- 创建Heroku账户并安装Heroku CLI
- 在项目根目录下,创建
Procfile
文件,内容如下:
web: npm start
- 初始化Git仓库并提交代码:
git init git add . git commit -m "Initial commit"
- 创建Heroku应用并推送代码:
heroku create git push heroku master
- 设置环境变量:
heroku config:set MONGO_URI=your_mongodb_connection_string heroku config:set JWT_SECRET=your_jwt_secret
- 打开应用:
heroku open
8. 总结与扩展
通过本文,你已经学会了如何从零开始构建一个完整的Node.js全栈应用。我们涵盖了以下内容:
- 环境搭建与准备工作
- 项目初始化与基础架构设计
- 后端API开发
- 前端界面开发
- 前后端连接
- 测试应用
- 部署应用
8.1 可能的扩展
这个项目还有很多可以扩展的地方,例如:
添加更多功能:
- 任务分类和标签
- 任务截止日期和提醒
- 任务优先级
- 任务评论和附件
改进用户界面:
- 添加更多动画和过渡效果
- 实现拖放功能来更改任务状态
- 添加深色模式
性能优化:
- 实现分页加载任务
- 添加缓存机制
- 优化数据库查询
添加测试:
- 单元测试
- 集成测试
- 端到端测试
安全性增强:
- 添加速率限制
- 实现更严格的输入验证
- 添加CSRF保护
8.2 学习资源
如果你想进一步学习全栈开发,以下资源可能会有所帮助:
官方文档:
- Node.js文档
- Express文档
- React文档
- MongoDB文档
在线课程:
- 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
书籍:
- “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
结语
全栈开发是一项复杂但非常有价值的技能。通过构建实际项目,你不仅学习了技术,还了解了如何将这些技术组合在一起创建完整的应用。希望本文能够帮助你开始你的全栈开发之旅,并激发你进一步探索和学习的兴趣。记住,实践是最好的老师,继续构建项目,不断学习新技术,你将成为一名优秀的全栈开发者。