在数字化阅读日益普及的今天,市面上的阅读软件层出不穷。但有时,我们只需要一个纯粹、无广告、可私有部署,且能完美适配手机单手操作的阅读器。
今天,我将分享如何使用 Node.js 作为后端,结合 EJS 模板引擎和强大的 EPUB.js 库,构建一个支持书架管理、阅读进度同步、高亮笔记、护眼模式的 Web 版电子书阅读器。
✨ 项目亮点
这个项目不仅仅是一个简单的文件查看器,它经过了多次迭代优化,具备了以下核心特性:
- 极简书架:自动扫描后台文件夹,支持搜索,包含"最近阅读"和"收藏夹"功能。
- 沉浸式阅读:去除了所有多余元素,采用"羊皮纸"护眼色调,字体强制优化(解决 EPUB 自带样式乱码问题)。
- 单手操作优化:针对移动端设计,屏幕左右边缘点击翻页,中间区域用于长按选词。
- 无数据库设计 :利用浏览器的
LocalStorage存储阅读进度、笔记和收藏状态,部署极其简单(即插即用)。 - 完整交互:支持底部滑块快速跳转、滑动翻页动画、高亮划线及笔记管理。
🛠 技术栈
- 后端:Node.js + Express (处理路由和文件扫描)
- 文件上传:Multer
- 模板引擎:EJS (服务端渲染 HTML 结构)
- 核心库:EPUB.js (解析和渲染电子书)
- 样式:原生 CSS3 (Grid 布局 + Flexbox)
- 图标库:LineIcons
🏗️ 核心实现解析
1. 后端:极简的文件服务
后端的逻辑非常轻量。我们不需要复杂的数据库,只需要扫描 public/uploads 文件夹下的 .epub 文件即可生成书架。
javascript
// app.js 核心逻辑
app.get('/', async (req, res) => {
// 扫描文件夹
let files = await fs.readdir(uploadDir);
// 过滤 epub 文件
let books = files.filter(f => f.toLowerCase().endsWith('.epub'));
// 简单的按文件名搜索
if (req.query.q) {
books = books.filter(book => book.toLowerCase().includes(req.query.q.toLowerCase()));
}
// 渲染 EJS 模板
res.render('index', { books, query: req.query.q });
});
这种设计意味着你可以直接通过 FTP 或系统文件管理器把几百本电子书丢进文件夹,刷新网页就能看。
2. 前端:解决"乱码"与"护眼"
很多 EPUB 电子书自带了千奇百怪的 CSS(比如黑色背景、极小的字体)。为了保证统一的阅读体验,我使用了 EPUB.js 的 hooks 功能,在电子书渲染时强制注入自定义样式。
javascript
// reader.ejs
rendition.hooks.content.register(function(contents) {
const doc = contents.document;
const head = doc.querySelector('head');
const s = doc.createElement('style');
// 强制覆盖样式,实现护眼模式
s.innerHTML = `
body {
font-family: 'Georgia', serif !important;
font-size: 19px !important;
line-height: 1.8 !important;
color: #4a3b2a !important; /* 深褐色文字 */
background-color: #f7f1e3 !important; /* 米黄色背景 */
padding: 0 10px !important;
text-align: justify !important;
}
img { max-width: 100% !important; }
`;
head.appendChild(s);
});
3. 交互:解决移动端痛点
在手机上开发 Web 阅读器最大的挑战是触摸冲突。我们需要区分"点击翻页"、"滑动翻页"和"长按选词"。
我的解决方案是:
- 左右边缘 20% :放置透明的
div(.nav-zone),点击触发展示上一页/下一页。 - 中间区域:留给用户直接操作文字(用于长按高亮)。
- 底部滑块 :为了防止滑块拖动时页面疯狂跳转,我们监听
input事件只更新数字,监听change事件(松手后)才真正执行跳转。
javascript
// 底部滑块逻辑优化
slider.addEventListener('input', (e) => {
// 拖动时只更新显示的百分比文字
document.getElementById('page-info').innerText = Math.floor(e.target.value / 10) + "%";
});
slider.addEventListener('change', (e) => {
// 松手后,才进行计算资源密集的跳转
if (book.locations.length() > 0) {
rendition.display(book.locations.cfiFromPercentage(e.target.value / 1000));
}
});
4. 数据存储:巧妙利用 LocalStorage
为了让项目部署极其简单(不需要配置 MySQL 或 MongoDB),所有的用户数据(收藏列表、阅读进度 CFI、笔记内容)都存储在浏览器的 localStorage 中。
- 进度记忆 :监听
relocated事件,将当前的 CFI (EPUB 的定位标识) 存入本地。 - 收藏夹:在首页通过 JS 读取本地数组,动态将喜欢的书克隆到"收藏区"。
javascript
// 笔记数据结构示例
const note = {
id: 172839201,
cfi: "epubcfi(/6/14[chapter1]!/4/2/1:0)", // 精确的定位
text: "被选中的原文内容",
note: "这是我的读后感...",
date: "2023/10/01"
};
// 存入 localStorage
🎨 UI 设计细节
为了达到"美观易用"的标准,CSS 方面做了很多细微的调整:
- Grid 布局书架 :使用了
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));,这使得书架在宽屏电脑和窄屏手机上都能自动完美排列。 - 层级管理 (Z-Index) :书架卡片上有一个"爱心"按钮。为了防止点击爱心时误触进入书籍,我们将爱心按钮的
z-index设为 10,将书籍链接层设为 1,完美分离了点击事件。 - 长标题处理 :使用
-webkit-line-clamp: 2限制书名最多显示两行,保持了界面的整洁统一。
🚀 如何运行本项目
-
初始化项目 :
bashmkdir my-reader && cd my-reader npm init -y npm install express multer ejs fs-extra -
创建文件结构 :按照源码建立
app.js,views/,public/等目录。 -
启动 :
bashnode app.js -
访问 :
- PC 端访问
http://localhost:3000 - 手机端访问
http://你的电脑IP:3000(需在同一 WiFi 下)
- PC 端访问
📝运行界面
