在开发Cent之前,我曾经是另一个免费记账软件的重度用户,当时它是市面上我能找到的唯一个免费支持多人协同记账的软件,其他的软件要么是云同步收费,要么是共享记账收费,只有这一个软件是这些功能全免费的,刚好适合我的需求:和对象一起记账。
说真的,记账是一个“反人类”的事情,我当时搜索记账软件,介绍视频下面就会有很多用户都在问一件事,能不能支持自动记账。我很能理解这些用户的心情,因为手动记账实在是太难坚持下去了,每次消费一笔都要记录一次,本来花钱已经不开心了,还要再额外打开软件记一次,真的很反人类。不过这也正是“记账”的核心作用,通过一次次地反思自己的消费行为,从而达到控制支出的目的,无论是刚刚消费完就要记账的负反馈,还是每个月看记账报表时,把所有消费都赤裸裸呈现出来的“割肉感”,都是为了这个目的服务的。
为了克服“不想记账”的阻力,必须要有一个反动力来抵消,不然记账很难坚持下去,对我来说,就是和对象一起记账,两个人互相监督,互相鼓励,才能让这个枯燥痛苦的行为坚持下去,最终变成一个习惯,最终转变为对消费的控制力。这也是我认为自动记账的鸡肋之处,如果全都自动记账了,那为什么不直接看支付宝/银行卡账单呢。
在我看来记账最为重要的功能之一,是协作记账,或者说共享账本,它不仅能给记账行为一个初始的推力,也确实能满足很多人的其他需求,例如朋友一起出游,需要记录每个人的花销,或者一起开办聚会,同事聚餐等等。有很多记账软件敏锐地捕捉到了这一点,将这些作为一个付费功能,这非常合理,但对于我这种精打细算的人来说,免费的优先级永远更胜一筹。
但正所谓天底下没有免费的午餐,在我使用这个记账软件的第三年,我开始发现一个十分恐怖的事情,当我想要查看过去几年的消费情况,甚至是查找某一条久远的账单时,这么一个简单而又基础的功能居然要收费才能使用,虽然我理解软件开发和服务器都需要成本,但是在这么基础的功能上收费吃相也实在过于难看,在这之前,我已经忍受了每次打开都有的开屏广告,习惯了软件角落里的贷款推销,但是搜索账单需要付费,这一点我实在无法理解。我觉得是时候换一个软件了,但是当我点开软件的数据导出设置时,才发现我实在天真,导出账单当然也是需要付费的。
这件事情开始让我思考,一个记账软件的出路到底是什么?记账软件的受众天生价格敏感,是典型的“钱少事多”,就比如我。但是对于云同步,共享账本这些又天然需求云服务器,是一笔必不可少的支出,不收费只能用爱发电,或者植入广告,靠当传统流量入口赚钱,可想而知这样的方式不可持续,不然也不可能把一个好端端的软件逼上变着法地收费之路。
最终,我还是付费导出了我的数据,软件不坏,但是我已经开始担忧了。我希望我的数据能够牢牢掌握在自己的手中,而不是受制于人,我尝试了很多市面上的其他记账软件,但似乎多人协作,免费,数据自持这三个功能就好像是不可能三角一样,没有一个能够完全满足。
这时候,我终于想起我似乎是一个程序员,为什么我不可以自己写一个呢?当然我并没有直接开始造轮子,我调研和尝试了Github中很多的开源记账软件,真的百花齐放,你几乎可以找到任何前后端语言编写的记账软件UI和配套的服务端,也能找到数据完全保存在本地的开源App,但是同时满足我想要的“记账三角”的,再加上还要跨平台(安卓+iOS),确实是屈指可数,并且也全部建立在自建服务器的基础上,如果想要使用,就不得不自己购买一台服务器,然后陷入那些与记账完全无关的问题中去。
很多人调侃记账软件是程序员必做三件套,这确实是事实,但是也不能忽略背后的逻辑:真的有很多人想要一个符合自己需求的记账软件,并且真的没有完全满足他们需求的App。就像我,最终,我还是选择不跟服务器扯上任何关系,因为我深知这是一个“技术黑洞”,一旦决定使用服务器,就必须要考虑安全、鉴权,域名,备案,防DDOS,数据备份,数据库管理等等,麻烦事会接二连三地出现,即使使用Sass服务,例如Cloudflare等等,也需要投入大量的精力去琢磨KV Cache、Workers等东西,即使有AI的帮助,我的直觉也意识到这部分与记账无关的事情会消耗我的大量精力,而且后续维护也会变的相当困难。
雏形
最后我决定做一个完全本地的离线记账App,依靠每月一次的手动数据同步来完成“共享记账”。于是Oncent诞生了,它是一个纯粹的PWA,基于浏览器,完全不依赖任何服务端接口,连登录都不需要。它的”协作“能力是简单粗暴的,通过peerjs提供的Web RTC接口封装手动传输数据,有点像两个人定期通过QQ把账单发给对方,十分原始,但是有用。我是从peek-transfer这个项目中了解到peerjs的,这也是我顺手做的一个小工具,用webrtc进行点对点互传文件,还挺有用的,在电脑没装QQ的情况下我用这个来传一些手机上的小文件。确定这个方案可行之后,我和我对象就切换到了Oncent中。
其实一开始这个App就想叫Cent,但是奈何Github名称早已被注册了,无法使用Pages部署,只能找一个相似的名字,后来使用自己的域名部署后,终于可以改成自己喜欢的名字了。

