![Image](/_next/image?url=%2Fimage%2Fpost%2F7b0dfcdf-b8b3-429f-8b8a-81729d4a6af4_1664886373805-ba21e207-cf9d-4656-b3cd-f7c5eac42ac1.png&w=3840&q=75)
前几天给 RSSHub 提交了一个 PR,把我所就读的大学的两个学院官网做成了 RSS 源。
RSS 好是好,但就是缺少即时性,于是打算利用企业微信机器人做一个及时推送的服务, 并把服务范围扩大。
关于配置模式的思考
{ "service": [ { "src": "./components/core/swpu/is.js", "config": { "channel": ["xyxw", "tzgg"], "uid": "123456", "push": { "wechat": { "username": "WangRenJie" } } } } ] }
我想到了几种配置方案:
- 用户为单位,每个用户下面配置他订阅的路由,以及他的推送信息。
- 路由为单位,每个路由下面记录订阅该路由的用户。
由于暂时只有我一个人使用,便使用了第二种方案,并把本该配置在用户字段下的”订阅频道“提到了全局。
架构设计
由于不确定各个模块的具体逻辑,这方面我也做了很多技术权衡:
A.每个模块负责返回最新推文
这种模式源于 RSSHub 的实现模式。控制器依次注册每个模块,传递一个上下文方法。
模块内部有一个定时器,每隔一段时间透过上下文更新推文。
推文数据保存在控制器,一旦数据不一致则执行推送流程。
如此,推送逻辑就由控制器处理。好处是,如果以后想抓取其他网站的文章,可以很方便地扩展。但是,后续的开发流程中数据可能污染污染控制器(因为每个网站的推文结构不一样,比如 B 站就会有 video 字段。)
B.每个模块执行全部自己的逻辑
这个方法简单粗暴,模块只需暴露一个start()方法,控制器直接调用即可。
start(interval) { this.instance.forEach((instance) => { instance.start(); }); }
如果后续要扩展,比如用户选择推送逻辑,也可以提取出公共组件。
并且,以后可能会有一些不需要推送的服务,例如网课签到。
这也是最终采用的代码。
C.每个模块提供一个更新、推送方法
看似简洁,事实上这是一个很糟糕的设计,数据到了上游,但是数据消费却到了下游。
理解发布-订阅模式
事实上,这并非是一个标准的发布-订阅模式。
我们先来看看入口函数如何调用个人助理:
assistant.regsiter("./components/core/swpu/is.js", { channel: ["tzgg", "xyxw"], uid: "123456", push: ["wechat"], }) && assistant.start();
这确实是标准的订阅模式。
如果 push 中写进一个具体的推送方法,并且每个模块仅仅返回最新的推文,那么发布者(控制器)则会存储一系列推送函数,一旦模块返回了最新的推文,就依次执行发布流程。
但本项目中,推送流程其实由控制器自己调度了(事实上,每个模块都是控制器的组成部分,此处推送逻辑在模块里面)。所以仅仅实现了订阅模式。
后续开发
基于发布订阅模式,我保留了许多方法,例如list() destory(),后续可以搭配 Web 应用进行远程管理。
国内开放 API 的 IM 平台还比较少,后续将接入国外的一些平台。值得一提的是,配置结构可能就要采取”订阅源“和”用户“分开存储的逻辑。