我给自己写了一个本地 RSS 阅读器:安静、不焦虑、UI 全自定义实录

举报
yd_294377644 发表于 2026/10/24 21:05:13 2026/10/24
【摘要】 信息越读越焦虑,市面 RSS 阅读器要么强推云端、要么 UI 拥挤、要么订阅源必须同步到账号。我想要一个安静、本地、UI 自定义的阅读器,于是自己写了一个。这篇博客记录从需求到落地的全过程:技术选型、RSS 解析、稍后读、UI 定制心得,以及那些自己写一遍才明白的细节。

目录

  1. 缘起:为什么我不再用任何云端阅读器
  2. 需求拆解:从"能看文章"到"读得舒服"
  3. 技术选型:浏览器 + 一个 OPML 文件就够了
  4. 核心实现一:RSS 解析与去重
  5. 核心实现二:稍后读与已读状态
  6. UI 定制:阅读器也是"自己的书房"
  7. 三个让我卡了一阵的坑
  8. 结尾:信息消费的"主动权"才是奢侈

1. 缘起:为什么我不再用任何云端阅读器

我订阅了几十个 RSS 源——技术博客、独立思考者、几个新闻站。原本是为了"主动获取信息",但用着用着变味了:

  • 云端阅读器要登录、要同步、哪天收费策略变了订阅就废了;
  • UI 拥挤,未读数红点、推荐位、"今日热门"抢走注意力;
  • 算法推荐开始介入,"你可能感兴趣"悄悄替换了我自己选的订阅源。

我忽然意识到:我订 RSS 就是为了"我自己决定读什么",不是为了"被推荐读什么"。既然现成阅读器都在背离这个初衷,那就自己写一个。

2. 需求拆解:从"能看文章"到"读得舒服"

把"做一个 RSS 阅读器"拆成可验收清单:

诉求(人话) 拆解后(可验收)
能订阅 导入 OPML 或手动添加源,本地存储订阅列表
能抓取 定时拉取各源最新文章,解析标题/链接/时间/摘要
不重复 文章按 GUID/链接去重,已读状态持久化
能读 三栏布局:源列表 / 文章列表 / 正文阅读
稍后读 标星文章单独列出,跨设备导出/导入
UI 我说了算 字体、行宽、暗色模式、布局比例可自定义

这张表决定了阅读器的骨架。功能不多,但每一项都做到位——这是我对所有"自己用的工具"的一贯要求。

3. 技术选型:浏览器 + 一个 OPML 文件就够了

我犹豫过要不要上 Electron,但仔细想需求——我不需要系统级后台抓取、不需要托盘通知,浏览器 + setInterval 完全够用:

能力 方案
订阅导入 解析 OPML(XML 格式)
抓取 RSS fetch + CORS 代理兜底(部分源不支持跨域)
解析 自写 RSS/Atom 解析器,约 50 行
存储 IndexedDB 存文章 + 订阅列表
渲染正文 提取全文 RSS 或 iframe 嵌入原文

整个阅读器就是一个 HTML 文件,双击就能开。没有安装包、没有更新提示、没有后台进程——订阅源列表就是一个 OPML 文件,换机器复制走即可。

心得:选型时先问"我到底需要什么"。本地小工具的最高境界是双击即用、关掉即走。

4. 核心实现一:RSS 解析与去重

RSS 解析听起来复杂,其实格式很规整。一个最小的解析器:

function parseRSS(xml) {
    const doc = new DOMParser().parseFromString(xml, 'text/xml');
    return [...doc.querySelectorAll('item')].map(item => ({
        title:   item.querySelector('title')?.textContent,
        link:    item.querySelector('link')?.textContent,
        pubDate: new Date(item.querySelector('pubDate')?.textContent),
        summary: item.querySelector('description')?.textContent,
        guid:    item.querySelector('guid')?.textContent,
    }));
}

去重是关键。每篇文章用 GUID 优先、链接兜底 作为唯一键:

const key = article.guid || article.link;
if (seen.has(key)) return;        // 已抓过,跳过
seen.add(key); articles.push(article);

两个细节值得说:

  1. Atom 格式要单独处理。<entry> 而不是 <item>,<updated> 而不是 <pubDate>——一个解析器同时兼容 RSS 2.0 和 Atom,能少踩一半坑;
  2. 抓取失败要静默。某个源暂时挂了不应让整个阅读器崩,记下"上次抓取失败"展示给用户即可。

5. 核心实现二:稍后读与已读状态

阅读器的状态管理就两件事:已读和标星。都存在 IndexedDB 里:

// 文章 id → { read: bool, starred: bool, readAt: timestamp }
const state = await db.get('state', articleId) || {};
state.read = true;
state.readAt = Date.now();
await db.put('state', state);

几个让体验"舒服"的细节:

  • 已读文章不消失,只是变灰。很多阅读器"已读即隐藏",让人焦虑——“我是不是漏了什么”。变灰保留可见,主动权在用户;
  • 标星文章跨源聚合。"稍后读"视图不分来源,按标星时间倒序——这是 RSS 阅读最常用的入口;
  • 未读数只显示在源名右侧,不弹通知。红点足够提示,通知是打扰。

心得:阅读器的核心指标不是"功能多",而是"用户能不能控制自己读还是不读"。任何"替用户决定"的设计,都是 RSS 初衷的背离。

6. UI 定制:阅读器也是"自己的书房"

本地工具最大的特权,是 UI 完全我说了算。我给自己定了几个商业软件不会这么做的设计选择:

  • 三栏布局:源 / 列表 / 正文,比例可拖拽,默认 1:2:5,正文占大头;
  • 正文区固定行宽(max-width 680px),长文阅读不费眼;
  • 暗色为主,跟随系统。夜间阅读不刺眼;
  • 无未读数徽章在应用图标上。不制造"必须打开处理"的压力。

这些设计在商业产品里几乎不可能出现——它们太"个人化"了。但本地工具的意义,恰恰就是让软件贴合你的阅读习惯,而不是让你迁就软件的功能预设。

7. 三个让我卡了一阵的坑

坑一:CORS 拦截抓取。 很多 RSS 源不支持跨域 fetch,浏览器直接报 CORS 错。解决办法是走一个公共 CORS 代理兜底,或者干脆做一个轻量后端转发——本地工具也要预先想好"浏览器限制兜底方案"。

坑二:RSS 时间格式不统一。 RSS 2.0 用 RFC 822(Wed, 02 Oct 2026 13:00:00 GMT),Atom 用 ISO 8601(2026-10-02T13:00:00Z)。统一用 new Date() 解析大部分能过,但个别源会写本地时区不带标识,排序错乱。教训:解析时间后统一存时间戳,展示时再按本机时区格式化。

坑三:订阅源"死链"。 几个月后总有几个源不再更新或域名失效。我加了一个"最近 30 天无新文章"的提示,让用户自己决定是否清理——工具不替你删除,但替你看见。

8. 结尾:信息消费的"主动权"才是奢侈

做完这个阅读器,我对"信息消费"有了一种更深的体会:

  1. 它不抢你注意力。没有推荐位、没有红点通知、没有"今日热门";
  2. 它尊重你的订阅。源列表是你的 OPML 文件,不是平台账号里的数据;
  3. 它尊重你的节奏。已读不隐藏、未读不催促、标星不丢失。

在这个所有软件都在用算法替你决定读什么的时代,一个"只读你自己选的源、按你自己节奏读"的本地工具,反而成了一种奢侈。

如果你也订阅了 RSS、也想要一个"安静的阅读器",我强烈建议自己写一个。技术门槛比想象中低,一个 HTML 文件就够;真正难的是克制住加功能的冲动——不做推荐、不做社交、不做"发现",就做一个老老实实的本地阅读器。

工具小而静,价值往往不在多,而在"它永远在那里,永远不替你做决定"。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。