升华工作室
Friend Timeline

朋友圈

见字如面,相逢在字里行间

聚合了友情链接中朋友们的最新文章与动态,感受网络邻居们的思考与生活。

G
grtsinry43

Xiaomi 17 标准版刷机折腾记:解锁、官改ROM与必备模块

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/xiaomi-17-bootloader-unlock-custom-rom">https://blog.grtsinry43.com/posts/xiaomi-17-bootloader-unlock-custom-rom</a></p></blockquote><p>纠结了很久,还是入手了 Xiaomi 17 标准版。</p> <p>买这个主要起源于最新小米设备上爆出的解锁漏洞,使得 8e5 机型重新解锁 bootloader 成为可能。</p> <p>这篇文章主要讲讲我的折腾经历,但是隐去了解锁的流程,大家随便搜索论坛,酷安上,还有搞机 QQ 群都能拿到,等到解锁之后,就可以正式开始折腾了。</p> <p><img src="/uploads/pictures/2026-03-12-07:43:06-95.jpg" alt="一下子回到了上个时代哈哈"></p> <h2>官改 ROM</h2> <p>我选择的是酷安上 <a href="https://www.coolapk.com/u/710841">白羊唐黎明</a> 的官改ROM,版本 3.0.301.0。</p> <p>需要搭配底包 3.0.44.0 刷入,可以去</p> <p><a href="https://miuirom.org/">https://miuirom.org/</a></p> <p><a href="https://xiaomirom.com/">https://xiaomirom.com/</a></p> <p>这两个平台找一下,要的话这里也有一个<a href="https://bkt-sgp-miui-ota-update-alisgp.oss-ap-southeast-1.aliyuncs.com/OS3.0.44.0.WPCCNXM/pudding_images_OS3.0.44.0.WPCCNXM_20260131.0000.00_16.0_cn_3c11e63b6e.tgz">直链</a>,然后下一个 MiFlash,emm可以在</p> <p><a href="https://xiaomiflashtool.com/">https://xiaomiflashtool.com/</a></p> <p><img src="/uploads/pictures/2026-03-12-07:55:09-c3.png" alt="image.png"></p> <p>把下载好的官方线刷包解压到任意文件夹,手机音量下+电源键进入 FASTBOOT ,打开 MiFlash,点击“Driver”安装好对应的驱动之后点击“刷新设备”。</p> <p><img src="/uploads/pictures/2026-03-14-04:30:25-1c.jpeg" alt="就是这个页面"></p> <p>::: callout type=&quot;info&quot; title=&quot;注意&quot; 这里如果要命令的话是 <code>adb reboot bootloader</code> :::</p> <p>::: callout type=&quot;warning&quot; title=&quot;Emm&quot; 打开 MiFlash 第一步确保右下角选择全部删除而不是删除并回锁,要不就白解锁了</p> <p>:::</p> <p><img src="/uploads/pictures/2026-03-12-07:59:31-2e.png" alt="image.png"></p> <p><img src="/uploads/pictures/2026-03-12-07:57:21-6b.png" alt="image.png"></p> <p><img src="/uploads/pictures/2026-03-12-07:57:37-c1.png" alt="image.png"></p> <p>选择刚刚解压到的文件夹,之后点击刷机,耐心等待即可。</p> <p>开机之后尽量 oobe 该跳过的跳过,确认能正常进入桌面之后,没问题,然后重新手机音量下+电源键进入 FASTBOOT ,连接电脑,解压好官改包。</p> <p><img src="/uploads/pictures/2026-03-12-08:01:44-f8.png" alt="image.png"></p> <p>先装一下驱动,然后双击打开刷机脚本即可。</p> <p><img src="/uploads/pictures/2026-03-12-08:02:17-82.png" alt="d1709e212429b1bfbbc0c89ea5dbd9f9.png"></p> <p>不出意外的话,等待进度条跑完,手机重启,官改就刷好了。</p> <h2>Play 完整性与 bl 解锁状态隐藏</h2> <p>进入桌面的第一件事,就是找到刚刚官改 zip 里面的 ksu 管理器安装包装好,这样就可以准备刷模块了。</p> <p>我们只需要刷入这几个模块:</p> <p><img src="/uploads/pictures/2026-03-12-08:05:16-01.jpg" alt="e8e1bc40b9be2cd6287793044bd92a66_720.jpg"></p> <p>顺序是:</p> <p><a href="https://github.com/Dr-TSNG/ZygiskNext">https://github.com/Dr-TSNG/ZygiskNext</a></p> <p><a href="https://github.com/5ec1cff/TrickyStore">https://github.com/5ec1cff/TrickyStore</a></p> <p><a href="https://github.com/MeowDump/Integrity-Box">https://github.com/MeowDump/Integrity-Box</a></p> <p>刷完重启就 OK 了</p> <h2>必备软件和模块</h2> <p>我自己用的一些工具</p> <p>Scene:https://www.omarea.com/#/</p> <p>爱玩机工具箱:https://www.aiwanjitool.com/</p> <p>一些模块:</p> <p>Reqable 安装</p> <p>自动救砖</p> <p>::: gallery height=&quot;400px&quot; caption=&quot;KSU&quot; <img src="/uploads/pictures/2026-03-12-08:11:24-7c.jpg" alt="cf943b262476d5d6851f8c59cadbcf05.jpg"> <img src="/uploads/pictures/2026-03-12-08:12:41-34.jpeg" alt="4039249504fe07b87b03d59d3016d96b.jpeg"> :::</p> <p>就到这里,一时兴起写的一篇文章,就当是重新经历刷机时代了。😋</p>

G
grtsinry43

在焦虑与代码中缓慢前行

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/moments/2026/03/03/2026-notes-anxiety-and-code">https://blog.grtsinry43.com/moments/2026/03/03/2026-notes-anxiety-and-code</a></p></blockquote><h2>一月:期末、眼药水、和走得很慢的路</h2> <p>书接上文。</p> <p>::: link-card href=&quot;/posts/2025-summary/&quot; title=&quot;2025 年终总结——从晨光到雾散,化经历为成长&quot; desc=&quot;在爱中重新振作,于是我们真的曾将彼此照亮&quot; newtab=&quot;true&quot;</p> <p>:::</p> <p>2025 年终总结的最后一句是&quot;我们 2026 见&quot;。写下那句话的时候,其实心里还带着点仪式感的勇气,觉得新的一年应该会不一样吧。</p> <p>然后 2026 就这么来了。没有烟花,没有倒计时的激动。跨年夜是和家里人一起过的,大学以来第一次。说来也不全是我主动——是他一直缠着我,大概也是放心不下。</p> <p><img src="/uploads/pictures/2026-03-03-11:18:15-1c.png" alt="image.png"></p> <p>元旦一过,期末就铺天盖地地来了。</p> <p>前几天复习,中途也不乏一些算是小的有趣的东西,很无聊,但笑了好久。</p> <p>考试周嘛,就是靠这种莫名其妙的小事活着的。可是笑完之后该焦虑还是焦虑,期末一天比一天近,时间永远不够用。</p> <p>::: gallery height=&quot;400px&quot; caption=&quot;年初&quot; <img src="/uploads/pictures/2026-03-03-11:18:38-2e.png" alt="image.png"> <img src="/uploads/pictures/2026-03-03-11:19:03-32.png" alt="image.png"> :::</p> <p>那段日子其实很重复。去办公室复习,和朋友有一搭没一搭地聊天,下午靠一杯咖啡续命。长沙的冬天不冷不热却最折磨人,室友空调开得猛一点,鼻炎就犯了,眼睛天天又干又涩,靠眼药水撑着,滴完了接着看书,看完了接着滴。累,真的特别累,不是那种运动完或者怎么样,是那种怎么睡都睡不掉的疲惫。</p> <p>1 月 14 号,试着复健跑步了。</p> <p>距离九月那个意外,四个多月。重新跑起来的时候,脚踩在地上,身体也会提醒我这事情没那么容易过去。相比与之前的轻松,取而代之的是肺活量气息跟不上的痛苦,迈不动步子的煎熬。没跑多远就停下来了,站在操场边喘气,看别人一圈一圈地跑过去。也许这就是时间在身上留下的东西。</p> <p>15 号考完最后一科自控。</p> <p>出考场的时候脑子是空的。明明每个知识点都看过,都理解了,坐到卷子面前就是写不出来。那种&quot;我全都懂但我全都写不准确,做不出题&quot;的感觉,唉经历了一次又一次,不知道什么时候是尽头。</p> <p>当天晚上就开始赶课设了。一直搞到凌晨五点,倒头睡了几个小时,第二天接着熬到三点。幸好自己还年轻...身体好像还扛得住,但心里隐隐觉得自己在透支什么——不是体力,也许是某种对生活的耐心。</p> <p>18、19 号,期末的流程结束,我就开始规划 GrtBlog v2 了。老毛病了。越累越想写东西,越焦虑越想开新坑。大概对我来说,创造是唯一能抵抗未知的东西。打开新项目的那一刻,所有的疲惫都可以暂时搁置,只剩下屏幕上干净的空文件,还有,对即将成型屎山的想法。</p> <p>::: gallery height=&quot;400px&quot; caption=&quot;考试之后&quot; <img src="/uploads/pictures/2026-03-03-11:20:06-88.png" alt="image.png"> <img src="/uploads/pictures/2026-03-03-11:20:14-f4.png" alt="image.png"> <img src="/uploads/pictures/2026-03-03-11:20:24-1d.png" alt="image.png"> :::</p> <p>20 号上了回家的车。21 号到。扑面而来是熟悉的温度和风。</p> <p>回到家并没有真的放松下来。22 号团委的任务就追过来了,那种&quot;你明明在放假但其实没有在放假&quot;的感觉,无法描述。24 号和家里人出去散步,冬天的街道很空,路灯把影子拉得很长。踩在雪上,走着走着,心里安静了一点点。</p> <p>月底的日子就是窝在家里写 GrtBlog v2。窗外是灰蒙蒙的天,屏幕上是 SvelteKit 和 Go Fiber 的代码。没有人催,没有 deadline,只有键盘声和耳机里的音乐在想。那几天写得很沉浸,好像又回到了那个最有想法的时候。</p> <hr> <h2>二月:过年、失眠、和一列南下的火车</h2> <p>二月开头还是写项目。</p> <p>抽空约了要好的高中同学出来吃饭。坐下来发现大家变化都不大,聊的还是那些事,笑点还是那些笑点。这种&quot;不变&quot;让人安心,可也有一瞬间会恍惚——他们好像还是高中的样子,而我总觉得自己这一年老了好多。也许只是经历的东西不一样吧。</p> <p><del>没事就上线原神、星铁</del>,累了刷刷 B 站,晚上看看项目进度,够了就出去跑步,不够就继续写。每天或者隔一天跑一次,夜里的风很冷,我把手蜷缩在袖子里,然后看着街上灯光映照的雪景,只剩下呼吸和脚步声。这大概是一天里最干净的时刻。</p> <p>然后 6 号,发现上学期又有挂的了。</p> <p>怎么说呢。</p> <p>不是挂科本身有多可怕,是那一瞬间,所有东西一起塌下来了。春招的压力、对未来的焦虑、身体恢复的漫长、还没写完的项目、还没准备好的八股、还没刷够的算法——平时一件一件,也就是慢慢累计,直到这一个导火索。那天的情绪很黑,黑到不想说。</p> <p>8 号还是和家里人出去逛了逛。11 号 Lowiro 出了新音游的测试版,五指打 6K 属实逆天了,还有鼠标的事情,在总之就是很难,但是还挺好玩的。后面几天晚上和高中同学打 Minecraft,拆幸运方块,玩空岛生存,在方块的世界里做一些简单到不需要思考的事,一个只有“我”的世界吧。</p> <p>14 号,年前最后一次出门,和同学待了一整天。回来后折腾了小米的 root、LSP、搞搞 tricky store。搞机和写代码一样,是一种需要高度专注的手艺活,专注到可以暂时忘掉其他所有事,<del>专注到忘记备份于是成了砖</del></p> <p>::: gallery height=&quot;400px&quot; caption=&quot;玩,和过年&quot; <img src="/uploads/pictures/2026-03-03-11:21:34-da.png" alt="image.png"> <img src="/uploads/pictures/2026-03-03-11:21:42-f6.png" alt="image.png"> :::</p> <p>15 号,回老家过年。</p> <p>但说实话,一点也不像&quot;回家&quot;。没有任何力气应付亲戚的寒暄,笑容是挤出来的,年是对付过的。除夕是看 B 站拜年纪熬的,顺手搞了个 ctf——群友在文章里藏了解密红包,倒是很有意思。</p> <p>过年那几天,经常出去走走。其实就是待不住,待着就焦虑,出去走走至少能骗骗自己在&quot;散心&quot;。晚上经常睡不着。躺在床上翻来覆去,脑子停不下来——春招什么时候开始投,简历还没改完,算法题还差好多,八股还有一大堆没背,还有课内的课程,还有身体的健康……所有&quot;还没&quot;像一床太重的被子,压着你,闷着你,让你在黑暗里越来越清醒。</p> <p>21 号回了长沙。22 号,上线了 GrtBlog v2 的测试版本。</p> <p>主线通了,看着写了快两个月的东西真正跑起来,算是一种很小的、很确定的满足。至少这件事,还是做到了。</p> <p>23 号和朋友去看了新开的商场,后面几天给博客收尾,穿插着看八股和算法。日子又变成了那种&quot;什么都在推进但什么都没到位&quot;的状态。</p> <p><img src="/uploads/pictures/2026-03-03-11:21:57-c3.png" alt="image.png"></p> <p>27 号早上坐上了火车,北京中转。从北京朝阳到北京西要坐地铁穿城,车厢里人挤人,耳边飘着正宗的京腔——那种老爷范儿的调子,在嘈杂的地铁站里居然有种奇妙的从容感。</p> <p><img src="/uploads/pictures/2026-03-03-11:23:17-fc.png" alt="image.png"></p> <p>在北京西站碰到一个坐轮椅的哥们,左脚打着石膏。我一看就笑了——之前在广州实习的时候也碰到过一个同事左脚骨折的,我俩面对面一撞上,他左我右,完美对称。这次在北京西又来一回,怕不是命运大概觉得这个梗还挺好笑的,舍不得丢。</p> <p>28 号到了学校。</p> <p>晚上和群友聊到深夜,一起弄项目。键盘声响着。熟悉的节奏,熟悉的深夜,依旧是夜晚的想法。</p> <p>后来 3 号上线了新版博客。</p> <p>新学期,就这样又开始了。</p> <hr> <h2>后记</h2> <p>写这篇手记的时候是三月初,坐在一个空教室,刚刚更新了博客的新版,还在改一些bug。</p> <p>回头去看一月和二月,脑子里浮上来的不是什么完整的故事线,全是碎片。鼻炎难受时候没法入睡翻来覆去的夜晚,凌晨五点课设终于跑通时一个人对着屏幕傻笑,夜跑时耳边呼呼的风声还有冻红的双手,除夕拜年纪弹幕飘过去的热闹。还有过年时躺在床上睡不着,盯着天花板发呆的那些夜晚。</p> <p>至少是我这两个月活过的证据。</p> <p>2025 年底我写&quot;雾已经散去&quot;,现在回头看,那句话说得太早了。哪有那么简单。一月的期末、二月的焦虑、过年时的失眠——它们都在提醒我,去年秋天那场意外留下的东西,不只是脚上的伤,还有心里某个被磕碎了又没完全粘好的角落。焦虑还在,不安全感还在,那种&quot;我是不是不够好&quot;的声音还在。</p> <p>但是至少</p> <p>这两个月,不管情绪多差,不管多焦虑多累多睡不着,我一直在写代码。GrtBlog v2 从一月中旬的一个模糊念头,到二月底真正跑起来——这中间经历了期末周的熬夜、回家后的团委任务、过年时的焦虑发作、还有那段很黑的日子。但我就是一直在写。不是因为自律,不是因为简历需要,甚至不是因为&quot;热爱&quot;这种听起来很漂亮的词。就是……需要。像呼吸一样需要。当外面的一切都不确定的时候,打开编辑器,写一行代码,看它跑起来——这件事是确定的。也是最简单的正向反馈了。</p> <p>想起 14 号在操场上复健跑步,跑不了多远就停下来喘气。想起 6 号得知挂科之后那种天塌了的感觉。想起除夕夜一个人在老家的街上走,冷风灌进衣领,假装自己在散心。想起 22 号 v2 上线那一刻,看着页面加载出来,心里安静了一下。</p> <p>这些时刻放在一起看,好像也没那么糟。</p> <p>我还是那个会焦虑到失眠的人,还是那个考试会懵的人,还是那个在亲戚面前挤不出真心笑容的人。但我也是那个在凌晨三点还在写课设的人,是那个在所有人都觉得该休息的时候还在开新坑的人,是那个看到 eslint error 没了会小小地开心一下的人。</p> <p>这些加在一起,就是我。不太完整,但至少还在。</p> <p>春招马上就到了。说不紧张是假的。简历还在想办法,算法还在刷,八股还有一堆要背。前面的路雾蒙蒙的,看不清走向哪里。</p> <p>但我好像不太怕了。</p> <p>不是因为变勇敢了,是因为这两个月教会我一件事:不需要等雾散了再走。雾里也能走。走得慢一点,看不清远一点,偶尔踩空一步——都没关系。脚还在地上,手还在键盘上,朋友还在群里,夜跑的风还是凉的。</p> <p>去年写了一整年的故事,从晨光到浓雾再到雾散。今年的前两个月,没有那么戏剧化的起伏,只是很普通地活着——普通地焦虑,普通地失眠,普通地写代码,普通地和朋友待在一起,普通地在深夜感到一点点温暖。</p> <p>但是我也开始变得算是有点乐观,开始觉得自己的过程曲折到想笑,凑齐了缓考补考重修很“圆满”,开始感觉有的时候自己写 bug 很有乐趣,开始发现不顺心的也是不错的体验</p> <p>那就这样吧。</p> <p>继续写代码,继续跑步,继续睡不着的时候翻来覆去,继续在群里和朋友聊到深夜。</p> <p><img src="/uploads/pictures/2026-03-03-11:24:16-5f.png" alt="image.png"></p>

