异步路由
本课属于 Coddy Node.js 学习旅程的“Express 基础”单元。第 21 课,共 23 课。
服务器重启后,数组就消失了。文件可以保留数据。
import { readFile } from 'node:fs/promises';
app.get('/api/forecast', async (req, res) => {
const text = await readFile('data/forecast.json', 'utf8');
res.json(JSON.parse(text));
});路由可以是一个使用 async 的函数,它会通过 await 等待结果,这里等待的是来自 node:fs/promises 的 readFile。Express 5 会监视路由返回的 Promise:如果它被拒绝,比如文件不存在或 JSON 格式错误,错误就会像抛出异常一样传给你的错误处理程序。
不使用 await 时,你拿到的是一个 Promise,而不是数据,此时 res.json(promise) 会发送 {}。新建的 data/recipes.json 文件保存了四道食谱。
挑战
简单服务器每次重启时,内存中的数组都会丢失。请改用文件保存食谱:data/recipes.json,其中已经存有四道食谱。
- 在
routes/recipes.js中,从node:fs/promises导入readFile和writeFile,并停止从../data.js导入recipes。 - 编写一个辅助函数
loadRecipes(),它是一个异步函数,用于读取data/recipes.json并返回解析后的列表。后续课程会用这个名称调用它。 - 将文件中的每个路由都设为
async,并让每个路由一开始就等待loadRecipes():/tags、列表、单道食谱、POST 路由和份量调整路由。 - 在 POST 路由中,将新食谱添加到列表后,使用
writeFile将整个列表以 JSON 文本的形式写回data/recipes.json。 - 对于没有份数的食谱,份量调整路由仍会抛出错误,只是现在是在异步函数内部抛出。Express 5 会像之前一样将该错误传给你的错误处理程序,因此仍会返回状态码 500 和
{"error": "Something went wrong"}。不要在路由中捕获这个错误。
试试看:点击“运行”并发送 /api/recipes:会得到相同的四道食谱,现在它们是从文件中读取的。选择 POST,并向 /api/recipes 发送 {"title": "Gazpacho", "tags": ["vegan", "soup"]}:返回状态码 201 和这道食谱,其中 "id": 5。发送 /api/recipes/5:会得到 Gazpacho。发送 /api/recipes/99:返回状态码 404。然后发送 POST {"title": "Toast"},它没有份数,会成为第 6 道食谱;再发送 /api/recipes/6/scale?by=2:返回状态码 500 和 {"error":"Something went wrong"}。如果你看到的是 {},而不是你的数据,就说明缺少了一个 await。
亲手试试
import express from 'express';
import { recipes } from '../data.js';
const router = express.Router();
// 每个标签只出现一次,按字母顺序排列。
router.get('/tags', (req, res) => {
const tags = new Set(recipes.flatMap((recipe) => recipe.tags));
res.json([...tags].sort());
});
router.get('/', (req, res) => {
res.json(recipes);
});
router.get('/:id', (req, res) => {
const recipe = recipes.find((r) => r.id === Number(req.params.id));
if (!recipe) {
res.status(404).json({ error: 'No such recipe' });
return;
}
res.json(recipe);
});
router.post('/', (req, res) => {
const { title, tags } = req.body;
if (!title) {
res.status(400).json({ error: 'A recipe needs a title' });
return;
}
const recipe = { id: recipes.length + 1, title, tags: tags ?? [] };
recipes.push(recipe);
res.status(201).json(recipe);
});
router.get('/:id/scale', (req, res) => {
const recipe = recipes.find((r) => r.id === Number(req.params.id));
if (!recipe) {
res.status(404).json({ error: 'No such recipe' });
return;
}
const by = Number(req.query.by);
if (!Number.isFinite(by) || by <= 0) {
res.status(400).json({ error: 'Scale by a positive number' });
return;
}
if (!recipe.servings) {
throw new Error(`Recipe ${recipe.id} has no servings`);
}
res.json({ title: recipe.title, servings: recipe.servings * by });
});
export default router;
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
Express 基础中的所有课
自己练习: Express 在线编辑器