阅读视图

发现新文章,点击刷新页面。
☑️ ☆

一个六岁开源项目的崩溃与新生

我有一个维护了六年的开源项目—— RSSHub,它正在面临崩溃

背景

表面上,它有接近 30k Stars、900 多 Contributors、每月 3 亿多次请求和数不清的用户、每月几十刀的赞助、有源源不断的 issue 和 pr、代码几乎每天更新,非常健康和充满活力,但在不可见的地方,持续数年高昂的维护时间成本、每月一千多刀的服务器费用、每天重复繁琐且逐渐积累的维护工作,都让它在崩溃的边缘反复横跳

项目是六年前开发的,不少当时以 Next Generation 为口号的时髦 Node.js 技术栈和依赖库已经成为时代眼泪,现在看非常陈旧,很多现在流行的新技术没法应用,比如 JSX、TypeScript、Serverless 等;它的架构也非常不合理,每个路由的信息散落在多个地方,开发或者变更一个路由需要多处修改,一个地方去注册路由,一个地方去编写路由脚本,一个地方去编写 Radar 规则,一个地方去编写文档…这增加了很多工作量,也很容易出错,之前路由少的时候并不是个问题,但现在已经变得难以忍受

在如此糟糕的基础架构下能保持现状已经是竭尽全力,开发新功能更是无本之木,只会增加以后更新的难度,所以我有时候脑子蹦出的新奇想法也很难实现

要解决这些问题,唯一的办法是使用现代化的框架和新设计的架构来重写内核,但随着路由越来越多,改造成本也越来越高,每个基础改动可能都需要多达数月的工作量,所以虽然问题越来越严重,但秉承着又不是不能用的原则一拖再拖

但这又是不得不做的事情,所以我抽空花了几个月的时间重新设计和重写了它

技术栈更新

koa -> Hono

第一步也是最基础和难度最大的是换掉之前使用的 Web 框架 koa,作为六年前流行的下一代 Web 框架,作者早就弃坑了,调研之后决定换用对 JSX、TypeScript、Serverless 支持最好的 Hono

它们的 API 差异很大,需要重写所有中间件和替换所有路由中使用的 koa API

主要改动: https://github.com/DIYgod/RSSHub/pull/14295

image

Hono 作者也很喜欢这个改造

https://twitter.com/yusukebe/status/1762801106340782222

JavaScript -> TypeScript

改用 TypeScript 可以避免很多类型问题和低级错误,最重要的是可以保证数百名贡献者保持一致难以出错和后续贡献的路由代码质量不至于太糟糕

主要改动:

image

https://twitter.com/DIYgod/status/1764360942035312879

CommonJS -> ESM

ESM 是几年前一些 Node.js 核心开发者强推的规范,它有一些优点,但最多的是与之前 CommonJS 不兼容带来的生态割裂和功能简化带来的诟病

经过这几年的发展,现在可以说大部分场景勉强可用了,tsx 也为 CommonJS 和 ESM 混用的场景提供了支持

虽然已经尽了最大努力,但还是有一些 CommonJS 代码暂时难以迁移,导致现在只能使用 tsx 运行,与一些 Serverless 比如 Vercel 没法兼容,但也有机会后续慢慢解决

主要改动:

image

image

art-template -> JSX

art-template 是一个支持 koa 的模板引擎,记得六年前还有一个更流行的模板引擎,但是不记得名字了,选用 art-template 是因为那个更流行的我当时没看懂,这个很简单

Hono 自带了 JSX 支持,JSX 就不用多介绍了,根正苗红的 JavaScript 的语法扩展,等同于用 React

主要改动:

Jest -> Vitest

Jest 是曾经流行的测试框架,但是在 ESM 时代到来之后就越来越不行了,对 ESM 的支持一直是实现性「experimental support」,现在更流行的是 Vitest 了

主要改动: https://github.com/DIYgod/RSSHub/commit/38e42156a0622a2cd09f328d2d60623813b8df28

Got -> ?

目前使用的 Got 也已经是不积极维护的状态了,也没有找到好的替代品,后续也许会换成原生 Fetch 或者自封装的 Fetch,还没有动手

新路由标准

我自己能力还是不够的,在与社区开发者们讨论的过程中学习和改进了很多,过程很有意思:https://github.com/DIYgod/RSSHub/issues/14685