在开发过程中,我对记账App的内在结构认知也逐渐变得清晰起来,由于追求极简(懒),很多功能我都是能砍就砍,或者尽可能使用已有的功能来实现,这也让我对记账账单的数据结构设计、各种功能的自洽性有了一定的思考,这些思考后面也成为Cent宝贵的养料,极大降低了我的精神内耗。
意外“教训”
不过意外还是发生了,由于数据保存在浏览器本地,在一次手机空间清理时,浏览器的数据也被清理了,将近半年的数据灰飞烟灭,还好此前有一段时间在其他设备上同步过数据,不至于全军覆没,但这也是一次惨痛的教训:浏览器是脆弱的,尽管它有在变得更强,但依旧无法保障数据的安全性,必须要有一个可靠的,安全的同步机制,才能让记账这件事情带来足够的安全感,至少,不要把鸡蛋放在一个篮子里。
绕来绕去地,整件事情好像又进入了死胡同,想要支持同步就必须要有服务器,上服务器就必须要考虑那一堆麻烦事,难道真的没有其他路了吗?我也实在是不愿意花钱付费订阅其他的记账软件,除了抠门以外,在我看来,一定有还有更好的解决方法,只是我暂时没有发现而已。
停滞了很长一段时间,我都没有什么进展。工作之余,我有一些积累想记录下来,整理成博客,此前我尝试过基于Github Pages的很多套博客方案,但后面都放弃了,要么是不想去从头学Hugo、JekyII那一套,要么是博客里面插图太麻烦,要借助第三方图床。最根本的原因还是懒,我想着也许自己弄一套博客系统,说不定就有动力写下去了。于是我开始开发Urodele,这是一个基于Github Pages + Actions的静态博客站点,它解决了我的核心痛点,就是不需要第三方图床就能直接上传图片,很多时候博客就一两张图,还需要我去别的图床里上传再复制链接,再转成markdown格式,十分麻烦。而Urodele会自动把图片上传到Github中,在后台默默完成这个转换,完全无感,直接在自己的博客站点里就可以完成文章的编写和配图,编写完成后等几分钟,Github就自动编译好发布了,十分舒服。我把这套方案开源了,虽然只有几十个star,但是给了我极大的成就感。也就是在这时,我突然灵光一闪:既然博客可以上传到Github上,那账单数据不也一样可以上传到Github上吗?
我立马开始研究这个可能性,Github API不限制跨域,对浏览器十分友好,并且几乎可以做到Github网页能做的任何事情,而且Github仓库天然支持多人协作,直接完美解决了我的所有痛点!
我被这个喜悦的现实激动得一晚上睡不着觉,恨不得当场就把Oncent改造出来。然而很快我就意识到了一个大问题,Github再怎么大善人,它也只能充当一个“存储器”,如果我想要实现多人/多设备的协作记账,就必须要自己处理数据冲突问题,最典型的一个问题,我和对象同时记了一笔账,直接覆盖更新的话,总有一笔会丢失,这是绝对不可接受的。
同步机制
不过这对我来说并不是一个难题,因为很早之前,我在开发Oncent时,就已经思考到这个问题了,在我的设想中,这是一个抽象的异地数据库协同问题,已经有很多成熟的解决方案,例如OT算法等等,这些为复杂多人协同设计的算法,只有一个核心原理,那就是将所有的操作都视为“增加”,不去删除以往的数据,而是永远追加。对于账单这种几乎就是一个数组的数据结构来说就更简单了,不管是删除还是修改某笔账单,只需要将操作一股脑加到原来的数据里,再加个简单的标识就好了,这样,每个设备,每个用户在操作同一个账本时,都不会改动原有的数据,也就保证了数据不会因为冲突而被意外删除。
原理很简单,但落实到实现上,就有数不清的细节要考虑了:如何屏蔽协同算法的复杂性,使得上层UI无需考虑增量更新,能够直接使用暴露的API无感进行增删查改;上传照片附件的时候,如何处理本地文件与在线地址切换的问题;如何减少每次同步需要发送的请求,避免全量请求所有数据导致同步缓慢;如何将底层结构设计与具体的平台API解耦,不仅能使用Github API,还能快速切换到其他平台,例如Gitee,Web DAV等,这些都消耗了我大量的时间,即使有AI的帮助,让我在开发初始的原型demo时效率飞快,但一旦涉及到架构层面,如何设计简洁的API,如何兼容各种场景下的更新,AI就爱莫能助了。但最终,我还是完成了Cent的底层同步机制设计,并将原理记录在了博客中。我将这个机制命名为Tidal,意为同步的涟漪,各个设备上的更新像涟漪一样扰动着水面,但最终会归为平静,成为一个稳定的,安全的,无冲突的数据平面。

