资讯详情

Readest 修复 OPDS 2.0 JSON 目录搜索框置灰:RFC 6570 模板链接识别与展开实战

📅 2026/9/21 18:29:59 | 华诺云谱 👁 阅读
Readest 修复 OPDS 2.0 JSON 目录搜索框置灰:RFC 6570 模板链接识别与展开实战
桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载导读本文围绕 Readest 开源电子书阅读器在 issue #4502 中暴露的一个真实缺陷展开当用户添加的是OPDS 2.0 JSON 格式的书目源如type: application/opdsjson、templated: true、href: /opds/search{?query}时OPDS 浏览器顶部的导航栏搜索输入框会呈现灰色禁用状态输入任何关键词都被拒绝。文章将完整还原问题根因、三层修复方案MIME 常量与类型扩展、isSearchLink判定、expandOPDSSearchTemplate模板展开、一个极易踩坑的 URL 解析顺序陷阱以及配套的单元测试证据。读完本文你将掌握如何在 OPDS 客户端中正确识别并消费 OPDS 2.0 基于 RFC 6570 URI Template 的搜索链接并理解为何先展开模板、再解析相对 URL是不可颠倒的调用顺序。问题现场OPDS 2.0 JSON 目录的搜索框为何置灰OPDS 规范存在两个生态并存的版本基于 Atom XML 的 OPDS 1.x/2.0 XML以及基于 JSON 的 OPDS 2.0application/opdsjson。许多现代书目服务器如 Calibre-Web、Komga 等倾向于输出 JSON 目录。这些 JSON 目录通常以下列形态暴露搜索能力{ metadata: { title: My Catalog }, links: [ { rel: search, type: application/opdsjson, templated: true, href: /opds/search{?query} } ], publications: [] }注意这里的href并不是一个可直接请求的 URL而是一个符合 RFC 6570 的URI Template/opds/search{?query}中的{?query}是占位符需要由客户端在发起请求前用真实的查询词替换。在修复前的 Readest 中这类目录的表现是导航栏的搜索输入框被disabled渲染用户无法输入任何内容。从 opds/page.tsx 可以看到搜索框的可用性完全由一个布尔值驱动const hasSearch useMemo(() { return !!state.feed?.links?.find(isSearchLink); }, [state.feed]);hasSearch为false时Navigation组件就会渲染input disabled{!hasSearch}。因此问题被精确锁定在isSearchLink这一判定函数上它没有把 OPDS 2.0 JSON 的 templated 搜索链接识别为搜索链接。根因分析isSearchLink只认两种 MIME 类型修复前的isSearchLink实现位于 opdsUtils.ts逻辑只覆盖两种类型的链接application/opensearchdescriptionxmlOpenSearch 描述文档对应 OPDS 1.x 常见的link relsearch typeapplication/opensearchdescriptionxml href.../application/atomxmlATOM 类型搜索模板形如hrefsearch?q{searchTerms}。而 OPDS 2.0 JSON 目录把搜索链接声明为type: application/opdsjson且带templated: true两者都不在isSearchLink的匹配范围内于是hasSearch判定为false导航栏搜索输入框被置灰即便绕过 UI 强行触发handleSearch其内部也只处理 OPENSEARCH 与 ATOM 两种类型JSON 路径依旧无输出。这就是搜索框置灰 拒绝查询双重故障的完整链条。修复方案类型扩展、判定升级与模板展开三管齐下修复涉及三个文件类型定义、工具函数与页面逻辑。1. 类型层为OPDSBaseLink增加templated字段types/opds.ts 中的OPDSBaseLink接口新增了可选的templated?: boolean字段export interface OPDSBaseLink { rel?: string | string[]; href?: string; type?: string; title?: string; /** OPDS 2.0 / RFC 6570: href is a URI template (e.g. /search{?query}). */ templated?: boolean; }该字段专门承载 OPDS 2.0 规范中templated的语义当它为true时href是一个 RFC 6570 URI 模板而非静态 URL。值得注意的是OPDS 2.0 是纯 JSON 格式——XML 版本的getFeed解析结果里链接永远不会有templated标记因此这个字段天然只出现在 JSON 路径中。2. 判定层isSearchLink接受 OPDS2 模板链接opdsUtils.ts 的MIME常量表新增了 OPDS2 条目export const MIME { XML: application/xml, ATOM: application/atomxml, XHTML: application/xhtmlxml, HTML: text/html, EPUB: application/epubzip, PDF: application/pdf, OPENSEARCH: application/opensearchdescriptionxml, OPDS2: application/opdsjson, };isSearchLink升级后的判定逻辑是opdsUtils.tsexport const isSearchLink (link: OPDSBaseLink): boolean { const rels Array.isArray(link.rel) ? link.rel : [link.rel || ]; if (!rels.includes(search)) return false; return ( link.type MIME.OPENSEARCH || link.type MIME.ATOM || // OPDS 2.0 JSON feeds expose search as a templated link whose href is an // RFC 6570 URI template (e.g. /search{?query}). (link.type MIME.OPDS2 !!link.templated) ); };三个要点rel既可以是字符串也可以是数组判定前先统一规整新增的第三条分支要求type application/opdsjson且templated为真两者缺一不可之所以要求templated是为了防止把目录中那些type同为 OPDS2、但rel误标为search的普通静态链接例如指向某个固定结果页的链接当作动态搜索入口。3. 展开层expandOPDSSearchTemplate实现 RFC 6570 展开判定通过后真正把模板变成可请求 URL 的是新工具函数expandOPDSSearchTemplateopdsUtils.ts// Template variable names that conventionally carry a free-text search query. const SEARCH_TERM_VARS [query, searchTerms, q]; export const expandOPDSSearchTemplate (templateHref: string, queryTerm: string): string { const variables Array.from(getVariables(templateHref) as Setstring); const textVar variables.find((name) SEARCH_TERM_VARS.includes(name)) ?? variables[0]; if (!textVar) return templateHref; return expandURITemplate(templateHref, new Map([[textVar, queryTerm]])); };实现细节与设计取舍复用现成实现不重造轮子。RFC 6570 模板语法虽然不算复杂但涉及?//;///./#等多种运算符、命名参数与保留字符编码手写极易出错。项目直接复用了foliate-js的 uri-template.jsreplace与getVariables两个导出函数——这也是 Readest 依赖的 foliate-js 包在 OPDS 模块中早已内置的能力。主文本变量优先级。模板中可能出现多个变量如{?query,lang}展开时应把用户输入的查询词放入语义最贴切的变量。expandOPDSSearchTemplate按query→searchTerms→q的顺序查找全部不存在时退而求其次放入第一个变量如测试用例中的keyword没有任何变量的模板原样返回。例如/opds/search{?query}配合关键词dune会展开为/opds/search?querydune关键词含空格或特殊字符时由uri-template.js负责正确的encodeURIComponent编码harry potter→harry%20potter。4. 页面层handleSearch增加 OPDS2 分支opds/page.tsx 的handleSearch在原有 OPENSEARCH / ATOM 两个分支之外新增了 OPDS2 分支const searchLink state.feed.links?.find(isSearchLink); if (searchLink searchLink.href) { const searchURL resolveURL(searchLink.href, state.baseURL); if (searchLink.type MIME.OPENSEARCH) { handleNavigate(searchURL, true); } else if (searchLink.type MIME.OPDS2) { // OPDS 2.0 JSON: href is an RFC 6570 URI template (e.g. // /search{?query}). Expand it with the typed term BEFORE resolving // against the base URL — resolveURL would otherwise mangle the // {?query} template braces and drop the query. const expandedHref expandOPDSSearchTemplate(searchLink.href, queryTerm); handleNavigate(resolveURL(expandedHref, state.baseURL), true); } else if (searchLink.type MIME.ATOM) { // ...构造 OPDSSearch 对象把 URL 交给搜索表单视图 } }OPDS2 分支拿到queryTerm后先用expandOPDSSearchTemplate展开模板再交给handleNavigate执行导航与抓取。另外注意loadOPDS在收到404且isSearch标记为真时会 toast 提示No search results found并停留在搜索视图——这保证了搜不到结果也是一个体面的用户体验而不是报错页。关键陷阱为什么必须先展开模板、再解析 URL原文档特别强调了一个极易踩坑的调用顺序问题这也是整个修复中最有价值的知识点。opdsUtils中的resolveURL(url, relativeTo)用于把目录里的相对链接解析为绝对链接opdsUtils.ts。它基于标准URL构造函数实现会把?视作查询串的起始位置。当输入还是未展开的模板时输入: /opds/search{?query} 相对基准: https://catalog.example/root 输出: https://catalog.example/opds/search%7B?query花括号被百分号编码{→%7B?被当成查询分隔符模板被彻底破坏。因此OPDS 2.0JSON templated必须先expandOPDSSearchTemplate展开模板再resolveURL解析相对地址OPENSEARCH / ATOMXML顺序恰好相反——先resolveURL得到完整地址再对占位符{searchTerms}做字符串替换handleSearch的 ATOM 分支中正是decodedURL.replace({searchTerms}, encodeURIComponent(searchTerms))。两种协议形态的处理顺序南辕北辙这正是 OPDS2 分支无法复用顶部统一searchURL变量的根本原因——它必须独立走一遍先展开、后解析的流程。如果将来有人在重构时把 OPDS2 分支塞进共享的resolveURL管道就会重新踩中这个 bug。为什么没有使用 foliate-js 现成的getSearchfoliate-js 的opds.js其实提供了getSearch(link)这一异步方法可以从 OPDS 2.0 JSON 链接通过 uri-template 解析出OPDSSearch对象。那么 Readest 为什么不直接调用它从源码结构看答案在于数据流形态不匹配readest 的page.tsx从未把 JSON 目录喂给 foliate-js 的结构化解析器JSON 路径只是对响应体做了JSON.parse原样保留templated/type等原始字段opds/page.tsx。也就是说readest 的 JSON 链路维护的是原始 JSON 对象 自研工具函数的组合直接引入 foliate-js 的getSearch需要额外接线而自研的expandOPDSSearchTemplate已经足够短小精确还能与isSearchLink的判定逻辑保持一致的 MIME 口径。最终修复选择了后者保持 JSON 链路不依赖 foliate-js 的搜索封装。值得一提的关联知识同文件中的normalizeOpenSearchTemplatesopdsUtils.ts处理的是另一个相似陷阱——部分目录如 Nextcloud 托管的 Calibre2OPDSissue #5500把 OpenSearch 模板的花括号转义成%7B/%7Dfoliate-js 只认字面花括号因此需要先解码再解析。这两个函数共同构成了模板链接主题下的完整防御体系。测试验证单元测试锁死判定与展开行为修复并非一次性提交配套测试位于 opds-utils.test.ts覆盖了两组核心行为isSearchLink判定矩阵第 219-300 行附近rel: searchtype: application/opensearchdescriptionxml→truerel: searchtype: application/atomxml→truerel: searchtype: application/opdsjsontemplated: true→true新增能力无searchrel、type不匹配、或templated缺失/为false的 OPDS2 链接 →false防止误判。expandOPDSSearchTemplate展开矩阵第 302-330 行附近模板关键词期望输出/opds/search{?query}dune/opds/search?querydune/opds/search{?query}harry potter/opds/search?queryharry%20potter/search{?searchTerms}foo/search?searchTermsfoo/search{?query,lang}foo/search?queryfoo/search{?keyword}foo/search?keywordfoo回退到首个变量/search无变量foo/search原样返回这些用例同时验证了主文本变量优先级与含空格关键词的百分号编码两个边界条件为后续重构提供了回归保护。小结issue #4502 的修复给 OPDS 客户端开发带来三点可直接迁移的工程经验搜索链接的识别必须按 MIME 语义标记联合判定。对 OPDS 2.0 JSON 而言rel: search与templated: true缺一不可否则会误伤静态链接RFC 6570 模板展开应复用成熟实现本项目为foliate-js/uri-template.js并在展开时遵循主文本变量优先的语义约定URL 解析与模板展开的顺序因协议而异JSON 模板先展开后解析XML 模板先解析后替换——这决定了不同分支必须走独立的代码路径。若要继续深入本主题可对照阅读 opdsUtils.ts 中resolveURL的 HTTPS 升级逻辑与normalizeOpenSearchTemplates的转义解码以及关联问题 opds-firefox-strict-xml-4479Firefox 严格 XML 解析器对目录响应体尾随垃圾内容的容错处理它们共同勾勒出 Readest OPDS 客户端在异构目录生态下的完整兼容策略。赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐Readest OPDS 集成排障实战指南从目录解析、搜索、鉴权到自动下载的全链路缺陷修复与原理剖析Readest OPDS 集成排障实战指南从目录解析、搜索、鉴权到自动下载的全链路缺陷修复与原理剖析 导读 本文以 Readest 仓库内 apps/read桌面应用跨平台前端Readest OPDS 目录同步复活机制解析CRDT remove-wins 墓碑与 reincarnation token 修复实战Readest OPDS 目录同步复活机制解析CRDT remove wins 墓碑与 reincarnation token 修复实战 导读 本文围绕 Re桌面应用跨平台前端Dgeni源码解析从入口到输出的完整执行流程Dgeni源码解析从入口到输出的完整执行流程 Dgeni作为一款灵活的JavaScript文档生成工具被AngularJS、Protractor等知名JS项桌面应用跨平台前端上一篇面向机器学习的特征工程开源项目安装与配置指南下一篇jQuery Modal 插件安装与配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。