<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>开源 on F2077</title><link>https://from2077.top/tags/%E5%BC%80%E6%BA%90/</link><description>Recent content in 开源 on F2077</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Wed, 30 Sep 2026 21:00:00 +0800</lastBuildDate><atom:link href="https://from2077.top/tags/%E5%BC%80%E6%BA%90/index.xml" rel="self" type="application/rss+xml"/><item><title>Tappy 产品设计：给 15 个月宝宝做的 App</title><link>https://from2077.top/articles/tappy-product-design/</link><pubDate>Wed, 30 Sep 2026 21:00:00 +0800</pubDate><guid>https://from2077.top/articles/tappy-product-design/</guid><description>&lt;h2 id="给闺女做的-app"&gt;给闺女做的 App&lt;/h2&gt;&#10;&lt;p&gt;最近给我闺女珍宝做了一款 macOS 应用：Tappy，代码在 &lt;a href="https://github.com/F2077/Tappy"&gt;github.com/F2077/Tappy&lt;/a&gt;。&lt;/p&gt;&#10;&lt;p&gt;做这个起因很简单：陪 15 个月大的闺女玩时，她对我键盘特别感兴趣。我抱她拍键盘，VS Code 里冒出一串乱码。我想，倒不如做个应用，让她乱拍就能冒出可爱小动物，不比出乱码强，另外我可在旁边给她说这个小动物叫什么，让她温柔的感受到世界（也就是键盘和显示器）给她的反馈以及爸爸的陪伴。&lt;/p&gt;&#10;&lt;p&gt;怎么用：打开，全屏，孩子随便拍。点按、按键、滑动，小动物会从对应位置弹出；一定要和孩子一块玩，小动物弹出来时大人在旁边说。就这些。&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://from2077.top/articles/tappy-product-design/playfield.jpg" alt="Tappy 游戏界面：拍一下键盘，屏幕上就蹦出小动物" loading="lazy" decoding="async" width="640"&gt;&#10;&lt;/p&gt;&#10;&lt;h2 id="怎么设计的"&gt;怎么设计的&lt;/h2&gt;&#10;&lt;p&gt;设计时，我守住两条。&lt;/p&gt;&#10;&lt;h3 id="一陪伴"&gt;一、陪伴&lt;/h3&gt;&#10;&lt;p&gt;Tappy 是亲子互动道具。孩子拍一下，屏幕给回应，回应的意义由家长赋予。&lt;/p&gt;&#10;&lt;p&gt;内置的用系统合成音念名字功能默认关闭，名字该由家长亲口念。我和珍宝玩时，她拍出兔子，我说“白白”（珍宝给小白兔的称谓）。她记住的是我的声音和兔子的关联，不只是记住屏幕上跳出的小动物。&lt;/p&gt;&#10;&lt;h3 id="二克制"&gt;二、克制&lt;/h3&gt;&#10;&lt;p&gt;15 个月的孩子不需要那么多功能。核心因果就一个：拍一下，有东西蹦出来。&lt;/p&gt;&#10;&lt;p&gt;我曾花 3 天做语音触发，说“猫猫”就出猫。做完发现不对：珍宝其实不需要这么刁钻的功能，对她来说一开始就是拍键盘出乱码引起的她的注意力，何必要再增加一个呢。&lt;/p&gt;&#10;&lt;h2 id="乱码和小动物"&gt;乱码和小动物&lt;/h2&gt;&#10;&lt;p&gt;Tappy 的起点，是珍宝在我键盘上敲出的乱码。现在她拍的还是键盘，蹦出来的变成了小动物，不变的依旧是我的陪伴。&lt;/p&gt;&#10;&lt;p&gt;功能列表里，这个 App 大概是比较无聊的那个。但只要她还愿意拍，就值得。&lt;/p&gt;</description></item><item><title>造了个轮子：go-bus，一个 206 行的 Go 进程内事件总线</title><link>https://from2077.top/articles/go-bus/</link><pubDate>Mon, 21 Sep 2026 08:30:00 +0800</pubDate><guid>https://from2077.top/articles/go-bus/</guid><description>&lt;p&gt;先交代一下这是个轮子，而且是基于我自己的另一个轮子（&lt;a href="https://github.com/F2077/go-pubsub"&gt;go-pubsub&lt;/a&gt;）造的。如果你看到&amp;quot;事件总线&amp;quot;三个字想的是 NATS 或 Kafka，那这篇文章可以先帮你省点时间：go-bus 不管跨进程的事，一条消息从生到死都在同一个 Go 二进制里。&lt;/p&gt;&#10;&lt;h2 id="解决什么问题"&gt;解决什么问题&lt;/h2&gt;&#10;&lt;p&gt;进程内的模块解耦。配置热更新完了要通知几个模块，订单创建后要触发一串后续动作，这类需求几乎每个项目都有。最朴素的写法是直接函数调用，代价是模块互相 import，缠成一团。讲究一点的写法是自己搓 channel，搓过几次就知道，每个项目搓出来的东西都差不多，又都差那么一点。&lt;/p&gt;&#10;&lt;p&gt;我想要的其实很简单：发事件的人不用知道谁在听，监听的人不用 import 发的人。就这一件事。&lt;/p&gt;&#10;&lt;h2 id="api-就四个方法"&gt;API 就四个方法&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;On&lt;/code&gt;、&lt;code&gt;Emit&lt;/code&gt;、&lt;code&gt;Cancel&lt;/code&gt;、&lt;code&gt;Close&lt;/code&gt;。核心代码 206 行，一个文件。完整用法长这样：&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;button class="code-block__copy" type="button" data-copy-code data-copied-label="Copied"&gt;Copy&lt;/button&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ConfigReloaded&lt;/span&gt; = &lt;span style="color:#66d9ef"&gt;iota&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;bus&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bus&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;NewBus&lt;/span&gt;()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;reload&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bus&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;NewEvent&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ConfigReloaded&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;listener&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bus&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;NewListener&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;msg&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;any&lt;/span&gt;) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fmt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Println&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;config reloaded:&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;msg&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;bus&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;On&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;reload&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;listener&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;bus&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Emit&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;reload&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;new.yaml&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 不想监听了&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;listener&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Cancel&lt;/span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;事件是一个带名字的整数，用 &lt;code&gt;iota&lt;/code&gt; 常量定义；payload 是 &lt;code&gt;any&lt;/code&gt;，高基数的业务数据（用户 ID、时间戳）都放 payload 里，别拿来定义事件。一次性监听器加个 &lt;code&gt;WithOnetime(true)&lt;/code&gt;，触发一次后自动摘下来，适合&amp;quot;第一次连接成功时做点啥&amp;quot;这类场景。&lt;/p&gt;&#10;&lt;h2 id="它不做什么"&gt;它不做什么&lt;/h2&gt;&#10;&lt;p&gt;这部分我觉得比功能列表重要，README 里也花了同样多的篇幅写：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;不跨进程。没有网络协议，跨进程请用 NATS、Kafka。&lt;/li&gt;&#10;&lt;li&gt;不持久化，不重投。消息只活在订阅者的缓冲 channel 里。&lt;/li&gt;&#10;&lt;li&gt;没有背压。订阅者处理不过来、channel 满了，消息直接丢，而且是静默地丢。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最后一条是有意为之的取舍。&lt;code&gt;Emit&lt;/code&gt; 永远不阻塞，调用方拿到的&amp;quot;成功&amp;quot;只代表 broker 收下了这条消息，不代表任何人处理了它。如果你需要的是&amp;quot;这条消息必须被处理&amp;quot;，那这个库从根上就不合适，用它只会给自己埋雷。我宁愿把这句话写在最显眼的地方，也不想让人用着用着才发现。&lt;/p&gt;&#10;&lt;p&gt;还有一个边界值得一提：broker 默认最多 8192 个 topic，也就是 8192 个不同的事件值。所以事件应该是少量语义化的常量，别把 &lt;code&gt;user-login-{uid}&lt;/code&gt; 这种东西当事件名，那是 payload 该干的活。&lt;/p&gt;</description></item></channel></rss>