在Tidal中,每个平台(Github/Gitee/Web DAV)都是一个Syncer,它们只需要提供对应的上传&下载接口,并返回对应的数据结构即可,而复杂的同步过程都被掩藏在了水面之下。Tidal在内部维护了一个数据库,由一个主表Items(用于存放账单列表数据),一个暂存表Stash(用于存放“操作”),和一个配置表Config(用于存放配置信息等)构成,当上层应用调用其暴露出来的batch接口,新增或者修改账单时,它会自动将对应的操作写入到Stash中,并同时应用到Items中,这样本地数据会实时更新。同时一旦Stash中存在数据,对应的同步机制就会启动,会将Stash中的内容“追加”到对应平台中去。为了避免数据过大时,请求时长变长的问题,Tidal会根据配置将长数组切分,每次更新时只会请求最新的部分,通过比对本地数据哈希值与云端数据,只更新变动的部分,保证了同步体验不会随着数据日益膨胀而降低。除此之外,Tidal还会自动处理数据中的File对象,将其自动转换为字符串类型的AssetsKey,以便于保存在JSON之中,使用Tidal的API能十分轻松地将AssetsKey转换回真实的二进制文件,使应用能够专注于数据展示。当然Tidal并不单单只支持特定的结构数据,任何满足它的数据结构定义的数据都可以通过Tidal实现延迟同步,也就是说,除了记账,Tidal还可以用在很多其他场景中。Tidal的数据库甚至也是解耦的,除了IndexedDB外,任何实现了StashStorage接口的数据库都可以成为它的后端,让其能够运行在各种环境中,例如Node.js、React Navtive等。
Cent由此诞生了,它的UI风格与Oncent一脉相承,简洁,却又蕴藏着丰富的活力。它基于PWA,能够在几乎所有平台上像原生App那样运行。它有着与其他老牌记账软件相媲美的记账功能,二级分类、标签管理、图片上传等等甚至许多软件都需要付费的功能,它一个不落。最重要的是,它能通过各种平台实现账单数据的同步和协作,并且后续也能轻松接入更多的其他平台,再也不用担心意外数据丢失了。
“App化”
作为前端开发者,我对PWA的跨平台体验天然抱有好感,为了能让网页体验能做到与普通App相同的体验,Cent也下了一番功夫,在iOS中,它支持手势滑动返回;在Android设备上,它也支持通过返回键来返回,这得益于vooh这个web音乐播放器带来的突破性想法。Cent希望打通各个平台的记账体验,让记账成为一个随时随地的事情。
iOS 使用体验与App无异,Android 支持返回按钮
我和对象成为了Cent的首批用户,当我看到两台手机里的账单自然而然地同步完成后,巨大的成就感涌上我的心头。作为一个“纯白嫖”的产品,Cent的依赖极其干净,它的本体是一个React SPA,构建产物直接用静态服务器就能部署;所有的功能都不需要服务端支持,目前所谓的服务端也只是用于验证各个平台的OAuth所搭建的鉴权服务,同样不依赖任何数据库和持久化存储,如果担心权限问题也可以自己生成一个token手动输入使用,彻底摆脱服务器。也正因此,Cent也是一个极其“自托管友好”的开源项目,任何人都可以fork一份部署到自己的站点中,不必担心任何数据泄露的问题。Cent的官方地址就是托管在Clodflare Pages中的,自动构建,自动发布,只要Github和Clodflare不倒闭,就可以一直免费运行下去。

