<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://ykdz.me/" xml:lang="zh-CN">
  <id>https://ykdz.me/</id>
  <title>一颗丁子 | ykdz.me</title>
  <subtitle>软件工程 | 烹饪 | Agent | 一颗丁子 | 个人博客 - 不会做饭的软件工程师不是好厨师</subtitle>
  <link href="https://ykdz.me/" rel="alternate" type="text/html" hreflang="zh-CN" />
  <link href="https://ykdz.me/atom.xml" rel="self" type="application/atom+xml" />
  <updated>2026-07-12T06:28:15.000Z</updated>
  <author>
    <name>YKDZ</name>
  </author>
  <generator uri="https://github.com/YKDZ/blog">软件工程 | 烹饪 | Agent | 一颗丁子 | 个人博客 - 不会做饭的软件工程师不是好厨师</generator>
  <entry>
    <id>https://ykdz.me/blog/scaffold/</id>
    <title>脚手架设计与我的 AI 编码实践</title>
    <link href="https://ykdz.me/blog/scaffold/" rel="alternate" type="text/html" hreflang="zh-CN" />
    <link href="https://ykdz.me/blogs/1782921360036-scaffold/index.md" rel="alternate" type="text/markdown" hreflang="zh-CN" title="Markdown 源文档" />
    <published>2026-07-01T15:56:00.036Z</published>
    <updated>2026-07-12T06:28:15.000Z</updated>
    <content type="html">&lt;p&gt;介绍我的脚手架和常用技术栈，以及我在 AI 编码中追求的仓库结构和能力。&lt;/p&gt;