主要改动: https://github.com/DIYgod/RSSHub/pull/14718

image

历史

新标准主要为了解决路由信息过于分散的问题,这次应该算第三版

第一版来自 RSSHub 开发阶段,当时没有预见到路由数量会有这么多,所以几乎没什么规划,所有路由在同一个文件中注册,然后再去增加路由脚本和文档,后来这个文件越来越大,很容易冲突,另外所有路由脚本都会在启动阶段被加载,程序性能越来越差

第二版来自 NeverBehave 维护的时期,引入了命名空间,切割了 router.js、radar.js,同命名空间的路由集中在了一个同文件夹中和一个或多个 Markdown 文档中,还实现了懒加载,极大提升了可维护性和性能,但还是会分散在多个文件中,不同文件的信息也容易出现不一致导致错误

现在

本次把路由文件分为了两类,namespace.ts 和任意名字的路由文件

namespace.ts 会通过导出名为 namespace 的对象来定义命名空间的信息

import type { Namespace } from "@/types";

export const namespace: Namespace = {
  // ...
};

namespace 包含的字段通过 TypeScript 限制为

interface Namespace {
  name: string;
  url?: string;
  categories?: string[];
  description?: string;
}

这些信息会经过编译后被文档和 RSSHub Radar 利用

路由文件会通过导出名为 route 的对象来定义路由的信息

import { Route } from "@/types";

export const route: Route = {
  // ...
};

route 包含的字段通过 TypeScript 限制为

interface Route {
  path: string | string[];
  name: string;
  url?: string;
  maintainers: string[];
  handler: (ctx: Context) => Promise<Data> | Data;
  example: string;
  parameters?: Record<string, string>;
  description?: string;
  categories?: string[];

  features: {
    requireConfig?: string[] | false;
    requirePuppeteer?: boolean;
    antiCrawler?: boolean;
    supportRadar?: boolean;
    supportBT?: boolean;
    supportPodcast?: boolean;
    supportScihub?: boolean;
  };
  radar?: {
    source: string[];
    target?: string;
  };
}

之前 route.js mantainer.js radar.js 和文档的信息都被集中在这一个文件中,减少了多处定义也减少了出错的可能

实现

实现逻辑就是开发环境通过遍历整个 route 文件夹,找到所有 namespace.ts 和路由文件,读取信息,加载路由,在生成环境使用提前编译好的路径列表来避免遍历和不必要的加载过程,代码在:https://github.com/DIYgod/RSSHub/blob/master/lib/registry.ts

文档也是通过遍历 route 文件夹,找到所有需要的信息然后合成一系列的 Markdown 文件,不再需要手动维护,代码在:https://github.com/DIYgod/RSSHub/blob/master/scripts/workflow/build-routes.ts

当然使用之前路由标准开发的路由都需要迁移到新标准而不是直接放弃掉,已经通过脚本批量抓取整理信息后做了替换,但特别是文档比较混乱也有很多错误,所以抓取的信息也有很多错误,只能在后续逐渐人工修改了

未来

通过这一系列改进,RSSHub 终于能够扔掉历史包袱,安心开发新功能了,这里列出我积累的一些想法抛砖引玉:

  • 既然 RSSHub 是一个数据集合,用途不一定只有 RSS,JSON 输出功能可以做一些增强,作为通用的 RESTful API 来使用,比如可以提供获取下一页接口或者输出类似 Twitter 关注数的非 feed 数据
  • 用户系统和用户自定义配置,生成自己的私有订阅地址 #14706
  • 路由错误通知和健康度检测 #14712
  • 与 RSS3 节点的联动和加密货币收益共享 https://twitter.com/rss3_/status/1731822029199094012
  • AI 翻译和摘要
  • 更详细的实例数据分析及反向推导自动推荐的 Radar 规则
  • 与本地浏览器或客户端绑定的 RSSHub 实例,有希望真正解决反爬难题

最后,开源是一件很昂贵的事情,RSSHub 能活到现在离不开这些开发者的帮助

以及这些赞助的好心人

如果 RSSHub 正在帮助你,也希望你可以积极参与进来,为信息自由的未来贡献一份自己的微小力量

☑️ ☆

Twitter 对开源项目发起 DDoS 攻击

背景

