Express(一) ——简单入门
背景:参加的青训营项目,使用 Express 来实现后端,个人被分配到后端去。于是,简单速通了下 Express。项目结束,回头写下笔记,沉淀一下。
Express 是基于 Node.js 平台,快速、开放、极简的 Web 开发框架。
开始前可以先安装Postman,很好用的接口测试工具。
1. Hello World
首先,安装 express 到项目中 npm i express
然后,开始代码世界。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const express = require("express");
const app = express();
app.get("/", (req, res) => { res.send("Hello World!"); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
最后,命令行执行 nodemon app.js
或 node app.js
。nodemon
支持热更新。
2. 路由
路由是指服务器端应用程序如何响应特定端点的客户端请求。由一个 URI(路径标识)和一个特定的 HTTP 方法(GET、POST 等)组成的。
路由的定义结构:
1
| app.METHOD(PATH, HANDLER);
|
- app:express 实例
- METHOD:是一个 HTTP 请求方法
- PATH:服务端路径
- HANDLER:当路由匹配到时执行的处理函数。参数:
request
和 response
对象分别处理请求和响应数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const express = require("express");
const app = express();
app.get("/", (req, res) => { res.send("Hello World!"); });
app.post("/", (req, res) => { res.send("post /"); });
app.put("/user", (req, res) => { res.send("put user"); });
app.delete("/user", (req, res) => { res.send("delete user"); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
2.1 请求对象
req 对象代表 HTTP 请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const express = require("express");
const app = express();
app.get("/", (req, res) => { console.log("请求地址: ", req.url); console.log("请求方法: ", req.method); console.log("请求头: ", req.headers); console.log("请求参数: ", req.query);
res.end(); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
postman 测试用:http://localhost:8080/?name=clz
2.2 响应对象
res 对象表示收到 HTTP 请求后发送的 HTTP 响应。
2.2.1 状态码及状态信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const express = require("express");
const app = express();
app.get("/", (req, res) => { res.statusCode = 404; res.statusMessage = "test";
res.end(); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
2.2.2 发送多段文本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const express = require("express");
const app = express();
app.get("/", (req, res) => { res.write("hello "); res.write("world");
res.end(); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
2.2.3 cookie
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require("express");
const app = express();
app.get("/", (req, res) => { res.cookie("name", "clz");
res.end(); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
2.3 路由路径
可以使用正则表达式语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| app.get("/", function (req, res) { res.send("root"); });
app.get("/abc", function (req, res) { res.send("abc"); });
app.get("/test.text", function (req, res) { res.send("test.text"); });
app.get("/ab?cd", function (req, res) { res.send("ab?cd"); });
app.get("/ab*cd", function (req, res) { res.send("ab?cd"); });
app.get("/ab(cd)?e", function (req, res) { res.send("ab?cd"); });
app.get(/a/, function (req, res) { res.send("/a/"); });
app.get(/.*fly$/, function (req, res) { res.send("/.*fly$/"); });
|
2.4 动态路径
1 2 3 4 5 6 7 8
| app.get("/users/:userId/books/:bookId", function (req, res) { res.send(req.params); });
app.get("/:a(\\d+)", function (req, res) { res.send(req.params); });
|
3. 案例
创建一个简单的 CRUD 接口服务。增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)
- 查询任务列表:
GET /todos
- 根据 ID 查询单个任务:
GET /todos/:id
- 添加任务:
POST /todos
- 修改任务:
PATCH /todos
- 删除任务:
DELETE /todos/:id
3.1 路由设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const express = require("express");
const app = express();
app.get("/todos", (req, res) => { res.send("查询任务列表"); });
app.get("/todos/:id", (req, res) => { res.send(`根据ID查询单个任务, id是${req.params.id}`); });
app.post("/todos", (req, res) => { res.send("添加任务"); });
app.patch("/todos/:id", (req, res) => { res.send(`修改任务, id是${req.params.id}`); });
app.delete("/todos/:id", (req, res) => { res.send(`删除任务, id是${req.params.id}`); });
app.listen(8080, () => { console.log("http://localhost:8080/"); });
|
3.2 获取任务列表
数据文件 db.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { "todos": [ { "id": 1, "title": "express" }, { "id": 2, "title": "笔记" }, { "id": 3, "title": "更新博客" } ] }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| app.get("/todos", (req, res) => { fs.readFile("./db.json", "utf8", (err, data) => { if (err) { return res.status(500).json({ error: err.message, }); }
const db = JSON.parse(data); res.status(200).json(db.todos); }); });
|
3.3 根据 ID 查询单个任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| app.get("/todos/:id", (req, res) => { fs.readFile("./db.json", "utf8", (err, data) => { if (err) { return res.status(500).json({ error: err.message, }); }
const db = JSON.parse(data); const todo = db.todos.find( (todo) => todo.id === Number.parseInt(req.params.id) );
if (!todo) { return res.status(404).end(); }
res.status(200).json(todo); }); });
|
3.4 封装 db 模块
从上面的代码中可以发现,读取数据文件部分逻辑一样,即可以封装成单独的模块 db.js
db.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| const fs = require("fs"); const { promisify } = require("util"); const path = require("path");
const readFile = promisify(fs.readFile);
const dbPath = path.join(__dirname, "./db.json");
exports.getDb = async () => { const data = await readFile(dbPath, "utf8");
return JSON.parse(data); };
|
封装后的 app.js(后面的路由没有变化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const express = require("express");
const { getDb } = require("./db.js");
const app = express();
app.get("/todos", async (req, res) => { try { const db = await getDb(); res.status(200).json(db.todos); } catch (err) { res.status(500).json({ error: err.message, }); } });
app.get("/todos/:id", async (req, res) => { try { const db = await getDb(); const todo = db.todos.find( (todo) => todo.id === Number.parseInt(req.params.id) );
if (!todo) { return res.status(404).end(); }
res.status(200).json(todo); } catch (err) { res.status(500).json({ error: err.message, }); } });
|
3.5 添加任务
1 2 3 4 5 6
| app.post("/todos", (req, res) => { console.log(req.body);
res.end(); });
|
然后,会发现很恐怖的事情
那么,这个时候就需要配置表单请求体来解决上述问题
1
| app.use(express.json());
|
完美!!!(然而,并不是)
换种形式,就要换汤了。因为 express.json()只能解析 json 形式的
1
| app.use(express.urlencoded());
|
然后,因为需要保存到 db.json 中,所以也应该在 db.js 中封装一个 saveDb()方法(app.js 自然也要引入 saveDb,这部分就不行出来了)
db.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const fs = require("fs"); const { promisify } = require("util"); const path = require("path");
const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile);
const dbPath = path.join(__dirname, "./db.json");
exports.getDb = async () => { const data = await readFile(dbPath, "utf8");
return JSON.parse(data); };
exports.saveDb = async (db) => { const data = JSON.stringify(db); await writeFile(dbPath, data); };
|
添加任务的代码部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| app.post("/todos", async (req, res) => { try { const todo = req.body;
if (!todo.title) { return res.status(422).json({ error: "The field title is required.", }); }
const db = await getDb(); const lastTodo = db.todos[db.todos.length - 1];
todo.id = lastTodo ? lastTodo.id + 1 : 1; db.todos.push(todo);
await saveDb(db);
res.status(200).json(todo); } catch (err) { res.status(500).json({ error: err.message, }); } });
|
小优化:上面可以发现,添加任务后,db.json 格式很丑。其实就是把 JavaScript 对象转换为 JSON 字符串时的问题,所以只需要在JSON.stringify()
上下点功夫就行。
JSON.stringify()
1 2 3 4
| exports.saveDb = async (db) => { const data = JSON.stringify(db, null, " "); await writeFile(dbPath, data); };
|
眼尖的同学可能发现,添加的任务 id 和之前的位置不太一样。那么,有点小强迫症的我自然还是要在微操一手。
终于。。。
3.6 修改任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| app.patch("/todos/:id", async (req, res) => { try { const todo = req.body;
const db = await getDb(); const ret = db.todos.find( (todo) => todo.id === Number.parseInt(req.params.id) );
if (!ret) { return res.status(404).end(); }
Object.assign(ret, todo);
await saveDb(db); res.status(200).json(ret); } catch (err) { res.status(500).json({ error: err.message, }); } });
|
修改原有属性:
新增属性
3.7 删除任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| app.delete("/todos/:id", async (req, res) => { try { const todoId = Number.parseInt(req.params.id); const db = await getDb();
const index = db.todos.findIndex((todo) => todo.id === todoId);
if (index === -1) { return res.status(404).end(); }
db.todos.splice(index, 1); await saveDb(db); res.status(200).end(); } catch (err) { res.status(500).json({ error: err.message, }); } });
|
4. res.end()和 res.send()区别
官方说明:
4.1 res.end()
结束响应流程。用于在没有任何数据的情况下快速结束响应。
- 参数可以是 buffer 对象、字符串
- 只接受服务器响应数据,如果是中文会乱码
4.2 res.send()
发送 HTTP 响应。
- 参数可以是 buffer 对象、字符串、对象、数组
- 发送给服务端时,会自动发送更多的响应报文头,包括 Content-Type: text/html;charset=utf-8,所以中文不会乱码
res.send()发送对象响应
1 2 3 4 5 6 7 8 9 10 11 12 13
| const express = require("express");
const app = express();
app.get("/", (req, res) => { res.send({ name: "clz", }); });
app.listen(3000, () => { console.log("http://localhost:3000/"); });
|
改为用 res.end()发送
res.send()发送中文(使用浏览器查看,postman 可能自动设置了响应头)
**改为 res.edn()**:
学习参考视频:
Node.js 系列教程之 Express