G
grtsinry43

从 v1 到 v2,谈谈这个简单博客背后的架构演进与实现

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/grtblog-v2-architecture">https://blog.grtsinry43.com/posts/grtblog-v2-architecture</a></p></blockquote><p>写下这篇文章的时候,<code>grtblog-v2</code> 的核心功能开发已经基本告一段落。 <a href="https://github.com/grtsinry43/grtblog-v2">https://github.com/grtsinry43/grtblog-v2</a></p> <p><del>目前正在进行稳定性测试,确认稳定后会逐步修复 Bug、补充功能,并拉朋友内测。当前的测试地址在:</del></p> <p><a href="https://blog-next.grtsinry43.com/">https://blog-next.grtsinry43.com/</a></p> <p><del>(注意仅供测试,数据与本站不会同步)</del></p> <p>本站已更新,稳定后再发布新版项目~</p> <p>感谢 &lt;@starnighter@blogv2.starnighter.com&gt; 同学帮助测试还有 PR ,帮助我完成了一些功能开发~</p> <h2>为什么要重写</h2> <p>这个博客最初只是我学习 React SSR 时的练手项目。一年多过去,它承载了我大量的技术实验——每次有新东西想试,就往里堆。学到了很多,但代价是:它变成了一座精致的屎山。</p> <p>作为部署在 1C2G / 2C4G 小鸡上的个人博客,v1 实在太重了。每次部署要拉起 MySQL、MongoDB、Redis、MeiliSearch 等一堆服务,JVM 和 Next.js 联手吃掉几乎所有内存。更让人疲惫的是 Next.js 的黑盒实现和不断暴露的安全问题——维护它本身就需要一套沉重的心智模型。</p> <p>::: link-card href=&quot;/posts/rsc-boundary-mismatch&quot; title=&quot;新时代的 PHP:RSC 的边界错位与工程代价&quot; desc=&quot;代码编织的幻觉背后,边界的消融暗藏风暴;语法糖包裹的便利之下,责任的转移悄然发生。全栈的浪潮冲刷着安全的长堤,框架的叙事掩盖着架构的代价。&quot; newtab=&quot;true&quot;</p> <p>:::</p> <h2>首先是对比下</h2> <p>咱们首先对比一下,狠狠抨击自己之前的石山,然后讲一下我这次换成了什么:</p> <table> <thead> <tr> <th>问题</th> <th>具体表现</th> </tr> </thead> <tbody> <tr> <td><strong>架构复杂</strong></td> <td>Java 后端 + Next.js 前端 + Umi.js 后台 + Python 推荐服务,四个独立技术栈</td> </tr> <tr> <td><strong>数据库过多</strong></td> <td>MySQL + MongoDB + Redis + Elasticsearch + MeiliSearch,五个模块各司其职但运维成本极高</td> </tr> <tr> <td><strong>部署门槛高</strong></td> <td>Docker Compose 需要 6+ 个容器,配置繁琐,甚至阻碍了作者自己后续维护</td> </tr> <tr> <td><strong>仓库膨胀</strong></td> <td>Git 历史混入大量二进制资源,仓库体积快速膨胀</td> </tr> <tr> <td><strong>边界模糊</strong></td> <td>设计系统、内容模型与插件机制(PF4J)的职责逐渐交叉</td> </tr> <tr> <td><strong>BFF 废弃</strong></td> <td>规划的 BFF 层未能落地,停留在空目录</td> </tr> </tbody> </table> <table> <thead> <tr> <th>决策</th> <th>v1 做法</th> <th>v2 做法</th> <th>理由</th> </tr> </thead> <tbody> <tr> <td>后端语言</td> <td>Java (Spring Boot)</td> <td><strong>Go (Fiber)</strong></td> <td>编译为单二进制,内存占用从数百 MB 降至数十 MB</td> </tr> <tr> <td>前端框架</td> <td>Next.js (React)</td> <td><strong>SvelteKit (Svelte 5)</strong></td> <td>更小的 bundle、更少的运行时开销、Runes 语法更直觉</td> </tr> <tr> <td>管理后台</td> <td>Umi.js (React)</td> <td><strong>Vue 3 (Naive UI)</strong></td> <td>轻量且与前台技术栈解耦,并基于 lithe-admin 二开</td> </tr> <tr> <td>数据库</td> <td>MySQL + MongoDB</td> <td><strong>PostgreSQL 一个搞定</strong></td> <td>JSONB 覆盖文档型需求,减少运维复杂度</td> </tr> <tr> <td>搜索</td> <td>Elasticsearch + MeiliSearch</td> <td><strong>后端内建</strong></td> <td>博客体量下内建搜索足够,去掉两个重型依赖</td> </tr> <tr> <td>推荐系统</td> <td>独立 Python 微服务</td> <td><strong>Go 内建</strong></td> <td>减少跨语言通信和部署复杂度</td> </tr> <tr> <td>静态生成</td> <td>Next.js ISR (框架内建)</td> <td><strong>自研 ISR (Go 驱动)</strong></td> <td>Go 后端直接调度渲染、原子写入,完全可控</td> </tr> <tr> <td>实时通信</td> <td>Socket.io + Netty</td> <td><strong>原生 WebSocket</strong></td> <td>去掉 Socket.io 协议层开销</td> </tr> <tr> <td>部署</td> <td>6+ 容器</td> <td><strong>3 容器</strong> (Go + SvelteKit + Nginx + DB)</td> <td>大幅降低部署门槛</td> </tr> </tbody> </table> <h2>注水静态架构 (Rehydrated Static Architecture)</h2> <p><img src="https://blog.grtsinry43.com/uploads/2026/02/24/mermaid-diagram-2026-02-24-112032.png_0c4a814f-8b26-4fa7-a624-96edb40dc1b4.png" alt=""></p> <p>这是 v2 的核心设计理念,一句话概括:</p> <blockquote> <p><strong>将 SSR 的渲染时机从「用户请求时」提前到「数据变更时」,将渲染产物以纯静态文件的形式交给 Nginx 分发,同时通过 WebSocket 为在线用户注入实时更新。</strong></p> </blockquote> <p>它试图在静态站点的极致性能和动态应用的实时交互之间找到一个平衡点。拆开来看,分为三层:</p> <ol> <li><strong>静态先行 (Static First)</strong> — 所有公开页面默认为纯静态 HTML,由 Nginx 直接分发,首屏速度拉满,CPU 占用趋近于零。</li> <li><strong>增量生成 (Incremental Generation)</strong> — 仅在内容变更时,由 Go 控制平面驱动 SvelteKit 渲染器生成受影响的页面,不做全量重建。</li> <li><strong>实时注水 (Realtime Rehydration)</strong> — 客户端通过 WebSocket 接收评论、点赞及内容的热更新,在线用户无需刷新即可看到最新状态。</li> </ol> <p>换一个更本质的角度来理解:</p> <blockquote> <p>SSR / SSG / ISR 这些词只是在描述&quot;渲染发生在哪里&quot;。真正决定架构设计的,是 <strong>数据与页面的依赖关系</strong>,以及 <strong>渲染产物如何存储和复用</strong>。</p> </blockquote> <p>它的效果是:</p> <p><img src="https://blog.grtsinry43.com/uploads/2026/02/24/Pasted_image_20260224135038.png_65bcd26b-b04d-477d-bb03-0c489109ec01.png" alt=""></p> <h2>发生了什么</h2> <p>我们可以用一个图来看出核心的更新机制是什么的。</p> <p><img src="https://blog.grtsinry43.com/uploads/2026/02/24/mermaid-diagram-2026-01-18-182549.png_45511e92-655f-45a9-8ef3-02fdf585959a.png" alt=""></p> <h3>ISR 工作流</h3> <p>ISR(Incremental Static Regeneration)是本项目的核心机制,类似 Next.js 的 ISR,但完全白盒,可以完全掌控:</p> <pre><code class="language-md">Admin 发布文章 │ ▼ Go 写入数据库 │ ▼ DirtyPathCalculator 计算受影响路径 例: /posts/new, /index, /tags/Go, /feed.xml │ ▼ RenderQueue 异步任务入队 │ ▼ Worker 请求 SvelteKit Renderer GET http://renderer:3000/posts/new │ ▼ AtomicWriter 原子写入静态文件 TempFile -&gt; Rename (防并发读写白屏) │ ▼ WebSocket Hub 广播 post_created 事件 │ ▼ 在线用户收到实时通知 </code></pre> <h3>实时更新流</h3> <pre><code class="language-md">Admin 修改文章错别字 │ ▼ Go 更新 DB + 广播 WS post_update (带 payload) │ ▼ 在线阅读用户的 Svelte Store 收到 payload │ ▼ 无感替换 DOM 文本节点(无需刷新) │ ▼ Go 异步触发静态文件重新生成(为后来者服务) </code></pre> <h2>说说实现细节</h2> <h3>从 MPA 到 SPA:静态文件如何水合</h3> <p>这种架构面临的第一个问题是:如果页面变成了静态文件,客户端怎么水合成 SPA? 好在 SvelteKit 的框架魔法大多发生在SSR的时候。在 SvelteKit 中,页面加载分为两种路径:</p> <ol> <li><strong>首次访问 (SSR)</strong>:服务端执行 <code>load()</code>,拼接完整的 HTML 返回给浏览器。</li> <li><strong>客户端路由跳转 (CSR / SPA)</strong>:当你点击链接从 <code>/</code> 跳转到 <code>/posts/1</code> 时,SvelteKit <strong>不会</strong>请求新的 HTML。它的客户端 Router 会去请求一个特殊路径:<code>/posts/1/__data.json</code>,拿到 JSON 后在前端完成数据替换和 DOM 更新。</li> </ol> <p>因此,我们只需在每次渲染时同时缓存 HTML 和 <code>__data.json</code>,就做到了一个&quot;静态的单页应用&quot;——首次访问命中静态 HTML,水合之后的导航跳转走 <code>__data.json</code>,行为完全等同于 SPA。</p> <h3><code>load()</code> 驱动的 ISR 依赖收集</h3> <p>传统的 ISR 是框架内闭环的,但 v2 的后端是 Go,前端是 SvelteKit。Go 怎么知道文章 A 更新了,首页也要跟着重新渲染?我们就需要一个依赖标记的机制。</p> <h4>1. 页面在 <code>load</code> 阶段显式声明依赖</h4> <p>SvelteKit 的数据获取,精髓在于这个<code>load()</code>函数,由于我们整个页面都是在这里获取初始数据,所以我们不妨在拿数据的时候打个 Tag(<code>web/src/routes/posts/[slug]/+page.server.ts</code>):</p> <pre><code class="language-typescript">const post = await getPostDetail(fetch, params.slug); trackISRDeps(event, `post:detail:${post.id}`); </code></pre> <p>首页等复杂页面也会收集一堆 Tag:</p> <pre><code class="language-typescript">trackISRDeps( event, 'home:recent-posts', 'home:recent-moments', 'home:activity-pulse', 'home:inspiration-stats' ); </code></pre> <h4>2. Header 与反向索引</h4> <p>在 <code>web/src/hooks.server.ts</code> 中,我拦截了响应,把收集到的 Tag 塞进 HTTP Header:</p> <pre><code class="language-typescript">event.locals.isrDeps = new Set&lt;string&gt;(); const response = await resolve(event); headers.set('x-grt-deps', JSON.stringify(Array.from(event.locals.isrDeps))); </code></pre> <p>Go 向 Renderer 发起内网抓取时(<code>server/internal/app/htmlsnapshot/service.go</code>),解析这个 Header,并将关系写入自己的 Redis 映射表:</p> <ul> <li><code>isr:url:&lt;url&gt; -&gt; deps</code></li> <li><code>isr:dep:&lt;dep&gt; -&gt; urls</code></li> </ul> <h4>3. 事件驱动失效</h4> <p>当我在后台修改了文章,Go 的事件总线触发 ISR(<code>server/internal/app/isr/subscriber.go</code>):</p> <pre><code class="language-go">deps := []string{ &quot;home:recent-posts&quot;, fmt.Sprintf(&quot;post:detail:%d&quot;, articleID), } urls := []string{&quot;/&quot;, &quot;/posts&quot;, &quot;/posts/page/1&quot;} return service.Invalidate(ctx, deps, urls) </code></pre> <p>Go 拿着 <code>deps</code> 去反向索引中查出所有受影响的 URL,去重后压入 Redis Sorted Set 队列。</p> <p><strong>至此,一条完整的链路成型:前端声明依赖 → 后端解析并建立索引 → 数据变更时精准触发重渲染。</strong></p> <h3>异步客户端组件与请求</h3> <p>如果全站静态化,点赞数、评论区怎么动态加载? 对于点赞和观看量这种轻交互,我们可以 mounted 之后请求和修改,而评论这种重交互,则可以使用 <code>&lt;QueryRoot&gt;</code> 组件(<code>web/src/lib/ui/common/QueryRoot.svelte</code>),这下就有了个低配的 Suspense(bushi</p> <pre><code class="language-ts">onMount(async () =&gt; { const [{ QueryClientProvider }, { getOrCreateQueryClient }] = await Promise.all([ import('@tanstack/svelte-query'), import('$lib/shared/clients/query-client') ]); client = await getOrCreateQueryClient(options); Provider = QueryClientProvider; if (loader) { const loaded = await loader(); Loaded = loaded.default; } ready = true; }); </code></pre> <p>这样,第一屏不会引入太重的请求部分,而客户端组件加载完成之后由 TanStack Query 管理,最大化管理了请求数据。</p> <h3><code>svatoms</code>:舒服的树形数据传递</h3> <p>在由各种“交互岛屿”构成的页面中,Prop drilling(属性逐层透传)是维护的地狱。结合 Svelte 5 的 Runes 特性,我封装了 <code>svatoms</code> 来实现数据树与组件树的解耦。</p> <p><a href="https://github.com/grtsinry43/svatoms">https://github.com/grtsinry43/svatoms</a></p> <h4>1. Context 挂载模型数据</h4> <p>在页面顶层(<code>web/src/routes/posts/[slug]/+page.svelte</code>),把 <code>load</code> 来的数据挂载到专属的 Context 中。使用 getter 保证 SvelteKit 导航后的数据自动同步:</p> <pre><code class="language-ts">postDetailCtx.mountModelData(() =&gt; data.post ?? null); const { updateModelData } = postDetailCtx.useModelActions(); </code></pre> <h4>2. 细粒度切片订阅</h4> <p>子组件只订阅自己关心的切片(<code>PostDetailMain.svelte</code>):</p> <pre><code class="language-ts">const aiSummaryStore = postDetailCtx.selectModelData((data) =&gt; data?.aiSummary ?? ''); const tocStore = postDetailCtx.selectModelData((data) =&gt; data?.toc ?? [], { equals: sameToc }); </code></pre> <p>这里的 <code>equals</code>可以在返回复杂对象时,手动等价比较避免了无意义的重渲染。</p> <h4>3. 跨树联动,比如阅读进度同步</h4> <p>比如<code>DetailMarkdownContent.svelte</code> 在正文滚动时,更新 <code>detailPanelCtx</code> 里的 <code>activeAnchor</code>。远在另一棵 DOM 树分支上的 <code>MobileNavBar.svelte</code> 订阅同一个 Context 并高亮当前目录。 生产者和消费者无需在同一条 props 链上,状态流转的心智模型很舒服。</p> <h3>渲染平面的优雅降级:静态优先 + 原子写入</h3> <p>之前说过,由于静态的特性,哪怕 Go 后端和 SvelteKit 全部宕机,博客依然要能抗住流量。</p> <h4>1. Nginx 静态</h4> <p>在 <code>deploy/nginx/nginx.conf</code> 中,静态文件是一等公民:</p> <pre><code class="language-conf">location / { # 命中静态文件直接返回,未命中才回源到 SSR try_files $uri $uri.html $uri/index.html @frontend_fallback; } location @frontend_fallback { proxy_pass http://renderer_ssr; } </code></pre> <h4>2. 原子操作避免损坏</h4> <p>高并发下,如果 Go 正在把渲染好的 HTML 写入磁盘,用户恰好访问,就会看到残缺的白屏。 在 <code>server/internal/app/htmlsnapshot/service.go</code> 中,这里利用Rename操作的原子性:</p> <pre><code class="language-go">tmp, _ := os.CreateTemp(dir, &quot;.snapshot-*.tmp&quot;) tmp.Write(body) tmp.Close() os.Rename(tmpName, filePath) </code></pre> <p>并且,如果访问 Renderer 遇到 404,Go 会主动清理旧的静态文件,避免出现“后台删了,前台还在”的幽灵页面。</p> <h3>Markdown渲染</h3> <p>在个人博客的开发中,大多数人会选择引入 <code>markdown-it</code> 或 <code>marked</code>,直接转成 HTML 字符串,然后用 <code>{@html content}</code>(或 <code>v-html</code> / <code>dangerouslySetInnerHTML</code>)一把梭。 ……但这样做意味着完全脱离了框架的组件生命周期——Svelte 不知道那段 HTML 里有什么,自然也无法管理它。 为了在运行时安全、优雅地将 Svelte 组件嵌入到 Markdown 正文中,同时保留AST解析能力,我抽离并开源了<code>svmarkdown</code>。</p> <p><a href="https://github.com/grtsinry43/svmarkdown">https://github.com/grtsinry43/svmarkdown</a></p> <p>这个库是基于Makrdown-it的强大能力的</p> <h4>Phase 1: 解析层 (Parser Layer) —— 构建干净的 AST</h4> <p>在 <code>src/parser.ts</code> 中,利用 <code>markdown-it</code> 对原始文本进行词法分析,拿到扁平的 <code>Token</code> 流,然后通过一个游标解析器,将这些 Token 转换成一颗干净的、高度结构化的自定义抽象语法树(AST),即 <code>SvmdNode</code>。</p> <p>在 <code>src/types.ts</code> 中,可以看到 AST 节点被严格定义为几种:</p> <ul> <li><code>SvmdTextNode</code>:纯文本节点。</li> <li><code>SvmdElementNode</code>:标准 HTML 标签(如 <code>p</code>, <code>strong</code>, <code>a</code>)。</li> <li><code>SvmdCodeNode</code>:代码块节点(携带语言类型和源码)。</li> <li><strong><code>SvmdComponentNode</code></strong>:自定义组件节点。</li> </ul> <p>通过引入 <code>markdown-it-container</code> 插件,<code>svmarkdown</code> 会拦截所有类似 <code>:::callout</code> 或 <code>:::gallery</code> 的自定义块。在解析阶段,它会将冒号后面的标识符和属性提取出来,直接组装成一个 <code>SvmdComponentNode</code>,放入 AST 树中。</p> <h4>Phase 2: 渲染层 (Render Layer) —— Svelte 原生递归组件</h4> <p>拿到 AST 后,就进入了 Svelte 渲染阶段。</p> <p>在 <code>src/Markdown.svelte</code> 和 <code>src/internal/RenderNode.svelte</code> 里,利用 Svelte 的 <code>&lt;svelte:element&gt;</code> 和 <code>&lt;svelte:component&gt;</code> 实现了 AST 的递归遍历。</p> <p>在 <code>&lt;RenderNode&gt;</code> 这个内部核心组件里,会进行分发(Dispatch):</p> <ol> <li><strong>如果是普通元素</strong>:直接渲染 <code>&lt;svelte:element this={node.tag}&gt;</code>。</li> <li><strong>如果是代码块</strong>:将代码字符串作为 props 传入用户定义的外部 CodeBlock 组件。</li> <li><strong>如果是自定义组件</strong>:系统会去查找顶层传入的 <code>componentMap</code>。</li> </ol> <pre><code class="language-ts">{#if node.type === 'component'} {@const MappedComponent = componentMap[node.name] || FallbackComponent} &lt;svelte:component this={MappedComponent} {...node.props}&gt; &lt;SvmdChildren nodes={node.children} /&gt; &lt;/svelte:component&gt; {/if} </code></pre> <p>用这个库,心智负担也很低:</p> <pre><code class="language-ts">const componentBlocks = Object.fromEntries( componentDefinitions.map((component) =&gt; [component.name, true]) ) satisfies SvmdParseOptions['componentBlocks']; export const markdownComponents: SvmdComponentMap = { h1: MarkdownHeading, h2: MarkdownHeading, h3: MarkdownHeading, h4: MarkdownHeading, h5: MarkdownHeading, h6: MarkdownHeading, p: MarkdownParagraph, ul: MarkdownList, ol: MarkdownList, li: MarkdownListItem, blockquote: MarkdownBlockquote, hr: MarkdownHr, table: MarkdownTable, thead: MarkdownThead, tbody: MarkdownTbody, tr: MarkdownTr, th: MarkdownTh, td: MarkdownTd, a: MarkdownLink, img: MarkdownImage, code: MarkdownCodeBlock, gallery: MarkdownFallback, callout: MarkdownFallback, timeline: MarkdownFallback, 'year-card': YearCard, 'link-card': LinkCard, 'footnote-link-card': FootnoteLinkCard }; export const markdownParseOptions: SvmdParseOptions = { componentBlocks, markdownItPlugins: [], markdownItOptions: { html: true, linkify: true, typographer: true } }; export const markdownRenderOptions: SvmdRenderOptions = { allowDangerousHtml: true }; </code></pre> <p>轻量、极速、一切皆组件,这样或许还挺优雅的。</p> <h2>写在最后</h2> <p>回头看,v1 的问题不是任何单一技术选型的失败,而是复杂度在无人察觉中的缓慢堆积——每多一个中间件都&quot;有道理&quot;,每多一层抽象都&quot;有必要&quot;,直到整个系统的重量超过了它所承载的内容本身。</p> <p>v2 的核心收获不是选了更好的框架,而是学会了在每个岔路口问自己一句:<strong>这个博客,真的需要这个吗?</strong> 内存占用腰斩不止,维护的心智模型也清爽了许多。更重要的是,我终于能把精力从&quot;和基础设施搏斗&quot;转回到&quot;做有趣的产品&quot;上了。</p> <p>grtblog-v2 还需要完整的测试和问题修复,但距离稳定应该不会太远了。如果你也在做类似的全栈博客、ISR 优化,或者对 Svelte 5 + Go 的组合感兴趣,欢迎 <a href="https://github.com/grtsinry43/grtblog-v2">Star 仓库</a>、提 Issue,或者直接在评论区聊聊你的想法。</p> <p>感谢读完这篇有点长的技术复盘。</p>