Twitter 被马斯克收购后,从去年 8 月开始,他们对开源第三方集成和第三方客户端进行了一系列明里暗里的打压和攻击,这样做是为了阻止用户通过非官方客户端访问和使用 Twitter,来增加公司的广告和会员营收

而开源社区中以 Nitter 和 RSSHub 为代表的开源项目并没有放弃向信息自由的努力,通过众多聪明的开源开发者们想出来的一个个奇妙操作(issue),在一轮轮封锁和反封锁的对抗中短暂占了上风,其中最流行的做法是通过 Android 客户端使用的接口功能生成临时账号(细节

经过

在两天前(1月26日),许多 Nitter 实例的运行者和开发者报告称,他们正在使用的接口已被封锁。与此同时,他们的实例也开始遭受报复性 DDoS 攻击

image

image

起初,我并没有太在意这件事情。毕竟,谁会相信 Twitter 官方会做出如此令人不耻且自降身段的 DDoS 行为呢?我对此深感怀疑

然而,昨天当我打开 RSSHub 的 GitHub 仓库时,却意外地发现了以下内容

image

最近一个月的请求数达到了 4.5 亿,比正常水平高出 50%(正常水平仅为 3 亿多)

然后登录 Cloudflare 查看日志

image

自从 26 日 0 点(当天官方接口被封锁和 Nitter 遭受 DDoS 攻击)以来,RSSHub也一直遭受大规模的 DDoS 攻击。最近两天,请求量是平时的 170 多倍,每秒约 1 千次请求

尽管数量看起来可怕,但 Cloudflare 出色的缓存功能已经成功缓存了超过 99% 的 DDoS 请求,甚至没有触发报警

image

RSSHub 的负载均衡和自动扩容功能非常完善,没有受到太大的压力

https://twitter.com/DIYgod/status/1745090590419619865

🤣 就这样一直没有发现

进一步分析发现,所有请求都来自 IP 地址为 139.255.221.98 的设备。这些请求都是针对 /twitter/keyword 路由,并且后面跟着一串不同且无意义的参数

image

我很清楚为什么只针对 keyword 路由,尽管代码中没有明确表达,但根据我的使用经验,此路由使用的搜索接口受到最严格的访问频率限制,通过攻击该路由可以实现最大化效果。由此可以推断出 DDoS 攻击者也对 Twitter 的接口非常熟悉

虽然无法直接证明是官方人员所为,但各种无法解释的“巧合”已经清楚地表明了事情的真相,马斯克的简单粗暴行事风格也正在深刻影响这家公司

影响

进一步封锁 API 和进行 DDoS 攻击这两个操作可以说非常有效

Nitter 开发者 zedeus 表示 Nitter 已死

image

Twitter Monitor 开发者 MANKA 表示不愿意再浪费时间

https://twitter.com/manka_takami/status/1751450519829418342

nitter-status 开发者更是直接放出了告别页面

image

看起来就到此为止了吗?不,这远非终点。自由无法被阻挡,我们还有很多事情可以做

image

29日 更新:RSSHub 已经恢复

https://twitter.com/DIYgod/status/1751941869616148806

☑️ ☆

如何优雅编译一个 Markdown 文档

Markdown 是一种广泛使用的轻量级标记语言,允许人们使用易读易写的纯文本格式编写文档,也是 xLog 主要使用的文章格式,本文就以 xLog Flavored Markdown 为例来说明如何优雅地解析一个 Markdown 文档

架构

解析过程可以用这样一个架构来表示:

flowchart TB subgraph input Markdown end subgraph unified subgraph remark Markdown:::inputClass --string--> remark-parse:::remarkClass --mdast--> remarkPlugins[remark plugins]:::remarkClass remarkPlugins --mdast--> remark-rehype:::remarkClass & mdast-util-toc:::remarkClass end subgraph rehype remark-rehype --hast--> rehypePlugins[rehype plugins]:::rehypeClass rehypePlugins --hast--> hast-util-to-text:::rehypeClass & hast-util-to-html:::rehypeClass & hast-util-to-jsx-runtime:::rehypeClass end rehypePlugins --hast--> unist-util-visit:::rehypeClass end subgraph output mdast-util-toc --tocResult--> TOC:::inputClass hast-util-to-text --string--> plainText[Plain Text]:::inputClass hast-util-to-html --string--> HTML:::inputClass hast-util-to-jsx-runtime --JSX.Element--> ReactElement[React Element]:::inputClass unist-util-visit --custom--> Metadata:::inputClass end style input fill:#bbf7d0,stroke:#4ade80,color:#15803d classDef inputClass fill:#22c55e,stroke:#16a34a style output fill:#bbf7d0,stroke:#4ade80,color:#15803d style unified fill:#bfdbfe,stroke:#60a5fa,color:#1d4ed8 style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626 style rehype fill:#fef08a,stroke:##facc15,color:#a16207 classDef rehypeClass fill:#facc15,stroke:#ca8a04

关键概念:

  • unified:通过语法树和插件来解析、检查、转换和序列化内容的库
  • remark:unified 的生态项目之一,由插件驱动的 Markdown 处理库
  • rehype:unified 的生态项目之一,由插件驱动的 HTML 处理库
  • mdast:remark 使用的用于表示 Markdown 的抽象语法树规范
  • hast:rehype 使用的用于表示 HTML 的抽象语法树规范

简单来说就是把 Markdown 文档交给一个 unified 生态的解析器解析成 unified 可识别的语法树,再通过一系列 unified 生态的插件转换为需要的内容,再通过一系列 unified 生态的工具库输出为需要的格式,下面就从 解析、转换、输出 这三个步骤来分别说明

解析 Parse

flowchart TB subgraph input Markdown end subgraph unified subgraph remark Markdown:::inputClass --string--> remark-parse:::remarkClass end end style input fill:#bbf7d0,stroke:#4ade80,color:#15803d classDef inputClass fill:#22c55e,stroke:#16a34a style unified fill:#bfdbfe,stroke:#60a5fa,color:#1d4ed8 style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626

无论输入是 Markdown、HTML 还是纯文本,都需要将其解析为可操作的格式。这种格式被称为语法树。规范(例如 mdast)定义了这样一个语法树的外观。处理器(如 mdast 的 remark)负责创建它们。

最简单的一步,我们需要解析的是 Markdown,所以这里就应该使用 remark-parse 来把 Markdown 文档编译成 mdast 格式的语法树

对应 xLog Flavored Markdown 中的

const processor = unified().use(remarkParse)

const file = new VFile(content)
const mdastTree = processor.parse(file)

转换 Transform

flowchart TB subgraph remark remark-parse:::remarkClass --mdast--> remarkPlugins[remark plugins]:::remarkClass remarkPlugins --mdast--> remark-rehype:::remarkClass end subgraph rehype remark-rehype --hast--> rehypePlugins[rehype plugins]:::rehypeClass end style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626 style rehype fill:#fef08a,stroke:##facc15,color:#a16207 classDef rehypeClass fill:#facc15,stroke:#ca8a04

这就是魔法发生的地方。用户组合插件以及它们运行的顺序。插件在此阶段插入并转换和检查它们获得的格式。

这一步最为关键,不仅包含了从 Markdown 到 HTML 的转换,还包含我们想在编译过程中夹带的私货,比如增加一些非标准的语法糖、清理 HTML 防止 XSS、增加语法高亮、嵌入自定义组件等

unified 的插件非常多,更新也比较及时,基本需求几乎都能满足,对于不能满足的特定需求,自己编写转换脚本也很容易实现

里面有一个特殊的插件是 remark-rehype,它会把 mdast 语法树转为 hast 语法树,所以在它之前必须使用处理 Markdown 的 remark 插件,在它之后必须使用处理 HTML 的 rehype 插件

xLog Flavored Markdown 中就加入了非常多的转换插件

const processor = unified()
  .use(remarkParse)
  .use(remarkGithubAlerts)
  .use(remarkBreaks)
  .use(remarkFrontmatter, ["yaml"])
  .use(remarkGfm, {
    singleTilde: false,
  })
  .use(remarkDirective)
  .use(remarkDirectiveRehype)
  .use(remarkCalloutDirectives)
  .use(remarkYoutube)
  .use(remarkMath, {
    singleDollarTextMath: false,
  })
  .use(remarkPangu)
  .use(emoji)
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeRaw)
  .use(rehypeIpfs)
  .use(rehypeSlug)
  .use(rehypeAutolinkHeadings, {
    behavior: "append",
    properties: {
      className: "xlog-anchor",
      ariaHidden: true,
      tabIndex: -1,
    },
    content(node) {
      return [
        {
          type: "text",
          value: "#",
        },
      ]
    },
  })
  .use(rehypeSanitize, strictMode ? undefined : sanitizeScheme)
  .use(rehypeTable)
  .use(rehypeExternalLink)
  .use(rehypeMermaid)
  .use(rehypeWrapCode)
  .use(rehypeInferDescriptionMeta)
  .use(rehypeEmbed, {
    transformers,
  })
  .use(rehypeRemoveH1)
  .use(rehypePrism, {
    ignoreMissing: true,
    showLineNumbers: true,
  })
  .use(rehypeKatex, {
    strict: false,
  })
  .use(rehypeMention)

