معالج 404
جزء من قسم أساسيات Express في رحلة Node.js على Coddy. الدرس 16 من 23.
يعرض Express مسارًا غير معروف في صفحة خاصة به، وهي Cannot GET /x، لا تناسب موقعك ولا واجهة API الخاصة بك.
app.use((req, res) => {
res.status(404).send('No forecast here. Try /weather/oslo');
});تُنفَّذ البرمجية الوسيطة التي لا تحدد مسارًا مع كل طلب يصل إليها. ضعها بعد جميع المسارات وبعد express.static: فالطلبات التي عالجتها هذه العناصر لا تصل إليها، لذا لا ترى سوى الطلبات التي لم يعالجها أي شيء. وهي تعالج الطلب، لذا لا تحتاج إلى next.
يمثل req.path المسار من دون سلسلة الاستعلام، لذا يخبرك req.path.startsWith('/api/') ما إذا كان الطلب استدعاءً لواجهة API أم لصفحة.
التحدي
سهلالمسارات غير الموجودة تعرض صفحة Express العادية Cannot GET. استبدلها بصفحتك الخاصة: JSON لواجهة API، وصفحة ترحيبية للزوار.
- في
server.js، بعد كل المسارات وبعدexpress.static، أضف أخيرًاapp.useمن دون مسار. الطلبات التي سبق أن استجابت لها إحدى الجهات لا تصل إليه، لذا لن يرى إلا الطلبات التي لم يتعامل معها أي شيء. - عندما يبدأ المسار بـ
/api/، تكون الاستجابة برمز الحالة 404 وبـ JSON التالي:{"error": "Not found"} - بالنسبة إلى أي مسار آخر، تكون الاستجابة برمز الحالة 404 وبصفحة HTML تحتوي على العبارة
Page not found، مثل<h1>Page not found</h1>. وإضافة رابط للعودة إلى الصفحة الرئيسية لمسة لطيفة.
جرّب ذلك: اضغط «تشغيل» وأرسل /api/nope. ينبغي أن ترى رمز الحالة 404 و{"error":"Not found"}. أرسل /nope: رمز الحالة 404، معروضةً كصفحة تقول Page not found. أرسل /api/recipes/1: رمز الحالة 200 وTomato Soup، وهذا يوضح أن مساراتك ما زالت تأتي أولًا.
جرّب بنفسك
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.listen(3000, () => {
console.log('Recipe Box is running on port 3000');
});
جميع دروس أساسيات Express
تدرّب بنفسك: Playground لـ Express