成长
我在一些小众论坛中发布了Cent的消息,很快就有人开始尝试Cent,并且收获了不少的star,最让我开心的是,阮一峰老师在每周科技周刊中也推荐了Cent,让Cent的star来到了一个新的高度,尽管相比于其他真正热门的项目来说这个数量远远不够看,但对我来说依旧是一个很大的鼓励。也是在这个时候,我才开始意识到自己做着玩的项目,和真正开发开源项目的不同,许许多多的issue开始纷至沓来,有的是bug反馈,有的是功能建议,每一次更新,我都要慎重思考对现有用户的影响,是否会导致破坏性的变更,在设计新功能的数据结构和修改同步机制时,都必须小心翼翼,尽可能地覆盖测试范围。更让我开心的是,开始有其他的开发者来帮助我回答其他人的问题,甚至是提交PR修复一些bug,这让我感觉我终于不是自我感动了,原来真的有人在关心这样一个小小的App,并愿意为它付出精力。

Cent收到的改进建议也开始变得千奇百怪,我尽可能地在每个issue下答疑,也逐渐意识到,不是所有的建议都适合Cent,有些是受限于Cent的数据结构和同步机制无法实现,有些则是理念上的冲突。
关于Cent的一些设计细节
智能预测

当然你在Cent中记录了足够多的账单时,Cent会根据地点、时间,智能预测出此时你记账时最有可能选择的分类,并且提供备选的备注词供你快速输入,不仅可以节省记账时间,对我来说,还能激发我为每笔账单记录更为详细的备注,因为有时候我会好奇,之前的我在这个时间都在干什么呢,有时候打开记账界面,看到预测的备注,就能让我回忆起某天的这个时刻。
最强的标签管理,完全抛弃“资产管理”

在issue区中,呼声最高的是希望加入资产账户功能,即在记账时可以选择使用指定的银行卡、信用卡等等,而我认为这其实是从其他记账软件中带来的惯性依赖,在大部分记账App中,筛选和统计功能是完全受限的,只能用软件预设的筛选条件进行统计,例如只能查看每月、每年的支出情况,但不能查看某一个类别/标签的支出,在某一段具体时间内的支出情况,这种细粒度的筛选要么是付费功能,要么根本不支持,所以用户习惯使用资产账户作为一个“筛选条件”,因为其他软件将账户视为一种预设的筛选条件,有很好的统计条件支持,当用户使用Cent时,自然而然地将这种习惯带了过来,却忽略了Cent本身其实具有超高自由度的筛选条件支持,可以统计任何一种交易类型、标签在任何时间段内的消费情况,因此完全不需要一个单独的资产管理功能。为此,我转而更新了Cent的标签功能,支持了自由度更高,更符合Cent设计理念的标签组,让其成为“资产管理”功能的上位代替,并且还能更好地支持其他相似的功能,例如旅游记录、报销系统等等,还专门为其编写了一篇使用指南,而这个功能也收到了许多好评,就像拼积木一样,将原本分散的功能结合在一起,就能爆发出超强的威力。
直观的预算展示

在预算设计上,Cent也别出心裁地使用一个特殊的预算进度来直观表现出当前的预算进度,试想一下,设置某个月的预算后,如果只能看到总进度,就得在心底默默算一遍,接下来每天还能花多少钱,那为什么不直接在预算中展示出来呢?Cent就是这么做的,它会贴心地帮你算出接下来的预算周期内,每天还能花多少钱,如果设置了分类预算,那么该分类剩余的每日花销也会一并计算出来。在预算进度条中,还可以直观地看到总进度和当日进度,如果发现按照当前的消费继续大手大脚下去,很有可能会超支的话,那么预算就会变成醒目的红色,提醒用户注意开支。
未来

作为Cent最忠实的用户,我的账本中已经记录了超过1w条账单数据,其中有从其他软件导入的,更多的是我每天用Cent勤勤恳恳记下的。从Cloudflare的访问数据上看,也开始有许多人每天使用Cent记录自己的支出,我很高兴看到自己的工作能帮助到其他人,被更多人认可,当然,最感谢我对象的鼓励和支持,陪我一起使用和改进Cent的最初版本,让它最终变成了现在的完整形态。Cent当然还不够完美,我正在着手改进它的同步机制,支持更完整的增量更新,探索如何使用AI分析账单,以及App化的可能性。我由衷地希望有更多的人参与到Cent的成长之中,让其变得越来越好。