const hastTree = pipeline.runSync(mdastTree, file)

下面介绍部分用到的插件

  • remarkGithubAlerts:增加 GitHub 风格的 Alerts 语法,演示
  • remarkBreaks:不再需要空一行才能被识别为新的自然段
  • remarkFrontmatter:支持前置内容(YAML、TOML 等)
  • remarkGfm:支持非标准的 GitHub 在原版 Markdown 语法上扩展的一系列语法(但其实这系列语法已经被非常广泛使用,成为了事实意义上的标准)
  • remarkDirective remarkDirectiveRehyp:支持非标准的 Markdown 通用指令提案
  • remarkMath rehypeKatex:支持复杂的数学公式,演示
  • rehypeRaw:支持 Markdown 中夹杂的自定义 HTML
  • rehypeIpfs:自定义插件,为图片、音频、视频支持 ipfs:// 协议的地址
  • rehypeSlug:为标题添加 id
  • rehypeAutolinkHeadings:为标题添加指向自身的链接 rel = "noopener noreferrer"
  • rehypeSanitize:清理 HTML,用于确保 HTML 安全避免 XSS 攻击
  • rehypeExternalLink:自定义插件,给外部链接添加 target="_blank"rel="noopener noreferrer"
  • rehypeMermaid:自定义插件,渲染绘图和制表工具 Mermaid,本文的架构图就是通过 Mermaid 渲染的
  • rehypeInferDescriptionMeta:用于自动生成文档的描述
  • rehypeEmbed:自定义插件,用于根据链接自动嵌入 YouTube、Twitter、GitHub 等卡片
  • rehypeRemoveH1:自定义插件,用于把 h1 转为 h2
  • rehypePrism:支持语法高亮
  • rehypeMention:自定义插件,支持 @DIYgod 这样艾特其他 xLog 用户

