وصفة اليوم
جزء من قسم أساسيات Express في رحلة Node.js على Coddy. الدرس 22 من 23.
يمكن لـ Recipe Box استخدام مهارات مألوفة في المسارات وقراءة الملفات لتقديم وصفة واحدة لكل يوم. يحوّل المساعد dayOfYear المزوّد تاريخًا إلى رقم: 1 يناير هو 1، و1 مارس 2026 هو 60. إذا لم يُحدَّد تاريخ، فسيُستخدم تاريخ اليوم؛ أما النص الذي يتعذّر قراءته فيُرجع null.
const day = dayOfYear(req.query.date);
// ?date=2026-03-01 يُرجع 60
// ?date=soon يُرجع nullاستخدم عامل باقي القسمة لاختيار موضع: day % recipes.length. مع أربع وصفات، يختار اليوم 60 الموضع صفر. حمّل الوصفات داخل الطلب كي تُؤخذ الوصفات المنشورة حديثًا في الحسبان أيضًا. تحليل التاريخ متوفّر مسبقًا؛ ركّز على تركيب المسار.
التحدي
سهلأضف نقطة نهاية لوصفة اليوم إلى Recipe Box. يصدّر الملف المقدَّم day-of-year.js الدالة dayOfYear(text)، التي تُعيد رقم اليوم أو null للتاريخ غير الصالح.
- صدّر
loadRecipesمنroutes/recipes.js. استورده فيserver.js: يبقى الموجّه هو الاستيراد الافتراضي، ويتبعه{ loadRecipes }بعد فاصلة. - استورد
dayOfYear. فيserver.js، أضف مسارًا غير متزامنًا لـGET /api/todayفوق معالج 404. مرّرreq.query.dateإلى الدالة المساعدة. - عندما تكون القيمة
null، أعد الحالة 400 مع{"error":"Use a date like 2026-03-01"}. - وإلا، انتظر
loadRecipes()وأعدrecipes[day % recipes.length]بتنسيق JSON. أبقِ المسارات الحالية ومعالج الأخطاء كما هي.
جرّب GET /api/today?date=2026-03-01: ستُعاد الوصفة الأولى لأن رقم اليوم هو 60 ولأن عدد الوصفات أربع. جرّب ?date=soon: سيُعاد خطأ 400 المذكور أعلاه.
جرّب بنفسك
import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
import recipesRouter from './routes/recipes.js';
import reviewsRouter from './routes/reviews.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.use('/api/recipes', recipesRouter);
app.use('/api/recipes/:id/reviews', reviewsRouter);
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.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Something went wrong' });
});
app.listen(3000, () => {
console.log('Recipe Box is running on port 3000');
});
جميع دروس أساسيات Express
تدرّب بنفسك: Playground لـ Express