Last year I experienced the all-too-common career burnout. I had a couple of bad projects in a row, yes, but more generally I was disillusioned with the software industry. There seemed to be a disconnection between what I used to like about the job, what I was good at, and what the market wanted to buy from me.
去年我经历了司空见惯的职业倦怠。我连续遇到了几个糟糕的项目,是的,但更普遍的是我对软件行业感到幻灭。我以前喜欢这份工作的原因,我擅长的领域,以及市场想要从我这里购买的东西之间似乎存在着脱节。
I did the usual thing: I slowed down, quit my job, started therapy. I revised my habits: eat better, exercise, meditate. I tried to stay away from programming and software-related reading for a while. Because I didn’t like the effect it had on me, but also encouraged by its apparent enshittification, I quit Twitter, the last social media outlet I was still plugged into.
我做了一些常规的事情:放慢了节奏,辞掉了工作,开始接受治疗。我改变了自己的习惯:饮食更健康,锻炼身体,冥想。我试图暂时远离编程和与软件相关的阅读。因为我不喜欢它对我产生的影响,同时也受到它明显的恶化的鼓励,我退出了Twitter,这是我仍然使用的最后一个社交媒体平台。
Not working was one thing, but overcoming the productivity mandate —the feeling that I had to make the best of my time off, that I was “recharging” to make a comeback— was another. As part of this detox period, I read How to Do Nothing, a sort of artistic manifesto disguised as a self-help book that deals with some of these issues. The author Jenny Odell mentions Mastodon when discussing alternative online communities. I had heard about Mastodon, I had seen some colleagues move over there, but never really looked at it. I thought I should give it a try.
不工作是一回事,但克服生产力要求——感觉我必须充分利用休息时间,我正在“充电”以重新出发——是另一回事。在这个戒断期间,我读了《如何无所事事》,这是一本伪装成自助书的艺术宣言,涉及了一些这些问题。作者詹妮·奥德尔在讨论替代在线社区时提到了Mastodon。我听说过Mastodon,我看到一些同事转移到那里,但从未真正看过。我想我应该试试。
∗ ∗ ∗
I noticed a few things after a while on Mastodon.
在使用Mastodon一段时间后,我注意到了一些事情。
First, it felt refreshing to be back in control of my feed, to receive strictly chronological updates instead of having an algorithmic middleman trying to sell me stuff.
首先,回到掌控自己的信息流感觉很清新,能够接收严格按照时间顺序排列的更新,而不是被一个算法中间人试图向我推销东西。
Second, many people were going through a similar process as mine, one of discomfort with the software industry and the Web. Some of them were looking back at the old times for inspiration: playing with RSS, Bulletin Board Systems, digital gardens, and webrings; some imagined a more open and independent Web for the future.
其次,许多人正在经历与我类似的过程,对软件行业和网络感到不适。其中一些人回顾过去,寻找灵感:玩弄RSS、公告板系统、数字花园和网站联盟;一些人则想象未来一个更开放、更独立的网络。
Third, not only wasn’t I interested in micro-blogging myself, but I didn’t care for most of the updates from the people I was following. I realized that I had been using Twitter, and now Mastodon, as an information hub rather than a social network. I was following people just to get notified when they blogged on their websites; I was following bots to get content from link aggregators. Mastodon wasn’t the right tool for that job.
第三,我不仅对微博不感兴趣,而且对我关注的人的大部分更新也不感兴趣。我意识到我一直把Twitter和现在的Mastodon当作信息中心而不是社交网络来使用。我关注别人只是为了在他们的网站上发表博客时收到通知;我关注机器人是为了获取链接聚合器的内容。Mastodon并不适合这个工作。
Things clicked for me when I learned about the IndieWeb movement, particularly their notion of social readers. Trying Mastodon had been nice, but what I needed to reconnect with the good side of the Web was a feed reader, one I could adjust arbitrarily to my preferences. There are plenty of great RSS readers out there, and I did briefly try a few, but I knew this was the perfect excuse for me to get back in touch with software development: I was going to build my own personal reader.
当我了解到IndieWeb运动,特别是他们关于社交阅读器的概念时,一切都变得清晰起来。尝试过Mastodon是不错的,但我需要重新与网络的好方面建立联系的是一个可以根据我的偏好任意调整的订阅阅读器。市面上有很多优秀的RSS阅读器,我也试用了几款,但我知道这是我重新接触软件开发的绝佳机会:我要构建自己的个人阅读器。
As a user, I had some ideas of what I wanted from this project.
作为一个用户,我对这个项目有一些想法。
Rather than the email inbox metaphor I’ve most commonly seen in RSS readers, I wanted something resembling the Twitter and Mastodon home feed. That is: instead of a backlog to clear every day, a stream of interesting content whenever I opened the app. The feed would include articles from blogs, magazines, news sites, and link aggregators, mixed with notifications from personal accounts (Mastodon, Goodreads, GitHub). The parsing should be customizable to ensure a consistent look and feel, independent of the shape of the data each source published.
与我最常见的RSS阅读器中的电子邮件收件箱隐喻不同,我希望有一种类似Twitter和Mastodon主页的东西。也就是说:不是每天都要清理的积压,而是每次打开应用程序时都有一条有趣的内容流。该内容流将包括来自博客、杂志、新闻网站和链接聚合器的文章,以及个人账户(Mastodon、Goodreads、GitHub)的通知。解析应该是可定制的,以确保外观和感觉的一致性,独立于每个来源发布的数据形式。
I wasn’t interested in implementing the “social” features of a fully-fledged indie reader. I didn’t mind opening another tab to comment, nor having my “content” scattered across third-party sites.
我对实现完整的独立阅读器的“社交”功能不感兴趣。我不介意打开另一个标签进行评论,也不介意我的“内容”分散在第三方网站上。
That’s what I started with but, once I had the basic functionality in place, I planned to drive development by what felt right and what felt missing as a user. My short-term goal was to answer, as soon as possible, this question: could this eventually become my primary —even my sole— source of information on the Web? If not, I’d drop the project right away. Otherwise, I could move on to whatever was missing to realize that vision.
这就是我开始的方式,但一旦我建立了基本功能,我计划根据用户的感觉和缺失来推动开发。我短期的目标是尽快回答这个问题:这个网站最终能否成为我在互联网上获取信息的主要甚至唯一来源?如果不能,我会立即放弃这个项目。否则,我可以继续努力实现那个愿景所缺少的东西。
∗ ∗ ∗
As a developer, I wanted to test some of the ideas I’d been ruminating on for over a year. Although I hadn’t yet formulated it in those terms, I wanted to apply what I expressed in another post as: user > ops > dev
. This meant that, when prioritizing tasks or making design trade-offs, I would choose ease of operation over development convenience, and I would put user experience above everything else.
作为一名开发者,我想测试一些我已经思考了一年多的想法。虽然我还没有用那些术语来表达,但我想应用我在另一篇文章中所表达的: user > ops > dev
。这意味着,在确定任务优先级或进行设计权衡时,我会选择操作的便利性而不是开发的方便性,并且我会把用户体验放在首位。
Since this was going to be an app for personal use, and I had no intention of turning it into anything else, putting the user first just meant dogfooding: putting my “user self” —my needs— first. Even if I eventually wanted other people to try the app, I presumed that I had a better chance of making something useful by designing it ergonomically for me, than by trying to satisfy some ideal user. It was very important to me that this didn’t turn into a learning project or, worse, a portfolio project. This wasn’t about productivity: it was about reconnecting with the joy of software development; the pleasure wouldn’t come from building something but from using something I had built.
由于这将成为一个供个人使用的应用程序,而且我没有将其变成其他任何东西的意图,将用户放在首位只意味着自用:将我的“用户自我”——我的需求——放在首位。即使我最终希望其他人也能尝试这个应用程序,我认为通过为自己人体工程学地设计它,我有更好的机会制作出有用的东西,而不是试图满足某个理想用户。对我来说,这非常重要,这不会变成一个学习项目,或者更糟糕的是,一个作品集项目。这不是关于生产力:而是关于重新与软件开发的乐趣相连;快乐不会来自于构建某物,而是来自于使用我自己构建的东西。
Assuming myself as the single target audience meant that I could postpone whatever I didn’t need right away (e.g. user authentication), that I could tackle overly-specific features early on (e.g. send to Kindle), that I could assume programming knowledge for some scenarios (e.g. feed parser customization, Mastodon login), etc.
假设我自己是唯一的目标受众意味着我可以推迟一些我暂时不需要的事情(例如用户认证),我可以提前处理过于具体的功能(例如发送到Kindle),我可以假设在某些情况下具备编程知识(例如自定义订阅解析器、Mastodon登录)等等。
Given that mental framework, I needed to make some initial technical decisions.
鉴于这个思维框架,我需要做一些最初的技术决策。
Although this was going to be a personal tool, and I wanted it to work on a local-first setup, I knew that if it worked well I’d want to access it from my phone, in addition to my laptop. This meant that it needed to be a Web application:
虽然这将是一个个人工具,我希望它能在本地优先的设置下运行,但我知道如果它运行良好,我还想在我的手机上访问它,除了我的笔记本电脑。这意味着它需要成为一个Web应用程序。
I wanted the Web UI to be somewhat dynamic, but I didn’t intend to build a separate front-end application, learn a new front-end framework, or re-invent what the browser already provided. Following the boring tech and radical simplicity advice, I looked for server-side rendering libraries. I ended up using a mix of htmx and its companion hyperscript, which felt like picking Web development up where I’d left off over a decade ago.
我希望Web用户界面有一定的动态性,但我并不打算构建一个单独的前端应用程序,学习一个新的前端框架,或者重新发明浏览器已经提供的功能。遵循无聊的技术和激进的简单原则,我寻找了服务器端渲染库。最终我选择了htmx和它的伴侣hyperscript的混合使用,感觉就像是在十多年前离开的地方继续进行Web开发。
Making the app ops-friendly meant not only that I wanted it to be easy to deploy, but easy to set up locally, with minimal infrastructure —not assuming Docker, Nix, etc.
使应用程序适合操作意味着我不仅希望它易于部署,而且易于在本地设置,使用最少的基础设施-不假设使用Docker、Nix等。
A “proper” IndieWeb reader, at least as described by Aaron Parecki, needs to be separated into components, each implementing a different protocol (Micropub, Microsub, Webmentions, etc.). This setup enforces a separation of concerns between content fetching, parsing, displaying, and publishing. I felt that, in my case, such architecture would complicate development and operations without buying me much as a user. Since I was doing all the development myself, I preferred to build a monolithic Web application. I chose SQLite for the database, which meant one less component to install and configure.
一个“合适的”IndieWeb阅读器,至少根据Aaron Parecki的描述,需要分成不同的组件,每个组件实现不同的协议(Micropub、Microsub、Webmentions等)。这种设置强制将内容获取、解析、显示和发布分开。我觉得,在我的情况下,这样的架构会使开发和运营变得复杂,而对我作为用户来说并没有太多好处。由于我自己进行所有的开发工作,我更喜欢构建一个单体式Web应用程序。我选择了SQLite作为数据库,这意味着少了一个需要安装和配置的组件。
In addition to the Web server, I needed some way to periodically poll the feeds for content. The simplest option would have been a cron job, but that seemed inconvenient, at least for the local setup. I had used task runners like Celery in the past, but that required adding a couple of extra components: a consumer process to run alongside the app and something like Redis to act as a broker. Could I get away with running background tasks in the same process as the application? That largely depended on the runtime of the language.
除了Web服务器,我还需要一种定期轮询内容的方法。最简单的选择可能是cron作业,但对于本地设置来说似乎不方便。我过去使用过类似Celery的任务运行器,但这需要添加一些额外的组件:一个与应用程序一起运行的消费者进程,以及类似Redis的东西作为代理。我能否在同一进程中运行后台任务和应用程序?这在很大程度上取决于语言的运行时。
At least from my superficial understanding of it, Go seemed like the best fit for this project: a simple, general-purpose language, garbage-collected but fast enough, with a solid concurrency model and, most importantly for my requirements, one that produced easy-to-deploy binaries. (I later read a similar case for Golang from the Miniflux author). The big problem was that I’d never written a line of Go, and while I understood it’s a fairly accessible language to pick up, I didn’t want to lose focus by turning this into a learning project.
至少从我对此的肤浅理解来看,Go语言似乎是这个项目的最佳选择:一种简单的通用语言,具有垃圾回收功能但足够快速,拥有可靠的并发模型,最重要的是,它能产生易于部署的可执行文件。(后来我从Miniflux的作者那里读到了类似的观点)。最大的问题是,我从未写过一行Go代码,虽然我知道它是一种相对容易上手的语言,但我不想把这变成一个学习项目而分散注意力。
Among the languages I was already fluent in, I needed to choose the one I expected to be most productive with, the one that let me build a prototype to decide whether this project was worth pursuing. So I chose Python.
在我已经精通的语言中,我需要选择那个我预计能够最有效地使用的语言,那个能够让我建立一个原型来决定这个项目是否值得追求。所以我选择了Python。
The bad side of using Python was that I had to deal with its environment and dependency quirks, particularly its reliance on the host OS libraries. Additionally, it meant I’d have to get creative if I wanted to avoid extra components for the periodic tasks. (After some research I ended up choosing gevent and an extension of the Huey library to run them inside the application process).
使用Python的不好之处是我必须处理它的环境和依赖问题,特别是它对主机操作系统库的依赖。此外,这意味着如果我想避免额外的组件来执行定期任务,我就必须有创意。(经过一些研究,我最终选择了gevent和Huey库的扩展来在应用程序进程中运行它们)。
The good side was that I got to use great Python libraries for HTTP, feed parsing, and scraping.
好的一面是我可以使用很棒的Python库来处理HTTP、解析Feed和爬取数据。
I decided not to bother writing tests, at least initially. In a sense, this felt “dirty”, but I still think it was the right call given what I was trying to do:
我决定最初不费心写测试。从某种意义上说,这感觉“不干净”,但考虑到我当时的目标,我仍然认为这是正确的决定。
Since I was going to experiment, adding, removing, and rearranging features, the cost of maintaining unit tests would outweigh their value. I didn’t mind introducing little logic bugs; I was going to use the app myself anyway, so I expected that most significant bugs would just surface over time.
由于我打算进行实验,添加、删除和重新排列功能,维护单元测试的成本将超过其价值。我并不介意引入一些逻辑错误;反正我自己会使用这个应用程序,所以我预计大部分重要的错误会随着时间的推移逐渐显现出来。
In my experience, integration tests are the ones that provide the most value in terms of confidence that the application works as expected. More so for this project, where the bulk of the work (and the majority of the bugs) came from interacting with external sources and from the UI. But, while I could have caught some bugs earlier and prevented some regressions if I had integration tests in place, implementing them required an effort that just wasn’t worth it upfront.
根据我的经验,整合测试是提供应用程序按预期工作的信心的价值最大的测试。对于这个项目来说尤其如此,因为大部分工作(以及大部分错误)都来自与外部来源和用户界面的交互。但是,虽然如果我在适当的位置进行整合测试,我可能会更早地发现一些错误并防止一些回归,但实施这些测试需要付出的努力并不值得。
There’s a kind of zen flow that programmers unblock when they use their software daily. I don’t mean just testing it but experiencing it as an end user. There’s no better catalyst for ideas and experimentation, no better prioritization driver than having to face the bugs, annoyances, and limitations of an application first-hand.
程序员在日常使用软件时会产生一种禅意的流动。我指的不仅仅是测试,而是以最终用户的身份体验它。没有比亲身面对应用程序的错误、烦恼和限制更好的创意和实验的催化剂,也没有比这更好的优先级驱动因素。
After some trial and error with different UI layouts and features, a usage pattern emerged: open the app, scroll down the main feed, pin to read later, open to read now, bookmark for future reference.
经过尝试不同的用户界面布局和功能后,出现了一种使用模式:打开应用程序,向下滚动主要内容,将内容固定以便稍后阅读,打开以立即阅读,将内容加书签以备将来参考。
I decided early on that I wanted the option to read articles without leaving the app (among other things, to avoid paywalls and consent popups). I tried several Python libraries to extract HTML content, but none worked as well as the readability one used by Firefox. Since it’s a JavaScript package, I had to resign myself to introducing an optional dependency on Node.js.
我早早决定,我想要在应用内阅读文章的选项(除了其他原因外,为了避免付费墙和同意弹窗)。我尝试了几个Python库来提取HTML内容,但没有一个像Firefox使用的readability库那样工作得那么好。由于它是一个JavaScript包,我不得不接受在Node.js上引入一个可选的依赖。
With the basic functionality in place, a problem became apparent. Even after curating the list of feeds and carefully distributing them in folders, it was hard to get interesting content by just scrolling items sorted by publication date: occasional blog posts would get buried behind Mastodon toots, magazine features behind daily news articles. I needed to make the sorting “smarter”.
基本功能已经就位,但出现了一个问题。即使经过筛选和分类,仍然很难通过按发布日期排序的项目来获取有趣的内容:偶尔的博客文章会被Mastodon的toots所掩盖,杂志特写会被日常新闻文章所掩盖。我需要让排序变得“更智能”。
Considering that I only followed sources I was interested in, it was safe to assume that I’d want to see content from the least frequent ones first. If a monthly newsletter came out in the last couple of days, that should show up at the top, before any micro-blogging or daily news items. So I classified sources into “frequency buckets” and sorted the feed to show the least frequent buckets first. Finally, to avoid this “infrequent content” sticking at the top every time I opened the app, I added a feature that automatically marks entries as “already seen” as I scroll down the feed. This way I always get fresh content and never miss “rare” updates.
考虑到我只关注我感兴趣的来源,可以安全地假设我希望首先看到最不频繁的内容。如果一个月度通讯在最近几天发布,那么它应该在微博或每日新闻之前显示在顶部。因此,我将来源分类为“频率桶”,并对订阅进行排序,以显示最不频繁的桶。最后,为了避免每次打开应用时都将“不频繁的内容”固定在顶部,我添加了一个功能,当我向下滚动订阅时自动将条目标记为“已看过”。这样我总是能够获取新鲜内容,不会错过“罕见”的更新。
∗ ∗ ∗
At first, I left the app running on a terminal tab on my laptop and used it while I worked on it. Once I noticed that I liked what was showing up in the feed, I set up a Raspberry Pi server in my local network to have it available all the time. This, in turn, encouraged me to improve the mobile rendering of the interface, so I could access it from my phone.
起初,我将该应用程序保持在我的笔记本电脑的一个终端选项卡上运行,并在工作时使用它。一旦我注意到我喜欢在动态中显示的内容,我就在我的本地网络中设置了一个树莓派服务器,以便随时可用。这反过来又鼓励我改进界面的移动渲染,以便我可以从手机上访问它。
I eventually reached a point where I missed using the app when I was out, so I decided to deploy it to a VPS. This forced me to finally add the authentication and multi-user support I’d been postponing and allowed me to give access to a few friends for beta testing. (The VPS setup also encouraged me to buy a domain and set up this website, getting me closer to the IndieWeb ideal that inspired me in the first place).
最终,我达到了一个点,当我外出时,我想念使用这个应用程序,所以我决定将其部署到一个VPS上。这迫使我最终添加了我一直推迟的身份验证和多用户支持,并允许我给几个朋友提供测试版。 (VPS的设置也鼓励我购买了一个域名并建立了这个网站,使我更接近一开始激励我的IndieWeb理想)。
It took me about three months of (relaxed) work to put together my personal feed reader, which I named feedi. I can say that I succeeded in reengaging with software development, and in building something that I like to use myself, every day. Far from a finished product, the project feels more like my Emacs editor config: a perpetually half-broken tool that can nevertheless become second nature, hard to justify from a productivity standpoint but fulfilling because it was built on my own terms.
我花了大约三个月的(轻松的)工作时间来组建我的个人订阅阅读器,我给它取名为feedi。我可以说,我成功地重新参与了软件开发,并构建了一个我每天都喜欢使用的东西。这个项目远未成品,更像是我的Emacs编辑器配置:一个永远半破的工具,但却可以变得如此自然,从生产力的角度来说很难被证明是合理的,但因为是按照我自己的方式构建的,所以很充实。
I’ve been using feedi as my “front page of the internet” for a few months now. Beyond convenience, by using a personal reader I’m back in control of the information I consume, actively on the lookout for interesting blogs and magazines, better positioned for discovery and even surprise.
我现在已经使用feedi作为我的“互联网首页”几个月了。除了方便之外,通过使用个人阅读器,我重新掌控了我所消费的信息,积极寻找有趣的博客和杂志,更好地进行发现,甚至会有一些意外的惊喜。
12/12/2023
2023年12月12日 #软件 #编程