输出 Stringify

flowchart TB subgraph unified subgraph remark remarkPlugins[remark plugins]:::remarkClass --mdast--> mdast-util-toc:::remarkClass end subgraph rehype rehypePlugins[rehype plugins]:::rehypeClass rehypePlugins --hast--> hast-util-to-text:::rehypeClass & hast-util-to-html:::rehypeClass & hast-util-to-jsx-runtime:::rehypeClass end rehypePlugins --hast--> unist-util-visit:::rehypeClass end subgraph output mdast-util-toc --tocResult--> TOC:::inputClass hast-util-to-text --string--> plainText[Plain Text]:::inputClass hast-util-to-html --string--> HTML:::inputClass hast-util-to-jsx-runtime --JSX.Element--> ReactElement[React Element]:::inputClass unist-util-visit --custom--> Metadata:::inputClass end classDef inputClass fill:#22c55e,stroke:#16a34a style output fill:#bbf7d0,stroke:#4ade80,color:#15803d style unified fill:#bfdbfe,stroke:#60a5fa,color:#1d4ed8 style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626 style rehype fill:#fef08a,stroke:##facc15,color:#a16207 classDef rehypeClass fill:#facc15,stroke:#ca8a04

最后一步是将(调整后的)格式转换为 Markdown、HTML 或纯文本(可能与输入格式不同!)

unified 的工具库也很多,可以输出各种我们需要的格式

比如 xLog 需要在文章右侧展示自动生成的目录、需要输出纯文本来计算预估阅读时间和生成 AI 摘要、需要生成 HTML 来给 RSS 使用、需要生成 React Element 来渲染到页面、需要提取文章的图片和描述来展示文章卡片,就分别使用了 mdast-util-toc、hast-util-to-text、hast-util-to-html、hast-util-to-jsx-runtime、unist-util-visit 这些工具

