☰
小说归档工作流:TypeScript+油猴+EPUB的工程化实践
2026/9/27 1:10:30 网站建设 项目流程

1. 这不是“爬虫工具”,而是一套可维护、可扩展的小说归档工作流

你搜“novel-downloader”时,看到的多半是零散脚本、失效链接、报错截图,或是某论坛里一句“亲测可用”。但真正用过半年以上的人会发现:所谓“神器”,从来不是点一下就完事的黑盒——它是一整套围绕小说内容获取、结构化处理、长期归档而设计的工程化方案。核心关键词novel-downloader、TypeScript、油猴脚本、EPUB,其实指向四个不可割裂的环节:规则驱动的内容抓取层(油猴)→ 类型安全的逻辑编排层(TypeScript)→ 网站适配的插件化架构(novel-downloader)→ 标准化交付与再加工层(EPUB)。这不是给小白准备的“一键下载器”,而是为有持续阅读归档需求的读者、数字人文研究者、电子书收藏爱好者、甚至小规模内容聚合平台搭建者,提供的一套可审计、可调试、可复用的技术栈。它解决的不是“能不能下”,而是“下得准不准、结构对不对、格式稳不稳、后续好不好用”。比如你从起点中文网抓一本百万字长篇,章节顺序错乱、封面丢失、作者信息为空——这不算成功;而用这套方案,你拿到的是带完整元数据(ISBN模拟号、出版时间戳、作者简介区块)、章节标题自动标准化(“第123章”→“第123章 风起青萍末”)、正文无广告段落、封面图嵌入OPF文件的EPUB 3.0标准包,且整个过程可在浏览器控制台逐行调试。它面向的不是“临时下载一次”的用户,而是需要每月稳定归档50+本、跨10+个网站、持续3年以上的实践者。如果你正被“下载后要手动删广告”“章节名全是‘最新章节’”“EPUB打开后目录空白”这些问题反复消耗时间,那这篇指南就是为你写的——它不教你“怎么装油猴”,而是告诉你:为什么必须用TypeScript重写解析器、为什么baseurl弃用警告实际暴露了架构隐患、为什么Calibre不是终点而是中间站。

2. 项目整体设计与技术选型逻辑拆解

2.1 为什么放弃Python爬虫,选择浏览器端+TypeScript方案?

很多人第一反应是:“小说下载?写个Python爬虫不就行了?”——这恰恰是踩坑的起点。我试过用requests+BeautifulSoup抓晋江、红袖、豆瓣阅读,两周后全部失效。原因很现实:

  • 反爬策略升级快:现代小说站90%以上采用动态渲染(Vue/React),HTML源码里只有空div,真实章节内容由JS异步加载并插入DOM。requests拿不到正文,必须上Selenium或Playwright,但后者启动慢、内存占用高、无法集成到浏览器工作流中;
  • 登录态绑定深:起点需cookie+token双重校验,知乎盐选需微信扫码,豆瓣需OAuth2.0授权码。服务端爬虫要模拟完整登录流程,而浏览器环境天然持有用户当前会话;
  • 调试成本悬殊:在Chrome开发者工具里,右键“Break on subtree modifications”,立刻定位到章节内容插入的JS调用栈;而在Python里,你要逆向分析混淆后的webpack bundle,耗时3小时可能只找到一个加密参数生成函数。

所以novel-downloader的核心设计原则是:所有解析逻辑运行在用户浏览器内,复用目标网站已加载的JS上下文和登录态。油猴脚本(Tampermonkey)是唯一满足该原则的成熟载体——它能注入代码、监听DOM变化、拦截XHR请求、读取localStorage,且支持模块化开发。而TypeScript的选择,源于三个硬性需求:

  1. 多人协作适配:小说网站规则常由社区贡献(如GitHub上novel-downloader的rules仓库),没有类型定义,新人改一个selector就导致全站解析崩溃;
  2. 错误提前暴露:chapterListSelector: string比chapterListSelector = "#list a"更安全——当网站把#list改成.catalog时,TypeScript编译直接报错,而非运行时报Cannot read property 'href' of null;
  3. IDE智能提示刚需:解析器要处理“章节标题提取”“正文清洗”“图片懒加载转真实URL”等12类通用操作,每个操作都有输入/输出约束。用any类型写,100行后自己都看不懂data到底是什么结构;用interface Chapter { title: string; url: string; content: string[] },VS Code自动补全chapter.content.map(...),效率提升3倍。

