搜索页面
本课属于 Coddy Node.js 学习旅程的“Express 基础”单元。第 23 课,共 23 课。
搜索页面是一个表单,服务器会像处理其他 GET 请求一样处理它。
<form action="/books" method="get">
<input name="author">
<button>Find</button>
</form>提交此表单会让浏览器跳转到 /books?author=,后面接着输入到名为 author 的字段中的内容。路由会从 req.query 中读取它。将这个词写回字段的 value,搜索后它就会继续显示在字段中;它来自访问者,所以要对它进行转义。
挑战
简单Recipe Box 新增了一个搜索页面。访客输入一个词,页面会列出标题中包含这个词的食谱。
- 在
server.js中,404 处理程序上方,添加一个异步的/search路由。它从req.query.q读取这个词;如果没有,就使用空字符串。 - 页面始终显示一个搜索表单:
<form action="/search" method="get">,其中有一个名为q的文本框和一个按钮。提交表单后,会打开/search?q=,后面接上这个词。 - 文本框会显示搜索的词:搜索
curry后,文本框中会保留curry。将这个词写入文本框的value属性,并先通过escapeHtml处理,因为它来自访客。 - 在表单下方,为
loadRecipes()中标题包含这个词的每个食谱列出一个链接,不区分大小写:soup和SOUP都能找到 Tomato Soup。每个链接都指向对应食谱的页面,并显示经过转义的标题,就像主页上的链接一样:<a href="/recipes/1">Tomato Soup</a>。 - 如果搜索了一个词,但没有标题包含它,就显示文本
No recipes found,而不是列表。
试试看:按“运行”并访问 /search:页面会显示空白表单。在文本框中输入 SOUP,然后按按钮。页面会列出 Tomato Soup,文本框中仍会显示 SOUP。搜索 pizza:显示 No recipes found。搜索 fish:会正确显示 Fish & Chips。然后向 /api/recipes 发送 POST 请求,内容为 {"title": "Pea Soup", "tags": ["soup"]},再搜索 soup:列表中也会出现 Pea Soup。
亲手试试
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') 会发送纯文本,因此每个标题都会单独显示在一行。
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');
});
Express 基础中的所有课
自己练习: Express 在线编辑器