兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
同学们,前端框架Vue.js是现代Web前端开发的核心。掌握它,你就能高效地构建出美观、交互性强的用户界面。 至此,我们已经完成了第三阶段**“全栈应用开发实战”**的第五课“前端框架 - Vue.js基础”的所有内容。接下来,我们将正式进入全栈开发的另一半——**后端开发**的世界,首先从高效的**Node.js/Express**开始。请大家稍作休息,我们稍后继续。 好的,同学们,我们继续第三阶段**“全栈应用开发实战”**的学习!前面我们已经打下了坚实的前端基础,掌握了HTML、CSS、JavaScript和Vue.js,能够构建出美观且交互性强的用户界面。现在,我们将把目光转向Web应用的另一半——**后端开发**。 如果说前端是Web应用的“脸面”,那么后端就是它的“大脑”和“骨骼”,负责处理数据、执行业务逻辑、与数据库交互、提供API接口等。没有后端,大多数现代Web应用都只是静态展示,无法实现用户登录、数据存储、在线交易等核心功能。 本节课,我们将学习使用**Node.js和Express框架**来构建后端应用。Node.js让我们可以用JavaScript来编写后端代码,实现了前端和后端语言的统一,这对于全栈开发者来说是一个巨大的优势。 --- ### 课程3.5:后端开发基础 - Node.js/Express(超详细版) #### 一、Node.js基础:用JavaScript构建后端 ##### 1.1 Node.js简介:JS的“跨界”之旅 * **什么是Node.js?** * **定义**:Node.js是一个**基于Chrome V8 JavaScript引擎**的**JavaScript运行时环境**。它使得JavaScript代码可以在**服务器端、桌面应用、命令行工具**等浏览器环境之外的地方运行。 * **核心特性**: 1. **单线程**:Node.js的JavaScript执行是单线程的(不包括I/O线程池)。 2. **事件驱动(Event-driven)**:基于事件循环(Event Loop)机制,响应事件和回调函数。 3. **非阻塞I/O(Non-blocking I/O)**:所有I/O操作(文件读写、网络请求、数据库查询)都是异步的。当发起I/O操作时,Node.js不会等待其完成,而是立即返回,并在I/O完成后通过回调函数或Promise来通知。 * **优点**:非常适合构建**高并发、I/O密集型**的应用,如Web服务器、API服务、实时通信应用。因为它不需要为每个连接创建新的线程,减少了内存消耗和上下文切换开销。 * **npm生态**:Node.js拥有全球最大、最活跃的开源包管理平台npm(Node Package Manager),提供了海量的第三方模块和工具,极大地加速了开发。 ##### 1.2 Node.js的工作原理:事件循环的“魔力” * **事件循环(Event Loop)**: * 这是Node.js实现高并发、非阻塞I/O的核心机制。 * **原理**:Node.js将所有I/O操作(如文件读取、网络请求、数据库查询)交给底层操作系统处理,而不会阻塞JavaScript主线程。当这些I/O操作完成后,操作系统会将其结果放入一个**事件队列(Event Queue)**中。JavaScript主线程(单线程)会不断地从事件队列中取出事件,并执行对应的**回调函数(Callback Function)**。 * **比喻**:你(JavaScript主线程)是一个大厨,你把“烧水”(I/O操作)这个任务交给小弟(操作系统)去完成,你不需要一直盯着水壶。水烧开后,小弟会通知你(事件队列中添加事件),你再回来处理“沏茶”(执行回调)。这样你就不会被“烧水”这个慢任务阻塞,可以继续处理其他切菜、配料的快任务。 ##### 1.3 Node.js应用场景:JS的“多面手” * **RESTful API服务器**:构建高性能的Web API服务,为前端应用提供数据接口。 * **实时通信应用**:如聊天室、在线协作工具(WebSocket)。 * **跨平台命令行工具**:如npm、Vue CLI、Create React App。 * **微服务(Microservices)**:作为轻量级、高并发的微服务组件。 * **BFF(Backend For Frontend)**:为特定前端应用聚合和适配后端数据。 * **前端构建工具**:如Webpack、Vite、Rollup等,它们都是用Node.js开发的。 #### 二、Node.js核心模块与常用API:后端开发的“基本功” Node.js提供了许多内置的核心模块,无需安装即可直接使用。 ##### 2.1 核心模块 * **`fs` (File System)**:用于文件系统操作,如读取、写入、删除文件,创建、删除目录等。 * **特点**:提供同步和异步两种API。异步API是主流。 * **示例**: ```javascript const fs = require('fs'); // 导入fs模块 // 异步读取文件 fs.readFile('example.txt', 'utf8', (err, data) => { if (err) { console.error('读取文件失败:', err); return; } console.log('文件内容:', data); }); // 异步写入文件 fs.writeFile('output.txt', 'Hello Node.js!', (err) => { if (err) console.error('写入文件失败:', err); else console.log('文件写入成功。'); }); ``` * **`http`/`https`**:用于创建Web服务器和发起HTTP/HTTPS请求。 * **示例**: ```javascript const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, Node.js HTTP Server!'); }); server.listen(3000, () => { console.log('Server running at http://localhost:3000'); }); ``` * **`path`**:用于处理文件和目录路径,提供跨操作系统的路径操作方法。 * **示例**:`path.join(__dirname, 'data', 'file.txt')` * **`os`**:提供操作系统相关的信息和工具方法。 * **示例**:`os.cpus()` (CPU信息), `os.totalmem()` (总内存) * **`events`**:Node.js的核心事件发射器,支持发布-订阅模式。 * **`stream`**:用于处理流式数据,如文件流、网络流。适合处理大文件或连续数据。 * **`process`**:提供当前Node.js进程的信息和控制方法,如`process.env` (环境变量), `process.argv` (命令行参数)。 ##### 2.2 模块管理与npm:Node.js的“基础设施” * **模块(Module)**: * Node.js使用**CommonJS模块规范**(`require()`和`module.exports`/`exports`)。 * 每个`.js`文件都被视为一个独立的模块。 * **示例**: * `my_module.js`: ```javascript // my_module.js function greet(name) { return `Hello, ${name}!`; } module.exports = { greet }; // 导出greet函数 ``` * `app.js`: ```javascript // app.js const myModule = require('./my_module'); // 导入模块 console.log(myModule.greet('Node.js Developer')); ``` * **npm(Node Package Manager)**: * 前面在前端课程中已经介绍过npm。在后端开发中,npm同样是管理第三方依赖(如Express、MongoDB驱动)的核心工具。 * **`package.json`**:所有Node.js项目都包含这个文件,用于记录项目信息、依赖、脚本命令等。 * **常用命令**: * `npm init -y`:快速初始化一个项目,生成`package.json`。 * `npm install <package-name>`:安装包并添加到`dependencies`。 * `npm install`:安装`package.json`中所有依赖。 * `npm start`:运行`package.json`中`scripts`字段的`start`脚本。 #### 三、Express框架基础:构建API的“骨架” 虽然Node.js内置的`http`模块可以创建服务器,但它过于底层,编写复杂应用会很繁琐。这时,就需要Web框架来简化开发。**Express**是Node.js最流行、最轻量级的Web开发框架,它提供了构建Web应用和API所需的核心功能。 ##### 3.1 Express简介 * **定位**:一个基于Node.js的**极简、灵活的Web应用框架**,专注于高效构建RESTful API和Web应用。 * **特点**: * **高度可扩展**:本身功能不多,但通过大量的**中间件(Middleware)**可以灵活地添加功能。 * **路由管理**:提供了清晰的路由定义方式。 * **请求/响应处理**:封装了HTTP请求和响应对象,简化了操作。 ##### 3.2 Express的核心概念 * **中间件(Middleware)**: * **含义**:Express应用程序中的**函数**,能够访问请求对象(`req`)、响应对象(`res`)和应用程序的请求/响应循环中的`next`中间件函数。 * **作用**:在请求到达最终路由处理函数之前或之后,对请求进行预处理或后处理。它们像一个处理请求的**函数链**。 * **常见用途**:日志记录、身份验证、解析请求体(JSON/表单)、CORS处理、会话管理、错误处理等。 * **比喻**:你有一个快递(请求),它需要经过安检(身份验证中间件)、称重(请求体解析中间件)、记录信息(日志中间件)等多个环节,才能最终送到收件人手里(路由处理函数)。 * **路由(Routing)**: * **含义**:根据HTTP请求的方法(GET, POST, PUT, DELETE等)和URL路径,将请求分派到对应的处理函数。 * **作用**:定义应用程序的URL结构和对应的业务逻辑。 * **请求/响应对象(`req`/`res`)**: * **`req` (Request)**:HTTP请求对象,包含了客户端发送的所有信息,如请求头、请求体、URL参数、查询参数等。 * **`res` (Response)**:HTTP响应对象,用于构建并发送HTTP响应给客户端,如设置状态码、响应头、发送响应体等。 ##### 3.3 创建Express项目 1. **初始化项目**: ```bash mkdir my-express-app && cd my-express-app npm init -y # 快速初始化 package.json npm install express # 安装Express框架 ``` 2. **编写入口文件 `app.js`**: ```javascript const express = require('express'); // 导入Express模块 const app = express(); // 创建Express应用实例 const port = 3000; // 定义端口号 // 定义一个简单的GET路由,当访问根路径时响应 app.get('/', (req, res) => { res.send('Hello, Express!'); // 向客户端发送响应 }); // 启动服务器并监听指定端口 app.listen(port, () => { console.log(`Express server running at http://localhost:${port}`); }); ``` 3. **运行项目**: ```bash node app.js ``` 然后在浏览器中访问`http://localhost:3000`,你将看到“Hello, Express!”。 #### 四、RESTful API设计:Web服务的“语言规范” 现代Web应用通常以后端提供**RESTful API**的形式进行数据交互。 ##### 4.1 RESTful API基本原则 * **资源(Resource)**:URL应该代表**资源**,而不是操作。资源通常是名词,而不是动词。 * **推荐**:`/users` (所有用户), `/users/123` (ID为123的用户) * **不推荐**:`/getAllUsers` * **统一接口(Uniform Interface)**:通过HTTP方法(动词)来表示对资源的操作。 * `GET`:从服务器**获取**资源(查询)。 * **示例**:`GET /users` (获取所有用户), `GET /users/123` (获取ID为123的用户) * `POST`:在服务器上**新建**资源。 * **示例**:`POST /users` (创建新用户) * `PUT`:**完整更新**资源(替换资源)。 * **示例**:`PUT /users/123` (完整更新ID为123的用户信息) * `PATCH`:**局部更新**资源(部分修改)。 * **示例**:`PATCH /users/123` (只修改ID为123用户的某个字段) * `DELETE`:从服务器**删除**资源。 * **示例**:`DELETE /users/123` (删除ID为123的用户) * **无状态(Stateless)**:每个来自客户端的请求都必须包含服务器处理该请求所需的所有信息。服务器不应该存储任何关于客户端的上下文信息。 * **优点**:简化服务器设计,提高可扩展性。 * **比喻**:每次你打电话给客服,都需要重新告诉他你是谁,要办什么事,而不是指望他记住你上次的通话内容。 * **层次化架构(Layered System)**:客户端与服务器通过中间层进行交互,如代理、网关等。 * **表现层状态转移(HATEOAS, Hypermedia as the Engine of Application State)**:通过超媒体(如响应中包含的链接)来驱动应用程序状态的改变。 ##### 4.2 Express路由与请求方法 在Express中,你可以使用`app.method(path, handler)`来定义路由。 ```javascript const express = require('express'); const app = express(); // 假设我们有一个简单的用户数据存储(实际应用中会是数据库) let users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ]; let nextId = 3; // 用于生成新的用户ID // GET /api/users - 获取所有用户 app.get('/api/users', (req, res) => { res.json({ code: 0, data: users, message: '获取用户列表成功' }); }); // GET /api/users/:id - 获取单个用户 app.get('/api/users/:id', (req, res) => { // 从 req.params 获取路径参数 const userId = parseInt(req.params.id); const user = users.find(u => u.id === userId); if (user) { res.json({ code: 0, data: user, message: '获取用户详情成功' }); } else { res.status(404).json({ // 设置HTTP状态码 code: 404, message: '用户未找到' }); } }); // POST /api/users - 创建新用户 // 需要中间件来解析请求体 (见下一节) // app.post('/api/users', (req, res) => { ... }); // PUT /api/users/:id - 更新用户 // app.put('/api/users/:id', (req, res) => { ... }); // DELETE /api/users/:id - 删除用户 // app.delete('/api/users/:id', (req, res) => { ... }); app.listen(3000, () => console.log('Server started on port 3000')); ``` ##### 4.3 请求参数与响应 * **路径参数(Path Parameters)**: * **获取**:通过`req.params`对象获取。例如,`/users/:id` 中的`id`会是`req.params.id`。 * **用途**:用于标识资源的特定实例。 * **查询参数(Query Parameters)**: * **获取**:通过`req.query`对象获取。例如,`/search?keyword=node&page=1` 中的`keyword`是`req.query.keyword`,`page`是`req.query.page`。 * **用途**:用于过滤、排序、分页等非资源标识的操作。 * **请求头(Request Headers)**: * **获取**:通过`req.headers`对象获取。例如,`req.headers['content-type']`。 * **用途**:包含客户端、请求类型、认证信息等元数据。 * **请求体(Request Body)**: * **获取**:对于`POST`、`PUT`、`PATCH`请求,数据通常放在请求体中。 * **处理**:默认Express无法直接解析请求体,需要使用**中间件**来解析。最常用的是`express.json()`来解析JSON格式的请求体。 ```javascript // 在所有路由之前使用中间件 app.use(express.json()); // 用于解析JSON格式的请求体 // POST /api/users - 创建新用户 app.post('/api/users', (req, res) => { // 从 req.body 获取请求体数据 const { name, email } = req.body; // 假设请求体是 { "name": "...", "email": "..." } if (!name || !email) { return res.status(400).json({ code: 400, message: '姓名和邮箱是必填项' }); } const newUser = { id: nextId++, name, email }; users.push(newUser); res.status(201).json({ // 201 Created,表示资源已成功创建 code: 0, data: newUser, message: '用户创建成功' }); }); ``` #### 五、中间件与错误处理:构建健壮API的“守护者” 中间件是Express的灵魂,它使得请求处理流程高度灵活和可插拔。 ##### 5.1 常用中间件 * **`express.json()`**:前面已经提到,用于解析JSON格式的请求体。 * **`express.urlencoded({ extended: true })`**:解析URL编码的请求体(如HTML表单提交的数据)。 * **`cors`**: * **含义**:第三方中间件,用于处理**跨域资源共享(CORS)**问题。当你的前端和后端部署在不同域名或端口时,浏览器会因为同源策略而阻止跨域请求。`cors`中间件可以设置响应头,允许特定来源的请求。 * **安装**:`npm install cors` * **使用**: ```javascript const cors = require('cors'); app.use(cors()); // 允许所有来源的跨域请求 (开发环境常用,生产环境应限制) // 或 app.use(cors({ origin: 'http://localhost:8080', credentials: true })); // 限制特定来源 ``` * **`morgan`**: * **含义**:第三方中间件,用于记录HTTP请求日志。 * **安装**:`npm install morgan` * **使用**:`app.use(morgan('dev'));` (在控制台输出简洁的开发日志) * **`helmet`**: * **含义**:第三方中间件,通过设置各种HTTP响应头来帮助**提高应用程序的安全性**。 * **安装**:`npm install helmet` * **使用**:`app.use(helmet());` (会设置X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security等安全头) ##### 5.2 自定义中间件 你可以编写自己的中间件函数,插入到请求处理流程中的任何位置。 * **语法**:中间件函数接收`req`, `res`, `next`三个参数。 * `next()`:调用`next()`函数,将请求传递给下一个中间件或路由处理函数。如果没有调用`next()`,请求就会在这里终止。 * **示例**:自定义一个日志中间件,记录请求时间和方法 ```javascript // 自定义日志中间件 app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { // 监听响应完成事件 const duration = Date.now() - start; console.log(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`); }); next(); // 将请求传递给下一个中间件或路由 }); ``` * **老师提示**:中间件的顺序很重要!它们会按`app.use()`的顺序依次执行。例如,`express.json()`通常放在所有路由之前,以便所有路由都能解析请求体。 ##### 5.3 错误处理中间件 * Express提供了特殊的错误处理中间件,它有四个参数:`(err, req, res, next)`。 * **作用**:集中捕获和处理路由或普通中间件中抛出的错误。 * **放置位置**:通常放在所有路由和普通中间件的**最后**。 * **示例**: ```javascript // ... 所有路由和普通中间件 ... // 错误处理中间件 (必须是四个参数) app.use((err, req, res, next) => { console.error(err.stack); // 打印错误栈到服务器控制台 res.status(500).json({ // 设置响应状态码为500 (Internal Server Error) code: 500, message: '服务端发生未知错误', error: err.message // 生产环境不建议直接暴露错误详情 }); // next(err); // 如果有多个错误处理中间件,可以继续传递 }); ``` * **抛出错误**:在路由或普通中间件中,可以通过`next(error)`将错误传递给错误处理中间件,或者直接`throw new Error(...)`(Express 5+支持,对于异步函数更方便)。 到这里,我们已经初步了解了Node.js和Express的基本概念、如何构建简单的API、以及中间件和错误处理的重要性。这些是构建任何后端服务的基石。 --- 好的,同学们,我们继续后端开发Node.js/Express基础的学习!上一节我们了解了Express框架、RESTful API设计以及中间件和错误处理。现在,我们将深入Node.js编程中最重要的特性——**异步编程模型**,并学习如何在Express中实现**身份认证与安全机制**。 异步编程是Node.js高性能的秘诀,但也是初学者最容易感到困惑的地方。掌握它,你才能真正写出非阻塞、高并发的Node.js应用。同时,任何Web应用都离不开用户认证和数据安全,我们将学习现代Web服务中最常用的认证方式——JWT。 --- #### 六、异步编程与Promise:Node.js高并发的“心脏” ##### 6.1 异步编程模型:非阻塞I/O的魔法 * **背景**: * **同步编程**:代码按顺序一行一行执行,如果遇到耗时操作(如文件I/O、网络请求、数据库查询),程序会阻塞,直到该操作完成才能继续执行下一行。这在高并发场景下会导致性能瓶颈,因为CPU会等待I/O操作完成。 * **异步编程**:发起耗时操作后,程序立即返回,继续执行后续代码,不阻塞主线程。当耗时操作完成后,通过回调函数或Promise通知主线程,执行后续处理。 * **Node.js的特点**:Node.js的核心API(`fs`, `http`, `net`, 数据库驱动等)几乎都是**异步非阻塞**的。 * **优势**: 1. **高并发**:Node.js可以处理大量的并发连接而不会创建大量的线程,因为它利用事件循环在单个线程中高效调度I/O操作。 2. **非阻塞**:I/O操作不会阻塞主线程,使得CPU可以持续处理其他请求或计算任务。 * **难点**: 1. **回调地狱(Callback Hell)**:早期异步编程的主要痛点。当有多个嵌套的异步操作时,代码会变得层层嵌套、难以阅读和维护。 2. **错误传递与捕获**:在回调函数链中,错误捕获和传递变得复杂。 ##### 6.2 Promise与async/await:异步编程的“救星” 为了解决回调地狱问题,JavaScript(以及Node.js)引入了`Promise`和`async/await`。 * **Promise(承诺)**: * **含义**:`Promise`是一个表示异步操作**最终完成(或失败)**的对象,以及其结果值。它有三种状态: 1. `pending` (进行中):初始状态,既不是成功也不是失败。 2. `fulfilled` (已成功):操作成功完成。 3. `rejected` (已失败):操作失败。 * **链式调用**:通过`.then()`方法处理成功结果,`.catch()`方法处理失败结果,可以避免回调嵌套。 * **示例**: ```javascript const fs = require('fs').promises; // Node.js的fs模块提供了Promise版本 function readFilePromise(filePath) { // fs.promises.readFile 返回一个Promise return fs.readFile(filePath, 'utf8'); } readFilePromise('config.json') .then(data => { console.log('文件内容:', data); return JSON.parse(data); // 可以继续返回Promise或普通值 }) .then(config => { console.log('配置对象:', config); // 更多异步操作... }) .catch(error => { // 捕获链中任何一个Promise的错误 console.error('操作失败:', error); }); ``` * **`async/await`(异步/等待)**: * **含义**:`async`和`await`是基于Promise的**语法糖**,它们让异步代码的编写和阅读变得更加直观,**看起来就像同步代码**一样。 * **`async`函数**:声明一个函数为异步函数。`async`函数总是返回一个`Promise`。 * **`await`表达式**:只能在`async`函数内部使用。它会暂停`async`函数的执行,直到`await`后面的`Promise`解析完成(成功或失败)。如果Promise成功,`await`会返回其解决值;如果失败,`await`会抛出异常。 * **错误处理**:`await`抛出的异常可以使用标准的`try...catch`语句捕获。 * **示例**: ```javascript const fs = require('fs').promises; async function processConfig() { try { // await会等待fs.readFile的Promise解析完成,然后将结果赋给data const data = await fs.readFile('config.json', 'utf8'); console.log('文件内容:', data); const config = JSON.parse(data); console.log('配置对象:', config); // 假设还有一个异步的数据库查询 // const dbResult = await db.query(config.dbConnection); // console.log('数据库结果:', dbResult); } catch (error) { console.error('处理配置失败:', error.message); } } processConfig(); // 调用异步函数 ``` * **Express中的异步路由处理函数**: * Express支持直接在路由处理函数中使用`async/await`,这使得后端API的逻辑编写更加简洁和清晰。 * **示例**: ```javascript app.get('/api/data', async (req, res) => { try { // 模拟异步数据库查询 const data = await new Promise(resolve => setTimeout(() => resolve({ value: '从数据库获取的数据' }), 1000)); res.json({ code: 0, data }); } catch (error) { res.status(500).json({ code: 500, message: '获取数据失败' }); } }); ``` #### 七、身份认证与安全机制:保护你的后端API 任何涉及到用户数据或敏感操作的Web应用,都必须实现身份认证和授权。 ##### 7.1 常见认证方式 1. **Session-based Authentication(基于Session的认证)**: * **原理**:用户登录成功后,服务器会生成一个唯一的**Session ID**,并将其存储在服务器端(如内存、Redis、数据库)。同时,将这个Session ID通过**Cookie**发送给客户端浏览器。客户端后续请求会携带这个Cookie,服务器通过Session ID查找对应的Session数据来识别用户。 * **特点**: * **有状态**:服务器需要存储和维护每个用户的Session状态。 * **安全性**:Session ID通常是随机的,但Session劫持风险存在。 * **跨域/扩展性**:在分布式系统或跨域应用中管理Session比较复杂。 * **应用**:传统多页应用。 2. **JWT (JSON Web Token) Authentication(基于JWT的认证)**: * **原理**:用户登录成功后,服务器不会存储Session。而是生成一个**JSON Web Token(JWT)**,其中包含了加密的用户信息(如用户ID、角色、过期时间)。服务器用一个密钥对JWT进行签名,然后将JWT发送给客户端。客户端后续每次请求时,都会在HTTP请求头(通常是`Authorization: Bearer <token>`)中携带这个JWT。服务器接收到请求后,会用自己的密钥**验证JWT的签名**是否有效,如果有效,则从JWT中解析出用户信息,从而识别用户。 * **特点**: * **无状态(Stateless)**:服务器无需存储Session,每次请求独立验证JWT即可。这使得API服务更易于扩展和实现负载均衡。 * **安全性**:JWT是签名的(通常是HMAC SHA256或RSA),不能被篡改。但如果JWT被截获,仍可能被冒用(直到过期),所以通常会设置较短的过期时间。 * **跨域/移动端友好**:JWT不依赖Cookie,非常适合前后端分离、移动App和微服务架构。 * **组成**:一个JWT通常由三部分组成,用`.`分隔: * **Header(头部)**:包含令牌类型(JWT)和所使用的签名算法(如HS256)。 * **Payload(载荷)**:包含实际的用户信息(如用户ID),以及一些标准声明(如`exp`过期时间、`iat`签发时间)。 * **Signature(签名)**:由Header、Payload和服务器密钥(Secret)使用指定算法生成的签名,用于验证JWT的完整性。 * **OAuth(开放授权)**: * **原理**:一种授权协议,允许用户授权第三方应用程序访问其在其他服务提供商上的信息,而无需共享密码。 * **用途**:常见的“使用GitHub/微信/QQ登录”功能。 * **比喻**:你授权一个APP(第三方应用)访问你的微信头像和昵称,但APP拿不到你的微信密码。 ##### 7.2 用户注册与登录流程:构建用户体系 1. **用户注册(Sign Up)**: * 客户端提交用户名、密码、邮箱等信息。 * 后端接收请求,对数据进行**验证**(格式、是否已存在)。 * **密码加密存储**:绝不能明文存储用户密码!通常使用**哈希算法加盐(Salt)**处理后存储,如`bcrypt`、`scrypt`、`Argon2`。 * 将用户信息(用户名、密码哈希)保存到数据库。 * 响应注册成功。 2. **用户登录(Login)**: * 客户端提交用户名、明文密码。 * 后端接收请求,根据用户名从数据库获取存储的密码哈希。 * 使用相同的哈希算法,对提交的明文密码进行哈希,然后**比对**与数据库中存储的哈希值是否一致。 * 如果一致,则认为认证成功。 * **发放凭证**: * **Session认证**:服务器生成Session ID并存储,将Session ID写入Cookie发送给客户端。 * **JWT认证**:服务器生成JWT,并将其作为响应体或HTTP头发送给客户端。 3. **鉴权(Authorization)**: * 客户端在后续访问**受保护路由(Protected Routes)**时,会携带认证凭证(Cookie中的Session ID或HTTP头中的JWT)。 * 后端在接收到请求后,通过中间件**验证凭证的有效性**(检查Session ID是否存在且未过期,或验证JWT签名、过期时间)。 * 如果验证通过,则允许请求继续访问后续路由处理函数;否则,返回`401 Unauthorized`(未授权)或`403 Forbidden`(无权限)。 ##### 7.3 JWT集成示例(Node.js/Express):构建无状态认证 我们需要用到`jsonwebtoken`库来生成和验证JWT。 * **安装**:`npm install jsonwebtoken` * **示例**: ```javascript const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); // 用于密码加密,需要 npm install bcrypt const app = express(); app.use(express.json()); // 用于解析请求体 // 定义一个密钥,用于JWT的签名和验证。生产环境务必使用复杂且安全的密钥,不要硬编码 const JWT_SECRET = process.env.JWT_SECRET || 'my_super_secret_key_123!@#'; // 假设一个简化的用户数据库 const usersDb = []; // 实际应用中会是数据库 // --- 用户注册路由 --- app.post('/api/register', async (req, res) => { const { username, password, email } = req.body; if (!username || !password || !email) { return res.status(400).json({ message: '用户名、密码和邮箱是必填项' }); } // 检查用户是否已存在 if (usersDb.find(u => u.username === username)) { return res.status(409).json({ message: '用户名已存在' }); } try { // 密码加盐哈希 const hashedPassword = await bcrypt.hash(password, 10); // 10是盐的轮数 const newUser = { id: usersDb.length + 1, username, password: hashedPassword, email }; usersDb.push(newUser); res.status(201).json({ message: '注册成功' }); } catch (error) { console.error('注册失败:', error); res.status(500).json({ message: '注册失败,请稍后再试' }); } }); // --- 用户登录路由 --- app.post('/api/login', async (req, res) => { const { username, password } = req.body; const user = usersDb.find(u => u.username === username); if (!user) { return res.status(400).json({ message: '用户名或密码错误' }); } try { // 比较密码哈希 const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ message: '用户名或密码错误' }); } // 登录成功,生成JWT const token = jwt.sign( { userId: user.id, username: user.username }, // Payload,包含用户信息 JWT_SECRET, // 签名密钥 { expiresIn: '1h' } // Token有效期,例如1小时 ); res.json({ message: '登录成功', token }); } catch (error) { console.error('登录失败:', error); res.status(500).json({ message: '登录失败,请稍后再试' }); } }); // --- JWT认证中间件 --- // 任何需要认证的路由,都可以在其前面加上这个中间件 function authenticateToken(req, res, next) { // 从请求头中获取 Authorization 字段 const authHeader = req.headers['authorization']; // 格式通常是 "Bearer TOKEN_STRING" const token = authHeader && authHeader.split(' ')[1]; if (token == null) { return res.status(401).json({ message: '未提供认证Token' }); // 401 Unauthorized } jwt.verify(token, JWT_SECRET, (err, user) => { if (err) { // err可能是 "TokenExpiredError" 或 "JsonWebTokenError" console.error('Token验证失败:', err.message); return res.status(403).json({ message: 'Token无效或已过期' }); // 403 Forbidden } req.user = user; // 将解析出的用户信息附加到req对象上 next(); // Token有效,继续处理请求 }); } // --- 受保护的API路由示例 --- app.get('/api/profile', authenticateToken, (req, res) => { // 只有通过authenticateToken中间件验证的请求才能到达这里 res.json({ message: '欢迎访问您的个人资料!', user: req.user // 访问通过Token解析出的用户信息 }); }); app.listen(3000, () => console.log('Server started on port 3000')); ``` **测试步骤**: 1. 安装依赖:`npm install express jsonwebtoken bcrypt` 2. 运行`node app.js`。 3. 用Postman或cURL测试: * **注册**:`POST http://localhost:3000/api/register`,请求体`{"username": "testuser", "password": "password123", "email": "test@example.com"}`。 * **登录**:`POST http://localhost:3000/api/login`,请求体`{"username": "testuser", "password": "password123"}`。你会得到一个`token`。 * **访问受保护资源**:`GET http://localhost:3000/api/profile`,在请求头中添加`Authorization: Bearer YOUR_TOKEN_STRING`(将`YOUR_TOKEN_STRING`替换为登录获取的实际token)。 到这里,我们已经深入学习了Node.js中的异步编程、Express框架的构建方式,以及如何在API中实现用户认证和授权。这些知识是构建任何现代Web应用后端的核心。 --- 好的,同学们,我们继续后端开发Node.js/Express基础的学习!上一节我们深入了解了Node.js的异步编程和Express中的身份认证机制。现在,我们将学习如何确保进入后端的数据是“干净”和“合法”的——**数据验证与错误处理**,以及如何处理网页中常见的**文件上传**和提供**静态资源服务**。 数据验证是后端安全的“第一道防线”,它能有效防止恶意输入、保证数据质量和业务逻辑的正确性。而文件上传和静态资源服务则是许多Web应用不可或缺的功能。 --- #### 八、数据验证与错误处理:后端API的“安检员”与“急救员” 在后端开发中,除了认证鉴权,对接收到的数据进行严格的验证和统一的错误处理也至关重要。 ##### 8.1 输入校验(Input Validation):数据的“合法性检查” * **重要性**: 1. **安全性**:防止各种攻击,如SQL注入、XSS、跨站脚本、命令注入等。恶意数据可能导致系统漏洞。 2. **数据质量**:确保数据符合业务规则和数据库约束。 3. **用户体验**:向前端返回明确的错误信息,帮助用户纠正输入。 4. **业务逻辑正确性**:防止无效数据进入业务处理流程,导致逻辑错误或崩溃。 * **校验时机**: * **客户端校验**:前端JavaScript进行初步校验,提供即时反馈,提升用户体验(但不可信,容易被绕过)。 * **服务端校验(必须且是核心)**:后端必须对所有接收到的输入进行严格验证,无论前端是否已经校验过。 * **校验内容**: * **类型**:是否是字符串、数字、布尔等。 * **格式**:是否符合邮箱、手机号、日期、URL等格式。 * **长度**:字符串的最小/最大长度。 * **范围**:数字的最小/最大值。 * **枚举值**:是否在允许的列表中(如性别:`male`/`female`)。 * **必填性**:是否是空值。 * **业务逻辑**:是否符合特定的业务规则(如库存不能为负、年龄不能超限)。 * **实现方式**: * **手动校验**:编写`if-else`语句进行逐个判断。 * **第三方库(推荐)**: * **`joi`**:强大的JavaScript对象模式描述语言和验证器。 * **`express-validator`**:一个基于`validator.js`的Express中间件,提供链式验证API。 * **`express-validator`示例**: 1. **安装**:`npm install express-validator` 2. **使用**: ```javascript const express = require('express'); const { body, validationResult } = require('express-validator'); // 导入校验函数 const app = express(); app.use(express.json()); // POST /api/register - 用户注册接口,带输入校验 app.post( '/api/register', [ // 校验用户名:不为空,长度在3-20之间 body('username') .notEmpty().withMessage('用户名不能为空') .isLength({ min: 3, max: 20 }).withMessage('用户名长度必须在3到20个字符之间'), // 校验邮箱:是合法邮箱格式 body('email') .isEmail().withMessage('邮箱格式不正确'), // 校验密码:不为空,长度至少6位 body('password') .notEmpty().withMessage('密码不能为空') .isLength({ min: 6 }).withMessage('密码长度至少为6位') ], (req, res) => { // 检查校验结果 const errors = validationResult(req); if (!errors.isEmpty()) { // 如果有校验错误,返回400 Bad Request 和错误信息 return res.status(400).json({ code: 400, errors: errors.array() }); } // 数据通过校验,执行业务逻辑 (例如保存用户到数据库) const { username, email, password } = req.body; console.log(`用户注册请求:用户名=${username}, 邮箱=${email}, 密码=${password}`); res.status(200).json({ code: 0, message: '注册请求已接收' }); } ); app.listen(3000, () => console.log('Validation server started on port 3000')); ``` * **测试**:发送一个不符合规则的POST请求到`/api/register`,例如`{"username": "ab", "email": "invalid-email"}`,会得到`400`状态码和详细的错误数组。 ##### 8.2 错误捕获与响应:统一的“错误报告” * **统一错误格式**: * 后端API应该返回统一的错误响应格式,以便前端或其他调用方能够清晰地解析和处理错误。 * **推荐格式**:`{"code": 错误码, "message": "错误描述", "data": null/详情}`。 * 例如:`{"code": 40001, "message": "用户输入参数不合法", "errors": [{"field": "username", "msg": "长度过短"}]}` * **集中式错误处理**: * 使用Express的错误处理中间件(`app.use((err, req, res, next) => { ... })`),集中捕获所有未被捕获的异常和错误。 * 在生产环境中,不要直接将`err.stack`等敏感信息暴露给客户端,只记录到服务器日志中。 * **示例**: ```javascript // ... 所有路由和中间件定义之后 ... // 假设这是一个通用的未找到路由处理 app.use((req, res, next) => { res.status(404).json({ code: 404, message: `API路径 '${req.originalUrl}' 未找到` }); }); // 错误处理中间件 (必须是最后定义的中间件,且有4个参数) app.use((err, req, res, next) => { console.error('全局错误捕获:', err.stack); // 打印错误堆栈到服务器日志 res.status(err.statusCode || 500).json({ // 优先使用错误对象上的 statusCode code: err.statusCode || 500, // 自定义错误码或默认500 message: err.message || '服务器内部错误' // 生产环境不暴露详细信息 // error_details: process.env.NODE_ENV === 'development' ? err.message : undefined // 开发环境才显示详情 }); }); ``` #### 九、文件上传与静态资源服务:Web应用的“附件管理”与“门面” ##### 9.1 文件上传:接收用户的“附件” * **原理**:Web中的文件上传通常通过HTML表单的`enctype="multipart/form-data"`来实现。当表单提交时,文件内容会以二进制流的形式发送到服务器。 * **Node.js处理**:Node.js原生处理`multipart/form-data`非常复杂,通常使用第三方中间件。 * **`multer`中间件(推荐)**: * **作用**:一个Node.js中间件,用于处理`multipart/form-data`类型的数据,主要用于文件上传。 * **安装**:`npm install multer` * **使用**: ```javascript const express = require('express'); const multer = require('multer'); // 导入multer const path = require('path'); const app = express(); // 配置multer存储设置 const storage = multer.diskStorage({ destination: function (req, file, cb) { // 设置文件上传的目录 cb(null, 'uploads/'); // 文件将保存在项目根目录下的 'uploads/' 文件夹 }, filename: function (req, file, cb) { // 设置文件名:原始文件名 + 时间戳 + 扩展名,确保唯一性 cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname)); } }); const upload = multer({ storage: storage }); // 创建multer实例,配置存储引擎 // POST /api/upload - 单文件上传接口 // upload.single('avatar'): 表示只接收一个名为 'avatar' 的文件字段 app.post('/api/upload', upload.single('avatar'), (req, res) => { if (!req.file) { return res.status(400).json({ message: '未选择文件' }); } console.log('文件已上传:', req.file); res.json({ message: '文件上传成功!', filename: req.file.filename, filepath: `/uploads/${req.file.filename}` // 返回可访问路径 }); }); // POST /api/upload-multiple - 多文件上传接口 // upload.array('photos', 10): 接收名为 'photos' 的多个文件字段,最多10个 // upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]) 接收不同字段名 app.post('/api/upload-multiple', upload.array('photos', 5), (req, res) => { if (!req.files || req.files.length === 0) { return res.status(400).json({ message: '未选择文件' }); } console.log('文件已上传:', req.files); res.json({ message: '多个文件上传成功!', files: req.files.map(file => ({ filename: file.filename, filepath: `/uploads/${file.filename}` })) }); }); // 为了让上传的文件能够通过HTTP访问,需要提供静态文件服务 // app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); app.listen(3000, () => console.log('File upload server started on port 3000')); ``` * **测试文件上传**:通常用Postman或Postman的替代工具,选择`form-data`模式,字段类型选择`File`。 ##### 9.2 静态文件服务:提供网页的“资源” * **含义**:指Web服务器直接提供给客户端的**不需要经过后端业务逻辑处理**的文件,如HTML文件、CSS文件、JavaScript文件、图片、字体等。 * **Express提供**: * **`express.static()`中间件**:Express内置的中间件,用于提供静态文件服务。 * **示例**: ```javascript const express = require('express'); const path = require('path'); // 用于处理路径 const app = express(); // 提供静态文件服务 // 当客户端请求 /public/index.html 时,实际上会查找项目根目录下 /static_assets/index.html app.use('/public', express.static(path.join(__dirname, 'static_assets'))); // 如果要提供项目根目录下的dist文件夹 (前端打包后的文件) // app.use('/', express.static(path.join(__dirname, 'dist'))); app.get('/', (req, res) => { res.send('访问 /public/your-file.html 或 /public/your-image.jpg'); }); app.listen(3000, () => console.log('Static server started on port 3000')); ``` * **注意**:在生产环境中,为了更好的性能和安全性,大型应用通常会将静态资源部署到专门的**CDN(内容分发网络)**或对象存储服务(如阿里云OSS、AWS S3)。 到这里,我们已经全面学习了Node.js/Express后端开发的关键技术点,包括异步编程、认证鉴权、数据验证、文件上传和静态资源服务。这些知识足以让你构建一个功能完善的后端API服务。 --- 好的,同学们,我们继续后端开发Node.js/Express基础的学习!至此,我们已经全面掌握了Node.js/Express的核心概念、异步编程、认证鉴权、数据验证、文件上传和静态资源服务。恭喜大家,你们已经具备了构建后端API服务的基础能力! 现在,我们将把这些知识串联起来,通过一个**RESTful项目实践**来整合所学,并探讨Node.js/Express在整个全栈开发学习路径中的**“数据大脑”作用**。 --- #### 十、RESTful项目实践:构建一个博客API系统 我们将通过构建一个经典的**博客API系统**,来整合所学的Node.js、Express、RESTful API设计、中间件、身份认证、数据验证等知识。 ##### 10.1 项目目标 * **目标**:实现一个具备核心功能的**博客文章API系统**。 * **核心功能(CRUD)**: * **用户管理**:用户注册、登录、获取个人资料。 * **文章管理**:创建文章、获取文章列表(支持分页)、获取文章详情、更新文章、删除文章。 * **安全机制**: * 用户认证:基于JWT实现用户登录和鉴权。 * 数据校验:对用户注册、文章创建等请求进行输入验证。 * **数据存储**: * 为了简化,初期可以使用内存数组模拟数据库存储。 * **进阶**:你可以对接真实数据库(如MongoDB或MySQL,这是下一阶段的课程)。 ##### 10.2 目录结构示例 一个组织良好的项目结构,有助于代码的模块化和可维护性。 ``` my-blog-backend/ ├── node_modules/ # npm安装的第三方依赖 ├── uploads/ # 文件上传目录(例如文章配图) ├── package.json # 项目配置文件 ├── .env # 环境变量文件(生产环境敏感配置) ├── app.js # 应用程序入口文件 ├── middleware/ # 自定义中间件 │ └── authMiddleware.js # 认证鉴权中间件 │ └── errorHandler.js # 错误处理中间件 ├── routes/ # 路由定义 (根据业务模块划分) │ ├── authRoutes.js # 认证相关路由 (注册、登录) │ ├── userRoutes.js # 用户相关路由 (获取个人资料) │ └── articleRoutes.js # 文章相关路由 (CRUD) ├── controllers/ # 业务逻辑处理 (控制器) │ ├── authController.js │ ├── userController.js │ └── articleController.js ├── models/ # 数据模型定义 (如果使用ORM/ODM) │ └── userModel.js │ └── articleModel.js └── README.md ``` ##### 10.3 关键功能代码示例(以文章管理为例) 我们将模拟一个文章的创建和获取列表功能。 **`models/articleModel.js`** (简化模拟数据库模型) ```javascript // models/articleModel.js // 实际生产中这里会使用Mongoose (MongoDB) 或 Sequelize/Prisma (MySQL/PostgreSQL) 来操作数据库 let articles = []; // 内存数组模拟文章存储 let articleNextId = 1; class Article { constructor(title, content, authorId, authorUsername, tags = []) { this.id = articleNextId++; this.title = title; this.content = content; this.authorId = authorId; // 作者ID,用于关联用户 this.authorUsername = authorUsername; // 方便前端显示 this.tags = tags; this.createdAt = new Date(); this.updatedAt = new Date(); } static findAll({ page = 1, limit = 10, tag = null, search = null }) { // 模拟分页、过滤 let filtered = articles; if (tag) { filtered = filtered.filter(article => article.tags.includes(tag)); } if (search) { const lowerSearch = search.toLowerCase(); filtered = filtered.filter( article => article.title.toLowerCase().includes(lowerSearch) || article.content.toLowerCase().includes(lowerSearch) ); } const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedArticles = filtered.slice(startIndex, endIndex); return { articles: paginatedArticles, total: filtered.length, page, limit }; } static findById(id) { return articles.find(article => article.id === id); } static create(articleData) { const newArticle = new Article( articleData.title, articleData.content, articleData.authorId, articleData.authorUsername, articleData.tags ); articles.push(newArticle); return newArticle; } static update(id, updateData) { const article = articles.find(a => a.id === id); if (article) { Object.assign(article, updateData); article.updatedAt = new Date(); return article; } return null; } static delete(id) { const initialLength = articles.length; articles = articles.filter(article => article.id !== id); return articles.length < initialLength; // 如果长度减少,说明删除了 } } module.exports = Article; ``` **`controllers/articleController.js`** (处理文章业务逻辑) ```javascript // controllers/articleController.js const Article = require('../models/articleModel'); // 导入文章模型 const { validationResult } = require('express-validator'); // 用于获取校验结果 // --- 创建文章 --- exports.createArticle = (req, res, next) => { // 检查请求体校验结果 const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ code: 400, message: '请求参数校验失败', errors: errors.array() }); } // 从 req.user 获取通过JWT解析出的用户信息 (在authMiddleware中注入) const { userId, username } = req.user; const { title, content, tags } = req.body; // 创建新文章实例 (调用模型方法) const newArticle = Article.create({ title, content, authorId: userId, authorUsername: username, tags: tags || [] }); res.status(201).json({ // 201 Created code: 0, message: '文章创建成功', data: newArticle }); }; // --- 获取文章列表 --- exports.getArticles = (req, res, next) => { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const tag = req.query.tag || null; const search = req.query.search || null; const { articles: paginatedArticles, total } = Article.findAll({ page, limit, tag, search }); res.json({ code: 0, message: '获取文章列表成功', data: { articles: paginatedArticles, total, page, limit, totalPages: Math.ceil(total / limit) } }); }; // --- 获取文章详情 --- exports.getArticleById = (req, res, next) => { const articleId = parseInt(req.params.id); const article = Article.findById(articleId); if (!article) { return res.status(404).json({ code: 404, message: '文章未找到' }); } res.json({ code: 0, message: '获取文章详情成功', data: article }); }; // --- 更新文章 --- exports.updateArticle = (req, res, next) => { const articleId = parseInt(req.params.id); const { title, content, tags } = req.body; const { userId } = req.user; // 验证用户权限 const article = Article.findById(articleId); if (!article) { return res.status(404).json({ code: 404, message: '文章未找到' }); } // 检查是否是文章作者本人 if (article.authorId !== userId) { return res.status(403).json({ code: 403, message: '无权修改此文章' }); } const updatedArticle = Article.update(articleId, { title, content, tags }); res.json({ code: 0, message: '文章更新成功', data: updatedArticle }); }; // --- 删除文章 --- exports.deleteArticle = (req, res, next) => { const articleId = parseInt(req.params.id); const { userId } = req.user; // 验证用户权限 const article = Article.findById(articleId); if (!article) { return res.status(404).json({ code: 404, message: '文章未找到' }); } // 检查是否是文章作者本人 if (article.authorId !== userId) { return res.status(403).json({ code: 403, message: '无权删除此文章' }); } const isDeleted = Article.delete(articleId); if (isDeleted) { res.status(204).json({ code: 0, message: '文章删除成功' }); // 204 No Content } else { res.status(500).json({ code: 500, message: '删除失败' }); } }; ``` **`routes/articleRoutes.js`** (定义文章路由) ```javascript // routes/articleRoutes.js const express = require('express'); const router = express.Router(); // 创建一个路由实例 const articleController = require('../controllers/articleController'); const authMiddleware = require('../middleware/authMiddleware').authenticateToken; // 导入认证中间件 const { body } = require('express-validator'); // 导入校验器 // --- 文章相关路由 --- // GET /api/articles - 获取文章列表 (无需认证) router.get('/articles', articleController.getArticles); // GET /api/articles/:id - 获取文章详情 (无需认证) router.get('/articles/:id', articleController.getArticleById); // POST /api/articles - 创建文章 (需要认证) router.post( '/articles', authMiddleware, // 先经过认证中间件 [ // 文章创建的输入校验 body('title') .notEmpty().withMessage('标题不能为空') .isLength({ min: 5, max: 100 }).withMessage('标题长度必须在5到100个字符之间'), body('content') .notEmpty().withMessage('内容不能为空') .isLength({ min: 20 }).withMessage('内容至少需要20个字符') ], articleController.createArticle ); // PUT /api/articles/:id - 更新文章 (需要认证) router.put( '/articles/:id', authMiddleware, [ body('title').optional().isLength({ min: 5, max: 100 }).withMessage('标题长度必须在5到100个字符之间'), body('content').optional().isLength({ min: 20 }).withMessage('内容至少需要20个字符') ], articleController.updateArticle ); // DELETE /api/articles/:id - 删除文章 (需要认证) router.delete('/articles/:id', authMiddleware, articleController.deleteArticle); module.exports = router; ``` **`middleware/authMiddleware.js`** (JWT认证中间件) ```javascript // middleware/authMiddleware.js const jwt = require('jsonwebtoken'); const JWT_SECRET = process.env.JWT_SECRET || 'my_super_secret_key_123!@#'; // 从环境变量获取密钥 exports.authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (token == null) { return res.status(401).json({ code: 401, message: '未提供认证Token' }); } jwt.verify(token, JWT_SECRET, (err, user) => { if (err) { console.error('Token验证失败:', err.message); return res.status(403).json({ code: 403, message: 'Token无效或已过期' }); } req.user = user; // 将解析出的用户信息附加到req对象上 (userId, username) next(); }); }; ``` **`app.js`** (应用程序入口文件) ```javascript // app.js const express = require('express'); const cors = require('cors'); // npm install cors const morgan = require('morgan'); // npm install morgan const helmet = require('helmet'); // npm install helmet const authRoutes = require('./routes/authRoutes'); const userRoutes = require('./routes/userRoutes'); const articleRoutes = require('./routes/articleRoutes'); const errorHandler = require('./middleware/errorHandler'); // 导入错误处理中间件 const path = require('path'); const app = express(); const port = process.env.PORT || 3000; // --- 常用中间件 --- app.use(express.json()); // 解析JSON格式的请求体 app.use(express.urlencoded({ extended: true })); // 解析URL编码的请求体 app.use(cors({ // 配置CORS,实际生产环境应限制origin origin: '*', // 允许所有来源 (开发环境用) // origin: 'http://localhost:8080', // 生产环境应指定前端域名 methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true // 允许携带cookie })); app.use(morgan('dev')); // HTTP请求日志 (开发模式) app.use(helmet()); // 安全HTTP头 // --- 静态文件服务 --- // 允许从 /uploads 路径访问 uploads 目录下的文件 app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // 例如,如果你想提供前端打包后的文件 // app.use('/', express.static(path.join(__dirname, '../frontend/dist'))); // --- 路由注册 --- app.use('/api', authRoutes); // 认证相关路由 (注册、登录) app.use('/api', userRoutes); // 用户相关路由 app.use('/api', articleRoutes); // 文章相关路由 // --- 未找到路由处理 (404) --- app.use((req, res, next) => { res.status(404).json({ code: 404, message: `API路径 '${req.originalUrl}' 未找到` }); }); // --- 全局错误处理中间件 (必须是最后定义的中间件) --- app.use(errorHandler); // --- 启动服务器 --- app.listen(port, () => { console.log(`Node.js Blog API server running at http://localhost:${port}`); }); // 模拟创建一些测试用户和文章 // (这部分代码仅为演示,实际应通过数据库初始化) const User = require('./models/userModel'); // 假设你也创建了userModel const bcrypt = require('bcrypt'); async function initData() { if (!User.findByUsername('testuser')) { const hashedPassword = await bcrypt.hash('password123', 10); User.create({ username: 'testuser', password: hashedPassword, email: 'test@example.com' }); console.log('Created testuser.'); } const testUser = User.findByUsername('testuser'); if (testUser && !Article.findById(1)) { Article.create({ title: '我的第一篇文章', content: '这是我的文章内容...', authorId: testUser.id, authorUsername: testUser.username, tags: ['Node.js', 'Express'] }); Article.create({ title: '第二篇关于Vue的文章', content: 'Vue是一个很棒的框架。', authorId: testUser.id, authorUsername: testUser.username, tags: ['Vue', '前端'] }); console.log('Created sample articles.'); } } initData(); ``` **运行项目**: 1. **初始化项目**: ```bash mkdir my-blog-backend && cd my-blog-backend npm init -y npm install express cors morgan helmet jsonwebtoken bcrypt express-validator ``` 2. 将上述所有代码文件按目录结构创建。 3. 运行:`node app.js` 4. 使用Postman或Insomnia测试API。 **测试API流程示例:** 1. **注册用户**:`POST /api/register`,body: `{"username": "youruser", "password": "yourpassword", "email": "your@example.com"}` 2. **登录用户**:`POST /api/login`,body: `{"username": "youruser", "password": "yourpassword"}` -> 获取JWT Token 3. **获取文章列表**:`GET /api/articles` (无需认证) 4. **创建文章**:`POST /api/articles`,headers: `Authorization: Bearer YOUR_JWT_TOKEN`,body: `{"title": "我的新博客", "content": "这是博客的内容...", "tags": ["tech", "nodejs"]}` 5. **获取文章详情**:`GET /api/articles/1` 6. **更新文章**:`PUT /api/articles/1`,headers: `Authorization: Bearer YOUR_JWT_TOKEN`,body: `{"title": "更新后的标题"}` 7. **删除文章**:`DELETE /api/articles/1`,headers: `Authorization: Bearer YOUR_JWT_TOKEN` 通过这个项目,你将亲身体验到后端API开发的完整流程,包括路由设计、控制器逻辑、认证鉴权和数据校验。 #### 十一、与全栈开发和后续课程的衔接:后端是“数据和逻辑的基石” Node.js/Express后端在整个全栈开发体系中扮演着核心角色,它直接处理数据和业务逻辑,并为前端提供服务。 * **前端(Vue/React等)通过AJAX/Fetch与Express API通信**: * 前端UI(我们用Vue构建的)通过HTTP请求与后端API进行数据交互,实现数据的增删改查。 * 后端提供标准的RESTful API接口,前端根据API文档进行调用。 * **比喻**:前端是顾客点的菜(需求),后端是厨房(业务逻辑处理),API就是菜单(接口),厨师(后端)根据菜单把菜做出来,服务员(前端)把菜端给顾客。 * **后端可对接数据库(如MongoDB、MySQL),实现数据持久化**: * 目前我们使用内存数组模拟数据库,但实际应用中,后端的核心任务是与数据库进行交互,实现数据的持久化存储。 * 这正是我们下一阶段即将深入学习的**数据库**课程。 * **支持JWT等现代认证机制,为后续权限、RBAC、OAuth扩展打基础**: * JWT认证的无状态特性是微服务架构的理想选择。 * 你可以基于用户ID和角色,进一步实现更细粒度的**权限控制(如RBAC,Role-Based Access Control)**和第三方登录(OAuth)。 * **文件上传、静态资源服务是Web开发的标准需求**: * 学会处理文件上传,你的应用就能支持用户头像、文章配图等功能。 * 静态资源服务是部署前端打包文件或图片资源的常见方式。 * **为后续学习数据库、NoSQL、DevOps、云服务等做好准备**: * 掌握后端开发,你将为学习如何选择、连接和优化数据库(SQL/NoSQL)打下基础。 * 后端服务是部署到云服务器、进行容器化(Docker/Kubernetes)和自动化运维(DevOps)的对象。 #### 十二、学习建议与扩展资源:持续精进 * **推荐文档**: * [Node.js官方文档](https://nodejs.org/zh-cn/docs/):了解Node.js核心API。 * [Express官方文档](https://expressjs.com/zh-cn/):学习Express的路由、中间件、API。 * [JWT官方网站](https://jwt.io/):了解JWT的原理和调试工具。 * **常用社区**:掘金Node.js专栏、SegmentFault、Stack Overflow。 * **推荐书籍**: * 《深入浅出Node.js》(朴灵):经典,深入Node.js底层。 * 《Node.js开发指南》(不老妖):入门到实战。 * 《Node.js实战》(Node.js in Action):项目实践。 * **实践**: * 动手实现自己的RESTful API,从小项目(如留言板、投票系统)开始。 * 使用Postman或cURL工具进行API测试,模拟各种请求和错误场景。 #### 十三、课后练习与思考:挑战你的后端技能 1. **实现一个简易留言板API**: * 使用Express,实现一个包含“获取留言列表”、“发表留言”、“删除留言”的API。留言数据可以存储在内存数组中。 * 要求:发表留言需包含“作者”和“内容”,内容不能为空。删除留言需要提供留言ID。 2. **完善博客API:用户权限**: * 在我们的博客API基础上,增加对文章的“更新”和“删除”操作进行**权限控制**。 * 只有文章的作者(`req.user.userId`与`article.authorId`匹配)才能修改或删除自己的文章。非作者尝试修改/删除时,返回`403 Forbidden`。 3. **API测试与错误排查**: * 用Postman或cURL,向你编写的API发送各种请求,包括: * 缺少必填参数的请求。 * 格式不正确的参数。 * 无效的Token。 * 尝试访问不存在的资源。 * 观察后端返回的状态码和错误信息是否符合预期。 4. **思考题**: * Node.js的单线程模型是如何应对高并发的?它与多线程(如Java Spring Boot)在高并发处理上有何异同? * 在Node.js中,你是如何处理CPU密集型任务(如复杂的图片处理、大量数据计算)而避免阻塞主线程的?(提示:`child_process`模块或工作线程`worker_threads`) * 请思考,为什么JWT认证是现代前后端分离应用的首选,它解决了Session认证的哪些痛点?
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章