صفحة البحث
جزء من قسم أساسيات Express في رحلة Node.js على Coddy. الدرس 20 من 20.
صفحة البحث عبارة عن نموذج، ويستجيب لها الخادم كما يستجيب لأي طلب GET آخر.
<form action="/books" method="get">
<input name="author">
<button>Find</button>
</form>يؤدي إرسال هذا النموذج إلى توجيه المتصفح إلى /books?author= متبوعًا بما كُتب في الحقل المسمى author. يقرأ المسار القيمة من req.query. إن كتابة الكلمة مجددًا في value الخاص بالحقل تُبقيها ظاهرة بعد البحث؛ فهي واردة من الزائر، لذا عليك تهريبها.
التحدي
سهل- يعرض
GET /search?q=soupصفحةً تحتوي على نموذج GET (الحقلq، ويُرسل إلى/search) وروابط إلى العناوين التي تحتوي على الكلمة، بصرف النظر عن حالة الأحرف، منloadRecipes(). - لا توجد نتائج مطابقة:
No recipes found. - يحتفظ الحقل بالكلمة بعد تهريبها.
افتح /search في عرض الصفحة وابحث عن SOUP، ثم عن pizza.
جرّب بنفسك
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
import recipesRouter, { loadRecipes } 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'));
const escapeHtml = (text) =>
text
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
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('/api/today', async (req, res) => {
const date = req.query.date ? new Date(req.query.date) : new Date();
if (Number.isNaN(date.getTime())) {
res.status(400).json({ error: 'Use a date like 2026-03-01' });
return;
}
const day = Math.floor((date - Date.UTC(date.getUTCFullYear(), 0, 0)) / 86400000);
const recipes = await loadRecipes();
res.json(recipes[day % recipes.length]);
});
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');
});