&lt;p&gt;我早期经常使用 &lt;a href=&quot;https://batijs.dev/&quot;&gt;bati&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/vuejs/create-vue&quot;&gt;create-vue&lt;/a&gt; 等通用产品，但是形成了自己的开发工具链和配置习惯后，这种通用项目就不太够用了。&lt;/p&gt;
&lt;p&gt;每次写新项目，前二十分钟的时间都是用来从上一个项目中复制那些通用的配置文件、建立项目的目录结构、安装依赖等，实在是有点烦人。让 Agent 帮忙配置又会导致各种小细节的差错，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript 6.0.0 的 &lt;code&gt;tsconfig.json&lt;/code&gt; 弃用了 &lt;code&gt;baseUrl&lt;/code&gt; 字段，但是 AI 还是倾向于写旧字段&lt;/li&gt;
&lt;li&gt;我习惯将 oxc 等工具的 *.config.ts 放在根目录，但是 AI 一般都会忘记也要将他们纳入根包的 fmt、lint 和 typecheck 中&lt;/li&gt;
&lt;li&gt;就算叫他直接不要指定依赖版本，AI 有时还是会直接写记忆中的依赖版本到 package.json&lt;/li&gt;
&lt;li&gt;AI 生成的 ci 的 &lt;code&gt;uses&lt;/code&gt; 基本都是早已过时的版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本就是老生常谈的模型记忆过时、全局一致性遗漏和供应链风险。&lt;/p&gt;
&lt;p&gt;以及我还有一些电子洁癖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会使用 .devcontainer，且使用自定义 Dockerfile 而不是 features 以确保镜像不会包含我不需要的工具（例如 typescript-node 镜像就自带 eslint 和对应扩展）&lt;/li&gt;
&lt;li&gt;项目结构一定是 Turborepo 管理的 Monorepo，方便程序化地维护包边界防止架构腐坏&lt;/li&gt;
&lt;li&gt;对 TS 库类型的包一定会使用 &lt;a href=&quot;https://turborepo.dev/docs/core-concepts/internal-packages#just-in-time-packages&quot;&gt;JIT-Package&lt;/a&gt;，因此也会广泛使用 &lt;a href=&quot;https://nodejs.org/api/packages.html#imports&quot;&gt;Node.js subpath imports&lt;/a&gt; 功能代替 &lt;code&gt;compilerOptions.path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;会使用最新的 pnpm 和 Node LTS&lt;/li&gt;
&lt;li&gt;会使用 pnpm catalog 统一声明所有依赖的版本&lt;/li&gt;
&lt;li&gt;会让项目有一个根 &lt;code&gt;check&lt;/code&gt; 命令可以运行所有包的 lint、fmt、typecheck、test，方便 Agent 形成反馈循环&lt;/li&gt;
&lt;li&gt;会使用 Dependabot 和 Github Actions&lt;/li&gt;
&lt;li&gt;会在 .vscode 中声明配置和扩展列表&lt;/li&gt;
&lt;li&gt;采取严苛的 oxlint 和 tsconfig 规则集&lt;/li&gt;
&lt;li&gt;tsconfig 中 &lt;code&gt;target&lt;/code&gt;、&lt;code&gt;module&lt;/code&gt; 和 &lt;code&gt;moduleResolution&lt;/code&gt; 的值会用小写（Agent 普遍大写）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;维护一个 skill 当然也可以在一定程度上满足我的需求，但是怎么解决依赖 uses 和 Node 版本最新的问题？每次都让模型去查主页听起来不错，但是我嫌他浪费 token。为了这种高度可重用的过程浪费 Token 很不值得。用自然语言描述我的一些隐性的需求也实在很低效且不稳定。为了提高技术设施层的确定性，不让我的项目从一开始就一股 AI 的混乱味道，我选择自己维护一个脚手架。&lt;/p&gt;
&lt;h2 id=&quot;设计和实现&quot;&gt;设计和实现&lt;/h2&gt;
&lt;p&gt;我的脚手架需要有以下特征和功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个仓库都是 Monorepo，开发进行过程中也可以再次引入新子包&lt;/li&gt;
&lt;li&gt;所有依赖都是可以半自动化更新的，包括 pnpm、npm 包、Rust、Node、.devcontainer 所用镜像和工具链、ci 中的 &lt;code&gt;uses&lt;/code&gt; 等，且对 npm 包，必须可解析原生的版本采用策略和 caret range 以缓解供应链攻击并复用原生的版本控制&lt;/li&gt;
&lt;li&gt;template 和他们的任意组合被生成出来后应该是没有任何 typecheck 和 lint 错误的&lt;/li&gt;
&lt;li&gt;遵守 DRY，同一个版本号 / 文件之类在全库尽量只写一次&lt;/li&gt;
&lt;li&gt;有一个干净的 CLI 可以给 Agent 用&lt;/li&gt;
&lt;li&gt;可以满足所有电子洁癖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我本就要求全部仓库都是 Monorepo，所以实现中就算是一个单包 preset 也直接生成为一个子项目，避免了之后再 add 时还需要迁移原本的单包的麻烦。&lt;/p&gt;
&lt;p&gt;依赖更新当然不能用自定义 updater 实现，基本思路是在仓库根目录维护一个 pnpm-workspace.yaml 和 Cargo.toml，让 Dependabot 去负责更新这两个文件引用的依赖版本。CLI 构造 preset 时再从文件中读取版本即可。&lt;/p&gt;
&lt;p&gt;在项目创建后，就应该遵循依赖最小原则，不应该再强制依赖 template 这个外部工具存在。因此我没有设计任何更新现有项目的功能（add 功能也依赖 &lt;code&gt;.template&lt;/code&gt; 目录存在，不使用就直接删除即可，已经添加的子包则没有任何删除或更新路径），现存项目的的依赖就用生成时包含的 Dependabot 进行更新即可。&lt;/p&gt;
&lt;p&gt;由于有在旧的仓库中 &lt;code&gt;add&lt;/code&gt; 新包的功能，所以需要确保这些 preset 不仅可以单独工作，也可以在被组合后保持功能正常，因此我设计了笛卡尔积测试矩阵，将所有 preset 两两组合并利用 &lt;code&gt;pnpm check&lt;/code&gt; 做 ci 时验证。&lt;/p&gt;
&lt;h2 id=&quot;效果&quot;&gt;效果&lt;/h2&gt;
&lt;p&gt;你可以运行 &lt;code&gt;pnpm dlx @ykdz/template@latest init my-app --preset vike-app --yes&lt;/code&gt; 生成一个新的 vike-app 模板（生成器不会替你 &lt;code&gt;git init&lt;/code&gt; / &lt;code&gt;pnpm install&lt;/code&gt;）。你也可以在根目录执行 &lt;code&gt;x&lt;/code&gt; 向这个 Web APP 添加一个 &lt;code&gt;packages/shared&lt;/code&gt; 的纯 TS 库，我一般把前后端同构的工具函数和 zod / valibot schema 都放在这里。&lt;/p&gt;
&lt;p&gt;有了 template，我可以在几十秒内就初始化一个完全符合我癖好和要求的项目脚手架。我的 Agent 在添加新的 ts 库来做架构隔离时也不需要自己发明仓库结构，而是可以关注业务代码。&lt;/p&gt;
&lt;h2 id=&quot;我的编码实践&quot;&gt;我的编码实践&lt;/h2&gt;
&lt;p&gt;脚手架中的不少细节就只是我的怪癖而已，但是确有几点我认为对 AI 编码确实是有好处的。&lt;/p&gt;
&lt;h3 id=&quot;根-check-任务&quot;&gt;根 check 任务&lt;/h3&gt;
&lt;p&gt;项目中 &lt;code&gt;typecheck&lt;/code&gt;、&lt;code&gt;lint&lt;/code&gt;、&lt;code&gt;fmt&lt;/code&gt;、&lt;code&gt;test&lt;/code&gt;、&lt;code&gt;test:e2e&lt;/code&gt; 等任务的重要性是显而易见的，它们可以在编译前就程序化地暴露错误。对 Agent 编码更是如此，在一轮自然语言指示的变更后，上下文需要回答 &quot;这轮工作是否足质足量地完成了&quot; 的问题。Spec&lt;sup&gt;&lt;a href=&quot;#user-content-fn-spec&quot; id=&quot;user-content-fnref-spec&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; 当然可以用自然语言指示工作的边界，但是只有程序化的错误报告才能消除 Agent 的质量幻觉。&lt;/p&gt;
&lt;p&gt;一个根 check 任务则可以进一步消除有时会因为上下文压缩或指示不清而出现的 &quot;没有完整运行全部测试&quot; 的问题。&lt;/p&gt;
&lt;p&gt;另外既然 Agent 需频繁运行这个任务，那么为了节省 token 和上下文窗口，就最好确保这个任务的输出最短，我认为最好是确保它仅输出详细的错误信息而不输出任何成功的子任务（只产生一行简单的标注等防止幻觉），且尽量不输出装饰用的内容。&lt;/p&gt;
&lt;p&gt;我使用的工具链大多都可以实现类似的效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;oxfmt&lt;/span&gt;&lt;span&gt; --list-different&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;oxlint&lt;/span&gt;&lt;span&gt; --quiet&lt;/span&gt;&lt;span&gt; --format=unix&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;tsc&lt;/span&gt;&lt;span&gt; --pretty&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;vitest&lt;/span&gt;&lt;span&gt; --reporter=agent&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;turbo&lt;/span&gt;&lt;span&gt; --output-logs=errors-only&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抱怨一下 &lt;a href=&quot;https://github.com/nrwl/nx/discussions/35144&quot;&gt;&lt;code&gt;nx&lt;/code&gt;&lt;/a&gt;，他们似乎没有原生实现类似功能的打算。&lt;/p&gt;
&lt;h3 id=&quot;严苛的-lint&quot;&gt;严苛的 lint&lt;/h3&gt;
&lt;p&gt;传统开发中过于严苛的 lint 实在烦人，会拖慢编码人员的效率。但是以 AI 的编码速度，我们就可以为它加上更严苛的 lint 规则集，充分让 lint 在编译前发现潜在问题。典型的规则是 &lt;code&gt;typescript/no-explicit-any&lt;/code&gt;，在 TS 中 AI 很喜欢用宽泛的类型定义逃课。&lt;/p&gt;
&lt;p&gt;严苛的 lint 规则也会显著增加每轮工作中返工的次数，但少即是多，单轮内的返工是对代码质量有明显提升的，相当于某种确定性的 review 过程。&lt;/p&gt;
&lt;p&gt;这里也要明确，只有那些真正对代码安全性和质量有提升的规则才是有价值的，至于 &lt;code&gt;eslint/no-plusplus&lt;/code&gt; 之类我觉得更像是个人嗜好了。&lt;/p&gt;
&lt;h3 id=&quot;每个仓库都是-monorepo&quot;&gt;每个仓库都是 Monorepo&lt;/h3&gt;
&lt;p&gt;在长期工作中的架构腐化一直是很多 skill 和工作流致力于解决的问题。但这些方案无非还是针对文档，在上下文上做文章。但 Monorepo 本身就可以稳定地避免高层架构的腐化。&lt;/p&gt;
&lt;p&gt;首先，Monorepo 的包结构本身有比非 Monorepo 的目录隔离更强的语义，可以传达项目的高层架构。&lt;/p&gt;
&lt;p&gt;另外，Monorepo 也是真的可以程序化地隔离没有在模块内部 export 的内容，比目录隔离不容易发生逃逸。&lt;/p&gt;
&lt;p&gt;最后，它为外部的检测脚本划定了界线。例如 &lt;code&gt;rustdoc&lt;/code&gt; 有将 crate 的模块结构和符号导出为 &lt;code&gt;json&lt;/code&gt; 的功能，用 rust workspace 隔离的 crate 就可以被我的脚本单独分析，避免 Agent 在实现过程中又暴露不应公开的符号，或者将被弃用的老概念又带回来。另外 Turbo 本身还设计了 &lt;a href=&quot;https://turborepo.dev/docs/reference/boundaries&quot;&gt;&lt;code&gt;turbo boundaries&lt;/code&gt;&lt;/a&gt; 的包依赖边界检测功能，我用它避免 AI 编码中本应消费 domain 包的宿主喜欢直接依赖 db 操作数据库对象的问题。这也只有在 Monorepo 中才能实现。&lt;/p&gt;
&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;
&lt;p&gt;AI 编码的可靠性是一个分级处理的过程，我们常用的提示词的堆砌只是其中的一个不确定性比较高的环节。若可能，最好让所有的架构设计都可以像 lint 一样被一系列可以检查的规则描述并拥有与 typecheck 同等的地位。当然想完美做到这一点很困难。至少我会在项目初始化的过程中下很多功夫，尽量在底层稳定地解决问题，让 Agent 每一轮修改都必须经过反馈闭环。&lt;/p&gt;
&lt;hr&gt;
&lt;section&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-spec&quot;&gt;
&lt;p&gt;Specification，在编码中理解为需求描述书，一般就等于那个描述要做什么、不要做什么和怎么算完成三项的 &lt;code&gt;md&lt;/code&gt; 文档 &lt;a href=&quot;#user-content-fnref-spec&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <id>https://ykdz.me/blog/braising/</id>
    <title>焖烤法，让慢炖更省心</title>
    <link href="https://ykdz.me/blog/braising/" rel="alternate" type="text/html" hreflang="zh-CN" />
    <link href="https://ykdz.me/blogs/1781842172576-braising/index.md" rel="alternate" type="text/markdown" hreflang="zh-CN" title="Markdown 源文档" />
    <published>2026-06-19T04:09:32.576Z</published>
    <updated>2026-06-19T09:14:52.000Z</updated>
    <content type="html">&lt;p&gt;焖烤法可以完全消除小火慢炖肉类食材时频繁抄底搅拌的工作量，让慢炖更轻松。&lt;/p&gt;
