编写种子搜索结果页面:基于 EJS + Vue 的混合渲染实践
在做资源类、搜索类网站时,「 搜索结果页 」几乎是用户停留时间最长、交互最密集的页面之一。它不仅承载着数据展示的职责,还需要支持排序、筛选、分页以及批量操作等复杂交互。

本文将结合一个实际的种子搜索结果页面,讲解如何使用 EJS 服务端模板 + Vue 客户端交互 的方式,构建一个功能完整、逻辑清晰、体验友好的搜索结果页。
一、整体页面结构设计
这个页面采用的是典型的 服务端渲染 + 前端增强 架构:
服务端(Node.js)
使用 EJS 渲染基础 HTML
负责数据查询、分页、首屏渲染
客户端(Vue + jQuery)
负责排序、筛选、批量操作等交互
通过修改 URL 参数刷新页面,实现“伪 SPA”体验
这种模式在 SEO 友好性、开发成本和可维护性之间取得了很好的平衡。
页面整体结构可以拆分为:
- 公共头部(
head.ejs、header.ejs) - 筛选与排序区域
- 搜索结果列表
- 分页组件
- 批量编辑工具栏
- 公共底部(
footer.ejs)
二、搜索筛选与排序区域
页面顶部是一个固定的筛选栏,支持 类型筛选 和 多维度排序 (大小、文件数、热度)。
<div class="filterWrap">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="filterItem">
<div class="dropdown">
<a class="filterItemALink" href="javascript:" data-toggle="dropdown">
{{ typeText }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li v-for="(item, index) in type" :key="index">
<a href="javascript:" @click="typeClick(item)">{{ item.label }}</a>
</li>
</ul>
</div>
</div>
...
</div>
</div>
</div>
</div>设计要点说明
类型筛选
类型数据由服务端注入(
JSON.stringify(type))Vue 只负责渲染和交互,不直接请求接口
排序逻辑
每个排序字段有三种状态:
auto→desc→asc任意一个排序生效时,其它排序自动重置,避免 SQL 组合过复杂
URL 驱动状态
排序和筛选都会反映到 URL 参数中
刷新页面或复制链接,状态可完整复现
这种“ URL 即状态 ”的设计,在搜索类页面中非常重要。
三、搜索结果列表渲染
搜索结果列表完全由服务端渲染,Vue 不参与 DOM 创建,只做事件增强。
<% if(torrents.length){ %>
<div class="torList">
<% torrents.forEach(function(item){ %>
<%-include('loop.ejs', { torrent: item })%>
<% }) %>
</div>
<%-include('pageNavi.ejs')%>
<% }else{ %>
<div class="empty">
<img src="/public/img/empty.png" />
<p>暂无数据</p>
</div>
<% } %>为什么这么做?
- 首屏渲染速度快
- 对搜索引擎友好
- 列表结构复杂时,EJS 比纯前端模板更直观
- 避免 Vue 接管整个列表带来的性能与状态同步问题
loop.ejs 中通常包含:
- 标题
- 种子大小
- 文件数量
- 热度 / 下载量
- 复选框(用于批量操作)
四、Vue 只负责“交互逻辑”
Vue 实例挂载在 #search 上,但它并不控制整个 DOM,而是:
- 管理排序状态
- 处理 URL 参数
- 控制批量操作逻辑
new Vue({
el: '#search',
data() {
return {
typeText: '',
typeVal: 0,
type: JSON.parse('<%-JSON.stringify(type)%>'),
size: 'auto',
file_count: 'auto',
hot: 'auto',
checkShow: false,
checkIds: [],
};
},
});URL 参数驱动页面刷新
sizeClick() {
if (this.size == 'auto') this.size = 'desc';
else if (this.size == 'desc') this.size = 'asc';
else if (this.size == 'asc') this.size = 'auto';
this.file_count = 'auto';
this.hot = 'auto';
let url = handleUrlParams(
location.href,
{ size: this.size },
['file_count', 'hot', 'page']
);
location.href = url;
}这里的关键点是:
- 不通过 Ajax 更新列表
- 而是通过修改 URL 触发服务端重新渲染
- 同时清理分页参数,避免排序切换后仍停留在旧页码
五、批量编辑与删除功能
页面右下角提供了一个简洁的“编辑模式”入口:
<div class="editBox">
<a href="javascript:" @click="checkShow=true">编辑</a>
<template v-if="checkShow">
<a href="javascript:" @click="checkAll">全选</a>
<a href="javascript:" @click="checkNone">取消</a>
<a href="javascript:" @click="deleteBatch">删除</a>
</template>
</div>批量删除实现思路
async deleteBatch() {
let that = this;
if (confirm('确认删除吗?')) {
loading('删除中');
let jom = $('.torItemCheck');
for (let i = 0; i < jom.length; i++) {
let obj = jom.eq(i);
if (obj.prop('checked')) {
await that.delete($(obj).val(), obj);
}
}
hideLoading();
}
}这里使用了 async + await 串行删除,优点是:
- 服务端压力可控
- 删除失败时容易定位
- 前端 DOM 可以逐条移除,反馈更直观
六、混合使用 Vue + jQuery 的取舍
你可能注意到,这个页面中 Vue 和 jQuery 是同时存在的 。这是一个非常现实的工程选择:
- Vue 负责状态与业务逻辑
- jQuery 负责 DOM 查询、事件代理和旧组件兼容
在 老项目渐进式改造 、或 非 SPA 页面 中,这种方式依然非常高效。
七、总结
这个搜索结果页的核心设计思路可以总结为:
- 服务端负责数据和结构
- 前端负责交互和状态
- URL 是唯一可信状态源
- Vue 不强行接管一切
如果你正在做的是:
- 种子搜索
- 资源聚合
- 日志查询
- 后台列表页
这种 EJS + Vue 的混合渲染模式 ,依然是一个成熟、稳定、易维护的选择。
后续你还可以进一步优化的方向包括:
- 排序状态视觉强化
- 批量操作权限控制
- 搜索条件缓存
- 前后端参数校验统一
希望这篇文章能对你构建复杂列表页面有所帮助。