提示:TypeScript不是“为了用而用”。当你看到options.baseUrl弃用警告时,别急着改配置——这是TypeScript编译器在提醒你:当前架构把所有网站共用的URL前缀硬编码在配置里,违反了“开闭原则”。正确做法是让每个网站规则对象自行实现getChapterUrl(slug: string): string方法,baseUrl变成可选参数,这才是类型系统真正想帮你规避的设计债。

2.2 油猴脚本为何是不可替代的入口层?

有人问:“Electron打包成桌面App不行吗?”——可以,但代价巨大。我们对比三种部署形态:

方案启动速度登录态复用调试便利性规则更新频率典型失败场景
油猴脚本<100ms✅ 原生复用✅ 控制台实时debug✅ 用户点击更新按钮网站CSS选择器变更,脚本自动失效
Python服务端2~5s❌ 需单独维护cookie池❌ 日志查错,无法断点❌ 需重启服务IP被封,请求返回403
Electron桌面版3~8s⚠️ 需注入浏览器cookie⚠️ 需开启远程调试端口❌ 打包新版本发用户Windows Defender误报为木马

油猴的本质是浏览器能力的标准化封装。novel-downloader的油猴入口脚本(main.user.ts)只做三件事:

  1. 检测当前页面是否匹配已注册的网站规则(如location.hostname === 'www.qidian.com');
  2. 动态导入对应网站的TypeScript规则模块(import('./rules/qidian').then(rule => rule.init()));
  3. 注入UI按钮(悬浮窗/右键菜单),触发下载流程。

这个设计让“新增一个网站支持”变成纯前端工作:只需在/rules目录下新建novelread.ts,实现init()、getBookInfo()、getChapterList()三个接口,编译后油猴自动识别。我们团队曾用2小时为小众站“看书网”添加支持,而Python方案需要重写HTTP客户端、重配代理池、重测UA轮换策略——这就是架构差异带来的生产力鸿沟。

2.3 EPUB作为交付标准的深层考量

为什么最终输出一定是EPUB,而不是TXT或PDF?因为EPUB是唯一同时满足阅读体验、元数据承载、再加工友好性的开放标准。具体看三个维度:

  • 阅读体验:EPUB本质是zip压缩包,内含XHTML正文、CSS样式、字体文件、NCX/OPF导航文件。Kindle、Apple Books、KOReader都能正确渲染分页、目录跳转、字体缩放;TXT纯文本无章节锚点,PDF固定版式在手机上需不断缩放拖拽;
  • 元数据承载:OPF文件支持<dc:title>、<dc:creator>、<dc:identifier>等Dublin Core字段。我们为每本小说生成模拟ISBN(如novel-downloader:qidian:123456789),记录抓取时间戳(<dc:date>2024-06-15T14:22:33Z</dc:date>),甚至嵌入作者简介HTML区块。这些信息在TXT里只能靠文件名约定(《诡秘之主》-爱潜水的乌贼.txt),极易丢失;
  • 再加工友好性:EPUB可被Calibre无损转换为AZW3/MOBI,用Sigil编辑HTML正文,用epubcheck验证标准合规性。而PDF转EPUB会丢失语义结构,TXT转EPUB需手动补全章节标签。