对应 xLog Flavored Markdown 中的

{
  toToc: () =>
    mdastTree &&
    toc(mdastTree, {
      tight: true,
      ordered: true,
    }),
  toHTML: () => hastTree && toHtml(hastTree),
  toElement: () =>
    hastTree &&
    toJsxRuntime(hastTree, {
      Fragment,
      components: {
        // @ts-expect-error
        img: AdvancedImage,
        mention: Mention,
        mermaid: Mermaid,
        // @ts-expect-error
        audio: APlayer,
        // @ts-expect-error
        video: DPlayer,
        tweet: Tweet,
        "github-repo": GithubRepo,
        "xlog-post": XLogPost,
        // @ts-expect-error
        style: Style,
      },
      ignoreInvalidStyle: true,
      jsx,
      jsxs,
      passNode: true,
    }),
  toMetadata: () => {
    let metadata = {
      frontMatter: undefined,
      images: [],
      audio: undefined,
      excerpt: undefined,
    } as {
      frontMatter?: Record<string, any>
      images: string[]
      audio?: string
      excerpt?: string
    }

    metadata.excerpt = file.data.meta?.description || undefined

    if (mdastTree) {
      visit(mdastTree, (node, index, parent) => {
        if (node.type === "yaml") {
          metadata.frontMatter = jsYaml.load(node.value) as Record<
            string,
            any
          >
        }
      })
    }
    if (hastTree) {
      visit(hastTree, (node, index, parent) => {
        if (node.type === "element") {
          if (
            node.tagName === "img" &&
            typeof node.properties.src === "string"
          ) {
            metadata.images.push(node.properties.src)
          }
          if (node.tagName === "audio") {
            if (typeof node.properties.cover === "string") {
              metadata.images.push(node.properties.cover)
            }
            if (!metadata.audio && typeof node.properties.src === "string") {
              metadata.audio = node.properties.src
            }
          }
        }
      })
    }

    return metadata
  },
}

这样我们就优雅地从原始 Markdown 文档开始,获得了我们需要的各种格式的输出

除此之外,我们还能利用解析出的 unified 语法树来编写一个可以左右同步滚动和实时预览的 Markdown 编辑器,可以参考 xLog 的双栏 Markdown 编辑器(代码),有机会我们下次再聊

☑️ ☆

对 Newsletter 说不

Image

衰退无处不在,这是很正常的现象。人们自然而然地更倾向于短平快的消费方式。然而,我一直无法忍受的一种奇怪趋势是,在一些地方,人们将 RSS 抛弃,转而使用 Newsletter。

本质上,电子邮件是一种私密的双向通信机制,而 RSS 是一种开放的单向通信机制。使用电子邮件进行私密的更新和文章推送是没有意义的,RSS 是更自然的选择。强行将这些功能应用于电子邮件可能会导致许多问题。这些问题让我觉得 Newsletter 就像一个没有能力但却拼命想证明自己的暴君,无法很好达到发布者期望的效果,又过分侵犯了用户的选择和效率。

五宗罪

封闭限制

RSS 是一种开放协议,它允许用户自主订阅和拉取感兴趣的网站的 RSS 源,获取最新的更新和文章,无需他人的许可。用户还可以通过阅读器的个性化设置自由选择所需内容,并且可以使用多种渠道接收通知,甚至可以使用 Telegram Bot 进行订阅。

image

而 Newsletter 是由发布者推送到用户私人邮箱的订阅信息,整个过程依赖于平台方,渠道全程封闭。这种封闭极大地限制了用户的选择权,用户被迫在平台的许可下,通过特定渠道特定格式接收固定信息

繁琐低效

相比之下,RSS 更加简洁高效。订阅源可以集中管理,分类、收藏、订阅和取消订阅的过程也非常简单。

image

而 Newsletter 则会将各种各样的邮件混合在一起,非常分散且难以管理。你很难知道自己到底订阅了哪些内容,它们什么时候会突然出现。而且,内容格式也是各种各样的,查看和阅读起来非常混乱,所以你也不能将一篇文章进行收藏,更不用说方便的第三方集成了。

