编写种子搜索结果页面:公共模板与组件拆解实践
在上一篇文章中,我们已经完成了种子搜索结果页的整体结构和核心交互逻辑。但在一个稍微成规模的项目中,仅靠一个模板文件显然是不够的。

为了提高 代码复用性、可维护性和可读性 ,我们通常会将页面中可复用、职责单一的部分拆分成多个公共模板。本篇文章将围绕以下几个 EJS 文件,逐一讲解它们的设计思路与实现细节:
loop.ejs:种子列表项模板pageNavi.ejs:分页组件header.ejs:页面头部与导航footer.ejs:页面底部head.ejs:HTML 头部公共配置
一、loop.ejs:种子信息循环体
loop.ejs 是搜索结果列表中最核心的模板之一,用来描述 单条种子记录的展示结构 。
<div class="torItem">
<h3>
<input class="torItemCheck" value="<%-torrent.id%>" v-if="checkShow" type="checkbox" />
<a href="/torrent/<%-torrent.hash%>"><%-torrent.name%></a>
</h3>
<div class="torFiles">
<% torrent.files.forEach(function(file){ %>
<p>
<span class="torFileName"><%-file.name%></span>
<span class="torSize"><%-ctx.helper.formatBytes(file.length)%></span>
</p>
<% }) %>
</div>
<div class="torMeta">
<span v-if="!checkShow"><a href="magnet:?xt=urn:btih:<%-torrent.hash%>">磁力</a></span>
<span>大小:<%-ctx.helper.formatBytes(torrent.length)%></span>
<span>收录时间:<%-ctx.helper.date('Y-m-d H:i', torrent.add_date)%></span>
<span>文件数:<%-torrent.file_count%></span>
<span>热度:<%-torrent.download_count+1%>℃</span>
<span><a href="javascript:" class="deleteTorrent" data-id="<%-torrent.id%>">删除</a></span>
</div>
</div>设计要点解析
服务端数据直出
- 种子名称、文件列表、时间、大小等全部由服务端渲染
- 有利于 SEO,也减少前端状态管理复杂度
Vue 与 EJS 的协作
v-if="checkShow"控制是否显示复选框- Vue 不生成 DOM,只控制 显示与否 ,非常轻量
工具方法统一格式化
ctx.helper.formatBytes:统一文件大小显示ctx.helper.date:统一时间格式- 避免在模板中写逻辑判断或字符串拼接
磁力链接的合理隐藏
- 编辑模式下隐藏磁力链接,避免误操作
- 普通模式下直接暴露,提升使用效率
二、分页组件:经典而稳定的实现方式
分页是搜索结果页中必不可少的一部分,这里采用的是 服务端分页 + URL 参数驱动 的方式。
<ul class="pageNavi">
<li>共 <%-maxPage%> 页</li>
<!-- 上一页 -->
<% if(page>1){ %>
<li>
<a class="pageNavi-t" href="<%-ctx.helper.handleUrlParams(ctx.request.url, {page:page-1})%>">上一页</a>
</li>
<% } %> <!-- 中间循环的 -->
<% pageArray.forEach(function(item, index){ %>
<li>
<a
class="pageNavi-t <%-item==page?'current':'' %>"
href="<%-ctx.helper.handleUrlParams(ctx.request.url, {page:item})%>"
>
<%-item%>
</a>
</li>
<% }) %> <!-- 下一页 -->
<% if(maxPage>page){ %>
<li>
<a class="pageNavi-t" href="<%-ctx.helper.handleUrlParams(ctx.request.url, {page:Number(page)+1})%>">下一页</a>
</li>
<% } %>分页设计思路
- 分页状态完全由 URL 决定
- 切换页码不会丢失搜索条件、排序方式
handleUrlParams统一处理参数拼接,避免手写字符串错误pageArray由服务端控制页码范围,防止前端逻辑过重
每页条数切换
<select class="form-control" id="pageNaviSelect">
<option value="10" :selected="<%-ctx.query.rows%> == 10">每页 10 条</option>
<option value="20" :selected="<%-ctx.query.rows%> == 20">每页 20 条</option>
<option value="50" :selected="<%-ctx.query.rows%> == 50">每页 50 条</option>
<option value="100" :selected="<%-ctx.query.rows%> == 100">每页 100 条</option>
</select>这种方式简单直接,也非常符合传统搜索网站的用户习惯。
三、header.ejs:导航与搜索入口
header.ejs 负责站点顶部导航和全局搜索入口,是 用户使用频率最高的组件之一 。
<header>
<nav class="navbar navbar-default navbar-fixed-top">
...核心功能点
- 导航高亮
<li class="<%-ctx.request.url.split('?')[0]=='/'?'active':''%>">- 根据当前 URL 自动设置 active 状态
- 无需前端参与,逻辑直观
- 全局搜索框
<form action="/search">- GET 请求,天然支持 URL 分享
- 搜索关键词回填,增强体验
- 登录状态判断
<% if(ctx.session.user.id){ %>- 服务端判断用户状态
- 前端无需关心权限问题
四、footer.ejs:统一资源引入与版权信息
<footer>
<div class="copyright">
Copyright © <%-ctx.helper.date('Y')%> 磁力虫 All rights reserved.
</div>
</footer><script src="/public/js/jquery.min.js"></script>
<script src="/public/js/bootstrap.min.js"></script>
<script src="/public/js/clipboard.min.js"></script>
<script src="/public/js/vue.min.js"></script>
<script src="/public/js/script.js"></script>这样设计的好处
- JS 统一放在底部,避免阻塞渲染
- 公共脚本集中管理,方便维护
- 页面模板本身更干净
五、head.ejs:HTML 头部公共配置
<head>
<title><%-title%></title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/public/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="/public/css/style.css" />
<script src="/public/js/vue.js"></script>
</head>设计说明
title由服务端动态注入,利于 SEO- Bootstrap + 自定义样式分层清晰
- Vue 在 head 中提前引入,方便模板内直接使用指令
六、总结
通过这两个章节,我们实际上完成了一套 完整、可复用、工程化程度较高的搜索结果页模板体系 :
- 页面结构清晰
- 模板职责单一
- 服务端与前端边界明确
- 非常适合中小型内容站点或工具类网站
这种 EJS + Vue + jQuery 的组合,在今天看来并不过时,反而在很多 非 SPA 场景 中依然是性价比极高的选择。
在下一步,你可以继续优化的方向包括:
- 模板进一步组件化
- 权限与操作按钮控制
- 前后端渲染边界收敛
- 搜索结果缓存与性能优化
希望这篇文章能对你理解和组织服务端模板结构有所帮助。




