我有一個維護了六年的開源專案 —— 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
Hono 作者也很喜歡這個改造
うおお、このPRはすごい。RSSHubがKoaからHonoへ移行してる。 Files changed 2,440 +23,545 −19,773 github.com/DIYgod/RSSHub/…
JavaScript -> TypeScript#
改用 TypeScript 可以避免很多類型問題和低級錯誤,最重要的是可以保證數百名貢獻者保持一致難以出錯和後續貢獻的路由代碼質量不至於太糟糕
主要改動:
CommonJS -> ESM#
ESM 是幾年前一些 Node.js 核心開發者強推的規範,它有一些優點,但最多的是與之前 CommonJS 不兼容帶來的生態割裂和功能簡化帶來的詬病
經過這幾年的發展,現在可以說大部分場景勉強可用了,tsx 也為 CommonJS 和 ESM 混用的場景提供了支持
雖然已經盡了最大努力,但還是有一些 CommonJS 代碼暫時難以遷移,導致現在只能使用 tsx 運行,與一些 Serverless 比如 Vercel 沒法兼容,但也有機會後續慢慢解決
主要改動:
- https://github.com/DIYgod/RSSHub/pull/14619
- https://github.com/DIYgod/RSSHub/pull/14691
- https://github.com/DIYgod/RSSHub/pull/14632
art-template -> JSX#
art-template 是一個支持 koa 的模板引擎,記得六年前還有一個更流行的模板引擎,但是不記得名字了,選用 art-template 是因為那個更流行的我當時沒看懂,這個很簡單
Hono 自帶了 JSX 支持,JSX 就不用多介紹了,根正苗紅的 JavaScript 的語法擴展,等同於用 React
主要改動:
- https://github.com/DIYgod/RSSHub/commit/3bfdf9427cb8cf063cf7d231ec621278495f5a44
- https://github.com/DIYgod/RSSHub/commit/94cf0742afa8bf18510ad9ded9b76dcd2ad52c90
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
歷史#
新標準主要為了解決路由信息過於分散的問題,這次應該算第三版
第一版來自 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 正在幫助你,也希望你可以積極參與進來,為信息自由的未來貢獻一份自己的微小力量