Maeiee系统搭建迭代1
转眼《Maeiee的成长周报》已经来到第 5 周。在第3、4周中,我坚持啃完《「Let's build GPT:from scratch, in code, spelled out.」课程笔记》,发到网上,引起了一些关注与阅读,很有成就感。在本周中,我放慢脚步,对自己的个人系统进行迭代完善,磨刀不误砍柴工。
『我的个人系统』指什么?为了方便自学与知识获取,我给自己开发了一套软件。比如对 qutebrowser 进行二次开发,使之成为可编程浏览器,又如使用 Flutter 开发了一套跨平台的 App,具备资讯、个人管理等功能。个人系统并不意味着,所有软件都要自己重头开发,一些优秀的软件也可以直接纳入,比如 0.0 Obsidian 介绍。
在本周中,我围绕这些软件进行维护、迭代。首先,这套系统还处于早期探索阶段,谈产品化或开源还太早。那作为读者,能从这篇文章中获得什么?如果你具备编程能力,我希望能向你传递“为自己开发软件,工欲善其事,必先利其器”的信念,这将让个人受益终身。另外,这些瞎折腾本身也是很有意思的,我从中也获取到一些感悟,将他们分享出来。
最后,就像在《Maeiee的自我介绍》中所说:
交友,对我来说,不仅仅是增长见识,更是一种精神上的慰藉。在这个信息爆炸的时代,真正的理解和深入的交流变得尤为珍贵。我相信,每一次的对话和分享,都有可能成为我们认识世界和自我更深一层的契机。
请不要犹豫,如果您对我的任一兴趣点有自己的见解或是有任何问题,都可以通过我的社交媒体账号联系我,或是在我的数字花园留言。我期待着从您那里学习新知识,同时也愿意分享我所了解的信息。在知识的海洋中,我们可以一起航行,探索未知的领域。
您可在《Maeiee首页》中查看我的联系方式。
1 新的浏览器自动化脚本
我将 qutebrowser 视作一个可编程浏览器,与 puppeteer 这种受控浏览器有什么区别呢?区别在于,前者我自己也能日常使用,并且手动与自动可以融合。总体来说,qutebrowser 就像浏览器版本的(低配)1.1 Emacs,可定制性超高!
qutebrowser 底层基于 PySide/PyQt,它们在对 WebView 的操作上,是基于异步回调。这种代码写起来乱,读起来也乱,打破了从上到下的先后顺序。写了几个自动化脚本之后,我就受不了了。于是,我我封装了一套流式调用,要好得多,示例代码如下:
OperationLoadUrlAndWait(url=QUrl(f"https://www.example.com"))
.then(OperationScrollTo(times=4, perc=100, timeout=3000))
.then(OperationFetchHtml())
.then(OperationExtractInfos())
.then(OperationScrollTo(times=1, perc=0, timeout=3000))
这段代码,模仿了 Promise 的 then 流式调用。相较于 Puppeteer 的 await...async
还方便性上还是差一些。后者在复杂逻辑上更加边界,比如条件、循环。而我这里,未来只能靠加算子(thenRepeat
, thenIf
)来实现,非常繁琐。不过,我已经满足了,比之前回调满天飞要好太多。
未来还可以添加录制功能,我在 qutebrowser 中演示一遍,然后自动生成上述脚本。
这让我想到 qutebrowser 本身就带有 Macro Mode!可以录制宏,回头研究下这部分功能!
2 自动化脚本应用:自动化搜索
自动化脚本的用途有很多,概括来说,把重复的事情交给机器去做。举一个例子:基于搜索引擎,实现自动化搜索功能。
有什么用呢?列举两例:
- 突然想查一个东西,通过一行命令
search something
即可,节省大量时间 - 对关注的东西,可以定期自动搜索,实现进展跟踪
除了自动化搜索外,有很多应用场景,与 RPA 的理念不谋而合。
3 资讯浏览页卡顿优化
我用 Flutter 开发了一个资讯阅读器(跟 RSS 阅读器差不多)。随着信息的增多,启动时越来越卡。在本节中进行优化。
同步阻塞优化
有一处逻辑:首先拉取所有站点,然后一个一个查询站点未读资讯的数量。(我在 Flutter 中直接访问 MongoDB,省去了后端 API 这一层)。
查询数据库本身是一个异步操作,同时,mongo_dart 也是支持一定的并发量的。但是,看下面这段 Dart 代码:
loadData(List<String> crawlers) async {
for (final crawler in crawlers) {
final count =
await getIt.get<RayInfoApp>().getCrawlerUnreadCount(crawler);
_unreadCountMap[crawler] = count;
notifyListeners();
}
}
有什么问题?把所有源的异步转成了顺序同步,导致没有并发了。假设一个请求 1s,200 个源就是 200s,所以打开耗时才会这么慢。
更好的实现是,利用 async...await
的阻塞释放控制权的机制,允许 N 个并行操作。代码优化如下:
Future<void> loadData(List<String> crawlers, {int concurrency = 10}) async {
// 分批处理爬虫列表
for (var i = 0; i < crawlers.length; i += concurrency) {
// 获取当前批次的爬虫列表
final batch = crawlers.skip(i).take(concurrency);
// 使用 Future.wait 并行处理当前批次
final results = await Future.wait(batch.map(
(crawler) => getIt.get<RayInfoApp>().getCrawlerUnreadCount(crawler)));
// 处理每个结果
int j = 0;
for (final count in results) {
_unreadCountMap[batch.elementAt(j)] = count;
j++;
}
}
// 更新未读消息总数等其他操作
notifyListeners();
}
同时,我在 MongoDB 中添加索引进行加速。这样,在“拉取所有站点,查询站点未读资讯数量”这一步上实现了秒开。
拉取所有未读资讯优化
目前,我有 7w 多条未读资讯。在拉取这个列表时,请求需要好几秒才能返回数据。经过优化后,也实现了秒开。其中,找到最大的性能瓶颈是联表操作没有放到最后。
在拉取未读资讯时,包含两个操作:一个是过滤 isNew=True
,第二个是联表,为资讯拼接上一些信息。这两个操作的先后顺序,耗时差异是巨大的:
- 先联表再过滤:会对所有资讯数据进行一次联表操作,然后在过滤。耗时长,都耗在联表上。
- 先过滤再联表:过滤出实际需要的资讯,针对检索出来的资讯进行联表,耗时大大缩短。
一开始我采用前者,后来改为了后者。同时通过在 MongoDB 中添加索引进行加速。
经过一顿优化之后,最终实现了资讯 App 的完全秒开!
4 Flutter Fluent UI 组件库
今天要坐火车返回北京,路上四、五个小时,得找点事干。我打算研究 fluent_ui | Flutter package,因为我的自用 App 界面实在太丑了。一直想弄却也没时间,趁这个机会,把这个组件库换上。
这个库我之前也有研究过,它的难点主要有两个:
- 仅有一个 Example,缺少入门教程
- Example 中的最佳实践非常出色,但涉及到大量的库,必须掌握这些库,才能全面掌握 fluent_ui
总结来说,问题不出在 fluent_ui,而是自己太菜。正好,趁这个机会,掌握 Flutter 最佳实践。
有两个自学路径,一是把 Example 跑起来,每个展示用例都提供了单一组件如何使用,相对简单。另一条路径是,看 Example 自身实现源码,这就是我说的最佳实践之所在,相对难,但更有价值,我选择这一条。我的目标是:把 Example 中的架构搬运到我的自用 App 中去。
对 fluent_ui 的自学体会,我记录在《Flutter fluent_ui》这篇文章中。
5 在长列表中持久化阅读状态
在资讯列表中,我使用了一个 ListView,列表中每个元素是一个信息卡片,组件为 InfoCard,是一个 StatefulWidget。我在 InfoCard 内存储了一些状态(isNew、isMark、isHub)。
这里的问题是,当我在 InfoCard 内修改状态,比如点击收藏,此时我在 InfoCard 内通过 setState 更新了状态,这样卡片上的 UI 会变化。(但是资讯列表中的 Model 数据并没有变化)。
这样,当这个卡片离开屏幕后(InfoCard 组件会被其它资讯复用),再切回来,StatefulWidget 中本地修改的状态会丢失。
一种成本比较低的方法是,以 mutable 的方式,直接修改 Model。本次我尝试采用这一低成本方案。
6 收藏资讯关联 Obsidian
我在 App 中收藏的资讯,会在 MongoDB 的资讯 Model 中添加一个 mark 标识。但是,我的知识管理系统是 0.0 Obsidian 介绍,“收藏资讯”在这两个系统间造成割裂。这种割裂,需要我通过手动操作,在 0.0 Obsidian 介绍 进行添加,操作成本太高,严重影响效率。
于是,我想的是,点击收藏,自动到 Obsidian 中去创建一篇笔记。我在 0.0 Obsidian 介绍 中已经搭建了一个网页书签库,因此我的资讯软件只需要按照已有的模板、约定,进行添加即可。
自用的资讯系统升级,成功与 0.0 Obsidian 介绍 打通,在资讯 App 中点击收藏后,将自动在 0.0 Obsidian 介绍 中创建对应书签!
理想中的资讯目录:
- 600.互联网收藏
- A(以 A 为开头的站点)
- ASite.com/
- ASite.com:站点的笔记
- info/:站点下的内容
- 这里,插入一个 hook,不同站点下,内容可以不同
- 简单站点,比如博客,把文章列在下面
- 每篇文章,以 title 作为目录/
- 以 title 作为笔记:存放笔记的元信息
- 以 title 作为笔记==.html==:未来存放离线 HTML
- 以 title 作为笔记==.md==:存放离线 MD
- images/:存放离线资源
- 每篇文章,以 title 作为目录/
- ASite.com/
- B
- C
- ……
- A(以 A 为开头的站点)
脑中构想点击收藏(未收藏 -> 收藏):
- 创建 site 的笔记
- 是否已有该 site 的笔记,如果没有则创建
- 命名规则是,去掉
www
后的 host,按照首字母划分二级目录 - 注:我会为每个站点创建一个笔记,里面用 DataView 索引站点下所有笔记
- 创建网页的笔记
- 首先看站点,是否有特殊处理 hook
- 如果没有,采用默认策略:
- 在 site 目录下创建,以 title 作为目录
- 以 title 作为笔记:存放笔记的元信息
- 完成,弹窗通知,点击确定后,通过 URL 触发 0.0 Obsidian 介绍 打开该网页笔记。
- 注:如果网页也已经存在,则弹窗已经存在,问是否需要覆盖,如果需要覆盖,则重走『创建网页的笔记』。
脑中构想点击收藏(收藏 -> 未收藏):
- site 创建后不再删除
- 弹窗问是否删除文件对应目录?
- 点击是,则删除 site 下以 title 作为目录
- 不再校验以 title 作为目录内的内容,一并删除
下面开始正式实现,首先创建一个 ObsidianProvider
,统一提供与 0.0 Obsidian 介绍 相关的所有功能。之后内部提供两个 Manager:ObsidianSiteManager 和 ObsidianInfoManager。还需要 ObsidianVaultManager、ObsidianTemplateManager 管理与路径相关的内容。
ObsidianSiteManager 功能:首先是对 site host 名称的清理,如果是 www.
开头的,去掉之。接下来是返回站点首字母。获取站点目录。
ObsidianInfoManager 功能:对外 API 是 addInfo
,通过参数传入所有需要的内容(url、description、host、created)。
7 Feed 流卡片布局优化
基于 Flutter fluent_ui 做了一些简单适配。整体跟 Flutter fluent_ui 比较和谐了,甚至还支持了夜间模式。至此,这样简单的适配也足够我用的了,等以后有更多需求,再升级。
本文作者:Maeiee
本文链接:Maeiee系统搭建迭代1
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!