在数字化阅读日益普及的今天,市面上的阅读软件层出不穷。但有时,我们只需要一个纯粹、无广告、可私有部署,且能完美适配手机单手操作的阅读器。
今天,我将分享如何使用Node.js作为后端,结合EJS模板引擎和强大的EPUB.js库,构建一个支持书架管理、阅读进度同步、高亮笔记、护眼模式的 Web 版电子书阅读器。
✨ 项目亮点
这个项目不仅仅是一个简单的文件查看器,它经过了多次迭代优化,具备了以下核心特性:
- 极简书架:自动扫描后台文件夹,支持搜索,包含“最近阅读”和“收藏夹”功能。
- 沉浸式阅读:去除了所有多余元素,采用“羊皮纸”护眼色调,字体强制优化(解决 EPUB 自带样式乱码问题)。
- 单手操作优化:针对移动端设计,屏幕左右边缘点击翻页,中间区域用于长按选词。
- 无数据库设计:利用浏览器的
LocalStorage存储阅读进度、笔记和收藏状态,部署极其简单(即插即用)。 - 完整交互:支持底部滑块快速跳转、滑动翻页动画、高亮划线及笔记管理。
🛠 技术栈
- 后端:Node.js + Express (处理路由和文件扫描)
- 文件上传:Multer
- 模板引擎:EJS (服务端渲染 HTML 结构)
- 核心库:EPUB.js (解析和渲染电子书)
- 样式:原生 CSS3 (Grid 布局 + Flexbox)
- 图标库:LineIcons
🏗️ 核心实现解析
1. 后端:极简的文件服务
后端的逻辑非常轻量。我们不需要复杂的数据库,只需要扫描public/uploads文件夹下的.epub文件即可生成书架。
// app.js 核心逻辑app.get('/',async(req,res)=>{// 扫描文件夹letfiles=awaitfs.readdir(uploadDir);// 过滤 epub 文件letbooks=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功能,在电子书渲染时强制注入自定义样式。
// reader.ejsrendition.hooks.content.register(function(contents){constdoc=contents.document;consthead=doc.querySelector('head');consts=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事件(松手后)才真正执行跳转。
// 底部滑块逻辑优化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 读取本地数组,动态将喜欢的书克隆到“收藏区”。
// 笔记数据结构示例constnote={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限制书名最多显示两行,保持了界面的整洁统一。
🚀 如何运行本项目
- 初始化项目:
mkdirmy-reader&&cdmy-readernpminit-ynpminstallexpress multer ejs fs-extra - 创建文件结构:按照源码建立
app.js,views/,public/等目录。 - 启动:
nodeapp.js - 访问:
- PC 端访问
http://localhost:3000 - 手机端访问
http://你的电脑IP:3000(需在同一 WiFi 下)
- PC 端访问