&lt;p&gt;我偶尔会大批量地做台式卤肉和意式番茄肉酱，吃不完的就冻在冰箱里当作 “小帮手”，懒得做饭的时候就拿出来拌饭拌面，十分好用省事。&lt;/p&gt;
&lt;p&gt;但这类高糖高蛋白质且需要长时间小火慢炖的料理，在炖煮阶段都需要偶尔搅拌，否则就会糊底，且时间越长需要的抄底频率就越高，一不小心就毁了一锅肉。&lt;/p&gt;
&lt;p&gt;为了解决此问题，我探索出了使用焖烤法（Braising）来代替小火慢炖的方法。所谓焖烤法，就是将需要长时间慢炖的食材放到 &lt;strong&gt;珐琅锅&lt;/strong&gt; 内，然后 &lt;strong&gt;带盖&lt;/strong&gt; 直接放到提前预热到 160°C 左右的烤箱中层（烤网上）即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ykdz.me/optimized-images/blogs/1781842172576-braising/assets/91ba8d2abff1.600w.webp&quot; alt=&quot;珐琅锅&quot; width=&quot;600&quot; height=&quot;600&quot; srcset=&quot;https://ykdz.me/optimized-images/blogs/1781842172576-braising/assets/91ba8d2abff1.480w.webp 480w, https://ykdz.me/optimized-images/blogs/1781842172576-braising/assets/91ba8d2abff1.600w.webp 600w&quot; sizes=&quot;(max-width: 767px) 100vw, 672px&quot;&gt;&lt;/p&gt;
&lt;p&gt;最爽的是，一旦食材进了烤箱，在正式出锅前就 &lt;strong&gt;再也不用&lt;/strong&gt; 管它了，只要锅内还有液体，它就基本不会糊底或糊边（在做番茄肉酱时我尝试烤过五个小时以上）。因此我可以真正的 “慢炖”，直到食材达到我期望的状态为止。&lt;/p&gt;
&lt;p&gt;至于原理，我觉得不管是再小的火，毕竟也还是激烈的单点 / 单面加热，锅底局部热流密度太高，而浓稠酱汁中的糖、蛋白质、肉末、番茄固形物、胶质会沉积或贴附在锅底；一旦局部水分不足，这些固形物就会迅速焦化甚至碳化。&lt;/p&gt;
&lt;p&gt;但是焖烤不一样，热空气的热流密度远低于火焰，会将整个锅锅体乃至锅盖都均匀地升温至一个适中的温度，接触食材的那一面很难到达 200°C 以上，也就不会糊锅。&lt;/p&gt;
&lt;p&gt;还有一个好处，由于锅盖也被加热了，所以食材中蒸发出的水蒸气不会像正常一样凝结在锅盖上再落回食材中，而是始终保持蒸汽状态，一部分留在锅内，一部分逸散到烤箱中（途中打开烤箱会有大量蒸汽，小心不要被烫伤），因此一是可以在全程创造一个高湿环境避免食材表面发干，二是可以减缓失水速度，让一锅食材可以承受住更久的慢炖，也就让卤肉和肉酱可以更加软烂融合。实际操作中我最后揭开锅盖时，内面一般都是没有任何水珠的。&lt;/p&gt;
&lt;p&gt;这种做法也可以自然延伸到所有需要慢炖且粘稠、高糖、高蛋白质而极易糊底的料理，比如炖牛肉、咖喱之类。&lt;/p&gt;
&lt;p&gt;当然这种做法也有坏处，就是这种均匀的加热很难做出大火收汁的效果，所以若有需要则还需要重新上灶开火来完成。当然我觉得这点坏处和之前节省的时间精力一比不值一提。&lt;/p&gt;
&lt;p&gt;想象下午把红葱酥炸好、五花肉切好焦化好，一股脑丢进锅中，调好味之后盖上盖子直接放进烤箱，就可以两手一摊做甩手掌柜，等着晚上吃肉了。我想这就是所谓幸福生活吧。&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://ykdz.me/blog/multi-mushroom-pilaf/</id>
    <title>多种菌菇焖饭</title>
    <link href="https://ykdz.me/blog/multi-mushroom-pilaf/" rel="alternate" type="text/html" hreflang="zh-CN" />
    <link href="https://ykdz.me/blogs/1781705584893-multi-mushroom-pilaf/index.md" rel="alternate" type="text/markdown" hreflang="zh-CN" title="Markdown 源文档" />
    <published>2026-06-17T14:13:04.893Z</published>
    <updated>2026-06-18T00:38:57.000Z</updated>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;灵感来自 &lt;a href=&quot;https://www.bilibili.com/video/BV1uh411c7Fm&quot;&gt;高寒讲菌菇的这期视频&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是我常做的一道简单但好吃的主食，特点是方便制作和与之相对的惊艳的复合菌菇风味。&lt;/p&gt;
