|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
全栈开发是当今IT行业最受欢迎的技能之一,它要求开发者同时掌握前端和后端技术。Node.js作为基于Chrome V8引擎的JavaScript运行时,使得使用JavaScript进行服务器端编程成为可能,极大地简化了全栈开发的学习曲线。本文将通过一个实际项目案例,手把手教你从零开始构建一个完整的Node.js全栈应用,帮助你掌握全栈开发的核心技能。
1. 环境搭建与准备工作
在开始我们的项目之前,需要确保你的开发环境已经准备就绪。
1.1 安装Node.js和npm
首先,你需要在你的计算机上安装Node.js和npm(Node包管理器)。你可以访问Node.js官网下载最新的LTS(长期支持)版本。
安装完成后,打开终端或命令提示符,运行以下命令验证安装:
如果安装成功,你将看到已安装的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。
创建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);
复制代码
创建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 启动应用
在项目根目录下,运行以下命令启动应用:
这将同时启动前端(在端口3000)和后端(在端口5000)。
6.2 测试功能
打开浏览器,访问http://localhost:3000,你应该能够看到首页。尝试以下操作:
1. 注册一个新用户
2. 使用新注册的用户登录
3. 添加一些任务
4. 编辑任务
5. 删除任务
6. 按状态筛选任务
7. 登出并重新登录
如果所有功能都正常工作,那么恭喜你,你已经成功构建了一个全栈应用!
7. 部署应用
开发完成后,你可能希望将应用部署到生产环境。以下是部署应用的基本步骤。
7.1 构建前端应用
首先,构建前端应用:
这将在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文件,内容如下:
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. 打开应用:
8. 总结与扩展
通过本文,你已经学会了如何从零开始构建一个完整的Node.js全栈应用。我们涵盖了以下内容:
1. 环境搭建与准备工作
2. 项目初始化与基础架构设计
3. 后端API开发
4. 前端界面开发
5. 前后端连接
6. 测试应用
7. 部署应用
8.1 可能的扩展
这个项目还有很多可以扩展的地方,例如:
1. 添加更多功能:任务分类和标签任务截止日期和提醒任务优先级任务评论和附件
2. 任务分类和标签
3. 任务截止日期和提醒
4. 任务优先级
5. 任务评论和附件
6. 改进用户界面:添加更多动画和过渡效果实现拖放功能来更改任务状态添加深色模式
7. 添加更多动画和过渡效果
8. 实现拖放功能来更改任务状态
9. 添加深色模式
10. 性能优化:实现分页加载任务添加缓存机制优化数据库查询
11. 实现分页加载任务
12. 添加缓存机制
13. 优化数据库查询
14. 添加测试:单元测试集成测试端到端测试
15. 单元测试
16. 集成测试
17. 端到端测试
18. 安全性增强:添加速率限制实现更严格的输入验证添加CSRF保护
19. 添加速率限制
20. 实现更严格的输入验证
21. 添加CSRF保护
添加更多功能:
• 任务分类和标签
• 任务截止日期和提醒
• 任务优先级
• 任务评论和附件
改进用户界面:
• 添加更多动画和过渡效果
• 实现拖放功能来更改任务状态
• 添加深色模式
性能优化:
• 实现分页加载任务
• 添加缓存机制
• 优化数据库查询
添加测试:
• 单元测试
• 集成测试
• 端到端测试
安全性增强:
• 添加速率限制
• 实现更严格的输入验证
• 添加CSRF保护
8.2 学习资源
如果你想进一步学习全栈开发,以下资源可能会有所帮助:
1. 官方文档:Node.js文档Express文档React文档MongoDB文档
2. Node.js文档
3. Express文档
4. React文档
5. MongoDB文档
6. 在线课程:The Web Developer Bootcamp by Colt SteeleMERN Stack Front To Back by Brad TraversyNode.js, Express, MongoDB & More: The Complete Bootcamp by Jonas Schmedtmann
7. The Web Developer Bootcamp by Colt Steele
8. MERN Stack Front To Back by Brad Traversy
9. Node.js, Express, MongoDB & More: The Complete Bootcamp by Jonas Schmedtmann
10. 书籍:“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
11. “Node.js Design Patterns” by Mario Casciaro
12. “Learning React” by Alex Banks and Eve Porcello
13. “MongoDB: The Definitive Guide” by Shannon Bradshaw, Eoin Brazil, and Kristina Chodorow
官方文档:
• 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
结语
全栈开发是一项复杂但非常有价值的技能。通过构建实际项目,你不仅学习了技术,还了解了如何将这些技术组合在一起创建完整的应用。希望本文能够帮助你开始你的全栈开发之旅,并激发你进一步探索和学习的兴趣。记住,实践是最好的老师,继续构建项目,不断学习新技术,你将成为一名优秀的全栈开发者。 |
|