信息过载

Newsletter 很难对内容进行有效分类和过滤,又与所有正常邮件混合在一起,需要花费精力手动整理。这很容易导致信息过载和垃圾邮件的问题。

而 RSS 可以很方便地进行分类和过滤,对于不重要的内容,你也可以一键全部标记为已读瞬间解脱,完全没有压力。

image

image

更新周期长

对于 RSS 的更新虽然不算实时,但一般以小时计,类似 RSSHub 等自建服务,甚至可以做到每分钟更新。相比之下,Newsletter 的更新周期,以天甚至周月计,明显滞后了许多。

隐私和安全风险

RSS 的开放性体现在它不需要用户提供个人信息,从而确保了更好的隐私性和安全性。然而,Newsletter 至少需要提供一个邮箱地址,这增加了数据泄露或滥用的风险。更有甚者,电子邮件可能包含恶意链接或附件。

也有一些优点

尽管我对 Newsletter 的低效和局限性持批评态度,但我也承认它有其优点,它的流行也有一些合理性,特别是在卖方市场的情况下对于一些发布者来说。Newsletter 可以让他们获得更多的控制权和点击率,可以更容易知道有谁订阅了他们的内容,并通过邮件通知更强烈地唤起用户的注意。

然而,站在用户的角度,我必须明确表示,我更偏爱 RSS。我不愿意以放弃自己的选择权和效率来迎合发布者的控制欲。在我看来,获取信息的权力应该掌握在我自己手中,而非其他人或机构。所以,我在这里对 Newsletter 说不。

☑️ ☆

《献给阿尔吉侬的花束》读书分享

s28050760

上个季度读的书中最喜欢的一本,分类是科幻小说,但重点在于探讨成长、人生意义的人文话题。

故事讲述了在智力提升实验在小鼠阿尔吉侬身上获取成功之后,科学家们对一个智商只有 68 的笨蛋查理进行了手术,查理智商很快提升到了 185 成为了超级天才,又因为实验缺陷再次变成笨蛋的故事,小说通过一篇篇查理笔下的实验日记「进步报告」向读者第一人称讲述了查理从笨蛋到天才又到笨蛋的心路历程和生活的一系列变化。

成长#

故事中查理的成长来自于智力的提升,从一开始日记充满错别字,生活中处处受人嘲笑和欺凌而不自知,到后来错别字越来越少,句子越来越通顺,学到了越来越多的知识,逐渐开始认识和发掘自己,发现生活的美好和痛苦。

有些常识的人都会记得,眼睛的困惑有两种,也来自两种起因,不是因为走出光明,就是因为走进光明所致,不论是人体的眼睛或心灵的眼睛,都是如此。——《理想国》

成长的过程经常是残忍的,查理的经历也是我们成长的过程,我们也有从天真烂漫的童年开始逐渐理解这个世界黑暗面的经历,我们也会同样感到困惑和痛苦。

书中有一段是查理变聪明过程中逐渐想起自己之前受人欺凌,当时却不知道发生了什么,还以为大家在跟他玩耍,查理明白过来之后感到非常悲伤。这让我想起我初中也有类似经历,当时我年龄比其他同学都小,经常被人欺负,但被欺负了还以为是自己 “受欢迎”,以为大家就是这样相处的。

成长又是必要的,因为它可以帮助我们认识自己,获得人性尊严和高级快乐。

快乐#

我不知道哪种情况更糟:不了解自我但很快乐,还是实现理想但感到孤独

笨查理在面包店工作,有一群 “好朋友”,无忧无虑,感觉整个世界无比美好。但在变聪明过程中,各种问题和痛苦也随着出现,他感到他的工作没有意义,他的 “好朋友” 也因为无法捉弄他而离他而去,他还回忆起之前被朋友和陌生人嘲笑和捉弄、被家人抛弃的经历,得不到想要的爱情、尊严、存在意义,这些都让他感到痛苦,离快乐越来越远。

快乐是我们生活的追求之一,但讽刺的是人的心智越成熟似乎越难获得快乐,我们在童年更容易获得快乐,长大后明明知道的更多、拥有的更多却变成了痛苦的大人。