&lt;p&gt;这道料理在各种短视频里也很常见，但传统做法在预处理菌菇阶段一般会选择简单地用蒜和油打底，将所有菌菇统一炒至脱水上色（甚至有些明显没有炒到位）、统一用酱油调味，然后就放入电饭锅。最终的成品实际上缺少我期望的明显的 “菌菇感”，大多时候就只剩香菇的气味和菌菇的口感了，过于扁平化。&lt;/p&gt;
&lt;p&gt;我改良的思路在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对性质不同的菌菇采取不同的预处理方式和下锅时间&lt;/li&gt;
&lt;li&gt;最大程度利用菌菇在预处理阶段的所有副产物&lt;/li&gt;
&lt;li&gt;在调味和辅料上做减法，从而突出原本脆弱的菌菇风味&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终可以得到有强烈 “蘑菇感” 的焖饭，特点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有菌菇特有的淡淡青草香气而又不让人吃着难受&lt;/li&gt;
&lt;li&gt;主调是咖啡风格的焦香、微苦、微酸，整体有淡淡的木质感&lt;/li&gt;
&lt;li&gt;整体风味厚重深沉但又干净克制，以食材本味为主&lt;/li&gt;
&lt;li&gt;口感多样而不是糊成一团&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;做法&quot;&gt;做法&lt;/h2&gt;
&lt;p&gt;主要的工作量在于对多种菌菇进行多种预处理，举几个我常用的菌菇为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白蘑菇（菜场叫口蘑）、蟹味菇、平菇之类水分大的，可以先切中厚片或保留原样并加少量盐蒸制（保留蒸出的水），然后再煎至表皮出现焦黄色。这类菌菇在蒸制时会受热而大量出水且体积缩小，也就消除了海绵结构，同时让表面更平整，更容易上色&lt;/li&gt;
&lt;li&gt;牛肝菌、黑皮鸡枞菌之类厚实紧致的，可以直接切中厚片当牛排煎制。因为厚切，它们经受长时间电饭煲高温烹煮而仍保留原始风味，入口时有种 “菌菇风味炸弹” 的惊喜感，且口感也是最完整的&lt;/li&gt;
&lt;li&gt;干羊肚菌、干虫草花、干香菇之类干货，直接充分泡发（保留泡制用水），用于提供干菌菇特有的淡淡发酵系风味和深厚的回味。注意我觉得干香菇的风味太重了，容易喧宾夺主，所以可以酌情选择少加甚至不加&lt;/li&gt;
&lt;li&gt;松茸、杏鲍菇之类可生食的，直接切足够薄的片，不做其他预处理，用于提供生菌菇特有的青草香气和一点嚼劲。生的菌菇片可以和米饭一起焖制，但我更推荐 &lt;strong&gt;在确保食品安全的前提下&lt;/strong&gt; 出锅时再趁热下锅搅拌烫熟&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用上述三四种不同的方式处理五六种菌菇即可，太多了也显得成菜杂乱。&lt;/p&gt;
&lt;p&gt;目前这些预处理过的菌菇，单独食用实际上都太具侵略性，这种时候就需要水分和大米将它们的风味略微淡化并融合，也即按照电饭锅焖饭的方式组合。途中所需的任何水分都用处理菌菇时保留的水分，注意控制水量避免变成煮粥。&lt;/p&gt;
&lt;p&gt;另一个重点是调味，我喜欢在处理需要煎炒的菌菇时，出锅前趁热淋上一圈酱油，用余温进行焦化，可以为米饭提供我想要的咖啡风味。另外焦化酱油之后溶于水也可以给米饭上色。&lt;/p&gt;
&lt;p&gt;咸味则大多来自预处理菌菇时添加的琐碎的盐，也可以按需直接在煮饭水中补盐。&lt;/p&gt;
&lt;p&gt;蚝油等酱料我认为没有必要加，都会干扰菌菇风味。&lt;/p&gt;
&lt;p&gt;还可以微量加糖来综合一下风味，当然不加也没什么明显的影响。&lt;/p&gt;
&lt;p&gt;少许酸味对成品的风味有质的提升，例如在成品中少量淋入柠檬汁或白醋（尖锐明亮的酸味）、在煮饭水中加入红酒醋（明亮干净的酸味）、在成品中拌入少量意大利黑醋等等。&lt;/p&gt;
&lt;p&gt;若还想吃点肉，可以选择黑胡椒和盐腌制的、用厨房纸巾吸取水分后充分煎制表皮焦黄香脆的整块去皮鸡腿肉。无论是跟菌菇一起下锅焖制还是出锅后再像法餐一样组合装盘都很不错。选鸡腿肉的原因主要是不能让风味浓郁的肉（牛羊猪等）盖过了主调的菌菇风味，且鸡油和菌菇也很搭（若全程用鸡油替换植物油想来也很不错）。&lt;/p&gt;
&lt;p&gt;我们平常炒杂菇多加蒜，但是我认为蒜在焖饭的高湿环境下会产生烂趴趴的硫味，且抢味，故不加。&lt;/p&gt;
&lt;h2 id=&quot;成菜&quot;&gt;成菜&lt;/h2&gt;
&lt;p&gt;成菜整体是淡黄褐色系。出锅前可以补充一些小葱、欧芹碎之类香草增色，也可以提清香味。&lt;/p&gt;
&lt;p&gt;还可以随喜添加现磨黑胡椒碎，微微辛辣的感觉很配这道菜。&lt;/p&gt;
&lt;h2 id=&quot;改良点&quot;&gt;改良点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可以额外添加油浸菌菇佐餐来让风味更多样化&lt;/li&gt;
&lt;li&gt;为了方便我都是直接用电饭煲做这道菜，但是用 意大利调味饭&lt;sup&gt;&lt;a href=&quot;#user-content-fn-risotto&quot; id=&quot;user-content-fnref-risotto&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; 的思路做或许也可行&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;section&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-risotto&quot;&gt;
&lt;p&gt;特点是全程在平底锅中烹饪，分次添加水分，我觉得它的烹饪温度比电饭煲更低，可以保留更多菌菇风味 &lt;a href=&quot;#user-content-fnref-risotto&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
  <entry>
    <id>https://ykdz.me/blog/first-of-all/</id>
    <title>符合 DRY 和 DIP 的博客渲染器 &lt;:_&gt;</title>
    <link href="https://ykdz.me/blog/first-of-all/" rel="alternate" type="text/html" hreflang="zh-CN" />
    <link href="https://ykdz.me/blogs/1781577987795-first-of-all/index.md" rel="alternate" type="text/markdown" hreflang="zh-CN" title="Markdown 源文档" />
    <published>2026-06-16T02:46:27.795Z</published>
    <updated>2026-06-25T02:46:24.000Z</updated>
    <content type="html">&lt;p&gt;此文旨在测试博客的 md 渲染能力和内容页功能，也顺便介绍一下我的小博客。&lt;/p&gt;
