Menu
CoddyTech

Trang tìm kiếm

Thuộc phần Kiến thức cơ bản về Express trong hành trình Node.js của Coddy. Bài học 23 trên 23.

Trang tìm kiếm là một biểu mẫu, và máy chủ xử lý nó như mọi yêu cầu GET khác.

<form action="/books" method="get">
    <input name="author">
    <button>Find</button>
</form>

Khi gửi biểu mẫu này, trình duyệt sẽ chuyển đến /books?author=, theo sau là nội dung đã nhập vào trường có tên author. Tuyến đọc giá trị này từ req.query. Ghi lại từ đó vào value của trường sẽ giúp nó vẫn hiển thị sau khi tìm kiếm. Từ này do khách truy cập nhập, vì vậy hãy escape nó.

challenge icon

Thử thách

Dễ

Recipe Box có thêm một trang tìm kiếm. Khách truy cập nhập một từ, và trang sẽ liệt kê các công thức có tiêu đề chứa từ đó.

  1. Trong server.js, phía trên trình xử lý 404, hãy thêm một tuyến bất đồng bộ cho /search. Tuyến này đọc từ đó từ req.query.q, hoặc dùng chuỗi rỗng nếu không có.
  2. Trang luôn có biểu mẫu tìm kiếm: <form action="/search" method="get"> với một trường văn bản có tên q và một nút. Khi gửi biểu mẫu, trang sẽ mở /search?q= theo sau là từ cần tìm.
  3. Trường này hiển thị từ đã tìm: sau khi tìm curry, trường sẽ chứa curry. Ghi từ đó vào thuộc tính value của trường sau khi truyền qua escapeHtml, vì từ này do khách truy cập nhập.
  4. Bên dưới biểu mẫu, hãy liệt kê liên kết cho mỗi công thức từ loadRecipes() có tiêu đề chứa từ cần tìm, không phân biệt chữ hoa chữ thường: cả soup và SOUP đều tìm thấy Tomato Soup. Mỗi liên kết dẫn đến trang công thức và hiển thị tiêu đề đã được escape, giống như trên trang chủ: <a href="/recipes/1">Tomato Soup</a>.
  5. Khi đã tìm một từ nhưng không có tiêu đề nào chứa từ đó, hãy hiển thị dòng chữ No recipes found thay cho danh sách.

Thử nhé: Nhấn Chạy rồi gửi /search: trang sẽ hiển thị biểu mẫu trống. Nhập SOUP vào trường rồi nhấn nút. Trang liệt kê Tomato Soup, và trường vẫn hiển thị SOUP. Tìm pizza: No recipes found. Tìm fish: Fish & Chips được hiển thị chính xác. Sau đó gửi POST {"title": "Pea Soup", "tags": ["soup"]} đến /api/recipes rồi tìm lại soup: Pea Soup cũng có trong danh sách.

Tự làm thử

import { escapeHtml } from './html.js';
import { dayOfYear } from './day-of-year.js';
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'));

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') gửi văn bản thuần, nên mỗi tiêu đề hiển thị trên một dòng riêng.
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 day = dayOfYear(req.query.date);
    if (day === null) {
        res.status(400).json({ error: 'Use a date like 2026-03-01' });
        return;
    }
    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');
});

Tất cả bài học trong Kiến thức cơ bản về Express

Tự luyện tập: Playground Express