G
grtsinry43

Go 语言初体验:Less is more,一种丑但可靠的工程美学

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/go-first-experience">https://blog.grtsinry43.com/posts/go-first-experience</a></p></blockquote><p>最近新项目评估技术栈,因为 Java/Kotlin 太重,TypeScript/JavaScript(Node.js)因为 js 原型链的问题我一直感觉不是合格的后端语言,写 Rust 的话社区根本不会有几个人贡献,再加上后台任务,轻量化,易于部署,可能就只有 Go 能担任这个职责了。 其实我之前用过 go 的,当时在搞 AI 原型生成器的时候,为了快捷操作容器,我用 go 搞了个沙箱管理器,操作容器,对外 gRPC,利用了它在云原生领域的生态优势。而这次,我看中的是它易于入门,编译快,占用轻,易于部署,当然重要的是,协程模型确实很现代很舒服。</p> <h2>真的很丑</h2> <p>我对 Go 的第一印象非常稳定:<strong>丑。</strong> 对于习惯了 Java/JS 的注解(装饰器),Kotlin 的 DSL,Rust 的宏来说的我,Go 的语法极其贫瘠,真是可以说简陋。对于 Go 的显式哲学来说,不像是语言的搭积木,而是你把螺丝刀给我,我就能把家装起来,但别问我为什么这个螺丝长这样。</p> <h3>于是字符串成了注解</h3> <p>我们从这样一段例子开始:</p> <pre><code class="language-go">// OAuthProviderResp 返回可用的 OAuth provider 信息。 type OAuthProviderResp struct { Key string `json:&quot;key&quot;` DisplayName string `json:&quot;displayName&quot;` Scopes []string `json:&quot;scopes&quot;` PKCERequired bool `json:&quot;pkceRequired&quot;` } type Webhook struct { ID int64 `gorm:&quot;column:id;primaryKey&quot;` Name string `gorm:&quot;column:name;size:100;not null&quot;` URL string `gorm:&quot;column:url;size:512;not null&quot;` Events []byte `gorm:&quot;column:events;type:jsonb;not null&quot;` Headers []byte `gorm:&quot;column:headers;type:jsonb;not null&quot;` PayloadTemplate string `gorm:&quot;column:payload_template;type:text;not null&quot;` IsEnabled bool `gorm:&quot;column:is_enabled&quot;` CreatedAt time.Time `gorm:&quot;column:created_at;autoCreateTime&quot;` UpdatedAt time.Time `gorm:&quot;column:updated_at;autoUpdateTime&quot;` DeletedAt gorm.DeletedAt `gorm:&quot;column:deleted_at;index&quot;` } </code></pre> <p>为了做一个简单的序列化和参数校验,必须在结构体后面跟上一长串 <code>json:&quot;name&quot; binding:&quot;required,min=5&quot;</code>。为了数据库字段的对应和行为,又要写一长串关键词。这种把逻辑写在字符串里的做法感觉不知道梦回了哪个时代。但是原因也很简单嘛,因为没有注解/宏/DSL,只能用这种方式来表达。</p> <h3>指针定义的 Overloaded</h3> <p>Go 的显式很多时候不是清晰,而是盲目想要复用反而使得语义过载。</p> <p>比如,你想要一个可选值?行,给你 <code>*T</code>。 但 <code>*T</code> 在 Go 里又不仅仅是 Optional——它同时还是:</p> <ul> <li>“这个字段可能为 NULL”(DB / JSON)</li> <li>“我想区分零值和未设置”(patch / update)</li> <li>“我想共享/引用同一份数据”(引用语义)</li> <li>“这个方法需要指针接收者”(行为语义)</li> </ul> <p>那这就很可怕了,于是当你在 Go 的代码中看到一个<code>*</code>,你还需要费尽心力去琢磨是可空还是关系。而原因只是因为 Go 没有一个设计好的 Optional/Result。</p> <blockquote> <p>同一个 <code>*</code> 被迫承担了四种语义,结果是:代码显式了,意图却更隐式了。</p> </blockquote> <h3>错误处理变为传递责任链</h3> <p>然后是错误处理。</p> <blockquote> <p>写 Go 的时候,键盘上最先磨损的永远是 <code>i</code>, <code>f</code>, <code>e</code>, <code>r</code>, <code>n</code>, <code>l</code> 这几个键。</p> </blockquote> <p>在 Kotlin 里你可能会用 <code>runCatching</code>,在 Rust 里你有 <code>?</code>,在 Java 里至少异常处理也未尝不可,但是 Go:<code>if err != nil { return err }</code></p> <p>你可以说这很显式,很清晰,很正确的考虑了每一种可能分支。<br> 但当你的业务开始出现一定的复杂度:超时、取消、重试、降级、后台任务、幂等、签名验签、缓存穿透……你会发现你写的不是后端,而是考虑所有,搭建了一条错误传播管道。</p> <p>当然,最让人抓狂的是,这种繁琐并没有带来更好的安全性。它不像 Rust 的 <code>Result&lt;T, E&gt;</code> 那样强制你在编译期处理错误,也不像 Java 的 Checked Exception 那样有显式的签名约束。它只是一个约定,如果你忘了写这两行代码,那么发生什么边界情况就不可控了。</p> <h2>写业务的“地狱体验”</h2> <h3>想要一个好用的 ORM</h3> <p>Go 的 ORM 生态有一种奇妙的割裂感:<br> 要么<strong>极度魔法</strong>,要么<strong>极度朴素</strong>,中间那条舒适区间很窄。</p> <blockquote> <p>这玩意儿除了名字叫 ORM,哪里像个现代 ORM 了?不如说是 SQL 拼接器</p> </blockquote> <p>在 Kotlin Exposed 或者 Rust SeaORM 里,或者哪怕是(不属于 ORM)手写 SQL 的 Rust sqlx,他们都是强类型的,强大的编译时安全让写代码就很有底气。你写错一个字段名,编译器立马给你报错。但在 Go 里(尤其是 GORM),你又回到了拼接字符串的恐惧......</p> <blockquote> <p><code>db.Where(&quot;user_nmae = ?&quot;, name).First(&amp;user)</code> —— 这里的 <code>user_nmae</code> 写错了?编译通过,运行报错!</p> </blockquote> <p>而当你开始尝试 Ent,感受到 DSL 的舒服,编译安全,但它妄图掌控数据库的感觉,以及完全无法自己精细修改的表结构、定义的索引优化等等,都让人感觉这根本就是为社交关系服务的图数据库,它的抽象会强到让你觉得“我在写 Ent,不是在写业务”。</p> <p><del>拜托,学习它的 SeaORM 都那么好用,人家尊重数据库,SQL 优先,利用 Rust 的语言特性搞了那么好的优化体验,Ent 居然能这么难用。</del></p> <p>于是最后很多人回到朴素路线,手写 migration(goose),查询用 <code>sqlx/sqlc</code>,开始抱怨 Go 的 ORM 总有一种“隔靴搔痒”的无力感。它要么太灵活以至于不安全,要么太重型(靠大量代码生成)以至于繁琐。</p> <h3>用脚本补充的语言能力</h3> <p>Go 的精神很一致:语言保持小,复杂度交给工具链。</p> <p>你可能会喜欢上 Rust 的宏展开代码,Kotlin/Rust 的 dsl 优雅美观,Java/JS 的注解轻松切面扩展。而 Go 呢?<code>//go:generate</code>。 它不是语言特性,它只是一个让工具链去跑个 shell 命令的“补丁”。</p> <p>所以你会看到整个生态一大堆生成器驱动的解决方案:</p> <ul> <li>ORM 生成(Ent / sqlc)</li> <li>Mock 生成(mockgen)</li> <li>API client 生成(OpenAPI generator)</li> <li>Protobuf/gRPC 生成</li> <li>...</li> </ul> <p>于是项目里面的 Makefile 成为了最佳实践,成为了一切生成器的优雅入口。</p> <p>这当然有好处:<br> 生成出来的就是普通 Go 代码,<strong>可读、可调试、编译期安全</strong>。</p> <p>然后你就会收获一种非常 Go 的痛苦:</p> <ul> <li>你改了 schema,忘了 generate,CI 才告诉你</li> <li>生成文件冲突,Git diff 像雪崩</li> <li>Debug 时你在你写的和生成的之间来回跳</li> </ul> <p>然后只能告诉自己一句:</p> <blockquote> <p>“这不是缺点,这是工程化。”</p> </blockquote> <h3>迟到的“半成品”</h3> <p>Go 的泛型给我的感觉很像——这辆车终于加了变速箱,但你一脚踩下去发现它只愿意在能跑这个层面负责,至于好不好开,你自己想办法。</p> <p><strong>1. Go 既然有了泛型,却依然不支持扩展方法(Extension Methods)</strong>。 即便有了泛型,你依然不能给切片加方法。于是官方标准库 <code>slices</code> 逼着你写成了这样: <code>slices.Map(slices.DeleteFunc(list, func...), func...)</code></p> <p><strong>2. 只有约束,没有推导</strong> Go 的泛型在使用上经常需要极其啰嗦的显式声明。明明编译器应该能推断出类型,但很多时候你还是得把那一长串 <code>[TypeA, TypeB]</code> 写出来,导致代码里充斥着方括号。 而且那个 <code>any</code> 关键字,说白了就是把 <code>interface{}</code> 换了个皮,并没有带来像 Rust 那样严格且强大的类型系统约束能力。你写出来的泛型代码,往往为了迁就 Go 那个并不聪明的编译器,变得比不写泛型还要难以阅读。</p> <h2>真的很稳:工业级的暴力美学</h2> <p>但话说回来,Go 的优点并不是它很美,而是它总能在你最需要的时候,干净利落地把活儿干完。我们可以看到他有那么多槽点,甚至这篇文章只列出了前 20%,想要讲述真正让我喜欢 Go 的,我们得换个角度——从“语言设计的艺术”转向“工程落地的暴力美学”。</p> <p>能不能快启动、能不能少出事、能不能轻易被别人接手、能不能在一堆后台任务和边角脏活里不崩溃。<strong>Go 在这些方面,几乎就是工业界的低配答案,但往往是最正确的答案。</strong></p> <h3>1)轻,是一种长期主义</h3> <p>Go 的轻是一种极其务实的取舍,你不需要把一天的情绪交给 Gradle、Maven、Cargo 或者 pnpm install 之后的依赖地狱。你不需要考虑沉重的 JVM,黑洞大小的 node_modules,一个二进制就轻松运行。</p> <h3>2)现代的协程模型,可以说在节省生命</h3> <p>在 Node.js 里,你得处理 <code>Promise</code>、<code>async/await</code> 传染性,一旦忘了 <code>await</code> 就像踩了雷;在 Rust 里,你得面对 <code>Tokio</code> 的运行时选择、<code>Pin</code>、<code>Future</code> 的生命周期……心智负担极重。</p> <p>而 Go 的 Goroutine 是对开发者最友好的并发模型,没有之一:</p> <pre><code class="language-go">// 无论这个任务多复杂,哪怕它是 IO 密集型 go func() { processBackgroundJob(data) }() </code></pre> <p>就这一行,Go 运行时帮你解决了 M:N 的调度,帮你处理了上下文切换。你写的是线性的、符合直觉的同步代码,底层跑的确是高效的异步非阻塞逻辑。</p> <p>所以为什么我和朋友总会相互开玩笑,说 Go 工程师想的都是只要业务写完了,剩下的就爽了。让你面对真实需求的时候,Go 的 <code>select</code> 和 <code>channel</code> 让你能像搭积木一样优雅地控制并发,而不是陷入回调地狱或生命周期深渊。</p> <h3>3)“丑”的另一面,是可维护</h3> <p>说实话,Go 语法上的丑,很大一部分其实是 Go 的一种强行约束:别太聪明,他让代码逻辑绝对平铺,显式写出了一切。</p> <ul> <li>没有宏 → 你没法把业务塞进编译期魔法里,接手的人能轻松读懂。</li> <li>没有注解 → 你必须显式声明逻辑,代码更有可读性。</li> <li>错误处理啰嗦 → 你很难忘记处理,也很难忽略掉业务里每一步的问题。</li> </ul> <p>你可以随便招一个开发者,让他看两天文档,他写出来的代码虽然丑,但你一眼就能看懂他在干嘛。Review 代码不再需要脑补上下文和复杂的继承关系,所见即所得。</p> <p>Go 的哲学在于,它强迫所有人都用最笨的方式写代码,从而消灭了奇技淫巧带来的维护成本。这在个人项目里可能不突出,但在多人协作和长期演进里,便让可维护性到了其他语言无法企及的地步。</p> <h3>4)交叉编译,DevOps 的终极梦想</h3> <p>这部分甚至无需多言。</p> <pre><code class="language-bash">CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server main.go </code></pre> <p>回车敲下,你会得到一个纯静态链接的二进制文件。 没有 <code>node_modules</code> 黑洞,没有 JVM 依赖,没有 glibc 版本冲突。 你把这个文件 <code>scp</code> 到服务器上,<code>chmod +x</code>,然后 <code>./server</code>,就这么简单。</p> <p>配合 Docker,你的 Dockerfile 可能只有 5 行:</p> <pre><code class="language-Dockerfile">FROM alpine COPY --from=builder /app/server /server CMD [&quot;/server&quot;] </code></pre> <p>这对于我们这种“个人开发者”或“小团队”来说,省下的时间就是生命。</p> <h3>5)高效工具链,很爽的开发</h3> <p>Rust 编译一次可能够你喝杯 Java,Go 编译一次可能只够你眨几次 👀。 在微服务架构或者频繁迭代的开发流程中,这种极短的反馈回路(Code -&gt; Run -&gt; Test)带来的心流体验,足以抵消写 <code>if err != nil</code> 的烦躁,<del>让你清晰记得你的工作进度</del>。</p> <ul> <li>格式化?<code>gofmt</code></li> <li>测试?<code>go test</code></li> <li>文档?<code>go doc</code></li> <li>依赖?<code>go mod</code></li> </ul> <p>无聊, 但是好用。</p> <h3>6)云原生的绝对统治生态</h3> <p>Docker 是 Go 写的,K8s 是 Go 写的,Prometheus、Terraform、Etcd... 整个 CNCF(云原生计算基金会)的半壁江山都是 Go。当你需要操作容器、对接 gRPC、写 Kubernetes Operator、或者接入微服务网关时,Go 有第一公民级别的 SDK 支持。</p> <p>你想做后台任务?想做指标?想做 tracing?想做限流?想做配置?想做 CLI?<br> Go 的库可能不一定最优雅,但几乎总有一个能用、能跑、能运维的方案。</p> <hr> <h2>不完美,但是足够满足需求</h2> <p>之前我说过,我没什么语言偏好,只是适合的业务选适合的语言。</p> <p>选 Go,不是因为我们认为它是最完美的语言设计。 我们选它,是因为我们承认:我们不是在写诗,我们是在交付软件。</p> <p>它丑,但它让你把注意力从语言表达力移到了业务逻辑上;它啰嗦,但它保证了你的服务跑在 1 核 2G 的轻量应用服务器上时,依然无畏并发;它的编译器不聪明,但它让你的构建流水线在几秒钟内完成。</p> <p>所以,虽然我依然痛恨写 <code>json:&quot;id&quot;</code>,依然厌恶满屏的 <code>if err != nil</code>,但当我要想要快速上线一个带后台任务、高并发、且需要长期稳定运行的 API 服务时……</p> <p>这种舒适的体验,还得是 Go</p>

