错误处理
本课属于 Coddy Node.js 学习旅程的“Express 基础”单元。第 18 课,共 23 课。
路由抛出错误时,Express 会返回自己的错误页面。这个页面不是 JSON,而且会显示面向你的详细信息。
app.get('/api/fines/:member', (req, res) => {
throw new Error('Fines database is down');
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Try again later' });
});带有 4 个参数 (err, req, res, next) 的中间件是错误处理器。Express 会根据参数数量进行区分:有 3 个参数时,它是普通中间件,错误会跳过它。路由抛出错误时,Express 会跳过普通中间件,并将错误传给错误处理器。把它放在最后,也就是 404 处理器之后。把详细信息记录下来供自己查看,发送给客户端的消息则要简短。
throw new Error('text') 会停止路由并将错误交给 Express,调用 next(err) 也会有相同效果。访客发送了错误且能够自行修正时,返回 400。服务器出错时,则抛出错误。
挑战
简单继续完成 Recipe Box。你可以忽略单独的 practice-errors/ 文件;现在点击“运行”会启动项目的 server.js。
厨师想按比例调整食谱,例如为聚会准备双倍份量。没有保存份量的食谱无法按比例调整。这是数据中的问题,不是访客能修复的,因此会交给错误处理器,由它发送格式清晰的 JSON 500 响应。
- 在
server.js中的 404 处理器上方,为/api/recipes/:id/scale添加路由。它从查询字符串中读取倍数,例如/api/recipes/1/scale?by=2。 - 如果没有对应 id 的食谱,就以状态码 404 响应,并发送
{"error": "No such recipe"}。 - 将
by转换为数字。如果它不是数字,或者小于等于 0,就以状态码 400 响应,并发送{"error": "Scale by a positive number"}。Number('lots')的结果是NaN,Number.isNaN可以检测到它。 - 如果食谱没有
servings,就抛出一个新的Error,错误消息为Recipe 5 has no servings,其中的 5 要替换为该食谱的 id。 - 否则,使用 JSON 响应,内容仅包含标题和按比例调整后的份量。食谱 1 Tomato Soup 有 4 份,因此
/api/recipes/1/scale?by=2会发送{"title": "Tomato Soup", "servings": 8}。不要添加其他字段。 - 在文件的中间件末尾,也就是 404 处理器之后,添加错误处理器:一个
app.use,其函数接收四个参数(err, req, res, next)。它使用console.error输出错误,并以状态码 500 响应,发送{"error": "Something went wrong"}。
试试看:点击“运行”,然后发送 /api/recipes/1/scale?by=2。你应该会看到 {"title":"Tomato Soup","servings":8}。发送 /api/recipes/2/scale?by=lots:状态码为 400,并返回 {"error":"Scale by a positive number"}。发送 /api/recipes/99/scale?by=2:状态码为 404。现在选择 POST,并向 /api/recipes 发送 {"title": "Toast"}。它会成为食谱 5,但没有份量。发送 /api/recipes/5/scale?by=2:状态码为 500,并返回 {"error":"Something went wrong"},Console 中会显示错误 Recipe 5 has no servings。
亲手试试
import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
const app = express();
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.use((req, res, next) => {
res.set('X-Request-Id', randomUUID());
next();
});
app.use(express.json());
app.use(express.static('public'));
app.get('/', (req, res) => {
const items = recipes.map((r) => `<li><a href="/recipes/${r.id}">${escapeHtml(r.title)}</a></li>`);
res.send(`<link rel="stylesheet" href="/style.css"><h1>Recipe Box</h1><ul>${items.join('')}</ul>`);
});
// type('text') 会发送纯文本,因此每个标题各占一行。
app.get('/recipes', (req, res) => {
const { tag } = req.query;
const list = tag ? recipes.filter((recipe) => recipe.tags.includes(tag)) : recipes;
res.type('text').send(list.map((recipe) => recipe.title).join('\n'));
});
app.get('/recipes/:id', (req, res) => {
const recipe = recipes.find((r) => r.id === Number(req.params.id));
if (!recipe) {
res.status(404).send('No such recipe');
return;
}
res.send(recipe.title);
});
app.get('/api/recipes', (req, res) => {
res.json(recipes);
});
app.get('/api/recipes/: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);
});
app.post('/api/recipes', (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);
});
app.get('/about', (req, res) => {
res.send('Recipe Box collects family recipes.');
});
app.get('/menu', (req, res) => {
res.redirect(301, '/recipes');
});
app.get('/menu/:id', (req, res) => {
res.redirect(301, `/recipes/${req.params.id}`);
});
app.use((req, res) => {
if (req.path.startsWith('/api/')) {
res.status(404).json({ error: 'Not found' });
return;
}
res.status(404).send('<h1>Page not found</h1><p><a href="/">Back to Recipe Box</a></p>');
});
app.listen(3000, () => {
console.log('Recipe Box is running on port 3000');
});
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
Express 基础中的所有课
自己练习: Express 在线编辑器