&lt;p&gt;本站点由 vike&lt;sup&gt;&lt;a href=&quot;#user-content-fn-vike&quot; id=&quot;user-content-fnref-vike&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; 驱动，以 SSG&lt;sup&gt;&lt;a href=&quot;#user-content-fn-ssg&quot; id=&quot;user-content-fnref-ssg&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; 模式在构建阶段完成渲染并部署在 &lt;a href=&quot;https://github.com/YKDZ/blog/deployments/github-pages&quot;&gt;GitHub Pages&lt;/a&gt; 上。&lt;/p&gt;
&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;
&lt;p&gt;博客最终还是以内容为中心的，因此我觉得码字体验是高于一切的。我曾尝试过 WordPress、Halo、VitePress、Ghost、Mix Space 等各种形态的博客应用。但这些通用产品都有令我不满的地方，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VitePress 虽然是 SSG，但是毕竟还是为产品文档一类需求设计的，写文章还要考虑 Sidebar 位置，不能做到在一个 &lt;code&gt;.md&lt;/code&gt; 中就完成全部工作。另外文档标题、正文标题、&lt;code&gt;Sidebar Item&lt;/code&gt; 标题又都是分开定义的，很烦人&lt;/li&gt;
&lt;li&gt;Halo、WordPress、Ghost、Mix Space 等首先都是依赖 Web Server 的全栈应用。其次它也不是专门的博客应用，还兼顾知识库、官网等用例，且为了追赶潮流而大都有各种 AI 功能，对我就显得臃肿。另外写文章还得用 Web 编辑器，不够顺手&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此我就做了这么一个简单的小应用来满足自己的需求。用它，我可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接编辑 markdown 文件，不用为了写个博客去花时间适应那些所见即所得编辑器，且不用引入任何 MCP 就可以适配任何 Agent CLI&lt;/li&gt;
&lt;li&gt;产出静态页面，白嫖 GitHub Pages 的服务器，不用为了写博客多花钱&lt;/li&gt;
&lt;li&gt;文章直接以文件形式储存在 GitHub 上，安全可靠&lt;/li&gt;
&lt;li&gt;文章和博客代码维护在一起，写文章的过程中发现的代码问题可以立即修改并同步发布&lt;/li&gt;
&lt;li&gt;从写文章到预览到发布的全流程都可以在一个 VSCode 窗口内完成，很契合我的工作习惯
&lt;img src=&quot;https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.672w.webp&quot; alt=&quot;编辑器&quot; width=&quot;1920&quot; height=&quot;1040&quot; srcset=&quot;https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.480w.webp 480w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.640w.webp 640w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.672w.webp 672w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.768w.webp 768w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.1024w.webp 1024w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.1280w.webp 1280w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.1600w.webp 1600w, https://ykdz.me/optimized-images/blogs/1781577987795-first-of-all/assets/4e019bc19687.1920w.webp 1920w&quot; sizes=&quot;(max-width: 767px) 100vw, 672px&quot;&gt;&lt;/li&gt;
&lt;li&gt;可以自己控制全部样式和布局，因此可以做出现在的这种超级复 &lt;del&gt;ya&lt;/del&gt; 古 &lt;del&gt;yi&lt;/del&gt; 效果&lt;/li&gt;
&lt;li&gt;可以自由扩展我喜欢的阅读器功能，比如 &lt;a href=&quot;https://ykdz.me/blog/first-of-all/#icon&quot;&gt;hover 预览&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;设计理念&quot;&gt;设计理念&lt;/h2&gt;
&lt;h3 id=&quot;dry-和-dip&quot;&gt;DRY 和 DIP&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在一个系统中，每一处知识都必须单一、明确、权威地表达。&lt;br&gt;
《The Pragmatic Programmer》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;A. 高层次的模块不应该依赖于低层次的模块，两者都应该依赖于抽象接口。&lt;br&gt;
B. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。&lt;br&gt;
《Agile Software Development: Principles, Patterns, and Practices》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;博客系统天然需要 &lt;code&gt;Blog(title, description, content)&lt;/code&gt; 这样的实体，但传统的分开定义的方式难免有以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;content&lt;/code&gt; 里的 &lt;code&gt;# 文章标题&lt;/code&gt; 算正文还是标题？渲染和导出时要去重吗？&lt;/li&gt;
&lt;li&gt;我习惯在正文的第一段概括全文内容，我为什么非得把它单独写成文档描述，与文档分离？&lt;/li&gt;
&lt;li&gt;导出为 md 时标题要拼接到文档头部还是 frontmatter，描述呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我认为这是一种违反 DRY&lt;sup&gt;&lt;a href=&quot;#user-content-fn-dry&quot; id=&quot;user-content-fnref-dry&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; 原则的、讨厌的重复。&lt;/p&gt;
&lt;p&gt;VitePress 等很多博客系统引入了 frontmatter 用于定义文档实体的元数据，并让用户可以引用这些数据来解决重复问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;title: 文章标题&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# {{ $frontmatter.title }}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;正文内容&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这就让正文只有在被 VitePress 编译后才能正确渲染，也就是违反了 DIP&lt;sup&gt;&lt;a href=&quot;#user-content-fn-dip&quot; id=&quot;user-content-fnref-dip&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;。&lt;del&gt;而且我觉得 frontmatter 好丑&lt;/del&gt;。&lt;/p&gt;
&lt;p&gt;为了避免上述这些问题，我的博客标题取原始 MD AST&lt;sup&gt;&lt;a href=&quot;#user-content-fn-ast&quot; id=&quot;user-content-fnref-ast&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; 中第一个 &lt;code&gt;heading&lt;/code&gt;，而描述则取标题后的第一个 &lt;code&gt;paragraph&lt;/code&gt;。得益于此，我不用反复书写相同的标题和描述（DRY），也不用为了编译和渲染器需要的实体结构来改变我的 md 文档结构，而是让 &lt;code&gt;文档 -&gt; 解析器&lt;/code&gt; 反转为 &lt;code&gt;文档 -&gt; MD AST &amp;#x3C;- 解析器&lt;/code&gt; —— 典型的 DIP。&lt;/p&gt;
&lt;h3 id=&quot;易用性&quot;&gt;易用性&lt;/h3&gt;
&lt;p&gt;写文章本来就很费心了，我不想再给自己加任务。目前我的工作流是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# 创建文档模板&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; write&lt;/span&gt;&lt;span&gt; second&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;/workspaces/blog/public/blogs/1781614950277-second/index.md&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 启动开发服务器&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pnpm&lt;/span&gt;&lt;span&gt; dev&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以在侧边栏实时预览渲染效果，并在 VSCode 中编辑文档。&lt;/p&gt;
&lt;p&gt;写完后就直接通过 VSCode 的源代码管理 UI 进行提交和推送即可，GitHub Action 会自动触发并完成测试、构建和发布到 Pages 的工作。&lt;/p&gt;
&lt;p&gt;这种两条命令就可以开始写文章的复杂度对我来说是可以接受的，多几个步骤换来的是与编码心智模型相同的码字体验。&lt;/p&gt;
&lt;p&gt;站点还仅在 SSG 期间自动生成 sitemap.xml、robots.txt、llms.txt、atom.xml 等辅助检索、阅读的静态文件，同时将部分图片转换为 webp 以降低网络压力。所有这些功能对码字过程都是透明的。&lt;/p&gt;
&lt;h3 id=&quot;文档优先&quot;&gt;文档优先&lt;/h3&gt;
&lt;p&gt;我用 vite 原生支持的 Static Assets 机制在 &lt;code&gt;public/blogs/&lt;/code&gt; 目录下管理所有文档。&lt;code&gt;pnpm write &amp;#x3C;slug&gt;&lt;/code&gt; 命令会帮我创建以下格式的博客模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;timestamp-slug&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── assets/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└── index.md&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在写文章时，我不用考虑图片等静态文件的放置位置，可以自由地用 &lt;code&gt;public/a/b/c.png&lt;/code&gt;、&lt;code&gt;./assets/a.png&lt;/code&gt;、&lt;code&gt;../xxx/assets/b.png&lt;/code&gt;、&lt;code&gt;../xxx/index.md#title&lt;/code&gt; 等各种方式引用 &lt;code&gt;public&lt;/code&gt; 目录中的静态资源和其他文章等。在渲染时，一个 &lt;code&gt;remark&lt;/code&gt; 插件会完成目录的映射工作，将对文档的引用解析为 &lt;code&gt;/blog/slug/#hash&lt;/code&gt; 的形式，而将其他静态资源解析为相对于 &lt;code&gt;/&lt;/code&gt; 的 url 形式，以便直接利用 Static Assets 机制引用打包好的静态资源。&lt;/p&gt;
&lt;p&gt;同时，文章的创建时间也自然地被通过目录名中的 unix timestamp 维护起来，且可以实现文章自然地按时间顺序在目录下排序。&lt;/p&gt;
&lt;p&gt;最后，&lt;code&gt;index.md&lt;/code&gt; 和存储它的目录基本上完整描述了一篇博客的的所有数据，无需额外的配置文就可以定义一个 &lt;code&gt;Blog&lt;/code&gt; 实体，这种文档优先的做法也进一步降低了码字难度。&lt;/p&gt;
&lt;p&gt;当然我还没有引入文档 tags，也主要是 tags 很难像 title 和 description 一样从 AST 中被提取。这里就只能依赖后人的智慧了。&lt;/p&gt;
&lt;h2 id=&quot;icon&quot;&gt;Icon&lt;/h2&gt;
&lt;p&gt;本站的 favicon 是这个：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ykdz.me/favicon.svg&quot; alt=&quot;favicon&quot; width=&quot;128&quot; height=&quot;128&quot;&gt;&lt;/p&gt;
&lt;p&gt;原图是 &lt;a href=&quot;https://yesicon.app/gravity-ui/code&quot;&gt;Code&lt;/a&gt; 和 &lt;a href=&quot;https://yesicon.app/material-symbols/skillet-cooktop&quot;&gt;平底锅&lt;/a&gt; 两个 SVG 图，自行拼接并改为黑色背景和白色线条。&lt;/p&gt;
&lt;p&gt;我认为软件工程犹如烹饪，是纯粹的创造。因此这个博客除了技术内容之外应该也会整理一些原创食谱。&lt;/p&gt;
&lt;hr&gt;
&lt;section&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-vike&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://vike.dev&quot;&gt;vike&lt;/a&gt; 是基于 vite 的元框架，有极高的自由度，不限制前端框架、渲染策略、后端框架和部署方式等 &lt;a href=&quot;#user-content-fnref-vike&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-ssg&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Glossary/SSG&quot;&gt;Static Site Generator&lt;/a&gt;，静态站点生成器 &lt;a href=&quot;#user-content-fnref-ssg&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-dry&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Don%27t_repeat_yourself&quot;&gt;Don&apos;t Repeat Yourself&lt;/a&gt;，即不要重复自己，对本例来说就是标题写一次就够了 &lt;a href=&quot;#user-content-fnref-dry&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-dip&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/zh-hans/%E4%BE%9D%E8%B5%96%E5%8F%8D%E8%BD%AC%E5%8E%9F%E5%88%99&quot;&gt;Dependence Inversion Principle&lt;/a&gt;，即依赖倒置原则，对本例来说就是高层文档内容与 vue 风格的模板语法强耦合 &lt;a href=&quot;#user-content-fnref-dip&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-ast&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9&quot;&gt;Abstract Syntax Tree&lt;/a&gt;，即抽象语法树，博客使用的解析器是 &lt;a href=&quot;https://github.com/syntax-tree/mdast&quot;&gt;mdast&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-ast&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content>
  </entry>
</feed>