对此,我赞同罗翔的观点:快乐有质和量的区别,越能体现人性尊严的快乐,越是一种最大的快乐。最求高级快乐的过程中可能会遇到很多困惑和痛苦,但更能体现人性尊严和人生意义,这种快乐也是更强大和持久的。查理满足求知欲后的快乐和成功学会去爱的快乐是之前的傻乐无法相比的。

同时我也觉得低级快乐必不可少,这也是为什么我也热衷于看涩图。

爱情#

智慧是人类最伟大的恩赐之一,只是在追寻知识的过程中,对爱的追寻往往就被搁在一旁。这是我自己最近发现的结论。我可以把这个假设提供你参考:没有能力给予和接受爱情的智慧,会促成心智与道德上的崩溃,形成神经官能症,甚至精神病。而且我还要说,只知专注在心智本身,以致排除人际关系并因此形成封闭的自我中心,只会导致暴力与痛苦。

最让查理困惑的一件事就是爱情,查理智商逐渐变高,但他面对爱情的无可奈何和困扰却没有改变,他发现了爱情与智商无关。

小说中查理身边出现了两个女人。查理与邻居费伊相处纯粹、充满激情、无所顾虑,看似幸福美好,但费伊在发现查理变笨之后毅然离去。而与教授艾丽斯的感情要复杂得多,这也让查理发现了自己的心理问题,发现自己并没有爱与被爱的能力,真正的爱情与单纯的激情不同,查理与艾丽斯有着更深的羁绊,更愿意为对方做出牺牲,艾丽斯在查理变笨之后不离不弃,查理失去记忆之后最关心的仍然是艾丽斯。作者通过两人的对比深刻地诠释了激情和爱情的区别。

艾丽斯又一次来到门口但我说走开我不要见你。她哭了起来我也跟着哭但我不让她进来因为我不要她朝笑我。我告诉她我不在喜欢她而且我在也不要变聪明。这不是真的。我仍然爱她仍然想要变聪明但我必须这样说才能让她离开。穆尼太太告诉我艾丽斯带了更多钱要来照故我并且付房租。我不要我必须去找工作。

今天我做了一件很笨的事我忘了我已经不在纪尼安小姐的成人中心班级上课。我走进去坐在教室后面的老位子上她看到我时表情很怪然后问说查理你都到那里去了。所以我说哈罗纪尼安小姐我今天已准备好要上课只是我弄丢了我们在用的书本。她开始哭起来并且跑出去教室。大家都转头看我而我发现很多人都不是我以前班上的同学。...... 那就是我为什么要永远离开这里去沃伦之家的原因。我不要在做出这样的事来。我不要纪尼安小姐为我难过。我知到面包店里的每个人都为我难过但我也不要这个。

这段真的绷不住。

人生意义#

我来到这个世上,无非是想要明白些道理,遇见些有趣的事。倘能如我愿,我的一生就算成功。—— 王小波

日记再次出现错别字,命运残忍地剥夺他所珍惜的光明生活和美好记忆。

虽然查理的结局并不那么美好,崩塌的过程充满痛苦、挣扎和绝望,但他绝不后悔进行了智力提升手术,他学会了爱与被爱,与过去的自己和解,更好地认识了自己,体会到了生活的酸甜苦辣,明白了很多道理,即使这些最终都将消失。就像我们的人生充满意义,即使我们也总有一天会死亡会归于尘土。

找到我完整存在的意义,除了要掌握过去,也得知道未来的可能发展,不仅要知道自己来自何方,也得知道会去哪里。虽然我们知道,在迷宫尽头等着我们的是死亡。我现在认为,我在迷宫中选择的道路造就了现在的我。我不只是一件事物,也是种存在方式,众多方式的一种,了解自己选择的道路,以及那些我没踏上的道路,都能够协助我了解自己的转变。

纪尼安小姐如果你有机会读到这个请不要为我难过。我很感机我就像你说的得到生命中的弟二次机会。因为我学到很多我以前甚至不知到这世界上真的存在的事情。我很高兴能够看到这些即使只是很短的时间。我很高兴我发现了所有关于我的家人和我的事。好像在我想起他们并且看过他们之前我并没有家人似的但现在我知到我有家人而且我和大家一样也是一个人。

虽然死亡无法避免,但我们可以在阿尔吉侬坟前放上一束花束,证明我们曾经有意义地来过成长过。

💐

❌