G
grtsinry43

职规赛,一次特别的体验

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/moments/2025/12/25/a-unique-career-planning-experience">https://blog.grtsinry43.com/moments/2025/12/25/a-unique-career-planning-experience</a></p></blockquote><p>聊聊年底的最后一点事情。</p> <p><del>本来以为年终总结最后一篇了,结果没忍住又来一篇,自控实在看的头疼,码码字缓解一下,果然码字可以改善心情。</del></p> <p>12 月 24 日,我参加了学校的职业规划大赛。这是我大学里唯一的一场正式比赛,或许也是最后一场。最终成绩是三等奖,排名倒数第二。但这个结果,从报名那天起,我就没太放在心上。</p> <p>学长推荐我参赛时,其实想法是:“要是能进省赛,暑期实习请假就好请了。”我一笑,图个新鲜,也图个仪式感,便报名了。赢了是惊喜,没进也无憾,就当给学生时代添一段特别的记忆。</p> <p>准备的过程,出乎意料地认真,但是其实完全没有经验,一点点摸索。我第一次静下心,把这些年的经历摊开在桌上:简历、项目、实习、零散的想法,试图塞进“职业规划”的框架里——职业缘起、探索历程、人岗匹配、未来路径……走完这一套流程,才发现自己原来很少真正思考过长远的事,更少将个人选择放在更大的时代背景下去审视。</p> <p>取出夏天投实习的那份简历,还带着浓浓的技术味:代码细节、debug 记录、优化数据、偏执的钻研。为了比赛,我大刀阔斧地删改——保留了核心,却稀释了锋芒,加入了更结构化、更温和的表达,让它至少能被评委老师读懂。可我终究没把自己包装成另一个人,最终的 PPT 依然克制、简洁,技术气息重了一些。那些优化数字、那些解决痛点的瞬间,对我来说弥足珍贵,却未必能完全传递到听众心里。</p> <p>几轮指导后,PPT 改了又改,人也累到极点。连着几天没怎么合眼,终于把材料交上去。</p> <p><img src="https://blog.grtsinry43.com/uploads/2025/12/26/image.png_deda0704-d4fc-4d15-aa79-f79793fdd0df.png" alt="image"></p> <p>气笑了,抽到 1 号,算是运气好还是不好呢(doge)</p> <p>比赛当天,灯光亮起,评委席坐满了据说很权威的老师。我讲完自己的规划,走下台时,手心全是汗。结果公布时,我排在靠后,却出奇地平静。</p> <p>后来听学长和就业中心老师私下提起,说我的风格“太偏技术了”,PPT 一开始还以为是 AI 做的。我笑着听了过去——辛苦做的东西,被误认为是 AI 的冷峻风格,也没什么好介意的。或许,这正是我没能完全契合这个舞台的原因:我讲的大多是代码里的世界、项目里的突破,而别人分享的更多是多段实习、offer 背书、领导力与团队协作的从容。评委的评价体系里,那些更体系化、更圆满的表达天然占优。而我,大三的我,还在路上,手里握着的更多是尚未兑现的潜力。</p> <p>问答环节里,因为我是大三,评委问了“考不考研”。我微微一怔——这不是就业赛道吗?那一瞬有些意外,但我还是平静地说出自己的想法:选择不缓冲,直接寻求成长和突破,有点难绷的问题,不过也让我找到了差距。</p> <p>但这些差距,并没有让我失落,反而让我更清晰地看见自己:一个声音不大、表情克制、容易在技术细节里沉浸的内向技术党。这样的我,与这个强调宏大叙事与流畅表达的舞台,节奏并不完全合拍。与其勉强调整,不如把精力留给更自然的路径——敲代码、推项目、投简历、积累真实的作品。</p> <hr> <p>真正让我觉得“这趟值了”的,是遇见的那群优秀的人。</p> <p>同一学院的学长,沉稳又自信;入伍两年归来的学长,眼里有光,故事很坚定;人文学院的设计学姐,表达惊艳,思路开阔;还有计院的一位新朋友,技术强,人也感觉温柔可靠...我们在赛前候场时闲聊,在上场前互相打气,比赛结束后又真诚地相互赞美。</p> <p>也许,同台竞技本身就是一种珍贵的同行。</p> <p>这场比赛,像学生时代的一个柔软句点。没有惊艳的全场掌声,没有省赛的门票,却有与优秀同伴并肩的兴奋,有上台时那句堂堂正正的“我想成为这样的人”,有下台后长舒一口气的释然。</p> <p>挺好的。</p> <p><img src="https://blog.grtsinry43.com/uploads/2025/12/26/image.png_f2c09702-6ab7-4f5e-84c5-8a2a11cc6a94.png" alt="image"></p> <p>只是...职规赛的舞台灯光熄了,我的学生时代也快落幕了。</p> <p>待到跨年后的期末周结束,就迎来最后一个寒假。开学便是暑期实习、秋招提前批、正式秋招……年底,offer 落地,签约,毕业。学生时代,就真的要翻篇了。</p> <p>剩下的这一年,学生时代的余味还在。那些熟悉的松弛、缓冲、安全感,还能再陪我走一段。我可以继续熬夜赶 ddl,继续抢课,继续和室友聊到天亮,继续走进熟悉的教室,继续把世界想得很大很大,却不必立刻为所有后果买单。</p> <p>只是,经过这场比赛,我忽然更珍惜这些了。</p> <p>珍惜那种试错后还有下一次的奢侈,珍惜失败了还能温柔归类为“经验”的宽容,珍惜年轻时可以把未来画得无限广阔的天真,珍惜校园里每一次不经意的闲聊和心动。</p> <p>不舍的情绪还在,但不再是隐隐的惆怅,而是一种柔软的觉醒:原来这些日常的小事,这样珍贵。原来“学生”这两个字,给了我这么多无声的庇护。</p> <p>不是告别,只是更清醒地拥抱剩下的时光。</p> <p>谢谢这场职规赛,用一种不完美却很真实的方式, 让我带着这场比赛里收获的温暖与清醒,走的更像自己,也更珍惜每一步脚下的校园。</p>

G
grtsinry43