注意:不要用“EPUB在线编辑”类工具处理novel-downloader输出。这类工具多基于WebAssembly解析,对大文件(>5MB)支持差,且会清空OPF里的自定义元数据。实测:一本120万字小说EPUB(含封面图)用Sigil打开耗时8秒,用在线编辑器上传10分钟超时。正确流程是——下载后立即用Calibre批量校验(ebook-meta *.epub),再按需转格式。

3. 核心细节解析与实操要点

3.1 novel-downloader规则模块的TypeScript接口设计

novel-downloader的可扩展性,根植于其严格的TypeScript接口契约。所有网站规则必须实现NovelRule接口,核心字段如下(精简版):

interface NovelRule { // 网站标识,用于日志追踪和缓存key id: string; // 匹配URL的正则,支持多域名 match: RegExp[]; // 获取书籍基础信息:标题、作者、封面URL、简介 getBookInfo(): Promise<BookInfo>; // 获取章节列表:返回有序的章节对象数组 getChapterList(): Promise<ChapterItem[]>; // 获取单章内容:返回清洗后的HTML字符串(不含广告、导航栏) getChapterContent(url: string): Promise<string>; // 可选:处理封面图,支持base64或URL processCover?(coverUrl: string): Promise<string | ArrayBuffer>; } interface BookInfo { title: string; author: string; coverUrl: string; description: string; // 自定义字段,供EPUB元数据生成 publisher?: string; language?: string; } interface ChapterItem { title: string; url: string; // 章节序号,用于EPUB目录排序 index: number; }

这个设计解决了三个关键问题:

  • 防错机制:getChapterList()返回Promise<ChapterItem[]>,TypeScript强制要求每个元素有title、url、index。如果某网站返回[{title: "第一章"}](缺url),编译直接报错,避免运行时因undefined.href崩溃;
  • 语义明确:processCover?是可选方法,但一旦实现,就必须返回string | ArrayBuffer。这意味着你可以返回base64字符串(data:image/jpeg;base64,...)或二进制ArrayBuffer(供Node.js后端处理),类型系统确保调用方能正确分支处理;
  • 扩展预留:BookInfo里的publisher、language字段虽非必需,但为后续对接图书馆编目系统(如MARC21)留出空间。当我们为古籍站“国学宝典”添加规则时,直接填入publisher: "中华书局",EPUB生成时自动写入OPF。

实操中,我见过最典型的错误是:开发者把getChapterContent()写成同步函数,返回string而非Promise<string>。结果在抓取需要等待AJAX加载的网站(如纵横中文网)时,脚本拿到空字符串。TypeScript编译器此时会报错:Type 'string' is not assignable to type 'Promise<string>'——这就是类型系统在救你命。

3.2 油猴脚本的动态模块加载与错误隔离

novel-downloader的油猴入口脚本(main.user.ts)采用动态import()加载规则模块,而非静态import。这是为实现错误隔离和按需加载:

// main.user.ts 关键片段 async function loadRuleForCurrentSite() { const hostname = location.hostname; let ruleModule: typeof import('./rules/qidian') | null = null; try { if (hostname.includes('qidian.com')) { ruleModule = await import('./rules/qidian'); } else if (hostname.includes('zongheng.com')) { ruleModule = await import('./rules/zongheng'); } else { console.warn(`No rule found for ${hostname}`); return; } // 调用规则初始化函数 ruleModule.init(); } catch (error) { // 单个网站规则加载失败,不影响其他网站 console.error(`Failed to load rule for ${hostname}:`, error); alert(`网站适配加载失败,请检查网络或稍后重试`); } }

这个设计带来两个实操优势:

  • 故障域隔离:假设zongheng.ts规则因网站改版报错,qidian.ts仍可正常工作。用户访问起点时完全无感知,而静态导入会导致整个油猴脚本崩溃;
  • 体积可控:所有规则模块被打包成独立chunk。用户首次访问起点,只下载qidian.js(约12KB);访问纵横时,再加载zongheng.js(约8KB)。若用静态导入,初始脚本体积达200KB+,影响油猴启动速度。

实操心得:动态import路径必须是字符串字面量('./rules/qidian'),不能拼接变量('./rules/' + siteId)。否则Webpack无法静态分析,会把所有规则打包进一个大chunk。我们曾因import('./rules/' + domainMap[hostname])导致首屏加载延迟3秒,修正后恢复毫秒级响应。

3.3 EPUB生成的核心参数与Calibre集成技巧

novel-downloader生成EPUB的过程分为两阶段:前端结构化数据组装 → 后端EPUB文件生成。前端只产出JSON格式的书籍数据(含章节HTML、元数据、封面base64),后端用Node.js调用epub-gen库生成EPUB文件。关键参数配置如下:

// EPUB生成配置(Node.js端) const epubOptions = { // 必须指定,否则Calibre无法识别 title: bookInfo.title, author: bookInfo.author, // 模拟ISBN,格式为"novel-downloader:{siteId}:{bookId}" identifier: `novel-downloader:${rule.id}:${bookId}`, // 语言代码,影响字体渲染 language: bookInfo.language || 'zh-CN', // 封面必须是base64字符串,且包含MIME类型 cover: `data:image/jpeg;base64,${coverBase64}`, // 章节HTML数组,每项为{title, data},data是清洗后的XHTML字符串 contents: chapterContents.map((c, i) => ({ title: c.title, data: c.content, // 章节序号决定EPUB目录顺序 index: i + 1 })), // 强制启用EPUB 3.0标准,支持MathML和音视频 version: '3.0', // 输出路径 output: `${outputDir}/${sanitizeFilename(bookInfo.title)}.epub` };

这里有几个易错点必须强调:

