Menu
CoddyTech

异步路由

本课属于 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 文件保存了四道食谱。

challenge icon

挑战

简单

服务器每次重启时,内存中的数组都会丢失。请改用文件保存食谱:data/recipes.json,其中已经存有四道食谱。

  1. 在 routes/recipes.js 中,从 node:fs/promises 导入 readFile 和 writeFile,并停止从 ../data.js 导入 recipes。
  2. 编写一个辅助函数 loadRecipes(),它是一个异步函数,用于读取 data/recipes.json 并返回解析后的列表。后续课程会用这个名称调用它。
  3. 将文件中的每个路由都设为 async,并让每个路由一开始就等待 loadRecipes():/tags、列表、单道食谱、POST 路由和份量调整路由。
  4. 在 POST 路由中,将新食谱添加到列表后,使用 writeFile 将整个列表以 JSON 文本的形式写回 data/recipes.json。
  5. 对于没有份数的食谱,份量调整路由仍会抛出错误,只是现在是在异步函数内部抛出。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;
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

Express 基础中的所有课

自己练习: Express 在线编辑器