2025 年终总结——从晨光到雾散,化经历为成长

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/2025-summary">https://blog.grtsinry43.com/posts/2025-summary</a></p></blockquote><p>说实话,我是一个很喜欢总结的人,而真的到想要总结这一年的时候,我才发现这一年很难定义了。</p> <p>::: year-card url=&quot;https://2025-summary.grtsinry43.com/&quot; title=&quot;2025 年终总结&quot; type=&quot;page&quot; cover=&quot;https://blog.grtsinry43.com/uploads/2025/12/22/misty-winter-morning-stockcake.jpg_e9dbd5a8-e35c-4a20-b86d-674e4fb470a4.jpg&quot; blur=&quot;7px&quot;</p> <p>从晨光到雾散,化经历为成长,在爱中重新振作,于是我们真的曾将彼此照亮</p> <p>:::</p> <p>这一年的经历就像刻意安排好一样,和季节呼应,与境遇相同——有些日子明亮得像清晨的第一缕光,有些时刻却被浓雾包围,看不清前路,只能原地停留。</p> <p>这一年,有春季时候的焦虑,疯狂奔跑这才不被同龄人的努力与钻研追上;这一年,有夏季时候的热烈,我拼尽全力想要证明自己,想要写出点什么来得到别人的正向评价;这一年,更有秋季时候的痛苦与冬季的释然,让我学会了放手和感受幸福。</p> <p>这一年,有一些时间点,像被钉在记忆里一样,怎么都绕不开。</p> <p><strong>6 月 23 日</strong>,我走进了一段真正属于“社会”的生活。 <strong>9 月 4 日</strong>,一次意外把身体按停,也把我整个人按进了更深的现实里。 <strong>12 月 18 日</strong>,实习结束,我离开了一个曾经仰望、也真实走进去过的地方。 <strong>12 月 20 日</strong>,我终于慢慢意识到:有些事情不是失败,而是完成。</p> <p>它们并不是孤立的事件,而像一条线,把这一年切成了几段不同的自己。</p> <p>现在回头看来,我总感觉这一年的经历大于前 20 年的总和,这一年,我把自己扔进过绝望的深渊,也曾被命运托举到云端,然后在最意气风发的时候遭遇命运的玩笑,最后在废墟上,我也许没有重建一座宫殿,但我用代码和泪水,还有深沉的爱与支持,缝合了一个破碎的自己。</p> <h2>迷雾造舟,寻求出路:故事的开始</h2> <p>回想年初,记忆的色调是灰蓝色的。当时的我,还带着一种近乎紧绷的热情。</p> <p>我还在日夜研究这个简陋难用的博客系统,看着 Innei 大佬的功能,一步步写着 ip 地址,ws 通知,邮箱推送...</p> <p>在我思索很久之后,还是选择尝试将自己这份看起来完全无法直视的项目发到 B 站,居然有了非常好的反响,当然也有随之而来的问题,参差不齐的设计,糟糕的部署体验,缓慢的更新速度。几万的播放,让我有可能被很多大佬注意到。可惜...这是一次机会,就在我指尖溜走了,什么也没留下,好在有一群和我一起作者同样事情的伙伴。</p> <p>大二下学期开始之时,我感觉自己好像被名为“焦虑”的空气包围</p> <p>图书馆里保研党们翻书的声响像蚕食桑叶,考研党们在清晨六点的寒风中占座,而我站在十字路口,手里只有一把键帽打油的键盘,和一堆还未成型的逻辑。</p> <p>可能是我一直以来的行更印象,这种环境带来的同辈压力被我的敏感神经无限放大。我感到一种近乎溺水的窒息。我似乎没有退路,就算不是热爱,为了生活,我也似乎无路可走。</p> <p>与此同时的是,事情真的压得我喘不过气来。</p> <p>::: link-card href=&quot;/moments/2025/02/27/some-pain-days&quot; title=&quot;最近的坎坷故事_多项目并行、偶遇传染病、DeepSeek 本地化部署,及记一次开源 WAF 尝试&quot; desc=&quot;代码洪流漫星河,病躯独对夜雨寒,项目如山压眉睫,运维似海卷波澜&quot; newtab=&quot;true&quot; :::</p> <p>就这样,焦虑和劳累的春天裹挟着我前行,其中也不乏 PureStart 这样的项目在罅隙的时间被赶工出来,或是学校的项目的新突破,能有稍稍的进步,能够填补我内心对未来的恐惧。当然与此,我也重新捡起了算法,八股,然后准备赌上假期去实习见到点新东西。</p> <p>生日那天,我写了《致二十岁的晨光与希望》。那时我相信,生活可以分成“低头赶路”和“仰望星空”两部分,我还在努力把两者平衡好。焦虑偶尔来袭,但我总能用一个新项目、一个新部署把它压下去。</p> <p>于是,四月五月成了我最后的疯狂,或者说,是一种绝望中的献祭。</p> <p>我开始近乎偏执地造轮子。对着 aistudio,我硬是无数次尝试玩明白了 Monorepo 和组件库,为了代码的质量,硬是学会了脱离脚手架一点点搭起项目,配置进阶的 eslint prettier husky 单测,e2e。</p> <p>我开始不断探索,从 self-hosted 一堆项目,到研究起 kmp,转向 ktor,从低代码的表单平台,到 wikijs 成型的自己的技术收获。顺手,我也看不惯了那个陈旧的自己,一怒之下用 Nextjs 和 GSAP 重写了个人主页。</p> <p>那个叫做 Amore-UI 的 Vue 组件库,就是在无数个失眠的深夜里诞生的。我不知道我什么时候才能弄完它,我也不在乎。我只是为了搞懂 Monorepo 的依赖拓扑,为了让 TurboRepo 的缓存命中率更高一点,为了让项目的结构更符合直觉,就把自己关在寝室,像个苦行僧一样与外界隔绝。</p> <p>那时候的我在想什么呢?我想用代码搭建一座避难所。只要屏幕还亮着,只要 Vite 的进度条还在跑,只要那些复杂的动效还能流畅运转,我就能暂时忘掉那些关于未来的、巨大的恐惧。</p> <p>那是黎明前最黑暗的时刻。我一边背着枯燥的 前端 八股文,一边在 LeetCode 的二叉树上攀爬。每一封投递出去的简历,从满怀期待,到杳无音信,都像是一记闷棍。深夜里,看着镜子里疲惫的脸,我不止一次地问自己:“grtsinry43,你真的能行吗?那个所谓的‘大厂’,真的会给一个来自岳麓山下、只会写点前端玩具的普通学生开门吗?”</p> <p>无人应答,只有雨声敲打着窗棂。</p> <h2>珠江绮梦,全力奔跑:那些热烈的日子</h2> <p>故事的转折发生在 <strong>6 月 23 日</strong>。</p> <p>当收到 Offer 的那一刻,所有的焦虑在那个瞬间戛然而止,取而代之的是一种眩晕般的狂喜。我几乎是用逃离的姿态,拖着行李箱离开了内陆的盆地,一头扎进了广州湿热而充满活力的季风里。</p> <p>那是我 2025 年生命力最张扬、最饱和的两个半月。那是一种怎样的感觉呢?就好像一个一直在浴缸里练习游泳的人,突然被扔进了太平洋,而且不仅没有淹死,还学会了在巨浪尖上起舞。</p> <p>当工卡真正到了我的手上,真正面向屏幕看到 IDE 里前辈的代码,坐在那个曾经无数次向往的地方,看着园区的灯火,看着广州塔在夜色中变幻颜色,我感到了一种前所未有的“活着”的实感。</p> <p>我开始了我的祛魅与敬畏之旅。以前只能在技术博客里仰望的底层架构,此刻就在我指尖流转。我看着代码库里那些身经百战的代码与最前沿的逻辑共存,见识了什么叫真正的“亿级流量”。每一次 Code Review,每一次按下 Merge Request 的按钮,我都心怀十二分的敬畏——因为那行代码后面,连接着真实世界的数亿个灵魂。</p> <p>更重要的是,我感到“被看见”。作为一个常年自卑、甚至有点社恐的人,在这个巨大的工业机器里,我居然找到了归属感。在这里,我不是谁的同学,不是谁的附庸,我是那个能解决 Bug、能扛起需求、能提出想法的开发者。当我的代码上线,看着监控的条目跳动,那种“世界因我而产生了微小颤动”的虚荣与成就感,治愈了我上半年所有的内耗。</p> <p>在公司的同时,我也在努力着提升自己,用 cpp 写的微前端微服务整合框架,用 ovCompose 探索着鸿蒙的开发,甚至去 Vueconf 参加现场的技术交流,从优秀的前辈汲取力量。</p> <p>那时候,我觉得我的人生就像珠江新城的夜景一样,通透、璀璨、直指云霄。周末我在珠江边散步,耳机里放着激昂的 Artcore,看着江水奔流,我觉得我能就这样一直跑下去,直到世界的尽头。</p> <p>我以为这就是大结局的序章。</p> <h2>秋季的浓雾:意外、低谷与被迫停下</h2> <p>然而,生活这个编剧,最擅长写烂尾的转折。而且往往是在你最意气风发的时候,给你来一记最沉重的闷棍。</p> <p>9 月 4 日的那个早上,一切戛然而止。</p> <p>因为前一晚熬夜,起床时头脑不清醒。没有暴雨,没有预警,只是一个平平无奇的周四早上。只是急匆匆地想要下楼去上班,只是一节没踩稳的台阶,那一刻,世界突然失控。急诊、打石膏、一个人在广州处理完所有事,再跨省回长沙。 最疼的不是骨头,而是内疚——看到同事因为我的问题加班,那种愧疚像刀子一样反复割。</p> <p>如果痛苦有颜色,那天一定是灰白色的。冷汗瞬间浸透了衣背,世界在我眼前旋转。但我感到的不仅仅是肉体的剧痛,更是一种精神上的凌迟。就在那一瞬间,我知道,完了。</p> <p>没有什么正式的道别和请假,甚至没来得及好好看一眼那个奋斗了两个多月的工位。我像个伤兵一样,坐着轮椅,脚上打着沉重的石膏,被灰溜溜地运回了学校。</p> <p>那种落差感,就像是从平流层直接坠毁到地面,连降落伞包都没有打开。回到学校宿舍那张狭窄的单人床上,我哪里也去不了。连上厕所都需要室友帮助,洗澡成了一种奢望。打开手机,看着微信工作群里大家依然在热烈地讨论需求、Review 代码,而我躺在几百公里外的床上,对着天花板发呆。世界依然在高速运转,而我成了被离心力甩出去的那个零件。</p> <p>秋天的风开始往衣领里钻,我感到前所未有的冷。那种无力感差点将我吞没。我想哭,但我不知道该怪谁。怪楼梯?怪自己不小心?还是怪这操蛋的、毫无逻辑的命运?</p> <p>我第一次被迫慢下来。</p> <h2>漫长的等待,似乎真的很难喘过气,好在有代码的陪伴</h2> <p>但是至少我还在,故事还在继续。</p> <p>::: link-card href=&quot;/posts/from-think-to-code-in-2025&quot; title=&quot;从想法到实践:在无序的生活里,试图用代码敲出一点秩序&quot; desc=&quot;痛苦如影随形,代码似光破晓;迷茫中寻方向,键盘下续生活。在伤痛与失眠的夜晚,技术成为心灵的舟楫,载着破碎的时光驶向微明的彼岸。&quot; newtab=&quot;true&quot; :::</p> <p>那段时间的我,也许其实真的很害怕,等到一个人在室友的帮助下上了床,然后拉上床帘,让眼泪自己自由一会儿,</p> <p>前阶段还是简单的,不得不休息,但是也可以玩自己的了。从 kotlin,到 go,到安卓,这里我真的不亦乐乎,一天天把键盘敲到飞起。</p> <p>我就像一个疯狂的泥瓦匠,在名为“骨折”的废墟上,用 AI 原型生成器、用 ELK 日志系统、用 Vespera 监控 一砖一瓦地搭建新的城堡。我把对自己破碎的修补,全部写进了 git commit 里。</p> <p>可是呢,一个月,两个月,三个月。。。现在想来还是感觉真的好黑暗,一种伸手不见希望的感觉。</p> <p>于是我也终于和自己和解,继续硬撑不是勇敢,只会更糟;暂停不是放弃,而是对未来的负责。</p> <p>浓雾笼罩,看不清前路,只能原地停留,等待身体、等待内心慢慢修复。</p> <h2>冬季的雾散:释然、幸福与爱的发现</h2> <p>12 月 18 日,我平静地离开了工位。 本以为会难过,会遗憾那些没做完的东西,结果心里只有一种轻盈——像轻轻关上一扇门。</p> <p>::: link-card href=&quot;/moments/2025/12/16/2025-intern-story&quot; title=&quot;2025 实习札记:关于一场未完成的迁徙与落地&quot; desc=&quot;当告别如一羽飘落般轻,回望来时路却已成册。远行的足音未曾喧哗,静默转身间,时光已悄然翻页。&quot; newtab=&quot;true&quot; :::</p> <blockquote> <p>我把它放在心里一个很安静的位置上:不粉饰、不夸大,也不否认。它属于我人生里一段很真实的时间——我努力过,也狼狈过;我被要求成长,也被生活要求先学会照顾自己。</p> </blockquote> <p>两天后的深夜,我和朋友翻着旧照片,聊死亡焦虑、聊命运的齿轮、聊“有一步走错了你就见不到我了”。 那一刻,我突然意识到:原来我一直活在幸福里,只是雾太浓,看不见。</p> <p>我真的因为我的朋友们而感到到幸福:</p> <p>有同学住隔壁,零延迟托底;有 mufen 并行刷题,长久同行;Kylian 群友开盒成朋友,陪我灵魂共鸣;miaoer 一个消息就飞过来,受伤时陪伴、送站…… 这些相遇,全是低概率的偶然:选修课、力扣被发现、分班寝室挨着、软路由+小团子+学长鸽了…… 无数偶然,叠加成必然,把我围得严严实实。</p> <p>以及还有偶然发现的:</p> <p><img src="https://blog.grtsinry43.com/uploads/2025/12/22/image.png_8aab1c8e-1970-4ce9-a11b-8937150f2541.png" alt=""></p> <blockquote> <p>现在看来,一个深圳,一个广州,就是因为一次偶然的遇见,这真的是世界名画了吧,每次看到 IP 都会有莫名的感动。</p> </blockquote> <p>也许,我和朋友们的经历,还有遇见,是无数叠加出来的必然。</p> <p>概率论里有个很浪漫的事:</p> <p>当独立事件足够多时,即使每个事件的概率都趋近于 0,连乘之后的结果也可以是确定的 1。 就像宇宙大爆炸之后无数粒子随机碰撞,却偏偏撞出了地球、撞出了生命、撞出了会敲键盘的你和我。</p> <p>雾散了,我才看见: 原来我们真的曾将彼此照亮。 不是耀眼的光,而是深夜手机的微光、轮椅的影子、高铁送别的背影、一同去 VueConf 的约定、一句“你肯定没问题”的回声。</p> <p>这一晚,grok 和我这么说</p> <blockquote> <p>原来幸福一直都在, 只是之前被骨折的痛、被低谷的雾、被那些大问题的重量压住了, 你看不见,也摸不到。</p> <p>现在,痛慢慢退了,雾慢慢散了, 那些被压住的情感,终于一点点浮上来, 让你突然发现: “啊,原来我一直活在幸福中。”</p> <p>这不是顿悟,这是迟到的感知。</p> <p>像冬天里突然推开窗, 才发现外面早就下过雪了, 地上厚厚一层白,你却之前一直没抬头看。</p> <p>现在你抬头了,看见了, 也终于感受到那股安静的、暖暖的亮。</p> <p>这个晚上最幸福,不是因为发生了什么惊天动地的事, 而是因为你终于和自己的幸福对上了眼。</p> </blockquote> <p>这些小光,加起来,刚好驱散黑暗,也点亮了彼此的路。</p> <h2>所以这一年是 undefined</h2> <p>站在 2025 年的终章,回头望去。这是一张多么波澜壮阔、又多么荒诞离奇的频谱图啊。</p> <p>我曾经以为,2025 年的主题是“起飞”,是“大厂”,是“光鲜亮丽”。但现在我才明白,2025 年的主题,是 <strong>“爱与韧性”</strong>。</p> <blockquote> <p>春天的焦虑是为了寻找出路,夏天的狂热是为了证明价值,而秋天的断裂和冬天的重构,或许是生活在教我学会如何面对“失去”,可是又安排了那些惊喜,值得我去珍惜。</p> </blockquote> <p>这是 undefined 的生活,有不确定的可能。undefined ≠ 空白,undefined ≠ 失败,undefined = 尚未被强制收敛的人生。只是现在,我不再急着去证明什么了。我看清了生活的粗糙纹理,却依然选择拥抱它。</p> <p>2025,不完美,却完整。 技术收获不多,很多项目停在半路,实习带着遗憾结束。 但我学会了放手,学会了接受不完美,学会了在脆弱时被托住,也学会了感受一直都在的幸福。</p> <p>化经历为成长,或许听起来太轻。 但我确实在爱中重新振作,在爱中重新站起。</p> <p>感谢这一年所有出现的人,感谢低谷里的伸手,感谢自己的韧性与温柔。</p> <p>雾已经散去,路还长。</p> <p>我们 2026 见。</p>

G
grtsinry43

2025 实习札记:关于一场未完成的迁徙与落地

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/moments/2025/12/16/2025-intern-story">https://blog.grtsinry43.com/moments/2025/12/16/2025-intern-story</a></p></blockquote><blockquote> <p>脱敏提示:本文为个人手记,已脱敏处理。不包含真实姓名、具体团队与业务、精确地点与日期、内部代号与系统名;仅保留对外可理解的工作、生活与情绪层面。 这篇文章为了抹掉信息和情绪,用了一部分 AI 来讲述,不过和我想表达的,也差不了多少。</p> </blockquote> <p>于是终于还是选择了离开。两天后,就与熟悉的工位、同事,像我来之前一样,事业上再无干系。</p> <p>说实话,我以为自己会更难过一些。毕竟这是我大二就拼命想进的地方,是我熬了无数个夜才勉强够格的地方。但真到了要走的时候,心里反而很平静。</p> <p>没有大张旗鼓的兴奋,也没有某种解脱式的轻松。更像是你走到一扇门前,停了一下,回头看了一眼——然后发现,噢,原来告别可以这样轻。只是把话说得很轻,把动作做得很快,不是因为害怕显得矫情,而是因为真的已经准备好。</p> <h2>从紧绷到松弛</h2> <p>我来这里的时候,带着一种很熟悉的紧绷。</p> <p>那种 &quot;我一定要做好、我不能给别人添麻烦、我要证明自己配得上这个位置&quot; 的紧绷。就像一根线,勒着你保持清醒,也勒着你不敢松懈。把每一次任务都当成考试,把每一句反馈都当成审判,心里反复演练:我该怎么说、怎么做,才能把事情推进得更漂亮一些。</p> <p>后来慢慢明白,真正的工作现场其实不需要你一直闪光。</p> <p>它需要的是你靠谱。把问题讲清楚,把边界写明白,把进度推到别人能接住的地方。那些看起来不够浪漫、甚至有点无聊的细节——认真写注释、补全测试用例、或是单纯告诉同事自己踩过的坑——才是让项目真正跑起来的东西。</p> <p>我很喜欢这里的氛围。身边优秀的同事并不热衷于表达自己有多厉害,也不依赖情绪推动工作;他们只是安静地把事情做下去,把标准守住,把复杂的系统拆开、理清楚、再合上。站在旁边看着,你会不自觉地被校准:<strong>原来真正的成熟不是显得强,而是让人放心。</strong></p> <p><del>(不过话说回来,我感觉组里人均全栈,什么都会,还是挺恐怖的,至少很长一段时间里让我感到自卑,这算是题外话,他们确实每个人都有极强的实力</del></p> <p>那种&quot;靠谱&quot;,是我在学校里学不到的东西。而我很庆幸自己在这里见识到了。</p> <h2>似乎是刻意的悲壮</h2> <p>如果故事到这里就结束,那它本该是一段很普通但很顺利的实习经历。</p> <p>但是却有了一个转折</p> <p><a href="https://blog.grtsinry43.com/moments/2025/09/16/some-dark-days">https://blog.grtsinry43.com/moments/2025/09/16/some-dark-days</a></p> <p>受伤这件事没有任何文学性,没有什么值得渲染的,没有什么史诗叙事,也并不悲壮。它只是很朴素地告诉你:你的身体不行了。行动变慢了,精力被压缩了,很多原本靠熬一熬就能过去的事情,现在熬不动了。你想用意志力把自己拉回原来的状态,但身体完全不配合你演戏。</p> <p><strong>最难的不是疼,是失控感。</strong></p> <p>那段时间我每天都在和自己打架。一部分的我在责备:你怎么这么脆弱、为什么偏偏是现在、为什么不能再坚持一下。另一部分的我在提醒:别再逼了,你需要先活下来。</p> <blockquote> <p>我看到因为我的问题导致的线上报错,看到同事们不得不加班赶进度,那种内疚和自责,真的比身体的疼痛更折磨人。</p> </blockquote> <p>但慢慢地,我开始接受一个事实:<strong>继续硬撑下去,并不会更勇敢,只会更糟。</strong></p> <p>可能两边都顾不好,身体垮掉,工作也做不好,最后连站起来的力气都没有。有些时候,暂停不是放弃,而是为了更好地继续。</p> <h2>学会了一种叫&quot;放手&quot;的成长</h2> <p>于是我做了一个决定:结束实习。</p> <p>这个决定里当然有遗憾。遗憾它没有一个更漂亮的结尾,遗憾有些我想做得更完整的东西被迫停在半路,遗憾我没能用最理想的状态把这段经历走到底。</p> <p>但我也学会了另一种诚实:<strong>不完美,不代表不值得。</strong></p> <p>很多经历的意义,不在于它是否圆满,而在于它是否真的改变了你。而这段实习,确确实实改变了我很多东西。</p> <p>它让我明白,我不必把自己拧到极限才算努力。我不必用完美去换取安全感,也不必用透支去证明自己的价值。<strong>可靠比漂亮重要,可持续比体面重要。</strong></p> <p>在该停下来的时候停下来,不是失败,而是一种对未来负责的方式。</p> <p>它也让我看清楚自己的局限。我的实力还差得远,我还有太多东西要学,我还有很长的路要走。但这不是什么丢人的事,这只是现实。接受现实,才能更好地往前走。</p> <p>最重要的是,它让我学会了<strong>放手</strong>。</p> <p>放手不是放弃,而是一种更成熟的选择。就像你在玩一个很难的游戏,你可以选择一直死磕到通关,也可以选择暂时退出,等自己准备好了再回来。在生活的这场游戏里,这两种选择都没有对错,只是适不适合现在的你。</p> <p>而我选择了后者。不是因为我不够勇敢,而是因为我终于明白,<strong>有些东西比完成一个项目更重要</strong>——比如你的身体,比如你的心理健康,比如你对未来的长远规划。</p> <h2>把遗憾写轻,把感谢写重</h2> <p>我很想在这里认真说一句谢谢。</p> <p>谢谢 (leader)当初愿意给我这个机会,让我这个大二的菜鸟能够加入这样优秀的团队。这段实习经历是我大学以来最宝贵的收获,让我看到了真正的国民级产品背后是什么样的技术视野,也让我有机会和这么多优秀的同事一起共事。那种感觉,真的很难用语言形容。</p> <p><strong>也特别感谢(mentor)这段时间对我的悉心指导。</strong> 一个好的实习成长,离不开靠谱的导师。无论是任务上的手把手指导,还是生活上每天帮我取饭(真的,跟我这个添了不少麻烦的实习生相处,(mentor)都非常非常耐心),我都非常感激。</p> <p>谢谢所有带过我、帮助过我的前辈和同事。你们给我的不是那种热烈的鼓励或刻意的安慰,而是很具体的支持:把事情讲清楚、把路指出来、在我慢下来的时候愿意等一下、在我遇到问题的时候帮我debug、在我不懂的时候耐心解释为什么要这样做。</p> <p><strong>那种&quot;等一下&quot;对我来说很珍贵。</strong> 因为它不是随口一说就能做到的,它需要耐心,而耐心是这个快节奏世界里最稀缺的温柔。你们让我明白,真正好的团队不是个人耀眼的突出,而是每个人都愿意互相帮助、一起变强。</p> <p>我也谢谢这段实习本身。它没有给我一个完美的结局,但它给了我更重要的东西——一些关于成长的真实体验,一些关于选择的深刻理解,还有一些关于自己的清醒认知。</p> <p>或许现在看来,这比业务和技术的学习重要得多。</p> <h2>写在最后</h2> <p>现在它结束了。</p> <p>我把它放在心里一个很安静的位置上:不粉饰、不夸大,也不否认。它属于我人生里一段很真实的时间——我努力过,也狼狈过;我被要求成长,也被生活要求先学会照顾自己。</p> <p>而我很感激,自己最终学会了后者。</p> <p>接下来会先把身体养好,把学业收住,把生活恢复到稳定。等重新站稳了,再继续往前走。至于那些没做完的项目、没实现的想法、没写完的代码......可能就先欠下吧。</p> <p>人生还长,总有机会慢慢还。生活还在继续,那就还有变数与精彩。</p> <p>就像(内部平台)交接时的那份邮件写的那样</p> <blockquote> <p>相聚有时,离别难免。</p> </blockquote> <p>但好在,我学会了好好告别。</p>