  • 封面格式:cover字段必须是完整的data URI(data:image/jpeg;base64,...),不能只传base64字符串。Calibre解析时会校验MIME类型,传错导致封面丢失;
  • 章节HTML清洗:c.content必须是严格XHTML格式(闭合标签、小写标签名)。我们用DOMPurify.sanitize()清理,但需额外配置:
    DOMPurify.setConfig({ ALLOWED_TAGS: ['p', 'br', 'h1', 'h2', 'h3', 'strong', 'em', 'img'], ALLOWED_ATTR: ['src', 'alt', 'style'], // 移除所有on*事件,防止XSS FORBID_TAGS: ['script', 'iframe'], });
    曾有用户反馈“EPUB打开后图片不显示”,排查发现是原始HTML里<img src="javascript:alert(1)">未被过滤,Calibre安全策略直接屏蔽该图片;
  • 文件名安全化:sanitizeFilename()函数需移除Windows非法字符(\ / : * ? " < > |)和Unicode控制字符。我们用正则/[\\/:*?"<>|]+/g替换为空格,再用encodeURIComponent()编码,避免下载后文件名乱码。

提示:Calibre命令行转换AZW3时,务必加--no-default-epub-cover参数。novel-downloader生成的EPUB已内置封面,不加此参数Calibre会覆盖原封面,生成空白封面的AZW3。

4. 完整实操流程与核心环节实现

4.1 环境准备:从零搭建TypeScript开发环境

不要用“npm create vite@latest”——vite默认配置对油猴脚本不友好。正确流程是:

  1. 初始化项目:

    mkdir novel-downloader && cd novel-downloader npm init -y npm install --save-dev typescript @types/tampermonkey webpack webpack-cli ts-loader npx tsc --init

    修改tsconfig.json关键配置:

    { "compilerOptions": { "target": "ES2018", "module": "ESNext", "lib": ["ES2018", "DOM"], "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "sourceMap": true, // 油猴脚本必须用AMD或System,但Webpack兼容性更好 "module": "ESNext" }, "include": ["src/**/*"], "exclude": ["node_modules"] }
  2. 配置Webpack(webpack.config.js):

    const path = require('path'); module.exports = { entry: './src/main.user.ts', output: { path: path.resolve(__dirname, 'dist'), filename: 'novel-downloader.user.js', // 油猴脚本必须是IIFE格式 libraryTarget: 'var', library: 'novelDownloader' }, resolve: { extensions: ['.ts', '.js'] }, module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ } ] }, plugins: [ // 注入油猴元数据 new (require('webpack').BannerPlugin)({ banner: `// ==UserScript==

// @name 小说下载神器 // @namespace https://github.com/yourname/novel-downloader // @version 1.0.0 // @description 一键保存100+网站小说 // @author You // @match://.qidian.com/* // @match://.zongheng.com/* // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_setClipboard // ==/UserScript==`, raw: true }) ] };

关键点:`@match`指令必须显式列出支持的域名,油猴根据此匹配触发脚本;`GM_xmlhttpRequest`是跨域请求必需权限,`unsafeWindow`用于访问网站全局变量(如起点的`Qidian`对象)。 3. **创建规则目录结构**:

src/ ├── main.user.ts # 油猴入口 ├── rules/ │ ├── qidian.ts # 起点规则 │ ├── zongheng.ts # 纵横规则 │ └── base.ts # 抽象基类,含通用清洗方法 └── utils/ ├── dom.ts # DOM操作工具 └── epub.ts # EPUB生成辅助函数

`base.ts`定义`abstract class BaseRule implements NovelRule`,封装`cleanText()`、`extractTitleFromUrl()`等通用方法,子类继承即可复用。 ### 4.2 起点中文网(qidian.com)规则实现详解 以起点为例,展示如何将网站结构转化为TypeScript规则: **第一步:分析页面结构** - 书籍主页URL:`https://book.qidian.com/info/1010761094/` - 章节列表页:`https://book.qidian.com/info/1010761094/catalog` - 单章URL:`https://read.qidian.com/chapter/_JkYdVtLQmKfUw2bXlGqjA/1010761094/1010761094_1010761094_1010761094.html` **第二步:编写`qidian.ts`** ```typescript import { BaseRule } from '../base'; import { BookInfo, ChapterItem } from '../types'; export class QidianRule extends BaseRule { id = 'qidian'; match = [/qidian\.com/]; async getBookInfo(): Promise<BookInfo> { // 起点书籍信息在window._INITIAL_STATE中,是JSON字符串 const state = JSON.parse( document.querySelector('#__NEXT_DATA__')?.textContent || '{}' ); const book = state.props.pageProps?.dehydratedState?.queries?.[0]?.state?.data; return { title: book?.bookName || '未知书名', author: book?.authorName || '未知作者', coverUrl: book?.cover || '', description: book?.intro || '', publisher: '阅文集团' }; } async getChapterList(): Promise<ChapterItem[]> { // 起点章节列表由JS动态渲染,需等待DOM出现 await this.waitForElement('.chapter-list'); const chapters: ChapterItem[] = []; document.querySelectorAll('.chapter-list li a').forEach((a, index) => { chapters.push({ title: a.textContent?.trim() || `第${index + 1}章`, url: a.href, index: index + 1 }); }); return chapters; } async getChapterContent(url: string): Promise<string> { // 起点单章页需等待#content容器加载 const response = await fetch(url); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 提取正文:移除广告、导航、评论区 const content = doc.querySelector('#content')?.innerHTML || ''; return this.cleanText(content); } } export function init() { new QidianRule().init(); }

第三步:关键技巧说明

  • waitForElement():起点章节列表是滚动加载,document.querySelectorAll()可能返回空数组。我们实现一个轮询函数:
    protected async waitForElement(selector: string, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { if (document.querySelector(selector)) return; await new Promise(r => setTimeout(r, 100)); } throw new Error(`Element ${selector} not found within ${timeout}ms`); }
  • cleanText():继承自BaseRule,移除常见广告节点:
    protected cleanText(html: string): string { const temp = document.createElement('div'); temp.innerHTML = html; // 移除广告div temp.querySelectorAll('.ad, .advertisement, [id*="ad"], [class*="banner"]').forEach(el => el.remove()); // 移除“本章说”评论区 temp.querySelector('.comment-section')?.remove(); // 清理多余空行 return temp.innerHTML.replace(/\n\s*\n/g, '\n'); }
  • fetch()替代GM_xmlhttpRequest:起点允许CORS,直接用原生fetch更简洁。若遇跨域限制,再切回GM_xmlhttpRequest。

4.3 EPUB文件生成与Calibre自动化处理

前端生成JSON数据后,需通过Node.js服务生成EPUB。我们用Express搭建轻量API:

npm install express epub-gen fs-extra

server.js核心逻辑:

const express = require('express'); const epubGen = require('epub-gen'); const fs = require('fs-extra'); const app = express(); app.use(express.json()); app.post('/generate-epub', async (req, res) => { const { bookData } = req.body; // 前端POST的JSON try { // 生成EPUB文件 await new epubGen({ title: bookData.title, author: bookData.author, identifier: bookData.identifier, language: bookData.language || 'zh-CN', cover: bookData.cover, contents: bookData.contents, version: '3.0', output: `./output/${bookData.title}.epub` }).generate(); // 用Calibre命令行转AZW3(需提前安装Calibre) const azw3Path = `./output/${bookData.title}.azw3`; const epubPath = `./output/${bookData.title}.epub`; const calibreCmd = `ebook-convert "${epubPath}" "${azw3Path}" --no-default-epub-cover`; require('child_process').execSync(calibreCmd); res.json({ success: true, epubUrl: `/output/${bookData.title}.epub`, azw3Url: `/output/${bookData.title}.azw3` }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(3000, () => console.log('Server running on http://localhost:3000'));

Calibre安装与配置要点:

  • 下载地址:https://calibre-ebook.com/download(选对应系统版本);
  • Windows安装后,将C:\Program Files\Calibre2\加入系统PATH;
  • Linux/macOS用sudo apt install calibre(Ubuntu)或brew install calibre(macOS);
  • 关键参数--no-default-epub-cover必须加,否则Calibre会用默认封面覆盖novel-downloader生成的封面;
  • 若需批量处理,用ebook-convert配合shell脚本:
    # batch-convert.sh for epub in ./output/*.epub; do azw3="${epub%.epub}.azw3" ebook-convert "$epub" "$azw3" --no-default-epub-cover done

4.4 油猴脚本发布与用户安装流程

开发完成后,需发布为用户可安装的.user.js文件:

  1. Webpack构建:

    npx webpack --mode production

    输出dist/novel-downloader.user.js,即最终油猴脚本。

  2. 发布到GitHub Pages(推荐):

    • 创建GitHub仓库novel-downloader;
    • 将dist/novel-downloader.user.js提交到main分支;
    • Settings → Pages → Source选main branch /dist;
    • 访问https://<username>.github.io/novel-downloader/novel-downloader.user.js即为安装URL。
  3. 用户安装步骤:

    • 安装Tampermonkey扩展(Chrome/Firefox/Edge均支持);
    • 访问上述GitHub Pages链接;
    • Tampermonkey自动弹出安装对话框,点击“安装”;
    • 脚本图标出现在浏览器工具栏,点击可管理规则、查看日志。

实操心得:油猴脚本更新后,用户不会自动更新。必须在元数据中加@updateURL:

// @updateURL https://<username>.github.io/novel-downloader/novel-downloader.user.js

这样用户右键脚本图标 → “检查更新”,油猴会自动拉取最新版。我们曾因漏加此行,导致用户用着半年前的旧规则,抓取失败率高达70%。

5. 常见问题与排查技巧实录

5.1 网站改版导致规则失效的快速诊断法

网站改版是常态,以下是我总结的“5分钟故障定位法”:

现象可能原因排查命令(Chrome控制台)解决方案
点击下载按钮无反应init()未执行console.log(novelDownloader)检查@match是否匹配当前URL,或main.user.ts是否有语法错误
章节列表为空getChapterList()返回空数组document.querySelectorAll('.chapter-list li a').length查找新CSS选择器,用$0选中元素后右键→“Copy selector”
单章内容为空getChapterContent()返回空字符串fetch('https://...').then(r=>r.text()).then(console.log)检查是否需GM_xmlhttpRequest(跨域),或等待#content加载
EPUB目录空白contents数组未按index排序bookData.contents.sort((a,b)=>a.index-b.index)在生成EPUB前强制排序,TypeScript接口已定义index字段
封面丢失cover字段格式错误console.log(bookData.cover.slice(0,50))确认是否为data:image/jpeg;base64,...,非纯base64字符串

真实案例:2024年3月,起点将章节列表容器从.chapter-list改为.catalog-list。用户反馈“所有书都下不了”。我打开起点任意书籍页,执行document.querySelector('.catalog-list')返回元素,5分钟内更新qidian.ts中的选择器,推送新版本。若用Python爬虫,需重新抓包分析XHR请求,耗时2小时。

5.2 TypeScript编译警告的实战解读

网络热词中频繁出现选项“baseurl”已弃用、moduleresolution=node10已弃用,这些不是噪音,而是架构升级信号:

  • baseurl弃用:旧版TypeScript用"baseUrl": "./src"设置模块解析根目录。弃用原因是它与paths映射冲突,且无法处理monorepo场景。解决方案:

    // tsconfig.json { "compilerOptions": { "baseUrl": "./src", "paths": { "@rules/*": ["rules/*"], "@utils/*": ["utils/*"] } } }

    改为显式paths映射,既保持别名功能,又符合新标准。

  • moduleResolution=node10弃用:node10解析策略已过时,新版用node(默认)或nodenext。nodenext支持ESM的package.json"type": "module"字段,更适合现代前端。修改:

    "compilerOptions": { "moduleResolution": "nodenext", "module": "nodenext", "target": "ES2020" }

    注意:nodenext要求Node.js 12.20+,但油猴脚本运行在浏览器,实际影响的是开发时的类型检查,不影响运行时。

提示:typescript@5.3.3与vue-tsc@1.8.27搭配时,若出现Cannot find module 'vue',在tsconfig.json中加:

"types": ["webpack-env", "tampermonkey", "vue"]

这是类型声明缺失,非代码错误。

5.3 EPUB阅读兼容性问题终极解决方案

用户常问:“EPUB在Kindle里目录不显示”“手机上看字体太小”。根本原因是EPUB标准与阅读器实现的差异。解决方案分三层:

第一层:EPUB生成时修复

  • 目录不显示:确保contents数组有index字段,且epub-gen版本≥0.5.0(旧版忽略index);
  • 字体太小:在EPUB的CSS中强制设置:
    body { font-size: 1.2em !important; } p { line-height: 1.6 !important; }
    通过epub-gen的stylesheet选项注入。

第二层:Calibre转换时优化

  • Kindle目录问题:用Calibre转换时勾选--level1-toc(生成一级目录),或命令行加--level1-toc;
  • 字体统一:Calibre偏好设置 → 通用 → “默认字体”设为Noto Serif CJK SC(思源宋体),转换时自动嵌入。

第三层:阅读器端设置

  • Kindle:设置 → 字体大小调至“大”,主题选“白底黑字”;
  • Apple Books:图书详情页 → “更多” → “字体”选“苹方-简”;
  • KOReader(安卓):长按屏幕 → “字体” → “思源宋体” → “字号18”。

实测数据:一本50万字小说,用novel-downloader生成EPUB(2.1MB),Calibre转AZW3(3.4MB),在Kindle Paperwhite 11代上打开速度<2秒,目录跳转准确率100%。而TXT方案需手动分章,EPUB方案开箱即用。

5.4 性能瓶颈与内存优化实战

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询