编写搜索 Controller 用于搜索种子信息
在一个资源类站点或下载站中, 搜索功能几乎是核心入口 。无论是种子、文章还是商品,用户访问站点的第一诉求往往是「快速找到想要的内容」。本文将结合 Egg.js 框架,介绍一个 搜索 Controller 的完整实现思路 ,包括:
- 查询参数的标准化处理
- 调用搜索 Service 获取匹配结果
- 二次查询业务数据(种子信息)
- 搜索关键词高亮与 XSS 安全处理
- 分页导航与页面渲染
下面我们直接从代码入手。
搜索 Controller 代码实现
该 Controller 位于 app/controller/search.js ,核心职责是 协调搜索逻辑与页面渲染 ,而不是直接处理复杂业务。
/* eslint-disable prefer-const */
'use strict';
const Controller = require('egg').Controller;
module.exports = class extends Controller {
async index() {
const query = this.ctx.query;
query.page = Number(query.page || 1) || 1;
query.rows = Number(query.rows || 10) || 10;
query.word = this.ctx.service.search.filterWord(query.word) || '';
if (Number(query.type) === 0) query.type = '';
let result = { matches: [], words: [], total: 0, time: 0 };
if (query.word) result = await this.ctx.service.search.get(query);
let torrents = [];
if (result.matches.length) {
const ids = [];
for (let i = 0; i < result.matches.length; i++) ids.push(result.matches[i].id);
torrents = await this.ctx.service.torrent.get({ ids, rows: query.rows });
// 关键词替换
for (let i = 0; i < torrents.length; i++) {
// 1. 先替换内容里面的 尖括号
torrents[i].name = torrents[i].name.replace(/</g, '<').replace(/>/g, '>');
for (let f = 0; f < torrents[i].files.length; f++) {
torrents[i].files[f].name = torrents[i].files[f].name.replace(/</g, '<').replace(/>/g, '>');
}
// 2. 再替换关键词
for (let j = 0; j < result.words.length; j++) {
const word = result.words[j];
const reg = new RegExp(word, 'gi');
torrents[i].name = torrents[i].name.replace(reg, '<em>' + word + '</em>');
for (let f = 0; f < torrents[i].files.length; f++) {
torrents[i].files[f].name = torrents[i].files[f].name.replace(reg, '<em>' + word + '</em>');
}
}
}
}
const maxPage = Math.ceil(result.total / query.rows);
const pageArray = this.ctx.helper.pageNavi(query.page, 5, maxPage);
const pagePre = '/search';
let title = '站内搜索';
if (query.page) title = '第' + query.page + '页 - ' + title;
if (query.word) title = query.word + ' 的搜索结果 - ' + title;
await this.ctx.helper.renderView('search.ejs', {
torrents,
count: result.total,
page: query.page,
pageArray,
pagePre,
maxPage,
title,
type: this.app.config.site.type,
});
}
};查询参数的预处理与安全过滤
query.page = Number(query.page || 1) || 1;
query.rows = Number(query.rows || 10) || 10;这里对分页参数进行了 显式数值转换 ,可以避免字符串参与计算导致的问题,同时也防止非法参数直接影响逻辑。
query.word = this.ctx.service.search.filterWord(query.word) || '';搜索词在进入核心搜索逻辑之前,先通过 filterWord 进行过滤,这一步通常用于:
- 去除多余空格
- 过滤非法字符
- 防止搜索引擎异常或注入问题
这是一个 非常推荐放在 Service 层的通用处理逻辑 。
搜索与业务数据的解耦设计
result = await this.ctx.service.search.get(query);搜索 Service 返回的数据结构并不是完整的种子数据,而是类似:
matches:匹配到的 ID 列表words:实际参与搜索的关键词total:匹配总数time:搜索耗时
这种设计非常合理,因为:
- 搜索引擎(如 Sphinx / ES / Meilisearch)只负责“查 ID”
- 业务数据仍然以数据库为准
随后通过 ID 再去查询真正的种子信息:
torrents = await this.ctx.service.torrent.get({ ids, rows: query.rows });这是一种 典型的搜索系统分层设计 ,也方便后期替换搜索引擎实现。
XSS 防护与关键词高亮
搜索结果页最容易出现安全问题,尤其是 关键词高亮功能 。
torrents[i].name = torrents[i].name.replace(/</g, '<').replace(/>/g, '>');在插入 <em> 标签之前, 先转义尖括号 ,可以有效防止恶意 HTML 或脚本注入。
随后再进行关键词替换:
const reg = new RegExp(word, 'gi');
torrents[i].name = torrents[i].name.replace(reg, '<em>' + word + '</em>');这种处理顺序非常关键:
先转义内容 → 再插入安全的高亮标签
同样的逻辑也应用到了种子内部的文件名上,保证展示层的一致性与安全性。
分页导航与页面信息构建
const maxPage = Math.ceil(result.total / query.rows);
const pageArray = this.ctx.helper.pageNavi(query.page, 5, maxPage);分页逻辑被抽离到 helper 中,Controller 只负责传参与结果使用,保持了代码的清晰与可维护性。
页面标题也根据当前状态动态生成:
if (query.word) title = query.word + ' 的搜索结果 - ' + title;这不仅对用户友好,也有利于 SEO。
页面渲染与 Controller 的职责边界
最后通过:
await this.ctx.helper.renderView('search.ejs', { ... });将所有数据交给视图层,Controller 本身不关心页面结构,只关心 数据是否完整、是否安全 。
这正是 Egg.js 推荐的设计方式:
- Controller:流程调度
- Service:业务逻辑
- Helper:通用工具
- View:页面展示
总结
这个搜索 Controller 虽然代码不算复杂,但涵盖了一个真实项目中搜索功能的多个关键点:
- 搜索参数规范化
- 搜索引擎与业务数据解耦
- XSS 安全处理
- 关键词高亮
- 分页与 SEO 友好设计
在实际项目中,你可以在此基础上继续扩展,例如:
- 搜索历史与热门关键词
- 多字段权重搜索
- 搜索结果缓存
- 搜索耗时统计与监控
如果你正在使用 Egg.js 构建中大型应用,这种 Controller + Service 的搜索实现方式,值得作为一个稳定、可扩展的参考方案。