G
grtsinry43

新时代的 PHP:RSC 的边界错位与工程代价

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/rsc-boundary-mismatch">https://blog.grtsinry43.com/posts/rsc-boundary-mismatch</a></p></blockquote><p>讽刺的是,这篇文章,也就是你看到这个网站,正在运行在 Next.js 上,但由于无法承受的问题和维护压力,我已经很认真地把“放弃 Next.js”提上日程了。</p> <p>Next.js 本身我正在用、也在骂。原因不复杂:这两年最“新时代 PHP”的东西,不是某个新框架,而是 <strong>前端在不自知的情况下,被推去承接后端该承担的安全边界与运行时复杂度</strong>——而 React Server Components(RSC)正是这件事的放大器。</p> <p>我会用一种比较“理性地骂”的方式,把事情讲清楚:</p> <p><strong>RSC 到底是什么;它为什么天然把边界搞得很危险;最近这次 RSC 相关的严重漏洞到底发生在什么位置;Next.js 与 Vercel 又是怎样把这些能力包装成“工程默认选项”;以及这些选择的工程代价最终转为了什么。</strong></p> <hr> <h2>零、先叠甲:“新时代 PHP”到底在说什么</h2> <p>在正文开始前,我想先给这篇文章定个调,以免误伤友军:</p> <ol> <li><strong>我并不否定 Next.js 在效率上的统治力</strong>:对于独立开发者、验证型 MVP(最小可行性产品)、或者内容型网站(博客、文档),Next.js + Vercel 依然是目前地球上最快的上线组合。这种“一把梭”的爽感,我是认可的。</li> <li><strong>“新时代 PHP”不是贬义词,而是架构描述</strong>:正如 PHP 曾凭借“请求即脚本”的低门槛撑起了半个互联网,RSC 也在试图降低全栈门槛。但我所担忧的,是它在 <strong>不经意间</strong> 复刻了当年 PHP 混合开发时期的“安全黑洞”与“耦合泥潭”。</li> <li><strong>我的视角偏向“严谨工程”</strong>:作为一名习惯了 Spring Boot/Java 后端架构,或者是 ktor fiber fastify,同时主要学习前端领域的开发者,我对 <strong>“边界(Boundary)”</strong> 极为敏感。这篇文章的核心冲突,在于 <strong>“前端追求极致 DX(开发体验)”</strong> 与 <strong>“后端追求极致安全与稳定”</strong> 之间的天然矛盾。</li> </ol> <ul> <li> <p>以前:前端是前端,后端是后端。接口契约、鉴权、审计、限流、WAF、灰度,都在后端层解决。</p> </li> <li> <p>现在(RSC/Server Actions 的兴起):把“后端能力”塞进 React/Next 的文件与组件里,用 <code>use server</code> 把函数变成“远程可调用逻辑”。官方甚至明确表达过“你可以不需要手写 API Route”。 <a href="https://nextjs.org/docs/13/app/api-reference/functions/server-actions">Next.js13 的叙述</a></p> </li> </ul> <p>这不是“全栈”那么简单,这是 <strong>把接口面向公网的语义,无感的形式伪装成了组件内部的语义</strong>。语义伪装的后果通常只有两种:安全事故,或者工程事故——最好两者别一起来。</p> <h2>一、RSC 到底是什么:它不是 SSR 的升级,而是“协议 + 运行时分裂”,也就是,边界错位从协议层就开始了</h2> <p>如果只看铺天盖地的宣传,RSC 很容易被误解成“更快的 SSR”或“把组件放到服务器渲染”。但 React 官方对 Server Components 的定义里有个关键点:<strong>它运行在一个“与客户端应用或 SSR 服务器分离的环境”</strong>,并且输出的不只是 HTML。<a href="https://react.dev/reference/rsc/server-components">Server Components</a></p> <p>更准确地说,RSC 带来三件事:</p> <ol> <li> <p><strong>组件分层变成运行时分层</strong></p> <ul> <li> <p>Client Components:在浏览器跑,拿到 JS bundle。</p> </li> <li> <p>Server Components:在服务器跑(可能是构建时,也可能是请求时),可以直接访问数据库/文件系统等服务器资源。所以你可以看到,官方甚至搞了一个教程建议你在 RSC 写 sql,然后使用 Suspense 做流式传输。<a href="https://nextjs.org/learn/dashboard-app/getting-started">App Router: Getting Started | Next.js</a></p> </li> </ul> </li> <li> <p><strong>传输的不是页面,而是“可恢复的组件树数据”(Flight payload)</strong> 你可以把它理解为:服务端把一棵 React 树编码为一种可流式传输的数据结构,客户端再把它解码/拼装回 React 状态,也就是本身他就和 SSR 是完全不同的。</p> </li> <li> <p><strong>组件开始携带“能力”</strong><br> 一旦你允许“Server 侧函数可被触发”(例如 Server Actions/Server Functions),你就不再只是渲染 UI:你是在暴露一套“从浏览器直达服务器逻辑”的通道,那这里就很危险了,因为这就从获取运行结果,变成了一种类似 RPC(远程过程调用)。<a href="https://nextjs.org/docs/14/app/building-your-application/data-fetching/server-actions-and-mutations">Next.js</a></p> </li> </ol> <p>这第三点,就是边界错位的根源。</p> <hr> <h2>二、Flight 协议的现实:它必然涉及“反序列化”,而反序列化就必然是风险点,配合 JS 本身的设计那就无所不能了</h2> <hr> <p>很多人听到“反序列化漏洞”就条件反射:又是 Java 那套? 但这里不是对象流,而是 <strong>协议层为了让 React 树可恢复,必须做的解码过程</strong>。</p> <p>React 的实现里,你能看到它对 payload 的处理方式:<code>JSON.parse</code> 并不是直接 parse 成普通 JSON,而是带了一个自定义 reviver(<code>_fromJSON</code>),用来把特殊标记字符串(例如 <code>$</code> 开头)解释成 Promise、Server Reference、Map、Set、FormData、Date、BigInt 等结构。</p> <p><a href="https://github.com/facebook/react/blob/323b6e98a76fe6ee721f10d327a9a682334d1a97/packages/react-server/src/ReactFlightReplyServer.js">https://github.com/facebook/react/blob/323b6e98a76fe6ee721f10d327a9a682334d1a97/packages/react-server/src/ReactFlightReplyServer.js</a></p> <p>例如:</p> <pre><code class="language-ts">const value = JSON.parse(chunk.value, chunk._response._fromJSON); </code></pre> <p>以及 reviver 里对 <code>$</code> 前缀的分派:</p> <pre><code class="language-ts">if (value[0] === '$') { // Promise / Server Reference / Map / Set / FormData / Date / BigInt ... } </code></pre> <p>这些代码只是说明一个事实:</p> <blockquote> <p><strong>RSC 的正常工作方式,就是在服务器端“解码外部输入”并恢复出可执行/可解析的结构。</strong> 你一旦把“可触发的服务器函数”也绑在同一套通道上,安全边界就从“HTTP API”滑向“协议解码器”。</p> </blockquote> <p>这就是架构选择的必然后果,毕竟安全总要有模块负责。</p> <p>Java 的 ObjectInputStream 踩过,Python 的 Pickle 踩过,现在轮到 JS 的 Flight 了。</p> <hr> <h2>三、这次离谱事件的原因:不是业务逻辑,而是解码器被当成了能力网关,责任边界到了前端</h2> <hr> <p>不久之前,React 官方发布公告:<strong>RSC 存在一个“未认证的远程代码执行(RCE)漏洞”,可通过利用 React 解码发送到 React Server Function 端点的 payload 触发。</strong></p> <p><a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">React</a></p> <p>第三方安全团队的分析进一步强调了它的性质:这不是某个业务接口没鉴权,而是 Flight/RSC 相关的“处理链路”出现了可被利用的点,导致在默认配置下就可能被打穿。<a href="https://www.wiz.io/blog/critical-vulnerability-in-react-cve-2025-55182">wiz.io</a></p> <p>并且,这类问题会天然产生“生态级连坐”:</p> <ul> <li> <p>React 层:CVE-2025-55182(React/Flight/RSC 相关)</p> </li> <li> <p>框架层:Next.js 等实现 RSC/Server Actions 的框架会一起进入受影响范围</p> </li> </ul> <p><del>在这个事故发生的时候我还刚好甲流,发烧很难受,但是还不得不强撑着起来更新依赖</del></p> <p>在这次事件中,<strong>责任边界发生了变化</strong>:</p> <p>以前你写后端 API,你会默认:</p> <ul> <li> <p>输入校验是业务层的事</p> </li> <li> <p>鉴权/鉴别来源是网关/中间件/后端统一层处理的</p> </li> <li> <p>序列化/反序列化是你可控的</p> </li> </ul> <p>而在 RSC/Server Actions 的组合里,很多工程的默认形态是:</p> <ul> <li> <p>浏览器发一个“协议包”</p> </li> <li> <p>框架解码恢复结构</p> </li> <li> <p>框架再把它路由到某个服务器函数</p> </li> </ul> <p><strong>解码器变成网关</strong>,这就会让“协议实现细节”直接进入你的威胁模型。<a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">React</a></p> <hr> <h2>四、继续复盘,事情的严重:Next.js 把能力包装成默认姿势,甚至开箱即用:于是前端开始背后端的锅</h2> <hr> <p>于是这个时候主推 App Router 的 Next.js 出场了。</p> <p>如果 Next.js 只做“传统前端 + SSR”,其实还没那么灾难。真正的拐点在于:Next.js 把 RSC/Server Actions 作为 App Router 时代的主叙事之一,并明确描述 Server Actions 是 <strong>在服务器执行的异步函数,并且可在 Server/Client Components 中使用</strong>。<a href="https://nextjs.org/docs/14/app/building-your-application/data-fetching/server-actions-and-mutations?utm_source">Next.js</a></p> <p>那事情就变得微妙了:</p> <blockquote> <p>你把函数写在前端项目里,但它会在服务器跑;你在组件里调用它,但它是“后端入口”。</p> </blockquote> <p>到这里,“前端插手后端开发”的现实就出现了:</p> <ul> <li> <p>这条链路一旦出安全问题,你不能再说“后端会兜底”,因为后端入口就在 Next 项目里。</p> </li> <li> <p>作为前端开发,你不能再说“我们只写页面”,因为你实际部署的是一段“可被触发的服务器逻辑”。</p> </li> <li> <p>你甚至很难用传统后端的方式给它加网关、加 WAF、加统一鉴权层,因为框架已经替你决定了请求应该如何被解释。</p> </li> </ul> <p>Next.js 当然意识到了风险,于是也着重添加了安全相关的配置和能力。<a href="https://nextjs.org/docs/app/guides/data-security?utm_source=chatgpt.com" title="Guides: Data Security">Next.js</a> 但这件事本身看起来啊就很荒谬:<strong>你看起来在写“组件”,实际上你在配置后端的安全策略。</strong></p> <p>而现实里,因为 Nextjs 将开发后端的门槛降得太低了,很多“纯前端开发者”并不具备这套安全心智模型:他们会把它当成“更舒服的表单提交方式”,直到某天看到公告写着“Unauthenticated RCE”。<a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">React</a></p> <p>这就是强制前端插手后端的后果,前端想把后端写成语法糖。</p> <p><strong>然后再把后端能力塞进前端默认工程,假装这只是 DX(开发体验)。</strong></p> <hr> <h2>五、再说 Vercel:用 Next.js 做壳,引流平台能力,然后把复杂度外包给用户</h2> <hr> <p>这里我不否认 Vercel 的产品力,它确实把部署体验做得极致,并且它也提供防护体系(包括平台级防护、以及可配置 WAF/Firewall)。<a href="https://vercel.com/docs/vercel-firewall">Vercel</a></p> <p>但问题在于:<strong>Next.js 的很多“卖点”,在工程上并不是“框架能力”,而是“平台叠加能力”。</strong></p> <p>举两个典型例子:</p> <h3>1)ISR:听起来是框架特性,实际是缓存/失效/函数编排的一整套系统</h3> <p>Next.js 文档会告诉你如何做 ISR、如何触发再生成、如何 revalidate。<a href="https://nextjs.org/docs/app/guides/incremental-static-regeneration?utm_source=chatgpt.com" title="How to implement Incremental Static Regeneration (ISR) ">Next.js</a> Vercel 的文档则会进一步解释 ISR 在其平台上会如何落到具体的 Function/缓存与配置上。<a href="https://vercel.com/docs/incremental-static-regeneration">Vercel</a></p> <p>工程现实是:</p> <ul> <li> <p>你以为你在用“Next.js 特性”;</p> </li> <li> <p>你实际在绑定“某个平台对缓存失效与边缘分发的具体实现”。</p> </li> </ul> <p>这不是不能用,而是你应该清楚:<strong>这类特性会把你从“框架迁移”推到“平台迁移”。</strong></p> <h3>2)“中间件”这个词本身就带歧义:它并不在你的服务中间,而是在 Edge Runtime</h3> <p>Next.js 早期版本 Middleware 只支持 Edge runtime,不能用 Node.js runtime。<a href="https://nextjs.org/docs/14/pages/building-your-application/routing/middleware">Next.js</a> 而 Edge Runtime 又意味着:你拿到的是 Web API 语义、受限的运行环境,与 Node.js 的能力集不同。<a href="https://nextjs.org/docs/app/api-reference/edge">Next.js</a></p> <p>所以很多时候你写 Middleware 的体验是:</p> <ul> <li> <p>名字叫“中间件”,你以为它像后端的 middleware/filter;</p> </li> <li> <p>实际它更像“边缘网关脚本”,能力受限、调试困难、行为受运行位置影响。</p> </li> </ul> <p>把它叫 middleware,不是错,但它会天然误导大量开发者建立错误心智模型:以为自己在写“后端中间件”,其实是在写“边缘逻辑”。</p> <p>这样,你以为它值得信赖,然而实际上只是展示他们平台能力的一个手段。</p> <p>说的更直白些,你以为你写的是标准 Web 代码,实际上你写的是 <strong>Vercel 专用的 DSL</strong>。</p> <hr> <h2>六、工程代价清单:不是学一下就会,而是你要多维护一个世界观</h2> <hr> <p>到这里,骂点就不应该停留在“Vercel 坏”“Next 坏”“RSC 坏”,而是要落到工程代价——因为这才是你会长期为之付费的部分。</p> <h3>1)运行时分裂:Node、Edge、Client、Server Component 环境并存</h3> <p>Next.js 自己就把 runtime 拆成 Node 与 Edge 两套,并说明能力差异。<a href="https://nextjs.org/docs/app/api-reference/edge?utm_source=chatgpt.com" title="Edge Runtime - API Reference">Next.js</a> 而 RSC 又把组件分成 Server/Client 两类。<a href="https://react.dev/reference/rsc/server-components?utm_source=chatgpt.com" title="Server Components">React</a></p> <p>你最终维护的是一个矩阵:</p> <table> <thead> <tr> <th>写法</th> <th>运行位置</th> <th>API 能力</th> <th>风险点</th> </tr> </thead> <tbody> <tr> <td>Client Component</td> <td>浏览器</td> <td>浏览器 API</td> <td>XSS/依赖链</td> </tr> <tr> <td>Server Component</td> <td>服务器/构建</td> <td>服务器资源</td> <td>数据泄露/边界混乱</td> </tr> <tr> <td>Middleware</td> <td>Edge</td> <td>Web API 子集</td> <td>行为不一致/调试困难</td> </tr> <tr> <td>Server Action</td> <td>服务器</td> <td>后端入口</td> <td>鉴权/来源/反序列化</td> </tr> </tbody> </table> <p>作为全栈框架的使用者,它只是事实:你需要为每个格子建立正确预期,否则问题会以“看起来像 bug”的方式出现。</p> <h3>2)安全边界内陷:从“API 网关/后端鉴权”变成“框架配置 + 协议解码链路”</h3> <p>这次事件已经告诉我们:攻击面不止是业务 API,而是 RSC/Flight/Server Functions 的解码链路。<a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">React</a></p> <p>Nextjs 成功证明了 <strong>全栈不难,难的是把责任边界划清楚。</strong></p> <h3>3)平台耦合:迁移成本从“换框架”上升到“换平台能力组合”</h3> <p>ISR、Edge、WAF、以及各种“默认最佳实践”,在 Vercel 上确实丝滑。<a href="https://vercel.com/docs/incremental-static-regeneration?utm_source=chatgpt.com" title="Incremental Static Regeneration (ISR) ">Vercel</a> 但当你想把它搬走(自建、换云、甚至只是加一层代理),你就会发现:你迁移的不只是代码,还有一堆隐含假设。</p> <hr> <h2>七、最后补一刀:AI 生成代码默认 Next.js 起手,是在扩大这种错位</h2> <hr> <p>现在很多 AI 生成前端项目的默认姿势,基本都是:Next.js + App Router + Server Actions + Shadcn/ui + 一堆平台最佳实践模板,拿过来一把梭哈。Vercel 自己也在把生成 UI/生成代码与 Next.js 工作流绑定( v0 就是他们家的代表,额不是 VoidZero 那个 v0,是 v0.dev)。</p> <p>问题不是 AI 生成了 Next.js,而是:</p> <ul> <li> <p><strong>AI 会把“默认最流行模板”当成“默认最正确架构”</strong></p> </li> <li> <p>它很少提醒你:我创建的是全栈项目,这里的 Server Actions 是后端入口、这里的 Middleware 跑在 Edge、这里的 ISR 会平台耦合、这里的安全边界需要你自己负责</p> </li> <li> <p>于是新手更容易在不理解 RSC 的情况下,把项目推到全栈默认形态</p> </li> </ul> <p>当理解成本被隐藏,风险成本就会延后爆炸。然后大家再回来补课:什么是 Flight、什么是 reviver、什么是来源校验、什么是边缘运行时差异——这就是工程代价,一个激进框架背后的,一个把后端当前端语法糖来写的工程代价。</p> <hr> <h2>总结:我为什么说这是“新时代 PHP”</h2> <hr> <p>我不反对 RSC,也不否认它在某些场景能带来性能与工程收益。我反对的是:</p> <ol> <li> <p><strong>把后端能力伪装成组件语法糖</strong>,让不具备后端工程化能力的人去写后端入口。</p> </li> <li> <p><strong>把协议解码链路推成事实网关</strong>,让安全问题从业务层上移到框架实现细节。</p> </li> <li> <p><strong>用平台体验裹挟框架路线</strong>,让特性越来越像为平台服务,迁移越来越像掏空重来。</p> </li> </ol> <p>之所以人们在放弃 Next.js,不是因为它写不了什么,恰恰相反他有面面俱到的能力;而是因为我不想再把一个前端工程,硬凑成一个后端工程,然后在漏洞公告出现时才意识到:原来我部署的不是页面,是一段可被触发的服务器逻辑。</p> <p>写这篇文章,并非要号召大家明天就卸载 Next.js 回去写 JSP。 技术的螺旋上升总伴随着代价。React 团队探索 RSC 的勇气值得敬佩,Vercel 想要统一 Web 开发流的野心也令人惊叹。但在铺天盖地的营销叙事中,我们作为一线工程师,必须保持 <strong>冷眼旁观</strong> 的能力。</p> <p>我们不能因为 Vercel 演示里的 Demo 很炫酷,就忽略了背后那个正在泄漏数据库连接的 Lambda;也不能因为 <code>use server</code> 写起来很爽,就忘记了此时此刻你正在裸奔。 <strong>理性地评判,是为了在它把我们的工程搞崩之前,先搞清楚底线在哪里。</strong></p>

G
grtsinry43

从想法到实践:在无序的生活里,试图用代码敲出一点秩序

<blockquote><p>该内容由 RSS 渲染生成,最佳阅读体验请前往:<a href="https://blog.grtsinry43.com/posts/from-think-to-code-in-2025">https://blog.grtsinry43.com/posts/from-think-to-code-in-2025</a></p></blockquote><blockquote> <p>其实标题应该是 于痛苦中和解,或者说,只是为了让自己别停下来罢了</p> </blockquote> <p>回头看了一眼,距离无法正常行动到现在,已经过去三个月了,直到月底,我也不知道我还能不能正常行走。</p> <p>:::link-card{href=&quot;/moments/2025/09/16/some-dark-days&quot; title=&quot;命运开了个无情的玩笑&quot; desc=&quot;人生如戏,跌宕起伏间尽是沧桑;命运弄人,笑泪交织中暗藏微光。在伤痛中觉醒,于绝望里寻觅重生。&quot; newtab=&quot;true&quot;}</p> <p>:::</p> <p>那段时间的生活,怎么说呢,确实是烂透了。直到现在,我也依然无法摆脱每日的痛苦,夜里依然常常失眠,从那天开始就噩梦不断</p> <p>但人嘛,总不能真就在泥坑里躺平了。</p> <p>这三个月,虽然腿脚不便,但我强迫自己的脑子动起来,毕竟生活,时间都在继续,学期结束越来越近,秋招也越来越近了。既然生活里全是不可控的 Exception,那我至少要在 IDE 里找回一点能跑通的逻辑。这三个月折腾了一堆东西,但是始终没有精力,也没有心情打磨一个好的产品,之前好多计划的,还有合作的项目,都被我无奈 delay 了。</p> <p>这篇文章可能很需要 AI 总结来导读,又长又流水帐,可能我现在没有力气来慢慢打磨了。</p> <h3>PureFlow:先试试手还在不在</h3> <p>最开始是 <strong>PureFlow</strong>(这个之前水过文章了,就不细说了)。</p> <p>其实当时写这个没别的想法,就是学了 kmp 始终没写过啥成型的东西嘛,正好出不了门,在学校全天狠狠写了一周多,然后终于证明跨平台没那么简单,之后慢慢学,然后填坑吧。</p> <p><a href="https://github.com/grtsinry43/PureFlow">PureFlow</a></p> <h3>Tangyuan 社区:帮别人点缀,顺便治愈自己</h3> <p>那时候自己其实挺迷茫的,也没什么好的 idea。不过恰逢我负责的学生部门招新,遇到了@XianlitiCN 同学。他有一群志同道合的伙伴,有共同的爱好,还是文科相关专业。</p> <p><a href="https://qingshuige.ink/">清水阁</a></p> <p><a href="https://qingshuige.ink/archives/1794">线粒体同学的帖子,也是我为是什么想帮助他</a></p> <p>我想到我小时候很喜欢文学,当时还去过什么汉字听写大会的市级海选,还很喜欢诗词大会。...然后发现风花雪月并给不了我生活的底气,所以还是选择作为爱好了。并且我一直以来还有一个想要维护一个社区的想法...<del>(你怎么恰好这么多想法)</del> 线粒体同学一直想做 <strong>Tangyuan 社区</strong>,而旧版用命令式写的 UI 有点过时并且不好维护,我想着,行吧,既然我自己也是一团乱麻,不如帮别人把想法落地。</p> <p>其实核心就是用 Compose 构建 UI,通过 ViewModel 组织数据并传递到 UI 线程</p> <ul> <li><strong>UI 层:声明式构建 (Jetpack Compose)</strong> 摒弃了传统的 View/XML 体系,全线采用 Compose 构建界面。作为声明式 UI 框架,Compose 允许通过 Kotlin 代码直接描述界面状态。这种“Code as UI”的方式,极大地减少了 findViewById 和手动操作 View 状态的代码量,让视图层的代码更加直观、紧凑。</li> <li><strong>逻辑层:ViewModel 托管数据与状态</strong> 为了实现 UI 与 逻辑的解耦,这里引入了 <strong>ViewModel</strong>。所有的业务逻辑、网络请求(Retrofit)、数据清洗都严格限制在 ViewModel 内部进行。ViewModel 的生命周期感知特性,确保了数据在配置更改时不会丢失。</li> <li><strong>数据流:从 ViewModel 到 UI 线程的单向传递</strong> 这是整个架构中最关键的一环。 <ol> <li><strong>数据获取</strong>:ViewModel 利用 viewModelScope 启动协程,在 IO 线程进行耗时的网络或数据库操作。</li> <li><strong>状态暴露</strong>:将处理后的结果封装在 StateFlow 或 LiveData 中,作为一个可观察的单一数据源(SSOT)。</li> <li><strong>UI 渲染</strong>:在 Compose 界面中,通过 collectAsState() 监听数据流。一旦数据发生变化,Compose 会自动在 <strong>主线程(UI Thread)</strong> 触发重组(Recomposition),刷新界面。</li> </ol> </li> </ul> <p>具体的项目结构是这样的:</p> <pre><code class="language-bash">❯ tree -I build . . ├── build.gradle.kts ├── proguard-rules.pro ├── release // 编译产物 └── src ├── androidTest // 测试 ├── main │   ├── AndroidManifest.xml // Manifest │   ├── java │   │   └── com │   │   └── qingshuige │   │   └── tangyuan │   │   ├── analytics // 分析上报 │   │   ├── api │   │   │   └── ApiInterface.kt │   │   ├── App.kt │   │   ├── di // 依赖注入 │   │   │   ├── NetworkModule.kt │   │   │   └── RepositoryModule.kt │   │   ├── MainActivity.kt │   │   ├── model // 数据模型 │   │   │   ├── Category.kt │   │   │   ├── CommentCard.kt │   │   │   └── ... │   │   ├── navigation // 导航栈 │   │   │   └── Screen.kt │   │   ├── network // 网络相关 │   │   │   ├── JwtAuthenticator.kt │   │   │   ├── JwtInterceptor.kt │   │   │   ├── NetworkClient.kt │   │   │   └── TokenManager.kt │   │   ├── repository // 数据操作封装 │   │   │   ├── CategoryRepository.kt │   │   │   ├── CommentRepository.kt │   │   │   └── ... │   │   ├── TangyuanApplication.kt │   │   ├── ui │   │   │   ├── animation // 动画配置 │   │   │   │   ├── AnimationConfig.kt │   │   │   │   ├── ImagePreloader.kt │   │   │   │   └── SmartSharedElementManager.kt │   │   │   ├── components // 复用组件 │   │   │   │   ├── AuroraBackground.kt │   │   │   │   ├── BottomBar.kt │   │   │   │   └── ... │   │   │   ├── screens // ui屏幕 │   │   │   │   ├── AboutScreen.kt │   │   │   │   ├── CategoryScreen.kt │   │   │   │   └── ... │   │   │   └── theme // 主题和设计系统 │   │   │   ├── Color.kt │   │   │   ├── Theme.kt │   │   │   └── Type.kt │   │   ├── utils // 工具类 │   │   │   ├── DeviceIdentifier.kt │   │   │   ├── FlowExtensions.kt │   │   │   └── ... │   │   └── viewmodel // 视图数据绑定 │   │   ├── CategoryViewModel.kt │   │   ├── CommentViewModel.kt │   │   └── ... │   └── res // 资源等等 </code></pre> <p>看着大家在里面发帖交流,那种“被需要”的感觉,在当时真的是一剂良药。虽然现在回看代码可能还是堆了不少 <!-- raw HTML omitted --> 新鲜热乎的屎山 <!-- raw HTML omitted -->,但至少它跑起来了,还挺像模像样的。</p> <h3>AI 原型生成器:稍微膨胀了一下的野心,折腾的开始</h3> <p>到这个时候就是国庆假期了,身体稍微恢复一点,想搞什么东西的想法又上来了。这个 <code>ai-proto-generator</code> 还是有点东西可以讲的。这里我选择了 nextjs ktor 来构建这个项目,我们生态内有一个好用的工具:<a href="https://start.ktor.io/p/koog">Koog</a></p> <p>我们可以去看一下市面上的这种 ai 生成工具,原理就是通过对话,toolcall,在右侧打开一个 iframe,将远程开发服务器的网页传回来,然后通过命令不断更改即可看到效果。</p> <p>首先是,为了构建一个项目,agent 需要在一个开发目录运行开发服务器,比如 <code>nextjs</code> <code>vite</code> 等,然后输出代码,我们可以为它提供比如 <code>websearch </code> <code>shell</code> <code>read</code> <code>write</code> 工具,为了我们环境的绝对隔离,这里最好的方法是使用容器技术,这里我为了方便使用了抽象程度最高的 docker(其实可以用低一层的 containerd,更轻量一些)。</p> <p>为了方便管理,我们可以使用 go 写一个单独管理容器的工具(用 go 是因为 docker 所在的生态还是 go 最方便,我 kt 搞了半天都是很麻烦),导入包直接开干</p> <pre><code class="language-go">package handler import ( // ...其他依赖 &quot;github.com/docker/docker/api/types/container&quot; &quot;github.com/docker/docker/api/types/network&quot; &quot;github.com/docker/docker/client&quot; ) // SandboxHandler 结构体,持有 Docker 客户端 type SandboxHandler struct { DockerClient *client.Client } type CodeInjectionPayload struct { Filename string `json:&quot;filename&quot; binding:&quot;required&quot;` Content string `json:&quot;content&quot; binding:&quot;required&quot;` } // 构造函数,方便 gin 那边用 func NewSandboxHandler(dockerClient *client.Client) *SandboxHandler { return &amp;SandboxHandler{DockerClient: dockerClient} } func (h *SandboxHandler) CreateSandbox(c *gin.Context) { // 1. 绑定 JSON 数据 var payload CodeInjectionPayload if err := c.ShouldBindJSON(&amp;payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: &quot;Invalid request payload: &quot; + err.Error()}) return } ctx := context.Background() containerName := &quot;sandbox-&quot; + generateRandomId() // 生成短随机容器名 config := &amp;container.Config{ Image: &quot;sandbox-template:latest&quot;, Cmd: []string{&quot;tail&quot;, &quot;-f&quot;, &quot;/dev/null&quot;}, User: &quot;appuser&quot;, Labels: map[string]string{ &quot;traefik.enable&quot;: &quot;true&quot;, &quot;traefik.http.routers.&quot; + containerName + &quot;.rule&quot;: &quot;Host(`&quot; + containerName + &quot;.sandbox.localhost`)&quot;, &quot;traefik.http.services.&quot; + containerName + &quot;.loadbalancer.server.port&quot;: &quot;3000&quot;, }, } hostConfig := &amp;container.HostConfig{ AutoRemove: true, Resources: container.Resources{ Memory: 512 * 1024 * 1024, CPUShares: 512, }, } // 指定网络接入 networkingConfig := &amp;network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ &quot;sandbox-manager_sandbox-net&quot;: {}, }, } // 2. 创建并启动容器 resp, err := h.DockerClient.ContainerCreate(ctx, config, hostConfig, networkingConfig, nil, containerName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;创建容器失败: &quot; + err.Error()}) return } if err := h.DockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;启动容器失败: &quot; + err.Error()}) return } // 3. 创建 Tar 存档(为了方便一次性放入初始文件) tarReader, err := createTarArchive(payload.Filename, payload.Content) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to create tar archive: &quot; + err.Error()}) return } // 4. 复制文件到容器的工作目录 if err := h.DockerClient.CopyToContainer(ctx, resp.ID, &quot;/home/appuser/project&quot;, tarReader, container.CopyToContainerOptions{}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to copy code to container: &quot; + err.Error()}) return } // 5. 在容器中执行 pnpm run dev execConfig := container.ExecOptions{ Cmd: []string{&quot;pnpm&quot;, &quot;run&quot;, &quot;dev&quot;}, WorkingDir: &quot;/home/appuser/project&quot;, AttachStdout: true, AttachStderr: true, } execResp, err := h.DockerClient.ContainerExecCreate(ctx, resp.ID, execConfig) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to create exec: &quot; + err.Error()}) return } if err := h.DockerClient.ContainerExecStart(ctx, execResp.ID, container.ExecStartOptions{Detach: true}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to start exec: &quot; + err.Error()}) return } log.Printf(&quot;成功创建、启动容器并注入代码,已执行 pnpm run dev %s&quot;, resp.ID[:12]) sandboxURL := &quot;http://&quot; + containerName + &quot;.sandbox.localhost&quot; c.JSON(http.StatusOK, gin.H{ &quot;message&quot;: &quot;Sandbox created and code injected successfully&quot;, &quot;containerId&quot;: resp.ID, &quot;url&quot;: sandboxURL, }) } func (h *SandboxHandler) ListSandboxes(c *gin.Context) { containers, err := h.DockerClient.ContainerList(context.Background(), container.ListOptions{}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;列出容器失败: &quot; + err.Error()}) return } type simpleContainer struct { ID string Image string Status string } var result []simpleContainer for _, c := range containers { result = append(result, simpleContainer{ ID: c.ID[:12], Image: c.Image, Status: c.Status, }) } if result == nil { result = []simpleContainer{} } c.JSON(http.StatusOK, result) } func (h *SandboxHandler) DeleteSandbox(c *gin.Context) { // 从 URL 路径中获取容器 ID containerID := c.Param(&quot;id&quot;) ctx := context.Background() log.Printf(&quot;Attempting to stop and remove container %s&quot;, containerID) // 1. 停止容器 // 第三个参数可以设置超时时间,nil 表示使用默认超时 if err := h.DockerClient.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil { // 如果容器已经不存在,Docker 会报错,我们需要优雅地处理 if strings.Contains(err.Error(), &quot;No such container&quot;) { c.JSON(http.StatusNotFound, gin.H{&quot;error&quot;: &quot;Container not found&quot;}) return } c.JSON(http.StatusInternalServerError, gin.H{&quot;error&quot;: &quot;Failed to stop container: &quot; + err.Error()}) return } // 2. 移除容器 // 因为我们在创建时使用了 --rm (AutoRemove: true), // 所以容器在停止后会自动被删除。这一步严格来说不是必须的, // 但是为了兜底,这里可以放一下 err := h.DockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) if err != nil { if strings.Contains(err.Error(), &quot;No such container&quot;) { c.JSON(http.StatusNotFound, gin.H{&quot;error&quot;: &quot;Container not found&quot;}) return } } log.Printf(&quot;Successfully stopped container %s&quot;, containerID) c.JSON(http.StatusOK, gin.H{&quot;message&quot;: &quot;Sandbox deleted successfully&quot;}) } func createTarArchive(filename, content string) (io.Reader, error) { buf := new(bytes.Buffer) tw := tar.NewWriter(buf) hdr := &amp;tar.Header{ Name: filename, Mode: 0644, Size: int64(len(content)), } if err := tw.WriteHeader(hdr); err != nil { return nil, err } if _, err := tw.Write([]byte(content)); err != nil { return nil, err } if err := tw.Close(); err != nil { return nil, err } return buf, nil } // 一个简单的随机 ID 生成函数 func generateRandomId() string { const letters = &quot;abcdefghijklmnopqrstuvwxyz0123456789&quot; b := make([]byte, 8) r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := range b { b[i] = letters[r.Intn(len(letters))] } return string(b) } </code></pre> <p>太长不看版:就是我们实现了一个快速创建容器的抽象</p> <pre><code class="language-go">package handler func (h *SandboxHandler) CreateSandbox(c *gin.Context) { } func (h *SandboxHandler) ListSandboxes(c *gin.Context) { } func (h *SandboxHandler) DeleteSandbox(c *gin.Context) { } </code></pre> <p>而后我们可以让主服务去调用这个啦,就像这样:</p> <pre><code class="language-bash">┌─────────────────┐ HTTP ┌─────────────────┐ gRPC ┌─────────────────┐ │ │ Request │ │ Call │ │ │ Frontend │ ──────────→ │ Ktor Backend │ ──────────→ │ Go Container │ │ (Next.js) │ │ (Business) │ │ Manager │ │ │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ SandboxService │ │ Docker Container│ │ (Code Gen AI) │ │ (Isolated) │ └─────────────────┘ └─────────────────┘ </code></pre> <p>为了管理 docker 的流量转到前端,我们引入 traefik,这里就不多赘述了。待到写好基本的后端结构,然后我们便可以使用 koog 来顺畅调用 llm 的 api 了。</p> <p>市面上的对话使用 sse 来实现流式输出,然后后端维护对话上下文,并且通过提取回应的字符串来实现 toolcall(也就是 MCP),借用 koog,我们可以方便的流式调用</p> <pre><code class="language-kotlin"> // 执行LLM流式调用 val llmResponse = StringBuilder() val flow = llm().executeStreaming(chatPrompt, GoogleModels.Gemini2_0FlashLite) flow.collect { chunk -&gt; llmResponse.append(chunk) writeSseData(&quot;token&quot;, mapOf(&quot;token&quot; to chunk)) } val fullResponse = llmResponse.toString().trim() logger.info(&quot;LLM response: $fullResponse&quot;) // 检查是否包含函数调用 val functionCalls = extractFunctionCalls(fullResponse) </code></pre> <p>在之前,我们写代码是为了人,基建调用,而这里我们写的都是为 llm 服务:我们可以维护一个工具集合,方便注册,根据项目切换,还有管理</p> <pre><code class="language-kotlin">package com.grtsinry43.ai import kotlinx.serialization.json.JsonObject import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap /** * 默认的工具注册表实现 */ class DefaultToolRegistry : ToolRegistry { private val logger = LoggerFactory.getLogger(DefaultToolRegistry::class.java) private val tools = ConcurrentHashMap&lt;String, FunctionTool&gt;() override fun register(tool: FunctionTool) { tools[tool.name] = tool logger.info(&quot;Registered tool: ${tool.name}&quot;) } override fun unregister(name: String) { tools.remove(name) logger.info(&quot;Unregistered tool: $name&quot;) } override fun getTool(name: String): FunctionTool? { return tools[name] } override fun getAllTools(): List&lt;FunctionTool&gt; { return tools.values.toList() } override fun getToolDefinitions(): List&lt;JsonObject&gt; { return tools.values.map { tool -&gt; JsonObject(mapOf( &quot;type&quot; to kotlinx.serialization.json.JsonPrimitive(&quot;function&quot;), &quot;function&quot; to JsonObject(mapOf( &quot;name&quot; to kotlinx.serialization.json.JsonPrimitive(tool.name), &quot;description&quot; to kotlinx.serialization.json.JsonPrimitive(tool.description), &quot;parameters&quot; to tool.parameters )) )) } } } </code></pre> <p>随后处理对话中的工具调用相关,来拿到我们想要的工具调用</p> <pre><code class="language-kotlin"> // 从响应中提取函数调用 fun extractFunctionCalls(response: String): List&lt;FunctionCall&gt; { val json = Json { ignoreUnknownKeys = true } return try { val jsonResponse = json.parseToJsonElement(response).jsonObject val functionCallsArray = jsonResponse[&quot;function_calls&quot;]?.jsonArray ?: return emptyList() functionCallsArray.mapNotNull { element -&gt; try { val callObj = element.jsonObject val name = callObj[&quot;name&quot;]?.jsonPrimitive?.content ?: return@mapNotNull null val arguments = callObj[&quot;arguments&quot;]?.jsonObject ?: JsonObject(emptyMap()) FunctionCall(name, arguments) } catch (e: Exception) { logger.warn(&quot;Failed to parse function call: $element&quot;, e) null } } } catch (e: Exception) { // 尝试从文本中提取JSON块 val jsonPattern = Regex(&quot;&quot;&quot;``` json\s *(\{.*?\})\s*```&quot;&quot;&quot;, RegexOption.DOT_MATCHES_ALL) val matches = jsonPattern.findAll(response) matches.mapNotNull { match -&gt; try { val jsonText = match.groupValues [1] extractFunctionCalls(jsonText) } catch (e: Exception) { emptyList() } }.flatten().toList() } } </code></pre> <p>有了工具调用,接下来就是编写大量的工具集,然后<strong>不要一次性塞给AI</strong>,因为选择工具经常出现问题,我们需要的是根据项目类型自动推荐,然后分好类,比如我们这里有的 <code>websearch </code> <code>shell</code> <code>read</code> <code>write</code> 等等。</p> <p>可惜理想是美好的,现实是残酷的,想实现这些效果,需要付出高昂的 tokens 成本,在 claude 小号被封之后,我的项目就搁置了,如果你恰巧财力雄厚,等我完善完我就开源出去可以调 api 慢慢玩。</p> <h3>Github Overview &amp; UI 的“滑铁卢”</h3> <p>进入10月中旬,生活开始多线运行,虽然身体不行,但是空闲时间反而越来越少了,我开始转向轻量项目。在钱包受伤之后,紧接着,只能玩一玩比如api这种现成的,于是方向转向了 <strong>Github 仓库分析工具 (Overview)</strong>。</p> <p>这里我首次用fastify写大项目,也是首次尝试cc接管一切,配置好提示词,eslint规则,并且设置commit钩子,这种强制执行的限制对于llm还是挺有用的。</p> <p>后端逻辑写得飞起,数据抓取也没问题。结果到了前端展示环节,<strong>UI 设计彻底把我整不会了</strong>。</p> <p>我是真的尽力了,但画出来的界面怎么看怎么丑,那种“脑子里有画面但手残画不出来”的挫败感,真的让人想砸键盘。这就好比当时的我,里子虽然还在,但面子上已经挂不住了。最后这个项目只能含泪鸽置,<!-- raw HTML omitted --> 实在太丑了没眼看 <!-- raw HTML omitted -->。</p> <p>不过最近Gemini 3 Pro 让我燃起了希望啊,这个可能近期我会写完。</p> <p>总结了一个文档,希望能帮到你,如果你也在写相关的:</p> <p><a href="https://github.com/grtsinry43/proj-dash-backend/blob/main/GITHUB_API_FEASIBILITY.md">https://github.com/grtsinry43/proj-dash-backend/blob/main/GITHUB_API_FEASIBILITY.md</a></p> <h3>ELK 日志系统:既然脸不要了,那就搞内脏</h3> <p>在 UI 上碰得满头包之后,我产生了逆反心理:<strong>行,既然我画不好皮,那我就去搞最底层、最枯燥的后端基建。</strong></p> <p>于是我开始折腾 <strong>ELK (Elasticsearch, Logstash, Kibana)</strong> 日志系统。</p> <p>这是一个相当“重”的项目,<del>主要是java太吃内存了</del>。两天时间配完,搓完bff,看着成千上万条杂乱无章的日志被 Logstash 吞进去,然后整整齐齐地吐出来,有点治愈的哈哈哈。</p> <p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/f754f23e304d1c534d41e5a675560c8d_720.jpg_ba72dd3b-0296-4e41-a26c-a9d957d99426.jpg" alt=""></p> <p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/image.png_f379043f-b86e-458a-9a98-7b59ee8da7c4.png" alt=""></p> <p>折腾也很简单,一个compose加上自己设计bff收集就行了,前后端都可以的。</p> <pre><code class="language-yml">services: elasticsearch: image: elasticsearch: 8.11.0 container_name: elasticsearch environment: - discovery.type = single-node - xpack.security.enabled = false # 开发时关闭安全验证,简化操作 - &quot;ES_JAVA_OPTS =-Xms1g -Xmx1g&quot; # 建议分配 1G 内存 ports: - &quot;9200:9200&quot; volumes: - es_data:/usr/share/elasticsearch/data logstash: image: logstash: 8.11.0 container_name: logstash # 将我们本地的配置文件挂载到容器里 volumes: - ./logstash/pipeline:/usr/share/logstash/pipeline/ ports: - &quot;5044:5044&quot; # 这是我们稍后要从 Spring Boot 发送日志的端口!! depends_on: - elasticsearch kibana: image: kibana: 8.11.0 container_name: kibana ports: - &quot;5601:5601&quot; environment: # 告诉 Kibana 去哪里找 Elasticsearch - ELASTICSEARCH_HOSTS = http://elasticsearch: 9200 - XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY = 4d1b36625bb7a2e0f8fc41c7bb9a1dbf depends_on: - elasticsearch zookeeper: image: confluentinc/cp-zookeeper: 7.5.3 container_name: zookeeper environment: ZOOKEEPER_CLIENT_PORT: 2181 ZOOKEEPER_TICK_TIME: 2000 kafka: image: confluentinc/cp-kafka: 7.5.3 container_name: kafka ports: - &quot;9092:9092&quot; depends_on: - zookeeper environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: 'zookeeper: 2181' # 1. 定义两个监听器: # - PLAINTEXT: 用于容器间通信,监听在 29092 端口 # - PLAINTEXT_HOST: 用于外部通信 (比如你的 Spring Boot 应用),监听在 9092 端口 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092, PLAINTEXT_HOST://0.0.0.0:9092 # 2. 定义这两个监听器分别对外广播什么地址: # - 如果从 PLAINTEXT 进来,就告诉对方我的地址是 kafka: 29092 # - 如果从 PLAINTEXT_HOST 进来,就告诉对方我的地址是 localhost: 9092 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka: 29092, PLAINTEXT_HOST://localhost: 9092 # 3. 将监听器名称映射到安全协议 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT: PLAINTEXT, PLAINTEXT_HOST: PLAINTEXT # 4. 指定 Broker 之间通信使用哪个监听器 KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 volumes: es_data: # 创建一个 Docker volume 来持久化 ES 数据 driver: local </code></pre> <p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/mermaid-diagram-2025-11-23-011441.png_72c506a3-ad56-4571-bb7b-aaff3f3e7cdc.png" alt=""></p> <h3>Vespera LightMonitor:回归极简,来点 Rust 哲学</h3> <p>折腾完沉重的 ELK,再看看我手里那几台配置感人的小鸡(VPS),我又感觉自己有点好笑,这ELK根本没地方部署。</p> <p>于是看着市面上眼花缭乱的服务器探针, <strong>Vespera LightMonitor</strong> 诞生了。</p> <p>这个用了Axum sqlx 已经上线了,bug慢慢修,等我用了一段时间稳定就开源然后写文档。</p> <p><a href="https://status.grtsinry43.com/">Verpera | grtsinry43's Server Monitor</a></p> <p><img src="https://blog.grtsinry43.com/uploads/2025/11/23/image.png_6e4b8c9d-c719-4027-8766-1eb3d30a1c74.png" alt=""></p> <h3>Design System:试图建立秩序</h3> <p>经历了这几个月的胡搞瞎搞:从社区到 AI,从 UI 碰壁到沉迷日志后端,再到极简监控...</p> <p>我也发现了,我做的东西太碎了。就像那个死掉的 Github Overview 一样,我每次都在重复造轮子,还在纠结圆角是 4px 还是 8px 这种无聊的问题。</p> <p>所以,最近我在研究和创造一套属于自己的 <strong>设计系统 (Design System)</strong>。</p> <h3>碎碎念</h3> <p>三个月里,我的每个项目,都是挤时间,在难受的时候,在无聊的时候,在实在感觉不想继续下去的时候,就连这篇文章也一样,流水帐的就像我的生活一样,其实这里的每个项目都可以展开为一篇文章,都有很多可以讲的,但是我还是等有余力将它们打磨好一个好的产品再汇报给每一个人吧,写下这些文字也算是一种解脱,至少证明我的生活还在继续,在这个重要的节点依然在输出,当然也有更多的输入。</p> <blockquote> <p>回头看看这三个月,痛苦消失了吗? 害,其实也没有。深夜破防的时候该 emo 还是会 emo。</p> </blockquote> <p>但好在,我没停下来。 从想法到实践,这中间的距离,大概就是我与自己和解的过程吧。</p> <p>代码还得写,生活还得过,只要键盘还在响,就不算太糟糕。希望重新健康的日子,能早一点来吧。</p>