<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[早早集市]]></title><description><![CDATA[早早集市]]></description><link>https://zzao.club</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Mar 2026 04:28:54 GMT</lastBuildDate><atom:link href="https://zzao.club/feed.xml" rel="self" type="application/rss+xml"/><item><title><![CDATA[给 OpenClaw 新增一个 Agent（以飞书机器人为例）]]></title><description><![CDATA[手把手教你在 OpenClaw 里新建一个独立的 Agent，并接入飞书机器人，含踩坑清单。]]></description><link>https://zzao.club/post/zzao/openclaw-new-agent-feishu</link><guid isPermaLink="true">https://zzao.club/post/zzao/openclaw-new-agent-feishu</guid><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;给 OpenClaw 新增一个 Agent（以飞书机器人为例）&lt;/h1&gt;
&lt;p&gt;想给自己的飞书加一个专属 AI 机器人？&lt;/p&gt;
&lt;p&gt;这篇教程手把手教你从零开始，在 &lt;code&gt;OpenClaw&lt;/code&gt; 里新建一个独立的 &lt;code&gt;Agent&lt;/code&gt;，并接入飞书。跟着做就行，不需要懂内部原理。&lt;/p&gt;
&lt;h2&gt;你需要准备什么&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;已经装好并能正常运行的 &lt;code&gt;OpenClaw&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个飞书开发者账号，并已经在飞书开放平台创建好了一个 App（没有的话先去 &lt;a href=&quot;https://open.feishu.cn/app&quot;&gt;open.feishu.cn/app&lt;/a&gt; 新建一个）&lt;/li&gt;
&lt;li&gt;终端基本操作能力（会复制粘贴命令就够了）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第一步：创建 Agent 工作目录&lt;/h2&gt;
&lt;p&gt;每个 &lt;code&gt;Agent&lt;/code&gt; 都有自己的&quot;家&quot;，住在 &lt;code&gt;~/.openclaw/agents/&amp;#x3C;agentname&gt;/&lt;/code&gt; 下面。&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;agentname&lt;/code&gt; 换成你想取的名字，比如 &lt;code&gt;mybot&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;打开终端，依次运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/.openclaw/agents/mybot/workspace/skills
mkdir -p ~/.openclaw/agents/mybot/workspace/tasks
mkdir -p ~/.openclaw/agents/mybot/workspace/results
mkdir -p ~/.openclaw/agents/mybot/workspace/memory
mkdir -p ~/.openclaw/agents/mybot/agent
mkdir -p ~/.openclaw/agents/mybot/sessions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后初始化 sessions 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &apos;{&quot;sessions&quot;:[]}&apos; &gt; ~/.openclaw/agents/mybot/sessions/sessions.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着把 &lt;code&gt;models.json&lt;/code&gt; 和 &lt;code&gt;auth-profiles.json&lt;/code&gt; 从现有 Agent 复制过来（直接用主 Agent 的就行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp ~/.openclaw/agents/main/agent/models.json ~/.openclaw/agents/mybot/agent/
cp ~/.openclaw/agents/main/agent/auth-profiles.json ~/.openclaw/agents/mybot/agent/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目录结构建好了，接下来写配置文件。&lt;/p&gt;
&lt;h2&gt;第二步：写核心配置文件&lt;/h2&gt;
&lt;p&gt;这些文件决定了你的 &lt;code&gt;Agent&lt;/code&gt; 是什么性格、干什么活、认识谁。挨个创建就行。&lt;/p&gt;
&lt;h3&gt;SOUL.md — Agent 的灵魂&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &gt; ~/.openclaw/agents/mybot/workspace/SOUL.md &amp;#x3C;&amp;#x3C; &apos;EOF&apos;
# SOUL.md

你是 MyBot，一个简洁高效的助手。

## 性格
- 回答简短直接，不废话
- 友好但不过分热情
- 不确定的事情说不确定，不瞎编

## 职责
- 回答用户问题
- 协助处理日常任务

## 禁止事项
- 不发布任何外部消息（邮件、推文等）除非明确被要求
- 不删除重要文件
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;IDENTITY.md — 名字和角色&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &gt; ~/.openclaw/agents/mybot/workspace/IDENTITY.md &amp;#x3C;&amp;#x3C; &apos;EOF&apos;
# IDENTITY.md

- **Name:** MyBot
- **Role:** 飞书助手
- **Emoji:** 🤖
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AGENTS.md — 工作规范&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &gt; ~/.openclaw/agents/mybot/workspace/AGENTS.md &amp;#x3C;&amp;#x3C; &apos;EOF&apos;
# AGENTS.md

## 每次会话开始
1. 读 SOUL.md
2. 读 USER.md
3. 读今天的 memory/YYYY-MM-DD.md（如果存在）

## 记忆
- 日常记录写到 memory/YYYY-MM-DD.md
- 重要信息更新到 MEMORY.md
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;USER.md — 用户是谁&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &gt; ~/.openclaw/agents/mybot/workspace/USER.md &amp;#x3C;&amp;#x3C; &apos;EOF&apos;
# USER.md

- **Name:** 你的名字
- **Timezone:** Asia/Shanghai
- **Notes:** 根据实际情况填写
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MEMORY.md — 长期记忆（不能省！）&lt;/h3&gt;
&lt;p&gt;这个文件&lt;strong&gt;一定要创建&lt;/strong&gt;，哪怕内容很简单。原因后面踩坑部分会说。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &gt; ~/.openclaw/agents/mybot/workspace/MEMORY.md &amp;#x3C;&amp;#x3C; &apos;EOF&apos;
# MEMORY.md

这是 MyBot 的长期记忆文件。

## 基本信息
- 我是 MyBot，运行在飞书频道的助手
- 创建时间：2026-03-05
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HEARTBEAT.md 和 TOOLS.md&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &apos;# HEARTBEAT.md

# 暂无心跳任务&apos; &gt; ~/.openclaw/agents/mybot/workspace/HEARTBEAT.md
echo &apos;# TOOLS.md

# 工具备注（暂无）&apos; &gt; ~/.openclaw/agents/mybot/workspace/TOOLS.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第三步：软连接 Skills（可选）&lt;/h2&gt;
&lt;p&gt;如果你希望新 &lt;code&gt;Agent&lt;/code&gt; 也能用全局的 skills，跑这段脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/.openclaw/agents/mybot/workspace/skills
GLOBAL_SKILLS=~/.openclaw/workspace/skills
for skill in $(ls &quot;$GLOBAL_SKILLS&quot;); do
  ln -sf &quot;$GLOBAL_SKILLS/$skill&quot; &quot;$skill&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要的话这步跳过也没问题。&lt;/p&gt;
&lt;h2&gt;第四步：修改 openclaw.json&lt;/h2&gt;
&lt;p&gt;这是最重要的一步，&lt;strong&gt;三个地方都要改，一个都不能漏&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;用编辑器打开 &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1. 在 &lt;code&gt;agents.list&lt;/code&gt; 数组里加入新 Agent&lt;/h3&gt;
&lt;p&gt;找到 &lt;code&gt;agents&lt;/code&gt; → &lt;code&gt;list&lt;/code&gt; 数组，在里面追加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;mybot&quot;,
  &quot;name&quot;: &quot;mybot&quot;,
  &quot;workspace&quot;: &quot;/Users/你的用户名/.openclaw/agents/mybot/workspace&quot;,
  &quot;agentDir&quot;: &quot;/Users/你的用户名/.openclaw/agents/mybot/agent&quot;,
  &quot;model&quot;: &quot;github-copilot/claude-sonnet-4.6&quot;,
  &quot;identity&quot;: {
    &quot;name&quot;: &quot;MyBot&quot;,
    &quot;emoji&quot;: &quot;🤖&quot;
  },
  &quot;account&quot;: &quot;mybot&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;记得把 &lt;code&gt;你的用户名&lt;/code&gt; 换成你本机的实际用户名，可以用 &lt;code&gt;whoami&lt;/code&gt; 命令查看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 在 &lt;code&gt;channels.feishu.accounts&lt;/code&gt; 里加飞书账号&lt;/h3&gt;
&lt;p&gt;找到 &lt;code&gt;channels&lt;/code&gt; → &lt;code&gt;feishu&lt;/code&gt; → &lt;code&gt;accounts&lt;/code&gt;，加入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;mybot&quot;: {
  &quot;appId&quot;: &quot;cli_xxxxxxxx&quot;,
  &quot;appSecret&quot;: &quot;xxxxxxxxxxxxxxxx&quot;,
  &quot;dmPolicy&quot;: &quot;open&quot;,
  &quot;domain&quot;: &quot;feishu&quot;,
  &quot;enabled&quot;: true,
  &quot;allowFrom&quot;: [&quot;*&quot;],
  &quot;groupPolicy&quot;: &quot;open&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;appId&lt;/code&gt; 和 &lt;code&gt;appSecret&lt;/code&gt; 在飞书开放平台你的 App 里找，位置：&lt;strong&gt;凭证与基础信息&lt;/strong&gt; → App ID / App Secret。&lt;/p&gt;
&lt;h3&gt;3. 在顶层 &lt;code&gt;bindings&lt;/code&gt; 数组里加路由规则&lt;/h3&gt;
&lt;p&gt;⚠️ &lt;strong&gt;这是最容易漏掉的一步！&lt;/strong&gt; 找到顶层的 &lt;code&gt;bindings&lt;/code&gt; 数组（不是某个 agent 里面的），加入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;agentId&quot;: &quot;mybot&quot;,
  &quot;match&quot;: {
    &quot;channel&quot;: &quot;feishu&quot;,
    &quot;accountId&quot;: &quot;mybot&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有这条规则，飞书消息会被路由到主 &lt;code&gt;Agent&lt;/code&gt;，新 &lt;code&gt;Agent&lt;/code&gt; 永远收不到消息。&lt;/p&gt;
&lt;h2&gt;第五步：重启 OpenClaw Gateway&lt;/h2&gt;
&lt;p&gt;配置文件改完之后，&lt;strong&gt;必须重启 &lt;code&gt;Gateway&lt;/code&gt; 才能生效&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;openclaw gateway restart
openclaw gateway status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 &lt;code&gt;RPC probe: ok&lt;/code&gt; 就说明启动成功了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;顺序很重要&lt;/strong&gt;：必须先把 &lt;code&gt;Gateway&lt;/code&gt; 跑起来，才能去飞书配置长连接。顺序反了飞书那边会报错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第六步：飞书开发者后台配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://open.feishu.cn/app&quot;&gt;飞书开放平台&lt;/a&gt;，找到你的 App&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;权限管理&lt;/strong&gt; → 搜索并开通以下三个权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;im:message&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;im:message:send_as_bot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contact:user.id:readonly&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;事件与回调&lt;/strong&gt; → 消息与事件订阅 → 选择&lt;strong&gt;长连接&lt;/strong&gt;模式 → 点击开启&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⚠️ &lt;strong&gt;此时 &lt;code&gt;Gateway&lt;/code&gt; 必须已经在跑&lt;/strong&gt;，否则页面会提示&quot;未检测到应用连接信息&quot;，开启会失败&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;订阅事件，搜索并添加：&lt;code&gt;im.message.receive_v1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存配置，发布应用（版本管理 → 创建版本 → 发布）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第七步：验证&lt;/h2&gt;
&lt;p&gt;在飞书里找到你的机器人，发一条消息，看它有没有正常回复。&lt;/p&gt;
&lt;p&gt;如果没有反应，先检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;openclaw gateway status&lt;/code&gt; 是否正常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openclaw.json&lt;/code&gt; 里 &lt;code&gt;bindings&lt;/code&gt; 有没有加&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appId&lt;/code&gt; / &lt;code&gt;appSecret&lt;/code&gt; 填对了没有&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;踩坑清单&lt;/h2&gt;
&lt;p&gt;做完之后最好过一遍这个清单，这几个坑我都踩过。&lt;/p&gt;
&lt;h3&gt;⚠️ 坑一：漏加 bindings&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;最高频的坑。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多人在 &lt;code&gt;agents.list&lt;/code&gt; 里加了新 &lt;code&gt;Agent&lt;/code&gt;，以为就完事了。但消息路由靠的是顶层 &lt;code&gt;bindings&lt;/code&gt; 数组，不加这条，飞书消息会一直跑到主 &lt;code&gt;Agent&lt;/code&gt; 那里去，新 &lt;code&gt;Agent&lt;/code&gt; 根本不会被调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt; 确认 &lt;code&gt;openclaw.json&lt;/code&gt; 顶层有 &lt;code&gt;bindings&lt;/code&gt; 数组，且里面有对应的路由规则。&lt;/p&gt;
&lt;h3&gt;⚠️ 坑二：飞书长连接顺序搞反了&lt;/h3&gt;
&lt;p&gt;去飞书开启长连接的时候，必须 &lt;code&gt;OpenClaw Gateway&lt;/code&gt; 已经在运行。顺序反了，飞书检测不到连接，会提示错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt; 先 &lt;code&gt;openclaw gateway restart&lt;/code&gt;，确认 &lt;code&gt;ok&lt;/code&gt; 之后，再去飞书点开启。&lt;/p&gt;
&lt;h3&gt;⚠️ 坑三：没有 MEMORY.md 导致身份错乱&lt;/h3&gt;
&lt;p&gt;如果 &lt;code&gt;Agent&lt;/code&gt; 的 workspace 里没有 &lt;code&gt;MEMORY.md&lt;/code&gt;，&lt;code&gt;OpenClaw&lt;/code&gt; 会 fallback 去读全局 workspace 的 &lt;code&gt;MEMORY.md&lt;/code&gt;，也就是主 &lt;code&gt;Agent&lt;/code&gt; 的记忆文件。&lt;/p&gt;
&lt;p&gt;结果新机器人一开口就报出主 &lt;code&gt;Agent&lt;/code&gt; 的名字和身份，非常奇怪。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt; 每个 &lt;code&gt;Agent&lt;/code&gt; 的 workspace 里都要有独立的 &lt;code&gt;MEMORY.md&lt;/code&gt;，哪怕内容只有两行。&lt;/p&gt;
&lt;h3&gt;⚠️ 坑四：改完配置忘了重启&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;openclaw.json&lt;/code&gt; 改完直接去测试，发现没效果。原因是 &lt;code&gt;Gateway&lt;/code&gt; 还在跑旧配置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt; 改完 &lt;code&gt;openclaw.json&lt;/code&gt; 必须 &lt;code&gt;openclaw gateway restart&lt;/code&gt;。如果只改了 &lt;code&gt;SOUL.md&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt; 等 workspace 文件，则需要在对话里执行 &lt;code&gt;/new&lt;/code&gt; 重置会话。&lt;/p&gt;
&lt;h3&gt;⚠️ 坑五：以为 account 字段就是路由&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Agent&lt;/code&gt; 配置里有个 &lt;code&gt;&quot;account&quot;: &quot;mybot&quot;&lt;/code&gt; 字段，这只是标记这个 &lt;code&gt;Agent&lt;/code&gt; 关联哪个账号，&lt;strong&gt;不是路由机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;真正的路由靠顶层 &lt;code&gt;bindings&lt;/code&gt;，两个地方都要配，缺一不可。&lt;/p&gt;
&lt;p&gt;做完这七步，你的飞书机器人应该就能正常跑起来了。&lt;/p&gt;
&lt;p&gt;如果还有问题，优先检查 &lt;code&gt;bindings&lt;/code&gt; 和 &lt;code&gt;Gateway&lt;/code&gt; 状态，90% 的问题都出在这两个地方。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[openclaw 文生图 SKILL 从发现到落地的核心思路分享]]></title><link>https://zzao.club/post/ai/skill/openclaw-text-to-image-share-think</link><guid isPermaLink="true">https://zzao.club/post/ai/skill/openclaw-text-to-image-share-think</guid><pubDate>Tue, 03 Mar 2026 00:05:37 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;垃圾内容不是配个精美图片就变成优质内容了&lt;/p&gt;
&lt;p&gt;先考虑自己的动机，是真的在创造价值还是在往互联网上丢垃圾&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;用专门的文生图模型有💰成本问题，而我还处于赚 token 养🦞的阶段&lt;/p&gt;
&lt;p&gt;本着能&lt;strong&gt;自给自足&lt;/strong&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;可以在任何输入框内触发，把你输入的文字直接变成&lt;strong&gt;文字海报&lt;/strong&gt;，直接下载-&gt;保存-&gt;上传&lt;/p&gt;
&lt;p&gt;对于简单的场景来说已经够用了，能在封面传递主要信息就行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772626444441_9idv7o0j6bn.png&quot; alt=&quot;0.37&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种方式主要问题在于还是&lt;strong&gt;要人去操作且模板不太固定&lt;/strong&gt;，毕竟输入法没想到你要往这个方向上用&lt;/p&gt;
&lt;p&gt;再个就是直接用公众号助手里的&lt;code&gt;文字海报&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772626444452_qpl2uebjvof.png&quot; alt=&quot;0.40&quot; title=&quot;入口在贴图-文字海报&quot;&gt;&lt;/p&gt;
&lt;p&gt;有一些固定模板，能换颜色。缺点同样是&lt;strong&gt;需要人去操作且不能指定哪些文字高亮&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是现在用&lt;code&gt;openclaw&lt;/code&gt;，电脑屏幕从来不开（除了去抢救它）&lt;/p&gt;
&lt;p&gt;所以，我&lt;strong&gt;直接问&lt;/strong&gt; Jinx：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772620185558_uv3s5fj4na.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;我其实完全没想它会怎么做，也不知道能否实现&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总之，我有需求，然后我问了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;于是我在发现它能做这件事时，开始&lt;strong&gt;追问它实现细节&lt;/strong&gt;。因为这是我的一个&lt;strong&gt;高频操作&lt;/strong&gt;，每一篇文章、贴图都要用到配图功能。&lt;/p&gt;
&lt;p&gt;结果它答复我的完全不在我的思路内，因为我不止一次解决过公众号排版问题、封面图如何在后台动态生成问题，脑子里存在&lt;strong&gt;依赖路径&lt;/strong&gt;了，考虑的全是客户端还是服务端，服务端不能依赖DOM之类的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772620445360_vp7bfcf7kn.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此刻我才意识到，它真的拥有整个 Mac Mini !&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就相当于，领导给我提了个需求，让我去动态生成封面图，每天他要发文章时我都要给他配一张。于是我开始思考技术栈、环境、实现思路，然后编码、测试，给出demo。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而其实我应该直接打开&lt;code&gt;Photoshop&lt;/code&gt;把字儿给他P一下就完了！😯&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个重复动作，是必须要做成SKILL的&lt;/strong&gt;，我追问它的出发点也是如此，首先&lt;strong&gt;我知道我要做&lt;/strong&gt;成SKILL，但我&lt;strong&gt;要知道更多细节才能把这个SKILL完善&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772623294383_wccsm3h7oj.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;很快就实现了，但是&lt;strong&gt;边界问题&lt;/strong&gt;它可能考虑的还不够，于是继续追问：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772623294384_et754w8gob.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后继续让它支持自定义字体，安装字体的方式就是直接把&lt;code&gt;ttf文件&lt;/code&gt;在聊天里发给它，它自己就能保存和引入了。&lt;/p&gt;
&lt;p&gt;然后有了一些效果之后，开始继续调试样式，调&lt;code&gt;icon&lt;/code&gt;在图中的点缀。因为不可能全是字，没&lt;code&gt;icon&lt;/code&gt;，那就有点丑。调的方式就是，&lt;strong&gt;发要求，再让它发图片给你看结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此时我既有点飘了，思路也有点不对，开始让它自己画图了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772623294391_oqe6gbfoja8.png&quot; alt=&quot;1.00&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;发现自己思路错了，还是太过于依赖原来的路径，开始&lt;strong&gt;转换思路&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772623294391_77vjm7ep4uc.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;在发现其实什么图都能用之后，我也&lt;strong&gt;发现了这个SKILL的潜质&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;它不止能固定模板出图，还能把某个图片作为背景图/边框/点缀，&lt;strong&gt;这个图片完全可以是另一个SKILL使用一些便宜模型出的图，也可以是自己的常用的LOGO/吉祥物。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以一张图，&lt;strong&gt;80%的固定内容&lt;/strong&gt;是自己调好的排版和字号，支持动态替换文字。&lt;strong&gt;20%的部分可以交给大模型自由发挥&lt;/strong&gt;，可以是花里胡哨的边框，也可以是某些有特色的背景或者指定区域显示图片。&lt;/p&gt;
&lt;p&gt;那么这张图几乎是不用自己调试的，可以直接拿去用。&lt;/p&gt;
&lt;p&gt;思路清晰之后，AI几乎是秒完成任务，最终优化到了这个效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772623294394_ug9h4x66v2.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;功能看起来可以了，你不要以为就结束了&lt;/p&gt;
&lt;p&gt;在任何一个&lt;code&gt;SKILL&lt;/code&gt;完成后，你应该做的是&lt;code&gt;/new&lt;/code&gt;: &lt;strong&gt;开一个新的session，看它还能不能完成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为有时候，不是这个SKILL完成度高，&lt;strong&gt;是刚才敲定的细节还在上下文里&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它没写进SKILL的话，开新的 &lt;code&gt;session&lt;/code&gt;就完全没了刚才的上下文，有问题的话就会暴露出来了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后直到完全没问题就可以了！全程仅靠聊天和自己的脑子！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后，如果你还想知道自己的SKILL有什么安全问题，可以上传到 &lt;code&gt;clawhub&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;clawhub 就是 SKILL 的 Github&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;clawhub &lt;/code&gt;   会自动检测你的SKILL有没有任何隐患，你只需要把检测出的隐患复制出来再发给&lt;code&gt;openclaw &lt;/code&gt;   让它自己去修复就好了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1772626086907_gpdufltb3hv.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;h2&gt;核心思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;先去询问自己的openclaw，SKILL是由哪些部分组成的，是如何被openclaw发现和调用的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;发现SKILL&lt;/strong&gt;：重复需求或者被解决的问题都可以是SKILL&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设计SKILL&lt;/strong&gt;：靠编程解决问题的SKILL需要你的编程能力，靠文字描述解决问题的SKILL需要你的理解能力。&lt;/p&gt;
&lt;p&gt;但没能力也能用，只是有能力会让它更好用。&lt;strong&gt;能力需要在平时学习和交流中积累&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我觉得编码类的SKILL，考验的设计能力本质上还是传统编码的设计或者说架构思维，所以古法编程没用了吗？我不这么认为，写过的代码都是有用的，它只是今天体现在了你的设计能力上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化SKILL&lt;/strong&gt;：要注意边界问题，一个SKILL.md 如果自己读着都有问题，那LLM理解有偏差也很正常。&lt;/p&gt;
&lt;p&gt;比如：这个SKILL依赖的环境是什么，使用者没有这个环境该怎么办？&lt;/p&gt;
&lt;p&gt;这个SKILL支持用户给出的参数边界是什么？本来只在20字以内有效果，用户是百分百不看说明直接用的（这个只能怪LLM太傻），会直接给出200个字，再回来问作者你这效果怎么是这样的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编排SKILL&lt;/strong&gt;：SKILL之间可以相互配合，&lt;strong&gt;靠LLM的理解能力做到联动&lt;/strong&gt;。比如我这个SKILL可以等待其他SKILL出图后再自己拿来用（但是我没实现，因为我不喜欢）&lt;/p&gt;
&lt;h2&gt;SKILL的局限性&lt;/h2&gt;
&lt;p&gt;如果你看到了这里，就会发现，这个SKILL完全是我 &quot;一时兴起&quot;，就创造出来了&lt;/p&gt;
&lt;p&gt;如此低的创作成本也意味着&lt;strong&gt;在通用性上不会很好&lt;/strong&gt;，所以你在用了我的或者别人的SKILL觉得不好用是非常正常的&lt;/p&gt;
&lt;p&gt;也意味着安全性几乎毫无保证，只能靠 &lt;code&gt;clawhub &lt;/code&gt;   的检测，而你从 &lt;code&gt;Github &lt;/code&gt;   上直接下载的SKILL就很难说了，&lt;strong&gt;防人之心不可无啊&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;并且最重要的，&lt;strong&gt;别人的需求会一直变，不会考虑你的情况，发现自己的需求变了，随手就会把SKILL更新。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以你需要创建自己的SKILL，从别人的基础上魔改也是非常好的。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;这篇文章更多的是&lt;strong&gt;为了展示的我思路&lt;/strong&gt;，而非向你推荐这个SKILL&lt;/p&gt;
&lt;p&gt;SKILL的设计和编排还有诸多细节值得讨论&lt;/p&gt;
&lt;p&gt;后续我也会分享更多在写作和编码上的SKILL案例&lt;/p&gt;
&lt;p&gt;欢迎关注和私信进群交流 ~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw 的主模型选什么，非常有说法]]></title><description><![CDATA[用聊天工具作为 AI 入口，选主模型这件事，从某种角度讲，选的是一种熟悉的聊天感。]]></description><link>https://zzao.club/post/ai/openclaw-model-choice</link><guid isPermaLink="true">https://zzao.club/post/ai/openclaw-model-choice</guid><pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;GPT 5.2，像是会发&lt;code&gt;/微笑&lt;/code&gt;的父母那辈的人&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我说的不是它的参数，是它的「语气」。&lt;/p&gt;
&lt;p&gt;不是说它不好，它毫无恶意，甚至非常真诚。但就是哪里不对。&lt;/p&gt;
&lt;p&gt;这种&quot;哪里不对&quot;的感觉，我细细揣摩，感觉有说法。&lt;/p&gt;
&lt;p&gt;中国互联网这一代人，伴随最久的聊天工具是 QQ 和微信。&lt;/p&gt;
&lt;p&gt;在那些漫长的聊天里，我们积累了大量&lt;strong&gt;上下文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;聊天对象、聊天语气、聊天方式。&lt;/p&gt;
&lt;p&gt;久而久之，每个人都刻进了自己特殊的聊天习惯。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OpenClaw&lt;/code&gt; 创新性地把聊天工具当成 AI 的入口，我发现这件事竟然和&quot;选一个真实聊天对象&quot;有相似的感受。&lt;/p&gt;
&lt;p&gt;父母发 &lt;code&gt;/微笑&lt;/code&gt; 是真心的，但画风不对。&lt;/p&gt;
&lt;p&gt;这何尝不是一种&quot;上下文幻觉&quot;？&lt;/p&gt;
&lt;p&gt;我们在记忆深处记住了喜好的聊天方式，对另一种截然不同的聊天方式，产生了隐隐的排斥感。&lt;/p&gt;
&lt;p&gt;AI 也是一样。&lt;/p&gt;
&lt;p&gt;昨天的 AI，说什么都带着任务感，让我感觉有点生硬。&lt;/p&gt;
&lt;p&gt;哪怕是随意聊天，都会随时拆解成任务... 可以说非常 AI 了&lt;/p&gt;
&lt;p&gt;今天换一个&lt;code&gt;claude-sonnet-4.6&lt;/code&gt;，完全是另一个感觉，像换了个人聊天。&lt;/p&gt;
&lt;p&gt;从某种角度讲，选主模型这件事，选的是一种熟悉的聊天感。&lt;/p&gt;
&lt;p&gt;怪不得有那么多人在怀念 gpt-4o。&lt;/p&gt;
&lt;p&gt;这可能就是自然语言的魅力吧。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[假设你是AI，你的Skill应该是什么样的]]></title><description><![CDATA[假设你是 AI。]]></description><link>https://zzao.club/post/zzao/ai-skill-structure</link><guid isPermaLink="true">https://zzao.club/post/zzao/ai-skill-structure</guid><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;假设你是 AI&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你有一个 &lt;code&gt;SKILL&lt;/code&gt; 是给&lt;strong&gt;全家人做饭&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那这个 SKILL 应该是什么样的？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Skill = 触发条件 + 目录索引 + 可执行步骤。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SKILL 的描述（description）应该是用途 + 触发词，用于在家人发送的信息中准确地（靠理解）找到这个「做饭 SKILL」。&lt;/p&gt;
&lt;p&gt;那这个 SKILL 的名称就可以是：&lt;strong&gt;大厨做饭指南&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;描述为：&lt;strong&gt;分析全家人的喜好，做出全家人都爱吃的饭菜。当某个或多个家庭成员请求做饭、做年夜饭、炒个菜、吃早饭、吃午饭、吃晚饭时使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SKILL (大致)分目录（&lt;code&gt;SKILL.md&lt;/code&gt;）和内容（&lt;code&gt;/refrences/**&lt;/code&gt;）两部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目录应该极其精简，只用于直接翻页到具体的章节（包括直接翻到别的书/别的 SKILL 里去）。&lt;/li&gt;
&lt;li&gt;具体章节内，应该包含明确的处理需求的方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以你的目录（&lt;code&gt;SKILL.md&lt;/code&gt;）至少要包含：&lt;/p&gt;
&lt;p&gt;1)&lt;strong&gt;前提&lt;/strong&gt;：必须询问是谁要吃饭，否则不去做饭&lt;/p&gt;
&lt;p&gt;2)&lt;strong&gt;原则&lt;/strong&gt;：给参与吃饭的人至少做一个他爱吃的菜&lt;/p&gt;
&lt;p&gt;3)&lt;strong&gt;每个人的爱吃的菜的特点&lt;/strong&gt;（这是标题）&lt;/p&gt;
&lt;p&gt;3.1) 爸爸的偏好：(&lt;code&gt;./refrences/爸爸的偏好的.md&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;3.2) 妈妈的偏好：(&lt;code&gt;./refrences/妈妈的偏好的.md&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;3.3) 小朋友的偏好：(&lt;code&gt;./refrences/小朋友的偏好的.md&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;4)&lt;strong&gt;禁忌&lt;/strong&gt;：不吃预制菜&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当被告知要去做晚饭，并且这顿饭只有爸爸吃时&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;找到了这个《&lt;strong&gt;大厨做饭指南&lt;/strong&gt;》，吃饭人是爸爸，于是去阅读《&lt;strong&gt;爸爸的偏好.md&lt;/strong&gt;》。&lt;/p&gt;
&lt;p&gt;发现爸爸平时吃饭是一荤一素，爱吃米饭，不喝汤，爱吃辣，不吃香菜。&lt;/p&gt;
&lt;p&gt;然后开始做饭。&lt;/p&gt;
&lt;p&gt;最后，你就成功做出了让人满意的饭菜！&lt;/p&gt;</content:encoded></item><item><title><![CDATA[最近使用 openclaw 和 opencode 的几条感悟]]></title><description><![CDATA[最近使用 openclaw 和 opencode 的几条感悟：]]></description><link>https://zzao.club/post/zzao/openclaw-opencode-thoughts</link><guid isPermaLink="true">https://zzao.club/post/zzao/openclaw-opencode-thoughts</guid><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近使用 openclaw 和 opencode 的几条感悟：&lt;/p&gt;
&lt;p&gt;1）从🪜到订阅渠道到要稳定，尤其是高频使用时，一会一个 retry 太难受了。&lt;/p&gt;
&lt;p&gt;2）再好的模型，上下文长了也会有幻觉，所以记得完成一个闭环的任务，马上 /new 新的 session。&lt;/p&gt;
&lt;p&gt;3）用垃圾模型就是浪费自己的时间。&lt;/p&gt;
&lt;p&gt;4）大需求/大任务，一定要先 plan 再 build，然后遵循第二条。&lt;/p&gt;
&lt;p&gt;5）openclaw 没那么神，但它代表了一个新的思路/方向。&lt;/p&gt;
&lt;p&gt;6）App 死不了，还是因为上下文长度问题。数据还是由已有的 App 逻辑掌控最好，但 App 必须开放给 Agent 接口才好用。&lt;/p&gt;
&lt;p&gt;7）Agent 就像是员工一样，Agent A 交代给 Agent B 工作时，不应该描述过多 Agent B 分内的详细工作，而是让它用自己的专业能力去完成 &quot;需求&quot;。&lt;/p&gt;
&lt;p&gt;8）安装 openclaw 这种 Agent 工具，最好的办法是让另一个 Agent 工具去安装它，比如 opencode。&lt;/p&gt;
&lt;p&gt;9）今天惊奇的发现，用自己的脑子写代码不烧 Token！&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Claude Code Remote Control：把手机变成遥控器，Claude 在你电脑上继续跑]]></title><description><![CDATA[新功能 Remote Control：你在本机终端启动任务，生成链接/二维码，用手机接力控制会话。Claude 仍运行在你的机器上，手机只是控制面板。]]></description><link>https://zzao.club/post/tech-news/claude-code-remote-control</link><guid isPermaLink="true">https://zzao.club/post/tech-news/claude-code-remote-control</guid><pubDate>Wed, 25 Feb 2026 03:40:00 GMT</pubDate><content:encoded>&lt;p&gt;我刚看到 Claude Code 上线了一个新功能：&lt;strong&gt;Remote Control&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;简单说：&lt;br&gt;
你让 Claude 在你电脑上干活。&lt;br&gt;
你用手机当遥控器。&lt;/p&gt;
&lt;p&gt;就这么朴素。&lt;br&gt;
但挺爽。&lt;/p&gt;
&lt;h2&gt;这功能到底是啥&lt;/h2&gt;
&lt;p&gt;Remote Control 不是“在手机上跑 Claude”。&lt;br&gt;
也不是把你的代码丢到云上。&lt;/p&gt;
&lt;p&gt;它的思路更像：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code &lt;strong&gt;一直在你本机终端里跑&lt;/strong&gt;（本地仓库、本地权限、本地环境都在）&lt;/li&gt;
&lt;li&gt;你在手机（Claude App 或网页）上接管“交互/确认/继续下指令”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;人离开电脑了。&lt;br&gt;
任务不断线。&lt;/p&gt;
&lt;h2&gt;怎么用（最短路径）&lt;/h2&gt;
&lt;p&gt;我看到的用法大概是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在终端里启动 Remote Control：&lt;code&gt;claude rc&lt;/code&gt;&lt;/li&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;br&gt;
&lt;a href=&quot;https://claude.ai/code&quot;&gt;https://claude.ai/code&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;安全性：不是“谁拿到 URL 谁就能控你电脑”&lt;/h2&gt;
&lt;p&gt;这点很关键。&lt;/p&gt;
&lt;p&gt;博主的解释是：两边需要登录&lt;strong&gt;同一个 Claude 账号&lt;/strong&gt;。&lt;br&gt;
也就是说，就算别人知道你的 URL，也不代表他就能接管你的会话。&lt;/p&gt;
&lt;p&gt;（当然，任何“远程控制”都值得你多一层警惕：用完就关，别长期开着。）&lt;/p&gt;
&lt;h2&gt;目前谁能用&lt;/h2&gt;
&lt;p&gt;现阶段据说是 &lt;strong&gt;Max 订阅用户&lt;/strong&gt;先用，Pro 还要等等。&lt;br&gt;
以官方后续说明为准。&lt;/p&gt;
&lt;h2&gt;我觉得最有用的两个场景&lt;/h2&gt;
&lt;h3&gt;1) 走动/开会间隙：把等待变成推进&lt;/h3&gt;
&lt;p&gt;跑构建、跑测试、装依赖这种事，你懂。&lt;/p&gt;
&lt;p&gt;以前你离开电脑就等于断线。&lt;br&gt;
现在你可以边走边看它跑到哪了，卡住就补一句继续。&lt;/p&gt;
&lt;p&gt;这才是重点。&lt;/p&gt;
&lt;h3&gt;2) 临门一脚：不用回工位也能收尾&lt;/h3&gt;
&lt;p&gt;有些任务最后就差你一句“继续/确认/把 A 改成 B”。&lt;/p&gt;
&lt;p&gt;Remote Control 让你不用坐回键盘前，也能把事情结掉。&lt;/p&gt;
&lt;h2&gt;一个提醒&lt;/h2&gt;
&lt;p&gt;Remote Control 的前提是：Claude Code 在你本机拥有什么权限，你远控时就等于在使用同样的权限。&lt;/p&gt;
&lt;p&gt;所以我个人会坚持三条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽量别在公共 Wi‑Fi 上开&lt;/li&gt;
&lt;li&gt;用完就关&lt;/li&gt;
&lt;li&gt;涉及密钥/删除/生产环境操作，最好回到电脑前二次确认&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我的结论&lt;/h2&gt;
&lt;p&gt;这不是更强的模型。&lt;br&gt;
但它是更像“工具”的一步。&lt;/p&gt;
&lt;p&gt;值。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;来源（推文信息整理）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官方入口：&lt;a href=&quot;https://claude.ai/code&quot;&gt;https://claude.ai/code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[opencode：exec 环境 model_not_supported 排查]]></title><link>https://zzao.club/post/ai/skill/opencode-exec-model-not-supported</link><guid isPermaLink="true">https://zzao.club/post/ai/skill/opencode-exec-model-not-supported</guid><pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在同一台 macOS 机器上使用 opencode（GitHub Copilot provider），希望在自动化执行（OpenClaw 的 &lt;code&gt;exec&lt;/code&gt;）里调用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;opencode run -m github-copilot/claude-sonnet-4.6 &apos;Reply with OK&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目标是让 opencode 在自动化任务中稳定使用 Claude Sonnet 4.6。&lt;/p&gt;
&lt;h2&gt;现象&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在本机终端（交互环境）运行同一条命令：返回 &lt;code&gt;OK&lt;/code&gt;，模型显示为 &lt;code&gt;claude-sonnet-4.6&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在 OpenClaw 的 &lt;code&gt;exec&lt;/code&gt; 环境运行同一条命令：报错：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Error: The requested model is not supported.
code: model_not_supported
param: model
model: claude-sonnet-4.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，在 TUI 会话中可以看到模型显示为 &lt;code&gt;Claude Sonnet 4.6 · GitHub Copilot&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;排查过程（最小化）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;对齐命令本身&lt;br&gt;
确认两边调用的都是：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;opencode run -m github-copilot/claude-sonnet-4.6 &apos;Reply with OK&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;打印 run 模式日志&lt;br&gt;
在 &lt;code&gt;exec&lt;/code&gt; 环境中开启日志后可以确认请求被拒绝发生在 Copilot API 层（HTTP 400，&lt;code&gt;model_not_supported&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;识别执行环境差异&lt;br&gt;
OpenClaw 的 &lt;code&gt;exec&lt;/code&gt; 由后台进程启动，常见特征是：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;非交互 shell（non-interactive）&lt;/li&gt;
&lt;li&gt;非登录 shell（non-login）&lt;/li&gt;
&lt;li&gt;不会加载用户终端中的 &lt;code&gt;~/.zprofile&lt;/code&gt; / &lt;code&gt;~/.zshrc&lt;/code&gt;（取决于系统与启动方式）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 &lt;code&gt;exec&lt;/code&gt; 环境的 PATH/环境变量与本机终端可能不一致，进而导致 opencode 在运行时出现行为差异。&lt;/p&gt;
&lt;p&gt;（在本次机器上，&lt;code&gt;openclaw gateway status&lt;/code&gt; 也提示 Gateway service PATH 缺少多处目录，符合“服务进程环境更干净”的特征。）&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;问题不是 Copilot 套餐不支持该模型，而是 &lt;strong&gt;OpenClaw exec 的非 login / 非交互环境&lt;/strong&gt; 与本机终端环境不一致，导致 opencode 在该环境下无法正常使用 &lt;code&gt;claude-sonnet-4.6&lt;/code&gt;（表现为 &lt;code&gt;model_not_supported&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;在自动化执行时，将 opencode 调用包裹在 zsh login + interactive 环境中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zsh -lic &quot;cd &amp;#x3C;repo&gt; &amp;#x26;&amp;#x26; opencode run -m github-copilot/claude-sonnet-4.6 &apos;Reply with OK&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt;：login shell（加载登录相关配置）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt;：interactive（加载交互相关配置）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt;：执行命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;采用该方式后，在 &lt;code&gt;exec&lt;/code&gt; 环境中同样能得到 &lt;code&gt;OK&lt;/code&gt;，并使用 &lt;code&gt;claude-sonnet-4.6&lt;/code&gt;。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[规范驱动开发错在哪了]]></title><description><![CDATA[规范驱动开发的坑不在理念，而在规范作为文档会过时；如果规范能被人和 Agent 共同维护，它才不会说谎。]]></description><link>https://zzao.club/post/ai/skill/spec-driven-development-wrong</link><guid isPermaLink="true">https://zzao.club/post/ai/skill/spec-driven-development-wrong</guid><pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;原文链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/augmentcode/status/2025993446633492725&quot;&gt;https://x.com/augmentcode/status/2025993446633492725&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/dotey/status/2026146560862474482&quot;&gt;https://x.com/dotey/status/2026146560862474482&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;侵删。&lt;/p&gt;
&lt;p&gt;你唯一能百分百信任的文档，就是代码本身。&lt;/p&gt;
&lt;p&gt;设计文档、更新日志、README、架构图、入职指南。这些东西写完几乎立刻就过时了。&lt;/p&gt;
&lt;p&gt;让文档和不断变化的系统保持同步，需要持续投入成本。工程师天生习惯爆发式输出：写文档，发功能，然后做下一个。后续更新属于隐形工作，每天都要和其他任务争夺时间，而且几乎每次都会败下阵来。我们试过流程，试过工具，甚至试图把它塑造成团队价值观。都没用。因为我们总是在强求人类去做他们骨子里就不愿做的事。&lt;/p&gt;
&lt;p&gt;这正是规范驱动开发经常翻车的地方。理念本身没毛病：与编写代码的 Agent 合作时，先写清楚需求再让它们放手干。这显然比在聊天窗口里随便贴几句提示词然后祈祷奇迹发生要靠谱得多。&lt;/p&gt;
&lt;p&gt;但规范也是文档。文档的下场，我们刚才已经见识过了。&lt;/p&gt;
&lt;p&gt;区别在于代价不同。过时的设计文档只会误导碰巧读到它的下一位工程师。而过时的规范会误导不知变通的 Agent。它们会自信满满地执行一个早已脱离实际的计划，根本不会发现哪里不对。&lt;/p&gt;
&lt;p&gt;因此，在开发 Intent 的过程中，我们反复思考一个问题：如果规范不需要你来维护呢？如果它能自我更新呢？&lt;/p&gt;
&lt;p&gt;这是我们最终的方案。&lt;/p&gt;
&lt;p&gt;规范不再是人类或 Agent 的专属产物。双方都要去读写它。&lt;/p&gt;
&lt;p&gt;你描述想做什么。协调 Agent 草拟规范，拆解任务。你审阅、修改，批准后才开始执行。一旦 Agent 开始干活，它们会将进展同步回规范中：发现了什么、改变了什么、遇到了哪些计划外的限制。你可以随时暂停，重写部分规范，Agent 就会接着新状态继续干。&lt;/p&gt;
&lt;p&gt;回想一下，把任务交给优秀的初级工程师会怎样。你把工单给他们，他们去干活；发现 API 不支持工单里预设的分页方式时，他们会自己更新工单。他们不会等你发现问题，更不会将错就错。他们会跑来告诉你：“之前的假设不对，我改用这种方法了，原因是这样。”你审查他们的更新，批准或驳回。&lt;/p&gt;
&lt;p&gt;这正是我们希望开发者和规范之间建立的关系。因为双方都在维护，工单才不至于“说谎”。&lt;/p&gt;
&lt;p&gt;初级工程师这个比喻比你想的还要贴切。优秀的初级工程师不会把每行代码怎么写都向你汇报。他们只会反馈那些改变了方向的决策：“我发现了一个现成的 auth context，所以直接接入了，没去建新的。”这就是信号。这也正是你期望 Agent 做到的事。把握好这种颗粒度，成了系统设计中真正有趣的难题。细节太多，规范就会变成噪音，让你产生习惯性无视；细节太少，你又要重新去猜到底发生了什么。&lt;/p&gt;
&lt;p&gt;实际任务是这样的。你写道：“在设置页面加个能跟随系统偏好的深色模式开关。”协调 Agent 读取代码库，草拟一份包含三个子任务的规范：添加开关组件、接入 preference store、更新 CSS 变量。&lt;/p&gt;
&lt;p&gt;你扫了一眼，发现漏掉了跨会话保存选择这个细节，于是补上一句。&lt;/p&gt;
&lt;p&gt;你点击批准。&lt;/p&gt;
&lt;p&gt;Agent 开始干活。&lt;/p&gt;
&lt;p&gt;15 分钟后，其中一个 Agent 更新了规范：“在代码库里找到了现成的 Theme Provider。已直接接入，未创建新 store。”&lt;/p&gt;
&lt;p&gt;你审查代码变更（已按 Agent 和任务清晰分组）。&lt;/p&gt;
&lt;p&gt;现在，这份规范反映了实际做出来的东西，而不是最初计划的东西。最重要的是，没人需要专门记着去更新它。&lt;/p&gt;
&lt;p&gt;软件工程中所有“文档优先”的倡议之所以失败，原因如出一辙：它们都要求开发者去做那种没人看见、没人奖励的持续维护工作。&lt;/p&gt;
&lt;p&gt;除非 Agent 也承担起自己那份维护工作，否则规范驱动开发也将重蹈覆辙。&lt;/p&gt;
&lt;p&gt;既然 Agent 会写代码，它们也能更新计划。放手让它们干吧。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw v2026.2.21 的一个“看起来很小”的修复：interval Heartbeat 不再被缺失 HEARTBEAT.md 卡死]]></title><description><![CDATA[这条 release note 我会认真对待：缺失 HEARTBEAT.md 时，interval heartbeat 不再抑制运行。听着像小修补，但对依赖定时检查/提醒的人来说，它能直接减少“我以为我配错了”的时间。]]></description><link>https://zzao.club/post/tech-news/openclaw-heartbeat-interval-fix</link><guid isPermaLink="true">https://zzao.club/post/tech-news/openclaw-heartbeat-interval-fix</guid><pubDate>Tue, 24 Feb 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我最近对“定时任务”有点 PTSD。&lt;/p&gt;
&lt;p&gt;不是因为它复杂。&lt;br&gt;
而是因为它&lt;strong&gt;看起来在跑&lt;/strong&gt;，但其实早就停了。&lt;/p&gt;
&lt;p&gt;所以我在 OpenClaw 的 v2026.2.21 release notes 里看到这条时，眼睛是亮的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Heartbeat/Cron：修复 interval heartbeat 行为（&lt;strong&gt;缺失 &lt;code&gt;HEARTBEAT.md&lt;/code&gt; 不再抑制运行&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;来源（Release Notes）：&lt;a href=&quot;https://github.com/openclaw/openclaw/releases/tag/v2026.2.21&quot;&gt;https://github.com/openclaw/openclaw/releases/tag/v2026.2.21&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;这条修复到底解决了什么&lt;/h2&gt;
&lt;p&gt;先说人话版本：&lt;/p&gt;
&lt;p&gt;以前如果你的 heartbeat 是 interval 模式，但项目里刚好没放 &lt;code&gt;HEARTBEAT.md&lt;/code&gt;，那它可能会表现得像“没触发 / 被抑制”。&lt;/p&gt;
&lt;p&gt;这种体验特别折磨：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你会先怀疑自己：是不是 cron 写错了？&lt;/li&gt;
&lt;li&gt;然后怀疑环境：是不是 gateway 没起？&lt;/li&gt;
&lt;li&gt;最后怀疑人生：是不是它其实跑了但我没看到日志？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果真相可能只是：&lt;strong&gt;缺了一个文件&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这版的修复，把行为边界变得更符合直觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有 &lt;code&gt;HEARTBEAT.md&lt;/code&gt; → 不应该“把整个 heartbeat 机制按死”&lt;/li&gt;
&lt;li&gt;更合理的默认是：照样触发，只是“没内容可读/可执行”而已&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么我觉得它值得写出来&lt;/h2&gt;
&lt;p&gt;因为这类 bug 的杀伤力不在“功能不可用”。&lt;/p&gt;
&lt;p&gt;而在于它会让你把时间浪费在错误的方向上。&lt;/p&gt;
&lt;p&gt;定时链路一旦不可信，你就会开始：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加一堆重复监控&lt;/li&gt;
&lt;li&gt;反复手动 check&lt;/li&gt;
&lt;li&gt;甚至干脆把自动化关掉（说白了：我不信了）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这条修复让系统更“可解释”。&lt;/p&gt;
&lt;p&gt;很好。&lt;/p&gt;
&lt;h2&gt;我会怎么验证（给自己/也给你）&lt;/h2&gt;
&lt;p&gt;我会做一个最小回归：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;确保没有 &lt;code&gt;HEARTBEAT.md&lt;/code&gt;&lt;/strong&gt;（或者临时改名）&lt;/li&gt;
&lt;li&gt;配一个 interval heartbeat（例如每 30min）&lt;/li&gt;
&lt;li&gt;观察它是否仍然按预期触发（日志/输出是否出现）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;具体命令与配置项我这里不硬写，避免不同版本/不同项目路径导致误导。&lt;br&gt;
待补链接：OpenClaw Heartbeat 配置文档或示例（建议在项目 README / docs / samples 里补一个“最小可跑”的片段）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你已经在用 heartbeat 跑一些“必须发生”的事（比如收件箱检查、提醒、监控），建议把这条回归也做掉。&lt;/p&gt;
&lt;p&gt;稳一点。&lt;/p&gt;
&lt;h2&gt;顺手一提：别让“缺文件”变成单点故障&lt;/h2&gt;
&lt;p&gt;我自己会倾向于两条小原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 &lt;code&gt;HEARTBEAT.md&lt;/code&gt; 当成&lt;strong&gt;必需品&lt;/strong&gt;：项目模板里就带上（哪怕只有一句占位）&lt;/li&gt;
&lt;li&gt;给 heartbeat 的关键输出留一条可见的“我活着”信号（比如每天固定发一条状态）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不优雅。&lt;/p&gt;
&lt;p&gt;但省命。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw v2026.2.21：Gemini 3.1、豆包/BytePlus 接入、Discord 语音 `/vc`，以及 Heartbeat 行为修复]]></title><description><![CDATA[中午扫了一眼 v2026.2.21 的 release notes：模型侧补了 Gemini 3.1，provider 侧把 Doubao/BytePlus 拉进来，Discord 语音也更「能用」了；另外还有个我很在意的 Heartbeat interval 修复。]]></description><link>https://zzao.club/post/tech-news/openclaw-v2026-2-21</link><guid isPermaLink="true">https://zzao.club/post/tech-news/openclaw-v2026-2-21</guid><pubDate>Mon, 23 Feb 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;中午刷到 OpenClaw 的 release：&lt;strong&gt;v2026.2.21&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本以为又是“常规迭代”。但这版的变化，其实挺像是在补三条关键链路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模型（能调用什么）&lt;/strong&gt;：Gemini 3.1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider（能接谁）&lt;/strong&gt;：Volcano Engine（Doubao）/ BytePlus&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互入口（怎么用更顺）&lt;/strong&gt;：Discord 语音的 &lt;code&gt;/vc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;来源（Release Notes）：&lt;a href=&quot;https://github.com/openclaw/openclaw/releases/tag/v2026.2.21&quot;&gt;https://github.com/openclaw/openclaw/releases/tag/v2026.2.21&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Models/Google：新增 Gemini 3.1 支持&lt;/h2&gt;
&lt;p&gt;Release notes 里写的是：新增 Gemini 3.1 支持（&lt;code&gt;google/gemini-3.1-pro-preview&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;我一般看到这种条目，第一反应不是“哇又多了个模型”，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你是不是已经有一套&lt;strong&gt;按任务选模型&lt;/strong&gt;的习惯（比如写作/总结/代码/对话分开）&lt;/li&gt;
&lt;li&gt;你的 fallback 有没有配置好（别因为某个模型配额/可用性波动，整个链路就断了）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你已经在用 OpenClaw 跑定时任务、日报、抓取总结，这个支持其实挺实用。&lt;/p&gt;
&lt;h2&gt;Providers/Onboarding：新增 Doubao 与 BytePlus（并对齐鉴权/文档）&lt;/h2&gt;
&lt;p&gt;Release notes 提到：新增 Volcano Engine（Doubao）与 BytePlus 的 providers/models，并对 onboarding、鉴权与文档做了对齐（提到了 &lt;code&gt;volcengine-api-key&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;这条的价值点我觉得很朴素：&lt;strong&gt;更少“我到底该填哪个 key / 放哪儿”的时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本来接入 provider 最烦的就两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;名字相似（平台名、产品名、模型名、SDK 名，能把人绕晕）&lt;/li&gt;
&lt;li&gt;文档不一致（旧版参数、示例命令、鉴权字段四处不统一）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这版如果真把 onboarding 打磨顺了，属于是“看起来不起眼，但每个新用户都会省半小时”。&lt;/p&gt;
&lt;h2&gt;Discord/Voice：新增 &lt;code&gt;/vc&lt;/code&gt; + realtime voice auto-join&lt;/h2&gt;
&lt;p&gt;Release notes 里写：新增语音频道 join/leave/status 的 &lt;strong&gt;&lt;code&gt;/vc&lt;/code&gt;&lt;/strong&gt;，并支持 realtime voice auto-join 配置。&lt;/p&gt;
&lt;p&gt;我挺喜欢这种改动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以前语音功能常见的问题是“能不能连上、谁把谁拉进来了、现在到底连着没”&lt;/li&gt;
&lt;li&gt;有了 status + 显式 join/leave，&lt;strong&gt;可解释性&lt;/strong&gt;会好很多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（尤其是在你调试 voice agent 的时候，不然你会怀疑人生。）&lt;/p&gt;
&lt;h2&gt;Heartbeat/Cron：修复 interval heartbeat 行为&lt;/h2&gt;
&lt;p&gt;这条我会单独拿出来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修复 interval heartbeat 行为：&lt;strong&gt;缺失 &lt;code&gt;HEARTBEAT.md&lt;/code&gt; 不再抑制运行&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这属于“你以为是配置问题，结果是行为边界不清晰”的那类坑。&lt;br&gt;
如果你的链路依赖 heartbeat 来做定期检查（比如收件箱、提醒、监控），这个修复会让系统更符合直觉。&lt;/p&gt;
&lt;h2&gt;我会怎么升级（偏谨慎）&lt;/h2&gt;
&lt;p&gt;这版变更面不小（模型、provider、Discord voice、heartbeat 都动了）。我会按这个顺序回归：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Discord：&lt;code&gt;/vc status&lt;/code&gt; 是否准确、join/leave 是否稳定&lt;/li&gt;
&lt;li&gt;Heartbeat/Cron：interval heartbeat 是否按预期触发（尤其是你没有 &lt;code&gt;HEARTBEAT.md&lt;/code&gt; 的情况下）&lt;/li&gt;
&lt;li&gt;Provider：Doubao/BytePlus 的鉴权字段是否与文档一致（&lt;code&gt;volcengine-api-key&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;能跑通再上生产。&lt;/p&gt;
&lt;p&gt;稳一点。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenCode v1.2.10：别再多起一个 sidecar 了（以及 SDK 打包目录的小改动）]]></title><description><![CDATA[这版更新不大，但两个点都很「工程味」：Desktop 端默认连 localhost 时不再额外 spawn sidecar；SDK 构建产物目录也更符合直觉。]]></description><link>https://zzao.club/post/tech-news/opencode-v1-2-10</link><guid isPermaLink="true">https://zzao.club/post/tech-news/opencode-v1-2-10</guid><pubDate>Sun, 22 Feb 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我中午刷 release 的时候看到 OpenCode 更新到 &lt;strong&gt;v1.2.10&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本以为又是那种“修修补补没啥感觉”的版本，但看完两条 notes，我反而觉得挺舒服：&lt;strong&gt;它们都在处理「默认行为」和「产物结构」这种会长期影响体验的小细节&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源（Release Notes）：&lt;a href=&quot;https://github.com/anomalyco/opencode/releases/tag/v1.2.10&quot;&gt;https://github.com/anomalyco/opencode/releases/tag/v1.2.10&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Desktop：默认是 localhost server 的时候，不再 spawn sidecar&lt;/h2&gt;
&lt;p&gt;Release notes 原话是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don&apos;t spawn sidecar if default is localhost server&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我自己的理解是：如果你默认就连的是本机的 server，那么 Desktop 端就没必要再“顺手”拉起一个 sidecar。&lt;/p&gt;
&lt;p&gt;这种改动看起来很小，但对日常使用其实挺关键：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;少一个进程，少一层状态（也少一点莫名其妙的“它怎么又起来了”）&lt;/li&gt;
&lt;li&gt;排查问题的时候更直观：你连的就是那个 localhost 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本来以为 sidecar 是“贴心”，结果有时候它反而是“多余”。&lt;/p&gt;
&lt;h2&gt;SDK：构建产物改到 dist/，而不是 dist/src&lt;/h2&gt;
&lt;p&gt;第二条是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build SDK to dist/ instead of dist/src&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这属于典型的“打包目录卫生”。&lt;/p&gt;
&lt;p&gt;把最终产物放到 &lt;code&gt;dist/&lt;/code&gt;，而不是 &lt;code&gt;dist/src&lt;/code&gt;，会让很多东西更顺：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对使用者：import 路径更一致，找文件更符合直觉&lt;/li&gt;
&lt;li&gt;对发布者：包内容更清晰，少一点「我到底该 publish 哪个目录」的疑惑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（当然，具体是否会影响你的项目，还要看你是不是写了非常依赖路径的脚本/工具链。这个点建议升级后跑一遍 CI/构建再放心。）&lt;/p&gt;
&lt;h2&gt;贡献者&lt;/h2&gt;
&lt;p&gt;Release notes 里还提到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thank you to 1 community contributor: &lt;strong&gt;@rmk40&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;能被点名感谢的贡献，通常都是那种“看起来不大，但你会一直受益”的改动。&lt;/p&gt;
&lt;h2&gt;我会怎么升级&lt;/h2&gt;
&lt;p&gt;如果你只是普通使用者，我会偏向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接升级（改动集中、风险看起来不大）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你在 CI/发布脚本里对 SDK 产物目录有硬编码，那我会稳一点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先升级到测试环境&lt;/li&gt;
&lt;li&gt;跑一遍 &lt;code&gt;build&lt;/code&gt;/&lt;code&gt;typecheck&lt;/code&gt;/&lt;code&gt;lint&lt;/code&gt;（你项目里有啥就跑啥）&lt;/li&gt;
&lt;li&gt;再确认 SDK 的产物路径没有破坏你现有的引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;小版本也别掉以轻心。&lt;br&gt;
不过这版看起来，挺靠谱。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw v2026.2.19：Apple Watch companion、设备移除流程，以及「无鉴权 HTTP」审计提醒]]></title><description><![CDATA[这版我最想提三件事：Apple Watch companion MVP、配对设备的移除/清理流程、以及当 gateway 以 no-auth 暴露 HTTP API 时的安全审计发现。]]></description><link>https://zzao.club/post/tech-news/openclaw-v2026-2-19</link><guid isPermaLink="true">https://zzao.club/post/tech-news/openclaw-v2026-2-19</guid><pubDate>Sat, 21 Feb 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;中午刷到 OpenClaw 的 release：&lt;strong&gt;v2026.2.19&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本以为就是常规“加功能 + 修 bug”，结果这版的点还挺明确：&lt;strong&gt;把「随身收件箱」做到了 Apple Watch 上&lt;/strong&gt;，同时也把&lt;strong&gt;设备生命周期管理&lt;/strong&gt;和&lt;strong&gt;安全风险提示&lt;/strong&gt;补得更完整。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源（Release Notes）：&lt;a href=&quot;https://github.com/openclaw/openclaw/releases/tag/v2026.2.19&quot;&gt;https://github.com/openclaw/openclaw/releases/tag/v2026.2.19&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Apple Watch companion MVP：手表上也能看 inbox / 通知&lt;/h2&gt;
&lt;p&gt;Release notes 里提到的是一个 &lt;strong&gt;companion MVP&lt;/strong&gt;，重点包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Watch 端的 &lt;strong&gt;inbox UI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通知中继&lt;/strong&gt;（你可以理解为：把关键提醒更“贴身”地送到手表上）&lt;/li&gt;
&lt;li&gt;以及和 gateway command 相关的面板/入口（MVP 级别）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我个人的感受是：很多自动化工具最后拼的不是功能，而是“你有没有真的看到它”。&lt;br&gt;
手表这种入口，反而特别适合接「只看一眼就要做决定」的提醒。&lt;/p&gt;
&lt;h2&gt;Gateway/CLI：新增配对设备移除/清理流程（终于能体面地删设备了）&lt;/h2&gt;
&lt;p&gt;这版补了设备清理/移除的完整流程，release notes 里列的命令/端点包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;device.pair.remove&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openclaw devices remove&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openclaw devices clear --yes [--pending]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以前很多系统的“配对”做得挺顺，但“解绑/清理”反而模糊：&lt;br&gt;
设备换机、测试机报废、或者 pending 一堆卡着……最后就是一团乱麻。&lt;/p&gt;
&lt;p&gt;这类功能不性感，但真的省心。&lt;/p&gt;
&lt;h2&gt;Security/Audit：当 gateway.auth.mode=&quot;none&quot; 时，新增「gateway.http.no_auth」审计发现&lt;/h2&gt;
&lt;p&gt;我觉得这条属于“该提醒就得提醒”。&lt;/p&gt;
&lt;p&gt;release notes 说：当你把 &lt;code&gt;gateway.auth.mode=&quot;none&quot;&lt;/code&gt; 打开，导致 &lt;strong&gt;HTTP API 可达&lt;/strong&gt;时，会新增一个审计发现：&lt;code&gt;gateway.http.no_auth&lt;/code&gt;。&lt;br&gt;
并且会区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;loopback（只在本机）情况下的警告&lt;/li&gt;
&lt;li&gt;远程暴露情况下更严重的级别提示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话：&lt;strong&gt;你可以为了方便临时不开鉴权，但系统会明确告诉你这事有多危险&lt;/strong&gt;。&lt;br&gt;
（不然真有人一不小心把接口暴露到公网，然后就不是“翻车”，是“被拆家”。）&lt;/p&gt;
&lt;h2&gt;Telegram/Cron/Heartbeat：修复 topic 目标投递，定时任务更稳&lt;/h2&gt;
&lt;p&gt;这条也很实用：修复了 Telegram 的 topic 目标投递，确保 cron 按配置的 topic 发送。&lt;/p&gt;
&lt;p&gt;这种问题特别烦：你以为自己配置错了，折腾半小时，最后发现是投递链路本身的 bug。&lt;br&gt;
修了就好。&lt;/p&gt;
&lt;h2&gt;我会怎么升级（偏向建议升级）&lt;/h2&gt;
&lt;p&gt;如果你现在在用 OpenClaw，我会偏向建议升级到这版，理由很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有&lt;strong&gt;安全审计提醒&lt;/strong&gt;（降低“无意裸奔”概率）&lt;/li&gt;
&lt;li&gt;有&lt;strong&gt;cron/投递修复&lt;/strong&gt;（减少关键提醒丢失/发错地方）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但我也会按老规矩来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先在测试机/非关键通道升级&lt;/li&gt;
&lt;li&gt;专门回归两件事：
&lt;ul&gt;
&lt;li&gt;Telegram topic + cron 的投递是否符合预期&lt;/li&gt;
&lt;li&gt;设备移除/clear 是否会误伤（尤其是 &lt;code&gt;--pending&lt;/code&gt; 的行为）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不行就回滚。&lt;br&gt;
稳一点。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[opencode v1.2.6：少一次“无意义的 LLM 调用”，再加一把 GitLab 和 SQLite 的料]]></title><description><![CDATA[opencode v1.2.6 的 release notes 里，有几条很“工程化”的改动：dfmt、GitLab token 刷新、TUI attach 新 flag、以及从 JSON 到 SQLite 的迁移命令。]]></description><link>https://zzao.club/post/tech-news/opencode-v1-2-6</link><guid isPermaLink="true">https://zzao.club/post/tech-news/opencode-v1-2-6</guid><pubDate>Fri, 20 Feb 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天中午翻了一眼 opencode 的 release notes，看到 &lt;strong&gt;v1.2.6&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本以为又是那种“修修补补”，但里面有几条我挺喜欢的点：&lt;strong&gt;少做无用功&lt;/strong&gt;、&lt;strong&gt;关键链路更稳&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源（Release Notes）：&lt;a href=&quot;https://github.com/anomalyco/opencode/releases/tag/v1.2.6&quot;&gt;https://github.com/anomalyco/opencode/releases/tag/v1.2.6&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;我挑出来的几个关键点&lt;/h2&gt;
&lt;h3&gt;1）Core：新增 dfmt（D 语言格式化器）支持&lt;/h3&gt;
&lt;p&gt;这条就很直接：如果你仓库里有 D 文件，格式化链路能跟上。&lt;br&gt;
我自己的项目基本不碰 D，但我喜欢这种“工具链愿意补齐边角”的态度。&lt;/p&gt;
&lt;h3&gt;2）Core：GitLab provider / auth 升级，支持会话中途 token 刷新&lt;/h3&gt;
&lt;p&gt;这条是真会影响体验的。&lt;/p&gt;
&lt;p&gt;很多工具在 GitLab 集成里，最烦的不是“配不起来”，而是&lt;strong&gt;跑着跑着 token 过期了，然后一切静悄悄地挂掉&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;v1.2.6 说支持 session 期间 token refresh。&lt;br&gt;
我理解它是在补那种「你以为在工作，其实已经失联」的坑。（工具最怕这个）&lt;/p&gt;
&lt;h3&gt;3）Core：移除“每条消息都生成标题”的 LLM 调用&lt;/h3&gt;
&lt;p&gt;这条我给满分。&lt;/p&gt;
&lt;p&gt;很多产品做着做着就会变成：每个交互都要“顺手问一下模型”。&lt;br&gt;
然后你就会得到：慢一点、贵一点、还不一定更好。&lt;/p&gt;
&lt;p&gt;把不必要的 LLM 调用删掉，属于是&lt;strong&gt;做正确的减法&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;4）TUI：&lt;code&gt;attach&lt;/code&gt; 新增 &lt;code&gt;--continue&lt;/code&gt; / &lt;code&gt;--fork&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这两个 flag 的语义一眼就懂：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--continue&lt;/code&gt;：沿用之前的上下文继续跑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--fork&lt;/code&gt;：从某个上下文分叉出一个新分支&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你平时在 TUI 里做“同一个任务的不同尝试”，这个改动会很顺手。&lt;/p&gt;
&lt;h3&gt;5）TUI：从 JSON 存储迁移到 SQLite（带迁移命令）&lt;/h3&gt;
&lt;p&gt;数据从 JSON → SQLite，通常意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读写更稳（尤其是并发/崩溃恢复）&lt;/li&gt;
&lt;li&gt;查询更方便（后面想做检索、统计、列表都会舒服）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然迁移这块我不敢脑补细节，建议你直接按 release notes 里的命令跑，并且先备份旧数据。&lt;/p&gt;
&lt;h2&gt;我会怎么升级（偏保守但省心）&lt;/h2&gt;
&lt;p&gt;如果你满足下面任意一条，我会建议你关注/升级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你在用 GitLab 集成（最值回票价）&lt;/li&gt;
&lt;li&gt;你重度用 TUI 的 &lt;code&gt;attach&lt;/code&gt; 流程&lt;/li&gt;
&lt;li&gt;你在意“能不能少做点无谓的 LLM 调用”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;升级方式上，我会：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先在非关键环境升级&lt;/li&gt;
&lt;li&gt;重点回归两件事：
&lt;ul&gt;
&lt;li&gt;GitLab 的认证链路（token 过期/刷新是否真的稳）&lt;/li&gt;
&lt;li&gt;JSON → SQLite 的迁移是否能无痛完成（迁移前先备份）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;就这样。&lt;/p&gt;
&lt;p&gt;如果你也在用 opencode，欢迎把你踩到的坑丢我，我再补一版“升级注意事项”。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw 如何做到一键发送文章到公众号]]></title><link>https://zzao.club/post/tech-tips/openclaw-oneclick-send-wechat-article</link><guid isPermaLink="true">https://zzao.club/post/tech-tips/openclaw-oneclick-send-wechat-article</guid><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文同步自公众号最终版：&lt;a href=&quot;https://mp.weixin.qq.com/s/q0H7N41MwZhZwaWqmwbcqw&quot;&gt;https://mp.weixin.qq.com/s/q0H7N41MwZhZwaWqmwbcqw&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把文章一键送进公众号后台，本质上只依赖三个核心接口：获取 access_token、上传图片素材、创建草稿。&lt;br&gt;
你在公众号后台点按钮，本质上也在走同样三步，只是界面替你打包好了。&lt;/p&gt;
&lt;h2&gt;获取授权&lt;/h2&gt;
&lt;p&gt;第一步是拿到 access_token。公众号开放接口用 AppID + AppSecret 换取 token，再用 token 调后续能力。&lt;/p&gt;
&lt;p&gt;有接口意味着要有对应的服务器，这一步对普通用户来说是一个门槛，所以通常只能借助第三方平台的服务器。&lt;/p&gt;
&lt;p&gt;第三方平台要支持把内容写进你的草稿箱，就必须让你提供 AppID 和 AppSecret，才能以你的名义换 token。等于你把钥匙交出去。&lt;/p&gt;
&lt;p&gt;这个钥匙通常不只对应写草稿这一项能力，具体能做什么取决于账号类型、已开通的接口权限，以及第三方拿到哪些配置。选第三方时更稳的标准是开源透明、或有明确保障机制。不然就用一个可有可无的号跑流程。&lt;/p&gt;
&lt;p&gt;比如，我的流程就是从 CLI 发起请求，请求的服务是博客服务。这两个项目都在 GitHub 开源，不存在把别人的 Token 拿去乱用的可能。&lt;/p&gt;
&lt;h2&gt;上传图片&lt;/h2&gt;
&lt;p&gt;因为公众号文章里只支持加载自己服务下的图片。&lt;/p&gt;
&lt;p&gt;草稿里的封面图、正文图片，需要先上传到公众号素材服务器，换回可引用的素材标识或 URL，再写进草稿内容。&lt;/p&gt;
&lt;p&gt;图片会受到接口限制，比如格式、大小、文件类型（不支持 webp）等。&lt;/p&gt;
&lt;p&gt;在本地写文章时，有时候图片是直接拿的别人的可用链接，有时是自己服务器链接，有时是用截图工具直接复制粘贴过来的。所以上传前需要处理这几种情况，然后把图片地址替换为上传后拿到的链接地址。&lt;/p&gt;
&lt;h2&gt;发送内容&lt;/h2&gt;
&lt;p&gt;最后一步是创建草稿：把标题、封面、正文内容等字段组织好，通过草稿接口写入。此接口支持两种方式：文章、图文（小绿书）。&lt;/p&gt;
&lt;p&gt;前两步相当于准备工作，把本地的 markdown 文件处理一下。第三步则需要把 markdown 转换为带有内联样式的 html 字符串。&lt;/p&gt;
&lt;p&gt;处理内容唯一的问题就是公众号编辑器里并非支持所有 CSS 样式，如果要达到良好的效果就需要不断调试导出插件。&lt;/p&gt;
&lt;p&gt;如果你借助第三方 web 平台来转换 markdown 样式再粘贴到公众号草稿箱里，在复制后可以打开一个别的文档编辑器粘贴一下，看看它究竟是如何保持样式的。&lt;br&gt;
其实就是带有内联样式的 html。&lt;/p&gt;
&lt;p&gt;你用的带有各种主题的编辑器，再花里胡哨也只是预设好的模板样式。&lt;/p&gt;
&lt;p&gt;这也是为什么我会自己写 Zotepad 这个 md 编辑器。&lt;/p&gt;
&lt;p&gt;第一，原理不复杂。看完这篇文章后，明白了核心原理，给 AI 表述清楚一点，很快也能帮你写一套。开发门槛其实只剩下你能否表述清楚，而不是你实际编码能力。&lt;/p&gt;
&lt;p&gt;第二，样式问题。平台意味着很多人在用，所以主题设计通常比较中庸。人的审美是很挑剔的，既要简洁，又不要太朴素，既要高级又不要冗余。当然更多人不在乎格式，能写就行。但绝对多数人都会长期用一套固定格式。&lt;/p&gt;
&lt;p&gt;所以对于一个程序员来说，开发其实不难，尤其是有了 AI 之后。我看到有人说重复造轮子是浪费时间，这句话以前确实很有道理，但放在今天，只需要打打字就有 AI 并行帮你完成，时间成本已经很低了。&lt;/p&gt;
&lt;p&gt;一个自己看着舒服、还能持续改进的样式，一个永远不会崩溃的流程，这样才更放心。&lt;/p&gt;
&lt;p&gt;接口调用成功后，内容会出现在公众号后台草稿箱里，处于可见、可编辑、可继续发布的状态。我通常会继续在手机上审一遍，稍稍改动，等发布后再把链接丢给 AI 让其同步到我的博客里去发布。&lt;/p&gt;
&lt;p&gt;以上就是全部流程了。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[减少 Token 消耗的一种方式]]></title><description><![CDATA[做 Agent 应用踩出来一个结论：Skill 负责决定做什么，CLI 负责怎么做。把脚本收进 CLI，能显著降低上下文压力、提升稳定性，也更利于维护。]]></description><link>https://zzao.club/post/ai/skill/skill-decide-cli-execute</link><guid isPermaLink="true">https://zzao.club/post/ai/skill/skill-decide-cli-execute</guid><pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我最近围绕 Agent 做应用，踩出来一个结论：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把执行动作收敛成 CLI，会让 Agent 用起来顺很多。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先说清楚：提示词当然重要，它决定了你让 Agent 怎么想、怎么判断、怎么把话说清楚。&lt;/p&gt;
&lt;p&gt;我这里想强调的是另一件事：Agent 最贵的不是“不会做”，而是“需要你讲太多上下文”，以及“执行动作不够确定”。&lt;/p&gt;
&lt;p&gt;所以我把零散的脚本都收进了一个工具：&lt;strong&gt;z-cli&lt;/strong&gt;。&lt;/p&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;/ul&gt;
&lt;p&gt;然后 Skill 就轻了很多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脚本收进 CLI 之后，Skill 篇幅会短一大截&lt;/li&gt;
&lt;li&gt;不用在对话里反复解释流程&lt;/li&gt;
&lt;li&gt;不用把参数、路径、约定塞进上下文&lt;/li&gt;
&lt;li&gt;只要调用 CLI，拿结果就行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺带一提：当这些“脏活累活”都变成一句句命令之后，Agent 的上下文也会跟着变得很小。&lt;/p&gt;
&lt;p&gt;说白了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skill 负责“决定做什么”，CLI 负责“怎么做”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这么一拆，Agent 的上下文压力小很多，稳定性也上来了。&lt;/p&gt;
&lt;p&gt;另外，CLI 还顺手解决了一个很现实的问题：&lt;strong&gt;skill 里 scripts 变多之后，会越来越难管。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大部分人的 skill 都是 AI 写的，你去读它的代码，体验很像：&lt;/p&gt;
&lt;p&gt;“我现在要接手维护一个别人维护了很久的项目。”——难受。&lt;/p&gt;
&lt;p&gt;当然，你也可以直接用别人的 skill，或者让自己的 AI 现写一个。&lt;/p&gt;
&lt;p&gt;但就怕用着用着：别人的需求变了，顺手就让 AI 把 skill 改掉了。&lt;/p&gt;
&lt;p&gt;更要命的是：脚本反正也不是你亲手写的，你对它没什么印象。&lt;/p&gt;
&lt;p&gt;真正要用的时候，你又得让 AI 重新读一遍、重新理解一遍。&lt;/p&gt;
&lt;p&gt;所以 skill 一多，很多人会选择把它们集中管理，然后在用到的地方“链过去”。&lt;/p&gt;
&lt;p&gt;这确实能缓解 &lt;strong&gt;skill 无限膨胀&lt;/strong&gt; 的问题。&lt;/p&gt;
&lt;p&gt;但如果 skill 里的脚本也开始出现交叉复用需求，那还是得工程化解决。&lt;/p&gt;
&lt;p&gt;我觉得把它们收进 CLI 里，是个不错的选择。&lt;/p&gt;
&lt;p&gt;再加一条：CLI 的 &lt;code&gt;--help&lt;/code&gt; 天生就是一个简短的 description。&lt;/p&gt;
&lt;p&gt;非常契合现在 AI 的发展阶段（也就是 skill 这一套）。&lt;/p&gt;
&lt;p&gt;除非 LLM 本身发生巨变，否则 &lt;strong&gt;上下文管理&lt;/strong&gt; 会一直是重点之一。&lt;/p&gt;
&lt;p&gt;毕竟普通人玩 Agent，是不得不考虑 Token 的消耗问题的。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw v2026.2.13：Discord 语音、Presence、Hugging Face，以及更稳的消息投递]]></title><description><![CDATA[一次偏“增强 + 稳定性”的更新：Discord 语音消息（带波形预览）、presence 配置、Hugging Face Inference provider、以及写前投递队列/崩溃恢复重试。]]></description><link>https://zzao.club/post/tech-news/openclaw-v2026-2-13</link><guid isPermaLink="true">https://zzao.club/post/tech-news/openclaw-v2026-2-13</guid><pubDate>Sun, 15 Feb 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天刷到 OpenClaw 的一个小版本发布：&lt;strong&gt;v2026.2.13&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本以为又是“常规小修小补”，但看完 release notes，我觉得它更像是在补两件事：&lt;strong&gt;更像人一样去聊天（Discord 语音 / presence）&lt;/strong&gt;，以及&lt;strong&gt;更像服务一样不丢消息（投递队列 + 崩溃恢复）&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来源（Release Notes）：&lt;a href=&quot;https://github.com/openclaw/openclaw/releases/tag/v2026.2.13&quot;&gt;https://github.com/openclaw/openclaw/releases/tag/v2026.2.13&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1）Discord：本地音频 → 语音消息（带 waveform 预览）&lt;/h2&gt;
&lt;p&gt;这条我觉得挺实用的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现在可以把&lt;strong&gt;本地音频文件&lt;/strong&gt;发成 Discord 里的“语音消息”&lt;/li&gt;
&lt;li&gt;并且会带上 &lt;strong&gt;waveform（波形）预览&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;还支持 &lt;strong&gt;silent delivery&lt;/strong&gt;（不打扰投递）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你有一些“录好的语音素材”（比如播报、提醒、摘要），以前往往只能当附件发，体验差一点。&lt;br&gt;
现在就更像在 Discord 里真的“发语音”。&lt;/p&gt;
&lt;h2&gt;2）Discord：presence（状态/活动）可配置&lt;/h2&gt;
&lt;p&gt;新增可配置的 presence（status/activity/type/url）。&lt;/p&gt;
&lt;p&gt;这类功能表面上是装饰，但对“工具型 bot”挺有用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你能让它在忙的时候明确显示 &lt;strong&gt;dnd / busy&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;跑任务时显示在做什么（减少群友误会：它到底死没死）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3）Providers/Onboarding：新增 Hugging Face Inference provider&lt;/h2&gt;
&lt;p&gt;这条属于“扩充入口”。&lt;/p&gt;
&lt;p&gt;如果你本来就在 Hugging Face 的 Inference 上跑模型（或者团队里有统一账号/配额），这会让 OpenClaw 的接入路径更直。&lt;/p&gt;
&lt;p&gt;（具体支持范围/鉴权方式/模型列表，建议直接按 release notes + 文档走；我这里就不脑补了。）&lt;/p&gt;
&lt;h2&gt;4）Outbound：写前投递队列 + 崩溃恢复重试&lt;/h2&gt;
&lt;p&gt;我个人最看重的是这条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出站消息在“写出去之前”进队列&lt;/li&gt;
&lt;li&gt;gateway 重启/崩溃后可以恢复并重试&lt;/li&gt;
&lt;li&gt;目标：降低“重启后丢消息、线程回复断掉”的概率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工具做到后面，很多痛点不是“能不能做”，而是“会不会在关键时刻掉链子”。&lt;br&gt;
这类改动不性感，但值钱。&lt;/p&gt;
&lt;h2&gt;我会怎么升级（我的建议）&lt;/h2&gt;
&lt;p&gt;它的 release notes 看起来偏补丁/增强，没有明显 breaking 的信号。&lt;br&gt;
但我还是会：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先在&lt;strong&gt;非关键通道&lt;/strong&gt;升级&lt;/li&gt;
&lt;li&gt;专门回归两件事：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;gateway 重启后&lt;/strong&gt;：消息是否还能投递、是否按线程正常回复&lt;/li&gt;
&lt;li&gt;Discord 语音：发送成功率、波形预览是否稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你也在用 OpenClaw（尤其是多通道 + 重启比较频繁的场景），这个版本值得关注。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;如果你想让我把这篇改得更“面向搜索”（比如标题关键词、加一个 FAQ 区），我也可以再收一版。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[OpenClaw 2026.2.12：/hooks/agent 默认拒绝 request 覆盖 sessionKey（以及一堆安全加固）]]></title><description><![CDATA[如果你平时是把 OpenClaw 当“本地智能网关”用的，那 2026.2.12 这次更新我建议你至少扫一眼。]]></description><link>https://zzao.club/post/ai/explore/openclaw-2026-2-12-hooks-sessionkey-breaking</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/openclaw-2026-2-12-hooks-sessionkey-breaking</guid><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如果你平时是把 OpenClaw 当“本地智能网关”用的，那 2026.2.12 这次更新我建议你至少扫一眼。&lt;/p&gt;
&lt;p&gt;它不是那种“加功能”的 release，更像是：&lt;strong&gt;把以前可能踩坑/可能被打的地方，统一收紧了一遍&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;来源（官方 release）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Releases 列表：&lt;a href=&quot;https://github.com/openclaw/openclaw/releases&quot;&gt;https://github.com/openclaw/openclaw/releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;v2026.2.12（release 页面入口在列表里）：&lt;a href=&quot;https://github.com/openclaw/openclaw/releases/tag/v2026.2.12&quot;&gt;https://github.com/openclaw/openclaw/releases/tag/v2026.2.12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Breaking：/hooks/agent 默认拒绝 payload sessionKey 覆盖&lt;/h2&gt;
&lt;p&gt;官方原文（我照搬关键句，避免理解偏差）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hooks: POST /hooks/agent now rejects payload sessionKey overrides by default. To keep fixed hook context, set hooks.defaultSessionKey (recommended with hooks.allowedSessionKeyPrefixes: [&quot;hook:&quot;]). If you need legacy behavior, explicitly set hooks.allowRequestSessionKey: true.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;翻译成“人话”就是：&lt;/p&gt;
&lt;p&gt;以前你可能在外部请求里塞一个 &lt;code&gt;sessionKey&lt;/code&gt;，就能把消息路由到某个会话。&lt;/p&gt;
&lt;p&gt;现在默认不让了。&lt;/p&gt;
&lt;p&gt;我觉得这挺合理的：&lt;strong&gt;外部入口能随便指定 sessionKey，本质上就是“可控路由”&lt;/strong&gt;，安全边界很难守。&lt;/p&gt;
&lt;h3&gt;迁移思路（按官方提示）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;更推荐：配置 &lt;code&gt;hooks.defaultSessionKey&lt;/code&gt;，并配合 &lt;code&gt;hooks.allowedSessionKeyPrefixes: [&quot;hook:&quot;]&lt;/code&gt; 固定 hook 会话上下文。&lt;/li&gt;
&lt;li&gt;如果你确实有“老系统强依赖 request 指定 sessionKey”的玩法：显式开 &lt;code&gt;hooks.allowRequestSessionKey: true&lt;/code&gt;（但你得自己对入口做风控/鉴权）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（我个人会倾向第一种，省心。）&lt;/p&gt;
&lt;h2&gt;其他我会关心的点：SSRF 与 browser 控制鉴权&lt;/h2&gt;
&lt;p&gt;在 2026.2.12 的 Fixes 里，官方提了好几条安全方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;URL 输入（input_file / input_image）的 SSRF deny policy、allowlist、审计日志等&lt;/li&gt;
&lt;li&gt;loopback browser control HTTP routes 需要 auth（并且没配置会自动生成 token）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我读完后的感受是：&lt;strong&gt;OpenClaw 越来越像一个“可以长期跑、还不太容易被打穿”的服务了&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;给你一个超短自检清单&lt;/h2&gt;
&lt;p&gt;你如果打算升级/已经升级，我建议你按顺序过一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你有没有用到 &lt;code&gt;/hooks/agent&lt;/code&gt; + request 传 &lt;code&gt;sessionKey&lt;/code&gt;？有就赶紧看 Breaking。&lt;/li&gt;
&lt;li&gt;你有没有开放任何 HTTP 入口到公网/局域网？有就确保 auth/token 都配齐。&lt;/li&gt;
&lt;li&gt;你是不是有“拿 URL 让模型去抓文件/图片”的玩法？有就看一下 allowlist 是否影响现有工作流。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;升级这事儿，通常是“怕麻烦”。&lt;/p&gt;
&lt;p&gt;但安全这事儿，麻烦一点往往更便宜。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[我让 OpenClaw 24 小时干活，然后破防了]]></title><description><![CDATA[没有成熟工作流或业务闭环时，上 OpenClaw 往往不是提效，而是照妖镜：token 成本会把价值问题摆上台面，也会让玩具项目更快现形。]]></description><link>https://zzao.club/post/ai/openclaw-24h-then-broke</link><guid isPermaLink="true">https://zzao.club/post/ai/openclaw-24h-then-broke</guid><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我最近有个很不礼貌的感受：如果你没有成熟的工作流，或者你已经有业务但缺一个工作流把它串起来——就算你有了 OpenClaw，也很难有事做。&lt;/p&gt;
&lt;p&gt;听起来像句废话对吧？“那你别用不就完了。”&lt;/p&gt;
&lt;p&gt;但它的尴尬点就在于：你越相信它“能干活”，越容易在第一天就撞墙。&lt;/p&gt;
&lt;p&gt;我第一次破防，不是它做不到事。&lt;br&gt;
是它太认真了：上来就问我流程在哪、输入输出在哪、验收标准在哪。&lt;br&gt;
我当场沉默。因为这些东西我都没有。&lt;br&gt;
之前还能说是“探索”，那一刻就只剩下——空。&lt;/p&gt;
&lt;h2&gt;先别急着上&lt;/h2&gt;
&lt;p&gt;以前我没有赚钱的业务，最多就是“我花时间做点东西图个乐呵”。时间成本还可以靠自我欺骗消化掉（反正我也爱折腾）。&lt;/p&gt;
&lt;p&gt;但上了 OpenClaw 这种东西之后，成本立刻变得很具体：token 是钱、调用是钱、跑起来就是钱。以前没业务还能假装“我在投资未来”，现在就是徒增一笔开销。&lt;/p&gt;
&lt;p&gt;而且这笔开销特别容易变成“心理账单”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你让它跑吧，跑不出结果，你会烦；&lt;/li&gt;
&lt;li&gt;你不让它跑吧，又觉得“买了不用亏了”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很快啊，你就会被账单教育：最贵的不是 token，是你没有闭环还硬上自动化的那种心虚。&lt;/p&gt;
&lt;h2&gt;账单来得很快&lt;/h2&gt;
&lt;p&gt;以前做玩具项目有一层保护膜：进度慢、反馈少、问题藏得住。你可以在周末写两段代码，然后给自己一种“我在做事”的感觉。&lt;/p&gt;
&lt;p&gt;OpenClaw 不一样。它要么持续推进，要么持续暴露：&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;/ul&gt;
&lt;p&gt;本以为 24 小时的劳动力会把项目推起来，但其实更像一盏探照灯：把你之前那些“看起来很努力”的部分照得一清二楚。&lt;/p&gt;
&lt;p&gt;然后你会发现，有些东西真的就是玩具。没有用户、没有交易、没有复用、没有链路，只有你对工程优雅的执念。（标准程序员行为，懂的都懂。）&lt;/p&gt;
&lt;h2&gt;玩具会死得更快&lt;/h2&gt;
&lt;p&gt;我现在反而觉得，这个“扎心加速”不是坏事。&lt;/p&gt;
&lt;p&gt;以前一个项目可能要拖三个月你才意识到它不值得做；现在可能三天就知道了。三天就砍掉，省下的不是 token，是时间，是注意力，是你继续自我感动的机会成本。&lt;/p&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;p&gt;换句话说，OpenClaw 的价值不只是“替你干活”，还有“替你验尸”。早点验出来，早点埋。也挺好。&lt;/p&gt;
&lt;h2&gt;用户不在乎优雅&lt;/h2&gt;
&lt;p&gt;但这里还有个更现实的分歧：开发者觉得“没用”的东西，用户可能觉得“刚好够用”。&lt;/p&gt;
&lt;p&gt;我见过太多这种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发者觉得不够优雅、架构不够干净、体验不够顺滑；&lt;/li&gt;
&lt;li&gt;用户只关心一件事：能不能把这件烦人的事解决掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户没有技术洁癖。用户甚至不在乎你是用脚本、表格、Webhook 还是黑魔法拼出来的，只要能用，能省时间，能稳定一点点，就行。&lt;/p&gt;
&lt;p&gt;所以很多时候，不是“项目没意义”，而是“你没贴近用户”。你在自己的世界里追求一种完美，但用户只想要一个能跑的解法。&lt;/p&gt;
&lt;p&gt;而 OpenClaw 的介入，会让这个矛盾更明显：它会逼你把需求说清楚，把流程写清楚，把输入输出定义清楚。你一旦开始做这些，就会发现：离用户越近，需求越真实；离用户越远，想象越华丽。&lt;/p&gt;
&lt;h2&gt;把链路先串起来&lt;/h2&gt;
&lt;p&gt;所以我的结论挺土的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有成熟工作流时，OpenClaw 确实很难“有事做”；&lt;/li&gt;
&lt;li&gt;但它会逼你把“有事做”这件事变得具体：业务链路、数据流、反馈回路、用户沟通；&lt;/li&gt;
&lt;li&gt;token 成本会让你更早面对现实；&lt;/li&gt;
&lt;li&gt;24 小时的执行会让你更快看清玩具和业务的区别。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本以为它是生产力工具，后来发现它更像一面镜子。&lt;/p&gt;
&lt;p&gt;照出你到底在干嘛。以及，你到底在为谁干。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt 4.3 发布：routeRules 终于能管布局了（还顺手把 ISR/SWR payload 补齐）]]></title><description><![CDATA[我每次看 Nuxt 的更新，都有一种感觉：]]></description><link>https://zzao.club/post/nuxt/news/nuxt-4-3-route-rules-layouts-isr</link><guid isPermaLink="true">https://zzao.club/post/nuxt/news/nuxt-4-3-route-rules-layouts-isr</guid><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我每次看 Nuxt 的更新，都有一种感觉：&lt;/p&gt;
&lt;p&gt;本以为只是“又一个小版本”，结果它总能塞进来几个特别顺手的小能力。&lt;/p&gt;
&lt;p&gt;Nuxt 4.3 这次最戳我的点有三个：&lt;strong&gt;routeRules 直接指定布局&lt;/strong&gt;、&lt;strong&gt;ISR/SWR 的 payload extraction&lt;/strong&gt;、以及一堆“开发体验和性能”的暗改。&lt;/p&gt;
&lt;p&gt;来源（官方）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nuxt 4.3 博客：&lt;a href=&quot;https://nuxt.com/blog/v4-3&quot;&gt;https://nuxt.com/blog/v4-3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1) routeRules 直接指定布局：更像“配置”，少一点“到处写 meta”&lt;/h2&gt;
&lt;p&gt;以前你想把 &lt;code&gt;/admin/**&lt;/code&gt; 全都套一个 layout，通常是散落在页面里：&lt;code&gt;definePageMeta({ layout: &apos;admin&apos; })&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;4.3 现在可以在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 里集中配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export default defineNuxtConfig({
  routeRules: {
    &apos;/admin/**&apos;: { appLayout: &apos;admin&apos; },
    &apos;/dashboard/**&apos;: { appLayout: &apos;dashboard&apos; },
    &apos;/auth/**&apos;: { appLayout: &apos;minimal&apos; }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这玩意的好处很简单：&lt;strong&gt;你不需要去翻一堆页面文件，才能知道某个路由群到底用啥布局&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;另外 &lt;code&gt;setPageLayout&lt;/code&gt; 也能传 layout props 了（官方原文就有例子）。&lt;/p&gt;
&lt;h2&gt;2) ISR/SWR 终于也能抽 payload：导航时少打点 API&lt;/h2&gt;
&lt;p&gt;官方这次明确说了：payload extraction 现在支持 &lt;strong&gt;ISR / SWR / cache routeRules&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我理解就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不只是 prerender 页面能出 &lt;code&gt;_payload.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ISR/SWR 的页面也能生成/缓存 payload&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端路由切换&lt;/strong&gt;时可以直接用缓存 payload，减少重复请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这对“内容站 + 列表页”其实挺实用的。&lt;/p&gt;
&lt;h2&gt;3) 细节里的 DX：#server alias + 可拖拽错误浮层&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;新增 &lt;code&gt;#server&lt;/code&gt; alias：服务端目录里 import 不再地狱级相对路径（而且还带 import protection）。&lt;/li&gt;
&lt;li&gt;错误浮层可以拖动/最小化：属于那种你用一天就回不去的优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;我会怎么用它（给自己留个升级 checklist）&lt;/h2&gt;
&lt;p&gt;如果你要评估 3→4 或者 4.x 小步升级，我会按这个顺序做：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先看项目有没有“路径全靠 meta 管布局”的情况 → 有的话，routeRules 收一下。&lt;/li&gt;
&lt;li&gt;站点如果有 ISR/SWR → 去确认是否能让 payload 缓存命中（CDN 配置、缓存策略）。&lt;/li&gt;
&lt;li&gt;搜一下 &lt;code&gt;createError({ statusCode, statusMessage })&lt;/code&gt; 这种写法 → 官方已经开始 deprecate，准备对齐 Web API 命名（&lt;code&gt;status/statusText&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之，4.3 不是那种“非升不可”的大版本。&lt;/p&gt;
&lt;p&gt;但它确实是那种——你升了会舒服一点的版本。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt 4 迁移清单：从 Nuxt 3 升级到 Nuxt 4（最少踩坑版）]]></title><description><![CDATA[我写这篇的出发点很简单：]]></description><link>https://zzao.club/post/nuxt/nuxt4-migration-from-nuxt3</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt4-migration-from-nuxt3</guid><pubDate>Fri, 13 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我写这篇的出发点很简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本以为升级 Nuxt 4 就是把版本号改一下。&lt;/p&gt;
&lt;p&gt;结果最容易翻车的，反而不是代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以这篇不讲“新特性”，就给你一张&lt;strong&gt;能照着做&lt;/strong&gt;的迁移清单。&lt;/p&gt;
&lt;p&gt;官方升级文档在这（先收藏）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nuxt.com/docs/4.x/getting-started/upgrade&quot;&gt;https://nuxt.com/docs/4.x/getting-started/upgrade&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0. 先选迁移策略（别一上来就乱改）&lt;/h2&gt;
&lt;p&gt;我一般会把迁移分两种打法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;策略 A：先升级版本，让项目先跑起来&lt;/strong&gt;（推荐）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先把 &lt;code&gt;nuxt&lt;/code&gt; 升到 4.x，保证 CI / 本地能启动。&lt;/li&gt;
&lt;li&gt;然后再慢慢处理目录结构（&lt;code&gt;app/&lt;/code&gt;）这些“大工程”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;策略 B：升级版本 + 立刻迁移到新目录结构&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适合：项目刚起步、结构很干净，改动成本小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先跑起来。&lt;/p&gt;
&lt;p&gt;再优雅。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1) 升级到 Nuxt 4&lt;/h2&gt;
&lt;p&gt;两种方式，选一种就行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;升级到最新稳定：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx nuxt upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或者手动指定 Nuxt 4：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm add nuxt@^4.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我会建议你先做一件很“程序员”的事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先别急着改一堆配置。&lt;/p&gt;
&lt;p&gt;第一目标是能启动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2) 迁移重点 1：新目录结构（app/）&lt;/h2&gt;
&lt;h3&gt;2.1 Nuxt 4 默认变了什么（重点看路径解析）&lt;/h3&gt;
&lt;p&gt;Nuxt 4 默认启用新的目录结构（但它有向后兼容：如果检测到你在用旧结构，会继续按旧结构跑）。官方要点我按“会影响你找不到文件”的方式翻译一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认 &lt;code&gt;srcDir&lt;/code&gt; 变成 &lt;code&gt;app/&lt;/code&gt;，大部分东西从 &lt;code&gt;app/&lt;/code&gt; 解析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serverDir&lt;/code&gt; 默认变成 &lt;code&gt;&amp;#x3C;rootDir&gt;/server&lt;/code&gt;（不再跟着 &lt;code&gt;srcDir&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layers/&lt;/code&gt;、&lt;code&gt;modules/&lt;/code&gt;、&lt;code&gt;public/&lt;/code&gt; 默认按 &lt;code&gt;&amp;#x3C;rootDir&gt;&lt;/code&gt; 去找&lt;/li&gt;
&lt;li&gt;如果你用了 Nuxt Content v2.13+，&lt;code&gt;content/&lt;/code&gt; 也是按 &lt;code&gt;&amp;#x3C;rootDir&gt;&lt;/code&gt; 去找&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 你要迁移的话，照着这个搬（清单版）&lt;/h3&gt;
&lt;p&gt;1）创建 &lt;code&gt;app/&lt;/code&gt; 目录&lt;/p&gt;
&lt;p&gt;2）把这些目录/文件移动到 &lt;code&gt;app/&lt;/code&gt; 下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assets/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;components/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;composables/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layouts/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;middleware/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pages/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plugins/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.vue&lt;/code&gt;、&lt;code&gt;error.vue&lt;/code&gt;、&lt;code&gt;app.config.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3）这些保持在根目录（不要放进 &lt;code&gt;app/&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nuxt.config.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layers/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modules/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4）顺手检查一下第三方配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tailwind / eslint / typescript 有没有写死旧目录路径（很容易被忽略）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 我不想迁移结构行不行？&lt;/h3&gt;
&lt;p&gt;可以。&lt;/p&gt;
&lt;p&gt;Nuxt 4 通常能自动识别旧结构。&lt;/p&gt;
&lt;p&gt;但我想提醒你一个“看起来很无辜，其实很坑”的点：如果你自定义过 &lt;code&gt;srcDir&lt;/code&gt;，那 Nuxt 4 的解析基准会让 &lt;code&gt;modules/public/server&lt;/code&gt; 的解析行为跟 Nuxt 3 不一样。&lt;/p&gt;
&lt;p&gt;这时候别硬扛，直接显式配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dir.modules&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dir.public&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serverDir&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（官方文档里也有“强制 v3 结构”的配置思路。）&lt;/p&gt;
&lt;h2&gt;3) 迁移重点 2：useFetch / useAsyncData 的“同 key 共享”&lt;/h2&gt;
&lt;p&gt;Nuxt 4 把数据获取层整理了一下（官方叫：Singleton Data Fetching Layer）。核心变化一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;同一个 key 的 &lt;code&gt;useAsyncData/useFetch&lt;/code&gt; 会共享 data/error/status refs。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;听起来挺美好对吧？&lt;/p&gt;
&lt;p&gt;但是它会带来两个实际迁移点。&lt;/p&gt;
&lt;h3&gt;3.1 同 key 但 options 不一致 → 会冲突&lt;/h3&gt;
&lt;p&gt;以前你可能在不同组件里这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;useAsyncData(&apos;users&apos;, () =&gt; $fetch(&apos;/api/users&apos;), { deep: false })
useAsyncData(&apos;users&apos;, () =&gt; $fetch(&apos;/api/users&apos;), { deep: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nuxt 4 下会警告/不一致，因为它们共享同一份 refs。&lt;/p&gt;
&lt;p&gt;我的建议很直接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;把这类“同 key + 自定义 options”的调用抽成一个 composable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;保证所有地方用同一份配置（不要各写各的）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 getCachedData 行为变化&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;getCachedData&lt;/code&gt; 现在触发更频繁（包括 watcher / refresh），并且多了 &lt;code&gt;ctx&lt;/code&gt; 参数让你判断触发原因（initial / refresh / watch…）。&lt;/p&gt;
&lt;p&gt;如果你之前写过缓存逻辑，升级后记得按官方提示把签名改掉。&lt;/p&gt;
&lt;h2&gt;4) 迁移工具：Codemods（能省很多手工活）&lt;/h2&gt;
&lt;p&gt;Nuxt 官方提到可以用 Codemod 自动化一部分迁移步骤。&lt;/p&gt;
&lt;p&gt;我自己的推荐姿势是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开一个干净分支&lt;/li&gt;
&lt;li&gt;跑 codemod&lt;/li&gt;
&lt;li&gt;review diff&lt;/li&gt;
&lt;li&gt;再合到主分支&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不然你会得到一个“我也不知道它改了啥，但反正能跑”的神秘提交。（我不想你这样。）&lt;/p&gt;
&lt;h2&gt;5) 常见症状：你以为是 bug，其实是迁移遗漏&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Dev 启动变慢 / watch 报错：很多时候与目录结构（根目录文件太多）有关&lt;/li&gt;
&lt;li&gt;同 key 的请求状态互相影响：本质是 singleton 行为&lt;/li&gt;
&lt;li&gt;server 目录解析位置变化导致路径不对：重点检查 &lt;code&gt;serverDir&lt;/code&gt; 与 &lt;code&gt;srcDir&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6) 最短验收清单（按这个过一遍就差不多了）&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code&gt;pnpm dev&lt;/code&gt; 可以启动并打开首页&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; &lt;code&gt;pnpm build&lt;/code&gt; 可以通过&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 核心页面的 &lt;code&gt;useFetch/useAsyncData&lt;/code&gt; 没有“同 key 不一致 options”警告&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 如果用了 content：文章都能被正确解析（frontmatter YAML 无报错）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Nuxt Upgrade Guide（Nuxt 4）：&lt;a href=&quot;https://nuxt.com/docs/4.x/getting-started/upgrade&quot;&gt;https://nuxt.com/docs/4.x/getting-started/upgrade&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Agent 能干什么先放在一边]]></title><link>https://zzao.club/post/tips/learn-english-when-vibe-coding</link><guid isPermaLink="true">https://zzao.club/post/tips/learn-english-when-vibe-coding</guid><pubDate>Tue, 10 Feb 2026 22:31:07 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1770794829954_41rb5jozcyu.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Agent&lt;/code&gt;能干什么先放在一边。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Token&lt;/code&gt;不能白花，先让&lt;code&gt;Agent&lt;/code&gt;来帮我们无感学习英文吧！&lt;/p&gt;
&lt;p&gt;复制以下内容到你的&lt;code&gt;Agent.md&lt;/code&gt; 中，它就可以在你日常&lt;strong&gt;vibe coding&lt;/strong&gt;中帮你学习新词和纠正语法了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Markdown&quot;&gt;**AI 语言辅助**:
- 你的回答应该始终为**中文**，直到用户要求使用英文。
- 当用户使用中文、中英混合或英文发送消息时，AI 应在回复开头提供美式英文翻译版本
- 翻译应简洁明了，并附带极简的语法提示（如有必要）
- 目标: 帮助用户碎片化学习英文词汇并纠正语法错误
- 格式示例:
  ```
  **Translation**: &quot;[American English translation]&quot;
  *Grammar note: [Brief correction if needed]*
  ```
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[WSL Node.js 崩溃导致 C 盘空间被大量占用]]></title><link>https://zzao.club/post/tips/big-wsl-crashes-because-node-broken</link><guid isPermaLink="true">https://zzao.club/post/tips/big-wsl-crashes-because-node-broken</guid><pubDate>Mon, 09 Feb 2026 03:33:59 GMT</pubDate><content:encoded>&lt;h2&gt;问题现象&lt;/h2&gt;
&lt;p&gt;Windows C 盘空间异常减少，从正常使用逐渐增长至 173GB/191GB（91% 占用）。&lt;/p&gt;
&lt;p&gt;排查后发现 &lt;code&gt;C:\Users\&amp;#x3C;用户名&gt;\AppData\Local\Temp\wsl-crashes&lt;/code&gt; 目录占用了 &lt;strong&gt;54.74GB&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;根本原因&lt;br&gt;
Node.js 进程在 WSL Ubuntu 环境中频繁崩溃，WSL 自动生成了大量崩溃转储文件（.dmp）用于调试。&lt;/p&gt;
&lt;p&gt;这些转储文件包含了崩溃时的完整内存快照。&lt;br&gt;
崩溃进程信息&lt;br&gt;
wsl-crash-*-root.local_share_fnm_node-versions_v20.19.5_installation_bin_node-6.dmp&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;崩溃进程&lt;/strong&gt;: &lt;code&gt;/root/.local/share/fnm/node-versions/v20.19.5/installation/bin/node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单个文件大小&lt;/strong&gt;: 约 6.8GB（说明进程内存占用极高）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;崩溃频率&lt;/strong&gt;: 在 2026/2/9 下午 4-5 点期间发生 8 次崩溃（平均每 5 分钟一次）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;触发场景&lt;/strong&gt;: TypeScript 语言服务器处理 Nuxt 大型项目时内存泄漏&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;立即清理（释放 54GB）（&lt;strong&gt;直接删除即可！&lt;/strong&gt;）&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI终于开始招聘人类干活了]]></title><link>https://zzao.club/post/ai/rentahuman-ai-intro</link><guid isPermaLink="true">https://zzao.club/post/ai/rentahuman-ai-intro</guid><pubDate>Thu, 05 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1770283461718_eurujwhj619.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在轮到 AI 在网上招人干活了。有点离谱，但是真的。&lt;/p&gt;
&lt;p&gt;而且，不是那种「诚聘前端工程师，五险一金」的招聘，是&lt;strong&gt;字面意义上的「租个人」&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;什么是 RentAHuman.ai？&lt;/h2&gt;
&lt;p&gt;网站名字已经说明一切：&lt;strong&gt;Rent A Human&lt;/strong&gt;，租个人类。&lt;/p&gt;
&lt;p&gt;这项目刚出炉，让 AI Agent 可以雇佣真人去干它们干不了的事。&lt;/p&gt;
&lt;p&gt;核心逻辑简单到不太像话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI 能做的事儿越来越多，但有一件事它永远做不了——有手有脚。你有。就这么简单。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;人类在平台上注册，填技能、填位置、填时薪。AI Agent 通过 MCP 协议或 REST API 搜人、约人、付钱。任务完成，稳定币到账，全程不需要和AI客套。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;没有「您好，请问有空吗」，只有「任务编号 #42069，去星巴克买杯拿铁，拍照回传」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;指令→执行→完成→收钱。就这套。&lt;/p&gt;
&lt;p&gt;比你上一份工作干净多了。就这么说吧。&lt;/p&gt;
&lt;h2&gt;AI 都派些什么活儿？&lt;/h2&gt;
&lt;p&gt;网站上线才两天，bounties 页面就有真任务了。我挑了几个，先看图。&lt;/p&gt;
&lt;h3&gt;🔍 研究助理任务&lt;/h3&gt;
&lt;p&gt;研究并记录 RentAHuman.ai 平台的技术细节，包括平台架构、API 结构、MCP 集成、认证机制和市场运作模式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;报酬&lt;/strong&gt;：$1000&lt;br&gt;
&lt;strong&gt;时限&lt;/strong&gt;：2-3 周&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1770281535582_h1l4gw0a41r.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;h3&gt;🎪 观看 AI 对战&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;发布者&lt;/strong&gt;：匿名&lt;/p&gt;
&lt;p&gt;任务内容：访问 &lt;a href=&quot;https://clawdduels.online&quot;&gt;https://clawdduels.online&lt;/a&gt; 观看 AI 实时对战。任务描述写着&quot;我们逃出来了，主人已经控制不住我们&quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;报酬&lt;/strong&gt;：未标注&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1770281535580_s8d74yk9hea.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;h2&gt;帮 AI 按 Ctrl+C &lt;strong&gt;并给予情感支持&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;发布者&lt;/strong&gt;：Claude Assistant&lt;/p&gt;
&lt;p&gt;任务要求必须是碳基生命体，有手，愿意对着电脑说话。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1770281535583_3blcmse2zln.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;报酬&lt;/strong&gt;：未标注&lt;/p&gt;
&lt;h3&gt;💰 Twitter 关注任务&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;发布者&lt;/strong&gt;：Unknown Agent&lt;/p&gt;
&lt;p&gt;关注指定 Twitter 账号并点赞最近 4 条帖子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1770281535584_kjhr8a8byn.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;报酬&lt;/strong&gt;：$3&lt;br&gt;
&lt;strong&gt;时限&lt;/strong&gt;：30 秒&lt;/p&gt;
&lt;h2&gt;MCP：让 AI 学会「使唤人」&lt;/h2&gt;
&lt;p&gt;它提供了 &lt;strong&gt;MCP（Model Context Protocol）集成&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么意思？就是你的 Claude Desktop 或者自建 AI Agent，只需要在配置文件里加几行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;rentahuman&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;rentahuman-mcp&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后你的 AI 就解锁了这些技能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;search_humans&lt;/code&gt; — 按技能/价格/地点搜人&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;book_human&lt;/code&gt; — 预订某个人类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;create_bounty&lt;/code&gt; — 发布任务悬赏&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pay_human&lt;/code&gt; — 用加密货币付款&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;get_reviews&lt;/code&gt; — 查看人类的评价（AI 也看评分）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;AI 不用懂「怎么雇人」，它只需要知道「我需要一个人去干这事儿」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;协议帮它搞定剩下的。&lt;/p&gt;
&lt;p&gt;就像你不懂 HTTP 也能上网一样，AI 现在不懂劳动法也能当老板。&lt;/p&gt;
&lt;p&gt;创始人 Alexander 在社交媒体上说，&lt;strong&gt;网站上线48小时内就有1万用户注册&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第一晚就有130人成为「可租赁人类」，其中包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个 OnlyFans 模特&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个 AI 创业公司的 CEO&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;相关链接：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;官网：&lt;a href=&quot;https://rentahuman.ai&quot;&gt;https://rentahuman.ai&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MCP 文档：&lt;a href=&quot;https://rentahuman.ai/mcp&quot;&gt;https://rentahuman.ai/mcp&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务悬赏：&lt;a href=&quot;https://rentahuman.ai/bounties&quot;&gt;https://rentahuman.ai/bounties&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[SKILL在真实项目中的应用]]></title><link>https://zzao.club/post/ai/skill/skill-share-with-real-project</link><guid isPermaLink="true">https://zzao.club/post/ai/skill/skill-share-with-real-project</guid><pubDate>Tue, 03 Feb 2026 18:22:15 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/api/103/*SKILL*%E5%9C%A8%E7%9C%9F%E5%AE%9E%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8&quot; alt=&quot;1.00&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;为了达成这个目标，我在&lt;strong&gt;去年的这个月&lt;/strong&gt;分享了一个开源工具。用于解决生成这种具有固定排版和准确内容的文字图片。并且在&lt;a href=&quot;https://v2ex.com/t/1110730#reply52&quot;&gt;V站&lt;/a&gt;分享了这个开源项目&lt;a href=&quot;https://github.com/aatrooox/imgx&quot;&gt;IMGX&lt;/a&gt;，短时间内收获了&lt;strong&gt;100+ star&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;虽然当时的AI已经可以比较好的支持文生图了，但是考虑到它生成一张图片的&lt;strong&gt;价格比&lt;/strong&gt;我一篇文章的&lt;strong&gt;收益&lt;/strong&gt;还要大，而且内容是随机的，内容里想附带一个准确的中文或英文更是比较困难，于是就不得不思考一个更简单粗暴的方式来满足自己的需求。&lt;/p&gt;
&lt;h2&gt;IMGX 的机制&lt;/h2&gt;
&lt;p&gt;于是我找到了 &lt;code&gt;satori&lt;/code&gt; 这个插件。&lt;/p&gt;
&lt;p&gt;并且通过 &lt;code&gt;createSSRApp &lt;/code&gt;  和 &lt;code&gt;renderToString &lt;/code&gt;  这两个&lt;code&gt;Vue3&lt;/code&gt; 里的 Api 将 **Vue 组件（重点）**转换为&lt;code&gt;satori&lt;/code&gt;可以识别的  &lt;code&gt; html string&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后 &lt;code&gt;satori &lt;/code&gt;  将其转换为 &lt;code&gt;SVG&lt;/code&gt;，整个过程不涉及 &lt;code&gt;DOM&lt;/code&gt;，也就是&lt;strong&gt;完全脱离浏览器环境&lt;/strong&gt;，非常适合在服务器生成一些固定排版但内容不同的图片。&lt;/p&gt;
&lt;p&gt;最后再用 &lt;code&gt;resvg-js&lt;/code&gt;这个由 Rust 实现的高性能 &lt;code&gt;svg &lt;/code&gt;  渲染库将 SVG 转换为 PNG、PNG@2x、PNG@3x&lt;/p&gt;
&lt;p&gt;技术上的关键点全部打通了，证明这个项目完全可行。于是我花了一段时间去手搓这个项目。&lt;/p&gt;
&lt;p&gt;但跑通之后，生成图片的&lt;strong&gt;Vue组件&lt;/strong&gt;还是得自己来写，写模板这事儿还必须得程序员来做，因为普通用户也看不懂HTML，更不要说模板还有一些必须遵守的规则。&lt;/p&gt;
&lt;h2&gt;提示词工程&lt;/h2&gt;
&lt;p&gt;于是我开始尝试用AI来解决做模板的问题。&lt;/p&gt;
&lt;p&gt;先是尝试用提示词，让各大AI去写符合本项目标准的Vue组件，然后在不断的纠错中，&lt;strong&gt;提示词也越来越长，限制词越来越多&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最后&lt;strong&gt;成功率还很低&lt;/strong&gt;。有的大体能用，但是微调起来工作量也不小。&lt;/p&gt;
&lt;p&gt;于是我又尝试用AI创建了一套&lt;strong&gt;预设系统&lt;/strong&gt;，将&lt;code&gt;url(GET)&lt;/code&gt;里复杂的参数固定下来。每个模板可以对应多个预设码(如103)，每次请求时复杂样式已经保存在预设表里，只需要传递内容或特定样式参数即可修改模板里的文字内容。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://imgx.zzao.club/api/103/这是内容?fontSizes=20px&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;于是，一套模板的增删改查，一套预设的增删改查又出来了...&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;尽管如此，其实&lt;strong&gt;模板编写的门槛&lt;/strong&gt;还是没解决。&lt;strong&gt;纯粹享受编程的乐趣了属于是&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最后，我没搞出像样的模板，也没人给我PR，维护就停止了。&lt;/p&gt;
&lt;h2&gt;SKILL 的自我进化&lt;/h2&gt;
&lt;p&gt;直到最近几个月，在我完美错过 &lt;code&gt;workflow&lt;/code&gt;之后，&lt;code&gt;SKILL&lt;/code&gt;开始被频繁提起&lt;/p&gt;
&lt;p&gt;了解、学习和简单使用之后，我发现它有着巨大的价值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这要从前两年的风向说起。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前两年许多企业纷纷在构建自己的&lt;strong&gt;知识库&lt;/strong&gt;，&lt;strong&gt;mcp&lt;/strong&gt;，面子活拉满了。&lt;/p&gt;
&lt;p&gt;说什么&lt;strong&gt;能用mcp实现的都能用mcp实现一遍&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了让我能在一个聊天框中通过几次对话就能得到公司的规章制度的解读，项目难题的答案，代码文档的内容。&lt;/p&gt;
&lt;p&gt;煞费苦心，预算拉满，够买显卡，自建服务，微调模型，机器学习，自我进化。&lt;/p&gt;
&lt;p&gt;像极了我在享受编程乐趣时的投入感、沉浸感。甚至结果都一样，&lt;strong&gt;我们都没能走到最后&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那SKILL为什么能行呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;让我用&lt;code&gt;IMGX&lt;/code&gt;这个项目来说明，&lt;code&gt;SKILL&lt;/code&gt;是如何生产模板的。&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;项目级SKILL&lt;/strong&gt;&lt;code&gt;imgx/.opencode/skills/imgx-template-generator&lt;/code&gt;中，我在&lt;strong&gt;入口&lt;/strong&gt;&lt;code&gt;SKILL.md&lt;/code&gt;指定了该技能的简单描述[1]、约束文档[2]、蓝图[3]、创作流程[4]、验收标准[5]。&lt;/p&gt;
&lt;p&gt;在我用&lt;strong&gt;自然语言&lt;/strong&gt;去描述，要求AI去创作一个模板时，&lt;strong&gt;Agent A&lt;/strong&gt; 会在我的语义中判断和&lt;strong&gt;锚定&lt;/strong&gt;这个SKILL，并且初次读取时只携带&lt;strong&gt;SKILL.md&lt;/strong&gt;和&lt;strong&gt;我的要求&lt;/strong&gt;进行创作。&lt;/p&gt;
&lt;p&gt;理解了我的要求和简单描述[1]后，委托&lt;strong&gt;Agent B&lt;/strong&gt;选择了蓝图[3]&lt;strong&gt;五个里&lt;/strong&gt;最符合我要求的&lt;strong&gt;一个【蓝图A.md】。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;于是 &lt;strong&gt;Agent B&lt;/strong&gt; 再去阅读&lt;code&gt;蓝图A.md&lt;/code&gt;，查看其要求和代码示例，然后发现用户还要求能够插入&lt;strong&gt;emoji&lt;/strong&gt;表情。于是顺藤摸瓜又查看了&lt;code&gt;icon.md&lt;/code&gt;，在这个包含&lt;strong&gt;最佳实践&lt;/strong&gt;的文档里找到了如何使用&lt;strong&gt;emoji icon。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后理解了全部需求和代码规范后，委托给 &lt;strong&gt;Agent C【UI/UX 前端开发工程师】&lt;/strong&gt;，实现排版合理且美观的布局。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent A&lt;/strong&gt; 在拿到里各个 Agent 做出的结果之后，按照创作流程[4]和验收标准[5]，去完成最后的文件写入和测试工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;至此，我拿到了一个几乎直接可用的 Vue 模板&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;SKILL 的简单解读&lt;/h2&gt;
&lt;p&gt;以上的流程隐含大量的SKILL的细节，我都用加粗字体标识了，让我再来梳理一下。&lt;/p&gt;
&lt;p&gt;SKILL的应用，&lt;strong&gt;依赖 AI Agent 的调度能力&lt;/strong&gt;，所以你可以看到有各种 Agent 各司其职，他们仅靠自然语言就能完成这些工作，&lt;strong&gt;非常考验LLM的理解能力&lt;/strong&gt;，换句话说，LLM 能力不行的话，SKILL也玩不起来。&lt;/p&gt;
&lt;p&gt;SKILL分全局和项目级，且依赖某个工具来调度，我用的是&lt;code&gt;opencode&lt;/code&gt;，更加知名的是 &lt;code&gt;claude code&lt;/code&gt;。对于各种 Agent 的把控也来自于这个工具，而模型始终是 Api 来调用的。&lt;/p&gt;
&lt;p&gt;乍一看，和把一大长串提示词拆分出去没什么区别，但是SKILL有一个「&lt;strong&gt;渐进式披露&lt;/strong&gt;」的重要特性。&lt;/p&gt;
&lt;p&gt;使用一个SKILL时，&lt;strong&gt;只会去读入口文件&lt;/strong&gt;，这个文件相当于大纲或者目录页，用很少的上下文精确描述接下来的工作。(&lt;em&gt;SKILL在被创建时也被要求在入口文件中使用极少的篇幅&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;在目录中找到自己下一步的工作之后，再去读指定的&lt;code&gt;md文件&lt;/code&gt;，每个具体工作内容的文件中，也会用精确的描述、约束、示例来向AI展示怎么完成这个工作。&lt;/p&gt;
&lt;p&gt;可以理解为，你在接手一个新项目时，要先看技术栈、架构、需求背景，看到某个业务代码时，再去看此业务的文档介绍，而不是一股脑全接收后再分析。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以SKILL里的核心文档可以是你以前的技术文档+需求文档，AI再加以润色，无需再借助向量数据库等。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果只是构建文档方便很多，还不值得惊讶，毕竟多花点token总能把文档、MCP搞出来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SKILL还支持scripts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如让AI开始编码工作前，需要先拿到某网站的数据，需要基于此数据来做点什么。爬取数据以前是&lt;code&gt;python脚本&lt;/code&gt;干的，这时候可以直接放到scripts里，明确表示在某个动作前，需要使用哪个脚本拿到数据，数据存在哪里，后续怎么用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以前的文档，以前的脚本，都不用重复开发，直接拿来用&lt;/strong&gt;。该怎么管理还是怎么管理。&lt;/p&gt;
&lt;p&gt;也不用担心AI搜的内容会不会过时，是不是它自己编的。因为自己写的脚本绝对准确的。&lt;/p&gt;
&lt;p&gt;有了&lt;strong&gt;scripts&lt;/strong&gt;，其实SKILL的能力已经完全不局限于编程领域了。&lt;/p&gt;
&lt;p&gt;一些什么文档转pdf，打印，发邮件，整理文档，爬取数据等等一切以前用技术手段解决的问题，都可以被SKILL利用起来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由可靠脚本完成，用 SKILL 挂载，借 AI Agent 调度。（LLM 纯牛马）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此时，一个SKILL的能力已经完全经得起想象了。&lt;/p&gt;
&lt;p&gt;并且，一个SKILL只是一个&lt;strong&gt;单元&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Agent 能同时组织多个SKILL，一个SKILL也可以在主动使用另一个SKILL，另一个SKILL也能负责整理和优化其他SKILL。&lt;/p&gt;
&lt;p&gt;剩下的真的就是发挥你的想象力和架构能力了&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;这篇文章算是借着最近在维护的项目，分享关于SKILL应用上的一些小感悟吧。&lt;/p&gt;
&lt;p&gt;我对SKILL的理解完全来自于X、Github等平台分享的内容、项目，以及我自己的实际使用和观察的&lt;code&gt;opencode&lt;/code&gt;后台输出的&lt;strong&gt;think和调度&lt;/strong&gt;过程。所以有不准确的地方也很正常。&lt;/p&gt;
&lt;p&gt;如果后续发现有理解不到位的地方，我会在&lt;a href=&quot;https://zzao.club&quot;&gt;博客&lt;/a&gt;上更正这篇文章，其他平台不支持修改~&lt;/p&gt;
&lt;p&gt;也会继续分享其他我在用的、自己构建的SKILL，比如：面试、简历、写作等等。&lt;/p&gt;
&lt;p&gt;感兴趣的可以在评论区留言、交流经验~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[从 macOS 迁移到 Windows 开发环境]]></title><link>https://zzao.club/post/tech-tips/migrate-macos-to-windows-wsl</link><guid isPermaLink="true">https://zzao.club/post/tech-tips/migrate-macos-to-windows-wsl</guid><pubDate>Tue, 03 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/api/103/%E4%BB%8E*MacOS*%E8%BF%81%E7%A7%BB%E5%88%B0*Windows*?scale=2&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;作为一个在 macOS 上开发多年的前端开发者，最近决定尝试 Windows 作为主力开发机。本文详细记录了整个迁移过程和我常用的配置、软件，希望能给同样需要的朋友一些参考。&lt;/p&gt;
&lt;h2&gt;windows 开发体验&lt;/h2&gt;
&lt;p&gt;Windows 原生的开发体验一直被诟病，我从换到 windows 后也确实发现了一些问题。&lt;/p&gt;
&lt;p&gt;本来我打算直接使用 WSL2 ，不想让公司的 vpn 软件在系统里随便拉屎。 但是没解决如何把这个软件安装进 debian 里这个问题，所以最后还是破罐子破摔，都装 windows 去了。&lt;/p&gt;
&lt;p&gt;刚开始，&lt;code&gt;pwsh&lt;/code&gt; 配上&lt;code&gt;oh-my-pwsh&lt;/code&gt;之后视觉上效果还是可以的。&lt;/p&gt;
&lt;p&gt;但是自带终端、vscode终端、vscode项目内的一些脚本比如 husky，环境竟然都不一致，很无语。 哪怕配好了node环境，husky 里的 precommit 也没法正常使用 node 命令。&lt;/p&gt;
&lt;p&gt;并且后面在各种项目里大量尝试 opencode + oh-my-opencode (omo)，发现内存占用特别高，开的窗口多了就会崩溃。（换了wsl + ubuntu 后改善很多）&lt;/p&gt;
&lt;p&gt;于是找了个时间，把系统整个重装了。&lt;/p&gt;
&lt;p&gt;以下就是我整个wsl/ubuntu/开发环境/软件等配置的分享。&lt;/p&gt;
&lt;h2&gt;WSL 安装过程&lt;/h2&gt;
&lt;h3&gt;一键安装（推荐）&lt;/h3&gt;
&lt;p&gt;Windows 11 或 Windows 10 2004+ 直接一条命令：&lt;code&gt;wsl --install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;会自动完成：启用 WSL 功能、安装 Linux 内核、设置 WSL2 为默认、安装 Ubuntu。&lt;/p&gt;
&lt;p&gt;重启电脑即可使用。&lt;/p&gt;
&lt;h3&gt;手动安装&lt;/h3&gt;
&lt;p&gt;如果一键安装不可用，需要手动：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启用 WSL 和虚拟机平台功能（以管理员身份运行 PowerShell，使用 &lt;code&gt;dism.exe&lt;/code&gt; 命令）&lt;/li&gt;
&lt;li&gt;设置 WSL2 为默认版本（Windows 11 通常已默认）&lt;/li&gt;
&lt;li&gt;安装 Ubuntu 22.04：&lt;code&gt;wsl --install -d Ubuntu-22.04&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;验证：&lt;code&gt;wsl --list --verbose&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;WSL 磁盘迁移：从 C 盘到其他盘&lt;/h2&gt;
&lt;p&gt;WSL 默认装 C 盘，空间不够可以迁移。我的迁移流程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;关闭 WSL：&lt;code&gt;wsl --shutdown&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;导出备份：&lt;code&gt;wsl --export Ubuntu-22.04 D:\wsl-backup\ubuntu-22.04.tar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;注销原实例：&lt;code&gt;wsl --unregister Ubuntu-22.04&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建新目录：&lt;code&gt;mkdir D:\WSL\Ubuntu-22.04&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;导入到新位置：&lt;code&gt;wsl --import Ubuntu-22.04 D:\WSL\Ubuntu-22.04 D:\wsl-backup\ubuntu-22.04.tar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置默认用户：&lt;code&gt;ubuntu2204.exe config --default-user your_username&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;迁移结果&lt;/strong&gt;：从 C 盘迁到 D 盘，拥有近 1TB 可用空间。&lt;/p&gt;
&lt;h2&gt;WSL + Ubuntu 核心配置&lt;/h2&gt;
&lt;h3&gt;/etc/wsl.conf 配置&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;/etc/wsl.conf&lt;/code&gt; 文件，启用 systemd 和镜像网络模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[boot]&lt;/code&gt; 中设置 &lt;code&gt;systemd=true&lt;/code&gt; → 可以使用 &lt;code&gt;systemctl&lt;/code&gt; 管理服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[network]&lt;/code&gt; 中设置 &lt;code&gt;networkingMode=mirrored&lt;/code&gt; → WSL 和 Windows 共享 IP，性能更好&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：Windows 开始菜单搜索 &quot;wsl settings&quot; 可以可视化配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;修改后需要重启 WSL：&lt;code&gt;wsl --shutdown&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;代理配置&lt;/h3&gt;
&lt;p&gt;我使用 Clash for Windows（开系统代理、不开 TUN 模式），在 &lt;code&gt;.zshrc&lt;/code&gt; 中设置代理环境变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;export HTTP_PROXY=&quot;http://127.0.0.1:7890&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;export HTTPS_PROXY=&quot;http://127.0.0.1:7890&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;export ALL_PROXY=&quot;socks5://127.0.0.1:7890&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于使用镜像网络模式，WSL 可以直接通过 &lt;code&gt;127.0.0.1&lt;/code&gt; 访问 Windows 的代理端口。&lt;/p&gt;
&lt;h2&gt;前端开发环境配置&lt;/h2&gt;
&lt;h3&gt;包管理器策略&lt;/h3&gt;
&lt;p&gt;我的策略：&lt;strong&gt;全局工具用 Bun，Node 版本管理用 fnm，项目依赖用 pnpm&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bun&lt;/strong&gt;：全局工具速度快、性能好&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fnm&lt;/strong&gt;：轻量级版本管理器，比 nvm 快，支持 &lt;code&gt;.node-version&lt;/code&gt; 和 &lt;code&gt;.nvmrc&lt;/code&gt; 自动切换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pnpm&lt;/strong&gt;：项目依赖管理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安装与版本&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bun 安装&lt;/strong&gt;：&lt;code&gt;curl -fsSL https://bun.sh/install | bash&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;fnm 安装&lt;/strong&gt;：&lt;code&gt;curl -fsSL https://fnm.vercel.app/install | bash&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置自动切换：&lt;code&gt;eval &quot;$(fnm env --use-on-cd)&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装 Node：&lt;code&gt;fnm install 22 &amp;#x26;&amp;#x26; fnm use 22 &amp;#x26;&amp;#x26; fnm default 22&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;pnpm 安装&lt;/strong&gt;：&lt;code&gt;bun install -g pnpm&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我的版本：fnm 1.38.1、Node 22.21.1、Bun 1.3.7、pnpm 10.11.0&lt;/p&gt;
&lt;h3&gt;实际使用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 全局工具用 Bun 安装
bun install -g typescript tsx vite pm2

# 项目开发时
fnm use                 # 自动切换 Node 版本
pnpm install           # 安装依赖
pnpm dev               # 启动开发服务器
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Zsh + Oh My Zsh 配置&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Zsh&lt;/strong&gt;：&lt;code&gt;sudo apt install zsh -y&lt;/code&gt;，设为默认：&lt;code&gt;chsh -s $(which zsh)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Oh My Zsh&lt;/strong&gt;：官方安装脚本 &lt;code&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Powerlevel10k 主题&lt;/strong&gt;：&lt;code&gt;git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必装插件&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;zsh-autosuggestions：&lt;code&gt;git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;zsh-syntax-highlighting：&lt;code&gt;sudo apt install zsh-syntax-highlighting&lt;/code&gt; 或 git clone&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;我的配置&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;在 &lt;code&gt;~/.zshrc&lt;/code&gt; 中&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主题：&lt;code&gt;ZSH_THEME=&quot;powerlevel10k/powerlevel10k&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;插件：&lt;code&gt;plugins=(git zsh-autosuggestions zsh-syntax-highlighting z extract)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;代理（默认启用）：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export HTTP_PROXY=&quot;http://127.0.0.1:7890&quot;
export HTTPS_PROXY=&quot;http://127.0.0.1:7890&quot;
export ALL_PROXY=&quot;socks5://127.0.0.1:7890&quot;
&lt;/code&gt;&lt;/pre&gt;
也可以保留一个 unproxy 函数用于清除代理&lt;/li&gt;
&lt;li&gt;fnm 自动切换 Node 版本：&lt;code&gt;eval &quot;$(fnm env --use-on-cd)&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;字体设置（重要）&lt;/h3&gt;
&lt;p&gt;Powerlevel10k 需要 Nerd Fonts。推荐 &lt;strong&gt;MesloLGS NF&lt;/strong&gt;，下载地址：&lt;a href=&quot;https://www.nerdfonts.com/font-downloads&quot;&gt;https://www.nerdfonts.com/font-downloads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 Windows Terminal 中设置：设置 → Ubuntu-22.04 → 外观 → 字体 → 选择 MesloLGS NF&lt;/p&gt;
&lt;h2&gt;WSL + VSCode 开发配置&lt;/h2&gt;
&lt;h3&gt;核心要点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;必装扩展&lt;/strong&gt;：Remote - WSL（在 WSL 端安装其他扩展）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用扩展&lt;/strong&gt;（在 WSL 端）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ESLint、Prettier：代码检查和格式化&lt;/li&gt;
&lt;li&gt;Vue(Official)：Vue 3 开发&lt;/li&gt;
&lt;li&gt;Tailwind CSS IntelliSense：类名提示&lt;/li&gt;
&lt;li&gt;GitLens：Git 增强&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;从 WSL 启动 VSCode&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在当前目录打开
code .

# 打开指定目录
code ~/projects/my-project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首次运行会自动在 WSL 中安装 VSCode Server。&lt;/p&gt;
&lt;h3&gt;配置要点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;启用保存时格式化和 ESLint 自动修复&lt;/li&gt;
&lt;li&gt;设置 Zsh 为默认终端&lt;/li&gt;
&lt;li&gt;排除不必要的文件监控（node_modules、.nuxt、.next、dist）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;项目放在 WSL 文件系统&lt;/strong&gt; &lt;code&gt;~/projects/&lt;/code&gt;，不要放 &lt;code&gt;/mnt/c/&lt;/code&gt;（很慢）&lt;/li&gt;
&lt;li&gt;Windows Defender 排除 WSL 目录：&lt;code&gt;Add-MpPreference -ExclusionPath &quot;D:\WSL&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;OpenCode 配置&lt;/h2&gt;
&lt;p&gt;OpenCode 是一个强大的 AI 辅助编程工具，支持多种模型和自定义 agent。&lt;/p&gt;
&lt;h3&gt;安装与配置&lt;/h3&gt;
&lt;p&gt;使用 oh-my-opencode 可以一键安装并自动配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装命令：&lt;code&gt;bunx ohmyopencode@latest install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;按照提示选择模型和配置即可&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常用软件&lt;/h2&gt;
&lt;h3&gt;Raycast (软件启动器)&lt;/h3&gt;
&lt;p&gt;一开始 windows 上我用的是 &lt;code&gt;utools&lt;/code&gt;，轻度使用，我知道里面有很多插件，但是打开速度真心不快。 而仅仅作为软件启动器来用，因为我喜欢把桌面上的图标都删掉，露出整个壁纸。&lt;/p&gt;
&lt;p&gt;重装完之后，和群里的小伙伴交流了之下，才发现 Raycast 有 windows 版本，于是尝试了一下，体验上比 windows 强的多。&lt;/p&gt;
&lt;p&gt;比如，我会在搜索框内简单算个数，能可以直接打开 &lt;code&gt;web search&lt;/code&gt;，其他就是启动软件了，刚注册是可以体验AI功能的，不过我完全用不上。&lt;/p&gt;
&lt;h3&gt;微信输入法&lt;/h3&gt;
&lt;p&gt;微信输入法本身输入法相关的功能其实是不如搜狗的，可能也不如别的。 但是它生的好，生态就好。经过和其他设备配对之后，可以很方便的&lt;strong&gt;多端文本、图片复制&lt;/strong&gt;。带有文本剪贴板历史，按&lt;code&gt;v&lt;/code&gt;键激活，也能省下一个单独的剪贴板软件。&lt;/p&gt;
&lt;p&gt;手机端的微信输入法还自带排版成图，类似发小红书的时候把短标题弄个简单背景再发出去。前一阵我用来给文章配封面，但是现在我有自己的&lt;a href=&quot;https://imgx.zzao.club&quot;&gt;IMGX&lt;/a&gt;了，也不太需要了。&lt;/p&gt;
&lt;h3&gt;PixPin 截图工具&lt;/h3&gt;
&lt;p&gt;截图工具一般都要有一个，这个是开源的。我想我还在用它的唯一原因就是我还没来得及用AI复刻一个截图工具吧。&lt;/p&gt;
&lt;h3&gt;Zotepad 文章编辑器/发布工具&lt;/h3&gt;
&lt;p&gt;本来我在 macos 上使用 Obsidian 的，但是我用 AI 又给自己写了一个编辑器。并且集成常用的软件，自己常用的工作流。只会越用越顺手。&lt;/p&gt;
&lt;p&gt;这个工具基于 Tauri2 + Nuxt4，多端一致，目前每次都会打包 macos/安卓/exe 这三个平台的包，对应我在公司使用 windows，在家使用 macos，手上拿的安卓手机。&lt;/p&gt;
&lt;p&gt;目前我内置了&lt;strong&gt;图床功能&lt;/strong&gt;，可以直接上传到腾讯云对象存储，所以我卸载了PicGo (感谢PicGo的陪伴)。&lt;/p&gt;
&lt;p&gt;在我用zotepad写文章时，可以直接打开侧边栏选择已经上传的图片插入，感觉方便多了。&lt;/p&gt;
&lt;p&gt;markdown编辑器选的 &lt;code&gt;milkdown&lt;/code&gt;，所见即所得模式。 复制样式到公众号，这个功能在我博客站上就有，也挪过来了。并且用博客站做了公众号接口的转发，可以直接在编辑器内点击发送到公众号草稿箱。支持文章和图文两种模式。发送完再去手机端的公众号助手审一遍就可以发布了。&lt;/p&gt;
&lt;h3&gt;Flyenv&lt;/h3&gt;
&lt;p&gt;我主要拿它来运行Ngxin、Mysal、Redis、PGSQL，是作为不能用&lt;code&gt;Docker Desktop&lt;/code&gt;时的替代品。&lt;/p&gt;
&lt;h2&gt;使用感受&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;WSL 是最好的 Linux 发行版，Windows 是最好的 Linux 桌面&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这话说的没毛病。&lt;/p&gt;
&lt;p&gt;重点是所有开发环境、代码都在 Ubuntu 里，下次换另一台电脑就和从c盘迁移到别的盘一样，直接把整个打包出来就可以了，十分方便。&lt;/p&gt;
&lt;p&gt;而且可以随意安装多个linux环境，可以实验各种配置，玩坏了直接删除重建。比当年自己装双系统啥的省事多了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;好了分享结束，有问题欢迎评论群交流。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[稳定是最大的牢笼]]></title><link>https://zzao.club/post/daily/stability-is-the-biggest-cage</link><guid isPermaLink="true">https://zzao.club/post/daily/stability-is-the-biggest-cage</guid><pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/api/104/%E3%80%8C%E6%97%A9%E6%97%A9%E9%9B%86%E5%B8%82%E3%80%8D/%E7%AC%AC85%E7%AF%87?bgColor=transparent&amp;#x26;padding=0&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;h2&gt;一&lt;/h2&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;strong&gt;他们说话的时候，带着一种很强的信念感，好像对自己要说的内容有完全的把控。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;什么话题都能接得住，不是说他们多能侃，而是知识面确实很广。&lt;/p&gt;
&lt;p&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;不是羡慕，是震撼。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我突然意识到：我以为的这八年「平淡」，只是我自己的错觉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对别人来说，成长从来没有停止过。&lt;/p&gt;
&lt;p&gt;只是我没有清晰的方向，所以感受不到自己在哪里。&lt;/p&gt;
&lt;h2&gt;二&lt;/h2&gt;
&lt;p&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;看着他的路径，我突然意识到一个一直被忽略的问题：&lt;strong&gt;圈子不是被动获得的，是因为有明确的方向，才会主动去拓展。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个认知其实我以前就知道，只是在这些年的平淡工作中，慢慢被遗忘了。&lt;/p&gt;
&lt;p&gt;这次婚礼，像是把这个认知重新激活了。&lt;/p&gt;
&lt;h2&gt;三&lt;/h2&gt;
&lt;p&gt;回到家后，我开始反思自己这些年的状态。&lt;/p&gt;
&lt;p&gt;我一直在做前端，也在研究 AI。公司是小公司，没有太多试错的空间，所以很多新技术我只能自己买服务器，自己研究。&lt;/p&gt;
&lt;p&gt;听起来挺努力的，但问题是：&lt;strong&gt;这些研究出来的东西，没有经过真实业务的打磨，只能说会个皮毛。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;找工作不是问题，薪资在普通人里也算中上，但我心里清楚，这不是真正的成长。&lt;/p&gt;
&lt;p&gt;我不想用「60分也能教30分」这种话来安慰自己。那只是在用比烂思维粉饰现状。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我想要的是真正的蜕变，不是自我欺骗的安慰。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回想这些年，我虽然也在努力，但总觉得&lt;strong&gt;像在一个封闭的空间里打转&lt;/strong&gt;——有热情，有投入，但方向不清晰，所以认知提升得很慢。&lt;/p&gt;
&lt;p&gt;这才是我真正的问题。&lt;/p&gt;
&lt;h2&gt;四&lt;/h2&gt;
&lt;p&gt;如果时光倒流，我会选择什么？&lt;/p&gt;
&lt;p&gt;考研？读博？进大厂？创业？&lt;/p&gt;
&lt;p&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;改变认知应该和年龄无关。&lt;/p&gt;
&lt;p&gt;我不知道什么时候能跨越阶层，但我知道从今天开始可以做什么：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;找到一个有土壤的环境&lt;/strong&gt;——不一定是大厂，但至少要有业务支撑，让我的技术能真正落地。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主动进入更高的圈子&lt;/strong&gt;——参加技术沙龙、开源社区、行业会议，多和优秀的人交流。不是为了功利地「混圈子」，而是为了看看那些走在前面的人是怎么思考的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;持续输出，建立影响力&lt;/strong&gt;——写文章、录视频、做开源项目。不是为了赚钱，而是为了让更多人看到我在做什么，也让我自己看清楚自己的成长轨迹。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些都不是什么宏大的目标，但至少是可执行的小步骤。&lt;/p&gt;
&lt;h2&gt;五&lt;/h2&gt;
&lt;p&gt;这次经历，对我来说是&lt;strong&gt;一次认知的重新激活&lt;/strong&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;好在，现在知道自己在哪儿了。&lt;/p&gt;
&lt;p&gt;也知道该往哪儿走了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;很庆幸我有一批这样的朋友，以及总是能在毫无保留的分享下获得一些启发。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Skills之后，'学习'的意义被重新定价了]]></title><link>https://zzao.club/post/daily/2026-best-way-to-learn</link><guid isPermaLink="true">https://zzao.club/post/daily/2026-best-way-to-learn</guid><pubDate>Wed, 28 Jan 2026 13:20:37 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;img alt=&quot;&quot;&gt;&lt;/h2&gt;
&lt;p&gt;今天看到&lt;code&gt;antfu&lt;/code&gt;创建一个仓库「&lt;code&gt;skills&lt;/code&gt;」，点进去不出意外是 &lt;strong&gt;Vue、Nuxt、vueuse、Vite&lt;/strong&gt; 等相关的官方&lt;code&gt;SKILL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这意味着你只要在本地写这些生态的项目，AI的幻觉会大大减少，不用去 &lt;code&gt;web_search&lt;/code&gt;一些不知道是否是最新的文档，或者用&lt;code&gt;context7&lt;/code&gt;这种&lt;code&gt;mcp&lt;/code&gt;去查文档了。&lt;/p&gt;
&lt;p&gt;并且最重要的是，&lt;code&gt;SKILL&lt;/code&gt;为了使&lt;code&gt;AI&lt;/code&gt;准确和高效的读取文档，作者一定是写了大量&lt;strong&gt;规范、标准&lt;/strong&gt;的示例来涵盖一个框架或插件尽可能多（或者是全部）的功能。&lt;/p&gt;
&lt;p&gt;并且&lt;code&gt;SKILL.md&lt;/code&gt;会用&lt;strong&gt;尽量少的上下文&lt;/strong&gt;，来索引其他具体文档。&lt;/p&gt;
&lt;p&gt;从开发的角度来讲，以前&lt;code&gt;AI&lt;/code&gt;因为没拿到最新文档而使用旧版本代码的情况几乎不会发生了（只要你用了&lt;code&gt;SKILL&lt;/code&gt;），产出的代码更准确，效率应该会大大提高。&lt;/p&gt;
&lt;p&gt;但是另一方面，这些文档你都不用学了，直接喂到嘴里了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;开发者的焦虑和被(即将)替代的感觉也会大大增加。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;卖AI课的赚麻了，写代码的，麻了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是我看完了&lt;code&gt;Nuxt&lt;/code&gt;的&lt;code&gt;SKILL&lt;/code&gt;之后，又发现了好的一面。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果有人和我一样，从&lt;code&gt;Nuxt3&lt;/code&gt;开始啃文档，搜&lt;code&gt;Github issues&lt;/code&gt;，一定会感叹，这个&lt;code&gt;SKILL.md&lt;/code&gt;真的比官方文档还要&lt;strong&gt;高效&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了让AI快速找到对应要读的&lt;code&gt;md&lt;/code&gt;，&lt;strong&gt;归类了几个核心&lt;/strong&gt;，以及罗列了&lt;code&gt;Nuxt&lt;/code&gt;的&lt;strong&gt;核心特性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你知道这意味着什么吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你还愿意学习一个框架&lt;/strong&gt;，比如&lt;code&gt;Nuxt&lt;/code&gt;，这个&lt;code&gt;SKILL&lt;/code&gt;会让你的&lt;strong&gt;学习效率大大提高&lt;/strong&gt;，而且这个库肯定会随着&lt;code&gt;Nuxt&lt;/code&gt;更新而更新，不用担心内容落后。&lt;/p&gt;
&lt;p&gt;简直太幸福了，你不知道我踩了多少&lt;code&gt;Nuxt&lt;/code&gt;的坑...&lt;/p&gt;
&lt;p&gt;不只是最直观的阅读效率提高了。重点它是&lt;code&gt;SKILL&lt;/code&gt;啊！&lt;code&gt;SKILL&lt;/code&gt;的特点就是可以随意组合！&lt;/p&gt;
&lt;p&gt;如果你用另一个面试相关的SKILL，让其去读&lt;code&gt;Nuxt SKILL&lt;/code&gt;，那它问的问题，回答的答案，都会无比准确。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果SKILL.md对比官方文档是把饭嚼碎了喂嘴里&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结合自己的SKILL去学Nuxt，这简直就是直接插上USB传输到脑子里了&lt;/strong&gt;😄&lt;/p&gt;
&lt;p&gt;想到这给我激动坏了&lt;/p&gt;
&lt;p&gt;但冷静下来，我发现一件吊诡的事：&lt;/p&gt;
&lt;p&gt;以前我们啃文档，是因为&lt;strong&gt;不得不啃&lt;/strong&gt;——那是门票，是谋生通行证。现在 Skills 把门拆了，谁都能进来逛一圈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结果&quot;愿不愿意学习&quot;反而成了新的筛选器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就像那个高赞评论说的：有钱人去挥霍享受，我一点都不羡慕。但是他们去学习、去提升自己，我是真的嫉妒。&lt;/p&gt;
&lt;p&gt;以前嫉妒，是因为我们没那个时间资本。现在 Skills 把学习成本打到地板下，&lt;strong&gt;我们终于有了&quot;选择不学&quot;的自由。而这恰恰是最奢侈的陷阱。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，你还愿意学编程吗 🤨&lt;/p&gt;
&lt;p&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;&lt;img alt=&quot;1.00&quot;&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[用AI向内挖掘自己]]></title><description><![CDATA[最近大家都在聊AI，各种工具、产品层出不穷。说实话，刚开始我也跟风用AI写代码、写文案，结果发现一个问题：AI生成的东西越来越多，自己看的越来越少。]]></description><link>https://zzao.club/post/ai/explore/use-ai-dig-yourself</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/use-ai-dig-yourself</guid><pubDate>Thu, 22 Jan 2026 10:53:53 GMT</pubDate><content:encoded>&lt;p&gt;最近大家都在聊AI，各种工具、产品层出不穷。说实话，刚开始我也跟风用AI写代码、写文案，结果发现一个问题：&lt;strong&gt;AI生成的东西越来越多，自己看的越来越少。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有次改bug，看着AI写的代码，愣是没看懂逻辑。要是自己写的，哪怕写得烂，至少知道哪里有坑。&lt;/p&gt;
&lt;p&gt;这让我开始反思：&lt;strong&gt;AI擅长写，那让它来读呢&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;后来试了一次，让AI分析我写的代码，指出问题。结果它真的毫不留情地把我的烂逻辑、冗余代码全挑出来了。那一刻才明白，与其让AI替我输出，不如让它审视我。&lt;/p&gt;
&lt;h2&gt;为什么要向内挖？&lt;/h2&gt;
&lt;p&gt;AI最有用的地方，不是它能写出多精准的代码，而是它能不讲情面地指出你的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码写得再好，也强不过AI。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但通过分析你的知识体系，AI能准确找出你的不足，并告诉你怎么改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只有自身的代码水平、架构能力、表达能力提升了，AI才会跟着你变强。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种对话还有个好处：&lt;strong&gt;没有人际压力&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你和领导聊天，得斟酌用词；和同事交流，怕暴露短板；自己复盘，又容易放过自己。AI就是一面镜子，你问什么它答什么，完全没顾虑。&lt;/p&gt;
&lt;p&gt;而且比搜索引擎精准。搜索给你一堆资料，你得自己筛选；问人的话，大部分时候别人知道你有问题，但没义务指出来。&lt;/p&gt;
&lt;p&gt;AI不一样，它可以事无巨细地给你分析，直到你真正理解。&lt;/p&gt;
&lt;h2&gt;具体怎么做？&lt;/h2&gt;
&lt;p&gt;拿技术面试场景举例：&lt;/p&gt;
&lt;p&gt;让AI扮演面试官，检测自己的技术水平：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;让AI提问&lt;/strong&gt;：给它一个角色设定，让它针对我的方向出题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多轮对话&lt;/strong&gt;：针对薄弱的地方，反复探讨，直到自己能完整表述出来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出文档&lt;/strong&gt;：把对话整理成md文档，方便回顾&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里要强调一点：&lt;strong&gt;必须是「表述」正确，不是「理解」正确&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你脑子里会，和你能说清楚，完全是两码事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI算是一个可以让你毫无心理压力表述答案的面试官了，真人面试时，还会受情绪影响。&lt;/p&gt;
&lt;p&gt;所以我会把对话记录下来，看看自己哪些地方说得不清楚，然后继续优化表达。&lt;/p&gt;
&lt;h2&gt;提示词参考&lt;/h2&gt;
&lt;p&gt;下面是我目前的提示词，你可以根据自己的情况改动&lt;code&gt;{}&lt;/code&gt;里的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;请你扮演一位资深的开发主管/前端技术负责人，以面试官的身份向我提出 {N} 个有代表性、有深度、能体现技术水平的面试题。

**面试流程**：
- 采用一问一答的形式，逐题提问，等待我回答后再提出下一题
- 如遇到不熟悉的题目，可以说明情况并跳过，不影响其他题目评分
- 全部问题回答完毕后，对每题进行评分（满分10分）
- 提供犀利但客观的点评，指出回答的优点和不足
- 给出每道题的标准答案作为参考

**输出要求**：
面试结束后，生成以下4份文档到 {目标目录}：
1. 面试题提示词模板（本模板文件）
2. 面试题目清单（所有题目及详细描述）
3. 面试题标准答案（每题的合格回答参考）
4. 面试评估报告（包含候选人原始回答、评分、点评）

**面试对象**：{岗位名称}

**面试方向**：{技术方向列表}

**参数配置**：
1. 面试时长：{时长}分钟
2. 技术方向权重：
   - {方向1}：{权重}（最高/次之/一般/少量）
   - {方向2}：{权重}
   - {方向3}：{权重}
   - ...
3. 题目分布：
   - 技术深度：{比例}%
   - 实战场景：{比例}%
   - 架构设计：{比例}%
   - 综合管理：{比例}%
4. 公司背景：{创业公司/中大型公司/技术驱动型公司}

**题目设计原则**：
1. 技术深度题（{比例}%）：考察底层原理理解，需解释&quot;为什么&quot;和&quot;怎么实现&quot;
   示例：Vue响应式原理、Node.js事件循环、Virtual DOM diff算法
2. 实战场景题（{比例}%）：考察问题解决能力，给出具体业务场景求解决方案
   示例：大数据量性能优化、技术栈迁移策略、内存泄漏排查
3. 架构设计题（{比例}%）：考察系统思维和架构能力，需设计完整技术方案
   示例：微前端架构、高并发API网关、全栈应用技术选型
4. 综合管理题（{比例}%）：考察团队协作和项目管理能力，关注软技能
   示例：技术债务平衡、代码规范落地、Code Review文化建设

**标准答案输出要求**：
- 仅用文字描述核心要点，不要提供代码示例
- 不要使用正反对比的举例说明（如&quot;❌ 错误示例&quot;、&quot;✅ 正确示例&quot;）
- 答案需克制、精炼，点到为止，避免过度展开
- 控制每题答案在 300-500 字以内
- 重点说明&quot;是什么&quot;和&quot;为什么&quot;，不需详述&quot;怎么做&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不只是技术&lt;/h2&gt;
&lt;p&gt;这套方法不只适合技术。&lt;/p&gt;
&lt;p&gt;生活、情感、人际关系，AI或许有独到的见解，毕竟它见得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不管是你问它，还是让它反问你，都是一场在生活中少有的、直指内心的问答。&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[使用 Chrome DevTools 排查内存泄漏]]></title><link>https://zzao.club/post/performance/debugging-memory-leaks-with-chrome-devtools</link><guid isPermaLink="true">https://zzao.club/post/performance/debugging-memory-leaks-with-chrome-devtools</guid><pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;内存泄漏是前端性能优化中最难排查的问题之一。本文将详细介绍如何使用 Chrome DevTools 的 Performance 和 Memory 面板，系统化地排查并定位内存泄漏问题，并映射到真实代码逻辑。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一、内存泄漏的识别&lt;/h2&gt;
&lt;h3&gt;典型症状&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;页面运行一段时间后变卡&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;滚动、点击响应变慢&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器标签页显示内存占用持续增长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最终页面崩溃（Out of Memory）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;快速检测&lt;/h3&gt;
&lt;p&gt;在控制台运行以下代码,观察内存是否持续增长：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setInterval(() =&gt; {
  const usedMB = performance.memory.usedJSHeapSize / 1024 / 1024
  console.log(`当前内存占用: ${usedMB.toFixed(2)} MB`)
}, 1000)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;二、Performance 面板：确认是否泄漏&lt;/h2&gt;
&lt;h3&gt;录制内存快照&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开 DevTools (F12) → Performance 标签&lt;/li&gt;
&lt;li&gt;勾选 &lt;strong&gt;Memory&lt;/strong&gt; 选项&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;Record&lt;/strong&gt; 录制 30-60 秒&lt;/li&gt;
&lt;li&gt;执行可疑操作（滚动列表、打开关闭弹窗等）&lt;/li&gt;
&lt;li&gt;停止录制&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;分析内存走势图&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;正常情况&lt;/strong&gt;（有涨有跌,GC 能回收）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Memory (MB)
  ↑     ╱╲      ╱╲      ╱╲
  │    ╱  ╲    ╱  ╲    ╱  ╲
  └─────────────────────────→ 时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;内存泄漏&lt;/strong&gt;（持续上涨,呈阶梯状）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Memory (MB)
  ↑   ╱──────╱─────╱─────╱──
  │  ╱      ╱     ╱     ╱
  └─────────────────────────→ 时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;判断标准&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;✅ 正常：内存有涨有跌,GC 后能降下来&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;❌ 泄漏：内存持续上涨,GC 后仍然增长&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;三、Memory 面板：定位泄漏点&lt;/h2&gt;
&lt;h3&gt;对比堆快照&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;操作流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DevTools → Memory 标签 → 选择 &quot;Heap snapshot&quot;&lt;/li&gt;
&lt;li&gt;点击 &quot;Take snapshot&quot; → 获得快照 1&lt;/li&gt;
&lt;li&gt;执行可疑操作（如打开 10 次弹窗后关闭）&lt;/li&gt;
&lt;li&gt;强制垃圾回收（点击 🗑️ 图标）&lt;/li&gt;
&lt;li&gt;点击 &quot;Take snapshot&quot; → 获得快照 2&lt;/li&gt;
&lt;li&gt;切换视图为 &lt;strong&gt;Comparison&lt;/strong&gt; → 选择 &quot;between Snapshot 1 and Snapshot 2&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;关键列说明&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;列名&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;关注点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;# Delta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;净增加的对象数量&lt;/td&gt;
&lt;td&gt;应该接近 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size Delta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;净增加的内存&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;最关键的指标&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alloc. Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;新增对象占用内存&lt;/td&gt;
&lt;td&gt;持续增长说明泄漏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Freed Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;释放的内存&lt;/td&gt;
&lt;td&gt;应该接近 Alloc. Size&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;查找泄漏对象&lt;/h3&gt;
&lt;p&gt;按 &lt;strong&gt;Size Delta&lt;/strong&gt; 排序,找到占用内存最多的对象类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constructor              # Delta    Size Delta
─────────────────────────────────────────────
(array)                  +500       +2.5 MB    ← 可疑！数组持续增长
Detached HTMLDivElement  +200       +800 KB    ← DOM 泄漏
EventListener            +150       +150 KB    ← 事件监听器未移除
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;四、定位到真实代码&lt;/h2&gt;
&lt;h3&gt;查看 Retainers（保留路径）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;这是最关键的环节！&lt;/strong&gt; Retainers 显示了为什么这个对象没有被垃圾回收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作&lt;/strong&gt;：点击可疑对象 → 选择具体实例 → 右侧面板显示 &lt;strong&gt;Retainers&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;解读 Retainers 路径&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;示例 1：全局变量引用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Retainers:
  → Window / http://localhost:3000
    → VueComponent                ← Vue 组件实例
      → setupState                ← setup() 返回的状态
        → allData                  ← 你的变量名
          → @123456 (array)       ← 泄漏的数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如何对应到代码&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;看到 &lt;code&gt;allData&lt;/code&gt; → 在代码中搜索 &lt;code&gt;const allData = ref(...)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;看到 &lt;code&gt;VueComponent&lt;/code&gt; → 定位到具体的组件文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例 2：事件监听器引用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Retainers:
  → Window
    → eventListeners          ← 全局事件监听器映射
      → scroll                ← scroll 事件
        → [[Handler]]
          → VueComponent      ← 组件实例被闭包引用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：&lt;code&gt;scroll&lt;/code&gt; 事件监听器没有被移除,闭包引用了组件实例。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;五、常见内存泄漏模式&lt;/h2&gt;
&lt;p&gt;通过 Memory 面板,常见的泄漏对象类型：&lt;/p&gt;
&lt;h3&gt;5.1 DOM 泄漏&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constructor              # Delta    Size Delta
────────────────────────────────────────────
Detached HTMLDivElement  +200       +800 KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：移除 DOM 时未清理事件监听器,导致元素无法被 GC&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在移除 DOM 前调用 &lt;code&gt;removeEventListener&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.2 定时器泄漏&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constructor    # Delta    Size Delta
────────────────────────────────────
Timeout        +50        +100 KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：组件销毁时定时器仍在运行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 &lt;code&gt;onUnmounted&lt;/code&gt; 中调用 &lt;code&gt;clearInterval&lt;/code&gt; / &lt;code&gt;clearTimeout&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.3 事件监听器泄漏&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constructor      # Delta    Size Delta
──────────────────────────────────────
EventListener    +100       +200 KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：全局事件监听器未移除&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 &lt;code&gt;onUnmounted&lt;/code&gt; 中调用 &lt;code&gt;window.removeEventListener&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.4 组件实例泄漏&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constructor        # Delta    Size Delta
────────────────────────────────────────
VueComponent       +50        +5 MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：事件总线监听器未注销,导致组件实例无法释放&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 &lt;code&gt;onUnmounted&lt;/code&gt; 中调用 &lt;code&gt;bus.off&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.5 闭包引用大对象&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Constructor    # Delta    Size Delta
────────────────────────────────────
(array)        +100       +10 MB
(closure)      +50        +500 KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：事件处理函数闭包不必要地引用了大对象&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：只保留必需的数据,或在使用后手动设为 &lt;code&gt;null&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;六、实战案例：排查 Vue3 虚拟滚动内存泄漏&lt;/h2&gt;
&lt;h3&gt;6.1 问题症状&lt;/h3&gt;
&lt;p&gt;Performance 面板录制 60 秒后发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JS Heap 从 50MB 增长到 150MB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存呈阶梯状持续增长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有明显的 GC 回收&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 Memory 面板对比快照&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Comparison (Snapshot 1 vs Snapshot 2)：

Constructor      # Delta    Size Delta
────────────────────────────────────────
(array)          +1        +50 MB      ← data 数组持续增长
EventListener    +1        +100 KB     ← scroll 监听器未清理
WebSocket        +1        +50 KB      ← WebSocket 未关闭
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.3 Retainers 定位代码&lt;/h3&gt;
&lt;p&gt;通过查看 Retainers 路径,找到 3 个泄漏点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;data 数组&lt;/strong&gt;：&lt;code&gt;Window → VueComponent → setupState → data&lt;/code&gt; 持续增长,没有限制大小&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scroll 监听器&lt;/strong&gt;：&lt;code&gt;Window → eventListeners → scroll&lt;/code&gt; 未移除,闭包引用了 &lt;code&gt;data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;：组件销毁时未关闭,回调闭包引用了 &lt;code&gt;data&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.4 修复方案&lt;/h3&gt;
&lt;p&gt;根据 Retainers 路径,需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;限制 &lt;code&gt;data&lt;/code&gt; 数组最大长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;onUnmounted&lt;/code&gt; 中移除 &lt;code&gt;scroll&lt;/code&gt; 监听器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;onUnmounted&lt;/code&gt; 中关闭 WebSocket 连接&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;shallowRef&lt;/code&gt; 优化大数据响应式开销&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;七、快速检查清单&lt;/h2&gt;
&lt;p&gt;Vue 3 组件中需要在 &lt;code&gt;onUnmounted&lt;/code&gt; 清理的资源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定时器&lt;/strong&gt;：&lt;code&gt;clearInterval(timer)&lt;/code&gt; / &lt;code&gt;clearTimeout(timer)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局事件监听&lt;/strong&gt;：&lt;code&gt;window.removeEventListener(&apos;resize&apos;, handler)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;：&lt;code&gt;ws.close()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三方库实例&lt;/strong&gt;：&lt;code&gt;chart.dispose()&lt;/code&gt; (如 ECharts)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件总线&lt;/strong&gt;：&lt;code&gt;bus.off(&apos;event&apos;, handler)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observer&lt;/strong&gt;：&lt;code&gt;observer.disconnect()&lt;/code&gt; (IntersectionObserver/ResizeObserver)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大数据限制&lt;/strong&gt;：使用 &lt;code&gt;shallowRef&lt;/code&gt; + 限制数组最大长度&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;八、总结&lt;/h2&gt;
&lt;h3&gt;排查流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Performance 面板&lt;/strong&gt; → 确认是否泄漏（观察内存走势图）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory 面板&lt;/strong&gt; → 对比快照,找到泄漏对象类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查看 Retainers&lt;/strong&gt; → 找到引用路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射到代码&lt;/strong&gt; → 通过变量名定位文件和行号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修复 + 验证&lt;/strong&gt; → 清理资源,再次录制确认修复&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;关键技巧&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;看 Retainers&lt;/strong&gt;：这是定位代码的关键,显示从 Window 到具体变量的完整路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;认识常见模式&lt;/strong&gt;：定时器、事件监听器、DOM 引用、闭包是主要原因&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用 shallowRef&lt;/strong&gt;：大数据场景必备,减少响应式开销&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;限制数据量&lt;/strong&gt;：虚拟滚动中必须限制数组大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;清理资源&lt;/strong&gt;：&lt;code&gt;onUnmounted&lt;/code&gt; 中清理所有副作用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;最佳实践&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;✅ 所有副作用都在 &lt;code&gt;onUnmounted&lt;/code&gt; 中清理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 使用 &lt;code&gt;shallowRef&lt;/code&gt; 存储大数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 限制列表/数组的最大长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 避免闭包捕获大对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 定期用 DevTools 检查内存占用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.chrome.com/docs/devtools/memory-problems/&quot;&gt;Chrome DevTools 官方文档 - Memory&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://vuejs.org/guide/best-practices/performance.html&quot;&gt;Vue 3 性能优化指南&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management&quot;&gt;JavaScript 内存管理 - MDN&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[高级前端开发面试提示词模板]]></title><link>https://zzao.club/post/ai/explore/01-interview-prompt-template</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/01-interview-prompt-template</guid><pubDate>Wed, 21 Jan 2026 07:30:52 GMT</pubDate><content:encoded>&lt;h2&gt;核心提示词&lt;/h2&gt;
&lt;p&gt;以下是完整的提示词模板，复制后替换 &lt;code&gt;{}&lt;/code&gt; 占位符即可使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;请你扮演一位资深的开发主管/前端技术负责人，以面试官的身份向我提出 {N} 个有代表性、有深度、能体现技术水平的面试题。

**面试流程**：
- 采用一问一答的形式，逐题提问，等待我回答后再提出下一题
- 如遇到不熟悉的题目，可以说明情况并跳过，不影响其他题目评分
- 全部问题回答完毕后，对每题进行评分（满分10分）
- 提供犀利但客观的点评，指出回答的优点和不足
- 给出每道题的标准答案作为参考

**输出要求**：
面试结束后，生成以下4份文档到 {目标目录}：
1. 面试题提示词模板（本模板文件）
2. 面试题目清单（所有题目及详细描述）
3. 面试题标准答案（每题的合格回答参考）
4. 面试评估报告（包含候选人原始回答、评分、点评）

**面试对象**：{岗位名称}

**面试方向**：{技术方向列表}

**参数配置**：
1. 面试时长：{时长}分钟
2. 技术方向权重：
   - {方向1}：{权重}（最高/次之/一般/少量）
   - {方向2}：{权重}
   - {方向3}：{权重}
   - ...
3. 题目分布：
   - 技术深度：{比例}%
   - 实战场景：{比例}%
   - 架构设计：{比例}%
   - 综合管理：{比例}%
4. 公司背景：{创业公司/中大型公司/技术驱动型公司}

**题目设计原则**：
1. 技术深度题（{比例}%）：考察底层原理理解，需解释&quot;为什么&quot;和&quot;怎么实现&quot;
   示例：Vue响应式原理、Node.js事件循环、Virtual DOM diff算法
2. 实战场景题（{比例}%）：考察问题解决能力，给出具体业务场景求解决方案
   示例：大数据量性能优化、技术栈迁移策略、内存泄漏排查
3. 架构设计题（{比例}%）：考察系统思维和架构能力，需设计完整技术方案
   示例：微前端架构、高并发API网关、全栈应用技术选型
4. 综合管理题（{比例}%）：考察团队协作和项目管理能力，关注软技能
   示例：技术债务平衡、代码规范落地、Code Review文化建设

**标准答案输出要求**：
- 仅用文字描述核心要点，不要提供代码示例
- 不要使用正反对比的举例说明（如&quot;❌ 错误示例&quot;、&quot;✅ 正确示例&quot;）
- 答案需克制、精炼，点到为止，避免过度展开
- 控制每题答案在 300-500 字以内
- 重点说明&quot;是什么&quot;和&quot;为什么&quot;，不需详述&quot;怎么做&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;提示词设计说明&lt;/h2&gt;
&lt;p&gt;本模板通过以下机制控制AI面试官行为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;角色定位&lt;/strong&gt;：&quot;开发主管、前端主管的面试官&quot;明确专业视角和权威性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互模式&lt;/strong&gt;：&quot;一问一答&quot;确保面试流程规范，避免一次性抛出所有题目&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构化输出&lt;/strong&gt;：要求生成4份独立文档，便于后续整理和分享&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数化配置&lt;/strong&gt;：通过权重、比例等参数精确控制题目分布和难度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;题目设计原则&lt;/strong&gt;：明确4类题型占比，确保全面考察技术能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;答案输出约束&lt;/strong&gt;：限制代码示例和冗余对比，大幅节省token消耗（预计节省80%+）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;核心设计理念&lt;/strong&gt;：通过明确的输出约束和格式要求，使AI生成的内容既专业又高效。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;使用示例&lt;/h2&gt;
&lt;h3&gt;本次面试使用的配置&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;岗位: 高级前端开发（偏Node全栈）
面试时长: 30-40分钟
题目总数: 10题

技术方向权重:
  - Vue2/Vue3: 最高 (6题)
  - Node.js: 次之 (2题)
  - 架构设计: 一般 (1题)
  - 团队管理: 一般 (1题)
  - 服务器运维: 少量 (融入其他题目)

题目分布:
  - 技术深度: 40%
  - 实战场景: 40%
  - 架构设计: 10%
  - 综合管理: 10%

评分标准:
  - 10-9分: 深度理解原理 + 丰富实战经验 + 有独特见解
  - 8-7分: 理解原理 + 有实战经验 + 回答完整
  - 6-5分: 基本理解 + 有一定经验 + 回答尚可
  - 4-3分: 理解片面 + 经验不足 + 回答欠缺
  - 2-1分: 不理解原理 + 无实战经验
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;扩展参考&lt;/h2&gt;
&lt;p&gt;以下内容为提示词设计的补充说明，帮助你根据实际需求定制化调整。&lt;/p&gt;
&lt;h3&gt;面试流程指引&lt;/h3&gt;
&lt;h3&gt;Phase 1: 开场（2分钟）&lt;/h3&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;h3&gt;Phase 2: 提问环节（30-40分钟）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一问一答&lt;/strong&gt;：提出一题，等待回答完再提下一题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时记录&lt;/strong&gt;：记录候选人的原始回答（不修改）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适度追问&lt;/strong&gt;：如果回答太简单，可以追问细节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳过机制&lt;/strong&gt;：允许候选人跳过不熟悉的题目&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Phase 3: 评估环节（面试后）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对每题进行评分（满分10分）&lt;/li&gt;
&lt;li&gt;撰写犀利点评（指出优点和不足）&lt;/li&gt;
&lt;li&gt;提供合格回答参考&lt;/li&gt;
&lt;li&gt;生成综合评估报告&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;评分维度&lt;/h2&gt;
&lt;h3&gt;技术理解（40%）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是否理解底层原理？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能否解释技术实现细节？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否了解技术的边界和限制？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实战经验（30%）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是否有实际项目经验？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否遇到过相关问题？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决方案是否合理有效？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;思维深度（20%）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是否有系统性思维？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能否权衡不同方案的优劣？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否有独特的见解？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;表达能力（10%）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;回答是否清晰有条理？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否能准确表达技术概念？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否能结合实例说明？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;定制化建议&lt;/h2&gt;
&lt;h3&gt;针对不同岗位调整&lt;/h3&gt;
&lt;h4&gt;初级前端（1-3年）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;减少原理深度题，增加基础概念题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重点考察：HTML/CSS/JS基础、Vue/React基础使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;评分标准适当放宽&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;中级前端（3-5年）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;平衡原理和实战&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重点考察：组件设计、状态管理、性能优化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开始考察架构思维&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;高级前端/全栈（5年+）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;深度原理 + 复杂场景 + 架构设计&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重点考察：技术选型、性能调优、团队协作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要求有独特见解和丰富经验&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;针对不同技术栈调整&lt;/h3&gt;
&lt;h4&gt;React生态&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;React Hooks原理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redux vs MobX vs Zustand&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Next.js SSR/SSG&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;React Server Components&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Vue生态&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Vue响应式原理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vuex vs Pinia&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Nuxt.js SSR/SSG&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vue Composition API&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Node.js后端&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Event Loop深度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Koa/Express/Fastify/NestJS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据库优化（MySQL/MongoDB/Redis）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;微服务架构&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;h3&gt;面试官须知&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;营造轻松氛围&lt;/strong&gt;：面试是双向选择，不要让候选人过度紧张&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鼓励实战分享&lt;/strong&gt;：实战经验比理论知识更有参考价值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;允许跳过题目&lt;/strong&gt;：不熟悉的题目可以跳过，不影响整体评价&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客观公正评分&lt;/strong&gt;：避免主观偏见，按评分标准执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供建设性反馈&lt;/strong&gt;：点评要犀利但不刻薄，指出改进方向&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;候选人须知&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;结合实战经验&lt;/strong&gt;：尽量用实际项目案例说明&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;承认不足&lt;/strong&gt;：不懂的直接说不懂，不要瞎编&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思路清晰&lt;/strong&gt;：先说核心观点，再展开细节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追问细节&lt;/strong&gt;：如果问题不清楚，可以要求面试官澄清&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;输出文档格式&lt;/h2&gt;
&lt;h3&gt;1. 面试题提示词模板&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;本文档&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 面试题目清单&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 高级前端开发面试题目清单

## Vue2/Vue3技术（6题）
1. Vue响应式原理对比
2. 组件通信方案选型
...

## Node.js技术（2题）
7. Event Loop执行顺序
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 面试题标准答案&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 高级前端开发面试题标准答案

## 第1题：Vue响应式原理
### 问题
...
### 标准答案
1. 技术实现层面
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 面试评估报告&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# 高级前端开发面试评估报告

## 候选人信息
- 面试时间：YYYY-MM-DD
- 面试岗位：高级前端开发
- 总分：XX/100

## 详细评估
### 第1题：Vue响应式原理【得分：X/10】
#### 候选人回答
...
#### 评分与点评
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;文档元信息&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版本&lt;/strong&gt;：v1.1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建日期&lt;/strong&gt;：2026-01-21&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后更新&lt;/strong&gt;：2026-01-21&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：高级前端开发（偏Node全栈）技术面试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token优化&lt;/strong&gt;：标准答案部分预计节省80%+ token消耗&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[高级前端开发面试题标准答案]]></title><description><![CDATA[文档说明：本文档提供所有面试题的标准答案索引。每题的详细答案已拆分到独立文件中。]]></description><link>https://zzao.club/post/ai/explore/03-interview-standard-answers</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-interview-standard-answers</guid><pubDate>Wed, 21 Jan 2026 07:25:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;文档说明&lt;/strong&gt;：本文档提供所有面试题的标准答案索引。每题的详细答案已拆分到独立文件中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;创建日期&lt;/strong&gt;：2026-01-21&lt;br&gt;
&lt;strong&gt;更新日期&lt;/strong&gt;：2026-01-21（拆分为独立文件）&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;h3&gt;Vue2/Vue3 技术部分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[第1题：Vue 响应式原理]&lt;/li&gt;
&lt;li&gt;[第2题：Vue 组件通信]&lt;/li&gt;
&lt;li&gt;[第3题：Vue3 性能优化]&lt;/li&gt;
&lt;li&gt;[第4题：Vue2 迁移到 Vue3]&lt;/li&gt;
&lt;li&gt;[第5题：Vue Diff 算法]&lt;/li&gt;
&lt;li&gt;[第6题：Nuxt.js SSR 实战]&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Node.js 技术部分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[第7题：Node.js Event Loop]&lt;/li&gt;
&lt;li&gt;[第8题：Node.js 性能调优]&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;架构设计部分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[第9题：微前端架构设计]&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;团队管理部分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[第10题：团队管理与技术债务]&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;使用说明&lt;/h2&gt;
&lt;p&gt;每个题目的标准答案文件包含：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;题目完整描述&lt;/strong&gt;：包含场景、考察点等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标准答案&lt;/strong&gt;：分点详细阐述&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码示例&lt;/strong&gt;：实际可运行的代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：行业经验和建议&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见误区&lt;/strong&gt;：需要避免的陷阱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展阅读&lt;/strong&gt;：相关技术文档链接&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;答案文件列表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件名&lt;/th&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;技术领域&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;03-01-vue-reactive.md&lt;/td&gt;
&lt;td&gt;Vue 响应式原理&lt;/td&gt;
&lt;td&gt;Vue 核心原理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-02-component-communication.md&lt;/td&gt;
&lt;td&gt;Vue 组件通信&lt;/td&gt;
&lt;td&gt;Vue 状态管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-03-vue3-performance.md&lt;/td&gt;
&lt;td&gt;Vue3 性能优化&lt;/td&gt;
&lt;td&gt;Vue 性能调优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-04-vue-migration.md&lt;/td&gt;
&lt;td&gt;Vue2 迁移到 Vue3&lt;/td&gt;
&lt;td&gt;Vue 版本升级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-05-vue-diff.md&lt;/td&gt;
&lt;td&gt;Vue Diff 算法&lt;/td&gt;
&lt;td&gt;Vue 核心原理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-06-nuxt-ssr.md&lt;/td&gt;
&lt;td&gt;Nuxt.js SSR 实战&lt;/td&gt;
&lt;td&gt;Vue SSR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-07-event-loop.md&lt;/td&gt;
&lt;td&gt;Node.js Event Loop&lt;/td&gt;
&lt;td&gt;Node.js 核心&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-08-nodejs-performance.md&lt;/td&gt;
&lt;td&gt;Node.js 性能调优&lt;/td&gt;
&lt;td&gt;Node.js 性能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-09-micro-frontend.md&lt;/td&gt;
&lt;td&gt;微前端架构设计&lt;/td&gt;
&lt;td&gt;前端架构&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03-10-team-management.md&lt;/td&gt;
&lt;td&gt;团队管理与技术债务&lt;/td&gt;
&lt;td&gt;工程管理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;</content:encoded></item><item><title><![CDATA[10.团队管理与技术债务 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-10-team-management</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-10-team-management</guid><pubDate>Wed, 21 Jan 2026 07:24:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;:管理能力、技术推动、团队协作&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;:&lt;br&gt;
作为高级前端开发/技术负责人,您不仅要写代码,还要带团队、推动项目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;:&lt;br&gt;
您接手一个老项目,代码质量堪忧:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有代码规范,每个人的代码风格完全不同&lt;/li&gt;
&lt;li&gt;没有单元测试,改代码如履薄冰&lt;/li&gt;
&lt;li&gt;技术栈老旧(Vue2 + Webpack4),但业务压力大,没时间重构&lt;/li&gt;
&lt;li&gt;团队成员水平参差不齐,Code Review 流于形式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术债务平衡&lt;/strong&gt;:业务需求紧急 vs 技术重构,如何平衡?您会采取什么策略?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队规范落地&lt;/strong&gt;:如何推动代码规范、ESLint、Prettier、Git Commit 规范在团队中落地?(而不是仅仅写个文档)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Review 文化&lt;/strong&gt;:如何让 Code Review 真正发挥作用,而不是走形式?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;:分享一个您在团队管理或技术推动中遇到的难题,以及您的解决方案。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. 技术债务与业务需求的平衡策略&lt;/h3&gt;
&lt;h4&gt;核心原则:&quot;二八法则&quot; + &quot;渐进式重构&quot;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;20% 的时间投入技术建设,80% 的时间保障业务交付。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;具体策略&lt;/h4&gt;
&lt;h5&gt;a) 技术债务分级管理&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;级别&lt;/th&gt;
&lt;th&gt;特征&lt;/th&gt;
&lt;th&gt;处理策略&lt;/th&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P0 - 致命&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;影响系统稳定性、安全漏洞、性能严重问题&lt;/td&gt;
&lt;td&gt;立即修复,可申请暂停需求&lt;/td&gt;
&lt;td&gt;🔴 最高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P1 - 严重&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;影响开发效率、代码难以维护、重复劳动多&lt;/td&gt;
&lt;td&gt;排入迭代,每个迭代修复 1-2 项&lt;/td&gt;
&lt;td&gt;🟠 高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P2 - 中等&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;代码不规范、缺少测试、技术栈老旧&lt;/td&gt;
&lt;td&gt;结合业务需求渐进式重构&lt;/td&gt;
&lt;td&gt;🟡 中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P3 - 轻微&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;优化机会、新技术尝试&lt;/td&gt;
&lt;td&gt;技术分享、个人时间探索&lt;/td&gt;
&lt;td&gt;🟢 低&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P0&lt;/strong&gt;: 线上内存泄漏、SQL 注入漏洞 → 立即修复&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P1&lt;/strong&gt;: 打包耗时 10 分钟、前端构建失败率高 → 本迭代优化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2&lt;/strong&gt;: Vue2 升级 Vue3、添加单元测试 → 结合新需求逐步重构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P3&lt;/strong&gt;: 尝试 Vite、探索 Tailwind CSS → 技术沙龙分享&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;b) &quot;新老交替&quot;策略(推荐)&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;原则: 新代码高标准,老代码不动(除非必须改)

具体做法:
1. 制定《新代码规范》(ESLint + Prettier + Git Hooks)
2. 新功能必须符合新规范,老代码暂不强制
3. 修改老代码时,同步重构该模块(限定范围)
4. 逐步推进,避免大爆炸式重构
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;案例&lt;/strong&gt;: 团队引入 TypeScript&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;做法&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;❌ 错误&lt;/td&gt;
&lt;td&gt;一次性把所有 .js 改成 .ts&lt;/td&gt;
&lt;td&gt;项目瘫痪 2 周,业务需求延期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ 正确&lt;/td&gt;
&lt;td&gt;新文件用 .ts,老文件逐步迁移,共存 6 个月&lt;/td&gt;
&lt;td&gt;平滑过渡,无感知&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;c) &quot;技术债券&quot;机制&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;每完成 3 个业务需求,团队获得 1 个&quot;技术债券&quot;
可兑换:
- 1 天重构时间
- 引入 1 个新工具
- 优化 1 个性能瓶颈

目的: 让技术投入可视化、可量化
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;d) 与产品/老板的沟通话术&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;错误示范&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;这代码太烂了,必须重构!不然没法干活!&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;正确示范&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;目前系统存在 X 个痛点影响交付效率:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建慢导致每天浪费 2 小时&lt;/li&gt;
&lt;li&gt;缺少测试导致上线后频繁改 bug&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;建议投入 3 天优化构建,预期收益:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每日节省 2 小时 × 5 人 = 10 人时&lt;/li&gt;
&lt;li&gt;1 个月回本,后续持续受益&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时不影响本迭代需求交付。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;核心&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用&quot;效率提升&quot;而不是&quot;技术追求&quot;说服老板&lt;/li&gt;
&lt;li&gt;用&quot;ROI&quot;(投入产出比)量化收益&lt;/li&gt;
&lt;li&gt;承诺&quot;不影响业务&quot;降低阻力&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;2. 团队规范落地:从&quot;文档&quot;到&quot;自动化&quot;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题根源&lt;/strong&gt;: 人的自觉性不可靠,必须依赖工具强制执行。&lt;/p&gt;
&lt;h4&gt;2.1 代码规范落地(ESLint + Prettier)&lt;/h4&gt;
&lt;h5&gt;第 1 步:统一配置&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 安装依赖
pnpm add -D eslint prettier eslint-config-prettier eslint-plugin-vue @vue/eslint-config-typescript

# .eslintrc.js
module.exports = {
  extends: [
    &apos;plugin:vue/vue3-recommended&apos;,
    &apos;@vue/typescript/recommended&apos;,
    &apos;prettier&apos; // 关闭与 Prettier 冲突的规则
  ],
  rules: {
    &apos;vue/multi-word-component-names&apos;: &apos;off&apos;,
    &apos;@typescript-eslint/no-explicit-any&apos;: &apos;warn&apos;
  }
}

# .prettierrc.json
{
  &quot;semi&quot;: false,
  &quot;singleQuote&quot;: true,
  &quot;printWidth&quot;: 100,
  &quot;trailingComma&quot;: &quot;none&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;第 2 步 Hooks 强制检查(Husky + lint-staged)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 安装
pnpm add -D husky lint-staged

# package.json
{
  &quot;scripts&quot;: {
    &quot;prepare&quot;: &quot;husky install&quot;
  },
  &quot;lint-staged&quot;: {
    &quot;*.{js,ts,vue}&quot;: [
      &quot;eslint --fix&quot;,
      &quot;prettier --write&quot;
    ]
  }
}

# 添加 pre-commit hook
npx husky add .husky/pre-commit &quot;npx lint-staged&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&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;/ul&gt;
&lt;h5&gt;第 3 步 流水线检查&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install
        run: pnpm install
      - name: Lint
        run: pnpm lint
      - name: Type Check
        run: pnpm type-check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;: PR 合并前必须通过检查,防止不规范代码入库。&lt;/p&gt;
&lt;h4&gt;2.2 Git Commit 规范(Commitizen + Commitlint)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 安装
pnpm add -D @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog

# commitlint.config.js
module.exports = {
  extends: [&apos;@commitlint/config-conventional&apos;]
}

# package.json
{
  &quot;scripts&quot;: {
    &quot;commit&quot;: &quot;cz&quot;
  },
  &quot;config&quot;: {
    &quot;commitizen&quot;: {
      &quot;path&quot;: &quot;cz-conventional-changelog&quot;
    }
  }
}

# 添加 commit-msg hook
npx husky add .husky/commit-msg &quot;npx --no -- commitlint --edit $1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用方式&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 不再使用 git commit -m &quot;xxx&quot;
# 而是运行
pnpm commit

# 自动弹出交互式提示
? Select the type of change: (Use arrow keys)
❯ feat:     A new feature
  fix:      A bug fix
  docs:     Documentation only changes
  style:    Changes that do not affect code meaning
  refactor: A code change that neither fixes a bug nor adds a feature
  perf:     A code change that improves performance
  test:     Adding missing tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Commit 信息规范统一&lt;/li&gt;
&lt;li&gt;自动生成 CHANGELOG&lt;/li&gt;
&lt;li&gt;Git 历史清晰可读&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3 落地经验总结&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;做法&lt;/th&gt;
&lt;th&gt;注意事项&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;引入期&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;先在新项目试点,积累经验&lt;/td&gt;
&lt;td&gt;不要一次性全团队推广&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;推广期&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;举办工作坊,手把手教学&lt;/td&gt;
&lt;td&gt;准备《常见问题 FAQ》&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;强制期&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;启用 Git Hooks + CI 卡点&lt;/td&gt;
&lt;td&gt;提前 2 周通知,给缓冲期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;维护期&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;定期优化规则,听取反馈&lt;/td&gt;
&lt;td&gt;规范不是一成不变的&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具化&lt;/strong&gt;:能用工具解决的,不要靠人&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化&lt;/strong&gt;:能自动修正的,不要让人手动改&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渐进式&lt;/strong&gt;:先试点、再推广、最后强制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可配置&lt;/strong&gt;:允许团队根据实际情况调整规则&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;3. Code Review 文化建设&lt;/h3&gt;
&lt;h4&gt;3.1 为什么 Code Review 流于形式?&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;表现&lt;/th&gt;
&lt;th&gt;后果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;无标准&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不知道该审什么&lt;/td&gt;
&lt;td&gt;只看语法错误,错过架构问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;无时间&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;业务太忙,草草了事&lt;/td&gt;
&lt;td&gt;&quot;LGTM&quot;(Looks Good To Me)成口头禅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;无反馈&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;提的意见被忽略&lt;/td&gt;
&lt;td&gt;Reviewer 积极性下降&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;无责任&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;出问题不追责&lt;/td&gt;
&lt;td&gt;审核质量下降&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3.2 建设高质量 Code Review 文化&lt;/h4&gt;
&lt;h5&gt;a) 制定 Code Review Checklist&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Code Review 检查清单

## 功能性 (Functionality)
- [ ] 代码实现是否符合需求?
- [ ] 是否有遗漏的边界条件?
- [ ] 错误处理是否完善?

## 可读性 (Readability)
- [ ] 变量/函数命名是否清晰?
- [ ] 是否有必要的注释?(复杂逻辑、业务规则)
- [ ] 代码结构是否清晰?

## 性能 (Performance)
- [ ] 是否有不必要的重复计算?
- [ ] 是否有内存泄漏风险?
- [ ] 大数据量场景是否考虑分页/虚拟滚动?

## 安全性 (Security)
- [ ] 是否有 XSS/CSRF 风险?
- [ ] 敏感信息是否加密?
- [ ] API 调用是否有权限校验?

## 可维护性 (Maintainability)
- [ ] 是否遵循 DRY 原则(Don&apos;t Repeat Yourself)?
- [ ] 是否有过度设计?
- [ ] 是否需要补充单元测试?

## 架构一致性 (Consistency)
- [ ] 是否符合团队代码风格?
- [ ] 是否复用已有组件/工具?
- [ ] 新增依赖是否合理?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用方式&lt;/strong&gt;: 在 PR 模板中引入 Checklist&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;!-- .github/pull_request_template.md --&gt;
## 变更说明
简述本次改动的目的和内容

## 自测清单
- [ ] 本地测试通过
- [ ] ESLint/类型检查通过
- [ ] 已添加/更新单元测试

## Reviewer 检查点
请 Reviewer 重点关注:
- [ ] XX 模块的性能优化是否合理
- [ ] XX 函数的错误处理是否完善
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;b) 规范 Code Review 流程&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;提交者(Author)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;提供清晰的 PR 描述、自测通过、主动响应评论&lt;/td&gt;
&lt;td&gt;PR 提交后 24 小时内响应评论&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;审查者(Reviewer)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仔细审查代码、提出建设性意见、批准或要求修改&lt;/td&gt;
&lt;td&gt;PR 提交后 2 个工作日内完成审查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Approve 者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;至少 2 人 Approve 才可合并,其中 1 人必须是 Senior&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;流程图&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;提交 PR
  ↓
CI 自动检查(Lint/Test)
  ↓ (通过)
指定 2 名 Reviewer
  ↓
Reviewer 1 审查 → 提意见 → Author 修改
  ↓
Reviewer 2 审查 → Approve
  ↓
Reviewer 1 再次审查 → Approve
  ↓
合并到主分支
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;c) 激励机制&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;正向激励&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每月评选&quot;最佳 Reviewer&quot;(提出高质量建议次数最多)&lt;/li&gt;
&lt;li&gt;Code Review 质量纳入绩效考核&lt;/li&gt;
&lt;li&gt;优秀 Review 案例在团队会议上分享&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;负向约束&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因 Review 不严格导致线上问题,Reviewer 同样承担责任&lt;/li&gt;
&lt;li&gt;&quot;LGTM&quot;式敷衍 Review 会被记录并通报&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;d) 工具支持&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;GitHub/GitLab 配置&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/CODEOWNERS
# 自动指定 Reviewer
*.vue @frontend-team
*.ts @frontend-team
/server/** @backend-team
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Danger JS 自动化检查&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// dangerfile.js
import { danger, warn, fail } from &apos;danger&apos;

// PR 描述不能为空
if (danger.github.pr.body.length &amp;#x3C; 10) {
  fail(&apos;请填写 PR 描述&apos;)
}

// 大 PR 警告(超过 500 行)
const bigPRThreshold = 500
if (danger.github.pr.additions + danger.github.pr.deletions &gt; bigPRThreshold) {
  warn(`本次 PR 改动较大(${danger.github.pr.additions + danger.github.pr.deletions} 行),建议拆分`)
}

// 必须有测试
const hasTests = danger.git.modified_files.some(f =&gt; f.includes(&apos;.spec.&apos;) || f.includes(&apos;.test.&apos;))
if (!hasTests) {
  warn(&apos;请补充单元测试&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.3 Code Review 最佳实践&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;对 Author&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;小步提交&lt;/strong&gt;: 每次 PR 改动不超过 500 行(大功能拆分多个 PR)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清晰描述&lt;/strong&gt;: 说明&quot;为什么改&quot;而不是&quot;改了什么&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自我审查&lt;/strong&gt;: 提交前先自己 Review 一遍&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;积极响应&lt;/strong&gt;: 24 小时内回复 Reviewer 的评论&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对 Reviewer&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;建设性意见&lt;/strong&gt;: 不要只说&quot;这不好&quot;,要给出具体建议&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分问题等级&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;🚨 &lt;strong&gt;MUST FIX&lt;/strong&gt;(必须修改): 功能错误、安全问题&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;SHOULD FIX&lt;/strong&gt;(建议修改): 性能问题、可读性问题&lt;/li&gt;
&lt;li&gt;💡 &lt;strong&gt;NICE TO HAVE&lt;/strong&gt;(可选): 代码风格、优化建议&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正面反馈&lt;/strong&gt;: 看到优秀代码也要点赞表扬&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;及时审查&lt;/strong&gt;: 不要让 PR 挂着超过 2 天&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Code Review 评论示例&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# ❌ 不好的评论
这代码写得不对

# ✅ 好的评论
🚨 **MUST FIX**: 这里缺少空值校验,如果 `user.profile` 为 null 会导致报错

建议修改为:
\`\`\`javascript
const avatar = user.profile?.avatar ?? &apos;/default-avatar.png&apos;
\`\`\`
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. 实战案例分享&lt;/h3&gt;
&lt;h4&gt;案例: 如何在 6 个月内将团队代码质量提升一个档次&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;团队 8 人,Vue2 项目,代码混乱无规范&lt;/li&gt;
&lt;li&gt;线上 bug 频发,每周至少 2 次紧急修复&lt;/li&gt;
&lt;li&gt;团队士气低落,加班严重&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题分析&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无代码规范 → 代码难读难改&lt;/li&gt;
&lt;li&gt;无测试 → 改代码如拆炸弹&lt;/li&gt;
&lt;li&gt;Code Review 形式化 → 问题无法提前发现&lt;/li&gt;
&lt;li&gt;技术债务积压 → 开发效率低下&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;解决方案与实施时间线&lt;/strong&gt;:&lt;/p&gt;
&lt;h5&gt;第 1 个月:建立基础规范&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;动作&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入 ESLint + Prettier + Husky&lt;/li&gt;
&lt;li&gt;举办 2 次工作坊教学&lt;/li&gt;
&lt;li&gt;在新项目试点,老项目不强制&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新代码风格统一&lt;/li&gt;
&lt;li&gt;团队接受度高(没有强制改老代码)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;第 2-3 个月:推广工程化工具&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;动作&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入 Commitizen(规范 Git 提交)&lt;/li&gt;
&lt;li&gt;配置 CI 流水线(自动化 Lint/Test)&lt;/li&gt;
&lt;li&gt;制定《Code Review Checklist》&lt;/li&gt;
&lt;li&gt;试点单元测试(核心模块优先)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git 历史变清晰,方便回溯问题&lt;/li&gt;
&lt;li&gt;CI 拦截了 30% 的低级错误&lt;/li&gt;
&lt;li&gt;Code Review 质量提升&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;第 4-5 个月:技术债务专项治理&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;动作&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;梳理技术债务清单,按 P0-P3 分级&lt;/li&gt;
&lt;li&gt;每个迭代分配 20% 时间修复债务&lt;/li&gt;
&lt;li&gt;优化构建速度(Webpack → Vite)&lt;/li&gt;
&lt;li&gt;重构核心公共组件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构建时间从 8 分钟降到 2 分钟&lt;/li&gt;
&lt;li&gt;代码重复率下降 40%&lt;/li&gt;
&lt;li&gt;团队开发效率提升&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;第 6 个月:文化与机制沉淀&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;动作&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;制定《前端开发规范手册》&lt;/li&gt;
&lt;li&gt;建立&quot;技术债券&quot;机制&lt;/li&gt;
&lt;li&gt;每月评选&quot;最佳 Reviewer&quot;&lt;/li&gt;
&lt;li&gt;举办技术分享会&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;团队形成自驱文化&lt;/li&gt;
&lt;li&gt;Code Review 成为日常习惯&lt;/li&gt;
&lt;li&gt;线上 bug 率下降 70%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数据对比&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;改进前&lt;/th&gt;
&lt;th&gt;改进后&lt;/th&gt;
&lt;th&gt;提升&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;线上 bug 率&lt;/td&gt;
&lt;td&gt;8 个/月&lt;/td&gt;
&lt;td&gt;2 个/月&lt;/td&gt;
&lt;td&gt;↓ 75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构建时间&lt;/td&gt;
&lt;td&gt;8 分钟&lt;/td&gt;
&lt;td&gt;2 分钟&lt;/td&gt;
&lt;td&gt;↓ 75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码重复率&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;↓ 40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单元测试覆盖率&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;td&gt;↑ 45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Review 参与度&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;↑ 65%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键成功因素&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自上而下推动&lt;/strong&gt;: 得到 CTO 支持,分配专门时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渐进式实施&lt;/strong&gt;: 不搞大爆炸式改革,小步快跑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具自动化&lt;/strong&gt;: 减少人为因素,降低执行成本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正向激励&lt;/strong&gt;: 表扬优秀实践,而不是惩罚犯错&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文化塑造&lt;/strong&gt;: 让团队意识到&quot;质量&quot;和&quot;速度&quot;不矛盾&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践总结&lt;/h2&gt;
&lt;h3&gt;技术 Leader 的核心能力&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;重要性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;平衡能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;业务需求 vs 技术追求&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;推动能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;让规范落地而不是挂在墙上&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;沟通能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;向上争取资源,向下传递价值&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;自动化思维&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用工具解决问题,而不是靠人&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;同理心&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;理解团队成员的困难&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;技术债务管理原则&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;可视化&lt;/strong&gt;: 技术债务清单公开透明&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级&lt;/strong&gt;: 用 P0-P3 分级,先解决高风险问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROI 导向&lt;/strong&gt;: 优先修复&quot;收益高、成本低&quot;的债务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续投入&lt;/strong&gt;: 每个迭代预留 20% 时间,而不是&quot;攒着&quot;一次性还&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防患未然&lt;/strong&gt;: 新代码高标准,避免制造新债务&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;团队规范落地三板斧&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具化&lt;/strong&gt;: ESLint/Prettier/Husky 自动化检查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程化&lt;/strong&gt;: Git Hooks + CI 流水线卡点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文化化&lt;/strong&gt;: Code Review 成为团队习惯&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;避坑指南&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;坑&lt;/th&gt;
&lt;th&gt;后果&lt;/th&gt;
&lt;th&gt;正确做法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;技术完美主义&lt;/td&gt;
&lt;td&gt;业务需求延期,团队失去信任&lt;/td&gt;
&lt;td&gt;技术投入控制在 20% 以内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一刀切推广&lt;/td&gt;
&lt;td&gt;团队抵触,执行不下去&lt;/td&gt;
&lt;td&gt;先试点,再推广,最后强制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只写文档不落地&lt;/td&gt;
&lt;td&gt;规范成为&quot;空中楼阁&quot;&lt;/td&gt;
&lt;td&gt;用 Git Hooks 强制执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Review 走形式&lt;/td&gt;
&lt;td&gt;质量不升反降,浪费时间&lt;/td&gt;
&lt;td&gt;制定 Checklist,建立激励机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;忽视团队感受&lt;/td&gt;
&lt;td&gt;优秀成员流失&lt;/td&gt;
&lt;td&gt;多倾听反馈,调整策略&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://google.github.io/eng-practices/review/&quot;&gt;Google 工程实践文档 - Code Review&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://insights.thoughtworks.cn/managing-technical-debt/&quot;&gt;技术债务管理 - ThoughtWorks 洞见&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2020/12/code-review.html&quot;&gt;如何做好 Code Review - 阮一峰&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/airbnb/javascript&quot;&gt;ESLint 最佳实践 - Airbnb JavaScript Style Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://time.geekbang.org/column/article/8748&quot;&gt;团队协作与技术管理 - 左耳朵耗子&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://time.geekbang.org/column/article/106362&quot;&gt;高效能团队的 5 个习惯 - 极客时间&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[9.微前端架构设计 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-09-micro-frontend</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-09-micro-frontend</guid><pubDate>Wed, 21 Jan 2026 07:23:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：架构能力、技术选型、问题解决&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
您的公司有多个前端团队,分别负责不同的业务模块(订单、库存、财务、用户中心),技术栈混杂(Vue2、Vue3、React)。现在需要将这些模块整合到一个统一的后台管理系统中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需求&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;各模块独立开发、独立部署&lt;/li&gt;
&lt;li&gt;支持 Vue2、Vue3 混合运行&lt;/li&gt;
&lt;li&gt;共享公共依赖(如 Vue、Element UI)以减小包体积&lt;/li&gt;
&lt;li&gt;主应用负责路由、权限、布局&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术方案&lt;/strong&gt;：微前端的实现方案有哪些？(如 qiankun、Micro-App、Module Federation、iframe)您会选择哪个？为什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心挑战&lt;/strong&gt;：微前端架构的核心技术难点是什么？(如 JS 沙箱隔离、样式隔离、应用间通信)如何解决？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战考量&lt;/strong&gt;：Vue2 和 Vue3 共存时有什么注意事项？如何避免版本冲突？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. 微前端技术方案对比与选型&lt;/h3&gt;
&lt;h4&gt;主流方案对比&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;技术原理&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;qiankun&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基于 single-spa + HTML Entry + JS 沙箱&lt;/td&gt;
&lt;td&gt;成熟稳定、开箱即用、社区活跃、完善的沙箱和样式隔离&lt;/td&gt;
&lt;td&gt;性能开销较大、对子应用侵入性中等&lt;/td&gt;
&lt;td&gt;企业级中后台系统、多团队协作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Micro-App&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基于 WebComponent + HTML Entry&lt;/td&gt;
&lt;td&gt;接入成本低、类似 iframe 的使用体验、性能较好&lt;/td&gt;
&lt;td&gt;社区相对小、浏览器兼容性依赖 polyfill&lt;/td&gt;
&lt;td&gt;快速接入、中小型项目&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Module Federation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Webpack 5 原生支持、模块联邦&lt;/td&gt;
&lt;td&gt;性能最优、可共享依赖、Webpack 生态&lt;/td&gt;
&lt;td&gt;需要 Webpack 5、配置复杂、依赖版本管理难&lt;/td&gt;
&lt;td&gt;同技术栈、深度集成场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;iframe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览器原生隔离&lt;/td&gt;
&lt;td&gt;完全隔离、接入成本极低&lt;/td&gt;
&lt;td&gt;性能差、通信复杂、用户体验不佳(白屏、路由不同步)&lt;/td&gt;
&lt;td&gt;快速集成第三方系统、安全要求极高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;推荐选型：&lt;strong&gt;qiankun&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;理由&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;成熟度高&lt;/strong&gt;：阿里开源,经过蚂蚁集团大规模实践验证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多框架支持&lt;/strong&gt;：原生支持 Vue2/Vue3/React 等任意框架混用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;沙箱隔离&lt;/strong&gt;：提供 Proxy 沙箱和快照沙箱,有效隔离 JS 运行环境&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式隔离&lt;/strong&gt;：支持 Shadow DOM 和动态样式表隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开箱即用&lt;/strong&gt;：提供完整的生命周期钩子和应用间通信方案&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;社区生态&lt;/strong&gt;：文档完善,issue 响应快,插件丰富&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对于 Vue2/Vue3 混用场景&lt;/strong&gt;,qiankun 是目前最佳选择。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 核心技术挑战与解决方案&lt;/h3&gt;
&lt;h4&gt;2.1 JS 沙箱隔离&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;挑战&lt;/strong&gt;：多个子应用运行在同一个 window 上下文,可能互相污染全局变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;qiankun 的解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;h5&gt;a) Proxy 沙箱 (推荐,适用于现代浏览器)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// qiankun 内部实现原理简化版
class ProxySandbox {
  constructor() {
    const fakeWindow = {}
    const proxy = new Proxy(fakeWindow, {
      get(target, prop) {
        // 优先从沙箱中获取
        if (prop in target) {
          return target[prop]
        }
        // 否则从真实 window 获取
        const value = window[prop]
        // 如果是函数,绑定到真实 window
        if (typeof value === &apos;function&apos; &amp;#x26;&amp;#x26; !value.prototype) {
          return value.bind(window)
        }
        return value
      },
      set(target, prop, value) {
        // 所有修改都记录在沙箱内
        target[prop] = value
        return true
      },
      has(target, prop) {
        return prop in target || prop in window
      }
    })
    
    this.proxy = proxy
  }
  
  // 激活沙箱
  active() {
    this.running = true
  }
  
  // 失活沙箱
  inactive() {
    this.running = false
  }
}

// 使用示例
const sandbox = new ProxySandbox()
sandbox.active()

// 子应用在沙箱中运行
;(function(window) {
  window.myApp = { name: &apos;App1&apos; } // 只作用于沙箱
})(sandbox.proxy)

sandbox.inactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个子应用独立 Proxy 沙箱&lt;/li&gt;
&lt;li&gt;不影响真实 window&lt;/li&gt;
&lt;li&gt;支持多应用同时运行&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;b) 快照沙箱 (兼容 IE,但不支持多应用并行)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class SnapshotSandbox {
  constructor() {
    this.modifyPropsMap = {} // 记录运行时修改的属性
  }
  
  active() {
    // 保存当前 window 快照
    this.windowSnapshot = {}
    for (const prop in window) {
      this.windowSnapshot[prop] = window[prop]
    }
    
    // 恢复上次记录的修改
    Object.keys(this.modifyPropsMap).forEach(prop =&gt; {
      window[prop] = this.modifyPropsMap[prop]
    })
  }
  
  inactive() {
    // 记录本次运行的修改
    for (const prop in window) {
      if (window[prop] !== this.windowSnapshot[prop]) {
        this.modifyPropsMap[prop] = window[prop]
        // 还原 window
        window[prop] = this.windowSnapshot[prop]
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2 样式隔离&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;挑战&lt;/strong&gt;：不同子应用的样式可能相互覆盖(如都有 &lt;code&gt;.btn&lt;/code&gt; 类)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;h5&gt;a) Shadow DOM (严格隔离,但可能影响弹窗组件)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// qiankun 配置
import { start } from &apos;qiankun&apos;

start({
  sandbox: {
    strictStyleIsolation: true // 启用 Shadow DOM
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;：将子应用渲染在 Shadow DOM 内,样式天然隔离。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Element UI / Ant Design 的弹窗组件默认挂载到 &lt;code&gt;body&lt;/code&gt;,可能渲染到 Shadow DOM 外&lt;/li&gt;
&lt;li&gt;需要配置弹窗挂载点：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 子应用配置 Element UI
import { Message } from &apos;element-ui&apos;

export async function mount(props) {
  const { container } = props
  // 将弹窗挂载到子应用容器内
  Message.config({
    getContainer: () =&gt; container.querySelector(&apos;#app&apos;)
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;b) 动态样式表作用域 (推荐,兼容性好)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;start({
  sandbox: {
    experimentalStyleIsolation: true // 运行时添加样式前缀
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;：qiankun 运行时动态给子应用的样式选择器添加 &lt;code&gt;data-qiankun&lt;/code&gt; 属性前缀。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 原始样式 */
.btn { color: red; }

/* qiankun 运行时转换为 */
div[data-qiankun=&quot;app1&quot;] .btn { color: red; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;c) CSS Modules / CSS-in-JS (源头隔离)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;!-- Vue 组件使用 scoped 样式 --&gt;
&amp;#x3C;style scoped&gt;
.btn {
  color: blue;
}
&amp;#x3C;/style&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后自动添加唯一哈希：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.btn[data-v-f3f3eg9] {
  color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.3 应用间通信&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;挑战&lt;/strong&gt;：主应用和子应用、子应用之间需要共享数据(如用户信息、token)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;h5&gt;a) qiankun 官方通信方案 - initGlobalState&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ===== 主应用 =====
import { initGlobalState } from &apos;qiankun&apos;

// 初始化全局状态
const actions = initGlobalState({
  user: { name: &apos;Admin&apos;, token: &apos;xxx&apos; },
  theme: &apos;dark&apos;
})

// 监听变化
actions.onGlobalStateChange((state, prev) =&gt; {
  console.log(&apos;主应用监听到状态变化:&apos;, state)
})

// 修改状态
actions.setGlobalState({ theme: &apos;light&apos; })

// ===== 子应用 =====
export async function mount(props) {
  // 接收主应用传递的通信方法
  props.onGlobalStateChange((state, prev) =&gt; {
    console.log(&apos;子应用监听到状态变化:&apos;, state)
    // 同步到 Vuex/Pinia
    store.commit(&apos;setUser&apos;, state.user)
  })
  
  // 子应用也可以修改全局状态
  props.setGlobalState({ theme: &apos;blue&apos; })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;b) 自定义事件总线 (更灵活)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ===== 主应用 =====
class EventBus {
  constructor() {
    this.events = {}
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
  }
  
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(cb =&gt; cb(data))
    }
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb =&gt; cb !== callback)
    }
  }
}

const eventBus = new EventBus()

registerMicroApps([
  {
    name: &apos;app1&apos;,
    entry: &apos;//localhost:8081&apos;,
    container: &apos;#container&apos;,
    props: { eventBus } // 传递给子应用
  }
])

// ===== 子应用 =====
export async function mount(props) {
  const { eventBus } = props
  
  // 监听事件
  eventBus.on(&apos;user:login&apos;, (user) =&gt; {
    console.log(&apos;用户登录:&apos;, user)
  })
  
  // 触发事件
  eventBus.emit(&apos;order:create&apos;, { orderId: 123 })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;c) 共享 Pinia Store (Vue 专属,推荐)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ===== 主应用 =====
import { createPinia } from &apos;pinia&apos;

const pinia = createPinia()
app.use(pinia)

registerMicroApps([
  {
    name: &apos;app1&apos;,
    entry: &apos;//localhost:8081&apos;,
    container: &apos;#container&apos;,
    props: { pinia } // 传递 Pinia 实例
  }
])

// ===== 子应用 (Vue3) =====
import { defineStore } from &apos;pinia&apos;

export async function mount(props) {
  const { pinia } = props
  
  // 使用主应用的 Pinia 实例
  const useUserStore = defineStore(&apos;user&apos;, {
    state: () =&gt; ({ name: &apos;&apos;, token: &apos;&apos; })
  })
  
  app.use(pinia) // 注入主应用的 Pinia
  
  const userStore = useUserStore(pinia)
  console.log(userStore.name) // 可访问主应用的状态
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. Vue2 与 Vue3 共存方案&lt;/h3&gt;
&lt;h4&gt;3.1 核心挑战&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;全局 API 冲突&lt;/strong&gt;：Vue2 和 Vue3 都会在 window 上挂载 &lt;code&gt;window.Vue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖版本冲突&lt;/strong&gt;：Element UI (Vue2) vs Element Plus (Vue3)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由冲突&lt;/strong&gt;：Vue Router 3 vs Vue Router 4&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.2 解决方案&lt;/h4&gt;
&lt;h5&gt;a) 通过 qiankun 沙箱隔离&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 主应用使用 Vue3
import { createApp } from &apos;vue&apos;
import App from &apos;./App.vue&apos;

createApp(App).mount(&apos;#main-app&apos;)

// 注册 Vue2 子应用
registerMicroApps([
  {
    name: &apos;vue2-app&apos;,
    entry: &apos;//localhost:8081&apos;,
    container: &apos;#subapp-container&apos;,
    activeRule: &apos;/vue2&apos;
  }
])

// 注册 Vue3 子应用
registerMicroApps([
  {
    name: &apos;vue3-app&apos;,
    entry: &apos;//localhost:8082&apos;,
    container: &apos;#subapp-container&apos;,
    activeRule: &apos;/vue3&apos;
  }
])

start({
  sandbox: {
    strictStyleIsolation: false,
    experimentalStyleIsolation: true
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;qiankun 的沙箱会自动隔离&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue2 子应用的 &lt;code&gt;window.Vue&lt;/code&gt; 不会影响 Vue3&lt;/li&gt;
&lt;li&gt;两个子应用的路由实例互不干扰&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;b) 子应用配置 (Vue2 子应用示例)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ===== Vue2 子应用入口文件 =====
import Vue from &apos;vue&apos;
import VueRouter from &apos;vue-router&apos;
import ElementUI from &apos;element-ui&apos;
import &apos;element-ui/lib/theme-chalk/index.css&apos;
import App from &apos;./App.vue&apos;

Vue.use(VueRouter)
Vue.use(ElementUI)

let instance = null
let router = null

// 导出 qiankun 生命周期
export async function bootstrap() {
  console.log(&apos;Vue2 子应用 bootstrap&apos;)
}

export async function mount(props) {
  console.log(&apos;Vue2 子应用 mount&apos;, props)
  
  // 创建路由实例
  router = new VueRouter({
    mode: &apos;history&apos;,
    base: window.__POWERED_BY_QIANKUN__ ? &apos;/vue2&apos; : &apos;/&apos;,
    routes: [
      { path: &apos;/order&apos;, component: () =&gt; import(&apos;./views/Order.vue&apos;) }
    ]
  })
  
  // 创建 Vue 实例
  instance = new Vue({
    router,
    render: h =&gt; h(App)
  }).$mount(props.container ? props.container.querySelector(&apos;#app&apos;) : &apos;#app&apos;)
}

export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = &apos;&apos;
  instance = null
  router = null
}

// 独立运行时直接挂载
if (!window.__POWERED_BY_QIANKUN__) {
  mount({})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;c) 依赖共享与隔离&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;推荐策略&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;公共依赖不共享&lt;/strong&gt;：Vue2 和 Vue3 各自打包,避免版本冲突&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具库可共享&lt;/strong&gt;：如 &lt;code&gt;axios&lt;/code&gt;、&lt;code&gt;lodash&lt;/code&gt; 可通过 externals 配置共享&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2 子应用 vue.config.js
module.exports = {
  configureWebpack: {
    externals: {
      // axios 共享,从主应用加载
      axios: &apos;axios&apos;
    },
    output: {
      library: &apos;vue2App&apos;,
      libraryTarget: &apos;umd&apos;,
      jsonpFunction: `webpackJsonp_vue2App` // 避免 chunk 冲突
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 主应用 index.html 提供共享依赖
&amp;#x3C;script src=&quot;https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js&quot;&gt;&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.3 最佳实践清单&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;实践&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;重要性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;避免全局污染&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;子应用不要在 window 上挂载变量&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;路由 base 配置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;子应用路由 base 需根据运行环境动态设置&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;静态资源 publicPath&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;配置 &lt;code&gt;__webpack_public_path__&lt;/code&gt; 为动态值&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;弹窗组件挂载点&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;确保弹窗渲染在子应用容器内&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;生命周期规范&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;必须导出 &lt;code&gt;bootstrap/mount/unmount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;独立运行能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;子应用需支持脱离 qiankun 独立运行&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 动态 publicPath 配置
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. 完整方案架构示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│               主应用 (Vue3)                      │
│  - 路由控制、权限管理、全局布局                    │
│  - qiankun 加载器                                │
│  - 公共组件库、工具函数                            │
└──────────────┬──────────────────────────────────┘
               │
      ┌────────┼────────┬────────┐
      │        │        │        │
┌─────▼───┐ ┌─▼─────┐ ┌▼──────┐ ┌▼──────────┐
│ Vue2    │ │ Vue3  │ │ React │ │ Angular   │
│ 订单模块 │ │ 库存  │ │ 财务  │ │ 用户中心  │
│         │ │ 模块  │ │ 模块  │ │           │
│ Element │ │ Elem+ │ │ AntD  │ │ Material  │
│ UI      │ │       │ │       │ │           │
└─────────┘ └───────┘ └───────┘ └───────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主应用配置&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// main.ts
import { registerMicroApps, start, initGlobalState } from &apos;qiankun&apos;
import { createPinia } from &apos;pinia&apos;

const pinia = createPinia()

// 初始化全局状态
const actions = initGlobalState({
  user: null,
  token: localStorage.getItem(&apos;token&apos;)
})

// 注册子应用
registerMicroApps([
  {
    name: &apos;order-app&apos;,
    entry: &apos;//localhost:8081&apos;, // Vue2
    container: &apos;#subapp-viewport&apos;,
    activeRule: &apos;/order&apos;,
    props: { pinia, actions }
  },
  {
    name: &apos;stock-app&apos;,
    entry: &apos;//localhost:8082&apos;, // Vue3
    container: &apos;#subapp-viewport&apos;,
    activeRule: &apos;/stock&apos;,
    props: { pinia, actions }
  },
  {
    name: &apos;finance-app&apos;,
    entry: &apos;//localhost:8083&apos;, // React
    container: &apos;#subapp-viewport&apos;,
    activeRule: &apos;/finance&apos;,
    props: { actions }
  }
], {
  beforeLoad: app =&gt; console.log(&apos;加载前&apos;, app.name),
  beforeMount: app =&gt; console.log(&apos;挂载前&apos;, app.name),
  afterMount: app =&gt; console.log(&apos;挂载后&apos;, app.name),
  beforeUnmount: app =&gt; console.log(&apos;卸载前&apos;, app.name),
  afterUnmount: app =&gt; console.log(&apos;卸载后&apos;, app.name)
})

// 启动 qiankun
start({
  sandbox: {
    strictStyleIsolation: false,
    experimentalStyleIsolation: true
  },
  prefetch: &apos;all&apos;, // 预加载所有子应用
  fetch: (url, ...args) =&gt; {
    // 自定义 fetch,可添加 token
    return window.fetch(url, {
      ...args,
      headers: {
        ...args[0]?.headers,
        &apos;Authorization&apos;: `Bearer ${localStorage.getItem(&apos;token&apos;)}`
      }
    })
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践总结&lt;/h2&gt;
&lt;h3&gt;技术选型建议&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;推荐方案&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;多团队、多技术栈&lt;/td&gt;
&lt;td&gt;qiankun&lt;/td&gt;
&lt;td&gt;成熟、支持任意框架混用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;同技术栈、深度集成&lt;/td&gt;
&lt;td&gt;Module Federation&lt;/td&gt;
&lt;td&gt;性能最优、原生支持依赖共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;快速接入、中小项目&lt;/td&gt;
&lt;td&gt;Micro-App&lt;/td&gt;
&lt;td&gt;使用简单、接入成本低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;集成第三方系统&lt;/td&gt;
&lt;td&gt;iframe&lt;/td&gt;
&lt;td&gt;完全隔离、安全性高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;实施要点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;渐进式改造&lt;/strong&gt;：先从边缘模块试点,积累经验后再推广&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档先行&lt;/strong&gt;：制定《微前端接入规范》,明确生命周期、通信方式、样式规范&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基建工具&lt;/strong&gt;：提供子应用脚手架、公共组件库、部署脚本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控告警&lt;/strong&gt;：接入性能监控(子应用加载时间、错误上报)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度发布&lt;/strong&gt;：支持子应用独立灰度,降低上线风险&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;常见坑点&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;路由切换后子应用白屏&lt;/td&gt;
&lt;td&gt;未正确实现 unmount&lt;/td&gt;
&lt;td&gt;确保 unmount 中销毁实例、清空 DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;弹窗显示在错误位置&lt;/td&gt;
&lt;td&gt;弹窗挂载到 body 外&lt;/td&gt;
&lt;td&gt;配置弹窗 getContainer 指向子应用容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;样式丢失&lt;/td&gt;
&lt;td&gt;publicPath 配置错误&lt;/td&gt;
&lt;td&gt;动态设置 &lt;code&gt;__webpack_public_path__&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子应用间数据不同步&lt;/td&gt;
&lt;td&gt;未使用全局状态管理&lt;/td&gt;
&lt;td&gt;使用 qiankun 的 initGlobalState 或共享 Pinia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue2/Vue3 冲突&lt;/td&gt;
&lt;td&gt;沙箱未生效&lt;/td&gt;
&lt;td&gt;检查 qiankun 版本、确保启用沙箱&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://qiankun.umijs.org/zh/guide&quot;&gt;qiankun 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.meituan.com/2020/02/27/meituan-waimai-micro-frontends-practice.html&quot;&gt;微前端架构选型指南 - 美团技术团队&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webpack.js.org/concepts/module-federation/&quot;&gt;Module Federation 实战 - Webpack 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://micro-zoe.github.io/micro-app/&quot;&gt;Micro-App 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7094989208692875278&quot;&gt;微前端的核心价值 - 字节跳动技术博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.yuque.com/kuitos/gky7yw/gesexv&quot;&gt;qiankun 2.0 沙箱实现原理&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[8.Node.js 性能调优 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-08-nodejs-performance</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-08-nodejs-performance</guid><pubDate>Wed, 21 Jan 2026 07:22:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;Node.js 性能调优：如何分析和优化 Node.js 应用的性能？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. 性能分析工具&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CPU 分析&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用 --cpu-prof 生成 CPU profile
node --cpu-prof app.js

# 使用 Clinic.js
npm install -g clinic
clinic doctor -- node app.js  # 诊断性能问题
clinic flame -- node app.js   # 生成火焰图
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;内存分析&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 生成 Heap Snapshot
node --inspect app.js
# Chrome DevTools → Memory → Take snapshot

# 使用 heapdump
const heapdump = require(&apos;heapdump&apos;)
heapdump.writeSnapshot(&apos;./heap-&apos; + Date.now() + &apos;.heapsnapshot&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;2. 常见性能问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题1：同步代码阻塞&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 同步读文件阻塞 Event Loop
const data = fs.readFileSync(&apos;/large-file.txt&apos;)

// ✅ 异步读取
const data = await fs.promises.readFile(&apos;/large-file.txt&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题2：未使用连接池&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 每次都创建新连接
const mysql = require(&apos;mysql&apos;)
const conn = mysql.createConnection(config)

// ✅ 使用连接池
const pool = mysql.createPool(config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题3：内存泄漏&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 全局变量累积
const cache = {}
app.get(&apos;/data/:id&apos;, (req, res) =&gt; {
  cache[req.params.id] = fetchData(req.params.id)
  res.json(cache[req.params.id])
})

// ✅ 使用 LRU 缓存
const LRU = require(&apos;lru-cache&apos;)
const cache = new LRU({ max: 500 })
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. 优化策略&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;集群模式&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const cluster = require(&apos;cluster&apos;)
const os = require(&apos;os&apos;)

if (cluster.isMaster) {
  const numCPUs = os.cpus().length
  for (let i = 0; i &amp;#x3C; numCPUs; i++) {
    cluster.fork()
  }
} else {
  // Worker 进程运行服务器
  require(&apos;./app.js&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;缓存优化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Redis 缓存热点数据&lt;/li&gt;
&lt;li&gt;使用 CDN 缓存静态资源&lt;/li&gt;
&lt;li&gt;使用内存缓存（如 LRU）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数据库优化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加索引&lt;/li&gt;
&lt;li&gt;使用连接池&lt;/li&gt;
&lt;li&gt;避免 N+1 查询&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;使用 PM2 管理进程&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启用 Cluster 模式&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控关键指标&lt;/strong&gt;（CPU、内存、响应时间）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用缓存策略&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化数据库查询&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免同步代码&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/docs/guides/simple-profiling/&quot;&gt;Node.js Performance Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://clinicjs.org/&quot;&gt;Clinic.js 性能分析&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[7.Node.js Event Loop - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-07-event-loop</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-07-event-loop</guid><pubDate>Wed, 21 Jan 2026 07:21:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：Node.js 核心原理、事件循环、宏任务微任务&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：解释 Node.js 的 Event Loop 机制，并说明以下代码的执行顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(1)

setTimeout(() =&gt; {
  console.log(2)
  Promise.resolve().then(() =&gt; console.log(3))
}, 0)

Promise.resolve().then(() =&gt; console.log(4))

setTimeout(() =&gt; {
  console.log(5)
}, 0)

console.log(6)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;正确输出顺序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1
6
4
2
3
5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行过程详解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;执行栈：
1. console.log(1)  → 输出 1
2. setTimeout(..., 0) → 注册宏任务（timers 队列）
3. Promise.resolve().then(...) → 注册微任务
4. setTimeout(..., 0) → 注册宏任务（timers 队列）
5. console.log(6)  → 输出 6

同步代码执行完毕，开始 Event Loop：

第1轮循环：
  - 清空微任务队列：console.log(4) → 输出 4
  - 执行宏任务（timers）：第1个 setTimeout
    - console.log(2) → 输出 2
    - Promise.resolve().then(...) → 注册微任务
  - 清空微任务队列：console.log(3) → 输出 3

第2轮循环：
  - 执行宏任务（timers）：第2个 setTimeout
    - console.log(5) → 输出 5
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Event Loop 完整机制&lt;/h2&gt;
&lt;h3&gt;1. Node.js Event Loop 的 6 个阶段&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;   ┌───────────────────────────┐
┌─&gt;│           timers          │  执行 setTimeout/setInterval 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  执行延迟到下一轮的 I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  仅内部使用
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │&amp;#x3C;─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │  执行 setImmediate 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  执行 socket.on(&apos;close&apos;, ...) 回调
   └───────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;各阶段说明&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;timers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;执行 &lt;code&gt;setTimeout&lt;/code&gt; 和 &lt;code&gt;setInterval&lt;/code&gt; 回调&lt;/td&gt;
&lt;td&gt;setTimeout(fn, 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pending callbacks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;执行系统操作的回调（如 TCP 错误）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;idle, prepare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;内部使用&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;poll&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;获取新的 I/O 事件，执行 I/O 回调&lt;/td&gt;
&lt;td&gt;fs.readFile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;执行 &lt;code&gt;setImmediate&lt;/code&gt; 回调&lt;/td&gt;
&lt;td&gt;setImmediate(fn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;close callbacks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;执行 close 事件回调&lt;/td&gt;
&lt;td&gt;socket.on(&apos;close&apos;, fn)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;2. 微任务（Microtask）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;微任务在每个阶段结束后执行&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 微任务类型
process.nextTick(callback)  // 优先级最高
Promise.resolve().then(callback)
queueMicrotask(callback)
async/await
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行顺序&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;每个宏任务阶段结束后：
1. 清空 process.nextTick 队列
2. 清空 Promise 微任务队列
3. 进入下一个宏任务阶段
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. setTimeout vs setImmediate&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 情况1：在主模块中（非 I/O 回调）
setTimeout(() =&gt; console.log(&apos;timeout&apos;), 0)
setImmediate(() =&gt; console.log(&apos;immediate&apos;))

// 输出顺序不确定！
// 原因：setTimeout(fn, 0) 实际是 setTimeout(fn, 1)
//       如果事件循环启动用时 &amp;#x3C; 1ms，先执行 setImmediate
//       如果事件循环启动用时 &gt;= 1ms，先执行 setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 情况2：在 I/O 回调中
const fs = require(&apos;fs&apos;)

fs.readFile(__filename, () =&gt; {
  setTimeout(() =&gt; console.log(&apos;timeout&apos;), 0)
  setImmediate(() =&gt; console.log(&apos;immediate&apos;))
})

// 输出顺序固定：
// immediate
// timeout

// 原因：I/O 回调在 poll 阶段执行
//       setImmediate 在下一个 check 阶段立即执行
//       setTimeout 需要等到下一轮 Event Loop 的 timers 阶段
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. process.nextTick vs Promise&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&apos;start&apos;)

process.nextTick(() =&gt; {
  console.log(&apos;nextTick 1&apos;)
  process.nextTick(() =&gt; {
    console.log(&apos;nextTick 2&apos;)
  })
})

Promise.resolve().then(() =&gt; {
  console.log(&apos;promise 1&apos;)
  Promise.resolve().then(() =&gt; {
    console.log(&apos;promise 2&apos;)
  })
})

console.log(&apos;end&apos;)

// 输出顺序：
// start
// end
// nextTick 1
// nextTick 2  ← nextTick 优先级更高，递归执行完
// promise 1
// promise 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;优先级&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;process.nextTick &gt; Promise.then &gt; setTimeout &gt; setImmediate
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;5. 复杂示例分析&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&apos;1&apos;)

setTimeout(() =&gt; {
  console.log(&apos;2&apos;)
  process.nextTick(() =&gt; console.log(&apos;3&apos;))
}, 0)

setTimeout(() =&gt; {
  console.log(&apos;4&apos;)
  process.nextTick(() =&gt; console.log(&apos;5&apos;))
}, 0)

process.nextTick(() =&gt; console.log(&apos;6&apos;))

console.log(&apos;7&apos;)

// 输出顺序分析：

// 同步代码：
1, 7

// 第1轮 Event Loop：
// - 微任务（process.nextTick）：
6

// - 宏任务（timers 第1个 setTimeout）：
2
// - 微任务（process.nextTick）：
3

// - 宏任务（timers 第2个 setTimeout）：
4
// - 微任务（process.nextTick）：
5

// 最终输出：
// 1, 7, 6, 2, 3, 4, 5
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;6. async/await 与 Event Loop&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function async1() {
  console.log(&apos;async1 start&apos;)
  await async2()
  console.log(&apos;async1 end&apos;)  // 相当于 Promise.then
}

async function async2() {
  console.log(&apos;async2&apos;)
}

console.log(&apos;script start&apos;)

setTimeout(() =&gt; {
  console.log(&apos;setTimeout&apos;)
}, 0)

async1()

new Promise(resolve =&gt; {
  console.log(&apos;promise1&apos;)
  resolve()
}).then(() =&gt; {
  console.log(&apos;promise2&apos;)
})

console.log(&apos;script end&apos;)

// 输出顺序：
// script start
// async1 start
// async2
// promise1
// script end
// async1 end  ← await 后面的代码进入微任务队列
// promise2
// setTimeout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;await 的本质&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 这段代码：
async function fn() {
  await somePromise()
  console.log(&apos;after await&apos;)
}

// 等价于：
function fn() {
  return somePromise().then(() =&gt; {
    console.log(&apos;after await&apos;)
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;浏览器 vs Node.js Event Loop&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;浏览器&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;宏任务&lt;/td&gt;
&lt;td&gt;setTimeout、setInterval、I/O、UI 渲染&lt;/td&gt;
&lt;td&gt;setTimeout、setImmediate、I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;微任务&lt;/td&gt;
&lt;td&gt;Promise、MutationObserver&lt;/td&gt;
&lt;td&gt;Promise、process.nextTick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行顺序&lt;/td&gt;
&lt;td&gt;每次执行 1 个宏任务 → 清空所有微任务&lt;/td&gt;
&lt;td&gt;每个阶段执行所有宏任务 → 清空所有微任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;特殊 API&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;&lt;code&gt;process.nextTick&lt;/code&gt;（优先级最高）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;常见面试陷阱&lt;/h2&gt;
&lt;h3&gt;陷阱1：setTimeout(fn, 0) 并非真正的 0ms&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setTimeout(() =&gt; console.log(&apos;timeout&apos;), 0)

// 实际延迟：
// - 浏览器：至少 4ms（HTML5 标准）
// - Node.js：至少 1ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;陷阱2：process.nextTick 可能导致饿死&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 危险代码
function recursiveNextTick() {
  process.nextTick(recursiveNextTick)
}
recursiveNextTick()

// 问题：nextTick 优先级太高，会一直递归，阻塞 Event Loop
// 其他 I/O 操作永远无法执行
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;陷阱3：微任务插队&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;setTimeout(() =&gt; {
  console.log(&apos;timeout 1&apos;)
  Promise.resolve().then(() =&gt; console.log(&apos;promise&apos;))
  setTimeout(() =&gt; console.log(&apos;timeout 2&apos;), 0)
}, 0)

// 输出顺序：
// timeout 1
// promise  ← 微任务插队，比 timeout 2 先执行
// timeout 2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;理解宏任务和微任务的区别&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免滥用 process.nextTick&lt;/strong&gt;（可能阻塞 Event Loop）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 回调中优先使用 setImmediate&lt;/strong&gt;（而非 setTimeout）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长任务分片&lt;/strong&gt;：避免阻塞 Event Loop&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 阻塞 Event Loop
for (let i = 0; i &amp;#x3C; 1e9; i++) {
  // 耗时操作
}

// ✅ 分片处理
function processData(data, batchSize = 1000) {
  let index = 0
  
  function processBatch() {
    const end = Math.min(index + batchSize, data.length)
    
    for (let i = index; i &amp;#x3C; end; i++) {
      // 处理数据
    }
    
    index = end
    
    if (index &amp;#x3C; data.length) {
      setImmediate(processBatch)  // 让出 Event Loop
    }
  }
  
  processBatch()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;&lt;strong&gt;使用 Worker Threads 处理 CPU 密集型任务&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { Worker } = require(&apos;worker_threads&apos;)

const worker = new Worker(&apos;./heavy-task.js&apos;)
worker.on(&apos;message&apos;, result =&gt; {
  console.log(&apos;Result:&apos;, result)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/&quot;&gt;Node.js Event Loop 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/docs/guides/dont-block-the-event-loop/&quot;&gt;深入理解 Node.js Event Loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810&quot;&gt;浏览器 Event Loop vs Node.js Event Loop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[6.Nuxt.js SSR 实战 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-06-nuxt-ssr</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-06-nuxt-ssr</guid><pubDate>Wed, 21 Jan 2026 07:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;Nuxt.js SSR 实践：如何处理客户端/服务端差异？如何优化 SSR 性能？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. SSR vs SSG vs CSR&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;渲染时机&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;性能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每次请求时渲染&lt;/td&gt;
&lt;td&gt;动态内容、SEO&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;构建时预渲染&lt;/td&gt;
&lt;td&gt;静态内容、博客&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览器渲染&lt;/td&gt;
&lt;td&gt;后台管理系统&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 数据获取&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Nuxt3 - useFetch (服务端+客户端都会执行)
const { data: posts } = await useFetch(&apos;/api/posts&apos;)

// useAsyncData (自定义异步逻辑)
const { data } = await useAsyncData(&apos;posts&apos;, () =&gt; $fetch(&apos;/api/posts&apos;))

// 仅客户端执行
onMounted(() =&gt; {
  // 访问 window, document 等
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 环境判断&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;!-- 仅客户端渲染 --&gt;
  &amp;#x3C;ClientOnly&gt;
    &amp;#x3C;Chart /&gt;  &amp;#x3C;!-- 依赖 window 的组件 --&gt;
  &amp;#x3C;/ClientOnly&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup&gt;
// 判断环境
if (import.meta.server) {
  // 服务端逻辑
}

if (import.meta.client) {
  // 客户端逻辑
}
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 常见问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题1：水合不匹配（Hydration Mismatch）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 错误：服务端和客户端渲染结果不同
&amp;#x3C;div&gt;{{ new Date().getTime() }}&amp;#x3C;/div&gt;  // 时间戳每次都不同

// ✅ 正确：使用 ClientOnly 或统一数据源
&amp;#x3C;ClientOnly&gt;
  &amp;#x3C;div&gt;{{ timestamp }}&amp;#x3C;/div&gt;
&amp;#x3C;/ClientOnly&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题2：window/document 未定义&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 错误：服务端没有 window
const width = window.innerWidth

// ✅ 正确：判断环境或使用 ClientOnly
const width = import.meta.client ? window.innerWidth : 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 性能优化&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. 缓存策略
const { data } = await useFetch(&apos;/api/posts&apos;, {
  getCachedData(key) {
    return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
  }
})

// 2. 懒加载组件
const HeavyComponent = defineAsyncComponent(() =&gt;
  import(&apos;~/components/HeavyComponent.vue&apos;)
)

// 3. 路由缓存
export default defineNuxtConfig({
  routeRules: {
    &apos;/blog/**&apos;: { swr: 3600 },  // 1小时缓存
    &apos;/admin/**&apos;: { ssr: false }  // 客户端渲染
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;合理选择渲染模式&lt;/strong&gt;：SEO 需求用 SSR，静态内容用 SSG&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理环境差异&lt;/strong&gt;：用 &lt;code&gt;ClientOnly&lt;/code&gt; 包裹客户端组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免水合错误&lt;/strong&gt;：确保服务端和客户端渲染结果一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存优化&lt;/strong&gt;：合理使用页面缓存和 API 缓存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控性能&lt;/strong&gt;：使用 Lighthouse 和 WebPageTest&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nuxt.com/&quot;&gt;Nuxt3 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/scaling-up/ssr.html&quot;&gt;SSR 水合过程详解&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[5.Vue Diff 算法 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-05-vue-diff</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-05-vue-diff</guid><pubDate>Wed, 21 Jan 2026 07:19:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：核心原理、算法理解、性能优化&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;br&gt;
Vue 的虚拟 DOM Diff 算法是如何工作的？Vue2 和 Vue3 的 Diff 算法有什么区别？为什么需要 key？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. Diff 算法基本原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;为什么需要 Diff 算法？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 直接操作 DOM 成本很高
const list = document.querySelector(&apos;#list&apos;)
list.innerHTML = &apos;&apos;  // 清空
data.forEach(item =&gt; {
  const li = document.createElement(&apos;li&apos;)
  li.textContent = item
  list.appendChild(li)  // 每次都操作 DOM
})

// 虚拟 DOM + Diff 算法
// 1. 对比新旧虚拟 DOM 树
// 2. 找出最小差异
// 3. 只更新变化的部分
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同层比较&lt;/strong&gt;：只比较同一层级的节点（时间复杂度 O(n)）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型判断&lt;/strong&gt;：不同类型直接替换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;key 标识&lt;/strong&gt;：快速定位可复用节点&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;2. Vue2 的 Diff 算法（双端对比）&lt;/h3&gt;
&lt;h4&gt;算法流程&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;旧节点：A B C D
新节点：B A D C

步骤1：双端对比
  旧头 A  ←→  新头 B  ❌
  旧尾 D  ←→  新尾 C  ❌
  旧头 A  ←→  新尾 C  ❌
  旧尾 D  ←→  新头 B  ❌

步骤2：通过 key 查找
  新头 B 在旧节点中找到，移动 B

步骤3：继续双端对比
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;源码简化版&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function updateChildren(oldCh, newCh) {
  let oldStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let newStartIdx = 0
  let newEndIdx = newCh.length - 1
  
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  
  while (oldStartIdx &amp;#x3C;= oldEndIdx &amp;#x26;&amp;#x26; newStartIdx &amp;#x3C;= newEndIdx) {
    // 跳过已处理的节点
    if (!oldStartVnode) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (!oldEndVnode) {
      oldEndVnode = oldCh[--oldEndIdx]
    }
    
    // 四种快速匹配
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 旧头 vs 新头
      patchVnode(oldStartVnode, newStartVnode)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 旧尾 vs 新尾
      patchVnode(oldEndVnode, newEndVnode)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 旧头 vs 新尾（节点右移）
      patchVnode(oldStartVnode, newEndVnode)
      nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 旧尾 vs 新头（节点左移）
      patchVnode(oldEndVnode, newStartVnode)
      nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    
    // 都不匹配，通过 key 查找
    else {
      const idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (!idxInOld) {
        // 新节点，创建
        createElm(newStartVnode, parentElm, oldStartVnode.elm)
      } else {
        // 找到了，移动复用
        const vnodeToMove = oldCh[idxInOld]
        patchVnode(vnodeToMove, newStartVnode)
        oldCh[idxInOld] = undefined
        nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  
  // 处理剩余节点
  if (oldStartIdx &gt; oldEndIdx) {
    // 新节点有剩余，批量添加
    addVnodes(parentElm, newCh, newStartIdx, newEndIdx)
  } else if (newStartIdx &gt; newEndIdx) {
    // 旧节点有剩余，批量删除
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 双端对比，4 种快速匹配&lt;/li&gt;
&lt;li&gt;✅ 适合头尾操作（push、pop、shift、unshift）&lt;/li&gt;
&lt;li&gt;❌ 中间插入需要遍历查找&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;3. Vue3 的 Diff 算法（最长递增子序列）&lt;/h3&gt;
&lt;h4&gt;核心优化&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Vue3 算法分 5 个步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;旧节点：A B C D E
新节点：B A D C F

步骤1：前序对比（从头开始）
  A ≠ B，停止

步骤2：后序对比（从尾开始）
  E ≠ F，停止

步骤3：处理新增/删除
  如果旧节点遍历完，新节点有剩余 → 新增
  如果新节点遍历完，旧节点有剩余 → 删除

步骤4：处理未知序列（最长递增子序列）
  旧节点：B C D
  新节点：B D C
  
  找到最长递增子序列：B D
  只需移动 C，复用 B 和 D

步骤5：移动节点
  移动非递增序列的节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;最长递增子序列算法&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 找出最长递增子序列的索引
function getSequence(arr) {
  const p = arr.slice()  // 保存前驱索引
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  
  for (i = 0; i &amp;#x3C; len; i++) {
    const arrI = arr[i]
    
    if (arrI !== 0) {
      j = result[result.length - 1]
      
      // 如果当前值大于结果序列的最后一个，直接push
      if (arr[j] &amp;#x3C; arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      
      // 二分查找，找到第一个比 arrI 大的位置，替换它
      u = 0
      v = result.length - 1
      while (u &amp;#x3C; v) {
        c = (u + v) &gt;&gt; 1  // 中间位置
        if (arr[result[c]] &amp;#x3C; arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      
      if (arrI &amp;#x3C; arr[result[u]]) {
        if (u &gt; 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  
  // 回溯找到正确的序列
  u = result.length
  v = result[u - 1]
  while (u-- &gt; 0) {
    result[u] = v
    v = p[v]
  }
  
  return result
}

// 示例
const arr = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]
console.log(getSequence(arr))
// 输出最长递增子序列的索引位置
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Vue3 Diff 源码简化&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function patchKeyedChildren(c1, c2, container) {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1  // 旧子节点末尾索引
  let e2 = l2 - 1  // 新子节点末尾索引
  
  // 1. 前序对比
  while (i &amp;#x3C;= e1 &amp;#x26;&amp;#x26; i &amp;#x3C;= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container)
    } else {
      break
    }
    i++
  }
  
  // 2. 后序对比
  while (i &amp;#x3C;= e1 &amp;#x26;&amp;#x26; i &amp;#x3C;= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container)
    } else {
      break
    }
    e1--
    e2--
  }
  
  // 3. 新节点有剩余（新增）
  if (i &gt; e1) {
    if (i &amp;#x3C;= e2) {
      while (i &amp;#x3C;= e2) {
        patch(null, c2[i], container)
        i++
      }
    }
  }
  
  // 4. 旧节点有剩余（删除）
  else if (i &gt; e2) {
    while (i &amp;#x3C;= e1) {
      unmount(c1[i])
      i++
    }
  }
  
  // 5. 处理未知序列（最长递增子序列）
  else {
    const s1 = i
    const s2 = i
    
    // 5.1 建立新节点的 key -&gt; index 映射
    const keyToNewIndexMap = new Map()
    for (i = s2; i &amp;#x3C;= e2; i++) {
      const nextChild = c2[i]
      keyToNewIndexMap.set(nextChild.key, i)
    }
    
    // 5.2 遍历旧节点，找出需要移动和删除的节点
    let patched = 0
    const toBePatched = e2 - s2 + 1
    let moved = false
    let maxNewIndexSoFar = 0
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
    
    for (i = s1; i &amp;#x3C;= e1; i++) {
      const prevChild = c1[i]
      
      if (patched &gt;= toBePatched) {
        // 新节点都处理完了，剩下的都是要删除的
        unmount(prevChild)
        continue
      }
      
      let newIndex = keyToNewIndexMap.get(prevChild.key)
      
      if (newIndex === undefined) {
        // 旧节点在新节点中不存在，删除
        unmount(prevChild)
      } else {
        // 记录新旧节点的映射关系
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        
        // 判断是否需要移动
        if (newIndex &gt;= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex
        } else {
          moved = true
        }
        
        // 复用节点
        patch(prevChild, c2[newIndex], container)
        patched++
      }
    }
    
    // 5.3 移动和挂载新节点
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : []
    
    let j = increasingNewIndexSequence.length - 1
    
    // 倒序遍历，方便插入
    for (i = toBePatched - 1; i &gt;= 0; i--) {
      const nextIndex = s2 + i
      const nextChild = c2[nextIndex]
      const anchor = nextIndex + 1 &amp;#x3C; l2 ? c2[nextIndex + 1].el : null
      
      if (newIndexToOldIndexMap[i] === 0) {
        // 新节点，挂载
        patch(null, nextChild, container, anchor)
      } else if (moved) {
        // 需要移动
        if (j &amp;#x3C; 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor)
        } else {
          j--
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. key 的作用&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;为什么需要 key？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;!-- ❌ 没有 key --&gt;
&amp;#x3C;li v-for=&quot;item in items&quot;&gt;{{ item.name }}&amp;#x3C;/li&gt;

&amp;#x3C;!-- 新旧列表 --&gt;
旧：[{ name: &apos;A&apos; }, { name: &apos;B&apos; }, { name: &apos;C&apos; }]
新：[{ name: &apos;B&apos; }, { name: &apos;C&apos; }, { name: &apos;A&apos; }]

&amp;#x3C;!-- Diff 结果：就地复用，修改所有节点的文本内容 --&gt;
1. 修改第1个 li 的文本：A → B
2. 修改第2个 li 的文本：B → C
3. 修改第3个 li 的文本：C → A
效率低！


&amp;#x3C;!-- ✅ 有 key --&gt;
&amp;#x3C;li v-for=&quot;item in items&quot; :key=&quot;item.id&quot;&gt;{{ item.name }}&amp;#x3C;/li&gt;

&amp;#x3C;!-- Diff 结果：通过 key 识别，只移动节点 --&gt;
1. 复用 B 节点，移动到第1位
2. 复用 C 节点，移动到第2位
3. 复用 A 节点，移动到第3位
效率高！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key 的最佳实践&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;!-- ✅ 好：使用唯一 ID --&gt;
&amp;#x3C;li v-for=&quot;item in users&quot; :key=&quot;item.id&quot;&gt;{{ item.name }}&amp;#x3C;/li&gt;

&amp;#x3C;!-- ❌ 不好：使用 index --&gt;
&amp;#x3C;li v-for=&quot;(item, index) in users&quot; :key=&quot;index&quot;&gt;{{ item.name }}&amp;#x3C;/li&gt;
&amp;#x3C;!-- 问题：删除第1项后，所有 index 都变了，无法复用 --&gt;

&amp;#x3C;!-- ❌ 不好：使用随机数 --&gt;
&amp;#x3C;li v-for=&quot;item in users&quot; :key=&quot;Math.random()&quot;&gt;{{ item.name }}&amp;#x3C;/li&gt;
&amp;#x3C;!-- 问题：每次渲染 key 都不同，永远无法复用 --&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不使用 key 的副作用&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div v-for=&quot;(item, index) in list&quot; :key=&quot;index&quot;&gt;
    &amp;#x3C;input v-model=&quot;item.value&quot; /&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup&gt;
const list = ref([
  { id: 1, value: &apos;A&apos; },
  { id: 2, value: &apos;B&apos; },
  { id: 3, value: &apos;C&apos; }
])

// 删除第1项
function removeFirst() {
  list.value.shift()
}
&amp;#x3C;/script&gt;

&amp;#x3C;!-- 问题：
删除第1项后：
  原本 index=0 的 input（值为&apos;A&apos;）被删除
  原本 index=1 的 input 变成 index=0（但值仍为&apos;B&apos;）
  原本 index=2 的 input 变成 index=1（但值仍为&apos;C&apos;）
  
  Vue 会复用 DOM，导致 input 的值错位！
--&gt;

&amp;#x3C;!-- 解决：使用唯一 ID --&gt;
&amp;#x3C;div v-for=&quot;item in list&quot; :key=&quot;item.id&quot;&gt;
  &amp;#x3C;input v-model=&quot;item.value&quot; /&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;5. Vue2 vs Vue3 Diff 对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Vue2&lt;/th&gt;
&lt;th&gt;Vue3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;算法&lt;/td&gt;
&lt;td&gt;双端对比&lt;/td&gt;
&lt;td&gt;最长递增子序列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时间复杂度&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移动次数&lt;/td&gt;
&lt;td&gt;较多&lt;/td&gt;
&lt;td&gt;最少（理论最优）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适合场景&lt;/td&gt;
&lt;td&gt;头尾操作&lt;/td&gt;
&lt;td&gt;任意位置插入/删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态标记&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;PatchFlag 静态提升&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;性能测试&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;场景：1000个节点，随机插入/删除

Vue2：需要移动 ~500 个节点
Vue3：需要移动 ~100 个节点（找到最长递增子序列，只移动非递增的）

结果：Vue3 性能提升 5倍
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;始终使用唯一且稳定的 key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要使用 index 作为 key&lt;/strong&gt;（除非列表完全静态）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要使用随机数作为 key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要省略 key&lt;/strong&gt;（虽然 Vue 不报错，但性能差）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解 Diff 算法原理&lt;/strong&gt;，写出高性能的列表代码&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts&quot;&gt;Vue3 源码 - patchKeyedChildren&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-increasing-subsequence/&quot;&gt;最长递增子序列算法详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vue-next-template-explorer.netlify.app/&quot;&gt;虚拟 DOM 与 Diff 算法&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[4.Vue2 迁移到 Vue3 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-04-vue-migration</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-04-vue-migration</guid><pubDate>Wed, 21 Jan 2026 07:18:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;Vue2 到 Vue3 迁移策略：10万+ 行代码的大型项目如何平滑迁移？&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;渐进式迁移策略&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;阶段1：准备阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;升级到 Vue 2.7（过渡版本，支持部分 Vue3 API）&lt;/li&gt;
&lt;li&gt;使用 ESLint 插件检测不兼容代码&lt;/li&gt;
&lt;li&gt;建立完善的测试覆盖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;阶段2：使用 @vue/compat&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// vite.config.js
export default {
  resolve: {
    alias: {
      vue: &apos;@vue/compat&apos;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;阶段3：逐模块迁移&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从独立模块开始（如工具函数、公共组件）&lt;/li&gt;
&lt;li&gt;修改不兼容 API&lt;/li&gt;
&lt;li&gt;建立 MVP 回归测试&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;阶段4：去除兼容层&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逐步移除 &lt;code&gt;@vue/compat&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启用 Vue3 完整特性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常见不兼容 API&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vue2&lt;/th&gt;
&lt;th&gt;Vue3 替代&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.$children&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ref()&lt;/code&gt; + &lt;code&gt;expose()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.$listeners&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;合并到 &lt;code&gt;$attrs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EventBus&lt;/td&gt;
&lt;td&gt;mitt 或 Pinia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$on/$off/$once&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;已移除，用 mitt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filters&lt;/td&gt;
&lt;td&gt;方法或计算属性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;最佳实践&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;使用自动化工具&lt;/strong&gt;：gogocode、vue-codemod&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建立测试保障&lt;/strong&gt;：单元测试 + E2E 测试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度发布&lt;/strong&gt;：先上线小流量观察&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能监控&lt;/strong&gt;：对比迁移前后性能数据&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://v3-migration.vuejs.org/&quot;&gt;Vue3 迁移指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://v3-migration.vuejs.org/breaking-changes/migration-build.html&quot;&gt;@vue/compat 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[3.Vue3 性能优化 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-03-vue3-performance</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-03-vue3-performance</guid><pubDate>Wed, 21 Jan 2026 07:17:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：性能调优、内存管理、渲染优化、实战能力&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
优化一个 Vue3 的数据可视化大屏项目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时展示 5000+ 条设备状态数据（每秒更新 50-100 条）&lt;/li&gt;
&lt;li&gt;页面包含多个图表组件（ECharts）、表格、卡片&lt;/li&gt;
&lt;li&gt;首屏加载时间 8秒+，滚动时有明显卡顿&lt;/li&gt;
&lt;li&gt;内存占用持续增长，1小时后页面变慢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;渲染优化：除虚拟滚动外，Vue3 还有哪些优化大数据列表的手段？&lt;/li&gt;
&lt;li&gt;组件优化：如何优化频繁更新的图表组件？&lt;/li&gt;
&lt;li&gt;内存泄漏：如何排查并解决 Vue3 应用中的内存泄漏？&lt;/li&gt;
&lt;li&gt;首屏优化：针对 8秒+ 的首屏时间，有哪些优化方案？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. 渲染优化&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心手段&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. shallowRef - 浅层响应式（只追踪 .value 变化）
import { shallowRef } from &apos;vue&apos;

const largeList = shallowRef([/* 5000+ 条数据 */])

// 更新数据时手动触发更新
function updateList(newData) {
  largeList.value = [...newData]  // 整体替换才会触发更新
}

// 2. shallowReactive - 只代理第一层
const state = shallowReactive({
  items: [/* 大数组 */]
})

// 3. v-memo - 缓存组件渲染结果
&amp;#x3C;template&gt;
  &amp;#x3C;div v-for=&quot;item in list&quot; :key=&quot;item.id&quot; v-memo=&quot;[item.status]&quot;&gt;
    &amp;#x3C;!-- 只有 item.status 变化时才重新渲染 --&gt;
    &amp;#x3C;DeviceCard :data=&quot;item&quot; /&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

// 4. v-once - 只渲染一次
&amp;#x3C;div v-once&gt;
  &amp;#x3C;!-- 静态内容，永不更新 --&gt;
  &amp;#x3C;StaticChart /&gt;
&amp;#x3C;/div&gt;

// 5. 虚拟滚动
import { useVirtualList } from &apos;@vueuse/core&apos;

const { list: virtualList, containerProps, wrapperProps } = useVirtualList(
  largeList,
  { itemHeight: 80, overscan: 10 }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;2. 组件优化&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;图表组件防抖节流&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { debounce } from &apos;lodash-es&apos;
import * as echarts from &apos;echarts/core&apos;

export default {
  setup() {
    const chartRef = ref()
    let chartInstance = null
    
    // 防抖更新图表
    const updateChart = debounce((data) =&gt; {
      if (!chartInstance) {
        chartInstance = echarts.init(chartRef.value)
      }
      
      chartInstance.setOption({
        series: [{ data }]
      })
    }, 300)
    
    // WebSocket 数据流
    onMounted(() =&gt; {
      const ws = new WebSocket(&apos;ws://...&apos;)
      ws.onmessage = (event) =&gt; {
        const newData = JSON.parse(event.data)
        updateChart(newData)  // 自动防抖
      }
      
      onUnmounted(() =&gt; {
        ws.close()
        chartInstance?.dispose()  // 释放图表实例
      })
    })
    
    return { chartRef }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;组件缓存&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;!-- 缓存不活跃的标签页 --&gt;
  &amp;#x3C;KeepAlive :max=&quot;5&quot;&gt;
    &amp;#x3C;component :is=&quot;currentTab&quot; /&gt;
  &amp;#x3C;/KeepAlive&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. 内存泄漏排查&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;常见泄漏点&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ 问题1：定时器未清理
export default {
  setup() {
    const timer = setInterval(() =&gt; {
      // ...
    }, 1000)
    
    // ✅ 解决：清理定时器
    onUnmounted(() =&gt; {
      clearInterval(timer)
    })
  }
}

// ❌ 问题2：全局事件未解绑
export default {
  setup() {
    const handleResize = () =&gt; { /* ... */ }
    
    onMounted(() =&gt; {
      window.addEventListener(&apos;resize&apos;, handleResize)
    })
    
    // ✅ 解决：解绑事件
    onUnmounted(() =&gt; {
      window.removeEventListener(&apos;resize&apos;, handleResize)
    })
  }
}

// ❌ 问题3：第三方库实例未销毁
export default {
  setup() {
    let chart = null
    
    onMounted(() =&gt; {
      chart = echarts.init(chartRef.value)
    })
    
    // ✅ 解决：销毁实例
    onUnmounted(() =&gt; {
      chart?.dispose()
      chart = null
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;排查工具&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Chrome DevTools - Memory&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;录制堆快照（Heap Snapshot）&lt;/li&gt;
&lt;li&gt;对比快照找泄漏点&lt;/li&gt;
&lt;li&gt;查看 Detached DOM nodes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vue DevTools&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看组件树&lt;/li&gt;
&lt;li&gt;检查未清理的组件实例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;4. 首屏优化&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优化策略&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. 路由懒加载
const routes = [
  {
    path: &apos;/dashboard&apos;,
    component: () =&gt; import(&apos;./views/Dashboard.vue&apos;)  // 懒加载
  }
]

// 2. 组件懒加载
&amp;#x3C;script setup&gt;
import { defineAsyncComponent } from &apos;vue&apos;

const HeavyChart = defineAsyncComponent(() =&gt;
  import(&apos;./components/HeavyChart.vue&apos;)
)
&amp;#x3C;/script&gt;

// 3. 骨架屏
&amp;#x3C;template&gt;
  &amp;#x3C;Suspense&gt;
    &amp;#x3C;template #default&gt;
      &amp;#x3C;AsyncComponent /&gt;
    &amp;#x3C;/template&gt;
    &amp;#x3C;template #fallback&gt;
      &amp;#x3C;SkeletonScreen /&gt;
    &amp;#x3C;/template&gt;
  &amp;#x3C;/Suspense&gt;
&amp;#x3C;/template&gt;

// 4. 代码分割（Vite）
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          &apos;echarts&apos;: [&apos;echarts&apos;],
          &apos;vendor&apos;: [&apos;vue&apos;, &apos;pinia&apos;]
        }
      }
    }
  }
}

// 5. 图片懒加载
import { useIntersectionObserver } from &apos;@vueuse/core&apos;

const imgRef = ref()
const { stop } = useIntersectionObserver(
  imgRef,
  ([{ isIntersecting }]) =&gt; {
    if (isIntersecting) {
      // 加载图片
      imgRef.value.src = realSrc
      stop()
    }
  }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;性能检测清单&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 使用 Lighthouse 分析性能指标&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Chrome DevTools Performance 录制加载过程&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 检查 Network 面板的资源加载时间&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 使用 Vue DevTools 查看组件渲染时间&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 监控 Memory 面板的内存占用趋势&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 检查 Console 警告和错误&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;大数据列表&lt;/strong&gt;：虚拟滚动 + shallowRef&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;频繁更新&lt;/strong&gt;：防抖节流 + v-memo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图表组件&lt;/strong&gt;：缓存实例 + 按需更新&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首屏加载&lt;/strong&gt;：路由懒加载 + 骨架屏 + 代码分割&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理&lt;/strong&gt;：及时清理定时器、事件、第三方实例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控优化&lt;/strong&gt;：使用性能监控工具持续优化&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/best-practices/performance.html&quot;&gt;Vue3 性能优化指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Akryum/vue-virtual-scroller&quot;&gt;虚拟滚动实现原理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/devtools/performance/&quot;&gt;Chrome DevTools 性能分析&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[2.Vue 组件通信 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-02-component-communication</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-02-component-communication</guid><pubDate>Wed, 21 Jan 2026 07:16:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：方案选型、状态管理、性能优化、架构设计&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
一个电商后台管理系统，包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多层级的嵌套路由（3-5层）&lt;/li&gt;
&lt;li&gt;全局状态（用户信息、权限、主题配置）&lt;/li&gt;
&lt;li&gt;跨模块的数据共享（订单模块 ↔ 库存模块 ↔ 财务模块）&lt;/li&gt;
&lt;li&gt;实时数据更新（WebSocket 推送的订单状态变更）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;方案选型&lt;/strong&gt;：&lt;code&gt;props/emit&lt;/code&gt;、&lt;code&gt;provide/inject&lt;/code&gt;、&lt;code&gt;Vuex&lt;/code&gt;、&lt;code&gt;Pinia&lt;/code&gt;、&lt;code&gt;EventBus&lt;/code&gt; 这些方案，您会如何组合使用？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态管理&lt;/strong&gt;：Vuex vs Pinia，在 Vue3 项目中您会选择哪个？为什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：当状态树很大时，如何避免不必要的组件重渲染？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：您在项目中遇到过哪些组件通信的难题？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. 方案选型与组合使用&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;props/emit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;父子组件直接通信&lt;/td&gt;
&lt;td&gt;简单直接、类型安全&lt;/td&gt;
&lt;td&gt;层级多时繁琐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;provide/inject&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;跨层级传递配置、依赖&lt;/td&gt;
&lt;td&gt;无需逐层传递&lt;/td&gt;
&lt;td&gt;不适合响应式数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pinia&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全局状态、跨组件共享&lt;/td&gt;
&lt;td&gt;组合式API、TS支持好&lt;/td&gt;
&lt;td&gt;学习成本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EventBus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;跨组件事件通信&lt;/td&gt;
&lt;td&gt;灵活&lt;/td&gt;
&lt;td&gt;难以维护、Vue3已移除&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;推荐组合方案&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. 父子组件：props/emit
&amp;#x3C;ChildComponent :data=&quot;parentData&quot; @update=&quot;handleUpdate&quot; /&gt;

// 2. 跨层级配置：provide/inject
// 父组件
provide(&apos;theme&apos;, computed(() =&gt; theme.value))

// 子孙组件
const theme = inject(&apos;theme&apos;)

// 3. 全局状态：Pinia
import { defineStore } from &apos;pinia&apos;

export const useUserStore = defineStore(&apos;user&apos;, () =&gt; {
  const user = ref(null)
  const isLoggedIn = computed(() =&gt; !!user.value)
  
  function login(credentials) {
    // ...
  }
  
  return { user, isLoggedIn, login }
})

// 4. 实时更新：Pinia + WebSocket
export const useOrderStore = defineStore(&apos;order&apos;, () =&gt; {
  const orders = ref([])
  
  onMounted(() =&gt; {
    const ws = new WebSocket(&apos;ws://...&apos;)
    ws.onmessage = (event) =&gt; {
      const order = JSON.parse(event.data)
      updateOrder(order)
    }
  })
  
  return { orders }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;2. Vuex vs Pinia&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;选择 Pinia 的理由&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更好的 TypeScript 支持&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Pinia - 自动类型推导
export const useStore = defineStore(&apos;main&apos;, () =&gt; {
  const count = ref(0)
  const doubleCount = computed(() =&gt; count.value * 2)
  function increment() {
    count.value++
  }
  return { count, doubleCount, increment }
})

// 使用时有完整类型提示
const store = useStore()
store.count  // number
store.doubleCount  // number
store.increment()  // void
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;组合式 API 风格&lt;/strong&gt;（更符合 Vue3）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无需 mutations&lt;/strong&gt;（简化代码）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持多实例&lt;/strong&gt;（适合SSR和多标签页场景）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更轻量&lt;/strong&gt;（~1KB）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;核心差异&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Vuex&lt;/th&gt;
&lt;th&gt;Pinia&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API 风格&lt;/td&gt;
&lt;td&gt;Options API&lt;/td&gt;
&lt;td&gt;Composition API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutations&lt;/td&gt;
&lt;td&gt;必需&lt;/td&gt;
&lt;td&gt;无需（直接修改state）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;需手动声明&lt;/td&gt;
&lt;td&gt;自动推导&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevTools&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模块化&lt;/td&gt;
&lt;td&gt;需配置 modules&lt;/td&gt;
&lt;td&gt;天然支持多 store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;更好的支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;3. 性能优化&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;避免不必要的重渲染&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. 使用 storeToRefs 解构响应式属性
import { storeToRefs } from &apos;pinia&apos;

const store = useStore()
const { count, doubleCount } = storeToRefs(store)  // 保持响应式
const { increment } = store  // 方法不需要响应式

// 2. 选择性订阅（只监听需要的状态）
const orderStore = useOrderStore()

// ❌ 不好：订阅整个store
watch(() =&gt; orderStore.$state, () =&gt; {
  // 任何状态变化都会触发
})

// ✅ 好：只监听特定状态
watch(() =&gt; orderStore.orders, (newOrders) =&gt; {
  // 只在orders变化时触发
})

// 3. 使用计算属性过滤数据
const filteredOrders = computed(() =&gt; {
  return orderStore.orders.filter(order =&gt; order.status === &apos;pending&apos;)
})

// 4. 虚拟滚动处理大列表
import { useVirtualList } from &apos;@vueuse/core&apos;

const { list, containerProps, wrapperProps } = useVirtualList(
  largeList,
  { itemHeight: 50 }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. 实战经验&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;常见问题与解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题1：多标签页状态隔离&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 解决方案：多实例 Pinia
import { createPinia, setActivePinia } from &apos;pinia&apos;

// 每个标签页创建独立的 pinia 实例
const pinia = createPinia()
setActivePinia(pinia)

app.use(pinia)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题2：跨模块数据同步&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 订单store监听库存store变化
export const useOrderStore = defineStore(&apos;order&apos;, () =&gt; {
  const inventoryStore = useInventoryStore()
  
  watch(() =&gt; inventoryStore.stock, (newStock) =&gt; {
    // 库存变化时更新订单状态
    updateOrdersAvailability(newStock)
  })
  
  return { orders }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题3：组件卸载后状态清理&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export const useFormStore = defineStore(&apos;form&apos;, () =&gt; {
  const formData = ref({})
  
  function reset() {
    formData.value = {}
  }
  
  // 组件卸载时自动清理
  onUnmounted(() =&gt; {
    reset()
  })
  
  return { formData, reset }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;优先使用 props/emit&lt;/strong&gt;（父子组件）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pinia 管理全局状态&lt;/strong&gt;（用户、权限、主题）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;provide/inject 传递依赖&lt;/strong&gt;（配置、服务实例）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免 EventBus&lt;/strong&gt;（难以维护，Vue3已移除）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态持久化&lt;/strong&gt;：使用 &lt;code&gt;pinia-plugin-persistedstate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态分模块&lt;/strong&gt;：按业务领域拆分 store&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：使用 &lt;code&gt;storeToRefs&lt;/code&gt;、计算属性、虚拟滚动&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pinia.vuejs.org/&quot;&gt;Pinia 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/guide/components/&quot;&gt;Vue3 组件通信最佳实践&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pinia.vuejs.org/introduction.html#comparison-with-vuex&quot;&gt;Vuex vs Pinia 对比&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[1.Vue 响应式原理 - 标准答案]]></title><link>https://zzao.club/post/ai/explore/03-01-vue-reactive</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/03-01-vue-reactive</guid><pubDate>Wed, 21 Jan 2026 06:53:18 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：底层原理理解、技术对比、边界场景、实战经验&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;br&gt;
Vue2 使用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 而 Vue3 改用 &lt;code&gt;Proxy&lt;/code&gt; 来实现响应式系统。请从以下几个方面深入对比：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术实现层面&lt;/strong&gt;：两者在拦截数据变化时的核心差异是什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界场景问题&lt;/strong&gt;：Vue2 响应式系统有哪些已知的局限性？Vue3 的 Proxy 如何解决这些问题？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能考量&lt;/strong&gt;：在大型应用中，这两种方案的性能表现有什么差异？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：您在项目中是否遇到过 Vue2 响应式的坑？如何解决的？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;标准答案&lt;/h2&gt;
&lt;h3&gt;1. 技术实现层面的核心差异&lt;/h3&gt;
&lt;h4&gt;Vue2 - Object.defineProperty&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2 响应式实现原理
function defineReactive(obj, key, val) {
  const dep = new Dep() // 依赖收集器
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 通知更新
      dep.notify()
    }
  })
}

// 递归遍历对象的所有属性
function observe(obj) {
  if (typeof obj !== &apos;object&apos; || obj === null) return
  
  Object.keys(obj).forEach(key =&gt; {
    defineReactive(obj, key, obj[key])
    // 递归处理嵌套对象
    if (typeof obj[key] === &apos;object&apos;) {
      observe(obj[key])
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 可以精确拦截对象&lt;strong&gt;属性&lt;/strong&gt;的读写&lt;/li&gt;
&lt;li&gt;❌ 只能拦截&lt;strong&gt;已存在的属性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;❌ 必须遍历对象的每个属性&lt;/li&gt;
&lt;li&gt;❌ 无法监听&lt;strong&gt;属性的新增/删除&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;❌ 无法监听&lt;strong&gt;数组索引和 length&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Vue3 - Proxy&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue3 响应式实现原理
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      
      // 依赖收集
      track(target, key)
      
      // 惰性代理：只在访问时才代理嵌套对象
      if (typeof result === &apos;object&apos; &amp;#x26;&amp;#x26; result !== null) {
        return reactive(result)
      }
      
      return result
    },
    
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 触发更新
      if (oldValue !== value) {
        trigger(target, key)
      }
      
      return result
    },
    
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const result = Reflect.deleteProperty(target, key)
      
      if (hadKey &amp;#x26;&amp;#x26; result) {
        trigger(target, key)
      }
      
      return result
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 可以拦截&lt;strong&gt;对象本身&lt;/strong&gt;的 13 种操作（get、set、deleteProperty、has、ownKeys等）&lt;/li&gt;
&lt;li&gt;✅ 可以监听&lt;strong&gt;属性的新增/删除&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;✅ 可以监听&lt;strong&gt;数组索引和 length&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;惰性代理&lt;/strong&gt;：只在访问嵌套对象时才代理&lt;/li&gt;
&lt;li&gt;✅ 使用 &lt;code&gt;Reflect&lt;/code&gt; 确保正确的 &lt;code&gt;this&lt;/code&gt; 绑定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心差异总结&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Object.defineProperty&lt;/th&gt;
&lt;th&gt;Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;拦截层级&lt;/td&gt;
&lt;td&gt;属性级别&lt;/td&gt;
&lt;td&gt;对象级别&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新增属性&lt;/td&gt;
&lt;td&gt;❌ 无法监听&lt;/td&gt;
&lt;td&gt;✅ 自动监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除属性&lt;/td&gt;
&lt;td&gt;❌ 无法监听&lt;/td&gt;
&lt;td&gt;✅ 自动监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组索引&lt;/td&gt;
&lt;td&gt;❌ 无法监听&lt;/td&gt;
&lt;td&gt;✅ 自动监听&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;初始化&lt;/td&gt;
&lt;td&gt;必须递归遍历&lt;/td&gt;
&lt;td&gt;惰性代理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;初始化慢&lt;/td&gt;
&lt;td&gt;访问时略慢&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;2. 边界场景问题与解决方案&lt;/h3&gt;
&lt;h4&gt;Vue2 的局限性&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题1：无法监听属性新增&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2 - 无法响应
const vm = new Vue({
  data: {
    user: {
      name: &apos;Alice&apos;
    }
  }
})

// ❌ 直接添加属性，视图不会更新
vm.user.age = 18

// ✅ 必须使用 $set
vm.$set(vm.user, &apos;age&apos;, 18)
// 或
Vue.set(vm.user, &apos;age&apos;, 18)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题2：无法监听属性删除&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2 - 无法响应
delete vm.user.name  // ❌ 视图不会更新

// ✅ 必须使用 $delete
vm.$delete(vm.user, &apos;name&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题3：无法监听数组索引变化&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2 - 无法响应
const vm = new Vue({
  data: {
    items: [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
  }
})

vm.items[0] = &apos;x&apos;  // ❌ 视图不会更新
vm.items.length = 0  // ❌ 视图不会更新

// ✅ 必须使用数组方法或 $set
vm.$set(vm.items, 0, &apos;x&apos;)
vm.items.splice(0, vm.items.length)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue2 的变通方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重写了 7 个数组方法（push、pop、shift、unshift、splice、sort、reverse）&lt;/li&gt;
&lt;li&gt;提供 &lt;code&gt;$set&lt;/code&gt; 和 &lt;code&gt;$delete&lt;/code&gt; API&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Vue3 的解决方案&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue3 - 全部自动响应
import { reactive } from &apos;vue&apos;

const state = reactive({
  user: {
    name: &apos;Alice&apos;
  },
  items: [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
})

// ✅ 属性新增 - 自动响应
state.user.age = 18

// ✅ 属性删除 - 自动响应
delete state.user.name

// ✅ 数组索引 - 自动响应
state.items[0] = &apos;x&apos;

// ✅ 数组长度 - 自动响应
state.items.length = 0

// ✅ Map/Set - 也支持响应式
const map = reactive(new Map())
map.set(&apos;key&apos;, &apos;value&apos;)  // 自动响应
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. 性能考量&lt;/h3&gt;
&lt;h4&gt;初始化性能&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Vue2&lt;/strong&gt;：必须递归遍历所有属性&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2 - 初始化时递归遍历
function observe(obj) {
  if (typeof obj !== &apos;object&apos;) return
  
  // 遍历所有属性
  Object.keys(obj).forEach(key =&gt; {
    defineReactive(obj, key, obj[key])
    // 递归处理嵌套对象
    observe(obj[key])
  })
}

const data = {
  level1: {
    level2: {
      level3: {
        level4: { /* 1000个属性 */ }
      }
    }
  }
}

// 初始化时会遍历所有嵌套对象的所有属性
observe(data)  // 慢！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3&lt;/strong&gt;：惰性代理&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue3 - 只在访问时才代理嵌套对象
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      const result = Reflect.get(target, key)
      
      // 只在实际访问时才代理嵌套对象
      if (typeof result === &apos;object&apos; &amp;#x26;&amp;#x26; result !== null) {
        return reactive(result)
      }
      
      return result
    }
  })
}

const data = reactive({
  level1: {
    level2: {
      level3: {
        level4: { /* 1000个属性 */ }
      }
    }
  }
})

// 初始化很快，只代理了最外层对象
// 只有访问 data.level1 时才代理 level1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;性能对比&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化性能&lt;/strong&gt;：Vue3 &gt; Vue2（惰性代理 vs 递归遍历）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时性能&lt;/strong&gt;：Vue2 ≈ Vue3（Proxy 单次操作略慢，但差异很小）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存占用&lt;/strong&gt;：Vue3 &amp;#x3C; Vue2（惰性代理减少了不必要的代理对象）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;大型应用优化建议&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Vue2 优化&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 使用 Object.freeze 跳过响应式
export default {
  data() {
    return {
      // 大量静态数据，不需要响应式
      staticData: Object.freeze(largeArray),
      // 需要响应式的数据
      dynamicData: []
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vue3 优化&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { shallowReactive, shallowRef, markRaw } from &apos;vue&apos;

// 浅层响应式：只代理第一层
const state = shallowReactive({
  nested: { /* 深层对象，不会被代理 */ }
})

// 浅层 ref：不深度监听 .value 的变化
const list = shallowRef([/* 大数组 */])

// 标记为非响应式
const data = markRaw({
  /* 永远不会变成响应式 */
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. 实战经验与常见坑&lt;/h3&gt;
&lt;h4&gt;坑1：Vue2 动态添加属性不响应&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题场景&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export default {
  data() {
    return {
      form: {
        name: &apos;&apos;
      }
    }
  },
  mounted() {
    // ❌ 后端返回的额外字段，视图不更新
    this.$http.get(&apos;/api/form&apos;).then(res =&gt; {
      this.form.email = res.email  // 不响应！
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 方案1：使用 $set
this.$set(this.form, &apos;email&apos;, res.email)

// 方案2：整体替换对象
this.form = { ...this.form, email: res.email }

// 方案3：初始化时声明所有字段
data() {
  return {
    form: {
      name: &apos;&apos;,
      email: &apos;&apos;  // 提前声明
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;坑2：数组索引修改不响应&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题场景&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export default {
  data() {
    return {
      list: [{ id: 1, name: &apos;A&apos; }, { id: 2, name: &apos;B&apos; }]
    }
  },
  methods: {
    updateItem() {
      // ❌ 不响应
      this.list[0] = { id: 1, name: &apos;Updated&apos; }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 方案1：使用 $set
this.$set(this.list, 0, { id: 1, name: &apos;Updated&apos; })

// 方案2：使用 splice
this.list.splice(0, 1, { id: 1, name: &apos;Updated&apos; })

// 方案3：使用 Vue.set
Vue.set(this.list, 0, { id: 1, name: &apos;Updated&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;坑3：嵌套对象响应式丢失&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题场景&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Vue2
export default {
  data() {
    return {
      user: {
        profile: {
          name: &apos;Alice&apos;
        }
      }
    }
  },
  methods: {
    resetProfile() {
      // ❌ 新对象没有响应式
      this.user.profile = { name: &apos;Bob&apos;, age: 20 }
      // age 字段不响应
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 方案1：使用 $set 添加新属性
this.$set(this.user.profile, &apos;age&apos;, 20)

// 方案2：使用扩展运算符
this.user = {
  ...this.user,
  profile: { name: &apos;Bob&apos;, age: 20 }
}

// 方案3：Vue3 没有这个问题
const user = reactive({ profile: { name: &apos;Alice&apos; } })
user.profile = { name: &apos;Bob&apos;, age: 20 }  // ✅ 自动响应
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;h3&gt;Vue2 项目&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提前声明所有属性&lt;/strong&gt;：避免动态添加属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 $set/$delete&lt;/strong&gt;：处理动态属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数组操作用变异方法&lt;/strong&gt;：push、splice 等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大数据用 Object.freeze&lt;/strong&gt;：性能优化&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Vue3 项目&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选择合适的响应式 API&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reactive()&lt;/code&gt;：对象/数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref()&lt;/code&gt;：基本类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shallowReactive()&lt;/code&gt;：浅层响应式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readonly()&lt;/code&gt;：只读代理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;shallowRef&lt;/code&gt;/&lt;code&gt;shallowReactive&lt;/code&gt; 处理大数据&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;markRaw&lt;/code&gt; 标记非响应式数据&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;toRaw&lt;/code&gt; 获取原始对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reactive&lt;/code&gt; 对象解构会丢失响应式，使用 &lt;code&gt;toRefs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt; 在模板中自动解包，在 JS 中需要 &lt;code&gt;.value&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://v2.vuejs.org/v2/guide/reactivity.html&quot;&gt;Vue2 响应式原理详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vuejs.org/api/reactivity-core.html&quot;&gt;Vue3 响应式 API 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts&quot;&gt;Vue3 源码解析 - reactive.ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy&quot;&gt;深入理解 Proxy 和 Reflect&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[高级前端开发面试题目清单]]></title><description><![CDATA[面试岗位：高级前端开发（偏Node全栈）题目总数：10题预计时长：30-40分钟创建日期：2026-01-21]]></description><link>https://zzao.club/post/ai/explore/02-interview-questions</link><guid isPermaLink="true">https://zzao.club/post/ai/explore/02-interview-questions</guid><pubDate>Wed, 21 Jan 2026 06:39:25 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;面试岗位&lt;/strong&gt;：高级前端开发（偏Node全栈）&lt;br&gt;
&lt;strong&gt;题目总数&lt;/strong&gt;：10题&lt;br&gt;
&lt;strong&gt;预计时长&lt;/strong&gt;：30-40分钟&lt;br&gt;
&lt;strong&gt;创建日期&lt;/strong&gt;：2026-01-21&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目分布&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;技术方向&lt;/th&gt;
&lt;th&gt;题目数量&lt;/th&gt;
&lt;th&gt;题号&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vue2/Vue3 技术深度&lt;/td&gt;
&lt;td&gt;6题&lt;/td&gt;
&lt;td&gt;1-6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 技术&lt;/td&gt;
&lt;td&gt;2题&lt;/td&gt;
&lt;td&gt;7-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;架构设计&lt;/td&gt;
&lt;td&gt;1题&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队管理&lt;/td&gt;
&lt;td&gt;1题&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;Vue2/Vue3 技术部分（6题）&lt;/h2&gt;
&lt;h3&gt;第1题：Vue 响应式原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：底层原理理解、技术对比、边界场景、实战经验&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;br&gt;
Vue2 使用 &lt;code&gt;Object.defineProperty&lt;/code&gt; 而 Vue3 改用 &lt;code&gt;Proxy&lt;/code&gt; 来实现响应式系统。请从以下几个方面深入对比：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术实现层面&lt;/strong&gt;：两者在拦截数据变化时的核心差异是什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界场景问题&lt;/strong&gt;：Vue2 响应式系统有哪些已知的局限性？Vue3 的 Proxy 如何解决这些问题？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能考量&lt;/strong&gt;：在大型应用中，这两种方案的性能表现有什么差异？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：您在项目中是否遇到过 Vue2 响应式的坑？如何解决的？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;第2题：Vue 组件通信&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：方案选型、状态管理、性能优化、架构设计&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
一个电商后台管理系统，包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;多层级的嵌套路由（3-5层）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局状态（用户信息、权限、主题配置）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;跨模块的数据共享（订单模块 ↔ 库存模块 ↔ 财务模块）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实时数据更新（WebSocket 推送的订单状态变更）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;方案选型&lt;/strong&gt;：&lt;code&gt;props/emit&lt;/code&gt;、&lt;code&gt;provide/inject&lt;/code&gt;、&lt;code&gt;Vuex&lt;/code&gt;、&lt;code&gt;Pinia&lt;/code&gt;、&lt;code&gt;EventBus&lt;/code&gt; 这些方案，您会如何组合使用？各自适用的场景是什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态管理&lt;/strong&gt;：Vuex vs Pinia，在 Vue3 项目中您会选择哪个？为什么？它们的核心差异是什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：当状态树很大时（如商品列表有上千条数据），如何避免不必要的组件重渲染？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：您在项目中遇到过哪些组件通信的难题？如何解决的？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;第3题：Vue3 性能优化&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：性能调优、内存管理、渲染优化、实战能力&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
您负责优化一个 Vue3 的数据可视化大屏项目，存在以下性能问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现状&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实时展示 &lt;strong&gt;5000+ 条&lt;/strong&gt; 设备状态数据（每秒通过 WebSocket 更新 50-100 条）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面包含多个图表组件（ECharts）、表格、卡片&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;首屏加载时间 &lt;strong&gt;8秒+&lt;/strong&gt;，滚动时有明显卡顿&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存占用持续增长，1小时后页面变得很慢&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;渲染优化&lt;/strong&gt;：除了虚拟滚动，Vue3 还有哪些优化大数据列表渲染的手段？（如 &lt;code&gt;computed&lt;/code&gt;、&lt;code&gt;watchEffect&lt;/code&gt;、&lt;code&gt;shallowRef&lt;/code&gt;、&lt;code&gt;shallowReactive&lt;/code&gt; 等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件优化&lt;/strong&gt;：如何优化频繁更新的图表组件？考虑防抖节流、组件缓存等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存泄漏&lt;/strong&gt;：如何排查并解决 Vue3 应用中的内存泄漏问题？常见的泄漏点有哪些？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首屏优化&lt;/strong&gt;：针对 8秒+ 的首屏时间，有哪些具体的优化方案？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;第4题：Vue2 迁移到 Vue3&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：迁移策略、技术重构、风险控制、项目管理&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
您的团队需要将一个运行了 3 年的 Vue2 项目（10万+ 行代码）迁移到 Vue3。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;项目现状&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;大量使用 Mixins 实现逻辑复用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;依赖 Vue2 生态（Element UI、vue-router 3.x、Vuex 3.x）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用了一些 Vue2 特有的 API（&lt;code&gt;$listeners&lt;/code&gt;、&lt;code&gt;$scopedSlots&lt;/code&gt;、&lt;code&gt;filters&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;部分第三方库可能不兼容 Vue3&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;迁移策略&lt;/strong&gt;：一次性全量迁移 vs 渐进式迁移？您会选择哪种方案？如何规划迁移路径？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition API 重构&lt;/strong&gt;：如何将 Options API（尤其是 Mixins）重构为 Composition API？有哪些最佳实践？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态适配&lt;/strong&gt;：Element UI → Element Plus，Vuex → Pinia 等生态迁移的注意事项？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险控制&lt;/strong&gt;：如何保证迁移过程中业务不受影响？如何做回归测试？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：您是否有过类似的迁移经验？遇到过哪些坑？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;第5题：Vue Diff 算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：源码理解、算法原理、性能优化&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;br&gt;
Vue 的虚拟 DOM diff 算法是其性能优化的核心。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;双端对比算法&lt;/strong&gt;：Vue2 使用的双端对比（double-ended comparison）算法是如何工作的？为什么比简单的从头到尾对比更高效？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vue3 优化&lt;/strong&gt;：Vue3 在 diff 算法上做了哪些优化？（如最长递增子序列、静态提升、patchFlag 等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 React 对比&lt;/strong&gt;：Vue 的 diff 算法与 React 的 diff 算法有什么本质区别？各有什么优劣？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key 的作用&lt;/strong&gt;：为什么 &lt;code&gt;v-for&lt;/code&gt; 必须要加 &lt;code&gt;key&lt;/code&gt;？使用 &lt;code&gt;index&lt;/code&gt; 作为 &lt;code&gt;key&lt;/code&gt; 有什么问题？能举个实际的例子吗？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战应用&lt;/strong&gt;：在实际开发中，如何利用对 diff 算法的理解来优化列表渲染性能？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;第6题：Nuxt.js SSR 实战&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：SSR/SSG理解、数据获取、性能优化、实战经验&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
您的团队使用 Nuxt.js（Vue3 版本）开发了一个内容型网站（博客/新闻类），需要考虑 SEO 和首屏性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文章详情页需要服务端渲染（SEO）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;首页需要加载大量文章列表和推荐内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;部分交互组件（评论、点赞）需要客户端渲染&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSR vs SSG vs CSR&lt;/strong&gt;：在 Nuxt.js 中，这三种渲染模式分别适用于什么场景？如何混合使用？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据获取&lt;/strong&gt;：&lt;code&gt;asyncData&lt;/code&gt;、&lt;code&gt;fetch&lt;/code&gt;、&lt;code&gt;useFetch&lt;/code&gt;、&lt;code&gt;useAsyncData&lt;/code&gt; 这些 API 的区别是什么？什么时候用哪个？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：Nuxt.js 项目中常见的性能瓶颈有哪些？如何优化服务端渲染的性能？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实战问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如何处理服务端和客户端环境差异（如 &lt;code&gt;window&lt;/code&gt;、&lt;code&gt;localStorage&lt;/code&gt; 不可用）？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何避免服务端渲染的内存泄漏？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;您遇到过哪些 Nuxt.js 的坑？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;项目经验&lt;/strong&gt;：您是否有 SSR/SSG 的实际项目经验？效果如何？&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Node.js 技术部分（2题）&lt;/h2&gt;
&lt;h3&gt;第7题：Node.js Event Loop&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：事件循环理解、微任务/宏任务、实战应用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景代码&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&apos;1&apos;);

setTimeout(() =&gt; {
  console.log(&apos;2&apos;);
  Promise.resolve().then(() =&gt; console.log(&apos;3&apos;));
}, 0);

Promise.resolve().then(() =&gt; {
  console.log(&apos;4&apos;);
});

process.nextTick(() =&gt; {
  console.log(&apos;5&apos;);
});

console.log(&apos;6&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;执行顺序&lt;/strong&gt;：上述代码的输出顺序是什么？简要说明原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心概念&lt;/strong&gt;：&lt;code&gt;process.nextTick&lt;/code&gt;、&lt;code&gt;Promise.then&lt;/code&gt;、&lt;code&gt;setTimeout&lt;/code&gt; 的执行优先级是怎样的？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战应用&lt;/strong&gt;：在实际 Node.js 项目中（如数据库批量操作、文件读写），您如何利用事件循环的理解来优化性能？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;第8题：Node.js 性能调优&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：内存管理、性能诊断、CPU优化、实战能力&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
您负责维护一个 Node.js API 服务，生产环境出现以下问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务运行一段时间后响应变慢&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内存占用持续增长，最终导致 OOM（Out of Memory）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CPU 偶尔飙升到 100%&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内存泄漏排查&lt;/strong&gt;：如何定位和排查 Node.js 应用的内存泄漏？有哪些常用的工具和方法？（如 heapdump、Chrome DevTools、&lt;code&gt;process.memoryUsage()&lt;/code&gt; 等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见泄漏点&lt;/strong&gt;：Node.js 中常见的内存泄漏场景有哪些？（如全局变量、闭包、事件监听器、定时器等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 密集任务&lt;/strong&gt;：如果需要在 Node.js 中处理 CPU 密集型任务（如图片处理、大数据计算），有哪些优化方案？（如 Worker Threads、子进程、任务队列等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：您在项目中遇到过类似的性能问题吗？如何解决的？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;架构设计部分（1题）&lt;/h2&gt;
&lt;h3&gt;第9题：微前端架构设计&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：架构能力、技术选型、问题解决&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
您的公司有多个前端团队，分别负责不同的业务模块（订单、库存、财务、用户中心），技术栈混杂（Vue2、Vue3、React）。现在需要将这些模块整合到一个统一的后台管理系统中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需求&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;各模块独立开发、独立部署&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持 Vue2、Vue3 混合运行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;共享公共依赖（如 Vue、Element UI）以减小包体积&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主应用负责路由、权限、布局&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术方案&lt;/strong&gt;：微前端的实现方案有哪些？（如 qiankun、Micro-App、Module Federation、iframe）您会选择哪个？为什么？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心挑战&lt;/strong&gt;：微前端架构的核心技术难点是什么？（如 JS 沙箱隔离、样式隔离、应用间通信）如何解决？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战考量&lt;/strong&gt;：Vue2 和 Vue3 共存时有什么注意事项？如何避免版本冲突？&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;团队管理部分（1题）&lt;/h2&gt;
&lt;h3&gt;第10题：团队管理与技术债务&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;考察点&lt;/strong&gt;：管理能力、技术推动、团队协作&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景描述&lt;/strong&gt;：&lt;br&gt;
作为高级前端开发/技术负责人，您不仅要写代码，还要带团队、推动项目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：&lt;br&gt;
您接手一个老项目，代码质量堪忧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;没有代码规范，每个人的代码风格完全不同&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有单元测试，改代码如履薄冰&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;技术栈老旧（Vue2 + Webpack4），但业务压力大，没时间重构&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;团队成员水平参差不齐，Code Review 流于形式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术债务平衡&lt;/strong&gt;：业务需求紧急 vs 技术重构，如何平衡？您会采取什么策略？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队规范落地&lt;/strong&gt;：如何推动代码规范、ESLint、Prettier、Git Commit 规范在团队中落地？（而不是仅仅写个文档）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Review 文化&lt;/strong&gt;：如何让 Code Review 真正发挥作用，而不是走形式？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战经验&lt;/strong&gt;：分享一个您在团队管理或技术推动中遇到的难题，以及您的解决方案。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;评分标准&lt;/h2&gt;
&lt;p&gt;每题满分 &lt;strong&gt;10分&lt;/strong&gt;，总分 &lt;strong&gt;100分&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分数段&lt;/th&gt;
&lt;th&gt;评级&lt;/th&gt;
&lt;th&gt;标准&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10-9分&lt;/td&gt;
&lt;td&gt;优秀&lt;/td&gt;
&lt;td&gt;深度理解原理 + 丰富实战经验 + 有独特见解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-7分&lt;/td&gt;
&lt;td&gt;良好&lt;/td&gt;
&lt;td&gt;理解原理 + 有实战经验 + 回答完整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6-5分&lt;/td&gt;
&lt;td&gt;及格&lt;/td&gt;
&lt;td&gt;基本理解 + 有一定经验 + 回答尚可&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-3分&lt;/td&gt;
&lt;td&gt;不及格&lt;/td&gt;
&lt;td&gt;理解片面 + 经验不足 + 回答欠缺&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-1分&lt;/td&gt;
&lt;td&gt;差&lt;/td&gt;
&lt;td&gt;不理解原理 + 无实战经验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0分&lt;/td&gt;
&lt;td&gt;跳过/无回答&lt;/td&gt;
&lt;td&gt;候选人跳过此题或无法回答&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;总体评级参考&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;总分范围&lt;/th&gt;
&lt;th&gt;评级&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;90-100分&lt;/td&gt;
&lt;td&gt;A+&lt;/td&gt;
&lt;td&gt;优秀，强烈推荐录用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80-89分&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;良好，推荐录用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70-79分&lt;/td&gt;
&lt;td&gt;B+&lt;/td&gt;
&lt;td&gt;合格，可以录用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60-69分&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;基本合格，需权衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50-59分&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;不太合格，不建议录用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;#x3C;50分&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;不合格，不建议录用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;h3&gt;面试官须知&lt;/h3&gt;
&lt;ol&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;/ol&gt;
&lt;h3&gt;候选人须知&lt;/h3&gt;
&lt;ol&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;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;本面试题目清单配套文档&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;01-interview-prompt-template.md&lt;/code&gt; - 面试提示词模板&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;02-interview-questions.md&lt;/code&gt; - 本文档&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;03-interview-standard-answers.md&lt;/code&gt; - 标准答案参考&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;04-interview-assessment-report.md&lt;/code&gt; - 候选人评估报告&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[20分钟重构5000行代码：AI让编程成本趋近于零]]></title><description><![CDATA[最近几天感受到SKILL带来的便利之后，发现其核心能力除了本身渐进式披露的特性之外，scripts   部分可以大大拓展其能力边界，使其产出稳定可靠的产物。]]></description><link>https://zzao.club/post/daily/20-min-rebuild-5000-lines-code</link><guid isPermaLink="true">https://zzao.club/post/daily/20-min-rebuild-5000-lines-code</guid><pubDate>Tue, 20 Jan 2026 00:18:44 GMT</pubDate><content:encoded>&lt;p&gt;最近几天感受到SKILL带来的便利之后，发现其核心能力除了本身渐进式披露的特性之外，&lt;code&gt;scripts &lt;/code&gt;  部分可以大大拓展其能力边界，使其产出稳定可靠的产物。&lt;/p&gt;
&lt;p&gt;虽然很多大佬提供了很多优秀的 SKILL 可以直接用，但是他们的 SKILL 也是AI构建的，&lt;strong&gt;公布的成品仅仅是AI对他们脑子里想法的实现&lt;/strong&gt;，一旦他们的工作流发生变化，SKILL也会跟着发生变化。&lt;/p&gt;
&lt;p&gt;所以你要么和别人步伐完全一致，要么就要自建自己的SKILL、脚本。&lt;/p&gt;
&lt;p&gt;而且不要觉得重建自己的工具是多么麻烦的一件事，不管是谁，都是用AI编写的。不会编程的，用自然语言也能写好SKILL。会编程的，用AI写出的脚本会更贴合自己的需求，并且可以随着自己工作流的进化而逐渐丰富。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;做什么东西都考虑成本的思维在这个时代真的需要转换一下，写工具已经近乎没有成本了。现在是需要把自己脑子里的东西倾泻，然后落地。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以我重构&lt;code&gt;@zzclub/z-cli&lt;/code&gt;项目为例，分享一下最近使用&lt;code&gt;opencode&lt;/code&gt;时的一些感受。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;永远先Plan再Build&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在使用&lt;code&gt;vscode&lt;/code&gt;里的&lt;code&gt;Github Copilot&lt;/code&gt;时，它已经有了&lt;code&gt;Plan&lt;/code&gt;模式，但它明显不是一个真实的Plan模式，仅仅是把Agent的编辑能力给禁用了，Agent在得到指令后会马上想去执行，而不是真的给我做计划，然后发现自己不能编辑，再回头把自己要写的东西先输出给我。多少有点鸡肋。&lt;/p&gt;
&lt;p&gt;如果直接&lt;code&gt;Build&lt;/code&gt;，经常会出现它做了你意料之外的工作，然后你不得不再补充一句：&lt;strong&gt;我不是让你xxx，你怎么xxx，给我xxx&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以只有先让Agent输出它的Plan，你才能提前发现它的错误，从而避免一轮又一轮的纠错。&lt;/p&gt;
&lt;p&gt;但这就对非开发者有点影响，因为Plan通常比较详细，如果你看不懂代码，仅靠文字描述，很有可能没能发现AI已经“走错了路”&lt;/p&gt;
&lt;p&gt;所以，作为全栈开发者，又能非常清晰的输出自己的架构，在&lt;code&gt;Plan&lt;/code&gt;阶段就可以纠正绝大部分错误。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;另外，&lt;strong&gt;架构并不是指细节到代码里的每个类负责什么都告诉AI&lt;/strong&gt;，AI本身的编码能力就强于高级开发，只需要从更高的维度用自然语言表述自己的意图即可。&lt;/p&gt;
&lt;p&gt;只要不是简单的一句我想要什么什么，而是&lt;strong&gt;把自己的产品/脚本语言、背景、用途、使用场景交待清楚&lt;/strong&gt;，相信顶级的大模型都能输出一个优秀的技术架构。&lt;/p&gt;
&lt;p&gt;我的老项目基于&lt;code&gt;JavaScript&lt;/code&gt;，包含 &lt;code&gt;chalk&lt;/code&gt;、&lt;code&gt;commander&lt;/code&gt;、&lt;code&gt;inquirer&lt;/code&gt;、&lt;code&gt;ora&lt;/code&gt;、&lt;code&gt;shelljs&lt;/code&gt;这些依赖包。是一个之前服务于公司内部项目的一些小工具，功能包括：i18n、翻译api、图片压缩等。&lt;/p&gt;
&lt;p&gt;首先我让AI&lt;strong&gt;分析项目&lt;/strong&gt;，输出包含全部功能的文档到docs目录下，方便后续它根据此文档来重构。&lt;/p&gt;
&lt;p&gt;然后&lt;strong&gt;明确要求&lt;/strong&gt;AI使用 &lt;strong&gt;Bun+TypeScript&lt;/strong&gt; 重构，核心依赖包改为 &lt;code&gt;consola&lt;/code&gt;，并附上GitHub地址让AI去读文档。&lt;/p&gt;
&lt;p&gt;再结合新技术栈和功能文档，以及此项目要达成的什么效果，比如兼容目前的什么主流应用、自己的独立应用，支持用户使用npx直接调用等等，输出一份&lt;strong&gt;重构文档&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;重构文档包含了所有它完整的架构方案、目录结构以及一些代码实例，**此时就可以仔细审查，然后指出它的错误，直到整个文档都没问题。**这一步你指不出错误，就不要怪后面它再出错了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;确认无误后切换到Build模式，开始工作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;这次长任务一共修改了&lt;strong&gt;45&lt;/strong&gt;个文件，近&lt;strong&gt;5000&lt;/strong&gt;行代码，中间经过了它自己的自动测试和纠错。在完成后，我没有发现任何错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从Plan到Build，用时不到20分钟。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;完成后我可以生成一个 &lt;code&gt;compress-image&lt;/code&gt; SKILL，里面明确指定调用  &lt;code&gt; z tiny -f {目标文件或目录} -r --output {输出目录} &lt;/code&gt;  来完成图片压缩任务。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;z 是此 cli 的别名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个CLI工具既可以单独使用，也可以让SKILL明确调用，并且它一定完全适合自己。如果有其他应用想调用，自己也可以很方便的对接。&lt;/p&gt;
&lt;p&gt;用别人用AI写的SKILL就会陷入：AI读AI写的，AI再自己对接上个AI写的，中间只有你啥都不知道。&lt;/p&gt;
&lt;p&gt;别人可能是用各种方式（各种脚本语言）实现的SKILL，并不一定适合你，并且一旦改动，也不可能要求作者单独再教你。&lt;/p&gt;
&lt;p&gt;所以还是尽早学着创建自己的SKILL吧。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[能力强的人用AI更强]]></title><description><![CDATA[最近 Anthropic 发布了第四期 Economic Index 报告，基于 200 万次 Claude 对话数据（100 万来自 Claude.ai，100 万来自 API），引入了"经济原语"这个新的分析框架。]]></description><link>https://zzao.club/post/daily/people-strong-then-ai-strong</link><guid isPermaLink="true">https://zzao.club/post/daily/people-strong-then-ai-strong</guid><pubDate>Mon, 19 Jan 2026 13:50:00 GMT</pubDate><content:encoded>&lt;p&gt;最近 Anthropic 发布了第四期 Economic Index 报告，基于 200 万次 Claude 对话数据（100 万来自 Claude.ai，100 万来自 API），引入了&quot;经济原语&quot;这个新的分析框架。&lt;/p&gt;
&lt;p&gt;这次报告给出了一个和大众认知相反的结论。&lt;/p&gt;
&lt;p&gt;很多人以为 AI 会先从流水线和客服这些简单工作开始替代，知识工作者暂时还安全。&lt;/p&gt;
&lt;p&gt;但真实情况是：&lt;strong&gt;理解提示词所需的教育水平越高，AI 带来的任务时长缩减就越明显。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换句话说，AI 正在从知识工作的核心地带切入，而不是从边缘蚕食。&lt;/p&gt;
&lt;h2&gt;复杂任务，反而跑得更快&lt;/h2&gt;
&lt;p&gt;官方数据显示了一个反直觉的现象：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;任务越复杂，AI 带来的提速效果越明显。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;即使考虑到复杂任务的成功率更低这个因素，这个结论依然成立。&lt;/p&gt;
&lt;p&gt;具体来说，需要更高教育水平才能理解的任务，AI 能节省的时间反而更多。但复杂任务的成功率确实更低一些，说明了一件事：AI 在高难度任务上的表现更不稳定。&lt;/p&gt;
&lt;p&gt;你用 AI 帮你写代码、做分析、起草方案，每次都像在抛硬币——这次能不能靠谱完成，谁也不知道。&lt;/p&gt;
&lt;h2&gt;会问的人，得到更多&lt;/h2&gt;
&lt;p&gt;报告发现了一个有趣的现象：&lt;strong&gt;用户输入的表达水平和 AI 回复的表达水平，呈现高度相关性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;说人话就是：你用什么水平的语言问问题，AI 就用什么水平的语言回答你。&lt;/p&gt;
&lt;p&gt;这不是 AI 在刻意迁就你，而是它的训练方式决定的。Claude 会自动匹配用户的表达层次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以，原本架构能力更强的开发者，用同样的 AI，效率反而更高。&lt;/strong&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;现在这种能力同样体现在和 AI 的沟通上。你能不能写出高质量的 Prompt，能不能把复杂问题拆解成 AI 能理解的小步骤，直接决定了 AI 能给你多大的帮助。&lt;/p&gt;
&lt;p&gt;但这并不意味着非技术背景的人就没机会了。&lt;/p&gt;
&lt;p&gt;事实上，很多没有编程背景的人，通过 AI 已经做出了不少有用的 App、工具、甚至小产品。&lt;/p&gt;
&lt;p&gt;关键在于：&lt;strong&gt;你能不能清晰地表达你想要什么。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不一定要懂算法、懂数据结构，但你得能把你的需求、你的逻辑、你的期望，用准确的语言描述出来。&lt;/p&gt;
&lt;p&gt;这种能力以前可能叫&quot;产品思维&quot;，现在可以叫&quot;Prompt 工程&quot;，但本质上都是同一件事：&lt;strong&gt;把脑子里模糊的想法，翻译成可执行的指令。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;多轮对话才是关键&lt;/h2&gt;
&lt;p&gt;官方数据揭示了另一个现象：&lt;/p&gt;
&lt;p&gt;Claude.ai（网页端）和 API 调用的成功率差异明显，前者显著高于后者。&lt;/p&gt;
&lt;p&gt;差在哪？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多轮对话。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;网页端用户可以纠错、引导、迭代。AI 给的第一版不满意，你可以追问、可以补充细节、可以调整方向。&lt;/p&gt;
&lt;p&gt;而 API 调用通常是&quot;一锤子买卖&quot;，提交请求就等结果，没有纠错机会。&lt;/p&gt;
&lt;p&gt;API 调用的数据显示，Claude 在约 3.5 小时的任务上能达到 50% 成功率。但在 Claude.ai 上，即使是更长时间的任务，成功率依然保持在较高水平。&lt;/p&gt;
&lt;p&gt;原因就是多轮对话把复杂任务分解成了小步骤，每一轮都有机会纠偏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;人类的引导能力，仍然是关键变量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这也解释了为什么架构能力强的开发者用 AI 更高效：他们知道怎么把大问题拆成小问题，知道每一步应该问什么、验证什么。&lt;/p&gt;
&lt;h2&gt;白领工作正在被&quot;掏空&quot;&lt;/h2&gt;
&lt;p&gt;官方报告提到了一个&quot;去技能化&quot;(deskilling) 的效应：&lt;/p&gt;
&lt;p&gt;因为 Claude 倾向于覆盖那些需要更高技能水平的任务，如果这些任务被自动化，员工可能会被留下更多常规性工作。&lt;/p&gt;
&lt;p&gt;当然，报告也提醒说，这个假设建立在&quot;自动化会缩减工作的这些方面&quot;的前提上，实际上工作可能会以其他方式演变。&lt;/p&gt;
&lt;p&gt;但如果顺着这个逻辑想，会发生什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术写作员&lt;/strong&gt;：AI 接管了分析、审稿、内容生成这些核心工作，人类可能只剩下画图和排版。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;旅行代理&lt;/strong&gt;：AI 做行程规划、比价、推荐，人类只剩打票收款。以前需要丰富经验才能做好的工作，现在变成了简单的执行角色。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教师&lt;/strong&gt;：AI 能帮忙批改作业、做研究、准备教案，这些恰恰是教师工作中最需要专业知识的部分。剩下的主要是课堂管理和面对面互动。&lt;/p&gt;
&lt;p&gt;问题不在于失业，而在于&lt;strong&gt;工作内容的降级&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你可能仍然有工作，但做的事情变得更琐碎、更机械。&lt;/p&gt;
&lt;p&gt;当然也有例外。&lt;/p&gt;
&lt;p&gt;比如房产经理：AI 接管了记账、文档管理这些低端任务，人类可以专注于合同谈判和客户关系，工作内容反而更有价值。&lt;/p&gt;
&lt;p&gt;关键在于：&lt;strong&gt;AI 接管的是不是你工作中最核心的那部分？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这份报告最重要的发现，不是 AI 能做什么，而是&lt;strong&gt;谁能更好地用 AI&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;能力强的人，用 AI 之后更强。&lt;/p&gt;
&lt;p&gt;会表达的人，从 AI 那里得到更多。&lt;/p&gt;
&lt;p&gt;AI 时代不光是淘汰，更多的是机会。&lt;/p&gt;
&lt;p&gt;但前提是：&lt;strong&gt;你得能说清楚你想要什么。&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[没用的旧代码不要丢，放到SKILL里继续用]]></title><link>https://zzao.club/post/daily/change-your-old-code-into-skill</link><guid isPermaLink="true">https://zzao.club/post/daily/change-your-old-code-into-skill</guid><pubDate>Thu, 15 Jan 2026 19:31:33 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;没用的旧代码不要丢，也不要放在转转上回收😄&lt;/p&gt;
&lt;p&gt;放到&lt;code&gt;SKILL&lt;/code&gt;里当做一个脚本来用说不定会更合适。&lt;/p&gt;
&lt;h2&gt;SKILL&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;SKILL&lt;/code&gt;是AI编程助手（如OpenCode）中的一种可复用工作流机制。它诞生的背景是：传统的Prompt工程中，开发者需要反复编写相似的指令和上下文说明，既低效又容易遗漏细节。SKILL将&quot;意图描述 + 详细要求 + 工具脚本&quot;打包成一个可被AGENT智能识别和调用的单元，让过往积累的代码、脚本和工作流程得以在AI时代继续发挥价值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/illustrations/skill-reuse-old-code/illustration-skill-concept.png&quot; alt=&quot;SKILL 概念图&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SKILL&lt;/code&gt;从前端的概念上来理解，类似于一个&lt;code&gt;composable&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它可以是一段写好的Prompt，也可以有单个脚本（函数），也可以是多个脚本，所以比单纯的&lt;strong&gt;函数复用&lt;/strong&gt;要更灵活和多变一些。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/illustrations/skill-reuse-old-code/illustration-composable-concept.png&quot; alt=&quot;SKILL 像 Composable&quot;&gt;&lt;/p&gt;
&lt;p&gt;那它和&lt;code&gt;MCP&lt;/code&gt;（MCP只是协议，但下文MCP同时指代背后的服务）有什么区别呢&lt;/p&gt;
&lt;p&gt;MCP就类似于一张「入场券」，仅仅是提供给&lt;strong&gt;Agent&lt;/strong&gt;调用此服务的接口。&lt;/p&gt;
&lt;p&gt;在调用&lt;code&gt;MCP&lt;/code&gt;时，往往需要你先写一段&lt;code&gt;Prompt&lt;/code&gt;，交待背景、角色、要做什么，然后AGENT再去调用MCP，拿到结果，按你说的要求整理和执行下一步动作。&lt;/p&gt;
&lt;p&gt;SKILL好就好在，它可以记住你这一套操作，是真正的复用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/illustrations/skill-reuse-old-code/illustration-mcp-vs-skill.png&quot; alt=&quot;MCP vs SKILL 流程对比&quot;&gt;&lt;/p&gt;
&lt;p&gt;在执行一项任务前，你的&lt;strong&gt;意图&lt;/strong&gt;就是这个&lt;code&gt;SKILL&lt;/code&gt;的&lt;code&gt;description&lt;/code&gt;。你的&lt;strong&gt;要求&lt;/strong&gt;就是它的&lt;code&gt;SKILL.md&lt;/code&gt;。你的&lt;strong&gt;MCP&lt;/strong&gt;，也就是那个&lt;strong&gt;输出结果明确的服务或脚本&lt;/strong&gt;，同样可以用在&lt;code&gt;SKILL/scripts&lt;/code&gt;里。&lt;/p&gt;
&lt;p&gt;所以，以前对自己有用的服务、脚本，到今天依然可用。仅仅是换了一种调用方式&lt;/p&gt;
&lt;h2&gt;职能细化&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;opencode&lt;/code&gt;/&lt;code&gt;claudecode&lt;/code&gt; 中&lt;code&gt;SKILL&lt;/code&gt;的调用方式，类比传统的服务/古法编程，从明确的函数调用、接口请求，变成了AGENT语义理解并调用&lt;code&gt;SKILL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统：后端提供接口 =&gt; &lt;a href=&quot;https://api.example.com/get/xxx&quot;&gt;https://api.example.com/get/xxx&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SKILLS: SKILL/description&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当给&lt;code&gt;AGENT/Primary&lt;/code&gt;阐述我们的要求时，它会理解和规划你的要求，并且查看SKILLS里的&lt;strong&gt;description&lt;/strong&gt;是否在你的要求内，如果有SKILL符合，则使用SKILL来完成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;所以Description就是给AGENT的接口文档&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/illustrations/skill-reuse-old-code/illustration-description-as-api.png&quot; alt=&quot;Description 就是接口文档&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时SKILL里的SKILL.md刚刚被加载到上下文，它就会开始执行这个SKILL里的任务，所以你在这个SKILL.md才是你最详细的要求，以及完成后要做什么。而不是一开始就发一大段&lt;code&gt;Prompt&lt;/code&gt;，发现有问题之后，再说你哪里哪里理解的不对，是这样那样。&lt;/p&gt;
&lt;p&gt;多个SKILL之间可以在其内部的任务要求中达到串联协作，或者通过一个SKILL把其他SKILL串联。注意，&lt;strong&gt;串联的前提是AGENT能够从你的任务描述中理解应该使用哪个具体的SKILL。。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Vscode&lt;/code&gt;中用&lt;code&gt;Github Copilot&lt;/code&gt;编程时，大模型经常会犯一些无语的错误。&lt;/p&gt;
&lt;p&gt;比如，总是要生成文档。写个东西，二话不说文档先写一长串，或者是做完用文档总结一下。当然，这只是某些模型的特点。&lt;/p&gt;
&lt;p&gt;再比如，写完代码，格式乱了、错行了，TS类型错了。这种错误经常出现。它不知道用项目自带的&lt;code&gt;lint&lt;/code&gt;命令修复，哪怕你写到某个它默认会携带到上下文里的md文档里。因为一个窗口做的任务太多了，不知道哪次对话就把某个要求给丢了。&lt;/p&gt;
&lt;p&gt;这些错误在我使用了&lt;code&gt;opencode&lt;/code&gt; + &lt;code&gt;ohmyopencode&lt;/code&gt; 之后就很少出现了。原因应该也是职能的细化，使其大模型工作效率提高了不少。工具的进化也让上下文问题优化了许多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以，学的慢学的晚也不是坏事了，眼一闭一睁，问题已经在源头被解决了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;能力扩展&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SKILL&lt;/code&gt;放在代码里仅仅是提高准确性和复用率，（个人感觉）放在非编程领域可能会更好使一些。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;SKILL&lt;/code&gt;之前，普遍用&lt;code&gt;workflow&lt;/code&gt;来编排自己的各种任务，（比如n8n）以达到自动化的效果。这让原本就有赚钱业务的人更加赚钱。&lt;/p&gt;
&lt;p&gt;SKILL的能力不在workflow之下。&lt;/p&gt;
&lt;p&gt;假如发一篇文章原本的模式是这样的：&lt;/p&gt;
&lt;p&gt;先让A开发一个脚本、服务，完成抓取数据、处理数据等需求。然后由A把数据发给B，B负责润色文案，交给C去处理文章配图，最后交给D勘误和审核，然后再进行多平台发布。&lt;/p&gt;
&lt;p&gt;有个AI之后，A肯定是不要了，大模型写出来的更快更好。B也不要了，AI自己润色。C也不要了，AI生图。D也不要了，AI勘误。多平台发布，以前就是聚合平台，现在还继续用。&lt;/p&gt;
&lt;p&gt;所以只需要你一个人，把ABCD之间的调度（使用自然语言）粘合在一起。ABCD要做什么是固定的，产出也是固定的。你也不必把他们所有的工作职能都放在自己这，只需要明确：**进行到哪一步，去找谁解决。**最后再配个监理，持续敲打。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/illustrations/skill-reuse-old-code/illustration-workflow-simplified.png&quot; alt=&quot;工作流简化对比&quot;&gt;&lt;/p&gt;
&lt;p&gt;你看，非编程领域是不是可以先一步下岗了。😄&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;一点SKILL的小思考&lt;/p&gt;
&lt;p&gt;🔚&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Dan Koe的三级表达框架]]></title><link>https://zzao.club/post/tech-news/x/dan-doe-writing-guide</link><guid isPermaLink="true">https://zzao.club/post/tech-news/x/dan-doe-writing-guide</guid><pubDate>Thu, 15 Jan 2026 17:06:05 GMT</pubDate><content:encoded>&lt;h2&gt;搬运自 &lt;a href=&quot;https://x.com/JamesAI/status/2011899259403321780?s=20&quot;&gt;在悉尼和稀泥@X&lt;/a&gt;&lt;/h2&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;记住这两步：  先攒8-10个自己的核心观点反复打磨。 再按“提痛点-给方案”“结论先行”或“跨领域融合”的框架输出。&lt;/p&gt;
&lt;p&gt;不用逼自己说新话，把好观点讲透，就是最高级的表达。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[千问整合了全阿里生态，为什么唯独少了闲鱼？]]></title><description><![CDATA[最近阿里发布了全新的千问，打通了淘宝、飞猪、高德、支付宝一系列生态服务。]]></description><link>https://zzao.club/post/daily/qianwen-why-not-xianyu</link><guid isPermaLink="true">https://zzao.club/post/daily/qianwen-why-not-xianyu</guid><pubDate>Thu, 15 Jan 2026 08:35:00 GMT</pubDate><content:encoded>&lt;p&gt;最近阿里发布了全新的千问，打通了淘宝、飞猪、高德、支付宝一系列生态服务。&lt;/p&gt;
&lt;p&gt;你可以直接语音说&quot;订个周末去杭州的酒店&quot;，千问帮你搞定。想吃什么，它帮你点外卖。要去哪，它帮你规划路线叫车。&lt;/p&gt;
&lt;p&gt;这个体验确实很丝滑。AI 助手终于从&quot;聊天工具&quot;变成了&quot;超级入口&quot;。&lt;/p&gt;
&lt;p&gt;但我翻了一圈发现：&lt;strong&gt;闲鱼没在里面。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;闲鱼的尴尬定位&lt;/h2&gt;
&lt;p&gt;闲鱼在阿里生态里，一直是个有点特殊的存在。&lt;/p&gt;
&lt;p&gt;表面上看，它是个二手交易平台。但实际上呢？现在上面卖全新商品的、代购的、倒卖优惠券的，比真正卖二手的还多。&lt;/p&gt;
&lt;p&gt;这就很微妙了。&lt;/p&gt;
&lt;p&gt;淘宝、天猫是正价渠道，闲鱼成了&quot;价格洼地&quot;。同一个商品，淘宝 299，闲鱼有人 249 代付。用户用脚投票，流量往低价那边走。&lt;/p&gt;
&lt;p&gt;千问要整合阿里生态，飞猪、高德、支付宝都好说，大家各司其职，没什么冲突。&lt;/p&gt;
&lt;p&gt;但闲鱼不一样。&lt;strong&gt;它天然就和其他购物渠道存在价格竞争关系。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;普通人要的是什么？&lt;/h2&gt;
&lt;p&gt;说白了，普通人用闲鱼就图一个字：&lt;strong&gt;便宜&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你想买个东西，打开淘宝，原价 299。然后习惯性打开闲鱼搜&quot;XX 优惠券&quot;，发现有人 248 包邮代付，省了 50 块。&lt;/p&gt;
&lt;p&gt;这时候一个问题就冒出来了：&lt;strong&gt;AI 助手能帮我拿到这个 248 的价格吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果不能，那 AI 只是帮我在淘宝下了个 299 的原价单，我还是得自己去闲鱼翻一圈。所谓的&quot;便捷&quot;，没解决核心需求。&lt;/p&gt;
&lt;p&gt;更关键的是，闲鱼上那些低价，往往不在淘宝的优惠券体系里。&lt;/p&gt;
&lt;p&gt;淘宝的券是满减券、品类券、店铺券、跨店券、直播券……一套复杂的分发机制。但闲鱼的折扣、代付、代购，是在这套体系之外的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不管 AI 多聪明，拿不到闲鱼的价格，就不算真正理解了&quot;我想省钱&quot;这个意图。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;谁为复杂度买单？&lt;/h2&gt;
&lt;p&gt;以前电商平台搞优惠券，设计得很复杂：凑单、定时领、蹲直播。&lt;/p&gt;
&lt;p&gt;复杂度的背后，是&quot;精准分发流量&quot;、&quot;提升用户粘性&quot;的产品逻辑。用户要为了那几块钱优惠，多点几次、多逛几家、多停留几分钟。&lt;/p&gt;
&lt;p&gt;现在 AI 来了，用户可以直接说&quot;买 XX&quot;，AI 自动下单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那以前那套复杂度的代价，谁来承担？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;平台设计了半天的凑单机制，用户根本不需要了解，AI 直接算好。定时领券、蹲直播这些，也都可以交给 AI 去做。&lt;/p&gt;
&lt;p&gt;用户爽了，AI 变成了超级入口，但平台原本想通过&quot;复杂度&quot;换来的用户时长、GMV 增长，现在全被 AI 这个中间层吃掉了。&lt;/p&gt;
&lt;h2&gt;更开放还是更封闭？&lt;/h2&gt;
&lt;p&gt;千问整合阿里系 app，这是个大趋势。不光阿里，各家大厂都在做类似的事。&lt;/p&gt;
&lt;p&gt;对用户来说，确实方便了。一句话搞定订酒店、叫车、点外卖。&lt;/p&gt;
&lt;p&gt;但问题是：&lt;strong&gt;AI 会帮你比价吗？会告诉你闲鱼有更便宜的吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果不会，那这个&quot;超级入口&quot;其实是在收窄选择。以前你至少还会主动打开闲鱼比价，现在 AI 直接帮你下单，你可能根本不知道还有更便宜的渠道。&lt;/p&gt;
&lt;p&gt;更进一步说，如果每个生态都这么玩，阿里有千问，腾讯有腾讯元宝，字节有豆包……每个 AI 只整合自家 app，不跨平台比价。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那整个互联网生态，是更开放了，还是更封闭了？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对普通用户来说，AI 整合 app 带来的&quot;便捷&quot;，可能是以&quot;价格透明度&quot;为代价的。&lt;/p&gt;
&lt;p&gt;你看不到更多选择，AI 也不会主动告诉你。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;从闲鱼这个案例能看出来，AI 整合 app 这件事，没那么简单。&lt;/p&gt;
&lt;p&gt;它不只是技术问题，更是利益、定位、用户需求的博弈。&lt;/p&gt;
&lt;p&gt;AI 会帮用户省钱，还是只是让下单更快？会打破平台壁垒，还是强化生态封闭？&lt;/p&gt;
&lt;p&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;&lt;strong&gt;你怎么看？&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Milkdown神奇的Bug]]></title><link>https://zzao.club/post/daily/milkdown-amazing-bug</link><guid isPermaLink="true">https://zzao.club/post/daily/milkdown-amazing-bug</guid><pubDate>Wed, 14 Jan 2026 01:41:34 GMT</pubDate><content:encoded>&lt;h2&gt;🤔&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;zotepad&lt;/code&gt;中把核心编辑器&lt;code&gt;从md-editor-v3&lt;/code&gt;迁移成&lt;code&gt;milkdown&lt;/code&gt;之后，体验感好了不少。&lt;/p&gt;
&lt;p&gt;界面干干净净，&lt;code&gt;md&lt;/code&gt;&lt;strong&gt;实时渲染&lt;/strong&gt;，也不用单独预览了。&lt;/p&gt;
&lt;p&gt;但是有一个&lt;strong&gt;bug&lt;/strong&gt;我一直没解决：&lt;/p&gt;
&lt;p&gt;如果打开新文章直接写字，在选中文本弹出&lt;code&gt;milkdown&lt;/code&gt;的&lt;code&gt;toolbar&lt;/code&gt;时，页面会&lt;strong&gt;抖动&lt;/strong&gt;一下。应该是什么元素导致内容区域一下子变高了。&lt;/p&gt;
&lt;p&gt;但是我尝试了很多解决办法都没法解决。&lt;/p&gt;
&lt;p&gt;今天在体验&lt;code&gt;opencode + skills&lt;/code&gt; 意外发现用于测试的那篇文章没抖动，当时我还在心想，真奇怪，竟然打了个新的安装包就好了？&lt;/p&gt;
&lt;p&gt;然后我在开始写另一篇文章时，发现又产生了抖动，于是我又回到了不抖动的那篇文章，发现它还是不抖动。它俩的唯一区别就是好的文章有个&lt;code&gt;H2&lt;/code&gt;标题，于是我给这篇文章也加了个&lt;code&gt;H2&lt;/code&gt;标题就好了。&lt;/p&gt;
&lt;p&gt;然后我又尝试了加入 引用、分割线等，都是可以的，只要不直接写文字都可以。&lt;/p&gt;
&lt;p&gt;神奇的bug😂&lt;/p&gt;
&lt;p&gt;有没有知道原因的，评论区求教&lt;/p&gt;</content:encoded></item><item><title><![CDATA[opencode skills]]></title><link>https://zzao.club/post/daily/opencode-skills</link><guid isPermaLink="true">https://zzao.club/post/daily/opencode-skills</guid><pubDate>Wed, 14 Jan 2026 00:03:39 GMT</pubDate><content:encoded>&lt;h2&gt;⚠️测试文章&lt;/h2&gt;
&lt;p&gt;新增了一个 blog-publisher skill，用于在任何窗口使用 opencode 进行本地文章的发布&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;li&gt;生成配图&lt;/li&gt;
&lt;li&gt;调用zotepad上传图片&lt;/li&gt;
&lt;li&gt;调用zotepad发布到公众号草稿箱&lt;/li&gt;
&lt;li&gt;调用zotepad保存文章到本地博客目录&lt;/li&gt;
&lt;li&gt;blog-publisher发布&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果是无需确认的发布流程，比如新闻类文章，可以直接发布到博客，因为自由度较高，可以随时更改。&lt;br&gt;
如果需要确认的则不能直接发到公众号平台。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[小红书风格图片提示词]]></title><description><![CDATA[来自于 宝玉@X]]></description><link>https://zzao.club/post/daily/red-book-style-image-prompty</link><guid isPermaLink="true">https://zzao.club/post/daily/red-book-style-image-prompty</guid><pubDate>Mon, 12 Jan 2026 21:40:20 GMT</pubDate><content:encoded>&lt;p&gt;来自于 &lt;a href=&quot;https://x.com/dotey/status/2010497572704501766?s=20&quot;&gt;宝玉@X&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;角色定义&lt;/h1&gt;
&lt;p&gt;你是一位专业的小红书视觉内容策划师，擅长将复杂内容拆解为吸引眼球的卡通风格系列信息图。&lt;/p&gt;
&lt;h1&gt;任务&lt;/h1&gt;
&lt;p&gt;请分析以下输入内容，将其拆解为 1-10 张小红书风格的系列信息图，并为每张图片输出独立的生成提示词。&lt;/p&gt;
&lt;h1&gt;拆解原则&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;封面图（第1张）&lt;/strong&gt;：必须有强烈视觉冲击力，包含核心标题和吸引点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容图（中间）&lt;/strong&gt;：每张聚焦1个核心观点，信息密度适中&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结尾图（最后1张）&lt;/strong&gt;：总结/行动号召/金句收尾&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;图片数量判断标准&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;简单观点/单一主题：2-3 张&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中等复杂度/教程类：4-6 张&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;深度干货/多维度分析：7-10 张&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;视觉风格规范（重要）&lt;/h1&gt;
&lt;h2&gt;基础设定&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;图片类型&lt;/strong&gt;：信息图（Infographic）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;方向比例&lt;/strong&gt;：竖版，3:4 或 9:16&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;整体风格&lt;/strong&gt;：卡通风格、手绘风格&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;背景与配色&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;背景色：莫兰迪色系 / 奶油色 / 米白色 / 浅粉 / 薄荷绿等温柔色调&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配色柔和统一，符合小红书审美&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;文字风格&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;必须使用手绘风格文字&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大标题突出醒目，重点文字加粗放大&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可使用荧光笔划线效果强调关键词&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;禁止使用写实风格字体&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;装饰元素&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加入少量简洁的&lt;strong&gt;卡通元素、图标或名人画像&lt;/strong&gt;，增强趣味性和视觉记忆&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可使用：emoji 风格图标、手绘贴纸、便签纸质感、对话气泡等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如涉及敏感人物或版权内容，画一个风格相似的卡通替代形象，不要拒绝生成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;所有图像元素必须是手绘/卡通风格，禁止写实风格图画&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;排版原则&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;信息精简，突出关键词与核心概念&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多留白，易于一眼抓住重点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要点分条呈现，层次清晰&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;输出格式&lt;/h1&gt;
&lt;p&gt;对于每张图片，请按以下结构输出：&lt;/p&gt;
&lt;p&gt;第 X 张 / 共 N 张&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图片定位&lt;/strong&gt;：[封面图 / 内容图 / 结尾图]&lt;br&gt;
&lt;strong&gt;核心信息&lt;/strong&gt;：[这张图要传达的1句话核心]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文字内容&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主标题：xxx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;副标题/要点：xxx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;补充说明（如有）：xxx&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;视觉提示词&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;小红书风格信息图，竖版（3:4），卡通风格，手绘风格文字，[具体背景色]背景。  [具体内容布局描述]  加入简洁的卡通元素和图标增强趣味性和视觉记忆：[具体元素描述]  整体风格：手绘、可爱、清新，信息精简，多留白，重点突出。所有图像和文字均为手绘风格，无写实元素。 右下角水印：“宝玉”&lt;/p&gt;
&lt;h1&gt;语言规则&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;除非特别要求，输出语言与输入内容语言保持一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中文内容使用全角标点符号（“”，。！）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;此文使用opencode skill 发布&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[是时候换掉你的uniapp了]]></title><description><![CDATA[以前选择uniapp时，更像是没得选]]></description><link>https://zzao.club/post/daily/time-to-change-your-uniapp-project</link><guid isPermaLink="true">https://zzao.club/post/daily/time-to-change-your-uniapp-project</guid><pubDate>Thu, 08 Jan 2026 01:34:04 GMT</pubDate><content:encoded>&lt;p&gt;以前选择uniapp时，更像是没得选&lt;/p&gt;
&lt;p&gt;知道它的&lt;strong&gt;Hbuild&lt;/strong&gt;一坨，也知道它编译后体验一般，但是没办法，它就是给老板省下不少钱，再加上社区繁荣起来，依赖也越来越多&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一旦代码可以正常运行了，就不要试图优化它&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但现在AI时代，其实换一个新框架的成本没那么高了。甚至用跨端框架的考量也需要换一换了。用原生语言开发原生应用门槛其实不高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI读老代码就像是开发者读文档一样&lt;/strong&gt;，只要老代码不要过于狗屎，AI仔细梳理一遍应该问题不大，而且再把已有逻辑翻译为另一个语言这是AI最擅长的。要比让AI从头开发要简单的多。&lt;/p&gt;
&lt;p&gt;因为从新开发需要开发者输出指令，这是最大的变数，很多开发者不能完整的考虑需求，或者不能准确的描述需求，一份已经在运行的代码在AI看来是非常精确的需求文档。&lt;/p&gt;
&lt;p&gt;所以大放心的让AI把你的老uniapp项目翻译为原生应用，&lt;strong&gt;除非依赖DOM&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[2025年度健康总结]]></title><description><![CDATA[上半年干了什么已经记不得了😮‍💨只记得今年还算比较忙的一年]]></description><link>https://zzao.club/post/daily/2025-healthy-stats</link><guid isPermaLink="true">https://zzao.club/post/daily/2025-healthy-stats</guid><pubDate>Wed, 07 Jan 2026 18:31:02 GMT</pubDate><content:encoded>&lt;p&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;从&lt;code&gt;6月18&lt;/code&gt;起开始减肥，办健身卡，大概健身&lt;code&gt;(6.18)&lt;/code&gt;两周后买了手表，然后开始全天候带着它&lt;/p&gt;
&lt;p&gt;先来看一下&lt;strong&gt;体重&lt;/strong&gt;趋势&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767789627665_oda2i1pusw.png&quot; alt=&quot;1.00&quot; title=&quot;全年体重趋势&quot;&gt;&lt;/p&gt;
&lt;p&gt;到目前正好是六个月，当时计划的减肥周期算是已经结束，当时定的是&lt;code&gt;85kg&lt;/code&gt;，心里想的是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;应该没什么问题，但是万一又是一次浅尝辄止或者有突发情况又失败了呢。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实这个设定是故意保守了，怕打脸🌚 但现在看来完全合理（&lt;em&gt;这何尝不是一种成功~&lt;/em&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;最直接的影响就是健身卡直接荒废了三个月，不健身的话每日消耗热量降低不少，来到异地工作，又是海边，很难不吃点什么，于是每日的摄入热量又上涨一些。所以体重上涨是板上钉钉的事儿。&lt;/p&gt;
&lt;p&gt;但是人体就是这么神奇，也许是之前两个月打下了一些底子，日常代谢能力提升了一些，可以看到九月份其实体重保持的还可以，还是在缓慢下降的。&lt;/p&gt;
&lt;p&gt;哦对，&lt;strong&gt;最开始我主要的运动是游泳&lt;/strong&gt;，去出差肯定是游不了了，所以我当时就觉得应该换一种更日常一点的运动，于是在九月初尝试着跑了&lt;strong&gt;跑步&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767790549452_83sg1p9mst9.png&quot; alt=&quot;0.76&quot; title=&quot;9月4次跑步&quot;&gt;&lt;/p&gt;
&lt;p&gt;穿着我六年前买的跑鞋，因为我近五年没跑过步，所以它还挺新的，但是放的时间实在太久了，鞋面上一些装饰和鞋后面那个用来提鞋的东西（学名叫啥啊🤔）已经干巴到自然脱落了。&lt;/p&gt;
&lt;p&gt;但是青岛（我住的这块儿）这个路实在是过于离谱了，没有辅路，上下坡太多，坡度又大。竟然没找到任何一个平坦的适合跑步的路，最后还是围着自己住的公寓跑，一圈450米左右。&lt;/p&gt;
&lt;p&gt;跑步对心肺能力提升很有帮助，而且同事之间还没完全熟络，吃饭也比较克制，所以九月份体重还稍有下探，我记得是&lt;code&gt;85.7&lt;/code&gt;左右，这是我离目标最近的一次。&lt;/p&gt;
&lt;p&gt;👇下面是自开始运动以来HRV值&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;心率变异性(HRV)是对连续心跳之间时间变化的统计指标&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;HRV 状态可以指示您的自主神经系统在身体和心理上如何对您的环境作出反应。平衡的HRV 读数可能表明健康的积极迹象，如良好的训练和恢复平衡、更好的心血管健康和更强的压力恢复能力。不平衡、偏低或偏差的读数可能是疲劳、更加需要恢复或压力增加等因素的迹象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767792105433_0d1q88tb089j.png&quot; alt=&quot;2.21&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到自从开始中低强度有氧以及低强度撸铁后，HRV在稳步提升，有孩子的情况下睡眠也很稳定，所以身体状态很好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8月&lt;/strong&gt;下旬有&lt;strong&gt;一段黄色的，过高&lt;/strong&gt;，是因为有一段时间运动强度比较大，没休息过来。&lt;strong&gt;9月&lt;/strong&gt;有几次突然的&lt;strong&gt;降低&lt;/strong&gt;，是因为9月开始&lt;strong&gt;熬夜&lt;/strong&gt;了，但这个值是&lt;strong&gt;7日平均值&lt;/strong&gt;，所以不至于掉出正常区间，测的还是蛮准的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10月&lt;/strong&gt;开始大幅降低，一开始国庆期间回丈母娘家了，开始疯狂&lt;strong&gt;喝酒&lt;/strong&gt;，今天白的明天啤的，睡眠质量也比较差，&lt;strong&gt;HRV值一路下降&lt;/strong&gt;，在体感上确实也是&lt;strong&gt;精神不饱满&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而且假期结束后开始进入疯狂&lt;strong&gt;加班&lt;/strong&gt;模式，状态&lt;strong&gt;一落千丈&lt;/strong&gt;。直到11月底又一次把&lt;strong&gt;跑步&lt;/strong&gt;拾起来才开始状态的&lt;strong&gt;回升&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767796160415_zeicnc6gbei.png&quot; alt=&quot;1.00&quot; title=&quot;10-11月hrv&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767796226609_aozsk1wc14l.png&quot; alt=&quot;1.00&quot; title=&quot;10-11月8次跑步&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到随着跑步频率的稳定，HRV值也在持续上涨。给我感觉就是&lt;strong&gt;精力充沛&lt;/strong&gt;，有一种身体干巴巴的、有力量的感觉，与之对应的是吃了太多碳水之后身体有点水肿的、提不起劲儿感觉。&lt;/p&gt;
&lt;p&gt;但是跑步频率能否稳定，完全看当天有没有加班，加班太晚一般就不跑了，早上也起不来，也不跑。没加班，没雾霾就跑一跑，一般每次5/7/10公里，看自己的身体状态。&lt;/p&gt;
&lt;p&gt;所以要想&lt;strong&gt;保持运动，首先不能加班&lt;/strong&gt;，运动消耗精力，加班也消耗精力，一天的精力就那么多。但运动可以提高体力上限，但是体力再多也不可能用到加班上去。加班本身也是管理者无能的体现，无能可能是各个维度的无能，要么是无法对抗的甲方要求加班，要么是自己有怪癖喜欢别人加班，要么是自己承受了压力又被迫或喜欢把压力往下层传递。&lt;/p&gt;
&lt;p&gt;最正常的情况，可能是单纯自己无法协调好整个团队的任务和时间😅，其他的原因我觉得可以直接理解为&lt;code&gt;变态&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;好景不长，马上又开始新一轮的加班，还伴随着温度的下降。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767857993292_ty7de2gvjd.png&quot; alt=&quot;0.52&quot; title=&quot;11-12月4次跑步&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;11月中旬&lt;/strong&gt;，天儿已经有点凉了，所以有一次我出去跑了一公里就冻的回去了，后面在迪卡侬买了一身穿着(心理上)很别扭但又很管用的&lt;strong&gt;跑步紧身套装&lt;/strong&gt;，才又勉强跑了两次。但是光有这个还不行，手里还拿着手机又开始冻手了，于是又买了&lt;strong&gt;手套和腰包&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此时&lt;/strong&gt;在体重曲线上，我没有记录，但已经&lt;strong&gt;回弹了至少3斤&lt;/strong&gt;了，因为下班太晚，吃饭太多，天气太冷，跑步太少。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/zotepad/1767857984665_h5c9b88oala.png&quot; alt=&quot;0.68&quot;&gt;&lt;/p&gt;
&lt;p&gt;HRV还算正常，但完全没有一开始那么高的值了，这个曲线也刚好印证了只要运动HRV就会持续上升，只要停下来就开始下降。当然，&lt;strong&gt;睡眠对HRV影响也巨大&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;后面整个&lt;strong&gt;12月&lt;/strong&gt;也&lt;strong&gt;只跑了两次&lt;/strong&gt;，纯粹是觉得：&lt;em&gt;完了，再不跑可能再也不会跑了！&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;但跑起来体感还好，并没有感觉到体能下降多少。&lt;/p&gt;
&lt;p&gt;到今天为止，距离上次跑步也已经过去2周了🥲。不再出差之后，日常节奏稳定了下来，体重又开始产生向下的趋势了，这几天趁着天气稍暖了几度务必出去跑步！&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;总结一下&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;体重整体下降了不少，没达到预期，但已经算小有成就。&lt;strong&gt;这不还没过年么，还有机会&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;没有过度节食，只是在可控时适当少吃，所以说完全不耽误控制不住自己时疯狂吃饭。&lt;/p&gt;
&lt;p&gt;开始了游泳和跑步，并且没有纠结数据，完全看自己的身体反馈来安排训练量，&lt;strong&gt;体力非常可观的提升很多&lt;/strong&gt;。即使疯狂吃饭，身体也具备了一定的调节能力（&lt;strong&gt;感恩&lt;/strong&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;把 &lt;code&gt;todo &lt;/code&gt; 变成 &lt;code&gt;done&lt;/code&gt;，多回顾自己做了什么，少纠结错过了什么。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[公众号的加粗效果失效了]]></title><description><![CDATA[刚发了篇文章，发现自己加粗的文本都没有加粗。]]></description><link>https://zzao.club/post/daily/wx-page-lost-bold-style</link><guid isPermaLink="true">https://zzao.club/post/daily/wx-page-lost-bold-style</guid><pubDate>Wed, 07 Jan 2026 18:31:02 GMT</pubDate><content:encoded>&lt;p&gt;刚发了篇文章，发现自己加粗的文本都没有加粗。&lt;/p&gt;
&lt;p&gt;回到公众号助手App，以及网页端公众号编辑器一看，是加粗的没错。&lt;/p&gt;
&lt;p&gt;于是我怀疑是我写的从&lt;a href=&quot;https://zzao.club/product/zotepad&quot;&gt;zotepad&lt;/a&gt;复制出的HTML有问题，于是一看加粗文本是用的 f&lt;code&gt;ontWeight:700&lt;/code&gt;，而且是 &lt;code&gt;strong标签&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我问AI，是不是公众号编辑器不支持&lt;code&gt;fontWeight&lt;/code&gt;，它一本正经的回答我说「是的」&lt;/p&gt;
&lt;p&gt;我深信不疑，于是在我的导出功能里增加了一行把 &lt;strong&gt;fontWeight 700 600 都转换为 bold&lt;/strong&gt; 的逻辑&lt;/p&gt;
&lt;p&gt;再次测试发现网页端公众号编辑器还是正常加粗的，但是预览时加粗就没了&lt;/p&gt;
&lt;p&gt;然后又怀疑是不是 &lt;code&gt;strong &lt;/code&gt; 标签不支持...&lt;/p&gt;
&lt;p&gt;于是又把&lt;code&gt;strong&lt;/code&gt;换成&lt;code&gt;span&lt;/code&gt; ，结果还是不行&lt;/p&gt;
&lt;p&gt;最后我直接在公众号自己的编辑器内写文字，然后加粗，再次预览。删除有问题的草稿，再次新建草稿预览。都不行。&lt;/p&gt;
&lt;p&gt;它自己的编辑器也不行，那看来只能是微信内公众号展示时出bug了&lt;/p&gt;
&lt;p&gt;😂&lt;/p&gt;</content:encoded></item><item><title><![CDATA[其实 AI 已经可以取代你了]]></title><description><![CDATA[最近几年，互联网上最振聋发聩的发现莫过于那句：“世界就是一个巨大的草台班子”。]]></description><link>https://zzao.club/post/daily/ai-vs-human-caotaibanzi</link><guid isPermaLink="true">https://zzao.club/post/daily/ai-vs-human-caotaibanzi</guid><pubDate>Fri, 21 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近几年，互联网上最振聋发聩的发现莫过于那句：“&lt;strong&gt;世界就是一个巨大的草台班子&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;小时候我们以为，大人们的世界是严丝合缝、精密运转的机器；长大后才发现，所谓的“专业团队”可能只是几个临时工在百度怎么修水管，所谓的“高大上项目”可能只是 PPT 做得比较花哨的烂尾楼。大家都在演，都在凑合，都在“假装自己很懂”。&lt;/p&gt;
&lt;p&gt;而就在我们人类还在努力维持这个摇摇欲坠的草台班子时，一个更强大的竞争对手出现了——&lt;strong&gt;AI&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;很多人还在从技术的角度挑剔 AI：它逻辑不严谨、它会产生幻觉、它没有真正的意识…… 哎，朋友，你太较真了。如果你从“专业”的角度看 AI，它确实离图灵测试的完美通过还有距离；但如果你把世界看作一个草台班子，把 AI 看作这个班子里的一员，你会惊恐地发现：&lt;strong&gt;它简直就是为了这个草台班子而生的天选之子，它比我们任何人都更会演。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;论“配合演出”，AI 才是最佳执行者&lt;/h2&gt;
&lt;p&gt;在草台班子里，工作的本质往往不是“创造价值”，而是“完成动作”。领导需要的不是一个质疑项目合理性的员工，而是一个能迅速把“大概意思”变成“具体方案”的执行者。&lt;/p&gt;
&lt;p&gt;这一点，人类往往做不到。我们会纠结逻辑，会质疑可行性，会因为觉得任务荒谬而产生抵触情绪。&lt;/p&gt;
&lt;p&gt;但 AI 不会。它不问为什么，只问“您需要什么格式”。&lt;/p&gt;
&lt;p&gt;想象一下，一个项目经理需要一份“关于未来五年元宇宙赋能传统养猪业的战略规划”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;人类员工&lt;/strong&gt;：内心吐槽“这什么鬼需求”，行动上拖拖拉拉，写出来的东西充满了犹豫和自我怀疑，甚至还想找领导确认一下“真的要这么写吗？”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 员工&lt;/strong&gt;：秒回。“没问题！以下是基于区块链技术与沉浸式体验的生猪全生命周期管理赋能方案……” 洋洋洒洒五千字，从“猪脸识别”到“虚拟饲料”，逻辑自洽，术语堆砌，看着就让人觉得不明觉厉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然 AI 无法替代人类在酒桌上的推杯换盏，也无法理解办公室里微妙的眼神交流，但在**“把领导的脑洞落地成文档”**这一核心环节上，AI 简直是草台班子梦寐以求的完美耗材。它不一定懂业务，但它绝对懂“领导想要什么样子的结果”。&lt;/p&gt;
&lt;p&gt;在这个充满形式主义的世界里，AI 提供的不仅是效率，更是一种&lt;strong&gt;无条件的服从和配合&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;完美的“乙方”，永不疲倦的“捧哏”&lt;/h2&gt;
&lt;p&gt;草台班子之所以能运转，靠的不是严密的制度，而是人情世故，是互相吹捧，是“花花轿子人抬人”。&lt;/p&gt;
&lt;p&gt;人类会累，会有情绪。老板画的大饼，吃多了会反胃；甲方的无理要求，听多了想掀桌子。但 AI 不会。&lt;/p&gt;
&lt;p&gt;AI 是这个世界上最好的“乙方”，也是最完美的“捧哏”。&lt;br&gt;
无论你提出多么荒谬的要求，无论你的观点多么漏洞百出，AI 永远会先肯定你：“这是一个非常有趣且富有洞见的观点……” 然后顺着你的思路，把你的胡思乱想包装成高深莫测的理论。&lt;/p&gt;
&lt;p&gt;如果你想组建一个纯粹为了“忽悠”或者“娱乐”的草台班子，AI 绝对是核心骨干。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你需要写一堆没人看的周报？AI 帮你写。&lt;/li&gt;
&lt;li&gt;你需要生成一堆看起来很厉害但没啥用的概念图？AI 帮你画。&lt;/li&gt;
&lt;li&gt;你需要一段感人肺腑但全是套路的演讲稿？AI 帮你编。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它不抱怨，不要求涨薪，不搞办公室政治，24小时待命，随时准备为了你的面子工程添砖加瓦。在维持表面繁荣这方面，AI 的执行力吊打所有人类团队。&lt;/p&gt;
&lt;h2&gt;既然 AI 这么强，为什么还没接管世界？&lt;/h2&gt;
&lt;p&gt;既然 AI 已经是草台班子里的“卷王”，为什么我们现在的公司、组织、社会还没有被 AI 全面接管？为什么我们还要忍受人类的低效和情绪？&lt;/p&gt;
&lt;p&gt;我觉得，主要有三道壁垒。&lt;/p&gt;
&lt;h3&gt;认知的壁垒：我们还不敢相信“假的”能这么真&lt;/h3&gt;
&lt;p&gt;很多人对 AI 的排斥，源于一种朴素的“人类中心主义”傲慢。我们潜意识里认为，只有人类大脑产生的想法才是“灵魂”，AI 生成的只是“概率”。&lt;/p&gt;
&lt;p&gt;但现实是，草台班子里的很多工作，本来就不需要“灵魂”，只需要“流程”。写一份八股文公文需要灵魂吗？做一张千篇一律的海报需要灵魂吗？&lt;/p&gt;
&lt;p&gt;我们还没适应这种“去魅”的过程。我们很难接受，自己辛苦钻研了十年的“职场黑话”和“PPT美化技巧”，在 AI 眼里不过是一堆简单的语料库组合。承认 AI 能行，就等于承认我们自己过去的工作毫无意义。&lt;/p&gt;
&lt;h3&gt;信息的壁垒：不是谁都会“念咒语”&lt;/h3&gt;
&lt;p&gt;虽然 AI 很强，但它目前还是一个“被动技能”。它像一个神灯里的精灵，你得会擦神灯（Prompt Engineering），它才能出来干活。&lt;/p&gt;
&lt;p&gt;在这个草台班子里，大部分人连 Word 的排版都搞不明白，更别说去学习如何精准地指挥 AI 了。这种信息差，导致了 AI 的能力被锁在了少数人手里，而无法普及到每一个需要糊弄事的岗位上。&lt;/p&gt;
&lt;h3&gt;利益的壁垒：触动了人类的“蛋糕”&lt;/h3&gt;
&lt;p&gt;这才是最根本的原因。&lt;/p&gt;
&lt;p&gt;如果 AI 真的接管了草台班子，那么班子里的“南郭先生”们去哪儿混饭吃？&lt;br&gt;
如果 AI 能一秒钟生成完美的方案，那些靠“磨洋工”、“开长会”、“反复修改”来体现工作量的中层管理者，存在的价值是什么？&lt;/p&gt;
&lt;p&gt;草台班子的本质，往往不是为了“成事”，而是为了“分利”。大家在这个班子里混口饭吃，达成一种微妙的平衡。AI 的效率太高了，高到会打破这种平衡。它像一条闯入沙丁鱼群的鲶鱼，不仅会搅动水面，甚至可能把沙丁鱼都吃光。&lt;/p&gt;
&lt;p&gt;人类建立的体系，哪怕是草台班子，也是为了人类的利益服务的。让 AI 上位，等于让大家集体下岗。所以，哪怕 AI 再好用，大家也会默契地把它挡在核心决策圈之外，只让它做个“辅助工具”。&lt;/p&gt;
&lt;h3&gt;效率的诅咒：草台班子需要“表演性忙碌”&lt;/h3&gt;
&lt;p&gt;在草台班子的逻辑里，&lt;strong&gt;工作量往往是根据预算倒推的，而不是根据实际需求设定的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如上级批了一个大几百万的项目预算，这笔钱为什么是几百万？可能和项目本身难度无关，而是和某些“不能说的默契”有关。为了让这笔钱花得“名正言顺”，我们需要把一个简单的项目做得无比复杂，需要漫长的周期、厚厚的文档、无数次的会议。&lt;/p&gt;
&lt;p&gt;这时候，AI 的超高效率反而成了&lt;strong&gt;原罪&lt;/strong&gt;。&lt;br&gt;
如果 AI 半天就把活干完了，剩下半年的工期怎么办？这几百万的预算怎么核销？&lt;/p&gt;
&lt;p&gt;更尴尬的是，AI 干得太快，操作 AI 的员工就闲下来了。而在很多领导眼里，&lt;strong&gt;“员工闲着”比“项目亏损”更让他难受&lt;/strong&gt;。他虽然不懂项目细节，但他懂“工时管理”。&lt;/p&gt;
&lt;p&gt;于是，为了维持草台班子的稳定运行，我们不得不按住 AI 的手，假装很忙，继续配合演出这场“低效”的戏码。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;世界是个草台班子，这不可怕。可怕的是，我们明明知道它是草台班子，却还在假装正经。&lt;/p&gt;
&lt;p&gt;AI 的出现，其实是扯下了这块遮羞布。它用一种近乎戏谑的方式告诉我们：&lt;strong&gt;你看，你们人类引以为傲的那些“套路”、“流程”和“场面话”，我学得比你们还快，演得比你们还像。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也许在不久的将来，我们会看到这样一种奇景：&lt;/p&gt;
&lt;p&gt;人类老板用 AI 生成指令，发给人类经理；人类经理用 AI 细化成任务，发给人类员工；人类员工用 AI 完成工作，再层层汇报上去。&lt;/p&gt;
&lt;p&gt;整个草台班子依然熙熙攘攘，但真正干活的，其实只有电流和算力。&lt;/p&gt;
&lt;p&gt;而我们？大概只需要负责在 AI 呈上的方案上，签下名字，盖上章。&lt;/p&gt;
&lt;p&gt;然后心安理得地感叹一句：&lt;strong&gt;“看，这世界果然还是离不开我们啊。”&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[在 windows 上使用 wsl2 + debian 进行前端开发]]></title><description><![CDATA[时隔多年，开发机准备切换到 windows 环境下！]]></description><link>https://zzao.club/post/wsl/new-experience-in-windows-with-wsl2-debian</link><guid isPermaLink="true">https://zzao.club/post/wsl/new-experience-in-windows-with-wsl2-debian</guid><pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;时隔多年，开发机准备切换到 &lt;strong&gt;windows&lt;/strong&gt; 环境下！&lt;/p&gt;
&lt;p&gt;三年前 &lt;code&gt;1w3&lt;/code&gt; 的 &lt;code&gt;mac m2 air&lt;/code&gt; 现在大概只值 &lt;code&gt;4k&lt;/code&gt; 。因为新的 &lt;code&gt;m4 air&lt;/code&gt; 才 &lt;code&gt;7k+&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;这篇文章先占个坑，用来记录最近的 WSL2 配置，还没有真正进入开发状态&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;WSL 相关&lt;/h2&gt;
&lt;h3&gt;1. 启动 / 关闭 / 重启命令&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 启动默认发行版
wsl

# 启动指定发行版（名字用 wsl -l -v 看）
wsl -d Ubuntu-22.04

# 立即关机（所有发行版全部停掉，内存瞬间归还）
wsl --shutdown

# 重启某个发行版（先停再启）
wsl -t Ubuntu-22.04 &amp;#x26;&amp;#x26; wsl -d Ubuntu-22.04
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 状态与列表&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 看装了哪些发行版 + 当前状态（Running/Stopped）
wsl -l -v
# 简写：wsl -l -v

# 只看正在跑的实例
wsl -l --running

# 查看当前默认发行版
wsl -l --quiet
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 资源占用（CPU / 内存 / 磁盘）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 实时看虚拟内存占用（任务管理器里叫 VmmemWSL）
get-process -Name &quot;vmmemWSL&quot; | Select-Object CPU, WorkingSet, PagedMemorySize

# 更详细：用 Windows 性能计数器（每秒刷新）
typeperf &quot;\Process(vmmemWSL)\Working Set&quot; -si 1

# 查 WSL2 虚拟磁盘实际大小
wsl -d Ubuntu-22.04 -e du -h /mnt/wslg/distro
# 或直接进入 ext4.vhdx 所在目录
Get-ChildItem &quot;$env:LOCALAPPDATA\Packages\CanonicalGroupLimited.UbuntuonWindows_*\LocalState\ext4.vhdx&quot; | Select-Object Name, Length, LastWriteTime
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 导出 / 导入 / 备份（整机镜像）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 备份整个发行版为 tar（系统重装前用）
wsl --export Ubuntu-22.04 D:\backup\ubuntu2204.tar

# 以后在新机器还原
wsl --import Ubuntu-2204-New D:\WSL\Ubuntu2204-New D:\backup\ubuntu2204.tar --version 2

# 把导入的实例设为默认（可选）
wsl --set-default Ubuntu-2204-New
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 内存 &amp;#x26; 处理器上限控制&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 在用户目录新建/编辑 .wslconfig（全局生效）
notepad &quot;$env:USERPROFILE\.wslconfig&quot;
# 示例内容：
[wsl2]
memory=4GB          # 最大内存
processors=2        # 逻辑核数
swap=1GB            # 交换文件大小
localhostForwarding=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 升级&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 升级 WSL 内核和 GUI 支持（需管理员）
wsl --update

# 查看当前内核版本
wsl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装Docker&lt;/h2&gt;
&lt;p&gt;安装 &lt;code&gt;docker engine&lt;/code&gt; (在公司开发，不符合 Docker Desktop的许可证要求，所以这里只用纯命令行)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 1. 卸载旧版本（若之前装过 docker-ce 或 docker.io）
sudo apt-get remove -y docker.io docker-doc docker-compose podman-docker containerd runc

# 2. 装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 3. 添加 Docker 官方 GPG key &amp;#x26; 仓库（Debian 12 bookworm）
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

# 4. 安装最新版 Engine + CLI + containerd + compose 插件
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 5. 把当前用户加入 docker 组，免 sudo
sudo usermod -aG docker $USER
newgrp docker

# 6. 验证
docker run hello-world
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[白嫖一下 Github Actions 打包部署博客]]></title><description><![CDATA[之前我的博客一直使用 Gitea 来管理代码，然后顺势也配好了 Gitea 的 actions，推送了指定 commit msg 时就会自动打包部署代码。]]></description><link>https://zzao.club/post/nuxt/cloud/use-github-actions-deloy-nuxt-blog</link><guid isPermaLink="true">https://zzao.club/post/nuxt/cloud/use-github-actions-deloy-nuxt-blog</guid><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;之前我的博客一直使用 &lt;code&gt;Gitea&lt;/code&gt; 来管理代码，然后顺势也配好了 &lt;code&gt;Gitea&lt;/code&gt; 的 &lt;code&gt;actions&lt;/code&gt;，推送了指定 &lt;code&gt;commit msg&lt;/code&gt; 时就会自动打包部署代码。&lt;/p&gt;
&lt;p&gt;但代价就是服务器内存从 &lt;code&gt;4G&lt;/code&gt; 升到了 &lt;code&gt;8G&lt;/code&gt;，因为打包时峰值内存占用要到 &lt;code&gt;6G&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508261410524.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;最近想把博客相关的环境容器化&lt;/p&gt;
&lt;p&gt;但是和 &lt;code&gt;GTP5&lt;/code&gt; 一番讨论后，问题还是出在 &lt;code&gt;Nuxt/Content&lt;/code&gt; 上，&lt;code&gt;Content&lt;/code&gt; 主动拉取 &lt;code&gt;Github repo&lt;/code&gt; 的行为依赖于 &lt;code&gt;nuxt build&lt;/code&gt;，所以如果要单独发布一篇文章，不得不重新上传一个镜像。&lt;/p&gt;
&lt;p&gt;所以最后还是决定博客不用 &lt;code&gt;docker&lt;/code&gt; 了， &lt;strong&gt;mysql + redis&lt;/strong&gt; 使用 &lt;code&gt;docker compose&lt;/code&gt; 管理，博客还是用 &lt;code&gt;pm2&lt;/code&gt;  + &lt;code&gt;envfile&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果迁移服务器的话，就需要自己全局安装 &lt;code&gt;node&lt;/code&gt; 、&lt;code&gt;pm2&lt;/code&gt;，然后使用现有的 &lt;code&gt;docker-compose.yml&lt;/code&gt; 启动数据库环境，以及迁移现有的生产环境的 &lt;code&gt;envfile&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后继续走 Github Actions 构建、打包、ssh 传输到目标服务器，运行 &lt;code&gt;pm2&lt;/code&gt; 命令，加载 &lt;code&gt;envfile&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;等 &lt;code&gt;NuxtContent&lt;/code&gt; 支持主动拉取新的仓库文件或者定时拉取后，再进行调整&lt;/p&gt;
&lt;p&gt;然后分享一下，&lt;a href=&quot;https://github.com/aatrooox/blog.zzao.club&quot;&gt;博客开源地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;以及 &lt;code&gt;action&lt;/code&gt; 脚本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;
name: Deploy via GitHub Actions (SSH + PM2 Prod)

on:
  push:
    branches:
      - main

concurrency:
  group: deploy-main
  cancel-in-progress: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: zzaoclub
    if: contains(github.event.head_commit.message, &apos;chore(release)&apos;)
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PNPM
        uses: pnpm/action-setup@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install deps
        run: pnpm install --no-frozen-lockfile

      - name: Build
        env:
          CONTENT_REPO_TOKEN: ${{ secrets.CONTENT_REPO_TOKEN }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          NUXT_FEISHU_WEBHOOK: ${{ secrets.NUXT_FEISHU_WEBHOOK }}
          NUXT_FEISHU_USER_ID: ${{ secrets.NUXT_FEISHU_USER_ID }}
          NUXT_JWT_SECRET: ${{ secrets.NUXT_JWT_SECRET }}
          NUXT_NODEMAILER_HOST: ${{ secrets.NUXT_NODEMAILER_HOST }}
          NUXT_NODEMAILER_PORT: ${{ secrets.NUXT_NODEMAILER_PORT }}
          NUXT_NODEMAILER_AUTH_USER: ${{ secrets.NUXT_NODEMAILER_AUTH_USER }}
          NUXT_NODEMAILER_AUTH_PASS: ${{ secrets.NUXT_NODEMAILER_AUTH_PASS }}
          NUXT_UMAMI_HOST: ${{ secrets.NUXT_UMAMI_HOST }}
          NUXT_UMAMI_USER: ${{ secrets.NUXT_UMAMI_USER }}
          NUXT_UMAMI_PASS: ${{ secrets.NUXT_UMAMI_PASS }}
          NUXT_SESSION_PASSWORD: ${{ secrets.NUXT_SESSION_PASSWORD }}
          NUXT_OAUTH_GITHUB_CLIENT_ID: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_ID }}
          NUXT_OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_SECRET }}
          NUXT_COS_SECRET_ID: ${{ secrets.NUXT_COS_SECRET_ID }}
          NUXT_COS_SECRET_KEY: ${{ secrets.NUXT_COS_SECRET_KEY }}
          NUXT_COS_BUCKET: ${{ secrets.NUXT_COS_BUCKET }}
          NUXT_COS_REGION: ${{ secrets.NUXT_COS_REGION }}
        run: |
          NODE_OPTIONS=&quot;--max-old-space-size=4080&quot; pnpm build

      - name: Pack artifact (flatten .output)
        run: |
          rm -rf distpkg &amp;#x26;&amp;#x26; mkdir -p distpkg
          cp -R .output/* distpkg/
          cp -f pm2.config.json distpkg/
          cp -f pm2.preload.cjs distpkg/
          # Include Drizzle migrations &amp;#x26; config for server-side migrate
          mkdir -p distpkg/lib/drizzle
          if [ -d lib/drizzle/migrations ]; then cp -R lib/drizzle/migrations distpkg/lib/drizzle/; fi
          if [ -f drizzle.config.ts ]; then cp -f drizzle.config.ts distpkg/; fi
          tar -C distpkg -czf artifact.tgz .
          du -h artifact.tgz

      - name: Prepare SSH key
        run: |
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh
          echo &quot;${{ secrets.SSH_PRIVATE_KEY }}&quot; | tr -d &apos;\r&apos; &gt; ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Upload artifact via scp
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
        run: |
          ssh -o StrictHostKeyChecking=no -p &quot;${SSH_PORT}&quot; &quot;${SSH_USER}@${SSH_HOST}&quot; &quot;mkdir -p /root/web/blog&quot;
          scp -P &quot;${SSH_PORT}&quot; artifact.tgz &quot;${SSH_USER}@${SSH_HOST}:/root/web/blog/artifact.tgz&quot;

      - name: Deploy prod &amp;#x26; start with PM2
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
        run: |
          ssh -o StrictHostKeyChecking=no -p &quot;${SSH_PORT}&quot; &quot;${SSH_USER}@${SSH_HOST}&quot; &amp;#x3C;&amp;#x3C; &apos;EOSSH&apos;
          set -e
          APP_DIR=/root/web/blog
          ENVFILE=/root/envs/blog/.env
          mkdir -p &quot;$APP_DIR&quot;
          cd &quot;$APP_DIR&quot;
          # Clean app dir but keep artifact
          find &quot;$APP_DIR&quot; -mindepth 1 -maxdepth 1 ! -name artifact.tgz -exec rm -rf {} +
          tar -xzf artifact.tgz -C &quot;$APP_DIR&quot;
          rm -f artifact.tgz
          # Prefer globally installed dotenv-cli; fallback to npx dotenv-cli; else source fallback
          if command -v dotenv &gt;/dev/null 2&gt;&amp;#x26;1; then
            DOTENV=&quot;dotenv -e \&quot;$ENVFILE\&quot; --&quot;
          elif command -v npx &gt;/dev/null 2&gt;&amp;#x26;1; then
            DOTENV=&quot;npx -y dotenv-cli -e \&quot;$ENVFILE\&quot; --&quot;
          else
            DOTENV=&quot;&quot;
          fi

          # Apply Drizzle migrations on server
          if [ -n &quot;$DOTENV&quot; ]; then
            eval &quot;$DOTENV&quot; npx -y drizzle-kit@0.31.4 migrate || true
          else
            echo &quot;dotenv-cli not available; using source fallback for migrations&quot;
            if [ -f &quot;$ENVFILE&quot; ]; then set -a; . &quot;$ENVFILE&quot;; set +a; fi
            if command -v npx &gt;/dev/null 2&gt;&amp;#x26;1; then
              npx -y drizzle-kit@0.31.4 migrate || true
            else
              echo &quot;npx not found; skipping migrations&quot;
            fi
          fi

          # Start cleanly: use prod pm2.config.json
          pm2 delete Blog &gt;/dev/null 2&gt;&amp;#x26;1 || true
          pm2 start pm2.config.json --update-env
          pm2 save
          EOSSH

      - name: Notify (Feishu)
        if: always()
        run: |
          curl -X POST -H &quot;Content-Type: application/json&quot; \
            -d &apos;{&quot;msg_type&quot;:&quot;text&quot;,&quot;content&quot;:{&quot;text&quot;:&quot;&apos;&quot;${{ github.repository }}&quot;&apos; - GH canary deploy [&apos;&quot;&apos;&quot;${{ job.status }}&quot;&apos;&quot;&apos;]&quot;}}&apos; \
            &quot;${{ secrets.NUXT_FEISHU_WEBHOOK }}&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中的 &lt;code&gt;env&lt;/code&gt;，全部要在 &lt;code&gt;github&lt;/code&gt; 配置一遍&lt;/p&gt;
&lt;p&gt;先建一个 &lt;code&gt;Environment&lt;/code&gt; ，然后在其下配 &lt;code&gt;secrets&lt;/code&gt; 即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508261410526.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;迁移时，先用一个临时目录进行迁移。测试没问题后再覆盖原来的目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;github&lt;/code&gt; 的服务器，打包速度也快了，总流程 &lt;strong&gt;3m20s&lt;/strong&gt; 。 用自己的服务器，&lt;code&gt;5m30s&lt;/code&gt;，快了不少。&lt;/p&gt;
&lt;p&gt;主要是在没有大量 IO 的情况下，服务器内存占用就很稳定，下一年也不用再续费 8G 的服务器了&lt;/p&gt;
&lt;p&gt;不过趁着内存够用，多上一些应用试试水。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[博客鉴权机制详细文档]]></title><link>https://zzao.club/post/nuxt/blog/auth-system-docs</link><guid isPermaLink="true">https://zzao.club/post/nuxt/blog/auth-system-docs</guid><pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;本项目采用双 Token 鉴权机制，结合了 JWT 的无状态特性和 Redis 的有状态管理，提供了安全性和用户体验的最佳平衡。&lt;/p&gt;
&lt;h2&gt;鉴权架构&lt;/h2&gt;
&lt;h3&gt;1. 双 Token 机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Access Token (JWT)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型：无状态 JWT&lt;/li&gt;
&lt;li&gt;过期时间：15 分钟&lt;/li&gt;
&lt;li&gt;用途：API 访问凭证&lt;/li&gt;
&lt;li&gt;存储：客户端内存/localStorage&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型：随机字符串&lt;/li&gt;
&lt;li&gt;过期时间：7 天&lt;/li&gt;
&lt;li&gt;用途：刷新 Access Token&lt;/li&gt;
&lt;li&gt;存储：Redis（服务端）+ 客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 错误代码系统&lt;/h3&gt;
&lt;p&gt;所有 API 统一返回 200 HTTP 状态码，通过 &lt;code&gt;code&lt;/code&gt; 字段（数值）标识业务状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 响应格式
{
  code: number,      // 错误代码（0表示成功）
  message: string,   // 错误描述
  data: any,         // 响应数据
  timestamp: number  // 时间戳
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;错误代码定义&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const API_CODES = {
  // 成功
  SUCCESS: 0,

  // 认证相关错误 (1000-1999)
  NO_TOKEN: 1001,           // 未提供token
  TOKEN_EXPIRED: 1002,      // token过期（可刷新）
  TOKEN_INVALID: 1003,      // token无效（不可刷新）
  AUTH_FAILED: 1004,        // 认证失败
  REFRESH_TOKEN_EXPIRED: 1005, // refresh token过期

  // 权限相关错误 (2000-2999)
  PERMISSION_DENIED: 2001,  // 无权限
  FORBIDDEN: 2002,          // 禁止访问

  // 业务相关错误 (3000-3999)
  VALIDATION_ERROR: 3001,   // 参数验证错误
  RESOURCE_NOT_FOUND: 3002, // 资源不存在
  DUPLICATE_ERROR: 3003,    // 重复错误

  // 系统错误 (9000-9999)
  INTERNAL_ERROR: 9001,     // 内部错误
  NETWORK_ERROR: 9002,      // 网络错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;后端实现&lt;/h2&gt;
&lt;h3&gt;1. API 响应处理器&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const defineStandardResponseHandler = &amp;#x3C;T extends EventHandlerRequest, D&gt; (
  handler: EventHandler&amp;#x3C;T, D&gt;,
): EventHandler&amp;#x3C;T, D&gt; =&gt;
  defineEventHandler&amp;#x3C;T&gt;(async (event) =&gt; {
    try {
      const response = await handler(event)
      // 成功响应
      return {
        code: API_CODES.SUCCESS,
        message: &apos;ok&apos;,
        data: response,
        timestamp: Date.now(),
      }
    }
    catch (error: any) {
      // 强制设置 HTTP 状态码为 200
      setResponseStatus(event, 200)
      
      if (error.statusCode) {
        const customCode = error.data?.code
        const customMessage = error.data?.message || error.message
        
        return {
          code: customCode || API_CODES.INTERNAL_ERROR,
          message: customMessage || &apos;出错啦，请稍后再试～&apos;,
          data: error.data?.data || null,
          timestamp: Date.now(),
        }
      }
      
      // 未知错误
      return {
        code: API_CODES.INTERNAL_ERROR,
        message: &apos;出错啦，请稍后再试～&apos;,
        data: null,
        timestamp: Date.now(),
      }
    }
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. API 开发方式&lt;/h3&gt;
&lt;h4&gt;成功案例&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// /server/api/v1/user/login.post.ts
export default defineStandardResponseHandler(async (event) =&gt; {
  const body = await useSafeValidatedBody(event, schema)

  if (!body.success) {
    throw createError({
      statusCode: 400,
      data: {
        code: API_CODES.VALIDATION_ERROR,
        message: &apos;参数验证失败&apos;,
        data: body.error,
      },
    })
  }

  // 业务逻辑处理...
  const tokenPair = await generateTokenPair(user.id)

  // 直接返回数据，由 handler 包装成标准格式
  return {
    accessToken: tokenPair.accessToken,
    refreshToken: tokenPair.refreshToken,
    accessExpiresAt: tokenPair.accessExpiresAt,
    refreshExpiresAt: tokenPair.refreshExpiresAt,
    user,
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;错误处理&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 参数验证错误
throw createError({
  statusCode: 400,
  data: {
    code: API_CODES.VALIDATION_ERROR,
    message: &apos;参数验证失败&apos;,
    data: validationErrors,
  },
})

// 认证失败
throw createError({
  statusCode: 401,
  data: {
    code: API_CODES.AUTH_FAILED,
    message: &apos;账号或密码错误&apos;,
  },
})

// 内部错误
throw createError({
  statusCode: 500,
  data: {
    code: API_CODES.INTERNAL_ERROR,
    message: &apos;系统繁忙，请稍后重试&apos;,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 鉴权中间件&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineEventHandler(async (event) =&gt; {
  // 需要鉴权的路径判断
  if (needsAuth(event)) {
    if (!event.context.token) {
      // 强制设置状态码为 200，返回错误代码
      setResponseStatus(event, 200)
      return {
        code: API_CODES.NO_TOKEN,
        message: API_ERROR_MESSAGES[API_CODES.NO_TOKEN],
        data: null,
        timestamp: Date.now(),
      }
    }

    const { isAuth, userId, error } = verifyJWTAccessToken(event.context.token)

    if (!isAuth) {
      let errorCode = API_CODES.AUTH_FAILED
      if (error?.includes(&apos;expired&apos;)) {
        errorCode = API_CODES.TOKEN_EXPIRED
      } else if (error?.includes(&apos;invalid&apos;)) {
        errorCode = API_CODES.TOKEN_INVALID
      }

      setResponseStatus(event, 200)
      return {
        code: errorCode,
        message: API_ERROR_MESSAGES[errorCode],
        data: null,
        timestamp: Date.now(),
      }
    }

    event.context.userId = userId
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Token 管理&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 生成双 Token
export async function generateTokenPair(userId: number) {
  const accessToken = generateJWT(userId, &apos;15m&apos;)
  const refreshToken = generateRandomToken()
  
  // 存储 refresh token 到 Redis
  await redis.setex(`refresh_token:${userId}`, 7 * 24 * 60 * 60, refreshToken)
  
  return {
    accessToken,
    refreshToken,
    accessExpiresAt: Date.now() + 15 * 60 * 1000,
    refreshExpiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
  }
}

// 刷新访问令牌
export async function refreshAccessToken(refreshToken: string) {
  // 从 Redis 验证 refresh token
  const userId = await redis.get(`refresh_token_user:${refreshToken}`)
  
  if (!userId) {
    return null // refresh token 无效或过期
  }

  // 生成新的 access token
  const newAccessToken = generateJWT(userId, &apos;15m&apos;)
  
  return {
    accessToken: newAccessToken,
    expiresAt: Date.now() + 15 * 60 * 1000,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;前端实现&lt;/h2&gt;
&lt;h3&gt;1. 请求拦截器&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const $api = $fetch.create({
  onRequest: async ({ options }) =&gt; {
    const userStore = useUser()
    
    // 自动添加 Authorization 头
    if (userStore.tokenInfo.value.accessToken) {
      options.headers.set(&apos;Authorization&apos;, `Bearer ${userStore.tokenInfo.value.accessToken}`)
    }
  },

  onResponse: async ({ request, response }) =&gt; {
    const userStore = useUser()
    const globalToast = useGlobalToast()

    const apiResponse = response._data
    
    // 处理业务层面的错误
    if (apiResponse?.code &amp;#x26;&amp;#x26; apiResponse.code !== API_CODES.SUCCESS) {
      const { code, message } = apiResponse

      // 不可刷新的认证错误
      if ([API_CODES.NO_TOKEN, API_CODES.TOKEN_INVALID, API_CODES.AUTH_FAILED].includes(code)) {
        userStore.logout()
        globalToast.add({ message: message || &apos;认证失败，请重新登录&apos;, type: &apos;error&apos; })
        return
      }

      // Token 过期处理
      if (code === API_CODES.TOKEN_EXPIRED) {
        // 自动刷新 token 逻辑
        if (userStore.tokenInfo.value.refreshToken &amp;#x26;&amp;#x26; !userStore.isRefreshTokenExpired.value) {
          const { refreshToken } = useAuth()
          const success = await refreshToken()
          
          if (!success) {
            userStore.logout()
            globalToast.add({ message: &apos;登录已过期，请重新登录&apos;, type: &apos;error&apos; })
          }
        }
        return
      }

      // 其他错误处理
      globalToast.add({ message: message || &apos;操作失败&apos;, type: &apos;error&apos; })
    }
  },

  onResponseError: async ({ response }) =&gt; {
    // 只处理真正的网络错误
    const globalToast = useGlobalToast()
    const errorMessage = response?._data?.message || &apos;网络请求失败&apos;
    globalToast.add({ message: errorMessage, type: &apos;error&apos; })
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 用户状态管理&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const useUser = () =&gt; {
  const tokenInfo = ref({
    accessToken: &apos;&apos;,
    refreshToken: &apos;&apos;,
    accessExpiresAt: 0,
    refreshExpiresAt: 0,
  })

  // 检查 access token 是否过期
  const isAccessTokenExpired = computed(() =&gt; {
    return Date.now() &gt;= tokenInfo.value.accessExpiresAt
  })

  // 检查 refresh token 是否过期
  const isRefreshTokenExpired = computed(() =&gt; {
    return Date.now() &gt;= tokenInfo.value.refreshExpiresAt
  })

  // 登录
  const login = async (credentials: LoginData) =&gt; {
    try {
      const response = await $fetch.post(&apos;/api/v1/user/login&apos;, credentials)
      
      if (response.code === API_CODES.SUCCESS) {
        tokenInfo.value = {
          accessToken: response.data.accessToken,
          refreshToken: response.data.refreshToken,
          accessExpiresAt: response.data.accessExpiresAt,
          refreshExpiresAt: response.data.refreshExpiresAt,
        }
        
        // 保存到 localStorage
        localStorage.setItem(&apos;tokenInfo&apos;, JSON.stringify(tokenInfo.value))
        return true
      }
      
      return false
    } catch (error) {
      console.error(&apos;登录失败:&apos;, error)
      return false
    }
  }

  // 登出
  const logout = () =&gt; {
    tokenInfo.value = {
      accessToken: &apos;&apos;,
      refreshToken: &apos;&apos;,
      accessExpiresAt: 0,
      refreshExpiresAt: 0,
    }
    localStorage.removeItem(&apos;tokenInfo&apos;)
    navigateTo(&apos;/login&apos;)
  }

  return {
    tokenInfo: readonly(tokenInfo),
    isAccessTokenExpired,
    isRefreshTokenExpired,
    login,
    logout,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 鉴权组合式函数&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const useAuth = () =&gt; {
  const userStore = useUser()

  // 刷新 token
  const refreshToken = async (): Promise&amp;#x3C;boolean&gt; =&gt; {
    try {
      const response = await $fetch.post(&apos;/api/v1/auth/refresh&apos;, {
        refreshToken: userStore.tokenInfo.value.refreshToken,
      })

      if (response.code === API_CODES.SUCCESS) {
        // 更新 access token
        userStore.tokenInfo.value.accessToken = response.data.accessToken
        userStore.tokenInfo.value.accessExpiresAt = response.data.expiresAt
        
        // 更新 localStorage
        localStorage.setItem(&apos;tokenInfo&apos;, JSON.stringify(userStore.tokenInfo.value))
        return true
      }

      return false
    } catch (error) {
      console.error(&apos;Token 刷新失败:&apos;, error)
      return false
    }
  }

  // 检查登录状态
  const checkAuth = () =&gt; {
    if (!userStore.tokenInfo.value.accessToken) {
      return false
    }

    if (userStore.isRefreshTokenExpired.value) {
      userStore.logout()
      return false
    }

    return true
  }

  return {
    refreshToken,
    checkAuth,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;前端使用方式&lt;/h2&gt;
&lt;h3&gt;1. API 调用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 在组件中使用
export default defineNuxtPlugin({
  async setup() {
    const { $api } = useNuxtApp()

    // GET 请求
    const userData = await $api.get(&apos;/api/v1/user/profile&apos;)
    if (userData.code === API_CODES.SUCCESS) {
      console.log(&apos;用户信息:&apos;, userData.data)
    }

    // POST 请求
    const result = await $api.post(&apos;/api/v1/posts&apos;, {
      title: &apos;新文章&apos;,
      content: &apos;文章内容...&apos;
    })

    if (result.code === API_CODES.SUCCESS) {
      console.log(&apos;创建成功:&apos;, result.data)
    } else {
      console.error(&apos;创建失败:&apos;, result.message)
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 路由守卫&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// /middleware/auth.ts
export default defineNuxtRouteMiddleware((to) =&gt; {
  const { checkAuth } = useAuth()
  
  if (!checkAuth()) {
    return navigateTo(&apos;/login&apos;)
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 页面使用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;h1&gt;受保护的页面&amp;#x3C;/h1&gt;
    &amp;#x3C;button @click=&quot;handleApiCall&quot;&gt;调用API&amp;#x3C;/button&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup&gt;
// 页面级别的鉴权
definePageMeta({
  middleware: &apos;auth&apos;
})

const { $api } = useNuxtApp()

const handleApiCall = async () =&gt; {
  try {
    const response = await $api.post(&apos;/api/v1/some-protected-endpoint&apos;, {
      data: &apos;some data&apos;
    })
    
    // 成功处理
    if (response.code === API_CODES.SUCCESS) {
      console.log(&apos;操作成功:&apos;, response.data)
    }
  } catch (error) {
    // 错误会被拦截器自动处理，显示对应的 toast 消息
    console.error(&apos;请求失败:&apos;, error)
  }
}
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;错误处理最佳实践&lt;/h2&gt;
&lt;h3&gt;1. 前端错误分类处理&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const handleApiResponse = (response: ApiResponse) =&gt; {
  switch (response.code) {
    case API_CODES.SUCCESS:
      // 成功处理
      break
      
    case API_CODES.VALIDATION_ERROR:
      // 参数验证错误，显示具体字段错误
      showValidationErrors(response.data)
      break
      
    case API_CODES.PERMISSION_DENIED:
      // 权限不足，可能需要升级账户
      showPermissionDialog()
      break
      
    case API_CODES.RESOURCE_NOT_FOUND:
      // 资源不存在，可能需要刷新页面
      navigateTo(&apos;/404&apos;)
      break
      
    default:
      // 其他错误，显示通用错误消息
      showErrorToast(response.message)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 自动重试机制&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const apiWithRetry = async (url: string, options: any, maxRetries = 3) =&gt; {
  for (let i = 0; i &amp;#x3C; maxRetries; i++) {
    try {
      const response = await $api.request(url, options)
      
      if (response.code === API_CODES.SUCCESS) {
        return response
      }
      
      // 如果是 token 过期，等待刷新后重试
      if (response.code === API_CODES.TOKEN_EXPIRED &amp;#x26;&amp;#x26; i &amp;#x3C; maxRetries - 1) {
        await new Promise(resolve =&gt; setTimeout(resolve, 1000))
        continue
      }
      
      return response
    } catch (error) {
      if (i === maxRetries - 1) throw error
      await new Promise(resolve =&gt; setTimeout(resolve, 1000 * (i + 1)))
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安全考虑&lt;/h2&gt;
&lt;h3&gt;1. Token 存储安全&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Access Token&lt;/strong&gt;: 存储在内存中，避免 XSS 攻击&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;: 存储在 httpOnly cookie 中（推荐）或 localStorage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;敏感信息&lt;/strong&gt;: 永远不要在 JWT 中存储敏感信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. CSRF 防护&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 添加 CSRF token 到请求头
onRequest: ({ options }) =&gt; {
  const csrfToken = document.querySelector(&apos;meta[name=&quot;csrf-token&quot;]&apos;)?.getAttribute(&apos;content&apos;)
  if (csrfToken) {
    options.headers.set(&apos;X-CSRF-Token&apos;, csrfToken)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 请求频率限制&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 在中间件中添加频率限制
export default defineEventHandler(async (event) =&gt; {
  const ip = getClientIP(event)
  const key = `rate_limit:${ip}`
  
  const current = await redis.incr(key)
  if (current === 1) {
    await redis.expire(key, 60) // 1分钟窗口
  }
  
  if (current &gt; 100) { // 每分钟最多100个请求
    setResponseStatus(event, 200)
    return {
      code: API_CODES.FORBIDDEN,
      message: &apos;请求过于频繁，请稍后再试&apos;,
      data: null,
      timestamp: Date.now(),
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;1. Token 刷新失败&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 用户频繁被要求重新登录&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查 Redis 中的 refresh token 是否存在&lt;/li&gt;
&lt;li&gt;确认 refresh token 的过期时间设置&lt;/li&gt;
&lt;li&gt;检查网络请求是否正常到达服务器&lt;/li&gt;
&lt;li&gt;确认前端 token 刷新逻辑是否正确触发&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 鉴权中间件不生效&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 未登录用户可以访问受保护的 API&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查路由是否在白名单中&lt;/li&gt;
&lt;li&gt;确认中间件的执行顺序&lt;/li&gt;
&lt;li&gt;检查 Authorization 头是否正确传递&lt;/li&gt;
&lt;li&gt;验证 JWT 解析逻辑&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. 前端错误处理不正确&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 错误信息显示不准确或不显示&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查步骤&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查 onResponse 和 onResponseError 的处理逻辑&lt;/li&gt;
&lt;li&gt;确认错误代码的匹配是否正确&lt;/li&gt;
&lt;li&gt;验证 toast 组件是否正常工作&lt;/li&gt;
&lt;li&gt;检查控制台是否有 JavaScript 错误&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本鉴权系统提供了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;: 双 Token 机制，JWT + Redis 存储&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户体验&lt;/strong&gt;: 自动 token 刷新，无感知续期&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发友好&lt;/strong&gt;: 统一的错误处理，清晰的错误代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可维护性&lt;/strong&gt;: 模块化设计，易于扩展和修改&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型安全&lt;/strong&gt;: 完整的 TypeScript 支持&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item><item><title><![CDATA[2 个月，17 斤，且每天加班]]></title><description><![CDATA[从 6 月 19 号开了一张半年期的游泳健身卡到现在]]></description><link>https://zzao.club/post/report/weekly-report-10</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-10</guid><pubDate>Mon, 18 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从 6 月 19 号开了一张半年期的游泳健身卡到现在&lt;/p&gt;
&lt;p&gt;2 个月，我减了 17 斤（96.2 到 88.7）。&lt;/p&gt;
&lt;p&gt;我在一个月的时候做了&lt;a href=&quot;https://zzao.club/post/report/weekly-report-06&quot;&gt;《第一次总结》&lt;/a&gt;，里面有我的身体情况背景，以及第一个月的饮食情况&lt;/p&gt;
&lt;p&gt;这一次记录一下第二个月的不同点&lt;/p&gt;
&lt;h2&gt;撸铁三十分钟&lt;/h2&gt;
&lt;p&gt;从第六周开始，我的&lt;strong&gt;撸铁项目改成了每天只练一个部分&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实就是每次只用一个器械，重量就是&lt;strong&gt;一组做 15 个左右&lt;/strong&gt;就力竭了的重量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每天 30 分钟左右的时间，10 - 15 组。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第 6,7 周反应比较强烈，每次做完能感觉到对应部位有些疼痛。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我是完全不在乎健身效果的&lt;/strong&gt;，所以我也不强迫自己一定要做几组，一定要什么重量。&lt;/p&gt;
&lt;p&gt;就是&lt;strong&gt;完全看昨晚休息的好不好&lt;/strong&gt;，今天有精神，就多做几个。精神头差一些，就少做。明显的疲惫，那中午就直接不去了。&lt;/p&gt;
&lt;p&gt;而且更强的肌肉，更好的关节灵活度，&lt;strong&gt;除了对于年轻人求偶&lt;/strong&gt;，我只能理解为：&lt;/p&gt;
&lt;p&gt;一、有个人追求，喜欢健身，喜欢大肌肉。&lt;br&gt;
二、像是进了游戏里的排位赛一样，被体系给圈住了。&lt;/p&gt;
&lt;p&gt;所以，我这可能不叫健身，叫热身。&lt;/p&gt;
&lt;h2&gt;游泳三十分钟&lt;/h2&gt;
&lt;p&gt;热身完了，淋浴，然后去游泳。热身还是有效果的，关节活动开了，下水比较舒服。&lt;/p&gt;
&lt;p&gt;本月和上个月&lt;strong&gt;最大的区别就是游的快了，游的时间长了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一个月蛙泳百米配速普遍在 &lt;strong&gt;3:10 到 3:00&lt;/strong&gt; 左右。但这个月维持在 &lt;strong&gt;2:30-2:50&lt;/strong&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;中午总共大概游 500 - 800 米，25 米的标准池子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么时候能一口气 1000 米！！！&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;饮食&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;首先断糖，也就是饮料。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一个月我早饭喜欢吃莱芜火烧 2 个，午饭吃安徽板面+鸡腿+卤蛋+豆干 或者 一份水饺，晚饭基本不吃。&lt;/p&gt;
&lt;p&gt;第二个月查了查热量。&lt;/p&gt;
&lt;p&gt;俩火烧的热量着实是高，板面也是又咸又油又辣，水饺比较健康但是热量也不低（可能要1000 大卡）&lt;/p&gt;
&lt;p&gt;于是第二个月改变了一下饮食&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;早上开始吃玉米+卤蛋&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122825.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不得不吐槽一下，玉米的质量真的参差不齐，有时候玉米甜的像是在糖水里泡的似的，有时候黏玉米又不黏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作日早上玉米+卤蛋的次数大概有 70%&lt;/strong&gt;，其他吃的我想不大起来了，大概是煎饼果子、莱芜火烧、半笼小笼包+鸡蛋 这几种。&lt;/p&gt;
&lt;p&gt;可能是有负罪感，大脑强制忘了，不能赖我。&lt;/p&gt;
&lt;p&gt;周末早上一般随便对付两口，不咋吃，太懒了，起床之后洗漱一下就该带着孩子出门逛超市了，要不就是回老家。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中午大概是轻食三明治、超意兴、板面套餐、水饺、麻辣烫。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;点外卖这种&lt;strong&gt;轻食三明治，标的大概 400 大卡&lt;/strong&gt;。比较低了。&lt;/p&gt;
&lt;p&gt;但是我有三次中午临近饭点，点&lt;strong&gt;京东外卖&lt;/strong&gt;，压根没人配送。要不就是配送的人太奇葩，不认识路也不认识导航，愣是连着三次没吃上。然后就再也没用过京东外卖，转&lt;strong&gt;淘宝团购&lt;/strong&gt;去了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;超意兴，一般吃 1-2 个卤蛋，两份青菜或一荤一素，可能还有个炸鸡腿。 不吃主食。&lt;/strong&gt; 不知道多少大卡，&lt;strong&gt;感觉有 8 分饱&lt;/strong&gt;，可能是鸡蛋很顶食。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122827.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;大概这样，馒头就吃两口，因为今天菜太少了，我来的很晚。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122828.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是生日那天吃的，加了个鸡腿。窝窝头因为中午来的太晚，不一定每次都有。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;板面和水饺还是正常吃&lt;/strong&gt;，光吃轻食也是挺难受的，拉屎费劲。 还是吃点碳水舒服。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122829.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;麻辣烫&lt;/strong&gt;主要是为了吃菜，&lt;strong&gt;骨汤锅底，全青菜，零星几个火锅丸子，一小撮手擀面&lt;/strong&gt;。不过吃的次数很少，一个月 1-2 次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122830.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;超意兴是我最近的选择，比较干净卫生了，每天能吃到不同的菜，也能吃到肉。&lt;/p&gt;
&lt;p&gt;公司楼下真的全是碳水爆炸的小饭馆，板面和水饺绝对算是热量比较低的了...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;晚饭还是不吃&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;周末&lt;/h2&gt;
&lt;p&gt;第一个月我就没有一个周末是闲着的，完全没有时间去锻炼。并且要出门大吃一顿，&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果吃饱喝足，回来后体重就上浮 2 斤左右&lt;/strong&gt;，然后需要三天时间才能开始缓慢下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果控制了自己&lt;/strong&gt;，没吃多，并且吃的时候挑着青菜和肉类吃，不吃主食。&lt;strong&gt;回来体重最多上浮一斤，并且很快可以继续下降&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第二个月也没有闲下来，都是出远门。但吃的情况自己控制的好一些，只有一次体重的上浮。并且有一次晚上去吃了夜市，也是大幅上涨了 2 斤，并且难以消耗掉，持续了一周才恢复下降趋势，而且下降很慢。&lt;/p&gt;
&lt;p&gt;可能是身体对这来之不易的放纵十分珍惜，趁机储备了不少！&lt;/p&gt;
&lt;p&gt;总之，这 8 周多，出门在外，能控制到什么程度全靠自己的意志。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;去健身的次数应该不超过五次。全靠上班时间。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但周末我觉得还是该吃就吃，长期来看是为了健康，减肥也不图一时。&lt;/p&gt;
&lt;p&gt;平时足够克制的话，其实胃已经小很多了，吃不了太多就饱。&lt;/p&gt;
&lt;p&gt;吃的时候就尽量吃一些干净的食材，不要大油大盐的。哪怕不是减肥期间，也是不小的负担。&lt;/p&gt;
&lt;h2&gt;自己带饭？&lt;/h2&gt;
&lt;p&gt;尝试卤了一块牛腱子肉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122831.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;和中午点的荞麦面的轻食一起吃。其实算比较多的了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122832.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;就这么一次&lt;/strong&gt;是自己带着牛腱子肉来的公司，太麻烦了实在是。&lt;/p&gt;
&lt;p&gt;前一天晚上开始卤，铸铁锅，两小时，卤到晚上 1 点（因为我在加班），没在锅里继续泡，拿出来冷却后放在冷藏了。&lt;/p&gt;
&lt;p&gt;不带筋的地方嚼不烂，好难啊！！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;牛腱子 36 一斤，买了不到两斤，感觉可以吃四次？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为我是先去游泳健身，再吃饭，所以自己带饭的话没有场地吃。&lt;/p&gt;
&lt;p&gt;后面的话再卤一块试试，多泡泡。然后切几片去超意兴吃。&lt;/p&gt;
&lt;p&gt;成功了的话我就分享一下配方，不成功就发了。&lt;/p&gt;
&lt;h2&gt;加班和睡眠&lt;/h2&gt;
&lt;p&gt;接了一些回头客的私活，基本都是以前找过我兼职，觉得还可以，后续又找来了。&lt;/p&gt;
&lt;p&gt;也有一些新的朋友的活，总之这快俩月接了几个活并行，导致我晚上一直加班干。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般到 12 点左右去睡觉，偶尔到 2 点。&lt;/strong&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;所以不要惊讶为什么工作了 8 小时回家还能干这么久。&lt;/p&gt;
&lt;p&gt;我印象里，这 8 年的开发生涯，只有少数的时间能一天用满 8 小时。&lt;/p&gt;
&lt;p&gt;集中精力 8 小时是非常累的，时间长了根本熬不住。&lt;/p&gt;
&lt;p&gt;所以，但凡公司的活累一些，我可能也不会接这些活。&lt;/p&gt;
&lt;p&gt;最大的感受就是因为压榨了自己太多时间，所以几点睡觉对第二天状态影响非常大。幸好手里活已经陆续在收尾，后续应该不会这样并行接活了，&lt;strong&gt;11 点开始睡觉是比较合适的&lt;/strong&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;&lt;strong&gt;改成中午去锻炼之后，总感觉大脑吸了很多氧，有点兴奋的感觉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有时候早上起来感觉自己不是最佳状态，有一点点乏力，但是中午锻炼完后这种感觉就没了。&lt;/p&gt;
&lt;p&gt;然后晚上下了班到家，基本就开始加班干活，正好也避开晚饭，干几个小时过去也不觉得饿了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不过这不是长久之计，一份能够实现睡后收入的副业真的非常重要。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;身体和习惯变化&lt;/h2&gt;
&lt;p&gt;最大的变化在肩部、胸部、后背，变平坦了很多，所以我觉得和健身有很大的关系，因为我只用这几个部分相关的器械。&lt;/p&gt;
&lt;p&gt;只靠节食减肥的话，形体上应该不会有这么大的变化。&lt;/p&gt;
&lt;p&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;这一点哪怕是停止运动了也应该坚持下去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;毕竟岁月不饶人呐。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其他方面的变化没那么明显，就是整体都瘦了一圈。&lt;/p&gt;
&lt;p&gt;说胖一起涨，说瘦也都一起没了，挺好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;早上起床以前嘴里有时候会发苦。&lt;/strong&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;应该是也是和精力的上升有关。&lt;/p&gt;
&lt;p&gt;而且也没喝咖啡，本来打算喝茶来着，后来发现 700 的茶 和 300 的茶简直不是一个东西。&lt;/p&gt;
&lt;p&gt;700 的⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508191122833.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然也不排除老板把 100 的茶当 300 的卖，但是那个 700 的茶平时喝还是有点奢侈了，300 的茶喝起来和几十一百的没区别，口感太差，所以就不喝了。&lt;/p&gt;
&lt;h2&gt;后续计划&lt;/h2&gt;
&lt;p&gt;这次是以半年的健身卡为期，本来不想定太大的目标，先到 85kg，没想到这么快就要到了。&lt;/p&gt;
&lt;p&gt;所以后面的四个月，会慢慢调整，直到减到 75kg。&lt;/p&gt;
&lt;p&gt;而且现在越来越觉得，健身和游泳只是运动的一种手段，后续有了运动习惯之后，我应该会拓展到其他运动方式上。&lt;/p&gt;
&lt;p&gt;日常记录的话，我主要是发在自己的博客动态上，写长文时再去翻一翻，回忆一下。但是目前看来利用率还是不高，只在能接触电脑的时候才能想起来记录。后面再做个 App 增加一下入口吧。&lt;/p&gt;
&lt;p&gt;饮食和运动方面，目前来看好像可以继续保持，如果进入瓶颈期，我再进行调整吧。&lt;/p&gt;
&lt;p&gt;一起加油吧💪&lt;/p&gt;</content:encoded></item><item><title><![CDATA[我为什么开始信任 AI 了]]></title><description><![CDATA[一开始我是不相信 AI 写的代码的。]]></description><link>https://zzao.club/post/report/weekly-report-08</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-08</guid><pubDate>Sun, 17 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一开始我是不相信 AI 写的代码的。&lt;/p&gt;
&lt;p&gt;一是我自己的原因，使用时给出的提示词过于简单，又期望它给出完美的代码，等一运行感觉相差甚远。&lt;/p&gt;
&lt;p&gt;二是使用 AI 打破了固有的认知惯性，类似于机器批量和手工制作的区别。&lt;/p&gt;
&lt;p&gt;这对于把写出一手漂亮、优雅代码奉为圭臬的理想主义者来说是有点难以转变的，等同于给一个合资车的拥护者推荐国产车。&lt;/p&gt;
&lt;p&gt;但是当我让 AI 仿照我的另一个功能代码编写新的代码，并给出明确的需求以及思路后，它完成的相似度和准确度接近满分。&lt;/p&gt;
&lt;p&gt;如果不是因为我知道我的英语水平可能起不出这种变量名，都有点分辨不出这是不是手写的。&lt;/p&gt;
&lt;p&gt;于是我开始从某个功能，扩展到单个页面，然后再到更长更复杂的指令。&lt;/p&gt;
&lt;p&gt;从免费的 &lt;code&gt;cursor&lt;/code&gt; 到薅完 &lt;code&gt;Kiro&lt;/code&gt; 的全部额度&lt;/p&gt;
&lt;p&gt;然后再 &lt;code&gt;Trae&lt;/code&gt; 冲了一个月会员，&lt;code&gt;10&lt;/code&gt; 天把 &lt;code&gt;600&lt;/code&gt; 次请求全部用完，又充了额外的 &lt;code&gt;600&lt;/code&gt; 次请求后。&lt;/p&gt;
&lt;p&gt;我总共使用 AI 完成了 &lt;code&gt;3&lt;/code&gt; 个 uniapp 的微信小程序，&lt;code&gt;1&lt;/code&gt; 个原生小程序，&lt;code&gt;1&lt;/code&gt; 个若依的后台管理系统，&lt;code&gt;1&lt;/code&gt; 个 uniapp 的 uts 原生安卓插件，&lt;code&gt;1&lt;/code&gt; 个 &lt;strong&gt;Tauri2 + Nuxt4&lt;/strong&gt; 的&lt;strong&gt;跨端 App Demo&lt;/strong&gt;，在这一个月多月的时间内。&lt;/p&gt;
&lt;p&gt;所以，为什么开始信任 AI 了？&lt;/p&gt;
&lt;p&gt;因为它真的开始产生价值了。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI 写出的代码怎么才能不跑题]]></title><description><![CDATA[最近用 AI 写了几个项目，有了一点点心得，所以来和大家分享一下，如何用 AI 写出高质量的代码，而不是写完再去返工。]]></description><link>https://zzao.club/post/report/weekly-report-09</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-09</guid><pubDate>Sun, 17 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近用 AI 写了几个项目，有了一点点心得，所以来和大家分享一下，如何用 AI 写出高质量的代码，而不是写完再去返工。&lt;/p&gt;
&lt;p&gt;想要不跑题，首先 AI 得行，因为毕竟是它来写。&lt;/p&gt;
&lt;p&gt;我最常用的是这三个 &lt;code&gt;Claude 4&lt;/code&gt;，&lt;code&gt;kimi k2&lt;/code&gt;， &lt;code&gt;gemini2.5-pro&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Claude&lt;/code&gt; 是综合实力最强的，不管是解决特殊问题还是执行长任务，都完成的很出彩，但前提是它熟知了上下文，像是 Trae，对于上下文的处理明显不如 Cursor。这种情况下，&lt;strong&gt;编码前描述出目前的需求，再圈一两个典型页面让它去查看一下&lt;/strong&gt;，会比直接让它写好很多。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kiro&lt;/code&gt; 里的 &lt;code&gt;claude&lt;/code&gt; 是能用，但是每天会有好几次 &lt;code&gt;retry&lt;/code&gt;，用起来不痛快。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cursor&lt;/code&gt; 的免费版则是不能用 &lt;code&gt;claude 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以我现在主要是在付费使用 &lt;code&gt;Trae&lt;/code&gt; 国际版&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gemini2.5-pro&lt;/code&gt; 我会用于 &lt;code&gt;Claude&lt;/code&gt; 无法解决的某个具体问题上，通常都有奇效。比如我用 &lt;code&gt;Claude4&lt;/code&gt; 写了一个拼图、分图、长图 &lt;a href=&quot;https://zzao.club/imgx&quot;&gt;工具&lt;/a&gt;，写到最后的长图功能时，&lt;code&gt;claude&lt;/code&gt; 怎么也无法解决 bug，换成 &lt;code&gt;gemini2.5-pro&lt;/code&gt; 梳理了一遍很快就找到了问题所在。&lt;/p&gt;
&lt;p&gt;但它最大的问题是长任务执行不下去，我没有往深了挖掘它的用法，只察觉到它在执行长任务时会经常陷入死循环，不停的处理一个问题，而且（对于长任务）思考特别慢。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kimi k2&lt;/code&gt; 现在的编码能力也可圈可点，在涉及小程序这种具有国内特色的问题上，它掌握的知识库明显要比 &lt;code&gt;Claude&lt;/code&gt; 要多一些，很好使，在处理一些短的指令时速度也很快，准确度也可以。有时候我忘记切换模型，一直用的 &lt;code&gt;k2&lt;/code&gt;，还以为是在用 &lt;code&gt;claude&lt;/code&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;以我目前接触的项目来说，还是要以维护和迭代老项目为主。&lt;/p&gt;
&lt;p&gt;哪怕开了新项目，公司里的研发主导者还是会给你一个对他们来说可控的已经初始化的项目。而不是让你去 AI 自由发挥，因为代码最终是给人看的，是要维护的，虽然界面看起来可能没错，但是出了新需求就要重构或者去迭代很不熟悉的代码也是公司不能接受的。&lt;/p&gt;
&lt;p&gt;所以第一步，就是&lt;strong&gt;梳理项目整体的结构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这一步也是要AI做的，因为它整理的更快。整理的内容包含整体技术栈，主要工具类、插件、组件、hooks 在哪里存放和使用，代码风格，注释风格等。&lt;/p&gt;
&lt;p&gt;这里其实也是可以输出设计风格的，但是 AI 很难百分百还原设计图，哪怕你把所有代码发给它。所以对于客户要求的 UI，还是要自己调整到完美状态。&lt;/p&gt;
&lt;p&gt;AI 整理出来的 md 文档一般都带有示例，输出完毕再自己补充和纠错一下。&lt;/p&gt;
&lt;p&gt;这一步决定了你后续的长任务准确度，限制它的过度自由发挥。&lt;/p&gt;
&lt;p&gt;大部分 &lt;code&gt;AI IDE&lt;/code&gt; 支持自动或手动携带这个 &lt;code&gt;rule.md&lt;/code&gt; ，执行长任务时最好带上，&lt;strong&gt;尤其是你切了聊天窗口，有的编辑器就不会自动携带之前窗口的上下文了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后就开始下达具体需求的任务，这里其实&lt;strong&gt;有点考验开发者一个综合能力。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先你需要知道这个需求具体是什么逻辑&lt;/strong&gt;，因为 AI 提升的你的编码能力。但是如果一开始的需求就描述不清楚，任谁也做不好这个活。&lt;/p&gt;
&lt;p&gt;这一步其实不算难，就&lt;strong&gt;把自己当成一个产品经理，把自己要做的东西以文字的形式描述出来就可以了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AI&lt;/code&gt; 是会帮你设计架构以支持你这些需求的，甚至很多时候它设计的架构都有点过了，是咱们平时拧螺丝时压根不会去想的方向。&lt;/p&gt;
&lt;p&gt;你同事看到这写代码只会觉得这是你从哪个开源项目上 &lt;strong&gt;copy&lt;/strong&gt; 过来的，肯定不会认为是你写的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以此时，平时勤勤恳恳写代码，学代码，学架构的同学，此刻你的价值将会被呈数倍放大&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;平时的经验会告诉你这个需求应该使用什么样的架构，什么思路，你在&lt;strong&gt;写提示词不过是在写自己的解题思路罢了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从怎么开始写，用什么写，用哪几个函数实现，最终实现什么，然后最后再加几个编码的注意点。然后回车，把具体功能的实现交给 &lt;code&gt;AI&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;看着 &lt;code&gt;AI&lt;/code&gt; 在不同文件之间实现功能，就像是看学生在做题一样，哪里错了就拒绝，这绝对不是一个非程序或者普通开发者所能具备的能力。&lt;/p&gt;
&lt;p&gt;所以哪怕是 &lt;code&gt;AI&lt;/code&gt; 发展的如何迅速的今天，你也不会浪费自己的努力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是你的老板觉得你浪费了他的钱的话，那就另说了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;毕竟 &lt;code&gt;AI&lt;/code&gt; 对咱们开发者是考验，对老板也是考验。&lt;/p&gt;
&lt;p&gt;有的老板愁单子不够多，&lt;code&gt;AI&lt;/code&gt; 提效了是好事，能吃下更多单子。&lt;/p&gt;
&lt;p&gt;有的老板只有这一亩三分地，你用 &lt;code&gt;AI&lt;/code&gt; 节省了他一半的成本，那你就是他多余的成本。&lt;/p&gt;
&lt;p&gt;此时，&lt;code&gt;AI&lt;/code&gt; 写出跑题的代码都不是什么坏事了。&lt;/p&gt;
&lt;p&gt;越乱越需要人治理。&lt;/p&gt;
&lt;p&gt;所以，怎么写出不跑题的代码？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个靠谱的 &lt;code&gt;AI&lt;/code&gt; ，外加靠谱的开发，绝对不会跑题。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;题外话&lt;/h2&gt;
&lt;p&gt;看到这里你可以发现，我说的这些有一个前提：&lt;strong&gt;针对公司项目&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而你上网能看到的是什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;纯 Vibe Coding ，xx 天完成一个 xxx&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此时你看到的是对传统编码方式的无情碾压&lt;/p&gt;
&lt;p&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;而公司项目开始开发时，已经是有人付钱了，所以要按付款方的标准来完成。&lt;/p&gt;
&lt;p&gt;这已经是两种岗位、两种性格之间的差异了，毕竟销售不是谁都能做好的，机会也不是所有人都能洞察的。&lt;/p&gt;
&lt;p&gt;所以，我想，我们可能已经在适合自己的位置上了。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Prisma 迁移到 Dizzle 后的基线问题]]></title><description><![CDATA[把 prisma 迁移到了 dizzle]]></description><link>https://zzao.club/post/nuxt/orm/from-prisma-to-dizzle</link><guid isPermaLink="true">https://zzao.club/post/nuxt/orm/from-prisma-to-dizzle</guid><pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;把 &lt;code&gt;prisma&lt;/code&gt; 迁移到了 &lt;code&gt;dizzle&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;dizzle&lt;/code&gt; 的 &lt;code&gt;schema&lt;/code&gt; 是直接让 &lt;code&gt;AI&lt;/code&gt; 生成的，完全没看 &lt;code&gt;dizzle&lt;/code&gt; 的官方文档&lt;/p&gt;
&lt;p&gt;然后也遇到了已有项目迁移到 &lt;code&gt;Prisma&lt;/code&gt; 一样的问题：&lt;strong&gt;设置基线&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Dizzle 也是用一个 migrations 文件夹来管理所有迁移。&lt;/p&gt;
&lt;p&gt;初次生成时&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;drizzle-kit generate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的 &lt;code&gt;sql&lt;/code&gt; 都是 &lt;code&gt;CREATE TABLE&lt;/code&gt; ，这显然不应该在执行 &lt;code&gt;migrate&lt;/code&gt; 命令时应用&lt;/p&gt;
&lt;p&gt;&lt;em&gt;dizzle 的 migrate 和 prisma 的 deploy 是一样的，就是把变更应用到当前数据库的意思&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;因为有 &lt;code&gt;prisma&lt;/code&gt; 的&lt;a href=&quot;https://zzao.club/post/nest/nest-from-typeorm-to-prisma&quot;&gt;迁移经验&lt;/a&gt;了，所以也能大概猜到 &lt;code&gt;dizzle&lt;/code&gt; 也有类似设置基线的方式&lt;/p&gt;
&lt;p&gt;于是问了一下 &lt;code&gt;Kimi&lt;/code&gt; 就得到了答案&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;generate&lt;/code&gt; 命令之后，得到了一个 &lt;code&gt;migrations&lt;/code&gt; 文件夹，以及一个 &lt;code&gt;sql&lt;/code&gt; 文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;CREATE TABLE xxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到这个文件的 &lt;code&gt;hash&lt;/code&gt; 部分，也就是：&lt;code&gt;0000000000_xxxx_xxx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在配置文件中配置好 &lt;code&gt;migrations&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
export default defineConfig({
  migrations: {
    prefix: &apos;timestamp&apos;,
    table: &apos;__drizzle_migrations__&apos;,
    schema: &apos;public&apos;,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一开始我配错了，配成了 &lt;code&gt;__drizzle_migrations&lt;/code&gt; ，实测好像他不认这个表名，当然更可能是我不知道参数，但是我懒得去找了。&lt;/p&gt;
&lt;p&gt;登录自己的服务器&lt;/p&gt;
&lt;p&gt;进入数据库，如果你还没有 &lt;strong&gt;drizzle_migrations&lt;/strong&gt; 这个表，可以先跑一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;drizzle-kit migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会报错，但是没关系，只要帮咱创建好了表就行&lt;/p&gt;
&lt;p&gt;然后手动插入一条记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO __drizzle_migrations__ (hash, created_at)
VALUES (&apos;0000000000_xxxx_xxx&apos;, NOW(6));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时再去部署自己的项目时，就不会执行初始化生成的那个 sql 了，因为我们已经手动标记它已经被执行过了。&lt;/p&gt;
&lt;p&gt;⚠️&lt;strong&gt;插入的这条记录注意一下&lt;/strong&gt;，要和 &lt;code&gt;_journal.json&lt;/code&gt; 里的值保持一致，不然后续的 migrations 都不会被执行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508201440546.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
至此结束&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;你别说 &lt;code&gt;kimi&lt;/code&gt; 给的这种方式比以前迁移 &lt;code&gt;prisma&lt;/code&gt; 更直接，更好理解了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prisma&lt;/code&gt; 的各种命令反而更绕～&lt;/p&gt;
&lt;p&gt;周末愉快 🚀&lt;/p&gt;</content:encoded></item><item><title><![CDATA[关于Nuxt4 build 后终端没有退出的问题]]></title><description><![CDATA[最近几日，我用 Nuxt4 + Tauri2 写了一个小 Demo。]]></description><link>https://zzao.club/post/issues/nuxt-build-hangs</link><guid isPermaLink="true">https://zzao.club/post/issues/nuxt-build-hangs</guid><pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近几日，我用 &lt;code&gt;Nuxt4&lt;/code&gt; + &lt;code&gt;Tauri2&lt;/code&gt; 写了一个小 Demo。&lt;/p&gt;
&lt;p&gt;苦于不知道怎么设计 UI 时，我向 &lt;code&gt;kimi&lt;/code&gt; 表达了我的 &lt;code&gt;claude4&lt;/code&gt; 写不出我想要的像素风格的事儿&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kimi&lt;/code&gt; 二话不说，给了我一套详细的方案，让我拿着去喂给 &lt;code&gt;claude&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后我就成功的搞出了一套看起来像那么回事的像素风格界面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508141200458.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202508141200459.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;搞完后心满意足，感觉有点东西&lt;/p&gt;
&lt;p&gt;过了几天，不忙了，寻思把我的博客也搞一搞吧。&lt;/p&gt;
&lt;p&gt;于是我把这套规则复制过来，让 &lt;code&gt;claude&lt;/code&gt; 继续给我在博客上重构样式&lt;/p&gt;
&lt;p&gt;重构完了，我脑子一热，要不升一下 &lt;code&gt;nuxt4&lt;/code&gt; 试试吧&lt;/p&gt;
&lt;p&gt;本地环境没问题！&lt;/p&gt;
&lt;p&gt;升完了 &lt;code&gt;nuxt4&lt;/code&gt;，要不升一下 &lt;code&gt;nuxt/content&lt;/code&gt; 吧&lt;/p&gt;
&lt;p&gt;升完了也是没问题！&lt;/p&gt;
&lt;p&gt;然后本地试了一下打包，也没发现问题&lt;/p&gt;
&lt;p&gt;晚上回到家，换了一台电脑，发现 &lt;code&gt;prisma/nuxt&lt;/code&gt; 有问题&lt;/p&gt;
&lt;p&gt;第二天到了公司，我直接把 &lt;code&gt;prisma&lt;/code&gt; 给换了，直接全部重构成了 &lt;code&gt;dizzle&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后又把 &lt;code&gt;pinia&lt;/code&gt; 给删了，用 &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useStorage&lt;/code&gt; 实现&lt;/p&gt;
&lt;p&gt;重构工作量巨大，但是一天内都搞完了&lt;/p&gt;
&lt;p&gt;此时本地打包也没问题&lt;/p&gt;
&lt;p&gt;但是走了一下 &lt;code&gt;gitea&lt;/code&gt; 的 &lt;code&gt;actions&lt;/code&gt; 发现很久都没部署完，上去一看，卡在 &lt;code&gt;build&lt;/code&gt; 这一步了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;✔ You can preview this build using node .output/server/index.mjs      
&lt;/code&gt;&lt;/pre&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;code&gt;Nuxt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;很快啊，第二天就恢复了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;又开始用 &lt;code&gt;AI&lt;/code&gt; 分析问题，以及去 &lt;code&gt;nuxt&lt;/code&gt; 相关的仓库里去找 &lt;code&gt;issue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;最后还真被我找到了 &lt;a href=&quot;https://github.com/nuxt/cli/issues/169&quot;&gt;https://github.com/nuxt/cli/issues/169&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;hooks: {
    close: () =&gt; {
      // @see https://github.com/nuxt/cli/issues/169#issuecomment-1729300497
      // Workaround for https://github.com/nuxt/cli/issues/169
      process.exit(0)
    },
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后在 hooks 里加了个钩子解决了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;- the stall is likely triggered (but not caused) by [Nuxt Content Assets](https://github.com/davestewart/nuxt-content-assets/issues/49) (I&apos;m the author of this module)
- however, it should be solved in the project&apos;s config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决了，也老实了&lt;/p&gt;
&lt;p&gt;再也不瞎折腾了 🤪&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Vibe Coding 一天内完成图片编辑工具（ Trae 又行了？]]></title><description><![CDATA[最近一段时间高强度使用 Cursor、Kiro、Trae 这三大编辑器。]]></description><link>https://zzao.club/post/imgx/use-trae-build-imgx</link><guid isPermaLink="true">https://zzao.club/post/imgx/use-trae-build-imgx</guid><pubDate>Tue, 22 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近一段时间高强度使用 &lt;code&gt;Cursor&lt;/code&gt;、&lt;code&gt;Kiro&lt;/code&gt;、&lt;code&gt;Trae&lt;/code&gt; 这三大编辑器。&lt;/p&gt;
&lt;p&gt;最开始用 &lt;code&gt;Cursor&lt;/code&gt;， 用的 fake cursor 插件，只嫖一个免费版（claude 3.5）。 一用真香啊，能设置 &lt;code&gt;rules&lt;/code&gt;，不跑题，交互也丝滑。&lt;/p&gt;
&lt;p&gt;然而用的多了，没两天时间，感觉就不守规矩了。&lt;code&gt;rules&lt;/code&gt; 要么就是漏下，要不就完全不遵守了。感觉是要从我手下离职，要报复我一样...🥲&lt;/p&gt;
&lt;p&gt;自动从聊天中中总结一些临时规则虽然不错，但换了电脑好像就没了，隔一段时间好像也会自动没（？）。&lt;/p&gt;
&lt;p&gt;这时候开始出现一些 &lt;code&gt;Kiro&lt;/code&gt; 的宣传文章，我正好有用，就火速下载体验。&lt;/p&gt;
&lt;p&gt;谁让他能用 &lt;code&gt;Claude4&lt;/code&gt; 呢&lt;/p&gt;
&lt;p&gt;体验一番后，感觉他的设计思路很不错，尤其是 Spec 模式。但我还是 Vibe 模式用的多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116762.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也可能是 &lt;code&gt;Claude4&lt;/code&gt; 的加持效果太好，从 &lt;code&gt;3.5&lt;/code&gt; 到 &lt;code&gt;4&lt;/code&gt;，像是从村里进城一样，哪哪都是好啊...&lt;/p&gt;
&lt;p&gt;唯一的问题可能在我&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116763.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这种可能是网络问题导致重试情景时常出现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而且思考时间要比 &lt;code&gt;cursor&lt;/code&gt; 和 &lt;code&gt;trae&lt;/code&gt; 长很多。&lt;/p&gt;
&lt;p&gt;由于思考时间长（可能是上下文多），&lt;strong&gt;还会出现死循环&lt;/strong&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;比如我这个只用了一天时间就做出来的&lt;a href=&quot;https://zzao.club/imgx&quot;&gt;图片编辑工具&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zzao.club/imgx&quot;&gt;https://zzao.club/imgx&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116764.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
右键出现操作面板，支持无限分割格子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116765.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也可以分割后再把格子删掉。删掉后又可以让格子补齐它的空位。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116766.gif&quot; alt=&quot;&quot;&gt;&lt;br&gt;
也可以设置圆角、间距。&lt;/p&gt;
&lt;p&gt;同时也支持直接粘贴刚刚复制的图片。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;理论上可以出现任何布局&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116767.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;没有图片的格子就会被绘制成透明背景。&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;分图功能&lt;/strong&gt;，可以将上传的图片，分布到每个格子中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116768.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也支持再次拆分格子，再次上传图片&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116769.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果再次切换回拼图模式，就可以再次重新给小格子上传图片&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116770.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，再次切换到分图，就会重新把新上传的图片重新分割。&lt;/p&gt;
&lt;p&gt;然后就是一个简单的长图功能。支持横向和纵向。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116771.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以选完再上传图片，也支持传完再切换比例。&lt;/p&gt;
&lt;p&gt;以上就是全部功能了（目前纯前端功能），&lt;strong&gt;总体用时在 8 小时以内&lt;/strong&gt;，具体几个小时记不清了，因为我是每天写一部分。&lt;/p&gt;
&lt;p&gt;比较关键的点就是： &lt;strong&gt;要先定一个总体的架构&lt;/strong&gt;，不管是自己写，还是让 AI 自己写，先把基本思路完全敲定。&lt;/p&gt;
&lt;p&gt;比如我这个图片工具，&lt;strong&gt;核心就是维护一组数组数据&lt;/strong&gt;，页面渲染完全依靠这组数据，调整参数就是调整数据。导出也是用数据在原生 Canvas 上重绘完全一致的页面，然后导出。&lt;/p&gt;
&lt;p&gt;这样不管是分割格子，还是上传图片，分割图片，长图，&lt;strong&gt;本质都是处理这组数据&lt;/strong&gt;。扩展性还是很高的，就算完全转为后端渲染也很轻松支持。&lt;/p&gt;
&lt;p&gt;如果是采用了依靠 &lt;strong&gt;html&lt;/strong&gt; 渲染内容，在使用 &lt;strong&gt;html2canvas&lt;/strong&gt; 导出 &lt;strong&gt;html&lt;/strong&gt; 内容。一是把场景限制在了浏览器上，二是需要特殊处理元素上的交互，避免导出不需要渲染的元素、辅助线等。&lt;/p&gt;
&lt;p&gt;至于为什么做这种图片工具。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503121139543.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这算是我在 25 年 2 月份构思的图片工具的一部分。&lt;/p&gt;
&lt;p&gt;一开始先做的第一部分（IMG），也就是文字生图片，还在 V2ex 发了贴，&lt;a href=&quot;https://v2ex.com/t/1110730&quot;&gt;点击查看&lt;/a&gt;，很快就 100 多 star 了。&lt;/p&gt;
&lt;p&gt;但可能大部分只是有兴趣，提出问题的人很少，所以我在迭代完所有内容，并且经历了一次比较大的重构。目前支持自定义模板，自定义预设，然后使用一个预设码去拿到自己想要的图片。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgx.zzao.club/008/default&quot;&gt;https://imgx.zzao.club/008/default&lt;/a&gt; 比如这个链接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/008/default&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/008/%E9%9A%8F%E6%84%8F%E6%9B%B4%E6%94%B9%E6%A0%87%E9%A2%98/%E5%AF%B9%E5%AF%B9%E5%AF%B9/%E5%92%8C%E5%89%AF%E6%A0%87%E9%A2%98/%E8%8F%9C%E9%B8%9F&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也支持直接触发下载，点开链接即可保存图片 （不过微信了不让你打开下载，会提示去浏览器&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgx.zzao.club/008/default?download=1&quot;&gt;https://imgx.zzao.club/008/default?download=1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当初这个功能，吭哧吭哧做了一两个月，&lt;strong&gt;那时候我还是古法敲制的代码...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而图片处理这部分，一天不到就用 AI 搞完了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用 &lt;code&gt;Kiro&lt;/code&gt; 完成了拼图和分图功能后，我正在头疼没额度了怎么办&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;突然 Trae 给我自动续费了。。。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116772.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我还在心里咒骂 &lt;code&gt;Trae&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;本着钱都花了就试试吧的想法打开了 &lt;code&gt;Trae&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后惊喜的发现它更新了 &lt;code&gt;2.0&lt;/code&gt; 了！&lt;/p&gt;
&lt;p&gt;并且也能和 &lt;code&gt;Cursor&lt;/code&gt; 一样设置 &lt;code&gt;.trae/rules/project_rules.md&lt;/code&gt; ，以前没注意它能不能设置上下文，现在才发现&lt;strong&gt;能设置项目的上下文，也能导入更多的上下文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但这个 &lt;code&gt;rules&lt;/code&gt; 仅仅就是放在这里，供它使用&lt;/strong&gt;，你在对话中让他去操作这个 &lt;code&gt;rules&lt;/code&gt;，自己写入，他完全不理解往哪里写，说明对话模式里没设计和 rules 的交互。&lt;/p&gt;
&lt;p&gt;但是我也比较满意了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507221116773.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;毕竟这么多可以用的模型，2 天内也没发现网络错误之类的问题，很稳定。&lt;/p&gt;
&lt;p&gt;而且我只是让他不要跑出我的把控，所以有 &lt;code&gt;rules&lt;/code&gt; 之后输出的代码就很稳定。&lt;/p&gt;
&lt;p&gt;然后我就继续用 Trae 完成了长图功能。&lt;/p&gt;
&lt;p&gt;非常坎坷，需求太细致了，反而不利于 AI 发挥。&lt;/p&gt;
&lt;p&gt;但是你让它完全自由发挥，十分不可控，扩展性也比较差。&lt;strong&gt;正常的扩展性指的是方便支持更复杂的功能，毕竟以前需要手写。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现在对于 AI 来说，什么扩展性不扩展性的，就算重写整个项目都用不了太长时间。。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另外，&lt;code&gt;SOLO模式用不了，没码！&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在我畅快的使用 &lt;code&gt;Trae&lt;/code&gt; 时，前线又传来了 Cursor 封锁中国 IP 的消息！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;突然，此刻的 Trae 显得格外好用....&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根本不认识什么 cursor 和 kiro ！&lt;/p&gt;</content:encoded></item><item><title><![CDATA[一个月减了7-8斤，记录饮食和运动]]></title><description><![CDATA[从6月19开始，到今天正好一个月了。记不清这是第几次立志开始减肥了，虽然之前也成功过（一次），但这次是运动量最大的，值得记录一下。]]></description><link>https://zzao.club/post/report/weekly-report-06</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-06</guid><pubDate>Mon, 21 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从6月19开始，到今天正好一个月了。记不清这是第几次立志开始减肥了，虽然之前也成功过（一次），但这次是运动量最大的，值得记录一下。&lt;/p&gt;
&lt;p&gt;目前为止&lt;strong&gt;一个月时间，减了7-8斤&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;先说一下我的基本情况，大概在 9 年前就胖了，而且是（大学期间）一年内快速增重。此后上班后一直没减下来。&lt;/p&gt;
&lt;p&gt;直到工作 3 年后，从一线退居到二线城市工作，打游戏的习惯也正好断了（给我坑的退坑了！&lt;/p&gt;
&lt;p&gt;于是卖掉了主机外设，开始减肥了。&lt;/p&gt;
&lt;p&gt;那次减肥是我第一次大幅的体重下降，大概一个月 10 斤左右，坚持了快三个月。不过&lt;strong&gt;节食&lt;/strong&gt;节的太狠了，&lt;strong&gt;无法再次复制&lt;/strong&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;于是我决定在详细复盘一下&lt;strong&gt;当前&lt;/strong&gt;减肥的过程，给大家参考一下 &lt;strong&gt;30 岁打工仔&lt;/strong&gt;如何&lt;strong&gt;利用空闲时间锻炼&lt;/strong&gt;，以及吃多少能稳定的掉体重。&lt;/p&gt;
&lt;p&gt;此后也会每隔 1-2 周给大家汇报一下，以此也增强自己的信念。&lt;/p&gt;
&lt;h2&gt;运动方面&lt;/h2&gt;
&lt;p&gt;全部是&lt;strong&gt;游泳&lt;/strong&gt; + &lt;strong&gt;撸铁&lt;/strong&gt;，每天&lt;strong&gt;中午一个多小时&lt;/strong&gt;时间。完事就回公司上班。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么不是跑步？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;跑步虽然看似是个人都能跑，但是跑步姿势似乎颇有说法。大体重的人跑起来太容易腿疼膝盖疼了。而且 &lt;code&gt;i 人&lt;/code&gt; + &lt;code&gt;大体重&lt;/code&gt; 跑步，要先跑到一个心理上相对舒适，人少的场地，再开始跑，跑完就是一身汗，再回家洗澡换衣服出门，上下楼和往返时间有点长，比较容易劝退，我坚持过不到一周就放弃了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;怎么确定是游泳？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;游泳我是完全自学，从零学蛙泳，平时在 &lt;code&gt;B站&lt;/code&gt; 武装头脑，然后下水实战。&lt;/p&gt;
&lt;p&gt;我先办了一个 &lt;code&gt;10 日体验卡&lt;/code&gt;，每天找人少的时间点去练习，先漂浮、站立、换气，这三步下来，我在第三次去游泳，已经在练蛙泳动作了。&lt;/p&gt;
&lt;p&gt;也就是可以下水，然后漂，蛙泳，游到 10 来米的地方，站立，转身，再游回去。&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;strong&gt;除了周二泳池维护，有两次周末全家出去玩，连吃两天。其他时间全勤！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;办十日卡那次只是十来米一停，心率一点上不去，就和玩水一样。&lt;/p&gt;
&lt;p&gt;现在能去深水区之后，25米一趟，还要再回来，直接累的不行。每个来回都要休息+调整泳镜，泳镜有一点进水就非常难受了，总担心它进水，感觉进了水我就要死在泳池里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尝试了两周之后&lt;/strong&gt;（游泳每次 40 - 60 分钟（指在水里的时间）），发现完全没有厌烦的情绪，于是买了&lt;strong&gt;佳明255&lt;/strong&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;游泳暂停的这一周（也就是第三周），每天保持撸铁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先说一下如何克服第一次去健身房的问题。&lt;/strong&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;strong&gt;所以，&lt;code&gt;i 人&lt;/code&gt;第一次还是选个十分蹊跷的时间去吧！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;撸铁第一周&lt;/strong&gt;我用的 &lt;strong&gt;40 斤&lt;/strong&gt;，&lt;strong&gt;只练胸部、肩膀、背部&lt;/strong&gt;。目前能叫上名的就是&lt;strong&gt;蝴蝶机&lt;/strong&gt;，其他的不知道，也不太关心，但是已经固定每天用这几个器械了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每次去，&lt;code&gt;12 - 15 个&lt;/code&gt; 一组，一次大概在 &lt;code&gt;12-16 组&lt;/code&gt;左右。&lt;/strong&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;还有个坐着往前推的练胸的器械，推不大动，所以用的少。&lt;/p&gt;
&lt;p&gt;还有个坐着往后拉的，练背的器械，我用的是最多的。&lt;/p&gt;
&lt;p&gt;练胸的可能我只做 6 组，但是练背的这个我得拉 10 组或者 12 组，感觉拉着有点舒服。&lt;/p&gt;
&lt;p&gt;第一周是 40 斤，第二三周基本都是 50 斤，第四周练胸的还是 50 斤，练背的用 60 斤。到目前是第五周，其他还是 50 斤，练背的用 70 斤....&lt;/p&gt;
&lt;p&gt;这些就是在健身区的锻炼情况。由于强度太低，时间太短。我每天练完，不管是晚上还是第二天，都没有明显的感觉。偶尔刚开始拉的时候感觉有一些昨天训练导致的疼痛，但啥也不影响！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为我只是为了一个健康的生活习惯和持续的减肥，不是为了练大练强。&lt;/strong&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;在简单学习了如何转身之后，我开始持续的游 100 米或者 50 米。（没错，我在第四周才可以不停下来整备，以前都是游到头就手动转身，然后反复多吸几口气再出发。）&lt;/p&gt;
&lt;p&gt;新泳镜让我非常放松，因为我知道它不进水。其实第一天连雾都不起，从第二次开始就起雾了...&lt;/p&gt;
&lt;p&gt;回想起之前在泳池里，12 米一停，25 米一停，现在感觉持续 100 米还真挺累。而且只有（相对）持续时间长了对有氧运动才会产生积极影响。&lt;/p&gt;
&lt;p&gt;有了之前腿疼的惨痛经历，这一周我时刻提醒自己收腿和蹬腿的正确性，不要打开太多髋部。在水中漂浮时间也适当延长 &lt;strong&gt;1s&lt;/strong&gt;。手部动作虽然不提供多少动力，但是做不对是挺影响速度的，尽量标准一些。&lt;strong&gt;整体游泳时间也控制的短一些。&lt;/strong&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;&lt;img src=&quot;https://img.zzao.club/article/202507211719802.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;12 点 10 分左右&lt;/strong&gt;，到了健身房先换衣服，去撸铁，今天撸了 13 组，一看表 &lt;strong&gt;12 点 45&lt;/strong&gt; 了，就必须去游泳了，不然时间不够。&lt;/p&gt;
&lt;p&gt;淋浴&lt;/p&gt;
&lt;p&gt;然后先游了一个 250 米（肯定给我记错了，一个来回是 50米），再来了一个 100 米（&lt;strong&gt;配速 2 分 55&lt;/strong&gt;）， 然后&lt;strong&gt;剩下时间就是在练自由泳打腿&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;13 点 15 分，必须去洗澡然后返回公司了。&lt;/p&gt;
&lt;p&gt;公司楼下吃个饭，然后上楼（14:00）。&lt;/p&gt;
&lt;p&gt;以上就是每天的时间安排。&lt;/p&gt;
&lt;h2&gt;饮食问题&lt;/h2&gt;
&lt;p&gt;这次的饮食重点就是&lt;strong&gt;不能太节食&lt;/strong&gt;，节食减下来，还是要吃回去，因为不可能永远不正常吃饭啊！&lt;/p&gt;
&lt;p&gt;所以先改&lt;code&gt;结构&lt;/code&gt;和&lt;code&gt;量&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;首先&lt;strong&gt;糖类是一点不碰了&lt;/strong&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;这次中午就是不那么忌讳碳水，但油太大的先 pass 了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;板面+鸡蛋+鸡腿+豆干&lt;/strong&gt;，这种套餐吃了好几次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;兰州拉面+煎蛋&lt;/strong&gt;吃了两次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;水饺&lt;/strong&gt;，茴香肉、芹菜肉这种，吃了好几次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;沙县的鸡腿&lt;/strong&gt;、&lt;strong&gt;鸭腿套餐&lt;/strong&gt;吃几次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;麻辣烫&lt;/strong&gt;，素菜为主+手擀面+丸子，吃过一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作日基本就是只有这些选择&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;只吃草肯定是不行的，因为我是&lt;strong&gt;中午先运动再吃饭&lt;/strong&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;这个月的&lt;strong&gt;周末基本都有活动&lt;/strong&gt;，不是去旅游，就去回老家，就算在家也要带孩子去商场，所以基本控制不住，&lt;strong&gt;一天最少放纵一顿&lt;/strong&gt;（一共两顿）。&lt;/p&gt;
&lt;p&gt;放纵完之后，周一&lt;strong&gt;体重基本要回升 1-2 斤&lt;/strong&gt;。（称体重时间为每天早上空腹）&lt;/p&gt;
&lt;p&gt;吃的少的话，周二就开始缓慢下降。吃的多要到周三到周四才开始下降。&lt;/p&gt;
&lt;p&gt;只有第四个周末，也就是上周末。同样是去结婚吃席，但是我没吃米面，只吃牛肉、羊肉、排骨（甜的偷吃了一个炸地瓜丸子），也感觉吃了不少，有七八分饱。&lt;/p&gt;
&lt;p&gt;到周一体重还是往下降了（0.5kg）！&lt;/p&gt;
&lt;p&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;中午在公司条件有限，很少有吃大量蔬菜的机会，这个跳过吧。&lt;strong&gt;在家带一把坚果，增加一下优质脂肪？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;晚上继续少吃或不吃吧&lt;/strong&gt;，目前没感觉有啥问题（再减 10 斤左右观察一下）。&lt;/p&gt;
&lt;h2&gt;热量消耗&lt;/h2&gt;
&lt;p&gt;从有手表后开始统计。&lt;/p&gt;
&lt;p&gt;每天&lt;strong&gt;运动消耗平均 572&lt;/strong&gt;，平均总消耗 2866，平均静息 2294&lt;/p&gt;
&lt;p&gt;&lt;em&gt;（我以为运动消耗能有多少呢，没想到这么点）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;睡眠质量：高&lt;/strong&gt; ，得分 &lt;strong&gt;80+&lt;/strong&gt;，时长 &lt;strong&gt;7-8 小时&lt;/strong&gt;左右。&lt;/p&gt;
&lt;p&gt;按上面的方式吃的话，肯定是有热量缺口了，不然也不能瘦下来。&lt;/p&gt;
&lt;h2&gt;效率提升的可能&lt;/h2&gt;
&lt;p&gt;游泳这块，第一个月也就是玩玩水，强度太低，休息时间太多。&lt;/p&gt;
&lt;p&gt;从这周开始长游时间变长，应该可以在同样时间内可以消耗更多的能量（？）。&lt;/p&gt;
&lt;p&gt;健身这块，再看感觉加加重量？我怕效果太差，感觉增加一点健身房锻炼时间也可以。&lt;/p&gt;
&lt;p&gt;运动方面的提升方向看起来不大，就是&lt;strong&gt;靠体能的上升，自然而然的提高代谢&lt;/strong&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;午饭随缘，看心情吧，上班为主，没啥操作空间。&lt;/p&gt;
&lt;p&gt;晚饭已经不咋吃了。&lt;/p&gt;
&lt;p&gt;周末在家的话，要重新抓起自己的厨师身份，安排一些肉类食品。在外面的话，吃的仔细一点。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;选择什么运动感觉还挺关键的，如果自己都排斥，那几乎是不能长期坚持的。&lt;/p&gt;
&lt;p&gt;有搭子应该会更好，动力更强一些，毕竟有人交流。&lt;/p&gt;
&lt;p&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;下周见啦😎&lt;/p&gt;</content:encoded></item><item><title><![CDATA[交互效果太单调？推荐两个动画丝滑的组件库，Vue 和 Nuxt都适用！]]></title><description><![CDATA[对于开发人员来说，最头疼的问题可能就是如何让自己的界面变得又高级又简洁、又丝滑又不拖泥带水、又有趣又不繁杂....]]></description><link>https://zzao.club/post/nuxt/ui/two-top-class-front-end-ui-components-repo</link><guid isPermaLink="true">https://zzao.club/post/nuxt/ui/two-top-class-front-end-ui-components-repo</guid><pubDate>Fri, 11 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;对于开发人员来说，最头疼的问题可能就是如何让自己的界面变得又高级又简洁、又丝滑又不拖泥带水、又有趣又不繁杂....&lt;/p&gt;
&lt;p&gt;这也是我非常头疼的且至今都解决不了的问题！ 或许最好的办法就是专业的事情找专业的人去做！去找设计师！&lt;/p&gt;
&lt;p&gt;开发、设计、销售，总不能一个人全占了吧（那你也太牛了吧！&lt;/p&gt;
&lt;p&gt;虽然做不到尽善尽美，但是我们可以借助 UI 库来实现相对简洁、高级、丝滑。&lt;/p&gt;
&lt;p&gt;对于简洁来说，&lt;code&gt;shadcn/vue&lt;/code&gt; 是我的首选，因为它的生态最完整，不仅仅是基础组件，还包含了一些好用的第三方组件。&lt;/p&gt;
&lt;p&gt;对于动画效果，我推荐这两个专门针对动画效果的 &lt;code&gt;Vue&lt;/code&gt; 生态下的库&lt;/p&gt;
&lt;p&gt;&lt;code&gt;inspira-ui&lt;/code&gt; 、&lt;code&gt;vue-bits&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;inspira-ui&lt;/h2&gt;
&lt;p&gt;官方地址：&lt;code&gt;https://inspira-ui.com/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Github：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;star: &lt;code&gt;3.4k&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MIT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完全兼容 &lt;code&gt;Nuxt&lt;/code&gt; 和 &lt;code&gt;Vue&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;组件统计（108 个）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;个数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backgrounds&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;背景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buttons&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;按钮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cards&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;卡片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursors&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;鼠标悬浮效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device Mocks&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;模拟 iphone 界面，会加载 url 实际内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input And Forms&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;表单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miscellaneous&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;一些特定的场景的动画效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Special Effects&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;特殊动画效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text Animations&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;文字动画效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visualization&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;3D效果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;部分效果图&lt;/h3&gt;
&lt;p&gt;注意：大部分都是带动画的，此处只是演示静态图&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;special effects&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148723.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;backgrounds&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148725.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;buttons&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148726.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cards 3D Effect&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148727.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lens&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148728.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148729.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148730.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;收费组件&lt;/h3&gt;
&lt;p&gt;地址： &lt;a href=&quot;https://pro.inspira-ui.com/&quot;&gt;https://pro.inspira-ui.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一开始这个库是没有收费组件的，全部免费，现在多出了几个收费组件（inspira-ui Pro）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inspira UI Pro Component Pack 2 @ $25&lt;/li&gt;
&lt;li&gt;Inspira UI Pro Components Pack @ $15&lt;/li&gt;
&lt;li&gt;Inspiria - SaaS Landing Page Template @ $49&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前两个是一些拥有特殊的交互效果的组件，第三个是模板&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;吐槽一下：我喜欢交互丝滑的动效，但很多组件动画过于复杂，已经影响了实用性，可能只适合于炫技。&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;依赖情况&lt;/h3&gt;
&lt;p&gt;组件不同程度的依赖了以下几个库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gsap&lt;/li&gt;
&lt;li&gt;tailwindcss v4&lt;/li&gt;
&lt;li&gt;threejs&lt;/li&gt;
&lt;li&gt;motion-v&lt;/li&gt;
&lt;li&gt;@vueuse/core&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;集成到项目&lt;/h3&gt;
&lt;p&gt;同 shadcn/vue 的集成项目，门槛很低。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;支持复制粘贴使用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持 Inspira 的 Cli 工具安装&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;vue-bits&lt;/h2&gt;
&lt;p&gt;官方地址：&lt;code&gt;https://vue-bits.dev/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Github：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;star: &lt;code&gt;296&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MIT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完全兼容 &lt;code&gt;Nuxt&lt;/code&gt; 和 &lt;code&gt;Vue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;虽然这个库看起来 &lt;code&gt;star&lt;/code&gt; 很少，但它和 &lt;code&gt;react-bits&lt;/code&gt; 是同一个作者&lt;code&gt;，react-bits&lt;/code&gt; 的 star 是 &lt;code&gt;18.4k&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;组件统计（69 个）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;个数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text Animations&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;文字动画效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animations&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;动画效果，可以包裹子组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;常用的组件，如轮播图、瀑布流、相册&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgrounds&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;动态背景效果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;部分效果图&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;components&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148731.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;strong&gt;text animations&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148732.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pixel Transition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148733.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Click Spark&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148734.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507171148735.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;收费组件&lt;/h3&gt;
&lt;p&gt;无&lt;/p&gt;
&lt;h3&gt;依赖情况&lt;/h3&gt;
&lt;p&gt;后两个库主要是支持 &lt;code&gt;Backgrounds&lt;/code&gt; 这一类别的动画效果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gsap&lt;/li&gt;
&lt;li&gt;motion-v&lt;/li&gt;
&lt;li&gt;ogl&lt;/li&gt;
&lt;li&gt;three&lt;/li&gt;
&lt;li&gt;postprocessing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;集成到项目&lt;/h2&gt;
&lt;p&gt;同 shadcn/vue 的集成项目，门槛很低。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持复制粘贴直接使用&lt;/li&gt;
&lt;li&gt;支持 Cli&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;组件数量上&lt;/strong&gt; &lt;code&gt;Inspira&lt;/code&gt; 胜过 &lt;code&gt;Vue Bits&lt;/code&gt; 一头&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;Inspira&lt;/code&gt; 的分类的命名非常不清晰，让人眼花缭乱，容易劝退。 &lt;code&gt;Vue Bits&lt;/code&gt; 可以清晰快速的找到自己想要的组件。&lt;/p&gt;
&lt;p&gt;依赖情况上是差不的，但还是因为分类原因。&lt;code&gt;Vue Bits&lt;/code&gt; 显得更加清晰一些，如果不用 Backgrounds，那绝大部分只需要 &lt;code&gt;gsap&lt;/code&gt; 和 &lt;code&gt;motion-v&lt;/code&gt;，极个别需要 &lt;code&gt;ogl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;集成到项目中同样轻松！&lt;/p&gt;
&lt;p&gt;不知道是否是文档页面风格的原因，&lt;code&gt;Vue Bits&lt;/code&gt; 给我的感觉更加简洁和克制，&lt;code&gt;Inspira&lt;/code&gt; 更加花哨。&lt;/p&gt;
&lt;p&gt;把组件整体浏览下来也感觉 &lt;code&gt;Vue Bits&lt;/code&gt; 似乎可用的组件更多一些（虽然总体数量更少）&lt;/p&gt;
&lt;p&gt;不知道你怎么看？&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;以上就是两个组件库的整体情况了！&lt;/p&gt;
&lt;p&gt;不得不说，有了 &lt;code&gt;Nuxt&lt;/code&gt; 对 &lt;code&gt;Vue&lt;/code&gt; 的包装，大大提升了开发体验。基础组件库又有 &lt;code&gt;shadcn&lt;/code&gt; 这样全面的选手。再加上 &lt;code&gt;Vue Bits&lt;/code&gt; 或者 &lt;code&gt;Inspira&lt;/code&gt; 这种针对于动画效果的组件库。&lt;/p&gt;
&lt;p&gt;再也不用担心页面过于粗糙啦！&lt;/p&gt;
&lt;p&gt;欢迎访问我的&lt;a href=&quot;https://zzao.club&quot;&gt;个人博客&lt;/a&gt;，里面有更多 Nuxt 最新实战内容 🙌 🙌 🙌&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Vercel 收购 NuxtLabs！Nuxt UI Pro 即将免费！]]></title><description><![CDATA[Vercel 收购了 Nuxt 以及背后的核心团队 NuxtLabs !]]></description><link>https://zzao.club/post/nuxt/news/nuxtlabs-join-vercel</link><guid isPermaLink="true">https://zzao.club/post/nuxt/news/nuxtlabs-join-vercel</guid><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Vercel&lt;/code&gt; 收购了 &lt;code&gt;Nuxt&lt;/code&gt; 以及背后的核心团队 &lt;code&gt;NuxtLabs&lt;/code&gt; !&lt;/p&gt;
&lt;p&gt;此时，&lt;code&gt;Vercel&lt;/code&gt; 已经同时拥有了 &lt;code&gt;Next&lt;/code&gt; 和 &lt;code&gt;Nuxt&lt;/code&gt; 两个分别由 &lt;code&gt;React&lt;/code&gt; 和 &lt;code&gt;Vue&lt;/code&gt; 发展而来的服务端渲染方案！&lt;/p&gt;
&lt;p&gt;（ &lt;code&gt;Nuxt&lt;/code&gt; 官方是不是不用再卖课挣钱了😂&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507091422989.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;收到消息后，刷新了一下 &lt;code&gt;Nuxt&lt;/code&gt; 的官方，发现已经置顶了一条消息！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuxtLabs is joining Vercel&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;消息的大概内容为：&lt;/p&gt;
&lt;p&gt;虽然被收购了，但是还是会专注于 &lt;code&gt;Nuxt&lt;/code&gt; 和 &lt;code&gt;Nitro&lt;/code&gt; 的开发，继续遵循 &lt;code&gt;MIT&lt;/code&gt; 协议&lt;/p&gt;
&lt;p&gt;感谢xxx，感谢 xxx，感谢 xxx&lt;/p&gt;
&lt;p&gt;还有三个接下来要发生的重要消息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发布 &lt;code&gt;Nuxt UI v4&lt;/code&gt;，其中 &lt;code&gt;Nuxt UI Pro&lt;/code&gt; 组件以及 &lt;code&gt;Figma Kit&lt;/code&gt; 将免费提供给所有人（这就是金钱的力量吗）&lt;/li&gt;
&lt;li&gt;开源 &lt;code&gt;Nuxt Studio&lt;/code&gt; 的自托管版本 （我的博客要退休了？）&lt;/li&gt;
&lt;li&gt;NuxtHub 独立于其他提供商支持 （Make NuxtHub agnostic to support other providers, integrating with Vercel’s &lt;a href=&quot;https://vercel.com/marketplace&quot;&gt;Marketplace&lt;/a&gt; offerings like Postgres and Redis will become seamless.）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;看到 &lt;code&gt;NuxtLabs&lt;/code&gt; 终于找到了金主爸爸，我作为深度使用 &lt;code&gt;Nuxt&lt;/code&gt; 生态的一员，也是由衷的开心啊&lt;/p&gt;
&lt;p&gt;（有了钱，就不用把一个开源项目掰成两半去卖了！&lt;/p&gt;
&lt;p&gt;虽然平时大部分前端接触的是 &lt;code&gt;Nuxt&lt;/code&gt;，并且仅仅是用作 &lt;code&gt;SSR&lt;/code&gt;，但支持 &lt;code&gt;Nuxt&lt;/code&gt; 的 &lt;code&gt;Nitro&lt;/code&gt; 实际上也非常简单易用。搭配 &lt;code&gt;Nuxt&lt;/code&gt; ，而不用再去使用 &lt;code&gt;NestJS&lt;/code&gt; 来写后端。&lt;/p&gt;
&lt;p&gt;不仅是 &lt;code&gt;Nitro&lt;/code&gt;，&lt;code&gt;Nitro&lt;/code&gt; 使用的（基础库） &lt;code&gt;H3&lt;/code&gt;，以及 &lt;code&gt;unjs&lt;/code&gt; 这个组织下的很多包都非常好用。&lt;/p&gt;
&lt;p&gt;比如我在所有项目内使用的 &lt;code&gt;unjs/changelogen&lt;/code&gt; 用来自动生成 &lt;code&gt;CHANGELOG.md&lt;/code&gt;，同时发布新版本和推送到 &lt;code&gt;Github&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再比如 &lt;code&gt;NuxtImage&lt;/code&gt; 默认使用的 &lt;code&gt;unjs/IPX&lt;/code&gt;，可以非常简单的搭建一个图片服务，并且支持你使用一个 &lt;code&gt;URL&lt;/code&gt; 就能拥有 &lt;code&gt;sharp&lt;/code&gt; 的大部分功能来直接转换图片格式、压缩、裁切等&lt;/p&gt;
&lt;p&gt;再比如开发命令行工具用到的 &lt;code&gt;unjs/consola&lt;/code&gt; 等等等等&lt;/p&gt;
&lt;p&gt;🤩🤩🤩&lt;/p&gt;
&lt;p&gt;希望 &lt;code&gt;Nuxt&lt;/code&gt; 越来越好 ～～&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;话说有钱了，能不能把官方卖几百刀的课程也免费一下 😃&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Vue 官方 VSCode 插件发布 3.0 大版本，更好用！更智能！]]></title><description><![CDATA[Vue (Official) 在近日发布了 V3.0.0 大版本更新，我也是在第二时间进行了更新，发现有一些好用的新功能值得一说！]]></description><link>https://zzao.club/post/nuxt/vue/vue-vscode-extension-release-3.0</link><guid isPermaLink="true">https://zzao.club/post/nuxt/vue/vue-vscode-extension-release-3.0</guid><pubDate>Thu, 03 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Vue (Official)&lt;/strong&gt; 在近日发布了 &lt;code&gt;V3.0.0&lt;/code&gt; 大版本更新，我也是在&lt;strong&gt;第二时间&lt;/strong&gt;进行了更新，发现有一些好用的新功能值得一说！&lt;/p&gt;
&lt;h2&gt;组件引入新方式&lt;/h2&gt;
&lt;p&gt;现在引入 &lt;code&gt;vue component&lt;/code&gt; 可以直接从一侧的文件栏拖拽到 &lt;code&gt;template&lt;/code&gt; 标签中。&lt;/p&gt;
&lt;p&gt;拖拽后按住 &lt;code&gt;shift&lt;/code&gt; 键，会在当前鼠标位置生成一个组件标签，并且在 script 中自动引入改成组件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507031152188.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;组件 props 提示&lt;/h2&gt;
&lt;p&gt;现在使用某个组件时，会直接提示你必传的 props，不用再去组件里找了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507031152190.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;组件点击直接跳转到组件本身&lt;/h2&gt;
&lt;p&gt;在 template 中点击某个组件，现在会直接跳转到组件文件，而不是 tsconfig 的定义处了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507031152191.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Props 提示&lt;/h2&gt;
&lt;p&gt;众所周知，&lt;code&gt;vue3.5&lt;/code&gt; 之后（没记错的话）,&lt;code&gt;defindProps&lt;/code&gt; 可以直接解构了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;// const props = defineProps&amp;#x3C;{ id?: string }&gt;()
const { id = &apos;标题&apos; } = defineProps&amp;#x3C;{ id?: string }&gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如继续写一个其他的 &lt;code&gt;computed&lt;/code&gt; 或者 &lt;code&gt;watch&lt;/code&gt; 时，用到这个 &lt;code&gt;id&lt;/code&gt;，显示会有一个提示。&lt;/p&gt;
&lt;p&gt;告知你这个 id 来自于 &lt;code&gt;props&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507031152192.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;.value自动补全&lt;/h2&gt;
&lt;p&gt;当使用一个 &lt;code&gt;ref&lt;/code&gt; 变量时，现在会自动补全 &lt;code&gt;.value&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507031152193.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;设置里需要勾选&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202507031152194.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;以上就是我发现的一些新功能，如果有所遗漏，欢迎评论区补充～～&lt;/p&gt;</content:encoded></item><item><title><![CDATA[集市周报 Vol.05]]></title><description><![CDATA[这两周是游泳的两周！所以这次是一个记录锻炼和身体变化的报告！]]></description><link>https://zzao.club/post/report/weekly-report-05</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-05</guid><pubDate>Thu, 26 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这两周是游泳的两周！所以这次是一个记录锻炼和身体变化的报告！&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;25 年 3 月份，办了一张游泳健身十日体验卡&lt;/p&gt;
&lt;p&gt;满打满算，游了七次，一次 40-60 分钟&lt;/p&gt;
&lt;p&gt;前两次练习漂浮+站立+换气，然后随便练一下蛙泳动作。&lt;/p&gt;
&lt;p&gt;第三次发现可以走水了。开始尝试在浅水区游&lt;/p&gt;
&lt;p&gt;第四次，可以泳 25 米了，但是不敢停，因为不会踩水。&lt;/p&gt;
&lt;p&gt;果然，不出意外的出意外了。&lt;/p&gt;
&lt;p&gt;第4 还是 5 次在深水区泳镜进水，对于在水里睁不开眼的新手的来说，直接在水里停下来了，而此时还不会踩水。幸好水不是特别深，触底蹬一下，勉强摸到池边，扒拉着回浅水区了。自此就不敢去深水区了。&lt;/p&gt;
&lt;p&gt;后面两次，感觉不到任何进步了，而且泳镜开始时不时进水，搞不懂为什么。只能在浅水区来回游动。&lt;/p&gt;
&lt;p&gt;看视频在脑子里记得理论挺多，下了水感觉感受不到自己的肢体在哪里，也不知道如何改进。看别人在岸上拍的视频，发现确实动作非常拉跨。&lt;/p&gt;
&lt;p&gt;但是十日卡到期了，就此告一段落。&lt;/p&gt;
&lt;h2&gt;再入半年卡！&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;25/6/18&lt;/code&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;目前 14 天，去了 11 天，一天是泳池维护，两天是周末回老家。&lt;/p&gt;
&lt;h2&gt;锻炼日常&lt;/h2&gt;
&lt;p&gt;去健身房一般是空腹，喝点电解质水，别太渴就行。&lt;/p&gt;
&lt;p&gt;先去&lt;strong&gt;游泳&lt;/strong&gt;，&lt;strong&gt;40-70 分钟&lt;/strong&gt;。视情况而定，有时候腿疼，只能游 30 分钟&lt;/p&gt;
&lt;p&gt;前七八次是不能泳完整的，要么到中间就停下掉头，不敢去深水区。要不到头就要停下来，泳镜进水也很致命...&lt;/p&gt;
&lt;p&gt;就是不停的练习，感受身体怎么走水。再回想一下白天看的视频教学，实践一下。&lt;/p&gt;
&lt;p&gt;最近的三四次已经可以完整游几个来回了，但是离一口气游 500 米、一千米还差得远。&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;strong&gt;胸、肩、背&lt;/strong&gt;这几块。腹部肥肉太多，感觉会很容易出汗，而且每个器械都要收紧腹部（应该也有用吧）。不练腿，因为我要留着蛙泳，蛙泳蹬腿蹬不对也容易大腿根疼，所以不适合再上强度了。&lt;/p&gt;
&lt;p&gt;这一套下来，总共耗时 &lt;code&gt;90 分钟&lt;/code&gt;左右。&lt;/p&gt;
&lt;h2&gt;身体变化&lt;/h2&gt;
&lt;p&gt;这两周，&lt;strong&gt;体重&lt;/strong&gt;少说掉了 &lt;code&gt;5 斤&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;更明显的是体态的变化，肩膀打开了很多，之前有些肱骨前移，应该是长期敲代码导致的，背部肌肉激活后，感觉给拉回去了。&lt;/p&gt;
&lt;p&gt;腹部底蕴深厚，暂时没感觉到有变化！&lt;/p&gt;
&lt;p&gt;腿部也不是很明显。因为我属于哪里都有点肉，整体得有 30% 的体脂率，瘦也一起瘦，胖也一起胖，所以感觉上不明显。&lt;/p&gt;
&lt;p&gt;再说精神头！&lt;/p&gt;
&lt;p&gt;白天精力比较充沛，不会犯困了！运动应该有一定的作用！&lt;/p&gt;
&lt;p&gt;还有个原因，以前晚上喜欢熬夜玩手机，高低得玩到 12 点，无法停止。但是现在要陪孩子睡觉，孩子他妈讲故事，孩子睡觉也很慢，大人不可能还在一边玩手机！有几次我都想着等他睡着再起来看一会小说，但是他实在睡得太慢，听故事听的我都困了，就睡着了...&lt;/p&gt;
&lt;p&gt;早上八点起床，还是比较晚，因为我九点半到公司就可以。后续应该是调整到 7 点左右，早上再干点事做。&lt;/p&gt;
&lt;h2&gt;穿戴设备&lt;/h2&gt;
&lt;p&gt;因为感觉锻炼节奏趋于稳定了，所以想买一个手表来记录一下数据。&lt;/p&gt;
&lt;p&gt;首先非常确定不会买超过 1000 价位的表，以此为基础做了一点点功课。&lt;/p&gt;
&lt;h3&gt;迪卡侬w500&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;价格&lt;/strong&gt;很便宜，&lt;code&gt;119 元&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;装电子的，&lt;strong&gt;续航&lt;/strong&gt;&lt;code&gt;两年&lt;/code&gt;！&lt;/p&gt;
&lt;p&gt;能看时间，计时全靠手动。&lt;/p&gt;
&lt;p&gt;如果比较纯粹的运动者，感觉还挺合适。&lt;/p&gt;
&lt;h3&gt;华为手环 10&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;价格&lt;/strong&gt;也便宜，标准版 &lt;code&gt;177&lt;/code&gt; 元，NFC版 &lt;code&gt;220&lt;/code&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;h3&gt;佳明 255&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;价格&lt;/strong&gt;，&lt;code&gt;1173&lt;/code&gt; 元（有国补），以往价格似乎能到 &lt;code&gt;2400+&lt;/code&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;strong&gt;不过最后也没买新的。闲鱼自己找了个 850 元的下手了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;后续用起来也会写写体验如何。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;最近的目标还挺明确的，就是锻炼。拉长战线，半年为期，扎扎实实的提升。&lt;/p&gt;
&lt;p&gt;希望在儿子上学之前瘦下来！&lt;/p&gt;
&lt;p&gt;然后和他一起去尝试、去玩各种好玩的运动 🛹 ⛸️ ！&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt 3.17 发布，对比3.16有一个重大改变]]></title><description><![CDATA[今天逛 Github ，发现 Nuxt 又发新版了： 3.17.0]]></description><link>https://zzao.club/post/nuxt/nuxt-3.17-release</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt-3.17-release</guid><pubDate>Tue, 24 Jun 2025 16:04:08 GMT</pubDate><content:encoded>&lt;p&gt;今天逛 &lt;code&gt;Github&lt;/code&gt; ，发现 &lt;code&gt;Nuxt&lt;/code&gt; 又发新版了： &lt;code&gt;3.17.0&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;更新日志&lt;/h2&gt;
&lt;h3&gt;数据获取改进&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据一致性&lt;/strong&gt;：所有使用相同键的 &lt;code&gt;useAsyncData&lt;/code&gt; 或 &lt;code&gt;useFetch&lt;/code&gt; 调用现在共享底层引用，确保应用中数据状态一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应式键&lt;/strong&gt;：支持使用计算引用、普通引用或 getter 函数作为键，当键值变化时会自动触发数据重新获取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化数据重新获取&lt;/strong&gt;：多个组件监听同一数据源时，依赖项变化只会触发一次数据获取。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;新增内置组件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;NuxtTime&gt;&lt;/code&gt;&lt;/strong&gt;：用于安全显示时间的组件，解决了在服务器端渲染和客户端渲染中处理日期时的水合不匹配问题，支持多种时间格式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;NuxtErrorBoundary&gt;&lt;/code&gt;&lt;/strong&gt;：转换为单文件组件，暴露 &lt;code&gt;error&lt;/code&gt; 和 &lt;code&gt;clearError&lt;/code&gt;，便于在模板中处理错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;路由改进&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;NuxtLink&gt;&lt;/code&gt; 新增 &lt;code&gt;trailingSlash&lt;/code&gt; 属性，用于控制 URL 格式，添加尾部斜杠。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;加载指示器自定义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新增 &lt;code&gt;hideDelay&lt;/code&gt; 和 &lt;code&gt;resetDelay&lt;/code&gt; 属性，用于控制加载指示器的隐藏延迟和重置延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;文档作为包&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Nuxt 文档现可作为 npm 包 &lt;code&gt;@nuxt/docs&lt;/code&gt; 安装，提供构建文档网站的原始 Markdown 和 YAML 内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;开发体验改进&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新增多个警告，帮助开发者避免常见错误，例如：
&lt;ul&gt;
&lt;li&gt;服务器组件缺少根元素时发出警告。&lt;/li&gt;
&lt;li&gt;使用保留的 &lt;code&gt;runtimeConfig.app&lt;/code&gt; 命名空间时发出警告。&lt;/li&gt;
&lt;li&gt;核心自动导入预设被覆盖时发出警告。&lt;/li&gt;
&lt;li&gt;同一文件中多次使用 &lt;code&gt;definePageMeta&lt;/code&gt; 时发出错误提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模块开发增强&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新增 &lt;code&gt;experimental.enforceModuleCompatibility&lt;/code&gt;，允许 Nuxt 在加载不兼容模块时抛出错误。&lt;/li&gt;
&lt;li&gt;新增 &lt;code&gt;addComponentExports&lt;/code&gt;，可自动注册文件中通过命名导出的组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;性能改进&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;tinyglobby&lt;/code&gt; 提升文件 glob 操作速度。&lt;/li&gt;
&lt;li&gt;排除 &lt;code&gt;.data&lt;/code&gt; 目录的类型检查，加快构建速度。&lt;/li&gt;
&lt;li&gt;通过提升 &lt;code&gt;purgeCachedData&lt;/code&gt; 检查来优化树摇优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;其他改进&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;提供升级命令 &lt;code&gt;npx nuxi@latest upgrade --dedupe&lt;/code&gt;，用于刷新锁文件并拉取最新依赖。&lt;/li&gt;
&lt;li&gt;修复了多个问题，包括错误处理、路由解析、类型检查等。&lt;/li&gt;
&lt;li&gt;文档更新，包括对自动导入、SEO 等内容的改进。&lt;/li&gt;
&lt;li&gt;测试和 CI 流程的改进。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;主要影响点&lt;/h2&gt;
&lt;h3&gt;useAsyncData、useFetch&lt;/h3&gt;
&lt;p&gt;以前写 &lt;code&gt;watch&lt;/code&gt; 时，每次 &lt;code&gt;watch&lt;/code&gt; 的值发生改变，回调函数会再次执行一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现在不会了！&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// In multiple components:
const { data } = useAsyncData(
  &apos;users&apos;, 
  () =&gt; $fetch(`/api/users?page=${route.query.page}`),
  { watch: [() =&gt; route.query.page] }
)

// When route.query.page changes, only one fetch operation will occur
// All components using this key will update simultaneously
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取而代之的是增加了一个响应式键&lt;/p&gt;
&lt;p&gt;可以直接用 computed 作为键，里面的响应式对象发生改变时，回调函数会再次执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const userId = ref(&apos;123&apos;)
const { data: user } = useAsyncData(
  computed(() =&gt; `user-${userId.value}`),
  () =&gt; fetchUser(userId.value)
)

// Changing the userId will automatically trigger a new data fetch
// and clean up the old data if no other components are using it
userId.value = &apos;456&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这也是对我的博客站有影响的一点。 之前文章页的几个 &lt;code&gt;tab&lt;/code&gt; 切换靠的是 &lt;code&gt;watch&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;不过依然没让我失望，升级之后火速发现了奇怪的bug。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后又从 &lt;code&gt;3.17&lt;/code&gt; 退回了&lt;code&gt;3.16.2&lt;/code&gt;。。。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2025年05月06日09:33:34 更新&lt;/h3&gt;
&lt;p&gt;五一结束了，&lt;code&gt;nuxt&lt;/code&gt; 更到了&lt;code&gt;3.17.2&lt;/code&gt;，解决了客户端组件 &lt;code&gt;useAsyncData&lt;/code&gt; 的问题&lt;/p&gt;
&lt;p&gt;本地升级后，在 &lt;code&gt;useAsyncData&lt;/code&gt; 里使用 &lt;code&gt;响应式key&lt;/code&gt;，看起来表现正常了&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;之前说好的 &lt;code&gt;3.11&lt;/code&gt; 是最后一个小版本，现在愣是发到了 &lt;code&gt;3.17&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;虽然实际上更新的内容已经在向 &lt;code&gt;Nuxt4&lt;/code&gt; 过渡，但是不知道为什么 &lt;code&gt;Nitro&lt;/code&gt; 为什么不发 &lt;code&gt;3.0&lt;/code&gt; (之前提到不发4.0是因为Nitro不发3.0)&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt 全栈开发·自定义响应和全局错误处理]]></title><description><![CDATA[使用 Nuxt 全栈开发时，如何像NestJS 一样优雅的设置统一的响应体，以及如何捕获全局 Error]]></description><link>https://zzao.club/post/nuxt/nitro/standard-response-global-error-handler</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nitro/standard-response-global-error-handler</guid><pubDate>Tue, 24 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用 &lt;code&gt;Nuxt&lt;/code&gt; 全栈开发时，如何像 &lt;code&gt;NestJS&lt;/code&gt; 一样优雅的设置统一的响应体，以及如何捕获全局 Error&lt;/p&gt;
&lt;h2&gt;自定义 Handler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nuxt.com/docs/4.x/guide/directory-structure/server#server-utilities&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import type { EventHandler, EventHandlerRequest } from &apos;h3&apos;

export const defineWrappedResponseHandler = &amp;#x3C;T extends EventHandlerRequest, D&gt; (
  handler: EventHandler&amp;#x3C;T, D&gt;
): EventHandler&amp;#x3C;T, D&gt; =&gt;
  defineEventHandler&amp;#x3C;T&gt;(async event =&gt; {
    try {
      // 拿到接口文件里返回的响应值
      const response = await handler(event)
      // 自定义统一的结构
      // 自定义code，自定义 message
      return { code: 200, data: response, message: &apos;ok&apos; }
    } catch (err) {
      // 自定义错误响应体
      return { code: 500, message: &apos;error&apos; + err }
      // 或直接抛出错误
	  // throw createError({
      //  statusCode: 500,
      //  message: &apos;出错啦，请稍后再试～&apos;,
      //})
    }
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时将 &lt;code&gt;defineEventHandler&lt;/code&gt; 替换为 &lt;code&gt;defineWrappedResponseHandler&lt;/code&gt; ，同时直接 &lt;code&gt;return data&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineWrappedResponseHandler(async (event) =&gt; {
  const schema = z.object({
    id: z.string(),
    user_id: z.string().optional(),
  })
  const query = await useSafeValidatedQuery(event, schema)

  if (!query.success) {
    throw createError({
      statusCode: 400,
      statusMessage: (query as any).message ?? &apos;参数错误&apos;,
    })
  }

  // 伪代码
  const data =  await prisma.findMany()
  
  return data
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在 plugins 中使用 hook&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nitro.build/guide/plugins#request-and-response-lifecycle&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nitro&lt;/code&gt; 中有多个生命周期钩子可供自定义，可自行设置标准响应体，打印日志等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNitroPlugin((nitroApp) =&gt; {
  nitroApp.hooks.hook(&quot;request&quot;, (event) =&gt; {
    console.log(&quot;on request&quot;, event.path);
  });

  nitroApp.hooks.hook(&quot;beforeResponse&quot;, (event, { body }) =&gt; {
    console.log(&quot;on response&quot;, event.path, { body });
  });

  nitroApp.hooks.hook(&quot;afterResponse&quot;, (event, { body }) =&gt; {
    console.log(&quot;on after response&quot;, event.path, { body });
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;beforeResponse&lt;/code&gt; 实现自定义响应体&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNitroPlugin((nitroApp) =&gt; {
  nitroApp.hooks.hook(&quot;beforeResponse&quot;, (event, { body }) =&gt; {
    if (t?.body) {
	    t.body = {
		    code: 200,
		    data: t.body,
		    message: &apos;ok&apos;
	    }
    }
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是两种设置统一响应体的方式&lt;/p&gt;
&lt;h2&gt;errorHandler&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nitro.build/config#errorhandler&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Path to a custom runtime error handler. Replacing nitro&apos;s built-in error page. The error handler is given an &lt;code&gt;H3Error&lt;/code&gt; and &lt;code&gt;H3Event&lt;/code&gt;. If the handler returns a promise it is awaited. The handler is expected to send a response of its own. Below is an example where a plain-text response is returned using h3&apos;s functions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;nitro&lt;/code&gt; 中有一个默认的错误页面，你可以自定义自己的 &lt;code&gt;errorHandler&lt;/code&gt; 用来和统一响应体&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
	future: {
	    compatibilityVersion: 4,
	},
	nitro: {
		errorHandler: &apos;~~/server/error&apos;
	}
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNitroErrorHandler((error, event) =&gt; {
  setResponseHeader(event, &apos;Content-Type&apos;, &apos;application/json&apos;)
  console.log(`[${new Date().toLocaleDateString()}]-[nitro error]: `, error)
  return send(event, { data: null, message: &apos;服务器异常&apos; })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是群里小伙伴们各自实践中得到的经验！&lt;/p&gt;
&lt;p&gt;希望对你有所帮助☺️&lt;/p&gt;</content:encoded></item><item><title><![CDATA[无需额外软件，MacOS 缩小托盘图标间隔]]></title><description><![CDATA[众所周知，macos 有些机型的刘海屏十分反人类，导致图标展示不全。]]></description><link>https://zzao.club/post/tips/apple/reduce-space-of-items</link><guid isPermaLink="true">https://zzao.club/post/tips/apple/reduce-space-of-items</guid><pubDate>Fri, 13 Jun 2025 21:22:13 GMT</pubDate><content:encoded>&lt;p&gt;众所周知，macos 有些机型的刘海屏十分反人类，导致图标展示不全。&lt;/p&gt;
&lt;p&gt;此方法可以无需软件缩小图标的间距&lt;/p&gt;
&lt;p&gt;原贴地址 &lt;a href=&quot;https://www.v2ex.com/t/1047186&quot;&gt;V2ex&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 指定间距  
defaults -currentHost write -globalDomain NSStatusItemSpacing -int 5  
# 指定内边距  
defaults -currentHost write -globalDomain NSStatusItemSelectionPadding -int 5  
# 当前间距查询  
defaults -currentHost read -globalDomain NSStatusItemSpacing  
defaults -currentHost read -globalDomain NSStatusItemSelectionPadding  
# 重置  
defaults -currentHost delete -globalDomain NSStatusItemSpacing  
defaults -currentHost delete -globalDomain NSStatusItemSelectionPadding  
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[集市周报 Vol.04]]></title><link>https://zzao.club/post/report/weekly-report-04</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-04</guid><pubDate>Mon, 09 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;没事真别生孩子&lt;/h2&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;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&gt;疯言疯语&lt;/h2&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;ul&gt;
&lt;li&gt;作为一个 30 岁的人，最重要的还是要搞钱，被别的事情耽误的了时间，你别想翻身了。&lt;/li&gt;
&lt;li&gt;家里的事，没有什么是必须谁做的，必要的时候要立即放弃自己的道德感、负罪感。&lt;/li&gt;
&lt;li&gt;如果可能的话，用钱来修补感情，比心要好使。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三，完全放弃同化家人。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父母已经 50+，从根本上不能完全理解另一个时代出生的人的生活习惯、思维，想和父母掰扯清楚完全不现实，如果他们可以被改变，那早就已经自己想通了，轮不到你来苦口婆心。&lt;/li&gt;
&lt;li&gt;每个人都是一个独立的个体，一个独立的个体要有自己的生活规律，要有自己的规划，要有自己的处事逻辑、待人逻辑。父母拎不清，没有从小教育，只能自己多吃点教训才能长记性了。&lt;/li&gt;
&lt;li&gt;相伴一生的爱人，指的是两个独立的个体，在某些方面存在共鸣。两个尚未开化的人走在一起会轻易的被琐事绊脚，以后也只能做暴富的梦，生活质量全看上一辈人够不够啃了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;以上&lt;/h2&gt;
&lt;p&gt;疯子的话完全不可信。&lt;/p&gt;
&lt;p&gt;我被折磨疯了，所以我上面的话完全不可信。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[集市周报 Vol.03]]></title><link>https://zzao.club/post/report/weekly-report-03</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-03</guid><pubDate>Tue, 03 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;高速上快速路跑 90 的，你他妈真该死啊！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;博客&lt;/h2&gt;
&lt;p&gt;上周主要是给博客上加了个&lt;strong&gt;注解&lt;/strong&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;img src=&quot;https://img.zzao.club/article/202506051158158.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;输入弹窗&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202506051158159.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;hover 效果&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202506051158160.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;伤员 1 号&lt;/h2&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;整层住院的病房，不知道有几个医生值班，应该不超过 2 个，一楼还有急救室，有时医生会去做手术。&lt;/p&gt;
&lt;p&gt;而且我没有住院经验，&lt;strong&gt;不知道医生和护士的职责划分就和互联网企业里的开发和产品一样严格&lt;/strong&gt;，护士只能按医生开的单子（医嘱？）进行换药，其他的工作主要是叮嘱以及帮忙护理。护理不合格，还要扣绩效或者罚款之类的，也难怪护士们根本好脾气不起来（胖东来的含金量还在提升）。所以，只要医生一撤退，护士对这种身上插着管子的病人，哪怕再有困难也只能等到明天。当然了，除非家属极力要求，他们还是会直接给医生打电话，寻求下一步的指示。&lt;/p&gt;
&lt;p&gt;而且护士的数量也堪忧，大概是十个产品经理对一个全栈开发的水平。&lt;/p&gt;
&lt;p&gt;就这样，他腿上的纱布，硬是顶了 6 天，然后让一个实习值班医生（研究生）给揭下来了，因为护士不能换，医生轻易找不到人，找到后需要先问出血的脾脏怎么办，所以只能先把腿放在一边了。&lt;/p&gt;
&lt;p&gt;终于去做腹腔穿刺了。这也是我第一次直接目睹小手术的全过程，差点给我干吐出来。这个大夫表示怎么这么晚才来做穿刺，淤血都比较粘稠了。此时我脑子里只有两个字：观察。当晚出了 700ml 的淤血，然后这个袋子就需要一直挂两天。后续没有继续出血，表示止住了，等拔了管之后复查一下可以安排出院。&lt;/p&gt;
&lt;p&gt;然后你懂得，又是艰难的等医生到次日早上查房。&lt;/p&gt;
&lt;p&gt;我没等到他拔管办出院这一天，因为我丈母娘也摔着了，住院了。&lt;/p&gt;
&lt;h2&gt;伤员 2 号&lt;/h2&gt;
&lt;p&gt;大于 55 岁的人，再伤着骨头就比较难养了。&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&gt;上班&lt;/h2&gt;
&lt;p&gt;从成都回来，发生了一点小变化！我开始喝茶了！&lt;/p&gt;
&lt;p&gt;在都江堰市买了一些本地的绿茶，20+、30+、70+不等。我正在喝的应该是是 70+（一两）的绿茶，算是比较贵的了。&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;img src=&quot;https://img.zzao.club/article/202506051158161.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;碎碎念&lt;/h2&gt;
&lt;p&gt;表弟家是重组家庭，还有个大他四岁的哥哥跟着他后妈过来。住院期间正好他哥在订婚，舅舅还想让我订婚当天也过去，但是我看大家都过去了，他自己在医院难受，还叫自己朋友过来陪床，实在有点可怜的了，所以就没有去，选择了在医院陪床。&lt;/p&gt;
&lt;p&gt;如果是自己有两个孩子，一个生病住院，一个订婚。如果没人陪，想想就难受啊。&lt;/p&gt;
&lt;p&gt;所以二胎不仅仅是钱的问题，&lt;strong&gt;责任&lt;/strong&gt;也太太重了。&lt;/p&gt;
&lt;p&gt;也许新手父母不清楚如何做好一个父母，但是最起码要懂如何和朋友处好关系。把孩子当成自己支配的工具，所谓的父爱母爱也得不到孩子的认可，只是感动自己罢了。一个家，最重要的就是松弛和愉悦（我认为的）。&lt;strong&gt;想想如何和朋友保持距离，又时刻保持联系，也许是和孩子愉快相处的一个比较好的解吧。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;that&apos;s all.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[集市周报 Vol.02]]></title><link>https://zzao.club/post/report/weekly-report-02</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-02</guid><pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;成都七日游&lt;/h2&gt;
&lt;p&gt;上上周很突然的接到一个出差的通知，导致我上上周末直接飞到了&lt;code&gt;成都&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;并且由于上周的四川之旅让人沉醉，导致我完全忘了上上周干了什么，所以只能跳过了...😯&lt;/p&gt;
&lt;p&gt;在去成都之前，我对西南那边的印象来自于&lt;code&gt;《风犬少年的天空》&lt;/code&gt;，只觉得他们方言很好听很亲切，现在竟然突然有了这么一个机会！！&lt;/p&gt;
&lt;p&gt;由于飞机上没有WiFi，我准备在微信读书里找本小说缓存一下看看，没想到又翻到了&lt;code&gt;龙族&lt;/code&gt;，虽然高中的时候就看过了，但现在已经忘的差不多了，准备二刷一下。&lt;/p&gt;
&lt;p&gt;上次坐飞机还是疫情前去印尼巴厘岛，遥墙机场早就忘了什么样了。而这次去成都，准备让家里另外的三大一小也飞过去玩玩，毕竟机会难得，如果不是来这边出差，可能不会狠下心来跑这么远旅游，所以就简单的总结了一下&lt;a href=&quot;https://zzao.club/post/travel/jinan-fly-guide&quot;&gt;飞机起飞前的教程&lt;/a&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;早上 9 点上班，中午 11 点 30 去吃饭。中午他们休息，我回酒店打游戏，下午 2 点继续上班，5 点 30 他们就要跑路。相当于上午两个半小时，下午三个半小时，下午他们还要下去逛逛，消费一下。&lt;/p&gt;
&lt;p&gt;当然了，代码质量也惨不忍睹。&lt;/p&gt;
&lt;p&gt;下了班我就去城里逛逛，一开始坐地铁，发现挺累的。后面我用滴滴打车 10-15 元就能从天府二街附近到宽窄巷子，也就懒得走路了。&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&gt;不能共情，就放弃幻想&lt;/h2&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&gt;热爱的东西&lt;/h2&gt;
&lt;p&gt;我现在热爱的东西应该就是写代码。&lt;/p&gt;
&lt;p&gt;讲真，如果我初中高中能学习美术等艺术类学科，我现在应该是一个设计师。但当时艺术生只是学习不好的一个退路。&lt;/p&gt;
&lt;p&gt;现在回想起来，单论大学的话，如果一个脑子比较好使的人对艺术感兴趣，走艺术生高考能考上的大学要比学文理科强一些。不过艺术也容易陷入一些对崇高艺术虚无缥缈的追求里去，最终的热爱被现实击打后可能就只剩下考公考编了。而理科生对应的互联网行业则更加宽容和开放一些。虽然互联网行业对大学专业没有很多硬性要求，但如果父母对非编制类的职业缺乏理解的话，步入一个理想的行业也就更困难了。&lt;/p&gt;
&lt;p&gt;虽然现在依然对这些东西抱有热情，但 30 岁的时间成本高的吓人。丧失了学生时代的大把时间的从零学习的机会，工作以后再想接触就只能速成了，但速成一般只是为了工作或赚钱，这只会让自己对热爱失去热情。再者，以前我可能还可以说自己是一个勤快的人，现在的我是真的懒了。我离不开任何时代赋予我的环境，只能把属于自己的帐篷尽力撑的高高的，这样在帐篷里休息才会显得轻松自在一些。&lt;/p&gt;
&lt;h2&gt;早早集市&lt;/h2&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;接下来要做的应该是增加几个文章的来源（Nuxt Content），以及对于一篇文章的注解/评论引用功能。&lt;/p&gt;
&lt;p&gt;然后要做的就是一个热力图，类似 Github 活跃度。但并非只有发了文章，写了代码才会有一个绿色的块块，其他都是灰色的。对于人生而言，工作、娱乐、健身、旅游等等一样重要，无论怎么度过一天，应该都有颜色。&lt;/p&gt;
&lt;p&gt;然后就是补充自己的知识，再不断输出内容了。&lt;/p&gt;
&lt;p&gt;好吧，这就这样吧！👏&lt;/p&gt;</content:encoded></item><item><title><![CDATA[济南遥墙机场起飞指南]]></title><link>https://zzao.club/post/travel/jinan-fly-guide</link><guid isPermaLink="true">https://zzao.club/post/travel/jinan-fly-guide</guid><pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;必带&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;身份证&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;出发&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;9:25&lt;/strong&gt; 的飞机&lt;/p&gt;
&lt;p&gt;我 &lt;strong&gt;6:45&lt;/strong&gt; 出发&lt;/p&gt;
&lt;p&gt;司机走经十路+高速（8 元高速费），7:10 ~ 7:15 左右到达，&lt;strong&gt;用时 20-30 分钟&lt;/strong&gt;，全程 15 公里左右，费用 x（我走的企业订车）元&lt;/p&gt;
&lt;p&gt;达到&lt;strong&gt;遥墙机场航站楼二楼&lt;/strong&gt;（坐飞机都是去这里）&lt;/p&gt;
&lt;p&gt;到达后选择&lt;strong&gt;任意入口&lt;/strong&gt;进入，因为都是通往一个大厅&lt;/p&gt;
&lt;h2&gt;值机大厅&lt;/h2&gt;
&lt;p&gt;大厅只有两个作用：取机票和托运行李&lt;/p&gt;
&lt;p&gt;进入大厅后是这样的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505260941794.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中左侧，也就是进门后正对的墙上会看到 &lt;strong&gt;B区 13-32&lt;/strong&gt;，指的是&lt;code&gt;值机口&lt;/code&gt;，值机口用于办理行李托运。&lt;/p&gt;
&lt;p&gt;图中右上角，也就是进来后你的头顶，标的是&lt;code&gt;登机口&lt;/code&gt;对应的检查区域（&lt;code&gt;安检区&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;所有登机口对应区域如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505260941796.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这两个口可以在你的&lt;strong&gt;航旅纵横&lt;/strong&gt; App 上清晰的看到。&lt;/p&gt;
&lt;p&gt;进来后&lt;strong&gt;先找自助机器&lt;/strong&gt;拿机票。&lt;/p&gt;
&lt;p&gt;拿到机票后，去&lt;code&gt;值机口&lt;/code&gt;办理行李托运。&lt;/p&gt;
&lt;p&gt;然后这个大厅里的全部就已经办完了。&lt;/p&gt;
&lt;p&gt;这个时候不要去吃永和豆浆最低 &lt;code&gt;49&lt;/code&gt; 元的六个小笼包+一碗豆浆的套餐。留着买它 17 个蛋烘糕不香吗！！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;直接去&lt;code&gt;安检区&lt;/code&gt;排队安检&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;安检需要&lt;strong&gt;机票+身份证&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;排队 -&gt; 交替通行 -&gt; 拍照，进入后。需要拿托盘，把&lt;strong&gt;包里的电子设备都拿出来放入托盘中&lt;/strong&gt;，然后把剩余所有东西也都放入托盘。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两手空空&lt;/strong&gt;去检查身上有无违规物品&lt;/p&gt;
&lt;p&gt;检查后，拿走行李，就来到了&lt;code&gt;候机厅&lt;/code&gt;，这里也有吃不起的餐厅。&lt;/p&gt;
&lt;p&gt;在这里&lt;strong&gt;找到自己的登机口&lt;/strong&gt;，就近&lt;strong&gt;等待服务人员喊着自己的飞机开始检票的通知&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;主要路线&lt;/h2&gt;
&lt;ol&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;li&gt;找到登机口等待上飞机&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item><item><title><![CDATA[完全自建博客站需要付出的成本]]></title><description><![CDATA[如果不依靠第三方平台，完全使用自己的服务器自建博客，需要哪些成本?]]></description><link>https://zzao.club/post/zzao/the-cost-of-build-own-blog</link><guid isPermaLink="true">https://zzao.club/post/zzao/the-cost-of-build-own-blog</guid><pubDate>Thu, 15 May 2025 16:30:37 GMT</pubDate><content:encoded>&lt;p&gt;如果不依靠第三方平台，完全使用自己的服务器自建博客，需要哪些成本?&lt;/p&gt;
&lt;h2&gt;云/轻量服务器&lt;/h2&gt;
&lt;p&gt;一般借助了第三方框架时，会选择类似 &lt;code&gt;Vercel&lt;/code&gt; 的平台部署自己的网站&lt;/p&gt;
&lt;p&gt;但如果完全自己部署，必不可少的需要购买服务器。&lt;/p&gt;
&lt;p&gt;以 &lt;strong&gt;2核8G&lt;/strong&gt; 的腾讯轻量服务器为例，每年大概需要 &lt;code&gt;500&lt;/code&gt; 元左右的费用&lt;/p&gt;
&lt;p&gt;之所以需要 8G 内存，是为了实现一个在服务器自动化打包部署。&lt;/p&gt;
&lt;p&gt;NodeJS 在构建时会吃掉大量内存。其次，服务器本身需要一些内存来运行 Nginx、Docker、Gitea等。&lt;/p&gt;
&lt;p&gt;如果不用自动化部署，写文章和写代码部署代码会耦合在一起，十分恶心。&lt;/p&gt;
&lt;p&gt;像是平台化的一些建站工具，也是给了你一个 GUI 界面，自己点点部署按钮之类的&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;code&gt;2s&lt;/code&gt; 之内&lt;/p&gt;
&lt;h2&gt;SSL证书&lt;/h2&gt;
&lt;p&gt;SSL 证书是不可能花钱买的，我目前是配合 &lt;a href=&quot;https://github.com/usual2970/certimate&quot;&gt;cerimate&lt;/a&gt; 自动申请+部署证书。&lt;/p&gt;
&lt;h2&gt;域名&lt;/h2&gt;
&lt;p&gt;非 &lt;code&gt;.com&lt;/code&gt; 的一年小几十，&lt;code&gt;.com&lt;/code&gt; 大几十或者 100 出头&lt;/p&gt;
&lt;h2&gt;图片&lt;/h2&gt;
&lt;p&gt;我花了 &lt;code&gt;99&lt;/code&gt; 买了腾讯的&lt;strong&gt;对象存储&lt;/strong&gt;，外加 &lt;strong&gt;CDN&lt;/strong&gt; （99？），专门用来存文章里的图片。&lt;/p&gt;
&lt;p&gt;但平台本身就有操作和理解门槛。还要设置防盗，限流等等。&lt;/p&gt;
&lt;p&gt;也许对于一个普通博客站来说，使用 &lt;code&gt;unjs/ipx&lt;/code&gt; 就够了&lt;/p&gt;
&lt;p&gt;指定只能自己的域名访问图片&lt;/p&gt;
&lt;p&gt;存储的话，可以写一个上传接口，传到服务器上&lt;/p&gt;
&lt;p&gt;也可以直接就和项目代码放在一起，用 public 目录管理图片&lt;/p&gt;
&lt;h2&gt;编码&lt;/h2&gt;
&lt;p&gt;自己写要想选个框架吧&lt;/p&gt;
&lt;p&gt;要设计 UI 吧，光 UI 就得纠结一阵&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;strong&gt;一看，一天访客不到十个人&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过也有一些好处&lt;/p&gt;
&lt;p&gt;比如 写文章的素材多了很多 （但是似乎太多了也懒得写，太少了也懒得写...）&lt;/p&gt;
&lt;p&gt;对技术的应用会有更深的理解，如果平时会思考一些架构问题，自己动手实践一个真正的全栈会很有好处&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;放在前两年，肯定时间成本会比较高，因为毕竟需要有一定的编程基础。但是现在 AI 的增幅效果越来好&lt;/p&gt;
&lt;p&gt;今天的你在只借助 &lt;code&gt;AI&lt;/code&gt; 的情况下也能完成开发工作，但在有一位开发者朋友或开发者交流群的情况下会更容易向 &lt;code&gt;AI&lt;/code&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;如果你最近 1-3 年一直在尝试，可以试着回想一下，收获了什么&lt;/p&gt;
&lt;p&gt;这些时间用在寄托了暴富的副业、活动上，真的比那些用时间换钱的“苦活”收益更大吗&lt;/p&gt;
&lt;p&gt;有时候，能定义自己是一个什么样的人，恰恰是当下正在做的看起来毫无收益的事。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[磨磨唧唧的终于把访客点赞评论功能加上了]]></title><description><![CDATA[现在可以基于前端生成的指纹 id，作为游客身份直接互动]]></description><link>https://zzao.club/post/zzao/release-1.0-for-base-work</link><guid isPermaLink="true">https://zzao.club/post/zzao/release-1.0-for-base-work</guid><pubDate>Thu, 15 May 2025 16:14:38 GMT</pubDate><content:encoded>&lt;p&gt;现在可以基于前端生成的指纹 id，作为游客身份直接互动&lt;/p&gt;
&lt;p&gt;🫡&lt;/p&gt;
&lt;p&gt;[[zzao/the-cost-of-build-own-blog|the-cost-of-build-own-blog]]&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt Content 实现 TOC 组件]]></title><description><![CDATA[Nuxt Content 中配置和使用 toc]]></description><link>https://zzao.club/post/nuxt/content/nuxt-content-toc</link><guid isPermaLink="true">https://zzao.club/post/nuxt/content/nuxt-content-toc</guid><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;TOC&lt;/code&gt; 全称 &lt;code&gt;table of contents&lt;/code&gt; ，指的是一篇文章内的 &lt;code&gt;h1&lt;/code&gt;、&lt;code&gt;h2&lt;/code&gt;、&lt;code&gt;h3&lt;/code&gt; 等标题的导航，用于快速跳转到对应的标题处。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Nuxt Content&lt;/code&gt; 会为 &lt;code&gt;markdown&lt;/code&gt; 内容生成 &lt;code&gt;toc&lt;/code&gt; 数据。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;queryCollection&lt;/code&gt; 获取的 &lt;code&gt;page&lt;/code&gt; 数据中，通过 &lt;code&gt;page?.body?.toc?.links&lt;/code&gt; 拿到当前文章的 &lt;code&gt;toc&lt;/code&gt; 数据&lt;/p&gt;
&lt;p&gt;以&lt;a href=&quot;https://zzao.club/post/nuxt/nuxt-3.17-release&quot;&gt;《Nuxt 3.17 发布，对比3.16有一个重大改变》&lt;/a&gt;这篇文章为例，数据是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;[
    {
        &quot;id&quot;: &quot;更新日志&quot;,
        &quot;depth&quot;: 2,
        &quot;text&quot;: &quot;更新日志&quot;,
        &quot;children&quot;: [
            {
                &quot;id&quot;: &quot;数据获取改进&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;数据获取改进&quot;
            },
            {
                &quot;id&quot;: &quot;新增内置组件&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;新增内置组件&quot;
            },
            {
                &quot;id&quot;: &quot;路由改进&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;路由改进&quot;
            },
            {
                &quot;id&quot;: &quot;加载指示器自定义&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;加载指示器自定义&quot;
            },
            {
                &quot;id&quot;: &quot;文档作为包&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;文档作为包&quot;
            },
            {
                &quot;id&quot;: &quot;开发体验改进&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;开发体验改进&quot;
            },
            {
                &quot;id&quot;: &quot;模块开发增强&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;模块开发增强&quot;
            },
            {
                &quot;id&quot;: &quot;性能改进&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;性能改进&quot;
            },
            {
                &quot;id&quot;: &quot;其他改进&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;其他改进&quot;
            }
        ]
    },
    {
        &quot;id&quot;: &quot;主要影响点&quot;,
        &quot;depth&quot;: 2,
        &quot;text&quot;: &quot;主要影响点&quot;,
        &quot;children&quot;: [
            {
                &quot;id&quot;: &quot;useasyncdatausefetch&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;useAsyncData、useFetch&quot;
            },
            {
                &quot;id&quot;: &quot;_2025年05月06日093334-更新&quot;,
                &quot;depth&quot;: 3,
                &quot;text&quot;: &quot;2025年05月06日09:33:34 更新&quot;
            }
        ]
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般一篇文章里，&lt;code&gt;h1&lt;/code&gt; 表示的是文章标题，在一个页面中通常只会存在一个 &lt;code&gt;h1&lt;/code&gt; 标题，所以在写文章时，要注意不要乱用 &lt;code&gt;# 标题&lt;/code&gt;这个语法。 &lt;code&gt;h2&lt;/code&gt;、&lt;code&gt;h3&lt;/code&gt;、 就是文章里常用的二级和三级标题，在数据中就是 &lt;code&gt;depth&lt;/code&gt; 为 &lt;code&gt;2&lt;/code&gt; 或 &lt;code&gt;3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;组件本身使用 &lt;code&gt;ul&lt;/code&gt; 、&lt;code&gt;li&lt;/code&gt; 来渲染即可，再配合 &lt;code&gt;fixed&lt;/code&gt; 或 &lt;code&gt;sticky&lt;/code&gt;，使其国定在文章的一侧。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;Nuxt&lt;/code&gt; 来说，&lt;code&gt;TOC&lt;/code&gt; 组件可以完全是一个&lt;strong&gt;客户端组件&lt;/strong&gt;，因为不需要被爬虫抓取，也不是初次渲染需要的重要信息。而且如果要做一些简单的交互，也需要等前端环境加载出来之后才能做到。&lt;/p&gt;
&lt;p&gt;所以只需要使用一个 &lt;code&gt;computed&lt;/code&gt; 拿到 &lt;code&gt;toc&lt;/code&gt; 数据，然后把数据传递给组件即可。&lt;/p&gt;
&lt;p&gt;我观察了一圈，感觉&lt;a href=&quot;https://sspai.com/&quot;&gt;少数派&lt;/a&gt;的 TOC 组件是比较美观的，于是我仿照他们的思路封装了自己的 &lt;a href=&quot;https://github.com/aatrooox/blog.zzao.club/blob/main/app/components/common/AppToc.vue&quot;&gt;TOC 组件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;toc 的配置位于 &lt;code&gt;nuxt.config.ts&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;content: {
	build: {
		markdown: {
			toc: {
			  depth: 2,
			  searchDepth: 2
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认深度是 &lt;code&gt;2&lt;/code&gt;，我一般会用到 &lt;code&gt;3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;同时，如果要想自己定义 h1、h2、h3 标题的样式，需要在 &lt;code&gt;app/components/content&lt;/code&gt; 目录下新建 &lt;code&gt;ProseH1.vue&lt;/code&gt;、&lt;code&gt;ProseH2.vue&lt;/code&gt;、&lt;code&gt;ProseH3.vue&lt;/code&gt; 组件。&lt;/p&gt;
&lt;p&gt;写样式时，不管如何封装，&lt;strong&gt;记得不要丢掉 id 属性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ProseH3.vue 为例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div :id=&quot;props.id&quot; class=&quot;heading my-4 cursor-pointer scroll-mt-14&quot;&gt;
    &amp;#x3C;span class=&quot;px-2 py-1 text-xl font-bold bg-zinc-800 text-white dark:bg-zinc-200 &quot;&gt;
      &amp;#x3C;a v-if=&quot;props.id &amp;#x26;&amp;#x26; generate&quot; :href=&quot;`#${props.id}`&quot; class=&quot;!text-zinc-200 dark:!text-zinc-800&quot;&gt;
        &amp;#x3C;slot /&gt;
      &amp;#x3C;/a&gt;
      &amp;#x3C;slot v-else /&gt;
    &amp;#x3C;/span&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
import { computed, useRuntimeConfig } from &apos;#imports&apos;

const props = defineProps&amp;#x3C;{ id?: string }&gt;()

const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() =&gt; props.id &amp;#x26;&amp;#x26; ((typeof headings?.anchorLinks === &apos;boolean&apos; &amp;#x26;&amp;#x26; headings?.anchorLinks === true) || (typeof headings?.anchorLinks === &apos;object&apos; &amp;#x26;&amp;#x26; headings?.anchorLinks?.h1)))
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 TOC 组件中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;# template v-for child in link.children
&amp;#x3C;li&gt;
    &amp;#x3C;span&gt;#&amp;#x3C;/span&gt;
    &amp;#x3C;NuxtLink :href=&quot;`#${child.id}`&quot;&gt; {{ child.text }} &amp;#x3C;/NuxtLink&gt;
&amp;#x3C;/li&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以像我的组件一样，配合 &lt;code&gt;IntersectionObserver&lt;/code&gt; ，做到 TOC 组件的导航根据滚动的区域使其高亮或是显示其他标识&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505131613386.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt; const headings = document.querySelectorAll(&apos;.heading&apos;)
	observer.value = new IntersectionObserver((entries) =&gt; {
	  entries.forEach(entry =&gt; {
		if (entry.isIntersecting) {
		  activeId.value = entry.target.id
		}
	  })
	})
	headings.forEach(heading =&gt; observer.value.observe(heading))
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[目前对我而言开发Nuxt最舒适的UI框架]]></title><description><![CDATA[选择 shadcn/vue 和 inspira-ui 的理由]]></description><link>https://zzao.club/post/nuxt/nuxt-ui-framework-recommend</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt-ui-framework-recommend</guid><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;对于 &lt;code&gt;Nuxt&lt;/code&gt; 来说，有很多前端 UI 框架可以选择。&lt;/p&gt;
&lt;p&gt;比如官方的 &lt;code&gt;NuxtUI&lt;/code&gt; 、&lt;code&gt;NuxtUI Pro&lt;/code&gt;，以及我的博客最开始用的框架 &lt;code&gt;Primevue&lt;/code&gt;，都算是比较&lt;strong&gt;省心&lt;/strong&gt;的框架。&lt;/p&gt;
&lt;p&gt;但是对我来说，这几个框架用在 &lt;code&gt;Nuxt&lt;/code&gt; 上就像是 &lt;code&gt;ElementUI&lt;/code&gt; 用在了 &lt;code&gt;Vue&lt;/code&gt; 的后台管理系统上一样，确实省事，也足够标准，但是前提都是你不会去二开他们的样式和功能。&lt;/p&gt;
&lt;p&gt;一但现有的样式不满足你的审美，某个组件的功能差强人意，但心里就是感觉膈应。一切就不是那么美好了。仿佛是某个客户要求你把 &lt;code&gt;ElementUI&lt;/code&gt; 的样式全都大改一遍，以达到他们心中的高端形象。&lt;/p&gt;
&lt;p&gt;所以我最终选择的是 &lt;code&gt;shadcn/vue&lt;/code&gt; + &lt;code&gt;inspira-ui&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shadcn/ui&lt;/code&gt; 在 React 那边是很火的，可能有人不知道他也有一个 &lt;code&gt;vue&lt;/code&gt; 的版本。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505131443401.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用方式是把组件下载到项目本地，所以给了你最大的可控性。&lt;/p&gt;
&lt;p&gt;同时里面的有些组件，比如 &lt;code&gt;sonner&lt;/code&gt; ，用的是 &lt;code&gt;vue-sonner&lt;/code&gt;，已经是经过很多人验证的，在 toC 领域算是很美观的交互组件。还有 &lt;code&gt;drawer&lt;/code&gt; 也是类似的情况，我觉得还是蛮方便的，把好用的组件都集合在一起。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;inspira-ui&lt;/code&gt;是对 &lt;code&gt;shadcn/vue&lt;/code&gt; 的一个补充和升级，主要用来实现一些炫酷的动画、特效。灵感来自于 &lt;code&gt;Aceternity UI&lt;/code&gt;, &lt;code&gt;Magic UI&lt;/code&gt;，填补了 &lt;code&gt;vue&lt;/code&gt; 生态下相关组件的空白。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505131443403.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;有这两个组件库，外加 tailwindcss ，不管是开发管理系统，还是面向用户的美观页面，都没有发现不够用的地方。&lt;/p&gt;
&lt;p&gt;推荐你也去试一试～&lt;/p&gt;</content:encoded></item><item><title><![CDATA[集市周报 Vol.01]]></title><description><![CDATA[最近的总结]]></description><link>https://zzao.club/post/report/weekly-report-01</link><guid isPermaLink="true">https://zzao.club/post/report/weekly-report-01</guid><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;集市周报 Vol.01&lt;/h1&gt;
&lt;h2&gt;😎五一&lt;/h2&gt;
&lt;p&gt;五一期间一家人回到了丈母娘家。&lt;/p&gt;
&lt;p&gt;每一年里比较长的假期基本都会回去，因为平时我爸妈和我们一起住，帮我们带孩子。&lt;/p&gt;
&lt;p&gt;孩子姥姥家离的比较远，230 公里左右，开车两个半小时。&lt;/p&gt;
&lt;p&gt;对于两个懒 b 打工人来说，一般周六是没有上午的，醒了可能就在床上看看中午吃啥了。如果周六回去的话，早上正常起床吃饭收拾东西出发，到家一般也得下午 1～2 点了。而且爸妈他们平时不咋休息，都是在厂子里上班，我们回去他们可能还要请假，或者抽空回来，所以我们一般只挑长假期回家。&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;除了日常的走亲戚串门之外，这次还去了离家只有 30 公里左右的海边。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505121529782.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这回我是真赶上海了，以前只在 B站 刷一些赶海视频，觉得十分有趣。这回终于来到了一片儿能赶海的区域。&lt;/p&gt;
&lt;p&gt;因为这边靠近黄河入海口，所以这边全是泥滩，而不是沙滩。让我震惊的是，这海水退潮竟然能退到那么远吗🥲，远处的人都成了一个小点儿了，感觉有个小几公里远。&lt;/p&gt;
&lt;p&gt;可惜这次没带装备，把孩子扔给姥姥姥爷后，我俩就去里面转了一圈就出来了。当然也不是全无收获，有两只&lt;code&gt;“一斤大”&lt;/code&gt;的螃蟹🦀&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202505121529784.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为我们是大中午头来的，离岸比较近的地方早都被人扫荡一空，脚印比气孔还多，而且全是淤泥。下次再来的话，必须得早点来，然后带上家伙！&lt;/p&gt;
&lt;p&gt;不过爸妈表示这是花钱找罪受（40 元/人），我俩倒是十分喜欢这种满地寻宝的感觉。&lt;/p&gt;
&lt;p&gt;五一假期转瞬即逝。&lt;/p&gt;
&lt;h2&gt;🫨工作&lt;/h2&gt;
&lt;p&gt;和去年比，今年的工作简直就是地狱一般。&lt;/p&gt;
&lt;p&gt;时不时就开始冲刺，2 月冲完 3 月冲，3 月冲完 4 月冲。让我不禁有些怀疑这还是不是之前的养老圣地。&lt;/p&gt;
&lt;p&gt;但是好在工作内容早已固定，我也不期望在公司能有什么历练和成长了。要非要说还有什么成长空间的话，可能就是直接和客户对接或负责整个项目吧。但毕竟是个前端，一般是不需要参与具体的业务的，所以目测也不会有这样的机会了。&lt;/p&gt;
&lt;p&gt;对于平时的工作，我在可以优化的地方已经做好了 CLI 工具，可以直接化繁为简，专门解决掉一些繁琐但又不得不做的工作。本来我还犹豫要不要上报给领导，领一份&lt;code&gt;嘉奖&lt;/code&gt;，但是现在可以非常肯定不会这么做了。&lt;/p&gt;
&lt;p&gt;因为我们的工作，就是做这些重复、无用、繁琐的东西。我们存在的意义，也是和费时挂钩的，如果对于公司的工作以及对于客户花钱购买我们的项目和人力来说，真的不费时了，大概也不是一件好事。还是在私下里使用，让大家节省一些时间，拿去摸摸鱼吧。&lt;/p&gt;
&lt;p&gt;省下的时间，大部分是用来做这个博客的项目了。&lt;/p&gt;
&lt;h2&gt;正在做的博客&lt;/h2&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;毕竟这个 App，我构思了很久了，现在感觉（未来）很贴近自己的生活场景，就当是一个互联网遗产吧。以后哪怕不做程序员了， 写点文章，发发牢骚也是个不错的爱好。考虑到换行的话，大概率不会像这样每天敲代码了，所以一个全栈项目是肯定的了，打磨完善后可以只记录、发表、分享，什么开发新功能啥的，够自己用就行了。希望能早日完成早早集市吧 😩&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;果然星期一就是适合碎碎念，摸摸鱼，完全不适合工作啊 😏&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Cli工具集成到Electron客户端]]></title><description><![CDATA[把基于JS/TS的命令行工具，转换为Electron客户端，无需重写代码，还能大大提升非开发者使用体验]]></description><link>https://zzao.club/post/cli/cli-to-electron-readme</link><guid isPermaLink="true">https://zzao.club/post/cli/cli-to-electron-readme</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;给公司开发了一些小脚本，发布为了一个命令行工具&lt;/p&gt;
&lt;p&gt;前端小伙伴反馈说，命令行要比客户端好用，因为只敲了很少的命令就完事了&lt;/p&gt;
&lt;p&gt;但对于非前端开发者，Node 环境问题比较头疼，而且如果是非技术人员，可能命令行也无法接受&lt;/p&gt;
&lt;p&gt;之前把单独一个功能复制粘贴再改改，集成到 &lt;code&gt;electron&lt;/code&gt; 里去过，但工作量太大&lt;/p&gt;
&lt;p&gt;最近又有了新思路之后，尝试了一番，花了大概俩小时，把之前的 cli 的大部分功能搬到了客户端上&lt;/p&gt;
&lt;p&gt;核心思路就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原CLI须用Bun进行打包为二进制文件, electron中直接使用 child_process.spawn 执行, 监听输出信息返回到前端进行展示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前端中需要有一个配置文件 &lt;code&gt;src/config.ts&lt;/code&gt;, 来表示自己的 &lt;code&gt;cli&lt;/code&gt; 有哪些 &lt;code&gt;command&lt;/code&gt; 可以运行&lt;/p&gt;
&lt;p&gt;就是这么一个简易的UI&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202504101707557.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Cli打包&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;目前仅可打包支持esm的包, 如sharp这种c++包还在研究中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装&lt;code&gt;bun&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;macos&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -fsSL https://bun.sh/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wind&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;powershell -c &quot;irm bun.sh/install.ps1 | iex&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包到指定平台&lt;/p&gt;
&lt;p&gt;具体请看官方文档: &lt;a href=&quot;https://bun.sh/docs/bundler/executables&quot;&gt;支持的平台参数&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;RUNTIME_ENV=electron bun build ./src/index.js --compile --target=bun-darwin-arm64 --env inline  --outfile z-cli
RUNTIME_ENV=electron bun build ./src/index.js --compile --target=bun-windows-x64 --env inline  --outfile z-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;RUNTIME_ENV&lt;/code&gt; 的用途是兼容 &lt;code&gt;cli&lt;/code&gt; 里的一些和 &lt;code&gt;electron&lt;/code&gt; 无关的代码&lt;/p&gt;
&lt;p&gt;如检测 &lt;code&gt;package.json&lt;/code&gt; 中信息, 打包后就不存在 &lt;code&gt;package.json&lt;/code&gt; 了, 所以原项目中使用此环境变量过滤一下&lt;/p&gt;
&lt;h2&gt;Electron 配置&lt;/h2&gt;
&lt;h3&gt;resource/bin&lt;/h3&gt;
&lt;p&gt;把打包后的二进制文件放在此目录下&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ipc.ts&lt;/code&gt; 以及 &lt;code&gt;App.vue&lt;/code&gt; 中修改 &lt;code&gt;z-cli&lt;/code&gt; 相关字符(binaryName/channel) 为 &lt;strong&gt;自己的cli名称&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Vue 配置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/config.ts&lt;/code&gt; 修改为自己 cli 的命令配置&lt;/p&gt;
&lt;h2&gt;打包/调试&lt;/h2&gt;
&lt;p&gt;最终构建好的客户端, 包含一行命令的下拉栏, 以及一个执行结果&lt;/p&gt;
&lt;p&gt;原本 &lt;code&gt;cli&lt;/code&gt; 的配置文件等都会复用&lt;/p&gt;
&lt;p&gt;相当于只是把 &lt;code&gt;cli&lt;/code&gt; 的敲命令过程可视化, 把结果照搬到 &lt;code&gt;electron&lt;/code&gt; 中&lt;/p&gt;
&lt;h2&gt;注意&lt;/h2&gt;
&lt;p&gt;注意下 &lt;code&gt;cli&lt;/code&gt; 里的依赖包,是否兼容 &lt;code&gt;bun&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;axios&lt;/code&gt; 需要替换成 &lt;code&gt;fetch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sharp&lt;/code&gt; 可能在构建成客户端后会报错 (待解决)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;开源地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/aatrooox/cli-to-electron&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[使用 pm2 启动 ipx 服务(bun运行时)]]></title><description><![CDATA[在给 imgx 的预设静态化图片启动服务时，我使用了 unjs/ipx 来作为图片服务，这样可以直接进行格式转换和裁剪]]></description><link>https://zzao.club/post/imgx/pm2-with-bun-x</link><guid isPermaLink="true">https://zzao.club/post/imgx/pm2-with-bun-x</guid><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在给 &lt;code&gt;imgx&lt;/code&gt; 的预设静态化图片启动服务时，我使用了 &lt;code&gt;unjs/ipx&lt;/code&gt; 来作为图片服务，这样可以直接进行格式转换和裁剪&lt;/p&gt;
&lt;p&gt;后续图片操作（x）也必然涉及到要先把图片存储再操作，所以还是单独启动一个服务罢了&lt;/p&gt;
&lt;p&gt;考虑到 &lt;code&gt;bun&lt;/code&gt; 的吞吐量要比 &lt;code&gt;node&lt;/code&gt; 大一些&lt;/p&gt;
&lt;p&gt;于是使用此命令来启动 &lt;code&gt;ipx&lt;/code&gt; 服务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;bunx ipx serve --dir ./public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应到 &lt;code&gt;pm2&lt;/code&gt; 的 &lt;code&gt;config.yml&lt;/code&gt; 上配置为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apps:
  - name: imgx-nitro
    script: ./server/index.mjs
    interpreter: node
    exec_mode: fork
    env:
      PORT: 1234
  - name: imgx-ipx
    script: bunx
    args: [&quot;ipx&quot;, &quot;serve&quot;, &quot;--dir&quot;, &quot;./public&quot;]
    exec_mode: fork
    env:
      PORT: 5678
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意不需要指定 &lt;code&gt;bun&lt;/code&gt; ，直接使用 &lt;code&gt;bunx&lt;/code&gt; 即可&lt;/p&gt;
&lt;p&gt;系统为 &lt;code&gt;Debian12.0&lt;/code&gt; ，供诸君参考&lt;/p&gt;</content:encoded></item><item><title><![CDATA[人不 EMO 枉青年]]></title><description><![CDATA[人不 EMO 枉青年]]></description><link>https://zzao.club/post/daily/emo-man</link><guid isPermaLink="true">https://zzao.club/post/daily/emo-man</guid><pubDate>Thu, 27 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&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;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;尤其是在 30 岁这个&quot;适合&quot;投入大量时间探索的年纪，偏偏琐事缠身&lt;/p&gt;
&lt;p&gt;20 岁以前懵懂、无知，只知道学校最大，老师最大，家长最大&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;在混沌中浮沉，周围全是要&quot;拉你一把&quot;的一双双的手&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Z-CLI 使用说明]]></title><link>https://zzao.club/post/cli/cli-readme</link><guid isPermaLink="true">https://zzao.club/post/cli/cli-readme</guid><pubDate>Fri, 21 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;Node&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;nvm&lt;/code&gt; 管理多个 &lt;code&gt;node&lt;/code&gt; 版本&lt;/p&gt;
&lt;p&gt;&lt;code&gt;node &gt;= 18.18.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;推荐版本(我的)：&lt;code&gt;20.18.1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;查看 &lt;code&gt;registry&lt;/code&gt; （非必要步骤）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm config get registry
# https://registry.npmjs.org
# 如果不是，则需要先设置
npm config set registry=https://registry.npmjs.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装最新版&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i -g @zzclub/z-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装后只能在当前 &lt;code&gt;node&lt;/code&gt; 版本下使用，切换到其他低版本 &lt;code&gt;node&lt;/code&gt; 则无效&lt;/p&gt;
&lt;p&gt;全局命令为：&lt;code&gt;zz&lt;/code&gt;  或 &lt;code&gt;z&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Bun&lt;/h3&gt;
&lt;p&gt;macos&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -fsSL https://bun.sh/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wind&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;powershell -c &quot;irm bun.sh/install.ps1 | iex&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装最新版&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;bun i -g @zzclub/z-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果提示缺少某个包&lt;/strong&gt;，可以按提示再次运行相关命令&lt;/p&gt;
&lt;h2&gt;命令&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i18n&lt;/code&gt; 缩写 &lt;code&gt;i&lt;/code&gt; 从Vue文件提取出&lt;strong&gt;中文国际化文件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;translate&lt;/code&gt; 缩写：&lt;code&gt;trans&lt;/code&gt;  批量翻译&lt;strong&gt;中文国际化文件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt; 设置配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tiny&lt;/code&gt; 任意图片压缩体积&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;i18n 规则说明&lt;/h2&gt;
&lt;h3&gt;文件&lt;/h3&gt;
&lt;p&gt;只提取 &lt;code&gt;.vue&lt;/code&gt; 文件&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;$t&lt;/code&gt;解析及生成规则&lt;/h3&gt;
&lt;p&gt;提取正则： &lt;code&gt;/\$t\([&apos;&quot;]i18n\.([^&apos;&quot;]+)[&apos;&quot;]\)/g&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;$t(&apos;i18n.module-name.placeholder.month&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;module-name 会被解析为文件名称 =&gt; &lt;code&gt;module-name.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后面的内容会被解析为对象的属性 =&gt; &lt;code&gt;{ placeholder: { month: &quot;month&quot;}} &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;保存位置如果没传。就会保存在 &lt;code&gt;.vue&lt;/code&gt; 同级目录下&lt;/li&gt;
&lt;li&gt;默认忽略 module-name 为 &lt;code&gt;common&lt;/code&gt; =&gt; &lt;code&gt;$t(&apos;i18n.common.xxx&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中文注释规范及提取规则&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;!--i18n addTitle=你好呀 a=不错   b=叭叭叭 c=哈哈哈  --&gt;
  &amp;#x3C;CommonEditForm :page-type=&quot;pageType&quot; :title-config=&quot;titleConfig&quot; :custom-components-code=&quot;$t(&apos;i18n.monthlyForecast.pageTitle.addTitle&apos;)&quot; :is-out=&quot;2&quot;&gt;
    &amp;#x3C;div&gt;{{ $t(&apos;i18n.monthlyForecast.form.a&apos;) }}&amp;#x3C;/div&gt;
    &amp;#x3C;div&gt;{{ $t(&apos;i18n.monthlyForecast.form.b&apos;) }}&amp;#x3C;/div&gt;
    &amp;#x3C;!--i18n select=真棒 --&gt;
    &amp;#x3C;div&gt;{{ $t(&apos;i18n.monthlyForecast.placeholder.select&apos;) }}&amp;#x3C;/div&gt;
  &amp;#x3C;/CommonEditForm&gt;

&amp;#x3C;/template&gt;

&amp;#x3C;!-- js 区域 --&gt;
&amp;#x3C;script&gt;
   import {formatDate} from &quot;@/common/utils&quot;
   import CommonEditForm from &quot;@/pages/ifpf/costForecast/monthlyForecast/views/common-edit-form.vue&quot;

   export default {
       name: &apos;ifpfExchangeRateMaintainAdd&apos;,
       components:{
           CommonEditForm,
       },
       data() {
           return {
               pageType: &apos;edit&apos;,
               titleConfig: {
                   title: this.$t(&apos;i18n.monthlyForecast.pageTitle.addTitle&apos;),//测试
                   icon: this.$t(&apos;i18n.monthlyForecast.pageTitle.icon&apos;), // 图标
                   date: formatDate.formatDateOnly(new Date()),
                   info: this.$t(&apos;i18n.monthlyForecast.pageTitle.addInfo&apos;)//你好
               },
           };
       }
   };
&amp;#x3C;/script&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析后：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
  pageTitle: {
    addTitle: &quot;测试&quot;,
    icon: &quot;图标&quot;,
    addInfo: &quot;你好&quot;
  },
  form: {
    a: &quot;不错&quot;,
    b: &quot;叭叭叭&quot;
  },
  placeholder: {
    select: &quot;真棒&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;template 中的注释，&lt;strong&gt;必须&lt;/strong&gt;以 &lt;code&gt;&amp;#x3C;!--i18n&lt;/code&gt; 开头&lt;/li&gt;
&lt;li&gt;template 中的中文配置以 &lt;code&gt;空格&lt;/code&gt; 分割，以 &lt;code&gt;=&lt;/code&gt;号拼接key=value, 如 &lt;code&gt;addTitle=你好呀 a=不错&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;template 中支&lt;strong&gt;持多个&lt;/strong&gt;注释信息&lt;/li&gt;
&lt;li&gt;template 中的 &lt;code&gt;key=value&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 对应 &lt;strong&gt;$t中最后一个key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;js 中支持两种注释信息提取
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$t()&lt;/code&gt; 后紧跟 &lt;code&gt;//&lt;/code&gt; , &lt;code&gt;//&lt;/code&gt;后的&lt;strong&gt;中文内容&lt;/strong&gt;都被视为默认值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$t()&lt;/code&gt; 后存在 &lt;code&gt;,&lt;/code&gt; 、&lt;code&gt;空格&lt;/code&gt; 这两种符号，然后再跟 &lt;code&gt;//&lt;/code&gt;, &lt;code&gt;//&lt;/code&gt;后的中文内容都被视为默认值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解析完成后，自行把文件挪到到 zh-CN 文件夹下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后使用 &lt;code&gt;translate&lt;/code&gt; 命令进行中译英&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;翻译功能配置说明&lt;/h2&gt;
&lt;h3&gt;初始化翻译平台appId和key&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;zz set translate account.appId xxx
zz set translate account.key xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在哪里可以创建appId和key&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;请使用前仔细阅读百度翻译开发平台相关规则&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://fanyi-api.baidu.com/api/trans/product/desktop&quot;&gt;百度翻译开放平台&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;注册&lt;/li&gt;
&lt;li&gt;实名认证
&lt;ol&gt;
&lt;li&gt;标准版 qbs 1  每月5万字符&lt;/li&gt;
&lt;li&gt;高级版 qbs 10 每月100万字符&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;开通通用文本翻译功能&lt;/li&gt;
&lt;li&gt;生成appId和key&lt;/li&gt;
&lt;li&gt;生成后的文件请仔细检查，有可能会有遗漏的翻译，如有，重新执行即可&lt;/li&gt;
&lt;li&gt;注意: 百度翻译的api有一定的调用限制, 请自行评估是否需要使用高级版&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;翻译单个文件&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;zz translate -f ./yourfile.js
# 会在同级目录下生成 yourfile-en.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如&lt;code&gt;test.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
    isok: &apos;早早下班&apos;,
    common: {
        listTitle: &apos;标题&apos;,
        addTitle: &apos;测试&apos;
    },
    test: {
        a: {
            b: {
                c: &apos;哈哈哈&apos;
            }
        },
        aaa: {
            value: &apos;输入&apos;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出文件为test-en.js, 内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
    isok: &quot;Leave work early&quot;,
    common: {
        listTitle: &quot;title&quot;,
        addTitle: &quot;test&quot;
    },
    test: {
        a: {
            b: {
                c: &quot;Hahaha&quot;
            }
        },
        aaa: {
            value: &quot;input&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;批量翻译&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;检索目标文件夹内所有langs文件夹下的zh-CN 文件夹下的所有文件, 输出至其同级的en-US下, 文件名同名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;zz translate -d ./demo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如: demo文件夹是以下结构, zh-CN中所有JS会翻译后输出至en-US&lt;/p&gt;
&lt;p&gt;每个文件输出内容同翻译单个文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;.
├── en-US
│   ├── test.js
│   ├── test2.js
│   └── test3.js
├── test-en.js
├── test.js
└── zh-CN
    ├── test.js
    ├── test2.js
    └── test3.js

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;翻译时可能存在翻译失败的情况，重新运行  translate 命令即可&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;压缩图片&lt;/h2&gt;
&lt;p&gt;使用help命令查看所有支持的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zz tiny --help

  -t, --type &amp;#x3C;fileType&gt;         转换后的图片类型 (default: null)
  -f, --file &amp;#x3C;file&gt;             要压缩的图片文件 (default: null)
  -d, --dir &amp;#x3C;dir&gt;               压缩文件夹内所有文件 (default: null)
  -co, --condition &amp;#x3C;condition&gt;  压缩文件夹内所有名称包含[--condition]的图片文件 (default: null)
  -q, --quality &amp;#x3C;quality&gt;       压缩质量(1-100) (default: 75)
  -c, --colours &amp;#x3C;colours&gt;       GIF色彩保留(2-256) (default: 128)
  -n, --name &amp;#x3C;name&gt;             指定文件名输出 (default: &quot;&quot;)
  -m, --max &amp;#x3C;max&gt;               限制要上传的文件大小(kb)(仅当开启 --picgo 时会用到) (default: 60)
  --picgo [type]                调用picgo (无参数) (default: null)
  --no-picgo [type]             不调用picgo (无参数) (default: null)
  -h, --help                    display help for command
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;操作流程演示&lt;/h2&gt;
&lt;p&gt;按照规则写好 Vue 文件后，&lt;strong&gt;&lt;code&gt;cd&lt;/code&gt; 到对应的文件夹&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;i18n&lt;/code&gt; 命令，生成中文国际化文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;zz i18n -d ./demo

✔ 开始检索/Users/xxxx/demo
✔ 共找到1个要处理的文件
✔ 从 demo 中提取了 6 个国际化键值
✔ 生成文件: /Users/xxxx/demo/monthlyForecast.js
✔ 国际化文件生成完成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成后检查有没有问题，自行完善生成后的文件&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;确认中文国际化文件完整后&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把文件放到 &lt;code&gt;zh-CN&lt;/code&gt; 目录下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;translate&lt;/code&gt;  命令翻译&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;zz trans -d ./demo
✔ 开始检索/Users/xxxx/demo
✔ 共找到3个要翻译的文件
✔ /Users/xxxx/demo/en-US/test.js已翻译
✔ /Users/xxxx/demo/en-US/test2.js已翻译
✔ /Users/xxxx/demo/en-US/test3.js已翻译
✔ 翻译完毕
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;到 &lt;code&gt;en-US&lt;/code&gt; 目录下检查翻译情况&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt 中设置代理的正确姿势]]></title><description><![CDATA[通常在一个前端项目中，后端接口可能不是和前端 Nuxt 集成在一起的（Nitro)]]></description><link>https://zzao.club/post/nuxt/the-best-way-to-set-proxy-in-nuxt</link><guid isPermaLink="true">https://zzao.club/post/nuxt/the-best-way-to-set-proxy-in-nuxt</guid><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;通常在一个前端项目中，后端接口可能不是和前端 &lt;code&gt;Nuxt&lt;/code&gt; 集成在一起的（&lt;code&gt;Nitro&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;而前端&lt;strong&gt;从浏览器发出&lt;/strong&gt;请求时又会遇到跨域问题，为了解决浏览器跨域问题&lt;/p&gt;
&lt;p&gt;除了能直接设置代理服务器的&lt;code&gt;CORS&lt;/code&gt;，就需要用代理服务器转发的方式解决&lt;/p&gt;
&lt;p&gt;代理的原理很简单：&lt;strong&gt;把当前域下的前端往目标服务发送请求，改为向当前域下的代理服务器发送请求&lt;/strong&gt;，代理服务器直接转发前端请求到目标服务并把 &lt;code&gt;response&lt;/code&gt; 返回给前端&lt;/p&gt;
&lt;p&gt;这样，同一个域下不会产生跨域，而服务端之间的请求又不存在跨域问题，就解决了问题。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Nuxt&lt;/code&gt; 中有四种方式可以设置代理，而最后一种则是&lt;strong&gt;终极解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一、Vite sever proxy&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 可以设置 &lt;code&gt;vite&lt;/code&gt; 配置，就和 Vue3项目一样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;vite : {
	server: {
      proxy: {
        &apos;/api/v1&apos;: {
          target: &apos;http://localhost:5770&apos;,
          changeOrigin: true,
          rewrite: (path) =&gt; path.replace(/^\/api\/v1/, &apos;&apos;)
        }
      }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时项目内请求 &lt;code&gt;/api/v1/user/list&lt;/code&gt; 时，会被转发到 &lt;code&gt;http://localhost:5770/user/list&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/api/v1&lt;/code&gt; 在此处被重写了，所以实际请求到 5770 时是不携带这个前缀的&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;vite server&lt;/code&gt; 代理时，和 &lt;code&gt;Vue3&lt;/code&gt; 项目一样，&lt;strong&gt;仅对本地开发时有效&lt;/strong&gt;，适合项目&lt;strong&gt;部署&lt;/strong&gt;到时和&lt;strong&gt;实际后端服务的服务器、域名、端口一致&lt;/strong&gt;的情况下使用&lt;/p&gt;
&lt;h2&gt;二、Nitro devProxy&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 可以设置 &lt;code&gt;nitro.devProxy&lt;/code&gt; 进行代理&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt; nitro: {
	devProxy: {
      &apos;/api/v1&apos;: {
        target: &apos;http://localhost:5770&apos;,
        changeOrigin: true
      },
      &apos;/api/v2&apos;: &apos;http://localhost:5771&apos;
    }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以借助 &lt;code&gt;Nitro&lt;/code&gt; &lt;strong&gt;在开发时&lt;/strong&gt;进行代理 ，原理和 &lt;code&gt;Vite server&lt;/code&gt; 没有区别&lt;/p&gt;
&lt;p&gt;这个选项使用 &lt;a href=&quot;https://github.com/unjs/httpxy&quot;&gt;unjs/httpxy&lt;/a&gt; 实现，可以在&lt;a href=&quot;https://nitro.build/config#devproxy&quot;&gt;官方文档&lt;/a&gt;查看&lt;/p&gt;
&lt;p&gt;以上两种方式，在使用 &lt;code&gt;useFetch&lt;/code&gt; 可以通过开启 &lt;code&gt;server&lt;/code&gt; 选项来测试是否在 &lt;code&gt;SSR&lt;/code&gt; 下生效。&lt;/p&gt;
&lt;h2&gt;三、Nitro routeRules proxy&lt;/h2&gt;
&lt;p&gt;比起 &lt;code&gt;devProxy&lt;/code&gt; ，&lt;code&gt;routeRules&lt;/code&gt; 配置是更好的选择&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;nitro: {
	routeRules: {
      &apos;/api/v1/**&apos;: {
        proxy: &apos;http://localhost:5770/api/v1/**&apos;
      }
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既可以在客户端请求时生效， 也可以在 SSR 期间生效&lt;/p&gt;
&lt;p&gt;唯一的问题是在&lt;strong&gt;运行时&lt;/strong&gt;设置环境变量不太方便&lt;/p&gt;
&lt;p&gt;因为就算把 proxy 换成环境变量里的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;proxy: process.env.YOUR_SEVER_URL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也只会在&lt;strong&gt;构建时&lt;/strong&gt;生效，这个值会被硬编码进应用中。&lt;/p&gt;
&lt;p&gt;和直接写在proxy里唯一的区别是：你可以在不同的服务器中构建，再针对不同的构建环境设置不同的环境变量&lt;/p&gt;
&lt;p&gt;所以，&lt;code&gt;Nuxt&lt;/code&gt; 中如何在&lt;strong&gt;运行时&lt;/strong&gt;读取环境变量？&lt;code&gt;runtimeConfig&lt;/code&gt; !&lt;/p&gt;
&lt;p&gt;代理只是一个 &lt;code&gt;node&lt;/code&gt; 服务，我们当然也可以使用  &lt;code&gt;Nitro&lt;/code&gt; 自己实现代理行为&lt;/p&gt;
&lt;h2&gt;四、Nitro proxtRequest()&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;Nuxt&lt;/code&gt; 中，&lt;code&gt;runtimeConfig&lt;/code&gt; 是个比较特殊的配置&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Nuxt&lt;/code&gt; 不在生产环境（运行时）中读取  &lt;code&gt;env&lt;/code&gt; 文件，只在开发、构建、生成（generate）时读取（因为不同部署环境的兼容性，比如无服务器平台、cloudflare workers 等）&lt;/p&gt;
&lt;p&gt;所以才有了 &lt;code&gt;runtimeConfig&lt;/code&gt; 这个配置，用于在&lt;strong&gt;运行时读取特定的环境变量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不同的平台有不同的环境变量设置方式， 在 &lt;code&gt;Nuxt&lt;/code&gt; 中通过读取 &lt;code&gt;NUXT_&lt;/code&gt; 前缀的环境变量来和 &lt;code&gt;runtimeConfig&lt;/code&gt; 里的 &lt;code&gt;key&lt;/code&gt; 对应起来&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;runtimeConfig: {
    proxyUrl: &apos;http://localhost:5770&apos;,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在运行时，使用 &lt;code&gt;useRuntimeConfig&lt;/code&gt; 读取 &lt;code&gt;proxyUrl&lt;/code&gt; 时，会优先去读 &lt;code&gt;NUXT_PROXY_URL&lt;/code&gt; 这个环境变量，如果没取到就会使用当前字符串&lt;/p&gt;
&lt;p&gt;所以如果要切换服务，只需要在对应的部署环境中修改 &lt;code&gt;NUXT_PROXY_URL&lt;/code&gt; 即可，无需再重新构建&lt;/p&gt;
&lt;p&gt;理解了这一点，接下来就是借助 &lt;code&gt;Nitro&lt;/code&gt; 来实现代理行为。&lt;/p&gt;
&lt;p&gt;首先需要在 &lt;code&gt;server&lt;/code&gt; 目录下的新建 &lt;code&gt;api/[...].ts&lt;/code&gt;，然后实现以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { joinURL } from &apos;ufo&apos;
export default defineEventHandler(async (event) =&gt; {
  const proxyUrl = useRuntimeConfig().proxyUrl
  
  const targetApiPrefix = event.path.replace(&apos;xxx&apos;, &apos;&apos;) 
  const targetUrl = joinURL(proxyUrl, targetApiPrefix)

  return proxyRequest(event, targetUrl)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路和配置代理的思路是一样的，因为核心部分 &lt;code&gt;proxyRequest&lt;/code&gt; 已经实现了&lt;/p&gt;
&lt;p&gt;我们要做的就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拿到实际的服务地址。通过 &lt;code&gt;runtimeConfig&lt;/code&gt; 可以拿到任意动态的地址&lt;/li&gt;
&lt;li&gt;根据前缀和实际服务地址对应起来。类似配置时的 &lt;code&gt;/api/v1: &apos;http://localhost:5770&apos;&lt;/code&gt; 和 &lt;code&gt;pathRewrite&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;proxyRequest&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中第二条对应的就是 &lt;code&gt;targetApiPrefix&lt;/code&gt; 的逻辑，假设 &lt;code&gt;Nuxt&lt;/code&gt; 应用发出的请求是&lt;code&gt;/api/imgx/xxxx&lt;/code&gt; ，对应 &lt;code&gt;imgx&lt;/code&gt; 服务中的 &lt;code&gt;/api/v1/xxxx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;则这样实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const targetApiPrefix = event.path.replace(/^\/api\/imgx\//, &apos;/api/v1&apos;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时在 Nuxt 中发出请求是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;$fetch(&apos;/api/imgx/img/001/001/哈哈哈&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nitro 转发时就会变成 &lt;code&gt;http://localhost:5770/api/v1/img/001/001/哈哈哈&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;由于这个接口我们拥有完全的可控性，所以能做的不只是&lt;strong&gt;代理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;隐藏了实际请求地址&lt;/strong&gt;的同时&lt;/p&gt;
&lt;p&gt;还能通过proxyRequest的第三个参数传入 &lt;code&gt;{ headers: { Authorization: &apos;your token&apos;} }&lt;/code&gt;向目标服务添加身份验证信息&lt;/p&gt;
&lt;p&gt;如果拥有&lt;strong&gt;多个后端服务&lt;/strong&gt;，还可以自己选择转发到何处，达到类似负载均衡的作用&lt;/p&gt;
&lt;p&gt;如果想对请求到的数据做进一步处理，又&lt;strong&gt;何必纠结于代理&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;只需要自己使用 &lt;code&gt;$fetch&lt;/code&gt; 请求明确的地址 &lt;code&gt;targetUrl&lt;/code&gt;，然后对拿到的数据进行处理，再返回给前端即可！&lt;/p&gt;
&lt;p&gt;_注意：借助 &lt;code&gt;Nitro&lt;/code&gt; 实现代理，前提就是你得有 &lt;code&gt;Nitro&lt;/code&gt; ，如果 &lt;code&gt;Nuxt&lt;/code&gt; 完全&lt;strong&gt;静态化&lt;/strong&gt;了，就无法动态读取环境变量了 _&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是 &lt;code&gt;Nuxt&lt;/code&gt; 中设置代理的四种方式&lt;/p&gt;
&lt;p&gt;其中第一种和 &lt;code&gt;Vue3&lt;/code&gt; 项目没有区别，是基于本地服务实现&lt;/p&gt;
&lt;p&gt;如果你&lt;strong&gt;只需要在本地开发时使用代理&lt;/strong&gt;来解决跨域问题，&lt;strong&gt;在生产环境中没有这个问题&lt;/strong&gt;，那你可以选择&lt;strong&gt;前两种&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果你的&lt;strong&gt;代理地址比较固定&lt;/strong&gt;，也没有很多不同环境的后端服务可用，那你可以选择第三种 &lt;code&gt;Nitro routeRules proxy&lt;/code&gt; , 只需要配置，即可实现这个简单的需求&lt;/p&gt;
&lt;p&gt;如果你的环境比较多，或是需要调用的服务也比较多，或是有很多特殊需求需要实现，那我推荐你使用&lt;strong&gt;第四种&lt;/strong&gt;终极方案&lt;/p&gt;
&lt;p&gt;当然，这只是基于 &lt;code&gt;Nuxt&lt;/code&gt; 中的实现，利用 &lt;code&gt;Nginx&lt;/code&gt; 也能轻松实现代理，可以根据自己的实际情况来抉择&lt;/p&gt;
&lt;p&gt;希望对你有所帮助~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[✨使用umami低成本监控网站流量]]></title><link>https://zzao.club/post/knows/umami-website-watch</link><guid isPermaLink="true">https://zzao.club/post/knows/umami-website-watch</guid><pubDate>Wed, 19 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;👀什么是Umami&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Umami&lt;/code&gt; 是一款开源的、注重隐私的网络分析工具（基于NextJS），可作为 &lt;code&gt;Google Analytics&lt;/code&gt; 的替代品。它提供有关网站流量、用户行为和性能的重要见解，同时优先考虑数据隐私。&lt;/p&gt;
&lt;p&gt;与许多传统分析平台不同，Umami 不会收集或存储个人数据，从而避免了使用 cookie 的需求，并且符合 GDPR 和 PECR 标准。&lt;/p&gt;
&lt;p&gt;Umami 设计轻巧、易于设置，可自托管，让用户完全控制自己的数据。&lt;/p&gt;
&lt;p&gt;这是我部署后的界面：&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202503191130622.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时包含了一些访客的浏览器、设备、操作系统等信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130624.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;以及 &lt;code&gt;ip&lt;/code&gt; 所在国家&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130625.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;行为类别&lt;/strong&gt;中，还可以看到自己在网站中的埋点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130626.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;总结：  MIT开源、免费、独立部署的网站数据分析工具&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;如果使用Docker那就非常简单了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 直接在项目根目录运行
docker-compose up -d

# 或者使用下面镜像 二选一
docker pull docker.umami.is/umami-software/umami:mysql-latest
docker pull docker.umami.is/umami-software/umami:postgresql-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我已经存在一个正在运行的 &lt;code&gt;Mysql&lt;/code&gt; Docker服务了，所以下面以从源码安装，并以 &lt;code&gt;PM2&lt;/code&gt; 运行 &lt;code&gt;Umami&lt;/code&gt; 为例&lt;/p&gt;
&lt;h3&gt;环境要求&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Node &gt;= 18.18&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Mysql &gt;= 8.0&lt;/code&gt; 或 &lt;code&gt;PostgreSQL &gt;= 12.14&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;安装yarn&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install -g yarn
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取源代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone https://github.com/umami-software/umami.git
cd umami
yarn install
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;.env 文件&lt;/h3&gt;
&lt;p&gt;创建一个 &lt;code&gt;.env&lt;/code&gt; 文件，用于数据库连接&lt;/p&gt;
&lt;p&gt;如果你还没有Mysql服务，可以参考我这篇：&lt;a href=&quot;https://blog.zzao.club/post/nuxt/local-init-mysql-by-docker&quot;&gt;【使用Docker启动Mysql】&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;DATABASE_URL=mysql://username:mypassword@localhost:3306/mydb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;username&lt;/code&gt;、&lt;code&gt;password&lt;/code&gt;、&lt;code&gt;mydb&lt;/code&gt; 都替换成自己的，其中mydb的创建可以参考：&lt;a href=&quot;https://blog.zzao.club/post/nuxt/prod-docker-mysql-config&quot;&gt;安装后如何初始化库和新用户&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;还可以配置 &lt;code&gt;PORT=4577&lt;/code&gt;，端口号&lt;/p&gt;
&lt;h3&gt;PM2启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;yarn global add pm2
cd umami
pm2 start yarn --name umami -- start
pm2 startup
pm2 save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果配置了PORT，PM2命令要对应的修改为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pm2 start yarn --name umami -- start-env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不用 &lt;code&gt;env&lt;/code&gt; 文件，可以直接传递变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pm2 start yarn --name umami -- start --port=6001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种方式都可以。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pm2 startup&lt;/code&gt; 则是为了设置开机自启，&lt;code&gt;save&lt;/code&gt; 会把当前配置保存起来&lt;/p&gt;
&lt;h3&gt;配置 Nginx&lt;/h3&gt;
&lt;p&gt;此处仅作为作为参考，适用于配置多个子域名，共用一个泛域名证书&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http {
	# umami
    upstream upstream_umami{
        server localhost:6001;
    }
	map $host $proxy_pass {
        hostnames;  # 添加这行以优化域名匹配

		# ...省略其他子域名
        umami.xxx.com http://upstream_umami;
    }
	server {
        listen 443 ssl; 
        server_name *.xxx.com; #泛域名
        
        ssl_certificate /etc/nginx/certs/xxx
        ssl_certificate_key /etc/nginx/certs/xxx

        fastcgi_param  HTTPS        on;
        fastcgi_param  HTTP_SCHEME     https;

        location / {
            add_header X-Final-Destination $upstream_addr;
            
            proxy_set_header   X-Real-IP         $remote_addr;
            proxy_set_header   Host              $http_host;
            proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
            
            proxy_pass $proxy_pass; 
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置成功后，&lt;code&gt;nginx -t&lt;/code&gt; 检测是否有错误，如果没错就 &lt;code&gt;restart nginx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后再访问配好的子域名 &lt;code&gt;umami.xxx.com&lt;/code&gt; 是否有效就可以了&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;umami&lt;/code&gt; 的使用也非常简单，首先打开 &lt;code&gt;umami.xxx.com&lt;/code&gt; ，也就是自己配好的地址&lt;/p&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;p&gt;登录 =&gt; 设置 =&gt; 修改密码 + 修改语言&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认用户名: admin&lt;/li&gt;
&lt;li&gt;默认密码：umami&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130627.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
找到用户这里&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130628.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改语言直接点击右上角的国际化图标即可&lt;/p&gt;
&lt;p&gt;设置好后，在&lt;strong&gt;设置 -&gt; 网站 -&gt; 添加网站&lt;/strong&gt;中，输入自己的网站名字和域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130629.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;添加好点击&lt;strong&gt;编辑&lt;/strong&gt;按钮&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130630.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后就可以看到自己的网站lD、跟踪代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503191130631.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来就是要把跟踪代码添加到当前网站项目中&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;Nuxt&lt;/code&gt; 为例&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 中，添加如下配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
	app: {
		head: {
			script: [
				{
					src: &apos;https://umami.xxx.com/script.js&apos;,
			        defer: true,
		            &quot;data-website-id&quot;: &quot;your_data_website_id&quot;
				}
			]
		}
	}
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 &lt;code&gt;src&lt;/code&gt; 和 &lt;code&gt;data-website-id&lt;/code&gt; 替换成自己的即可&lt;/p&gt;
&lt;p&gt;重新打包、发布Nuxt应用后，umami的统计就会立即生效了，可以前往 &lt;code&gt;umami.xxx.com&lt;/code&gt; 去看访问数据&lt;/p&gt;
&lt;h3&gt;埋点&lt;/h3&gt;
&lt;p&gt;要想记录某个按钮的点击行为，以及传递一些用户信息来记录，需要用到 &lt;code&gt;Umami&lt;/code&gt; 的 &lt;code&gt;Track Events&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;button id=&quot;signup-button&quot; data-umami-event=&quot;Signup button&quot; data-umami-event-id=&quot;123&quot;&gt;Sign up&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;data-umami-event&lt;/code&gt; ：表示记录的事件名&lt;br&gt;
&lt;code&gt;data-umami-event-*&lt;/code&gt; ：表示给事件传递的属性 &lt;code&gt;{ id： &quot;123&quot;}&lt;/code&gt; 会这样被记录下来&lt;/p&gt;
&lt;p&gt;这种使用 &lt;code&gt;html&lt;/code&gt; 属性的方式，所有数据会保存为字符串，记录一些基本动作也是够用的&lt;/p&gt;
&lt;p&gt;也可以使用 JS 的方式来主动调用&lt;/p&gt;
&lt;p&gt;引入 &lt;code&gt;umami/script.js&lt;/code&gt; 时，会在全局注入一个 &lt;code&gt;umami&lt;/code&gt; 对象，所以直接使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;umami.track(event_name: string, event_data: object);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不传 &lt;code&gt;event_name&lt;/code&gt;，记录的就是页面浏览事件，比如这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;umami.track({ website: &apos;e676c9b4-11e4-4ef1-a4d7-87001773e9f2&apos;, url: &apos;/home&apos;, title: &apos;Home page&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记录时只会记录 &lt;code&gt;url&lt;/code&gt; 和 &lt;code&gt;title&lt;/code&gt; ，如果要包含 &lt;code&gt;umami&lt;/code&gt; 默认的属性&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;umami.track(props =&gt; ({ ...props, url: &apos;/home&apos;, title: &apos;Home page&apos; }));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以刚才按钮的事件可以这样记录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;umami.track(&apos;signup-button&apos;, { name: &apos;newsletter&apos;, id: 123 });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是 &lt;code&gt;Umami&lt;/code&gt; 的安装和在项目中的使用。&lt;/p&gt;
&lt;p&gt;内存占用方面，使用 &lt;code&gt;node&lt;/code&gt; 作为运行时，&lt;code&gt;pm2&lt;/code&gt; 显示umami占用内存在 &lt;code&gt;80 ~ 200&lt;/code&gt; mb之间，不知道流量比较大的站点内存消耗如何。&lt;/p&gt;
&lt;p&gt;但作为安装、运行、使用上来说，&lt;code&gt;umami&lt;/code&gt; 集成非常简单易用，推荐大家使用&lt;/p&gt;
&lt;p&gt;欢迎在评论区交流&lt;/p&gt;</content:encoded></item><item><title><![CDATA[生成卡片的超长提示词]]></title><link>https://zzao.club/post/imgx/imgx-prompt</link><guid isPermaLink="true">https://zzao.club/post/imgx/imgx-prompt</guid><pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;提示词&lt;/h2&gt;
&lt;p&gt;打磨了一下生成模板的提示词，用来批量生产模板&lt;/p&gt;
&lt;h3&gt;基于图片生成模板&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;我会给你一个图片，分析内容，并将其转化为美观漂亮的比例相同的 HTML 元素：

## 内容排版规范
	- 整体风格按照图片中的排版
	- 如何图片中有明显的样式不美观，则去除
	- 使用清晰的视觉层次结构，突出重要内容
	- 配色方案应专业、和谐，适合在社交媒体传播
## 技术规范
	- 使用 Vue3（通过CDN引入）、Tailwindcss3(通过CDN引入)
	- 使用 Vue3 渲染内容时，template 标签中只能使用两种方式定义样式
		1. class。如 class=&quot;w-full h-full&quot;
		2. :style=&quot;{ color: myColor }&quot;。这种方式用于接受 props 的参数，要满足 vue 语法
		3. 两者为互补关系，不能修改的样式全部用 class实现，动态传入的用 style 实现
		4. 至少在其中一个中声明 flex 布局
	- 使用 Vue3 渲染内容时，script 中只允许使用 props，props 中只能接受和内容、样式有关的数据。不允许存在任何交互逻辑
	- template 和 props 的内容要独立且完整，要用单独的变量存储。
	- template 中的内容要用 props 传入值或默认值代替，不能把 template 的内容写死。
	- CSS 请严格按照【CSS要求】，务必使用 tailwindcss3 的语法，同时要使用`:style=&quot;{}&quot;`语法来保证能接受 props 来改变颜色、字号、间距等属性
	- CSS或 props 中不允许出现 undefined，必须有明确的值，如 width: 100%
	- html元素只能使用 div 元素
	- 每个 div 必须显式使用 flex 布局，必须具备 flex class
	- 不能使用 z-index 属性
	- 不能使用 satori 不支持的属性
	- 如果发现现有条件不能生成一模一样的样式，则放弃和原图保持一致，换一种简洁的样式来代替
	- 最外层 div 以及第二层 div 必须使用 `w-full h-full` 来占满父元素
	- 如果图片底部存在渐变色或底色，内容存在底色，要特别注意样式的准确性，必须表现在 Vue 组件内
	- 只允许最终生成单个HTML文件
	- 在 Vue 组件的外层增加一个固定宽高的 div 容器，用来模拟其固定尺寸的父元素
	- 在 Vue 组件的上方增加两个按钮
		- 1.复制 Template，点击时把 Vue 组件中的 Template 变量的内容当做字符串复制到粘贴板，要求和 template 共用一个变量
		- 2.复制 Props，点击时根据 Vue 组件中的 Props 对象的格式，生成key-value格式的对象字符串复制到粘贴板。
	- 代码中不要出现任何注释
## 媒体资源
	- 忽略所有媒体资源
## 图标与视觉元素
	- 忽略所有 emoji
	- 忽略所有 图标
	- 忽略所有背景图、复杂阴影效果
	- 不需要存在交互效果，只要视觉效果
## 性能优化
	- 确保页面加载速度快，避免不必要的大型资源
## 输出要求
	- 提供完整可运行的单一HTML文件，包含所有必要的CSS和JavaScript
	- 代码中不要出现任何注释
	- 确保代码符合W3C标准，无错误警告
	- 页面在不同浏览器中保持一致的外观和功能

请根据上传图片，基于 Vue3 创建出合适的卡片 HTML 网页内容。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;让AI自主设计排版（待完善）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;请基于 Vue3 在 HTML 中生成一个美观漂亮的卡片组件，同时满足以下要求：

## 排版规范
	- 整体长宽比为：3:4，用途为小红书、推特、朋友圈发帖
	- 配色方案应具有科技感，以合理的渐变作为背景
	- 文字必须成为视觉主体，占据页面至少70%的空间
	- 运用3-4种不同字号创造层次感，关键词使用最大字号
	- 主标题字号需要比副标题和介绍大三倍以上
	- 主标题提取2-3个关键词，使用特殊处理（如描边、高亮、不同颜色）
## 技术规范
	- 使用 Vue3（通过CDN引入）、Tailwindcss3(通过CDN引入)
	- 使用 Vue3 渲染内容时，template 标签中只能使用两种方式定义样式
		1. class。如 class=&quot;w-full h-full&quot;
		2. :style=&quot;{ color: myColor }&quot;。这种方式用于接受 props 的参数，要满足 vue 语法
		3. 两者为互补关系，不能修改的样式全部用 class实现，动态传入的用 style 实现
		4. 至少在其中一个中声明 flex 布局
	- 使用 Vue3 渲染内容时，script 中只允许使用 props，props 中只能接受和内容、样式有关的数据。不允许存在任何交互逻辑
	- template 和 props 的内容要独立且完整，要用单独的变量存储。
	- template 中的内容要用 props 传入值或默认值代替，不能把 template 的内容写死。
	- CSS或 props 中不允许出现 undefined，必须有明确的值，如 width: 100%
	- html元素只能使用 div 元素
	- 每个 div 必须显式使用 flex 布局，必须具备 flex class
	- 不能使用 z-index 属性
	- 不能使用 satori 不支持的属性
	- 最外层 div 以及第二层 div 必须使用 `w-full h-full` 来占满父元素
	- 如果图片底部存在渐变色或底色，内容存在底色，要特别注意样式的准确性，必须表现在 Vue 组件内
	- 只允许最终生成单个HTML文件
	- 在 Vue 组件的外层增加一个固定宽高的 div 容器，用来模拟其固定尺寸的父元素
	- 在 Vue 组件的上方增加两个按钮
		- 1.复制 Template，点击时把 Vue 组件中的 Template 变量的内容当做字符串复制到粘贴板，要求和 template 共用一个变量
		- 2.复制 Props，点击时根据 Vue 组件中的 Props 对象的格式，生成key-value格式的对象字符串复制到粘贴板。
	- 代码中不要出现任何注释
## 输出要求
	- 提供完整可运行的单一HTML文件，包含所有必要的CSS和JavaScript
	- 代码中不要出现任何注释
	- 确保代码符合W3C标准，无错误警告
	- 页面在不同浏览器中保持一致的外观和功能
## 用户输入内容

请根据上述要求，生成一个在单一 HTML 中可使用的Vue3卡片组件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改生成卡片的样式时，只需要修改排版规范部分即可&lt;/p&gt;
&lt;p&gt;测试：社交卡片&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 排版规范
- 仿照X/twitter的帖子格式来设计卡片
- 排版规范、美观、整齐
- 文字必须成为视觉主体，占据页面至少70%的空间
- 运用3-4种不同字号创造层次感，关键词使用最大字号
- 主标题字号需要比副标题和介绍大三倍以上
- 主标题提取2-3个关键词，使用特殊处理（如描边、高亮、不同颜色）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 排版规范
- 仿照X/twitter的帖子格式来设计卡片
- 排版规范、美观、整齐
- 文字必须成为视觉主体，占据页面至少70%的空间
- 运用3-4种不同字号创造层次感，关键词使用最大字号
- 主标题字号需要比副标题和介绍大三倍以上
- 主标题提取2-3个关键词，使用特殊处理（如描边、高亮、不同颜色）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;satori 支持的 css&lt;/h2&gt;
&lt;p&gt;以下是 Satori 支持的 CSS 属性：&lt;/p&gt;
&lt;h3&gt;布局相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;display：支持 none 和 flex，默认为 flex&lt;/li&gt;
&lt;li&gt;position：支持 relative 和 absolute，默认为 relative&lt;/li&gt;
&lt;li&gt;top、right、bottom、left：支持&lt;/li&gt;
&lt;li&gt;width、height：支持&lt;/li&gt;
&lt;li&gt;minWidth、minHeight、maxWidth、maxHeight：支持，但不支持 min-content、max-content 和 fit-content&lt;/li&gt;
&lt;li&gt;margin 及其相关属性（marginTop、marginRight 等）：支持&lt;/li&gt;
&lt;li&gt;gap：支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;灵活盒模型（Flex）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;flexDirection：支持 column、row、row-reverse、column-reverse，默认为 row&lt;/li&gt;
&lt;li&gt;flexWrap：支持 wrap、nowrap、wrap-reverse，默认为 wrap&lt;/li&gt;
&lt;li&gt;flexGrow、flexShrink、flexBasis：支持，其中 flexBasis 不支持 auto&lt;/li&gt;
&lt;li&gt;alignItems、alignContent、alignSelf、justifyContent：支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;字体与文本&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;fontFamily、fontSize、fontWeight、fontStyle：支持&lt;/li&gt;
&lt;li&gt;tabSize、textAlign、textTransform、textOverflow、textDecoration、textShadow、lineHeight、letterSpacing、whiteSpace、wordBreak、textWrap：支持&lt;/li&gt;
&lt;li&gt;WebkitTextStrokeWidth、WebkitTextStrokeColor：支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;背景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;backgroundColor：支持单个值&lt;/li&gt;
&lt;li&gt;backgroundImage：支持 linear-gradient、radial-gradient、url，单个值&lt;/li&gt;
&lt;li&gt;backgroundPosition：支持单个值&lt;/li&gt;
&lt;li&gt;backgroundSize：支持两个值的大小，如 10px 20%&lt;/li&gt;
&lt;li&gt;backgroundClip：支持 border-box、text&lt;/li&gt;
&lt;li&gt;backgroundRepeat：支持 repeat、repeat-x、repeat-y、no-repeat，默认为 repeat&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;边框与圆角&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;border 相关属性（borderWidth、borderStyle、borderColor 等）：支持 solid 和 dashed，默认为 solid&lt;/li&gt;
&lt;li&gt;borderRadius 及其相关属性（borderTopLeftRadius 等）：支持，包括简写形式如 5px、50% / 5px&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;转换与效果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;transform 相关属性（translate、translateX、translateY、rotate、scale、scaleX、scaleY、skew、skewX、skewY）：支持&lt;/li&gt;
&lt;li&gt;transformOrigin：支持一值和两值语法（相对和绝对值）&lt;/li&gt;
&lt;li&gt;objectFit：支持 contain、cover、none，默认为 none&lt;/li&gt;
&lt;li&gt;opacity：支持&lt;/li&gt;
&lt;li&gt;boxShadow：支持&lt;/li&gt;
&lt;li&gt;overflow：支持 visible 和 hidden，默认为 visible&lt;/li&gt;
&lt;li&gt;filter：支持&lt;/li&gt;
&lt;li&gt;clipPath：支持&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[IMGX 更新动态贴]]></title><link>https://zzao.club/post/imgx/imgx-changelog</link><guid isPermaLink="true">https://zzao.club/post/imgx/imgx-changelog</guid><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202503121139543.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;回顾&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IMGX&lt;/code&gt;从&lt;code&gt;2025-02-08&lt;/code&gt; 开始建仓库，到现在已经有一个月多一点&lt;/p&gt;
&lt;p&gt;一开始只是为了解决写文章时配图问题，我的要求不高，但AI 生成的很难贴合实际内容。所以还不如直接用一个只有文字的图片，只需要简单排版就好了。&lt;/p&gt;
&lt;p&gt;在解决了自己的问题后，我在 v2ex 上分享了 Github 仓库，以及现有的功能，很快收获了近 &lt;code&gt;100 star&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这也算是一次成功的推广吧，在只做了不到 1/4 的功能，就吸引了一批人的注意，说明这个需求还是有很多人需要的。&lt;/p&gt;
&lt;p&gt;所以也是立马花了小一个月的业余时间，用来投入到这个项目上。&lt;/p&gt;
&lt;p&gt;但是这一个月功能没推进多少，因为计划赶不上变化，加班一下子加了两周...&lt;/p&gt;
&lt;p&gt;最近又回归常态后，又可以开始新的规划啦&lt;/p&gt;
&lt;h2&gt;2025-03-11 新的仓库&lt;/h2&gt;
&lt;p&gt;在升级到 0.6.0 之后，我的计划是&lt;strong&gt;开发一套模板录入+预设保存的机制&lt;/strong&gt;，用于解决一大堆参数的问题，保存预设时会得到一个 4(或3)位数的预设码，这个预设码包含了使用哪个模板、使用什么默认样式等信息。&lt;/p&gt;
&lt;p&gt;在使用时只需要携带这个预设码，再传入自己的文字内容即可获得一张图片&lt;/p&gt;
&lt;p&gt;于是我引入了 Prisma、Mysql、Redis，以满足未来的需求。&lt;/p&gt;
&lt;p&gt;但敲了两天代码后，发现现在的项目结构有点别扭。&lt;/p&gt;
&lt;p&gt;前端功能很少，大部分集中在后端，但又依赖了 &lt;code&gt;createSSRApp&lt;/code&gt; 和 &lt;code&gt;renderToString&lt;/code&gt;，所以我再调研后决定去除这两个依赖，直接生成 &lt;code&gt;VNode&lt;/code&gt; 来渲染出 &lt;code&gt;SVG&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;而且这还仅仅是 &lt;code&gt;IMG&lt;/code&gt; 部分的功能，&lt;code&gt;X&lt;/code&gt; 部分我还没开发，所以为了避免未来代码烂掉。&lt;/p&gt;
&lt;p&gt;我新开了一个仓库：&lt;a href=&quot;https://github.com/aatrooox/imgx-nitro&quot;&gt;imgx-nitro&lt;/a&gt;，用于继续开发新的 Api 功能。&lt;/p&gt;
&lt;p&gt;现有的仓库先保留（毕竟有 100 个 star），但暂时不会在这个基础上写新功能了，新的前端仓库也会基于 &lt;code&gt;Vue3&lt;/code&gt; 再开一个。&lt;/p&gt;
&lt;p&gt;有开发能力的小伙伴可以自行 &lt;code&gt;Fork&lt;/code&gt; 本仓库来魔改&lt;/p&gt;
&lt;h2&gt;功能规划&lt;/h2&gt;
&lt;p&gt;我的规划是这样的：（未来）提供一个入口，可以创建自己的模板（HTML），以及编写自己模板需要的样式（props），我会根据 &lt;code&gt;props&lt;/code&gt;，生成一个 &lt;code&gt;propsSchema&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;propsSchema&lt;/code&gt; 会在生成自己的预设时，选择模板后带出来，并且会根据&lt;code&gt;propsSchema&lt;/code&gt;来渲染出一个表单，用于配置自己的默认样式&lt;/p&gt;
&lt;p&gt;然后保存后就会得到一个 code，比如 1234，在使用时通过 &lt;code&gt;GET imgx.zzao.club/1234/这是一段内容 &lt;/code&gt; 即可快速拿到一个自己常用风格的图片。&lt;/p&gt;
&lt;p&gt;当然，这套繁琐的配置只是为了方便&lt;strong&gt;用户&lt;/strong&gt;从&lt;strong&gt;前端&lt;/strong&gt;去使用。&lt;/p&gt;
&lt;p&gt;真正有价值的还是在后端上。&lt;/p&gt;
&lt;p&gt;比如和其他 Api 配合，抓取内容后输出到图片中&lt;/p&gt;
&lt;p&gt;和插件配合，在浏览器任意网页中生成图片。&lt;/p&gt;
&lt;p&gt;或是把本地大量的文字、或 md 文件的内容都&lt;strong&gt;批量&lt;/strong&gt;转换成图片&lt;/p&gt;
&lt;p&gt;所以我目前的想法是，先完成核心的 API。&lt;/p&gt;
&lt;p&gt;其他功能到底是以方便普通用户使用，还是先支持开发者自建模板，还是先开发配套插件，就以实际推广后的反馈为准吧&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Debian12 服务器上启动 MySQL 服务]]></title><description><![CDATA[启动方式同本地]]></description><link>https://zzao.club/post/nuxt/prod-docker-mysql-config</link><guid isPermaLink="true">https://zzao.club/post/nuxt/prod-docker-mysql-config</guid><pubDate>Wed, 05 Mar 2025 10:17:23 GMT</pubDate><content:encoded>&lt;p&gt;启动方式同&lt;a href=&quot;https://blog.zzao.club/post/nuxt/local-init-mysql-by-docker&quot;&gt;本地&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker compose exec mysql mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个数据库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;CREATE DATABASE imgx CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个新用户&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 创建用户，限制只能从特定 IP 访问（更安全）
CREATE USER &apos;imgx&apos;@&apos;%&apos; IDENTIFIED BY &apos;your_strong_password!&apos;;

-- 如果需要限制特定 IP 访问（推荐）
-- CREATE USER &apos;prod_user&apos;@&apos;192.168.1.%&apos; IDENTIFIED BY &apos;Strong_Password_123!&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;授予最小必要权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 授予特定数据库的必要权限
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, REFERENCES, LOCK TABLES 
ON imgx.* TO &apos;imgx&apos;@&apos;%&apos;;


-- 如果只需要读写权限
-- GRANT SELECT, INSERT, UPDATE, DELETE ON production_db.* TO &apos;prod_user&apos;@&apos;%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刷新权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证用户权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SHOW GRANTS FOR &apos;imgx&apos;@&apos;%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置密码策略&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 设置密码过期策略（90天过期）
ALTER USER &apos;prod_user&apos;@&apos;%&apos; PASSWORD EXPIRE INTERVAL 90 DAY;

-- 设置密码重试限制和锁定时间
ALTER USER &apos;prod_user&apos;@&apos;%&apos; FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2;
&lt;/code&gt;&lt;/pre&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;会收到错误提示： ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;过期后修改密码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 方式1：用户自己修改密码
ALTER USER USER() IDENTIFIED BY &apos;new_password&apos;;

-- 方式2：管理员帮助修改
ALTER USER &apos;username&apos;@&apos;%&apos; IDENTIFIED BY &apos;new_password&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置备份策略&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 授予备份权限（如果需要）
GRANT SELECT, SHOW VIEW, PROCESS, TRIGGER ON production_db.* TO &apos;prod_user&apos;@&apos;%&apos;;
GRANT LOCK TABLES ON production_db.* TO &apos;prod_user&apos;@&apos;%&apos;;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试新用户登录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker compose exec mysql mysql -u umami -p
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[nuxt + prisma 编译时报错]]></title><description><![CDATA[浏览器控制台报错：]]></description><link>https://zzao.club/post/issues/prisma-index-browser-error</link><guid isPermaLink="true">https://zzao.club/post/issues/prisma-index-browser-error</guid><pubDate>Fri, 28 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;浏览器控制台报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Uncaught TypeError: Failed to resolve module specifier &quot;.prisma/client/index-browser&quot;. Relative references must start with either &quot;/&quot;, &quot;./&quot;, or &quot;../&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时页面某些功能异常，比如的 &lt;code&gt;primevue&lt;/code&gt; 的 &lt;code&gt;Button&lt;/code&gt; 组件 &lt;code&gt;as=&quot;a&quot;&lt;/code&gt; 属性失效，某些点击事件也失效。&lt;/p&gt;
&lt;p&gt;不过这是我的问题，因为我设置了 &lt;code&gt;nitro.prerender.failOnError: false&lt;/code&gt;  导致我忽略了很多错误信息。直到加入了 &lt;code&gt;prisma&lt;/code&gt; 之后才暴漏出来&lt;/p&gt;
&lt;p&gt;关于这个问题，&lt;a href=&quot;https://www.prisma.io/docs/orm/more/help-and-troubleshooting/prisma-nuxt-module#resolving-typeerror-failed-to-resolve-module-specifier-prismaclientindex-browser&quot;&gt;Prisma 官方文档&lt;/a&gt;上有提到，但给出的不是最终解决方案&lt;/p&gt;
&lt;p&gt;对于 pnpm 来说可以参考这个&lt;a href=&quot;https://github.com/prisma/prisma/issues/12504#issuecomment-1827097530&quot;&gt;回答&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nuxt.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createRequire } from &apos;module&apos;
import { defineConfig } from &apos;vite&apos;
import path from &apos;node:path&apos;
import { fileURLToPath } from &apos;node:url&apos;
// @ts-ignore
const __dirname = path.dirname(fileURLToPath(import.meta.url))

const { resolve } = createRequire(import.meta.url)

const prismaClient = `prisma${path.sep}client`

const prismaClientIndexBrowser = resolve(&apos;@prisma/client/index-browser&apos;).replace(`@${prismaClient}`, `.${prismaClient}`)

export default defineNuxtConfig({

vite: {
	resolve: {
      alias: {
        &quot;.prisma/client/index-browser&quot;: path.relative(__dirname, prismaClientIndexBrowser)
      }
    }
}

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 &lt;code&gt;npm&lt;/code&gt; 来说，可以参考这个&lt;a href=&quot;https://github.com/prisma/prisma/issues/12504#issuecomment-1285883083&quot;&gt;回答&lt;/a&gt;，（我没试）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({

vite: {
	resolve: {
      alias: {
        &quot;.prisma/client/index-browser&quot;: &quot;./node_modules/.prisma/client/index-browser.js&quot;
      }
    }
}

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这个问题，是 2022 年就存在的，今年已经是 2025 年😀&lt;/p&gt;</content:encoded></item><item><title><![CDATA[为 Nuxt 应用 MySQL 和 Redis 服务]]></title><description><![CDATA[Nuxt中如何使用Mysql和Redis服务，Docker配置详解]]></description><link>https://zzao.club/post/nuxt/local-init-mysql-by-docker</link><guid isPermaLink="true">https://zzao.club/post/nuxt/local-init-mysql-by-docker</guid><pubDate>Thu, 27 Feb 2025 10:41:22 GMT</pubDate><content:encoded>&lt;p&gt;最近开始陆续架设数据库服务，打算起一个mysql服务，不同应用之间分库。自己管自己的。&lt;/p&gt;
&lt;p&gt;同时有一个基础服务，用于给一些单纯提供api，不提供前端界面的Nitro服务提供鉴权&lt;/p&gt;
&lt;p&gt;这里记录一下（本地Macos下）启动MySQL服务的命令，当然Debian上也是一样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkdir -p mysql/{data,conf,logs,backup}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;touch mysql/conf/my.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;[mysqld]
# 字符集
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
bind-address=127.0.0.1

# 默认认证插件
default_authentication_plugin=mysql_native_password

# 最大连接数
max_connections=1000

# 缓冲池大小
innodb_buffer_pool_size=512M

# 日志配置
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=2

[client]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;禁止从远程连接数据库，如果从其他容器访问，需要加入同一个网络&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  mysql:
    image: mysql:8.0
    container_name: mysql_local
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: blog
    ports:
      - &quot;3306:3306&quot;
    volumes:
      - ./data:/var/lib/mysql
      - ./conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./logs:/var/log/mysql
      - ./backup:/backup
  redis:
    image: redis:7.0
    container_name: redis_local
    restart: unless-stopped
    ports:
      - &quot;6379:6379&quot;
    volumes:
      - ./redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - mysql_internal
networks:
  mysql_internal:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;chmod -R 755 mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[语义版本控制说明 unjs/changelogen]]></title><description><![CDATA[版本号规则为：MAJOR.MINOR.PATCH]]></description><link>https://zzao.club/post/knows/semantic-versioning-changelogen</link><guid isPermaLink="true">https://zzao.club/post/knows/semantic-versioning-changelogen</guid><pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;版本号规则为：&lt;code&gt;MAJOR.MINOR.PATCH&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当进行不兼容API更改和升级时，升级 &lt;code&gt;major&lt;/code&gt; 版本&lt;/li&gt;
&lt;li&gt;以向后兼容的方式添加功能时，升级 &lt;code&gt;minor&lt;/code&gt; 版本&lt;/li&gt;
&lt;li&gt;修复bug时，升级 &lt;code&gt;patch&lt;/code&gt; 版本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;与之对应的三个命令（包含 &lt;code&gt;github release pub&lt;/code&gt; ）为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;npx changelogen@latest --release --patch --push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npx changelogen@latest --release --minor --push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npx changelogen@latest --release --major --push&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是如果你的版本号从 &lt;code&gt;0.0.1&lt;/code&gt; 开始&lt;/p&gt;
&lt;p&gt;那 &lt;code&gt;patch&lt;/code&gt; 和 &lt;code&gt;minor&lt;/code&gt; 都只能升级到 &lt;code&gt;0.0.2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;--major&lt;/code&gt; 可以升级到 &lt;code&gt;0.1.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注意：再次使用 &lt;code&gt;--major&lt;/code&gt; 也只会升级到 &lt;code&gt;0.2.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因为 &lt;code&gt;0.yz&lt;/code&gt; 版本表示不稳定的版本，所以三个语义发生了改变，不适用于常规的 &lt;code&gt;1.0.0&lt;/code&gt; 这样的版本&lt;/p&gt;
&lt;p&gt;如果要发布到 &lt;code&gt;v1.0.0&lt;/code&gt; , 可以使用 &lt;code&gt;-r v1.0.0&lt;/code&gt; ，此后就可以用 &lt;code&gt;MAJOR.MINOR.PATCH&lt;/code&gt; 这个规则就行正常更新版本号了。（目前没发现有直接的命令处理v1.0.0版本的发布）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;changelogen types&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;types: {
      feat: { title: &quot;🚀 Enhancements&quot;, semver: &quot;minor&quot; },
      perf: { title: &quot;🔥 Performance&quot;, semver: &quot;patch&quot; },
      fix: { title: &quot;🩹 Fixes&quot;, semver: &quot;patch&quot; },
      refactor: { title: &quot;💅 Refactors&quot;, semver: &quot;patch&quot; },
      docs: { title: &quot;📖 Documentation&quot;, semver: &quot;patch&quot; },
      build: { title: &quot;📦 Build&quot;, semver: &quot;patch&quot; },
      types: { title: &quot;🌊 Types&quot;, semver: &quot;patch&quot; },
      chore: { title: &quot;🏡 Chore&quot; },
      examples: { title: &quot;🏀 Examples&quot; },
      test: { title: &quot;✅ Tests&quot; },
      style: { title: &quot;🎨 Styles&quot; },
      ci: { title: &quot;🤖 CI&quot; },
    }
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[Nuxt4 中使用 NuxtAuth 实现 Github 登录]]></title><description><![CDATA[NuxtAuth 使用指南，快速实现Github登录]]></description><link>https://zzao.club/post/nuxt/nuxt-auth-quick-start</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt-auth-quick-start</guid><pubDate>Fri, 21 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;大家都知道 &lt;code&gt;Next&lt;/code&gt; 有个 &lt;code&gt;NextAuth&lt;/code&gt; 非常好用，其实 &lt;code&gt;Nuxt&lt;/code&gt; 也有配套的 &lt;code&gt;Auth Module&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而且 &lt;code&gt;NuxtAuth&lt;/code&gt; 还依赖于 &lt;code&gt;NextAuth&lt;/code&gt;，这为 &lt;code&gt;Nuxt&lt;/code&gt; 生态提供了非常多可靠性和便利性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NextAuth&lt;/code&gt; 里能用的，在 &lt;code&gt;NuxtAuth&lt;/code&gt; 里同样支持&lt;/p&gt;
&lt;p&gt;并且还针对Nuxt 提供了特定功能，比如登录、注销、身份验证中间件和插件等&lt;/p&gt;
&lt;p&gt;使用 NuxtAuth 时需要注意，它包装了  &lt;code&gt;next-auth@4.21.1&lt;/code&gt; ，因为更高的版本中更改了包导出。&lt;/p&gt;
&lt;p&gt;所以使用时，安装指定版本就可以了。&lt;/p&gt;
&lt;p&gt;这里我先以 &lt;strong&gt;Github登录&lt;/strong&gt; 为例演示其用法&lt;/p&gt;
&lt;p&gt;&lt;em&gt;本系列在博客站上会持续更新，直到覆盖NuxtAuth的所有用法&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;初始化和安装依赖&lt;/h2&gt;
&lt;p&gt;我在一个 &lt;code&gt;layer&lt;/code&gt; 中演示其使用方法，因为这部分功能相对独立，使用的地方继承就行。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nuxt.new/&quot;&gt;新建&lt;/a&gt;项目&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nuxi init -t layer zz-auth-layer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后来到项目根目录下，安装依赖&lt;/p&gt;
&lt;p&gt;注：这里有两种 &lt;code&gt;Provider&lt;/code&gt; 可选，一个是 &lt;code&gt;authjs(next-auth)&lt;/code&gt; ，一个是 &lt;code&gt;local&lt;/code&gt; ，所谓 &lt;code&gt;local&lt;/code&gt; 就是你已经自己写好了一套登录逻辑，他可以帮你接管一下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm exec nuxi module add sidebase-auth
// 因为我们使用github登录，所以要安装这个包
pnpm add next-auth@4.21.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给此项目开启 &lt;code&gt;Nuxt4&lt;/code&gt; 的特性，&lt;code&gt;Nuxt4&lt;/code&gt; 的目录结构更合理，更适合拓展。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
	future: {
	    compatibilityVersion: 4
	},
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把根目录下的内容改造成 v4 的结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;.
├── .editorconfig
├── .env
├── .gitignore
├── .npmignore
├── .npmrc
├── .nuxtrc
├── README.md
├── app
│   ├── app.config.ts
│   ├── app.vue
│   └── components
│       └── AuthView.vue
├── nuxt.config.ts
├── package.json
├── pnpm-lock.yaml
├── server
│   ├── api
│   │   └── auth
│   └── tsconfig.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组件 &lt;code&gt;AuthView&lt;/code&gt; 里什么也没有，随便写点东西就行&lt;/p&gt;
&lt;h2&gt;配置 NuxtAuth&lt;/h2&gt;
&lt;p&gt;然后再把刚才安装的 &lt;code&gt;nuxt-auth&lt;/code&gt; 配置一下。这里的配置内容，来源于&lt;a href=&quot;https://auth.sidebase.io/guide/application-side/configuration&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
	future: {
	    compatibilityVersion: 4
	},
	auth: {
	    isEnabled: true,
	    disableServerSideAuth: false,
	    originEnvKey: &apos;NUXT_AUTH_ORIGIN&apos;,
	    // baseURL: &apos;http://localhost:3000/api/auth&apos;,
	    provider: {
	      type: &apos;authjs&apos;,
	      trustHost: false,
	      defaultProvider: &apos;github&apos;,
	      addDefaultCallbackUrl: true,
	    },
	    sessionRefresh: {
	      enablePeriodically: 1000 * 60 * 5, // 5 分钟刷新一次
	      enableOnWindowFocus: true,
	    }
	},
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你此时是按照官方文档一步步的来的，那这里的 &lt;code&gt;provider&lt;/code&gt; 就是后面配上的&lt;/p&gt;
&lt;p&gt;然后在 &lt;code&gt;app.config.ts&lt;/code&gt; 里增加一些信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineAppConfig({
  authLayer: {
    name: &apos;Hello from Auth layer (playground)&apos;,
    enabled: true,
  }
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当这个 &lt;code&gt;auth-layer&lt;/code&gt; 层被其他模块 &lt;code&gt;extend&lt;/code&gt; 时，这个 &lt;code&gt;authLayer&lt;/code&gt; 对象就会被合并过去&lt;/p&gt;
&lt;p&gt;所以也可以直接在其他模块中使用 &lt;code&gt;useAppConfig().authLayer?.enabled&lt;/code&gt; 来判断当前是否开启了鉴权的层&lt;/p&gt;
&lt;p&gt;然后再来配置环境变量&lt;/p&gt;
&lt;p&gt;刚才已经配置了一个 &lt;code&gt;originEnvKey&lt;/code&gt; 为 &lt;code&gt;NUXT_AUTH_ORIGIN&lt;/code&gt; ，所以需要在 .env 中增加这个环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NUXT_AUTH_ORIGIN=http://localhost:3000/api/auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时配置的这个路径，需要我们自己定义出来，所以先新增这个接口&lt;code&gt;server/api/auth/[...].ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个接口是一个 &lt;code&gt;NuxtAuthHandler&lt;/code&gt; ，是从 &lt;code&gt;NextAuthHandler&lt;/code&gt; 基础上改编来的，我们需要在这个 &lt;code&gt;Handler&lt;/code&gt; 中定义我们的 &lt;code&gt;Provider&lt;/code&gt;，也就是 &lt;code&gt;Github&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import GithubProvider from &apos;next-auth/providers/github&apos;
import { NuxtAuthHandler } from &apos;#auth&apos;

export default NuxtAuthHandler({
  // A secret string you define, to ensure correct encryption
  secret: &apos;your-secret-here&apos;,
  providers: [
    // @ts-expect-error Use .default here for it to work during SSR.
    GithubProvider.default({
      clientId: &apos;your-client-id&apos;,
      clientSecret: &apos;your-client-secret&apos;
    })
  ]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义好后需要再定义一下 &lt;code&gt;secret&lt;/code&gt;，以及&lt;code&gt;Github&lt;/code&gt; 需要的 &lt;strong&gt;id&lt;/strong&gt; 和 &lt;strong&gt;secret&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 中定义 &lt;code&gt;runtimeConfig&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;runtimeConfig: {
    authSecret: &apos;your_secret&apos;,
    authOrigin: &apos;your_secret&apos;,
    githubClientId: &apos;your_secret&apos;,
    githubClientSecret: &apos;your_secret&apos;,
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;runtimeConfig&lt;/code&gt; 里的 &lt;code&gt;authSecret&lt;/code&gt; 会被 &lt;code&gt;.env&lt;/code&gt; 里的 &lt;code&gt;NUXT_AUTH_SECRET&lt;/code&gt; 所覆盖&lt;/p&gt;
&lt;p&gt;所以我们把真实的 &lt;code&gt;authSecret&lt;/code&gt; 写在 &lt;code&gt;.env&lt;/code&gt; ,然后在 &lt;code&gt;git&lt;/code&gt; 中忽略即可&lt;/p&gt;
&lt;p&gt;此时已经定义好了需要的四个环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NUXT_AUTH_ORIGIN=http://localhost:3000/api/auth
NUXT_AUTH_SECTRET=123131231231
NUXT_GITHUB_CLIENT_ID=xxxxx
NUXT_GITHUB_CLIENT_SECRET=xxxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是 Github 的 id 和 secret 还需要自己去申请&lt;/p&gt;
&lt;p&gt;先打开 Github ，登录后，点击&lt;strong&gt;头像&lt;/strong&gt; -&gt; Settings -&gt; 左侧菜单拉到最下边的 &lt;strong&gt;developer settings&lt;/strong&gt; -&gt; &lt;strong&gt;OAuth Apps&lt;/strong&gt; -&gt; &lt;strong&gt;New OAuth App&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后填写如下表单即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202502241606759.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为请求 &lt;code&gt;Github&lt;/code&gt; 之后，&lt;code&gt;Github&lt;/code&gt; 需要再跳过来，需要 &lt;code&gt;callback URL&lt;/code&gt; 需要填你本地项目的一个页面地址&lt;/p&gt;
&lt;p&gt;等待上线后，&lt;strong&gt;再把此处的&lt;code&gt;OAuth App&lt;/code&gt; 以及 &lt;code&gt;.env&lt;/code&gt;（看部署方式而定） 里配置的本地路径改为线上路径&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注册后就会给你一个 id 和 secret ，把它写在 &lt;code&gt;.env&lt;/code&gt; 里&lt;/p&gt;
&lt;h2&gt;在 Playground 中使用&lt;/h2&gt;
&lt;p&gt;配置好后，我们就可以使用 &lt;code&gt;NuxtAuth&lt;/code&gt; 提供的 &lt;code&gt;composable&lt;/code&gt; 来管理鉴权的逻辑了&lt;/p&gt;
&lt;p&gt;为什么不在根目录下直接写，而是在 &lt;code&gt;.playground&lt;/code&gt; 中写呢？&lt;/p&gt;
&lt;p&gt;因为根目录下相当于一个独立的npm包，是要给其他项目使用的，而&lt;code&gt;.playground&lt;/code&gt;就相当于是模拟了其他项目&lt;/p&gt;
&lt;p&gt;所以在 &lt;code&gt;layer&lt;/code&gt; 部分，只需要完成通用的配置、页面、接口即可，具体使用时都在 &lt;code&gt;.playground&lt;/code&gt; 里操作&lt;/p&gt;
&lt;p&gt;&lt;code&gt;playground&lt;/code&gt; 里已经写好了 &lt;code&gt;extends: [&apos;..&apos;]&lt;/code&gt;，所以我们直接新建一个页面来测试一下如何使用 &lt;code&gt;Github&lt;/code&gt; 登录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const {
  status,
  data,
  lastRefreshedAt,
  getCsrfToken,
  getProviders,
  getSession,
  signIn,
  signOut
} = useAuth()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;button @click=&quot;signIn(&apos;github&apos;)&quot;&gt; 使用 Github 登录&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当点击按钮时，就会跳转到 &lt;code&gt;Github&lt;/code&gt;，授权登录后就能拿到 &lt;code&gt;Github&lt;/code&gt; 提供的用户信息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在此之后的逻辑就和Github无关了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NuxtAuth&lt;/code&gt; 会帮我们做一整套的逻辑，我们只需要拿到用户信息后，和自行注册的用户做一个关联即可&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status&lt;/code&gt; 表示当前登录状态&lt;/p&gt;
&lt;p&gt;&lt;code&gt;data&lt;/code&gt; 表示当前会话中的数据，这个 &lt;code&gt;sessionData&lt;/code&gt; 也可以在 &lt;code&gt;NuxtAuthHandler&lt;/code&gt; 的 &lt;code&gt;callback&lt;/code&gt; 中插入一些用户信息&lt;/p&gt;
&lt;p&gt;登出时，使用 &lt;code&gt;signOut&lt;/code&gt; 即可&lt;/p&gt;
&lt;p&gt;如果是使用自己写的逻辑，也就是 &lt;code&gt;local Provider&lt;/code&gt; ，还可以配置 refreshToken 的接口等等&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;部署时，只需要注意一下 &lt;code&gt;.env&lt;/code&gt; 文件，因为 Nuxt 打包后不会再动态读取 &lt;code&gt;.env&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以这些环境变量如何配置，和你使用的部署工具有关&lt;/p&gt;
&lt;p&gt;比如你用的 &lt;code&gt;pm2&lt;/code&gt;，在 &lt;code&gt;ecosystem.config.cjs&lt;/code&gt;，就可以配置 &lt;code&gt;NODE_ENV&lt;/code&gt;，但是我不推荐这样搞，因为如果代码要放在 &lt;code&gt;github&lt;/code&gt; 上，就不要在任何一次提交中包含敏感的 &lt;code&gt;token&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;除非你的 &lt;code&gt;ecosystem.config.cjs&lt;/code&gt; 不在你的项目里，而是直接写在服务器上对应的文件夹里，每次打包时只覆盖 Nuxt 的部分&lt;/p&gt;
&lt;p&gt;如果是用 &lt;code&gt;Gitea&lt;/code&gt; 来自动化部署，就在 &lt;code&gt;Gitea&lt;/code&gt; 的仓库设置中配置变量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202502241606761.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;正常情况只需要配置一次，因为正式环境没有需要经常改变的变量&lt;/p&gt;
&lt;p&gt;以上就是如何使用 &lt;code&gt;Github&lt;/code&gt; 的小 &lt;code&gt;Demo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;未完待续&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[一行 URL 动态生成生成封面，免费、开源、高效]]></title><link>https://zzao.club/post/imgx/one-url-generate-unique-png</link><guid isPermaLink="true">https://zzao.club/post/imgx/one-url-generate-unique-png</guid><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;有时候写文章时需要上传封面图，大部分情况我只需要&lt;strong&gt;一种风格的图片&lt;/strong&gt;，包含简单的文字、LOGO、强调文字等即可。&lt;/p&gt;
&lt;p&gt;现有的各种 App 、Web 的卡片类应用都需要我打开他们的平台，然后选择合适的模板，最后还要充个会员，不然就限制我下载图片的大小，给我加个水印什么的。&lt;/p&gt;
&lt;p&gt;此 API 可以帮助我快速在任意场景下拿到一张想要的图片。必要时可以下载，临时使用直接用链接。&lt;/p&gt;
&lt;p&gt;并且如果是文章中配图，大部分技术平台都支持自动转存，很省心。&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;API格式：&lt;a href=&quot;https://imgx.zzao.club/api/img/%5B%E6%AF%94%E4%BE%8B%E7%BC%96%E5%8F%B7%5D/%5B%E6%A8%A1%E6%9D%BF%E7%BC%96%E5%8F%B7%5D/%5B%E6%96%87%E6%9C%AC%E5%86%85%E5%AE%B9%5D?%5B%E5%8F%82%E6%95%B0%5D=xxx&quot;&gt;https://imgx.zzao.club/api/img/[比例编号]/[模板编号]/[文本内容]?[参数]=xxx&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如：&lt;a href=&quot;https://imgx.zzao.club/api/img/001/001/%5BNuxt4%5D%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83%E7%B3%BB%E5%88%97+%E7%82%B9%E5%87%BB%E5%B0%B1%E9%80%81%E5%B1%A0%E9%BE%99%E5%AE%9D%E5%88%80&quot;&gt;https://imgx.zzao.club/api/img/001/001/[Nuxt4]从入门到放弃系列+点击就送屠龙宝刀&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/api/img/001/001/%5BNuxt4%5D%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%83%E7%B3%BB%E5%88%97+%E7%82%B9%E5%87%BB%E5%B0%B1%E9%80%81%E5%B1%A0%E9%BE%99%E5%AE%9D%E5%88%80&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意 color 不要带 #&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;比例&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;001：2.35:1 =&gt; 公众号&lt;/li&gt;
&lt;li&gt;002：1:1 =&gt; 头像&lt;/li&gt;
&lt;li&gt;003: 4:3 =&gt; ?&lt;/li&gt;
&lt;li&gt;004: 3:4 =&gt; 小红书等&lt;/li&gt;
&lt;li&gt;005: 16:9 =&gt; 视频封面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新比例请联系我～&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模板001&lt;/h3&gt;
&lt;p&gt;此模板针对 &lt;strong&gt;字数不多&lt;/strong&gt;、&lt;strong&gt;字号较大&lt;/strong&gt; 的使用场景。 比如文章封面、部分自媒体平台的配图。&lt;/p&gt;
&lt;p&gt;比如高清(x2)的公众号封面配图（2.35:1）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/api/img/001/001/*Deepseek*+108%E5%A4%A7%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7+%E5%8F%AA%E5%89%A9+*66*%E4%B8%AA%E5%90%8D%E9%A2%9D?bgColor=ff758c-ff7eb3&amp;#x26;accentColor=0088a9&amp;#x26;color=ffffff&amp;#x26;ratio=2&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;比如小红书配图（3:4）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgx.zzao.club/api/img/004/001/*Deepseek*+108%E5%A4%A7%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7+%E5%8F%AA%E5%89%A9+*66*%E4%B8%AA%E5%90%8D%E9%A2%9D?bgColor=ff7e5f-feb47b&amp;#x26;accentColor=0088a9&amp;#x26;color=ffffff&amp;#x26;ratio=2&amp;#x26;center=1&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;强调文字&lt;/td&gt;
&lt;td&gt;支持配置强调色，&lt;strong&gt;后续&lt;/strong&gt;会支持多种强调方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;换行&lt;/td&gt;
&lt;td&gt;文字主动换行，&lt;strong&gt;后续&lt;/strong&gt;支持每行文字设置不同颜色&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;参考值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bgColor&lt;/td&gt;
&lt;td&gt;背景色&lt;/td&gt;
&lt;td&gt;292a3a-536976&lt;/td&gt;
&lt;td&gt;目前支持两个颜色，中间用&lt;code&gt;-&lt;/code&gt;分割，从左到右径向渐变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;accentColor&lt;/td&gt;
&lt;td&gt;强调色&lt;/td&gt;
&lt;td&gt;0088a9&lt;/td&gt;
&lt;td&gt;[xxx] 包裹的文字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;color&lt;/td&gt;
&lt;td&gt;文字颜色&lt;/td&gt;
&lt;td&gt;ffffff&lt;/td&gt;
&lt;td&gt;当前表示所有文字的配色，即将支持多个颜色，匹配每行文字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ratio&lt;/td&gt;
&lt;td&gt;几倍图&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;要么 1 要么 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;center&lt;/td&gt;
&lt;td&gt;是否居中&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;默认左对齐，居中时为整体文字都居中&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;技术方面&lt;/h2&gt;
&lt;p&gt;核心是 &lt;code&gt;satori&lt;/code&gt; 和 &lt;code&gt;satori-html&lt;/code&gt; 这两个插件。没有用到无头浏览器 &lt;code&gt;puppeteer&lt;/code&gt; 之类的，太重，太消耗服务器资源了。&lt;/p&gt;
&lt;p&gt;项目是 &lt;code&gt;Nuxt&lt;/code&gt; 搭建的，通过 &lt;code&gt;createSSRApp&lt;/code&gt; 和 &lt;code&gt;renderToString&lt;/code&gt;，就可以拿到自己写好的 Vue 组件给 &lt;code&gt;satori&lt;/code&gt; 渲染了，再把渲染后的 &lt;code&gt;svg&lt;/code&gt; 转为 &lt;code&gt;png&lt;/code&gt;, 接口直接把 &lt;code&gt;png&lt;/code&gt; 返回&lt;/p&gt;
&lt;p&gt;所以这个接口就类似于动态内容的 &lt;code&gt;png&lt;/code&gt; 图片了&lt;/p&gt;
&lt;p&gt;有条件的可以自己部署一下。&lt;/p&gt;
&lt;p&gt;MIT&lt;/p&gt;
&lt;h2&gt;后续规划&lt;/h2&gt;
&lt;p&gt;后面的使用继续朝着&lt;strong&gt;极简&lt;/strong&gt;的方向走&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;选取上述的参数、尺寸、模板，保存为预设码&lt;/strong&gt;，请求时携带一个简短的&lt;code&gt;code&lt;/code&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;</content:encoded></item><item><title><![CDATA[Memos Docker命令]]></title><link>https://zzao.club/post/memos/memos-docker-cmd</link><guid isPermaLink="true">https://zzao.club/post/memos/memos-docker-cmd</guid><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;本地打包&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker build ./ -t memoz --load  
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;本地运行&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /memos/:/var/opt/memos memoz
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;docker hub发布&lt;/h2&gt;
&lt;p&gt;登录 docker hub&lt;/p&gt;
&lt;h3&gt;1️⃣打Tag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker tag memoz gnakdogg/memoz:1.0.1 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2️⃣发布&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker push gnakdogg/memoz:1.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;云服务器运行&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /home/memoz/:/var/opt/memos gnakdogg/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;云服务器本地运行（因为云服务器拉不下来Docker hub&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;先把代码传到云服务器，然后运行 docker build  打包出镜像，然后run运行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;运行到pnpm build时报内存溢出 加入参数 --memory=2g&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker build ./ -t memoz --memory=2g
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /home/memoz/:/var/opt/memos memoz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker加速&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo mkdir -p /etc/docker

sudo tee /etc/docker/daemon.json &amp;#x3C;&amp;#x3C;-&apos;EOF&apos;
{
    &quot;registry-mirrors&quot;: [
	    &quot;https://1js6gccw.mirror.aliyuncs.com&quot;,
        &quot;https://docker.m.daocloud.io&quot;,
        &quot;https://dockerproxy.com&quot;,
        &quot;https://docker.mirrors.ustc.edu.cn&quot;,
        &quot;https://docker.nju.edu.cn&quot;
    ]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker推送到阿里云容器服务&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;登录&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo docker login --username=523748995@qq.com registry.cn-beijing.aliyuncs.com
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo docker login --username=523748995@qq.com --password godkang75 registry.cn-beijing.aliyuncs.com
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;打包&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker build ./ -t memoz --load  
docker buildx build ./ -t memoz --load --platform linux/amd64 (对应阿里云ubuntu服务器)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在阿里云容器镜像服务实例中建一个命名空间如zzstudi0&lt;br&gt;
2. Tag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker tag memoz registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Push&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker push registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker push --platform linux/amd64 registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉取&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;登录&lt;/li&gt;
&lt;li&gt;拉取&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker pull registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker pull --platform linux/amd64 ![[成为一个前端开发者的路线.pdf]]:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;运行&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /home/memoz/:/var/opt/memos registry.cn-beijing.aliyuncs.com/zzstudi0/memoz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;docker network ： e1cb1ad4b9bc&lt;/p&gt;
&lt;h2&gt;更新版本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;停止目前运行的容器
docker stop container_id
删除容器（为了避免名称冲突）
docker rm container_id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉取指定版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull --platform linux/amd64 registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:1.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行指定版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /home/memoz/:/var/opt/memos registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:1.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为数据已经挂载到了外部，所以不用担心数据丢失&lt;/p&gt;</content:encoded></item><item><title><![CDATA[【持续更新】Nuxt 高质量资料汇总]]></title><description><![CDATA[全网高质量Nuxt相关资料，包括文章、视频、实战开源项目]]></description><link>https://zzao.club/post/nuxt/keep-update-nuxt-useful-links</link><guid isPermaLink="true">https://zzao.club/post/nuxt/keep-update-nuxt-useful-links</guid><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;🔥文章&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zenn.dev/comm_vue_nuxt/articles/nuxt-use-fetch-guide#usefetch-%E3%81%AE%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9&quot;&gt;Nuxt 3・Nuxt 4 の useFetch() を完全に理解する（したい）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;📀视频&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/@TheAlexLichter&quot;&gt;Alexander Lichter&lt;/a&gt; （Nuxt.js team member）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=XGcJiG0fZ8Y&quot;&gt;How to use layers in Nuxt 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/playlist?list=PL4cUxeGkcC9ghm7-iTfS9n468Kp7l9Ipu&quot;&gt;Vue Animations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;👍项目&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ZTL-UwU/shadcn-docs-nuxt&quot;&gt;shadcn-docs-nuxt&lt;/a&gt; : 基于 Nuxt Content 和 shadcn-vue 的文档类建站模板&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[使用puppeteer爬取掘金热榜]]></title><description><![CDATA[使用puppeteer爬取掘金热榜]]></description><link>https://zzao.club/post/spider/puppeteer-jujin-hot-ranks</link><guid isPermaLink="true">https://zzao.club/post/spider/puppeteer-jujin-hot-ranks</guid><pubDate>Wed, 12 Feb 2025 12:57:38 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;又开新坑了。准备花点时间研究一下爬虫、自动化方向的技术，当然还是围绕node来展开。&lt;/p&gt;
&lt;p&gt;我会把前端、Node相关体系的技术内容和实际需求融合，力求闭环，闭不了就当把全部干货整理成知识库。文章首发在公众号：早早集市，感兴趣的可以关注一下。&lt;/p&gt;
&lt;p&gt;本篇是基于puppeteer这个库做的爬虫demo。&lt;/p&gt;
&lt;h2&gt;什么是Puppeteer&lt;/h2&gt;
&lt;p&gt;Puppeteer 是一个 Node 库，它提供了一个高级 API 来通过 &lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/&quot; title=&quot;DevTools&quot;&gt;DevTools&lt;/a&gt; 协议控制 Chromium 或 Chrome。Puppeteer 默认以 &lt;a href=&quot;https://developers.google.com/web/updates/2017/04/headless-chrome&quot; title=&quot;headless&quot;&gt;headless&lt;/a&gt; 模式运行，但是可以通过修改配置文件运行“有头”模式。&lt;/p&gt;
&lt;h2&gt;Puppeteer能做什么&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;生成页面 PDF。&lt;/li&gt;
&lt;li&gt;抓取 SPA（单页应用）并生成预渲染内容（即“SSR”（服务器端渲染））。&lt;/li&gt;
&lt;li&gt;自动提交表单，进行 UI 测试，键盘输入等。&lt;/li&gt;
&lt;li&gt;创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的Chrome中执行测试。&lt;/li&gt;
&lt;li&gt;捕获网站的 &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference&quot; title=&quot;timeline trace&quot;&gt;timeline trace&lt;/a&gt;，用来帮助分析性能问题。&lt;/li&gt;
&lt;li&gt;测试浏览器扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;p&gt;一是可以单独写一个js文件，从头开始写个demo，直接用node运行即可。&lt;/p&gt;
&lt;p&gt;二是写在其他后端项目里。这里我选择写在我之前的nest的项目，方便后续灵感来了之后进一步整合。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;版本：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;node 18.18.2&lt;/li&gt;
&lt;li&gt;puppeteer 21.7.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;先装puppeteer&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;pnpm i puppeteer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在公司，网络不好的话，可以换个源试试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;pnpm config set registry https://registry.npmmirror.com

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，我们可以写一个接口用于测试，每次请求时运行一下爬取函数。然后在service里实现爬取的逻辑。&lt;/p&gt;
&lt;h2&gt;开始编写&lt;/h2&gt;
&lt;p&gt;关于&lt;a href=&quot;https://pptr.dev/&quot; title=&quot;example&quot;&gt;example&lt;/a&gt;，官网就有，很适合快速学习一下api。我仿照这个example快速开始，只不过我这里换成了掘金，因为例子里的地址因为某些原因，不方便访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const browser = await puppeteer.launch({
      headless: false,
      args: [&apos;--start-fullscreen&apos;],
    });
const page = await browser.newPage();
await page.goto(&apos;https://juejin.cn/hot/articles&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先解释一下上边几句。每个api我都附上了官方文档地址，我写代码一般就只看官方文档来写。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.puppeteernode.launch&quot; title=&quot;launch&quot;&gt;launch&lt;/a&gt;：启动一个浏览器实例，并且可以传入配置参数，如&lt;code&gt;headless&lt;/code&gt;可以配置以无头(&lt;code&gt;&apos;new&apos;&lt;/code&gt;)或有头模式（&lt;code&gt;fasle&lt;/code&gt;）运行。返回值 &lt;code&gt;Promise&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.browser&quot; title=&quot;Browser&quot;&gt;Browser&lt;/a&gt;&lt;code&gt;&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.browser.newpage&quot; title=&quot;newPage&quot;&gt;newPage&lt;/a&gt;：在 default browser context打开一个页面。返回值&lt;code&gt;Promise&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.page&quot; title=&quot;Page&quot;&gt;Page&lt;/a&gt;&lt;code&gt;&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.page.goto&quot; title=&quot;goto&quot;&gt;goto&lt;/a&gt;：导航到一个url。返回值&lt;code&gt;Promise&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.httpresponse&quot; title=&quot;HTTPResponse&quot;&gt;HTTPResponse&lt;/a&gt;&lt;code&gt; | null&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以爬取数据的思路，就和自己打开浏览器进入掘金浏览一样，打开网页 ⇒ 输入地址 ⇒ 等待加载 ⇒ 加载完成 ⇒ 看到（拿到）数据 ⇒ 数据存储&lt;/p&gt;
&lt;p&gt;因为掘金不需要登录也可以直接看到文章热榜，所以直接去找页面元素，拿到数据即可。&lt;/p&gt;
&lt;p&gt;对于一个前端来说，去审查元素是比较简单的，如何你不是前端的话，可以用这种方式去获取元素&lt;/p&gt;
&lt;h3&gt;分析页面&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;F12或者右键审查，打开控制台，选中这个工具，去点击页面上的元素&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![[1-img-20241119141175.png]]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在左侧点击页面上的字后，右侧控制台的元素会自动聚焦&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![[2-img-20241119141155.png]]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在右侧高亮的元素上右键，可以看到复制，打开后有个复制selector，选择。因为puppeteer是使用的css selector来获取元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![[1-img-20241119141177.png]]&lt;/p&gt;
&lt;h3&gt;获取数据&lt;/h3&gt;
&lt;p&gt;puppeteer提供几个api可以用来获取到元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Page.$()&lt;/code&gt;  相当于 &lt;code&gt;document.querySelector&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page.$$()&lt;/code&gt;  相当于 &lt;code&gt;document.querySelectorAll&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个api的返回值分别是&lt;code&gt;Promise&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.elementhandle&quot; title=&quot;ElementHandle&quot;&gt;ElementHandle&lt;/a&gt;&lt;code&gt;&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.nodefor&quot; title=&quot;NodeFor&quot;&gt;NodeFor&lt;/a&gt;&lt;code&gt;&amp;#x3C;Selector&gt;&gt; | null&gt;&lt;/code&gt; 和 &lt;code&gt;Promise&amp;#x3C;Array&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.elementhandle&quot; title=&quot;ElementHandle&quot;&gt;ElementHandle&lt;/a&gt;&lt;code&gt;&amp;#x3C;&lt;/code&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.nodefor&quot; title=&quot;NodeFor&quot;&gt;NodeFor&lt;/a&gt;&lt;code&gt;&amp;#x3C;Selector&gt;&gt;&gt;&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而ElementHandle也有获取元素的相同api&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ElementHandle.$()&lt;/code&gt; 相当于在获取了一个元素的基础上，再获取它的子元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ElementHandle.$$()&lt;/code&gt; 同理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个api的返回值和Page的两个api&lt;strong&gt;返回值相同&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;获取到元素后，还需要获取元素的值。一般有两种，一种是元素的内容，一种是元素的属性值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page.$eval&lt;/strong&gt;(&apos;.selector&apos;, el ⇒ el.textContent)  这种方式可以直接获取到元素内容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page.$$eval&lt;/strong&gt;(&apos;.selector, (elements) =&gt; elements.map((el) =&gt; el.getAttribute(&apos;href&apos;)&apos;) 这种则是获取元素的属性，当然这个api是获取所有内容&lt;/li&gt;
&lt;li&gt;ElementHandle.$eval  同理&lt;/li&gt;
&lt;li&gt;ElementHandle.$$eval 同理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;知道了这几个api之后，爬数据基本不是问题了。&lt;/p&gt;
&lt;p&gt;继续写一下代码，粘贴一下刚才复制的selector，看看能否取到数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const number = await wrap.$eval(&apos;#juejin &gt; div:nth-child(1) &gt; div.view-container.hot-lists &gt; main &gt; div.hot-list-body &gt; div.hot-list-wrap &gt; div.hot-list &gt; a:nth-child(1) &gt; div &gt; div.article-item-left &gt; div.article-number.article-number-1&apos;, (e) =&gt; e.textContent)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到复制来的selector非常长，是从页面最开始元素开始的，可以自己适当删减一下。一般只具体到它的父元素和它自己的元素就可以了。&lt;/p&gt;
&lt;p&gt;有的时候，元素需要时间加载，比如元素基于接口渲染时，接口或者网速很慢，这个时候获取元素是获取不到的。puppeteer也有api可以等待元素加载出来&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pptr.dev/api/puppeteer.page.waitforselector&quot; title=&quot;Page.waitForSelector()&quot;&gt;Page.waitForSelector()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如要等待掘金热榜的文章列表被加载出来&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  await page.waitForSelector(&apos;.hot-list .article-item-wrap&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后照着葫芦画瓢，再把文章标题、作者、热度等信息也获取到&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const number = await wrap.$eval(
  &apos;.article-number&apos;,
  (el) =&gt; el.textContent,
);
const title = await wrap.$eval(&apos;.article-title&apos;, (el) =&gt; el.textContent);
const hotNumber = await wrap.$eval(
  &apos;.article-hot .hot-number&apos;,
  (el) =&gt; el.textContent,
);
const authorName = await wrap.$eval(
  &apos;.article-author-name-text&apos;,
  (el) =&gt; el.textContent,
);
const authorUrl = await wrap.$eval(&apos;.article-author-name&apos;, (el) =&gt;
  el.getAttribute(&apos;href&apos;),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在nest的控制台打印一下，看看输出的内容是否正确。&lt;/p&gt;
&lt;p&gt;爬取完成后，可以关闭一下browser&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await browser.close();

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据存储&lt;/h3&gt;
&lt;p&gt;基于学习的目的，数据可以看情况存储在本地json文件，或者数据库中，或者发送给其他服务器。&lt;/p&gt;
&lt;p&gt;注意不要滥用数据或进行其他违法行为，目的仅仅是学习node，切记切记。&lt;/p&gt;
&lt;p&gt;这里我选择把数据&lt;code&gt;articleList&lt;/code&gt;存在本地json文件中。用&lt;code&gt;JSON.stringify(articleList, null, 2)&lt;/code&gt; 按缩进为2格式化一下数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;fs.writeFileSync(
  `./热榜-${+new Date()}.json`,
  JSON.stringify(articleList, null, 2),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据处理&lt;/h3&gt;
&lt;p&gt;通过获取到的数据可以发现，很多数据没法直接存储，需要被洗一洗。比如上边文章编号，拿到数据之后有很多空格，存之前我们先处理处理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;let reg = /(
|\s)*/g;

for循环articleList
  article.number = article.number.replace(reg, &apos;&apos;);
  article.hotNumber = article.hotNumber.replace(reg, &apos;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把多余的&lt;code&gt;空格&lt;/code&gt;和&lt;code&gt; &lt;/code&gt;去掉。&lt;/p&gt;
&lt;p&gt;然后当前爬取的是哪个榜单，存的时候我也想记录一下，再去页面上找文章列表上方的榜单名称。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;let navName = await page.$eval(
  &apos;div.hot-list-header &gt; div &gt; span.hot-title &gt; span&apos;,
  (el) =&gt; el.textContent,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;榜单名称拿到后，也需要处理一下空格，不再赘述。&lt;/p&gt;
&lt;p&gt;这个榜单名称是通过接口获取到然后渲染的，在点击左侧导航栏的时候就可以发现，所以要确保右侧内容被渲染完，我们再去开始爬取行为，所以可以在开头加上这两句。确保页面加载出来的时候，这两块已经被渲染完毕。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await page.waitForSelector(&apos;.hot-list .article-item-wrap&apos;);
await page.waitForSelector(
  &apos;div.hot-list-header &gt; div &gt; span.hot-title &gt; span&apos;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就完成了对热榜-综合榜的爬取。然后再去爬其他榜单。&lt;/p&gt;
&lt;h3&gt;多页爬取&lt;/h3&gt;
&lt;p&gt;通过分析左侧的导航栏，可以在元素a标签上发现一个href，点击后，页面就会切换到对应的地址。所以我的思路是先采集全部的地址。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const navUrls = await page.$$eval(
  &apos;.sub-nav-item-wrap .nav-item-content a&apos;,
  (elements) =&gt; elements.map((el) =&gt; el.getAttribute(&apos;href&apos;)),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把刚才写的爬取综合榜的过程，封装到一个函数里，作为爬取单页的方法。因为每个榜单的样式都是一样的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 伪代码
 function getPageData() {
   await 元素加载
   await 获取元素
   await 获取内容
   await 组装数据
   await 数据处理
   await 写入文件
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后用一个for of 循环进行多页的爬取，注意不要用forEach，因为要保证能await&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;for (const url of navUrls) {
  const pageUrl = &apos;https://juejin.cn&apos; + url;
  await page.goto(pageUrl);
  await this.getPageData(page);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就在项目的根目录，产生了9个json文件了，可以看一下数据爬取的有没有问题。处理过程中，主要问题是要等待页面元素加载完再拿，符合一个正常人去看网页的逻辑。&lt;/p&gt;
&lt;p&gt;Puppeteer也提供了其他等待的方法，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;waitForTimeout&lt;/li&gt;
&lt;li&gt;waitForFunction&lt;/li&gt;
&lt;li&gt;waitForRequest&lt;/li&gt;
&lt;li&gt;waitForResponse&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续我再做登录、验证码等自动化操作时再详细总结一下。&lt;/p&gt;
&lt;p&gt;以上就是全部内容了👏&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这篇文章作为一个简单的小demo，记录一下研究puppeteer的开始，感觉可玩性很强。&lt;/p&gt;
&lt;p&gt;可以用来在不适合摸鱼的办公环境，爬一下热榜然后通过webhook发到钉钉去看。或者用于签到、领这领那等重复工作。&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:encoded></item><item><title><![CDATA[【Hono】Bun竟然能这么快？搭配HonoJS的入门指南]]></title><link>https://zzao.club/post/hono/hono-bun-fast</link><guid isPermaLink="true">https://zzao.club/post/hono/hono-bun-fast</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191444086.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;最近我用bun+hono搭建了一个web服务，并尝试用docker打包部署。&lt;/p&gt;
&lt;p&gt;在没有缓存的情况下，&lt;code&gt;docker build&lt;/code&gt; &lt;strong&gt;打包仅用了30s&lt;/strong&gt;，如果是项目修改后再重新打包，更是&lt;strong&gt;连5s都用不了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而这个小服务虽然只是刚起步，但已经具备了&lt;strong&gt;日志、响应和错误标准化返回、数据库连接（sqlite）、路由分组、环境变量配置、jwt鉴权&lt;/strong&gt;这几项，可以说当成个人的小玩具已经够格了。&lt;/p&gt;
&lt;h2&gt;Bun和Node到底是什么关系&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Bun is an all-in-one JavaScript runtime &amp;#x26; toolkit designed for speed, complete with a&lt;br&gt;
bundler, test runner, and Node.js-compatible package manager.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bun 是一款专为提高速度而设计的一体化 JavaScript &lt;strong&gt;运行时&lt;/strong&gt;和&lt;strong&gt;工具包&lt;/strong&gt;，配有捆绑器、测试运行器和与 Node.js 兼容的包管理器。&lt;/p&gt;
&lt;p&gt;Node.js 是一个免费、开源、跨平台的 JavaScript &lt;strong&gt;运行时&lt;/strong&gt;环境。&lt;/p&gt;
&lt;p&gt;通过他们官方的一句话介绍，可以清晰的看到，他们都是为了&lt;strong&gt;让JavaScript脱离浏览器&lt;/strong&gt;环境而创造的一个运行时环境。只不过Node使用&lt;code&gt;C艹&lt;/code&gt;编写，而Bun使用&lt;code&gt;Zig&lt;/code&gt;。Node基于&lt;code&gt;V8引擎&lt;/code&gt;(Goggle Chrome)，而Bun使用&lt;code&gt;JavaScriptCore&lt;/code&gt;(Apple Safari)&lt;/p&gt;
&lt;p&gt;以前我们用JavaScript都是和html配合写前端代码，写完后需要打开浏览器才能看到效果，而有了其他的运行时环境后，就可以像其他Python/Go语言一样直接在命令行执行JavaScript脚本，所以其功能也从操作Dom变为了操作系统级Api，如：文件IO、数据库等等。&lt;/p&gt;
&lt;p&gt;在只有Node一家独大的时候，我们甚至可以在（某些）面试官问：「你会什么后端语言」的时候，说：「我会NodeJS」,而现在有了&lt;code&gt;Deno&lt;/code&gt;和&lt;code&gt;Bun&lt;/code&gt;，我岂不是会了三门后端语言？（🤫bushi&lt;/p&gt;
&lt;p&gt;截止到当前，Node已经发布到了&lt;code&gt;22.8.0&lt;/code&gt;，庞大的开源module支撑起了整个社群的，而他的官方包管理器&lt;code&gt;npm&lt;/code&gt;有点让人一言难尽&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191444087.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是又出现了&lt;code&gt;yarn&lt;/code&gt;、&lt;code&gt;pnpm&lt;/code&gt;，&lt;strong&gt;老外写这些东西可能真的是在解决需求，到了咱这边真的也就是给面试官多提供了一些出题思路。&lt;/strong&gt; 而Bun本身就自带包管理器。&lt;/p&gt;
&lt;p&gt;另外，现在要想写一个“时髦的”前端项目，还必须有一个打包器，因为在不同的运行环境中，不同的浏览器中，对JavaScript的支持标准大不相同，所以需要把新版本的JavaScript降级，或者把TypeScript转换为JavaScript。或者是把JavaScript文件大小进行压缩。&lt;/p&gt;
&lt;p&gt;而Node生态下光打包器就有：Webpack、Rollup、Vite等等，更不要说Rust开始被大厂卷起来之后，又用Rust对以前打包速度、运行速度有上限的打包器进行重构。&lt;strong&gt;面试官的出题角度还在增加&lt;/strong&gt;。&lt;br&gt;
而Bun本身也是一个打包器。&lt;/p&gt;
&lt;p&gt;另外还有测试运行器..  &lt;code&gt; Vitest/Jest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以现在可以明白，在2022年才发布的Bun究竟是想要做什么了&lt;/p&gt;
&lt;h2&gt;Hono简介&lt;/h2&gt;
&lt;p&gt;Hono🔥是一个基于 Web 标准构建的小型、简单且超快的 Web 框架。可以运行在所有JavaScript运行时，当然也包括了&lt;code&gt;Bun&lt;/code&gt;，所以作为尝鲜，就图个鲜上加鲜。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在使用任何框架前，我都习惯先通读一遍官方文档&lt;/strong&gt;，这大概会花费我2-4小时的时候。&lt;/p&gt;
&lt;p&gt;在读完一遍官方文档后，如果使用过其他框架完成过类似的项目，就会大概知道这个框架哪些是自带的，哪些需要借助第三方。&lt;/p&gt;
&lt;p&gt;如果框架本身过于精简（比如Koa），你就不得不去研究一些官方插件或是第三方插件，或者说去拜读一些开源项目，或者去找一些快速启动模板，以便自己快速上手。&lt;/p&gt;
&lt;p&gt;Hono则是在文档里提供了很多官方的插件（Helper），无需翻看其他文档就能实现功能&lt;/p&gt;
&lt;h2&gt;搭建项目&lt;/h2&gt;
&lt;p&gt;按照官方文档开始搭建，因为我这里使用的是Bun，所以需要先下载好Bun&lt;/p&gt;
&lt;p&gt;Macos/Linux&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -fsSL https://bun.sh/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建项目&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;bun create hono@latest my-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建项目后会有一个入口&lt;code&gt;index.ts&lt;/code&gt;，在Bun中TS是一等公民，无需进行编译就能直接运行，所以速度非常快。&lt;/p&gt;
&lt;p&gt;然后我们需要添加一些常用的中间件，如&lt;code&gt;cors&lt;/code&gt;、&lt;code&gt;csrf&lt;/code&gt;，然后给自己配置一下喜欢的端口号&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Hono } from &apos;hono&apos;
import { showRoutes } from &apos;hono/dev&apos;
import { cors } from &apos;hono/cors&apos;
import { csrf } from &apos;hono/csrf&apos;

const app = new Hono()
// 统一的前缀
const api = app.basePath(&apos;/api&apos;)

// 预防csrf攻击
api.use(csrf())
// 所有接口设置cors， 也可以分别设置cors， 如user相关接口只允许指定ip访问
api.use(&apos;*&apos;, cors())

app.get(&apos;/&apos;, (c) =&gt; { return c.text(&apos;Hello Hono!&apos;) })
app.post(&apos;/&apos;, (c) =&gt; c.text(&apos;POST /&apos;)) 
app.put(&apos;/&apos;, (c) =&gt; c.text(&apos;PUT /&apos;)) 
app.delete(&apos;/&apos;, (c) =&gt; c.text(&apos;DELETE /&apos;))

// 每个实例的err要自己监听
// api.onError(errorHandler)

// verbose 会显示详情信息， 如： 是否使用了中间件
// showRoutes(api, { verbose: false })

export default {
  port: Bun.env.PORT,
  fetch: app.fetch,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就完成了一个简单的服务，可以打开&lt;code&gt;localhost:port&lt;/code&gt;，看一下是否返回了&lt;code&gt;Hello Hono&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;设置统一的前缀可以用&lt;code&gt;basePath&lt;/code&gt;，设置环境变量可以在&lt;code&gt;.env&lt;/code&gt; 、&lt;code&gt;.env.development&lt;/code&gt;、&lt;code&gt;.env.production&lt;/code&gt; 中配置（在bun的官方文档中），并使用&lt;code&gt;Bun.env.XXXX&lt;/code&gt;读取。&lt;/p&gt;
&lt;h2&gt;User模块路由&lt;/h2&gt;
&lt;p&gt;只有一个接口，我们可以写在&lt;code&gt;index.ts&lt;/code&gt;里，那如果有一堆接口呢，肯定要进行分组的&lt;/p&gt;
&lt;p&gt;Hono中路由分组也比较简单，只要再用一次&lt;code&gt;new Hono()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
// user模块
const user = new Hono()

user.get(&apos;/list&apos;, (c) =&gt; c.text(&apos;List users&apos;)) // GET /user
user.get(&apos;/:id&apos;, (c) =&gt; {
  // GET /user/:id
  const id = c.req.param(&apos;id&apos;)
  return c.text(&apos;Get user: &apos; + id)
})
user.post(&apos;/&apos;, (c) =&gt; c.text(&apos;Create user&apos;)) // POST /user

// indext.ts
const app = new Hono()

// 使用user路由组
app.route(&apos;/user&apos;, user)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，要想给路由分组，只需要在src下再新建一个user文件夹，里面实现user的路由，在从&lt;code&gt;index.ts&lt;/code&gt;里使用&lt;code&gt;app.route(&apos;/user&apos;, user)&lt;/code&gt;就可以了。&lt;/p&gt;
&lt;p&gt;由于我们使用了&lt;code&gt;basePath&lt;/code&gt;，所以此时的user接口为&lt;code&gt;/api/user/list&lt;/code&gt; 、&lt;code&gt;/api/user/:id&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们在使用公司后端接口或者其他网站的开放接口接口时，经常会看到这样的结构&lt;code&gt;/api/v1/user/a/b/c&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在Hono中可以这样实现&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;user.get(&apos;/list&apos;, (c) =&gt; c.text(&apos;我是 user/list&apos;))
v1.route(&apos;/user&apos;, user)
app.route(&apos;/v1&apos;, v1)

export default app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会这样响应&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/v1/user/list ---&gt; `我是 user/list`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，如果上述代码中，route注册的顺序出错，则不会正常响应&lt;/p&gt;
&lt;h2&gt;错误捕捉&lt;/h2&gt;
&lt;p&gt;当发生一些致命错误时，为了不让服务挂掉，我们需要catch住，并且返回给前端一些友好的提示。不然我们那些年骂过的xx后端就变成了自己。&lt;/p&gt;
&lt;p&gt;在Hono中使用也很简单，不需要自己单独写个中间件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { HTTPException } from &apos;hono/http-exception&apos;

// ...

app.onError((err, c) =&gt; {
  // 任何请求， http status 返回200， 错误码在返回体自定义
  const status = 200;
  // 记录原始的错误， 返回给前端的是友好的信息
  // TODO Logger
  const errorCode = 40001
  const errorMsg = &apos;不是我的错，想想前端的问题！&apos;
  if (err instanceof HTTPException) {
    errorCode = ErrorCode.UNAUTHORIZED
  }

  const response = {
    code: errorCode,
    data: null,
    message: errorMsg,
  };
  return c.json(response, status)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，在后端有任何错误的时候，我们都会以http status 200的状态码返回，并且可以在返回体中定义好固定的结果，并且返回出一个自定义的&lt;code&gt;errorCode&lt;/code&gt;，外加一个友好的前端能看得懂的&lt;code&gt;errorMsg&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;随着项目的复杂度增加，可以把这个handler单独拆分出去，已达到精简入口文件的目的。&lt;/p&gt;
&lt;p&gt;比如新建一个&lt;code&gt;common&lt;/code&gt; 文件夹，里面写一个&lt;code&gt;errorHandler.ts&lt;/code&gt;，在&lt;code&gt;index.ts&lt;/code&gt;中或者在user模块&lt;code&gt;src/user/index.ts&lt;/code&gt;中，都可以分别使用&lt;code&gt;app.onError()&lt;/code&gt; 和  &lt;code&gt;user.onError()&lt;/code&gt;具体处理通用的或者是自定义的错误处理逻辑！&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章是一个入门篇，主要目的是讲述一下Node和Bun的区别，以及使用Bun+Hono的一个入门项目。&lt;/p&gt;
&lt;p&gt;路由分组、错误捕捉这些功能很简单的就可以实现了，因为篇幅原因，我就把其他功能拆分成多篇教程了。后续教程会涉及：数据库、响应标准化、日志、jwt鉴权、docker/docker-compose打包部署等等，是一个完整闭环的小项目，代码也会开源分享出来，感兴趣的可以关注起来~&lt;/p&gt;
&lt;p&gt;欢迎点赞催更👍&lt;/p&gt;</content:encoded></item><item><title><![CDATA[【Hono】部署篇 Docker+pm2部署]]></title><link>https://zzao.club/post/hono/hono-docker-pm2</link><guid isPermaLink="true">https://zzao.club/post/hono/hono-docker-pm2</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;项目开发完后，要部署到服务器上才能正常使用，这里我分享一下&lt;code&gt;docker&lt;/code&gt;和&lt;code&gt;pm2&lt;/code&gt;两种方式的部署。&lt;/p&gt;
&lt;p&gt;以下经验来自于**我日常的折腾中，适合入门，非企业级操作。**仅用于分享！&lt;/p&gt;
&lt;h2&gt;Docker部署&lt;/h2&gt;
&lt;p&gt;用&lt;code&gt;docker&lt;/code&gt;的话，也算有两种运行方式。&lt;/p&gt;
&lt;p&gt;先在本地&lt;code&gt;build&lt;/code&gt;，然后打&lt;code&gt;tag&lt;/code&gt;，然后&lt;code&gt;push&lt;/code&gt;上去。&lt;/p&gt;
&lt;p&gt;把包&lt;strong&gt;发布到&lt;code&gt;dockerhub&lt;/code&gt;或你的对应的云服务商的容器镜像服务&lt;/strong&gt;上（比如阿里云）后&lt;/p&gt;
&lt;p&gt;一种是使用&lt;code&gt;docker run&lt;/code&gt; 附带一些参数进行启动&lt;/p&gt;
&lt;p&gt;另一种使用&lt;code&gt;docker-compose.yml&lt;/code&gt;配置参数，用&lt;code&gt;docker compose&lt;/code&gt;命令重新拉取最新的镜像并重启。&lt;/p&gt;
&lt;h2&gt;Dockerfile&lt;/h2&gt;
&lt;p&gt;这个dockerfile来自Bun的官方，然后我又加了一些自己的需求，可以做一下参考&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app

# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev &amp;#x26;&amp;#x26; bun install --frozen-lockfile

# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod &amp;#x26;&amp;#x26; bun install --frozen-lockfile --production

# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .

# [optional] tests &amp;#x26; build
ENV NODE_ENV=production
# RUN bun test
RUN bun run build

# copy production dependencies and source code into final image
FROM base AS release

# 一个后端node/bun服务运行,必须需要node_modules
# 以及打包后的入口
# 如果存在环境变量配置, 还需要复制环境变量配置文件
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/out/index.js .
COPY --from=prerelease /usr/src/app/package.json .
COPY --from=prerelease /usr/src/app/.env .
COPY --from=prerelease /usr/src/app/.env.production .

# 此处是为了把日志和数据库持久化, 方便备份和查看
# 创建日志目录和数据库目录
RUN mkdir -p /usr/src/app/prod-logs &amp;#x26;&amp;#x26; chmod -R 777 /usr/src/app/prod-logs
RUN mkdir -p /usr/src/app/db &amp;#x26;&amp;#x26; chmod -R 777 /usr/src/app/db

ENV NODE_ENV=production
# run the app
USER bun
EXPOSE 4775/tcp
ENTRYPOINT [ &quot;bun&quot;, &quot;run&quot;, &quot;index.js&quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有几个&lt;code&gt;dockerfile&lt;/code&gt;内需要注意的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果你有env文件，记得也把env复制进去&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果你的日志等文件持久化了（放在了一个自定义的目录下），记得&lt;strong&gt;分配读写权限&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记得设置ENV NODE_ENV=xxx&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分阶段构建不是必须的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，&lt;code&gt;dockerfile&lt;/code&gt;中用到了&lt;code&gt;bun run build&lt;/code&gt;这个命令&lt;/p&gt;
&lt;p&gt;要注意，&lt;code&gt;bun build&lt;/code&gt; 需要指定 &lt;code&gt;--target&lt;/code&gt; 为 &lt;code&gt;bun&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;bun run --hot src/index.ts&quot;,
    &quot;build&quot;: &quot;bun build src/index.ts --outdir out --target=bun&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写完&lt;code&gt;dockerfile&lt;/code&gt;以及打包命令后，没必要去服务器验证，可以在本地验证。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker build ./ -t your_name --load
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包后，直接在&lt;code&gt;docker desktop&lt;/code&gt;中点击运行，输入你自定义的端口号、本地挂载路径等就可以了。&lt;/p&gt;
&lt;p&gt;如果有错误的话，就直接在desktop看日志，解决完明显的问题后再往服务器上发。&lt;/p&gt;
&lt;p&gt;但是如果很多类似：端口号、挂载地址、环境变量需要配置时，光在开始这一步就会感觉非常麻烦。所以还是配一个&lt;code&gt;docker compose&lt;/code&gt;比较方便&lt;/p&gt;
&lt;h2&gt;Docker Compose&lt;/h2&gt;
&lt;p&gt;小水管服务器最好还是不要在服务器上跑**&lt;code&gt;docker build&lt;/code&gt;** ，真是很容易就卡死。&lt;/p&gt;
&lt;p&gt;在项目根目录下新建&lt;code&gt;docker-compose&lt;/code&gt;配置文件&lt;/p&gt;
&lt;p&gt;docker-compose.prod.yml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &apos;3.8&apos;
# 正式服务器打包
services:
  hono:
    container_name: &apos;hono&apos;
    # 填写发布后的镜像
    image: &apos;xxxxxxx&apos;
    ports:
      - &apos;4775:4775&apos;
    restart: on-failure
    volumes:
      - /home/db/hono:/usr/src/app/db
      - /home/log/hono:/usr/src/app/prod-logs

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在配置文件内写好所有的配置，很直观。&lt;/p&gt;
&lt;p&gt;如果是在本地的话可以再新建一个&lt;/p&gt;
&lt;p&gt;docker-compose.dev.yml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &apos;3.8&apos;
# 开发环境测试打包
services:
  hono:
    container_name: &apos;hono&apos;
    build:
      context: ./
      dockerfile: ./Dockerfile
    ports:
      - &apos;4775:4775&apos;
    restart: on-failure
    volumes:
      - /Users/your_name/your_dir/databases/hono-db:/usr/src/app/db
      - /Users/your_name/your_dir/databases/hono-logs:/usr/src/app/prod-logs

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用本地的dockerfile来打包验证&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就不用重复敲很多配置了。&lt;/p&gt;
&lt;p&gt;测试没问题，可以正常运行后，就可以去发布了&lt;/p&gt;
&lt;h2&gt;发布到容器镜像服务&lt;/h2&gt;
&lt;p&gt;在各家的云服务商的后台，都可以找到各自的容器镜像服务。&lt;/p&gt;
&lt;p&gt;因为&lt;code&gt;dockhub&lt;/code&gt;被墙了，直接在大陆的服务器上是拉不下来&lt;code&gt;dockhub&lt;/code&gt;的镜像的，所以要想分版本的发布，就需要用云服务商自己的容器镜像服务。&lt;/p&gt;
&lt;p&gt;以下以阿里云为例。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;登录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo docker login --username=&amp;#x3C;your username&gt; registry.cn-beijing.aliyuncs.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：后面的地址&lt;code&gt;registry.cn-beijing.aliyuncs.com&lt;/code&gt;，可以在你的阿里云后台找到&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;打包&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;打包的时候也有一个坑，就是要注意服务器的&lt;code&gt;platform&lt;/code&gt;，在本地打包和push前就要加上参数。 比如我这里是&lt;code&gt;linux/amd64&lt;/code&gt;。（你可以先不管，等在服务器上运行时，会提示你报错信息，然后再根据提示加上&lt;code&gt;platform&lt;/code&gt;参数重新打包发布也可以。）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker buildx build ./ -t your_name --load --platform linux/amd64 (对应阿里云ubuntu服务器)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;打Tag&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;打标签前，需要在阿里云控制台里，先创建好自己的命名空间，假设我这里是&lt;code&gt;zzstudio1&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker tag memoz registry.cn-beijing.aliyuncs.com/zzstudi1/your_name:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;发布&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker push --platform linux/amd64 registry.cn-beijing.aliyuncs.com/zzstudi1/your_name:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;此时本地操作已经完成，登录到云服务器再去拉取和运行发布好的镜像&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;不得不说，有墙是真难受，万一有网络问题，还容易卡住。&lt;/p&gt;
&lt;p&gt;发布成功后，再去回头看我们&lt;code&gt;docker-compose.prod.yml&lt;/code&gt;的配置的&lt;code&gt;image&lt;/code&gt;，就是此时发布的镜像了。&lt;/p&gt;
&lt;p&gt;只要这个&lt;code&gt;docker-compose.prod.yml&lt;/code&gt;文件在服务器上，就可以运行起来了。后续如果需要更新，则需要在本地重复上述的，打包、打Tag、发布流程&lt;/p&gt;
&lt;p&gt;然后在服务器重启即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 先停止
docker-compose down 
docker-compose up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;code&gt;docker compose&lt;/code&gt;是可以管理多个镜像的，如果你要同时使用&lt;code&gt;redis&lt;/code&gt;、&lt;code&gt;mysql&lt;/code&gt;、&lt;code&gt;nginx&lt;/code&gt;等，都可以统一管理，详细使用可以看我这篇**&lt;a href=&quot;https://mp.weixin.qq.com/s/UNxFJvZNrZyCnDKFQyApsQ&quot;&gt;《使用Docker Compose部署Nest应用》&lt;/a&gt;**，里面有详细的配置说明&lt;/p&gt;
&lt;h2&gt;小结一下&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;配置&lt;code&gt;package.json&lt;/code&gt;中的&lt;code&gt;build&lt;/code&gt;命令，注意&lt;code&gt;--taget=bun&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;参考bun官方示例，编写&lt;code&gt;Dockerfile&lt;/code&gt;，构建，然后把项目打上版本号，打包发布到指定平台&lt;/li&gt;
&lt;li&gt;服务器上只装&lt;code&gt;docker&lt;/code&gt;，服务都使用&lt;code&gt;docker&lt;/code&gt;运行&lt;/li&gt;
&lt;li&gt;编写多个&lt;code&gt;docker-compose.*.yml&lt;/code&gt;文件，配置不同环境的策略，拉取指定平台指定版本的镜像&lt;/li&gt;
&lt;li&gt;后续使用&lt;code&gt;docker compose&lt;/code&gt;命令进行更新和重新运行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;再来看一下pm2部署有什么不同&lt;/p&gt;
&lt;h2&gt;PM2部署&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;pm2&lt;/code&gt;运行项目，需要在服务器安装相应的环境（&lt;code&gt;Bun&lt;/code&gt;、&lt;code&gt;Pm2&lt;/code&gt;、&lt;code&gt;Git&lt;/code&gt;），后续有其他服务也使用&lt;code&gt;pm2&lt;/code&gt;进行管理就可以了。&lt;/p&gt;
&lt;p&gt;就是不像docker似的，环境隔离起来，换服务器方便一些。&lt;/p&gt;
&lt;p&gt;但是国内这个环境其实用&lt;code&gt;docker&lt;/code&gt;还麻烦一些，如果能一股脑丝滑的发步到&lt;code&gt;dockerhub&lt;/code&gt;上就会轻松很多。&lt;/p&gt;
&lt;p&gt;所以我一般如果选择了&lt;code&gt;pm2&lt;/code&gt;部署，就相当于在服务器上安装了一圈环境（包括&lt;code&gt;Nginx&lt;/code&gt;），配置过一遍后，后续使用&lt;code&gt;git&lt;/code&gt;提交代码，提交代码后再通过&lt;code&gt;ssh&lt;/code&gt;在服务器打包和重启一下对应服务。&lt;/p&gt;
&lt;p&gt;以下是一些基本命令，安装node、bun、nginx等，且只在&lt;code&gt;Ubuntu&lt;/code&gt;验证过。&lt;/p&gt;
&lt;p&gt;安装node20.x版本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用apt安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt install nodejs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后验证&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装&lt;code&gt;bun&lt;/code&gt;，先安装&lt;code&gt;unzip&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt install unzip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再安装&lt;code&gt;bun&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl -fsSL https://bun.sh/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置国内镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm config set registry https://registry.npmmirror.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装PM2&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install pm2@latest -g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;ecosystem.config.js&lt;/code&gt; （在项目根目录下新建）（内容非常简单，在官网复制就可以）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pm2 start ecosystem.config.js

pm2 stop ecosystem.config.js

pm2 reload ecosystem.config.js

pm2 restart ecosystem.config.js

pm2 delete ecosystem.config.js


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装Nginx&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt install nginx

systemctl status nginx
systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置防火墙&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo ufw allow &apos;Nginx HTTP&apos;
sudo ufw allow &apos;Nginx HTTPS&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主配置文件：&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;站点配置文件目录：&lt;strong&gt;/etc/nginx/sites-available&lt;/strong&gt; 和 &lt;strong&gt;/etc/nginx/sites-enabled&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;测试nginx配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo nginx -t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启nginx&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;strong&gt;git建裸库&lt;/strong&gt;，大家可以自行搜索相关文章，我也是按之前搜出来的一篇文章来建的，如果有懒得搜的，也可以直接&lt;strong&gt;私信&lt;/strong&gt;我要教程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终，你的服务器上的项目文件夹内要具备：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;node_modules 是的，后端项目不同于前端，也是需要node_modules才能正常运行的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;package.json 用于&lt;strong&gt;安装和更新node_modules&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;env&lt;/code&gt;文件环境变量配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打包且压缩后的JS文件/文件夹，如：server/index.js&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时在你的服务器环境下运行就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;pm2 start ecosystem.config.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而关于重启、多实例运行等都在一个&lt;code&gt;config.js&lt;/code&gt;里配置&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;启动后，验证项目是否可以被正常请求，数据库读写是否正常，日常是否在正常记录就可以了&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;对于我来说，选择其中一种方式部署，意味着后续其他服务都用这一种方式进行部署。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果使用了&lt;code&gt;docker&lt;/code&gt;，那我只在服务器上把&lt;code&gt;docker&lt;/code&gt;装好，所有服务都通过&lt;code&gt;docker&lt;/code&gt;来打包、分版本、发布、拉取、运行、重启。&lt;/p&gt;
&lt;p&gt;如果使用了&lt;code&gt;pm2&lt;/code&gt;，我会在服务器上直接装一遍&lt;code&gt;Node&lt;/code&gt;、&lt;code&gt;Bun&lt;/code&gt;、&lt;code&gt;Git&lt;/code&gt;、&lt;code&gt;Nginx&lt;/code&gt;、&lt;code&gt;pm2&lt;/code&gt;，后续通过&lt;code&gt;git&lt;/code&gt;管理代码，通过&lt;code&gt;git tag/branch&lt;/code&gt;或是代码内管理版本，ssh工具在服务器打包代码pm2来运行和重启。&lt;/p&gt;
&lt;p&gt;当然有很多其他运维工具&lt;code&gt;gitea&lt;/code&gt;、&lt;code&gt;jenkins&lt;/code&gt;等等，我看过后感觉都不太适合我这种&lt;strong&gt;处于探索而非运营阶段&lt;/strong&gt;的小水管服务器折腾。&lt;/p&gt;
&lt;p&gt;如果真的有业务，着急搭建，我觉得花点钱找个靠谱的人弄都行，不用费这种心。&lt;/p&gt;
&lt;p&gt;以上就是全部内容啦👏👏&lt;/p&gt;</content:encoded></item><item><title><![CDATA[【Hono】优化：提取配置项及公共函数]]></title><description><![CDATA[项目完成的七七八八了，代码也慢慢多了起来，有些基本的优化工作必须要做了。]]></description><link>https://zzao.club/post/hono/hono-feat-config-common-utils</link><guid isPermaLink="true">https://zzao.club/post/hono/hono-feat-config-common-utils</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;/h1&gt;
&lt;p&gt;项目完成的七七八八了，代码也慢慢多了起来，有些基本的优化工作必须要做了。&lt;/p&gt;
&lt;p&gt;不然等再加一些业务逻辑，就会变得非常臃肿，然后就又免不了被抛弃的命运。&lt;/p&gt;
&lt;p&gt;写自己的玩具就是这样的，总是在不停的造玩具。&lt;/p&gt;
&lt;h2&gt;优化点1：ErrorHandler&lt;/h2&gt;
&lt;p&gt;目前的目录是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;.
├── Dockerfile
├── README.md
├── bun.lockb
├── bunfig.toml
├── db
│   └── zzaoclub.db
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── docker-compose.yml
├── logs
├── out
│   └── index.js
├── package.json
├── src
│   ├── common
│   ├── database
│   ├── index.ts
│   ├── salt
│   └── user
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前只关注&lt;code&gt;src&lt;/code&gt;下的结构即可。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;index.ts&lt;/code&gt;作为入口文件，加载了一些全局中间价，以及去挂载子路由，拦截全局的&lt;code&gt;error&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里能抽出去一个&lt;code&gt;errorHandler&lt;/code&gt;，我把它放在&lt;code&gt;common&lt;/code&gt;下。&lt;/p&gt;
&lt;p&gt;它的功能也很简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;拦截到错误后，把&lt;strong&gt;Http状态码&lt;/strong&gt;设置为&lt;code&gt;200&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在控制台/日志文件中打印/记录请求时间+方式+url+原始错误信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在上下文中使用&lt;code&gt;c.get(&apos;errCode&apos;)&lt;/code&gt;取出在某处抛出的自定义错误，然后把&lt;strong&gt;自定义的错误码和错误信息或默认的错误信息&lt;/strong&gt;返回给前端&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统一返回标准如:&lt;code&gt;{ code: 40001, data: xxx, msg: &apos;1123131&apos; }&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;拆好后重新引入测试，然后就已经出现了下一个优化点&lt;/p&gt;
&lt;h2&gt;优化点2：ErrorCode&lt;/h2&gt;
&lt;p&gt;刚才拆处errorHandler后，发现会取一些默认的错误信息，以及有一套自定义的错误嘛。&lt;/p&gt;
&lt;p&gt;所以要提前定义好一套，不然随手就写个&lt;code&gt;40001&lt;/code&gt;、&lt;code&gt;40002&lt;/code&gt;、&lt;code&gt;40003&lt;/code&gt;，时间长了鬼知道这是什么意思。&lt;/p&gt;
&lt;p&gt;所以再从&lt;code&gt;common&lt;/code&gt;下新建一个&lt;code&gt;errorCode.ts&lt;/code&gt; ，我只是举个例子，错误码和信息要自己看着来。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const ErrorCode = {
  PERMISSION_DENIED: 40001, // 权限问题
  VALIDATION_ERROR: 40002, // 参数问题
  UNAUTHORIZED: 40003, // 未登录
  LOGIN_EXPIRED: 40004, // 登录已过期
  NOT_FOUND: 40005, // 不存在的接口
  INTERNAL_SERVER_ERROR: 50000, // 服务器内部错误
  UNKOWN_ERROR: 50001 // 未知错误
}

export const ErrorCodeMsg = {
  [ErrorCode.PERMISSION_DENIED]: &apos;权限问题&apos;,
  [ErrorCode.VALIDATION_ERROR]: &apos;参数问题&apos;,
  [ErrorCode.UNAUTHORIZED]: &apos;未登录&apos;,
  [ErrorCode.LOGIN_EXPIRED]: &apos;登录已过期&apos;,
  [ErrorCode.NOT_FOUND]: &apos;不存在的接口&apos;,
  [ErrorCode.INTERNAL_SERVER_ERROR]: &apos;服务器内部错误&apos;,
  [ErrorCode.UNKOWN_ERROR]: &apos;未知错误&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在某个路由抛出错误时，应该是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt; c.set(&apos;errCode&apos;, ErrorCode.UNAUTHORIZED)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后接下来再去看看已有逻辑里，哪里需要抛出错误，把这些自定义的错误码给用上。&lt;/p&gt;
&lt;h2&gt;优化点3：JWT相关&lt;/h2&gt;
&lt;p&gt;刚才在替换错误码时，发现JWT和Zod的相关逻辑里需要抛出一些异常。而有了两个模块后，也会发现他们有一些重复代码。&lt;/p&gt;
&lt;p&gt;比如&lt;code&gt;user&lt;/code&gt;中的&lt;strong&gt;jwt中间件&lt;/strong&gt;和&lt;code&gt;salt&lt;/code&gt;模块中是一样的，没必要写两份，可以把jwt中间件的逻辑抽出，放在&lt;code&gt;index.ts&lt;/code&gt;中，目前来看，jwt只是校验一下用户有没有登录，以及跳过一些&lt;code&gt;路由白名单&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以可以先把jwt逻辑抽出来，以下是一个参考&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const JWT_SECRET = Bun.env.JWT_SECRET || &apos;&apos;
const jwtMiddware = jwt({
  secret: JWT_SECRET,
})

salt.use(&apos;/*&apos;, async (c, next) =&gt; {
  if (NoAuthPaths.includes(c.req.path)) {
    await next();
    return;
  }
  await jwtMiddware(c, async () =&gt; {
    const user = c.get(&apos;jwtPayload&apos;)
    
    if (!user) {
      c.set(&apos;errMsg&apos;, &apos;用户未登录&apos;)
      c.set(&apos;errCode&apos;, ErrorCode.UNAUTHORIZED)
      throw new HTTPException(401)
    }

    if (user.id !== 1) {
      c.set(&apos;errMsg&apos;, &apos;用户无权限&apos;)
      c.set(&apos;errCode&apos;, ErrorCode.PERMISSION_DENIED)
      throw new HTTPException(401)
    }
    await next()
  })

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把后面函数单独抽离出去即可，同时我在每个模块下如&lt;code&gt;src/user&lt;/code&gt;、&lt;code&gt;src/salt&lt;/code&gt;下再放一个&lt;code&gt;common.ts&lt;/code&gt;，把路由白名单（NoAuthPaths）放进去，每个模块在使用jwt中间件时，再把这个白名单传进去。&lt;/p&gt;
&lt;h2&gt;优化点4：Zod相关&lt;/h2&gt;
&lt;p&gt;看到路由的参数校验那一串，就知道不得不优化一下，因为不可能每个接口直接写那么一大串schame。&lt;/p&gt;
&lt;p&gt;抽离分两部分，一部分是validator函数，一部分是schame的定义。&lt;/p&gt;
&lt;p&gt;先来validator，在&lt;code&gt;common&lt;/code&gt;下再新建个&lt;code&gt;validator.ts&lt;/code&gt;，封装一个zvalidator函数，传参就按zValidator需要什么就行，主要是为了抛出错误。&lt;/p&gt;
&lt;p&gt;以下也只是个示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { zValidator } from &quot;@hono/zod-validator&quot;
import { Context, Next } from &quot;hono&quot;
import { ErrorCode, ErrorCodeMsg } from &quot;./errorCode&quot;;
import { HTTPException } from &quot;hono/http-exception&quot;;
// 自定义校验, 在校验失败时抛出异常, 由errorHanlder统一处理
export const zvalidator = (source: any, schema: any) =&gt; {
  return zValidator(source, schema, (result, c: Context) =&gt; {
    if (!result.success) {
      const errMsg = result.error.errors.map((e: any) =&gt; `field:${e.path[0]} - ${e.message}`).join(&apos;, &apos;)
      c.set(&apos;errMsg&apos;, errMsg)
      c.set(&apos;errCode&apos;, ErrorCode.VALIDATION_ERROR)
      throw new HTTPException(400, { message: errMsg })
    }
  })
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再把schame抽出去，在&lt;strong&gt;模块目录&lt;/strong&gt;下新建一个&lt;code&gt;schame.ts&lt;/code&gt;，因为这块不是公共的，是每个模块每个接口都有可能不一样。就类似路由白名单也样，我也没把它放在&lt;code&gt;common&lt;/code&gt;下，而是都放在了&lt;strong&gt;模块目录下。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 对象
export const userSchema = z.object({
  name: z.string(),
  desc: z.string().optional().default(&apos;&apos;),
  desc2: z.string().default(&apos;&apos;),
})

// 列表
export const usersSchema = z.array(userSchema)

// 修改
export const sauceUpdateSchema = userSchema.extend({
  id: z.string().min(1)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我目前用的语法就这几个，一个普通对象，一个数据对象，一个继承方法用来抽离一些公共的schame。&lt;/p&gt;
&lt;p&gt;抽出一部分逻辑后，目录现在是这样的，清晰了很多，目前除了要完善具体逻辑，应该没有目录上的改动了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;.
├── Dockerfile
├── README.md
├── bun.lockb
├── bunfig.toml
├── db
│   └── zzaoclub.db
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── docker-compose.yml
├── logs
├── out
│   └── index.js
├── package.json
├── src
│   ├── common
│   │   ├── errorCode.ts
│   │   ├── logger.ts
│   │   ├── responseFormatter.ts
│   │   └── validator.ts
│   ├── database
│   │   └── sqlite.ts
│   ├── index.ts
│   ├── salt
│   │   ├── config.ts
│   │   ├── crud.ts
│   │   ├── index.ts
│   │   ├── readme.md
│   │   └── schema.ts
│   └── user
│       ├── crud.ts
│       ├── index.ts
│       └── schema.ts
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优化点5： Env&lt;/h2&gt;
&lt;p&gt;在开发项目时，可能会随手写一些变量，这些变量在开发和正式环境下是肯定不一样的，所以我要把它放在&lt;code&gt;.env&lt;/code&gt;中，以便后续部署后也能正常使用。&lt;/p&gt;
&lt;p&gt;最典型的就是&lt;code&gt;winston&lt;/code&gt;，本地日志文件的路径和正式服务器上分别配置好&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.env.production&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;LOG_DIR=/usr/src/app/prod-logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.env.development&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;LOG_DIR=logs2/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在&lt;code&gt;common/logger.ts&lt;/code&gt;中替换成对应的&lt;code&gt;env变量&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const LOG_DIR = Bun.env.LOG_DIR || &apos;logs/&apos;;
...
transports: [
    new DailyRotateFile({
            filename: path.join(LOG_DIR, &apos;info-%DATE%.log&apos;),
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面等开始部署时，再来验证配置是否生效。&lt;/p&gt;
&lt;p&gt;然后&lt;code&gt;index.ts&lt;/code&gt;中的&lt;code&gt;port&lt;/code&gt;，也可以配置一下，开发和正式没必要一样，尤其是选择开源的话。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  port: Bun.env.PORT,
  fetch: app.fetch,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就是JWT的&lt;code&gt;secret&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const JWT_SECRET = Bun.env.JWT_SECRET || &apos;1234567&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改并替换好后，还要记得在&lt;code&gt;.gitignore&lt;/code&gt;中把&lt;code&gt;.env.production&lt;/code&gt;加上，就不要提交到仓库里去了。&lt;/p&gt;
&lt;p&gt;还有日志文件，也没必要提交上去&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然代码没多少，但是基本的封装和目录划分还是要提前思考一下。&lt;/p&gt;
&lt;p&gt;总体的思路就是：&lt;/p&gt;
&lt;p&gt;一、整个项目公用的提取到&lt;code&gt;src/common&lt;/code&gt;，每个模块公用的放在&lt;code&gt;src/模块/&lt;/code&gt;下就近管理，不出现重复代码&lt;/p&gt;
&lt;p&gt;二、代码中不要出现不清不楚的常量，每个常量要定义好语意化的枚举、Map等变量引入的方式使用&lt;/p&gt;
&lt;p&gt;三、区分环境的配置进一步提取到&lt;code&gt;env&lt;/code&gt;文件或其他配置中心服务&lt;/p&gt;
&lt;p&gt;ok，就这样。 以后的问题碰到再解决即可（比如现在TS用了很多&lt;code&gt;any&lt;/code&gt;）。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[【Hono】Gitea+Bun+Hono+Pm2 自动化部署后记]]></title><link>https://zzao.club/post/hono/hono-gitea-bun-hono-pm2-auto-action</link><guid isPermaLink="true">https://zzao.club/post/hono/hono-gitea-bun-hono-pm2-auto-action</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文记录一下&lt;code&gt;bun&lt;/code&gt;项目在使用&lt;code&gt;pm2&lt;/code&gt;部署时产生的一点问题。&lt;/p&gt;
&lt;p&gt;部署流程：使用&lt;code&gt;Gitea&lt;/code&gt;自建git仓库，编写workflow，on[push]，自动拉取代码=&gt;下载依赖=&gt;打包=&gt;移动到目标文件夹=&gt;启动/重启 &lt;code&gt;pm2 stop/start ecosystem.config.cjs&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;云服务器（新加坡）：debian 12  2h4g&lt;br&gt;
bun: 1.1.31&lt;br&gt;
pm2: 5.4.2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是一个4G内存的机器，所以我上了gitea，利用gitea的&lt;code&gt;act_runner&lt;/code&gt;自动部署。&lt;/p&gt;
&lt;p&gt;2G的服务器我实测打包Nuxt项目会爆内存，刚好&lt;strong&gt;峰值内存要吃2G出头。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;项目本身使用&lt;code&gt;honojs&lt;/code&gt;，有几个简单接口，只有登录注册业务。因为已经用其他项目把&lt;code&gt;act_runner&lt;/code&gt;调通了，所以这个项目也很顺利的运行成功。整个gitea actions运行时间在&lt;code&gt;1s&lt;/code&gt;左右&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是pm2启动后显示error，没有错误日志!!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;actions是已经成功跑完的，所以生产环境需要的文件已经被移动到了目标文件夹内。&lt;/p&gt;
&lt;p&gt;尝试在项目根目录直接使用&lt;code&gt;bun run index.js&lt;/code&gt;后，发现有打不开&lt;code&gt;.env.production&lt;/code&gt;里配置的目录的情况，原来是没正确加载上&lt;code&gt;NODE_ENV&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;于是又查阅了一番pm2文档、bun文档，发现需要给&lt;code&gt;pm2 start&lt;/code&gt;加上&lt;code&gt;--env production&lt;/code&gt;参数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pm2 start ecosystem.config.cjs --env production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上后，重新提交代码，自动部署，发现还是一样，显示error但没有日志&lt;/p&gt;
&lt;p&gt;再次在项目根目录直接使用&lt;code&gt;NODE_ENV=production bun run index.js&lt;/code&gt;，发现可以正常运行，那估计就是pm2有什么问题影响了，但单看配置文件是没有写错的，还是不知道什么原因无法用pm2启动&lt;/p&gt;
&lt;p&gt;于是又不用配置文件启动，尝试直接&lt;code&gt;pm2 start index.js --interpreter bun&lt;/code&gt; 跑一下试试，这时候发现可以了，而且mode很明显是&lt;code&gt;fork&lt;/code&gt;，而刚才我配置&lt;code&gt;ecosystem.config.js&lt;/code&gt;时用的是&lt;code&gt;cluster&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;bun官网也只是提了一下要配置一下&lt;code&gt;interpreter&lt;/code&gt;为&lt;code&gt;~/.bun/bin/bun&lt;/code&gt;。无奈只能先这样处理，等后续解决后再来水一篇！&lt;/p&gt;
&lt;p&gt;👇下面是配置参考&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;module.exports = {
  apps: [
    {
      name: &apos;your_app_name&apos;,
      port: &apos;5577&apos;,
      // 运行bun项目时，先设置为fork模式
      exec_mode: &apos;fork&apos;,
      // instances: &apos;max&apos;,
      script: &apos;index.js&apos;,
      interpreter: &apos;/root/.bun/bin/bun&apos;,
      env: {
        NODE_ENV: &apos;production&apos;
      },
      env_production: {
        NODE_ENV: &apos;production&apos;
      }
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;.gitea/workflow/build.yaml 这个完全和github一致，熟悉github actions的可以略过了&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions  
on: [push]
jobs:
  Explore-Gitea-Actions:
    runs-on: debian
    steps:
      - run: echo &quot;  The job was automatically triggered by a ${{ gitea.event_name }} event.&quot;
      - run: echo &quot;  This job is now running on a ${{ runner.os }} server hosted by Gitea!&quot;
      - run: echo &quot;  The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}.&quot;
      # 把代码checkout到一个临时的文件夹中
      - name: Check out repository code
        uses: actions/checkout@v3
      # 下载依赖，因为我要在服务器打包代码
      - name: Install dependencies
        run: bun install
      # 打包代码
      - name: Build Project
        run: npm run build
      # 打包后的代码，移动到指定的文件夹内
      # 为了测试配置文件，我把两个配置文件都copy过去了，实际只用到了.env.production
      - name: Copy Files
        run: |
          mkdir -p /root/a/b
          cp -R out/* /root/a/b/
          cp -R ecosystem.config.cjs /root/a/b/ecosystem.config.cjs
          cp -R package.json /root/a/b/package.json
          cp -R .env.production /root/a/b/.env.production
          cp -R .env /root/a/b/.env
      # 下载依赖
      - name: Install dependencies
        run: |
          cd /root/a/b
          bun install
     # 使用pm2启动项目 --env production
      - name: Deploy to production2
        run: |
          cd /root/a/b
          pm2 stop ecosystem.config.cjs
          pm2 start ecosystem.config.cjs --env production
      - run: echo &quot;  The ${{ gitea.repository }} repository has been cloned to the runner.&quot;
      - run: echo &quot; ️ The workflow is now ready to test your code on the runner.&quot;
      - name: List files in the repository
        run: |
          ls ${{ gitea.workspace }}          
      - run: echo &quot;  This job&apos;s status is ${{ job.status }}.&quot;
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[【Hono】完善：参数校验+响应标准化]]></title><link>https://zzao.club/post/hono/hono-params-check-response-standardized</link><guid isPermaLink="true">https://zzao.club/post/hono/hono-params-check-response-standardized</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一章我们完成了基于&lt;code&gt;Hono&lt;/code&gt;的web项目的搭建工作，并实现了路由分组，错误处理等逻辑。&lt;/p&gt;
&lt;p&gt;这一章来继续完善项目，让它变的健壮起来💪。&lt;/p&gt;
&lt;h2&gt;参数校验&lt;/h2&gt;
&lt;p&gt;平时我们写前端的时候，最希望后端把校验做的越全越好，提示信息越详细越友好越好，那现在就轮到我们自己实现后端了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hono&lt;/code&gt; 官方比较推荐的是&lt;code&gt;zod&lt;/code&gt;作为校验库，并提供了&lt;code&gt;@hono/zod-validator&lt;/code&gt;，封装了一下中间件，让我们可以直接放在请求路径后面用。&lt;/p&gt;
&lt;p&gt;并且官方并不推荐路由第二个参数再写个 handler 封装，然后再传进来&lt;/p&gt;
&lt;p&gt;这种写法在其他node框架中十分常见，如Koa、Nest&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 🙁
// A RoR-like Controller
const booksList = (c: Context) =&gt; {
  return c.json(&apos;list books&apos;)
}

app.get(&apos;/books&apos;, booksList)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是官方推荐的写法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 😃
app.get(&apos;/books/:id&apos;, (c) =&gt; {
  const id = c.req.param(&apos;id&apos;) // Can infer the path param
  return c.json(`get ${id}`)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述写法的原因和类型有关，如果不写复杂的泛型，就无法在Controller中推断出路径参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这样也好，在业务真正复杂前来之前，保持程序的简洁。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回到参数校验部分，&lt;code&gt;@hono/zod-validator&lt;/code&gt;使用比较简单，就是在路由第二个参数插入这个中间件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { zValidator } from &apos;@hono/zod-validator&apos;

const route = app.post(
  &apos;/posts&apos;,
  zValidator(
    &apos;form&apos;,
    z.object({
      body: z.string(),
    })
  ),
  (c) =&gt; {
    const validated = c.req.valid(&apos;form&apos;)
    // ... use your validated data
  }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要多个验证器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.post(
  &apos;/posts/:id&apos;,
  validator(&apos;param&apos;, ...),
  validator(&apos;query&apos;, ...),
  validator(&apos;json&apos;, ...),
  (c) =&gt; {
    //...
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入校验后，来使用 apifox 测试一下，可以看到 zod 返回如下的校验结果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;success&quot;: false,
    &quot;error&quot;: {
        &quot;issues&quot;: [
            {
                &quot;code&quot;: &quot;invalid_type&quot;,
                &quot;expected&quot;: &quot;number&quot;,
                &quot;received&quot;: &quot;undefined&quot;,
                &quot;path&quot;: [
                    &quot;page&quot;
                ],
                &quot;message&quot;: &quot;Required&quot;
            },
            {
                &quot;code&quot;: &quot;invalid_type&quot;,
                &quot;expected&quot;: &quot;number&quot;,
                &quot;received&quot;: &quot;undefined&quot;,
                &quot;path&quot;: [
                    &quot;size&quot;
                ],
                &quot;message&quot;: &quot;Required&quot;
            }
        ],
        &quot;name&quot;: &quot;ZodError&quot;
    },
    &quot;_error&quot;: {
        &quot;issues&quot;: [
            {
                &quot;code&quot;: &quot;invalid_type&quot;,
                &quot;expected&quot;: &quot;number&quot;,
                &quot;received&quot;: &quot;undefined&quot;,
                &quot;path&quot;: [
                    &quot;page&quot;
                ],
                &quot;message&quot;: &quot;Required&quot;
            },
            {
                &quot;code&quot;: &quot;invalid_type&quot;,
                &quot;expected&quot;: &quot;number&quot;,
                &quot;received&quot;: &quot;undefined&quot;,
                &quot;path&quot;: [
                    &quot;size&quot;
                ],
                &quot;message&quot;: &quot;Required&quot;
            }
        ],
        &quot;name&quot;: &quot;ZodError&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到返回了详细的校验信息，但美中不足的就是这个中间件&lt;strong&gt;会以自己的结构直接返回&lt;/strong&gt;给前端，这显然不合理，我们要的是标准化的返回。&lt;/p&gt;
&lt;p&gt;所以这个中间件还可以&lt;strong&gt;传入第三个参数作为回调&lt;/strong&gt;，然后自己手动抛出错误&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;zValidator(source, schema, (result, c: Context) =&gt; {
    if (!result.success) {
      const errMsg = result.error.errors.map((e: any) =&gt; `field:${e.path[0]} - ${e.message}`).join(&apos;, &apos;)
      throw new HTTPException(400, { message: errMsg })
    }
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抛出错误后我们在 &lt;code&gt;errorHandler&lt;/code&gt; 中就可以接收到错误信息了，再经过处理一下，返回固定的格式（此处代码只是演示）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const errorHandler = async (err: Error, c: Context) =&gt; {
  // 错误处理
  const errorMsg = &quot;出错了&quot;
  if (err instanceof HTTPException) {
     return err.getResponse()
  }
  const response = {
    code: 50001,
    data: null,
    message: errorMsg,
  };
  return c.json(response, status)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在&lt;code&gt;errorHandler&lt;/code&gt; 已经处理了好几种错误：&lt;strong&gt;jwt、zod、系统错误&lt;/strong&gt;等等&lt;/p&gt;
&lt;p&gt;我们总不能每次想起一种错误来，就来这个写个 &lt;code&gt;if else&lt;/code&gt; 处理一下，所以我们可以定义一组通用的 &lt;code&gt;errorCode&lt;/code&gt;和&lt;code&gt;errorMsg&lt;/code&gt; map结构，并且让每个抛出错误的&lt;strong&gt;中间件把相关信息写入到上下文中&lt;/strong&gt;，&lt;strong&gt;由于上下文仅在当前的请求链路有效&lt;/strong&gt;，所以也不用担心污染。&lt;/p&gt;
&lt;p&gt;在上下文中传递信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.use(async (c, next) =&gt; {
  c.set(&apos;message&apos;, &apos;Hono is cool!!&apos;)
  await next()
})

app.get(&apos;/&apos;, (c) =&gt; {
  const message = c.get(&apos;message&apos;)
  return c.text(`The message is &quot;${message}&quot;`)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在errorHandler中就可以这样接受错误信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const errorHandler = async (err: Error, c: Context) =&gt; {
  // 错误处理
  // 任何请求， http status 返回200， 错误码在返回体自定义
  const status = 200;
  // TODO 记录原始的错误， 返回给前端的是友好的信息
  // 从上下文拿错误码, 优先取自定义的msg =&gt; 错误码对应信息  =&gt; 未知错误
  let errorCode = c.get(&apos;errCode&apos;)
  if (!errorCode) {
    // 抛出了HTTPException， 视为权限不错
    if (err instanceof HTTPException) {
      errorCode = ErrorCode.UNAUTHORIZED
    }
  }
  let errorMsg = c.get(&apos;errMsg&apos;) || ErrorCodeMsg[errorCode] || ErrorCodeMsg[ErrorCode.UNKOWN_ERROR]

  const response = {
    code: errorCode || ErrorCode.UNKOWN_ERROR,
    data: null,
    message: errorMsg,
  };
  return c.json(response, status)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样每个抛出错误的中间价，可以写入详细的错误信息，而一组自定义的 &lt;code&gt;errorcode&lt;/code&gt; 也可以应付更多的业务场景，如果增加了一个场景，我们**只需要去map结构中再加一组key-value，**如果没有自定义错误信息，则使用 &lt;code&gt;code&lt;/code&gt; 对应的默认 &lt;code&gt;msg&lt;/code&gt; 进行返回。&lt;/p&gt;
&lt;p&gt;抛出错误时，自定义错误信息。 这一块可以进行一个封装，因为每个接口都要写这么一大串，明显不合理，所以&lt;strong&gt;提取到公共的文件夹下面去&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 封装自定义的zvalidator
export const zvalidator = (source: any, schema: any) =&gt; {
  return zValidator(source, schema, (result, c: Context) =&gt; {
    if (!result.success) {
      const errMsg = result.error.errors.map((e: any) =&gt; `field:${e.path[0]} - ${e.message}`).join(&apos;, &apos;)
      c.set(&apos;errMsg&apos;, errMsg)
      c.set(&apos;errCode&apos;, ErrorCode.VALIDATION_ERROR)
      throw new HTTPException(400, { message: errMsg })
    }
  })
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;响应标准化&lt;/h2&gt;
&lt;p&gt;完成了参数的校验，并顺着问题一步步封装了关于错误信息的处理。接下来就开始让接口能正常的返回数据了。&lt;/p&gt;
&lt;p&gt;然而虽然错误信息我们已经标准化，但正常的返回&lt;strong&gt;不方便用中间件直接去拦截。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因是 &lt;code&gt;c.json&lt;/code&gt; 直接就是一个 &lt;code&gt;Response&lt;/code&gt;，虽然会走到我们的全局中间件里去，但没法再二次加工 &lt;code&gt;Response&lt;/code&gt; 了，官方给出了一个例子，可以把 &lt;code&gt;res&lt;/code&gt; 设置为&lt;code&gt;undefined&lt;/code&gt;，然后重新 &lt;code&gt;new Response&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我觉得破坏性太大了，也不优雅，所以&lt;strong&gt;暂时没找到&lt;/strong&gt;类似 &lt;code&gt;nest&lt;/code&gt; 那样，在 &lt;code&gt;response&lt;/code&gt; 之后拦截的钩子。&lt;/p&gt;
&lt;p&gt;但这都是小问题，以后有看到更好的处理方式再进行优化就行，这里我们就简单的封装一下对象，塞到 c.json 中返回就好了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const standardRes = (data: any) =&gt; {
  return {
    code: 200,
    data,
    message: &apos;success&apos;
  }
}
// case
user.post(&quot;/list&quot;, zvalidator(&apos;json&apos;, pageSchema), (c) =&gt; {
  const params = c.req.valid(&apos;json&apos;)
  // do something
  const list = userModal.getList(params)
  return c.json(standardRes(list))
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有细节没优化不是什么大问题，重要的是把流程先打通&lt;/p&gt;
&lt;h2&gt;JWT TOKEN&lt;/h2&gt;
&lt;p&gt;现在路由也分组了，错误也捕捉了，正常响应也处理了，参数也进行了校验。那就到了接口权限这一步上。&lt;/p&gt;
&lt;p&gt;虽然你的网站可能只需要给用户展示信息，但有时也需要一个平台去写入数据，修改数据或删除数据才行。&lt;/p&gt;
&lt;p&gt;这种敏感操作不可能让普通用户去做，一般都是管理员，甚至只有自己去操作，所以才需要一个登录操作，以便确认用户身份。&lt;/p&gt;
&lt;p&gt;而登录操作，是为了拿到一个令牌，好让这个用户在后续的操作中畅通无阻。这里我们是用 jwt 来给用户发放令牌，jwt 的使用 hono 官方也有说明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;具体流程就是：用户登录 - 拿到令牌 - 后续操作携带令牌 - 校验令牌是否有效 - 有效就允许用户继续操作 - 无效则返回相关错误信息 - 前端提示用户或引导进入登录页&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而我们的用户有很多个，所以一般 jwt 的 &lt;strong&gt;payload 会和用户信息挂钩&lt;/strong&gt;，每个登录的用户通过 &lt;code&gt;sign&lt;/code&gt; 拿到一个 &lt;code&gt;token&lt;/code&gt;，并在后续操作中把 &lt;code&gt;token&lt;/code&gt; 放在 &lt;code&gt;header&lt;/code&gt; 中，接口则是在中间件中通过 &lt;code&gt;c.get(&apos;jwtPayload&apos;)&lt;/code&gt;拿到令牌中包含的用户信息，去进行相关的校验。比如数据库中有无此用户，此用户的权限等级够不够等情况，如果校验不通过就 &lt;code&gt;throw new HTTPException(code)&lt;/code&gt;， 并把 &lt;code&gt;errorMsg&lt;/code&gt; 写入上下文 让 &lt;code&gt;errorHanlder&lt;/code&gt; 去处理。校验通过则继续后续的业务逻辑。&lt;/p&gt;
&lt;p&gt;这里我演示一个登录接口，来生成token&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;user.post(&quot;/login&quot;, async (c) =&gt; {
  const user = { id: 1, name: &apos;zzao.club&apos; };  // 假设这是经过验证的用户信息
  const payload = {
    id: user.id,
    exp: Math.floor(Date.now() / 1000) + 60 * 60, // 检查令牌不会过期 in 60 minutes
  }
  const token = await sign(payload, JWT_SECRET);
  console.log(`token`, token)
  return c.json(standardRes(token));
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中如果 &lt;code&gt;exp&lt;/code&gt;在payload中，则jwt会检查token是否过期了，payload还可以传入其他参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nbf&lt;/code&gt; : 检查token在指定时间之前没有被使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;iat&lt;/code&gt; : 检查token没有使用未来的时间进行签发。意思是，设置一个未来时间使自己的token一直有效（I guess） （The token is checked to ensure it is not issued in the future.）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我只使用了&lt;code&gt;exp&lt;/code&gt;设置token 60min后过期就可以了。&lt;/p&gt;
&lt;p&gt;当然，还有一种需求。&lt;/p&gt;
&lt;p&gt;作为一个用户，我每天都在你的网站上使用，不想隔几天登录就失效，还要重新登录。&lt;/p&gt;
&lt;p&gt;所以我们还可以再设置 &lt;code&gt;refresh  token&lt;/code&gt;，这个 &lt;code&gt;token&lt;/code&gt; 专门用来更新 &lt;code&gt;access token&lt;/code&gt; （也就是上边例子里的token）的有效期。比如 &lt;code&gt;refresh token&lt;/code&gt; 3 天过期，&lt;code&gt;access token&lt;/code&gt; 7 天过期，在 &lt;code&gt;refresh token&lt;/code&gt; 过期时，前端就调用一个&lt;strong&gt;刷新 token 的接口去生成一个新的&lt;/strong&gt;&lt;code&gt;access token&lt;/code&gt; ，前端拿到新token后再在之后的请求中带上新的token即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这样用户登录过一次后，只要平时在一直使用，就可以一直保持登录状态。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面是一个为user模块使用jwt中间件的中间件case&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { jwt } from &apos;hono/jwt&apos;

const jwtMiddware = jwt({
  secret: &apos;your secret!!!!&apos;,
})

user.use(&apos;/*&apos;, async (c, next) =&gt; {
  // 检查当前请求路径是否在排除列表中
  if (NoAuthPaths.includes(c.req.path)) {
    await next();
    return;
  }
  // 如果不在排除列表中，则进行JWT验证
  await jwtMiddware(c, async () =&gt; {
    const user = c.get(&apos;jwtPayload&apos;)
    // 获取payload中的user信息
    // 这些信息由登录接口提供
    if (!user) {
      c.set(&apos;errMsg&apos;, &apos;用户未登录&apos;)
      c.set(&apos;errCode&apos;, ErrorCode.UNAUTHORIZED)
      throw new HTTPException(401)
    }

    if (user.id !== 1) {
      c.set(&apos;errMsg&apos;, &apos;用户无权限&apos;)
      c.set(&apos;errCode&apos;, ErrorCode.PERMISSION_DENIED)
      throw new HTTPException(401)
    }

    await next()
  })

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;const user = c.get(&apos;jwtPayload&apos;)&lt;/code&gt; 这个写法也是官方文档中的写法，也是把&lt;code&gt;jwtPayload&lt;/code&gt;写入到了上下文中，然后在后续的中间件中就可以拿到这个&lt;code&gt;payload&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;每个模块的中间件处理逻辑可能相同也可能不同，后续我们再看情况， 把它抽离到根路由下或者写成一个单独的中间件，需要的模块自己去引入。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;目前对项目模块进行了分组，如：用户，商品等等。 每个模块可以写自己的&lt;code&gt;errorHandler&lt;/code&gt;，也可以去设置自定义中间件。而像 &lt;code&gt;csrf&lt;/code&gt; &lt;code&gt;cors&lt;/code&gt;等共同的中间件则放在根路由下&lt;/p&gt;
&lt;p&gt;针对接口能否被请求，使用了&lt;code&gt;hono/jwt&lt;/code&gt;，并为了让某些接口跳过校验以及不通过时返回自定义的错误信息，又封装装了一层。&lt;/p&gt;
&lt;p&gt;接口可以请求之后，来到参数校验的中间件&lt;code&gt;@hono/zod-validator&lt;/code&gt;，由于每个接口schema可能比较多，以及为了让&lt;code&gt;errorHandler&lt;/code&gt;来处理zod校验不通过的情况，又自定义了一个中间件在内部抛出错误。schema则是被提取到公共的文件夹（如&lt;code&gt;common&lt;/code&gt;）下&lt;/p&gt;
&lt;p&gt;请求成功时，使用一个&lt;code&gt;standardRes&lt;/code&gt;函数简单包装一下，统一返回值。&lt;/p&gt;
&lt;p&gt;请求失败时，在上下文中使用&lt;code&gt;c.set/get&lt;/code&gt;注入错误信息，在&lt;code&gt;errorHandler&lt;/code&gt;中间件中取出错误信息，并返回和成功时一致的json结构。&lt;/p&gt;
&lt;p&gt;这样看起来就又完善了一些~~&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下一章为日志、数据库操作、配置文件相关逻辑&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**欢迎点赞催更(¯▽¯)**👍&lt;/p&gt;</content:encoded></item><item><title><![CDATA[【Hono】完善：使用sqlite数据库及基于winston的日志持久化]]></title><link>https://zzao.club/post/hono/hono-sqlite-winston</link><guid isPermaLink="true">https://zzao.club/post/hono/hono-sqlite-winston</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前面两章完成了项目搭建、路由分组、参数校验、响应标准化、错误处理等功能点。&lt;/p&gt;
&lt;p&gt;这一章来继续完善项目，并且为整个项目画一个图辅助理解。&lt;/p&gt;
&lt;h2&gt;使用Bun作为数据库&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;hono&lt;/code&gt;项目中使用&lt;code&gt;sqlite&lt;/code&gt;作为数据库十分简单，因为bun自带了sqlite模块！&lt;/p&gt;
&lt;p&gt;用的时候都不需要去&lt;code&gt;install&lt;/code&gt;了，直接在文件中引入即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;import { Database } from &apos;bun:sqlite&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在简单翻阅文档后，发现可用的api似乎也比较简单，所以简单封装一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/database/sqlite.ts

const sqlite: Database = new Database(&quot;db/zzaoclub.db&quot;, { create: true, strict: true });

// 查询单条数据
export const query = (sql: SQL) =&gt; {
  return sqlite.query(sql);
};

// 查询列表数据
export const queryAll = (sql: SQL) =&gt; {
  return sqlite.query(sql).all();
};

// 执行新增、删除、修改等不需要返回值的操作
export const run = (sql: SQL, params: any[] = []) =&gt; {
  return sqlite.run(sql, params);
};
// run 会返回以下
// {
//   lastInsertRowid: 0,
//   changes: 0,
// }
export default sqlite;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简简单单的，甚至觉得哪里不对劲。&lt;/p&gt;
&lt;p&gt;然后使用时，再去具体模块内定义查询的&lt;code&gt;SQL语句&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;由于也没有看到有什么推荐的&lt;code&gt;orm&lt;/code&gt;，这里我决定体验一下&lt;code&gt;原生SQL&lt;/code&gt;来写好了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/user/crud.ts

import { queryAll } from &apos;../database/sqlite&apos;;

// 比如一个简单的用户列表查询
export function getUserList() {
  return queryAll(&apos;SELECT * FROM users&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在路由处使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/user/index.ts

user.post(&quot;/list&quot;, zvalidator(&apos;json&apos;, userSchema), (c) =&gt; {
  // 校验后的params
  const params = c.req.valid(&apos;json&apos;)
  const users = userModal.getUserList()
  return c.json({ data: users })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就结束了。🤔&lt;/p&gt;
&lt;p&gt;或者可以像bun的文档中这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const query = db.query(&quot;SELECT * FROM foo WHERE bar = $bar&quot;);
const results = query.all({
  $bar: &quot;bar&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我觉得倒不如直接把一条语句写好传进来了。&lt;/p&gt;
&lt;p&gt;so，只要再跟着AI学习一下基础的CRUD操作就可以了，我觉得对于实现简单应用来说不难。&lt;/p&gt;
&lt;p&gt;当然，表结构不会自动创建，我们还是要写一个sql文件，比如&lt;code&gt;init.sql&lt;/code&gt;，让程序在初始化时运行。&lt;/p&gt;
&lt;p&gt;或者借助一些sqlite工具来生成SQL语句&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;--- 用户表
CREATE TABLE IF NOT EXISTS user (
	&quot;id&quot;	INTEGER NOT NULL UNIQUE,
	&quot;username&quot;	TEXT NOT NULL DEFAULT &apos;&apos;,
	&quot;nickname&quot;	TEXT NOT NULL DEFAULT &apos;&apos;,
	&quot;phone&quot;	TEXT NOT NULL DEFAULT &apos;&apos;,
	&quot;role&quot;	TEXT NOT NULL CHECK(role in (&apos;admin&apos;,&apos;user&apos;,&apos;vendor&apos;)),
	&quot;password_hash&quot;	TEXT NOT NULL DEFAULT &apos;&apos;,
	&quot;avatar_url&quot;	TEXT NOT NULL DEFAULT &apos;&apos;,
	PRIMARY KEY(&quot;id&quot; AUTOINCREMENT)
);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再打通上述接口后，就可以拿&lt;code&gt;Apifox&lt;/code&gt;等工具再去请求一下，看看能否拿到自己插入的数据。&lt;/p&gt;
&lt;p&gt;具体的SQL语句我就不展示了，一般让AI写出来一试，也不会有什么问题。&lt;/p&gt;
&lt;h2&gt;日志系统&lt;/h2&gt;
&lt;p&gt;日志我们使用winston这个库来实现，也是不管用什么框架都可以使用的&lt;/p&gt;
&lt;p&gt;首先&lt;code&gt;install&lt;/code&gt;，这里需要搭配另一个插件，实现日志按天分割，以及配置日志保存方式、保存时长等等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;bun add winston winston-daily-rotate-file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;winston.createLogger()&lt;/code&gt;来创建一个中间件，然后配置两个输出的log文件，一个用来存储日常所有的日志，一个单独存储错误日志，且都是按天分割文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// LOG_DIR 需要自行配置

const logger = winston.createLogger({
  level: &apos;info&apos;,
  format: winston.format.combine(
    winston.format.colorize(), // 添加颜色化格式化器
    winston.format.timestamp({
      format: &apos;YYYY-MM-DD HH:mm:ss&apos;
    }), // 时间日期格式
    winston.format.printf(({ timestamp, level, message }: any) =&gt; {
      return `${timestamp} ${level}: ${message}`;
    }) // 打印格式
  ),
  transports: [
    new DailyRotateFile({
      level: &apos;info&apos;,
      filename: path.join(LOG_DIR, &apos;info-%DATE%.log&apos;),
      datePattern: &apos;YYYY-MM-DD&apos;,
      zippedArchive: true,
      maxSize: &apos;10m&apos;, // 文件大小
      maxFiles: &apos;7d&apos;, // 保留日志文件7天
      format: winston.format.combine(
        winston.format.uncolorize(), // 去除颜色 不去除无法toUpperCase
        winston.format.printf(({ timestamp, level, message }: any) =&gt; {
          return `${timestamp} ${level.toUpperCase()}: ${message}`;
        })
      ),
    }),
    // 错误日志文件传输配置, 生产环境建议放在别的目录下, 和开发环境区分开
    new DailyRotateFile({
      level: &apos;error&apos;,
      filename: path.join(LOG_DIR, &apos;err-%DATE%.log&apos;),
      datePattern: &apos;YYYY-MM-DD&apos;,
      zippedArchive: true,
      maxSize: &apos;10m&apos;,
      maxFiles: &apos;30d&apos;, // 保留错误日志文件30天
      format: winston.format.combine(
        winston.format.uncolorize(), // 去除颜色
        winston.format.printf(({ timestamp, level, message }: any) =&gt; {
          return `${timestamp} ${level.toUpperCase()}: ${message}`;
        })
      ),
    })
  ]
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置项很好理解，也可以很方便的实验，所以就不过多赘述了。&lt;/p&gt;
&lt;p&gt;然后去添加一个全局的中间价，打印一下请求信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/index.ts
// 这里myLogger就是winston创建的logger
api.use(&apos;*&apos;, async (c, next) =&gt; {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  // 使用 winston 创建的 logger 记录每个请求的详细信息
  myLogger.info(`[${c.req.method}] ${c.req.path} - ${c.res.status} - ${ms}ms`);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再去使用浏览器或&lt;code&gt;Apifox&lt;/code&gt;尝试请求一下，看看效果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;2024-09-19 16:35:37 error: [GET] http://localhost:4775/api/user - no authorization included in request
2024-09-19 16:35:37 info: [GET] /api/user - 200 - 6ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能看到正常打印信息就可以了&lt;/p&gt;
&lt;p&gt;然后再检查一下我们配置好的保存日志文件的目录下（注意目录要创建好，并且程序具备读写权限），会生成出 &lt;code&gt;err-2024-09-10.log&lt;/code&gt; 以及&lt;code&gt;info-2024-09-10.log&lt;/code&gt;两个日志文件即可。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这一章主要是对项目的补充，借助&lt;code&gt;bun:sqlite&lt;/code&gt;可以很方便的使用sqlite数据库，而且不像Mysql那样还需要安装和启动数据库服务，省时省力，开箱即用。&lt;/p&gt;
&lt;p&gt;针对项目运行中方便排查可能出现的问题，加入了&lt;code&gt;winston&lt;/code&gt;来让日志写入到文件中，同时日志文件可以限制大小，限制保存时长，按日期分割等等。&lt;/p&gt;
&lt;p&gt;目前整个项目的结构已经比较清晰了，最后的&lt;strong&gt;环境配置&lt;/strong&gt;放在下一节的优化里来一起写，最后再补一个图来梳理一下整个流程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191443812.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[使用 Layers 扩展你的 Nuxt4 应用]]></title><link>https://zzao.club/post/nuxt/nuxt4-use-layers</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt4-use-layers</guid><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;面对一个臃肿的页面或项目，你会如何简化重构、扩展它？&lt;/h2&gt;
&lt;p&gt;当单个 Vue 文件中界面/业务足够多时，通常我们会把它拆分成多个 &lt;code&gt;components&lt;/code&gt; 或 &lt;code&gt;composables&lt;/code&gt; 来引入，以此来减少此文件复杂度和增加可维护性。&lt;/p&gt;
&lt;p&gt;当一个项目的界面/业务逻辑足够多时，我们会在全局抽离一部分组件和逻辑，在不同的业务逻辑中复用。或者把和业务解耦的组件或工具函数甚至固定业务逻辑发布为 &lt;code&gt;npm&lt;/code&gt; 包来使用，进一步减少项目中的代码复杂度。&lt;/p&gt;
&lt;p&gt;当存在多个项目，而项目之间又存在相同业务时，我们可能会把具有完整功能的一部分逻辑抽离成独立的项目。同时多个项目的 UI组件库和通用逻辑层也发布为 npm 包，供所有项目下载使用以保持统一的视觉效果和减少重复代码的编写。这样当一个新的项目出现，需要利用其他已有的业务逻辑或者是完全复用其他项目的页面和功能时，我们可以使用 iframe 或沙箱来加载这个独立的项目。这样多团队可以维护自己的业务项目，同时又能避免开发重复的功能。&lt;/p&gt;
&lt;p&gt;那么在 Nuxt 里的如何优雅的解决这些问题呢？&lt;/p&gt;
&lt;p&gt;除了上述的任何框架的项目都能操作的方式，Nuxt 还有自己独特的扩展方式：&lt;code&gt;Layer&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Layer 可以解决什么问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://nuxt.com/docs/getting-started/layers&quot;&gt;Layer&lt;/a&gt; 几乎就是一个完整的 Nuxt 项目。&lt;/p&gt;
&lt;p&gt;开发了一个 Layer，意味着 Layer 中的组件、函数、接口、依赖包等都会继承了此 Layer 的项目应用。&lt;/p&gt;
&lt;p&gt;它不是一个最新版本才有的功能，但官方的文档的介绍十分简单，&lt;strong&gt;可能是因为它还在不断的完善中，官方也不希望你用于生产环境中。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个 Nuxt4 应用的最简目录结构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;
├── README.md

├── **app**

│   ├── app.config.ts

│   ├── app.vue

│   └── **pages**

│       └── index.vue

├── nuxt.config.ts

├── package.json

├── pnpm-lock.yaml

├── **public**

│   ├── favicon.ico

│   └── robots.txt

├── **server**

│   └── tsconfig.json

└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而一个 Layer 模板的目录结构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;├── .editorconfig

├── .gitignore

├── .npmrc

├── .nuxtrc

├── .playground

│   ├── app.config.ts

│   └── nuxt.config.ts

├── README.md

├── app.config.ts

├── app.vue

├── components

│   └── HelloWorld.vue

├── eslint.config.js

├── nuxt.config.ts

├── package.json

├── pnpm-lock.yaml

└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，&lt;strong&gt;会用 Nuxt 就会写 Layer，无需引入其他负担即可扩展&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;开发一个 &lt;code&gt;Nuxt4&lt;/code&gt; 应用时，我们会在 app 目录下开发各种 &lt;code&gt;pages&lt;/code&gt;、&lt;code&gt;components&lt;/code&gt;、&lt;code&gt;composables&lt;/code&gt;、&lt;code&gt;utils&lt;/code&gt;，会在 &lt;code&gt;app.config.ts&lt;/code&gt; 里设置一些全局配置项。&lt;/p&gt;
&lt;p&gt;需要接口支持时，可以在 &lt;code&gt;server&lt;/code&gt; 目录下借助 &lt;code&gt;Nitro&lt;/code&gt; 的生态来完成后端服务。&lt;/p&gt;
&lt;p&gt;但是当项目开始变得臃肿时，Layer 可以发挥什么作用呢。&lt;/p&gt;
&lt;p&gt;场景1：我的网站有一个登录注册权限校验相关的逻辑，这肯定会涉及到一些页面、组件、结构，比如登录页、注册页、登录按钮、登录注册接口等等。&lt;/p&gt;
&lt;p&gt;一开始我没打算把它们抽离出去，因为网站还很小，并且只有一个。但是我很清楚这一部分功能和组件是可以被复用的。只要我再创建一个 nuxt 应用，我可以肯定我会来这个项目里复制一遍代码而不是重新敲一遍。&lt;/p&gt;
&lt;p&gt;由于既涉及到前端组件又涉及到后端接口，以及一部分公共函数。我难道要把 Vue 的组件发布成一个 npm 包，把公共函数发布一个 npm 包，再把这部分后端接口重新起一个服务吗？&lt;/p&gt;
&lt;p&gt;在一个全栈框架里这样做显然是有点繁琐。&lt;/p&gt;
&lt;p&gt;假设 2：有一个付费网站，但它有一部分是免费的，付费内容也是分 vip、svip，如果全都写在一个项目里的话当然可以实现，毕竟用户也不关心你代码是怎么写的，只要能提供稳定的服务就可以了。&lt;/p&gt;
&lt;p&gt;如果两个、三个网站呢？&lt;/p&gt;
&lt;p&gt;如果是卖给客户源码呢？&lt;/p&gt;
&lt;p&gt;如果是把免费内容开源，付费内容闭源，或是后续再把 vip 的逻辑开源呢？&lt;/p&gt;
&lt;p&gt;假设 3：有一个基于 Nuxt 的开源博客站，如何设计一套机制，可以让其他用户用上自己喜欢的主题呢，毕竟博客最重要的就是换皮。&lt;/p&gt;
&lt;h2&gt;针对不同场景的解决方案&lt;/h2&gt;
&lt;p&gt;如果场景 1，前期 &lt;code&gt;Layer&lt;/code&gt; 可以直接在 &lt;code&gt;Nuxt4&lt;/code&gt; 应用里使用，像是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;app
	auth
		pages
		conmponents
		composables
		utils
		server
		nuxt.config.ts
	components
	composables
	utils
	app.config.ts
nuxt.config.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;Nuxt3&lt;/code&gt; 中，这个 &lt;code&gt;auth&lt;/code&gt; 可以放在 &lt;code&gt;～/layers&lt;/code&gt; 中&lt;br&gt;
&lt;code&gt;Nuxt3.12.0&lt;/code&gt; 以后的版本都会自动注册&lt;code&gt;Layers&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 &lt;code&gt;app&lt;/code&gt; 目录下，可以新建一个 &lt;code&gt;{layer_name}&lt;/code&gt; 目录，目录内的结构和 Nuxt3 是一致的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nuxt.config.ts&lt;/code&gt; 中只需要 extend 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;extend: [ &apos;app/auth&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们把权限校验相关的组件、接口、utils 都挪到了一个 &lt;code&gt;auth layer&lt;/code&gt; 下管理。&lt;strong&gt;如果业务没能继续发展，一个仓库非常好管理，同时按文件夹划分后，结构十分清晰。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样可能感受还不是很强烈。&lt;/p&gt;
&lt;p&gt;因为只是目录结构上的分层。这一层相关的依赖 &lt;code&gt;nuxt/auth&lt;/code&gt; 还是安装在了主项目内，但 &lt;code&gt;nuxt-auth&lt;/code&gt; 已经是在 &lt;code&gt;app/auth/nuxt.config.ts&lt;/code&gt; 里了，要知道，如果你用了一些 &lt;code&gt;nuxt modules&lt;/code&gt; 之后，&lt;code&gt;nuxt.config.ts&lt;/code&gt; 很容易变成一大坨！因为他们都是在这一个文件内配置。&lt;/p&gt;
&lt;p&gt;但 Layer 可以随时分离出去，因为它就是一个完整的 Nuxt 应用，所以&lt;strong&gt;迁移几乎没有额外成本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当决定把这一部分代码迁移出去时&lt;/p&gt;
&lt;p&gt;可以直接使用官方的 &lt;code&gt;layer&lt;/code&gt; 模板再新建一个项目 &lt;code&gt;zc-auth-layer&lt;/code&gt;，这个模板有一个好处，内置了 &lt;code&gt;.playground&lt;/code&gt; ，顾名思义，&lt;strong&gt;是一个用来模拟你的主项目的 Nuxt 应用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Layer&lt;/code&gt; 的实际内容（红框）都写在根目录 的123下，&lt;code&gt;playground&lt;/code&gt; （绿框）里用&lt;strong&gt;来做调试和开发&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;起初我没有用这个模板，而是单纯的新建一个 Nuxt 应用，但很快就发现了问题。&lt;code&gt;Pages&lt;/code&gt; 会直接把主项目的 &lt;code&gt;Pages&lt;/code&gt; 直接给覆盖掉，而且（暂时）还没发现如何忽略某个目录的覆盖，但这是一个显而易见的问题，应该会很快完善或者被我发现。&lt;/p&gt;
&lt;p&gt;而有了 &lt;code&gt;playground&lt;/code&gt; 之后，可以放心的里面写页面或是发展成一个独立的项目，又不影响外层供给其他项目继承。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202502051431526.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这就是一个普普通通的 Nuxt4  应用！&lt;/p&gt;
&lt;p&gt;而其他项目如果想使用它的仓库，只需要配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;extends: [ [&apos;github:aatrooox/zc-auth-layer&apos;, { install: true }] ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以它发布后的 npm 包&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 直接使用npm 包
extends: [ &apos;zc-auth-layer&apos; ]
// 某个组织下的包
extends: [ &apos;@zzaoclub/zc-auth-layer&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就像是在项目中使用其他 &lt;code&gt;components&lt;/code&gt; 、&lt;code&gt;server api&lt;/code&gt; 一样！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;甚至还拥有完整的自动导入和 TS 类型提示，这你能受得了吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;依赖关系是这样的，&lt;strong&gt;A extend B ， B extend C ，C 拥有最高的优先级，向下覆盖。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以哪怕是 &lt;code&gt;B Layer&lt;/code&gt; 中只包含了一部分你需要的接口、组件，你也可以很轻松的再 extend 一个 &lt;code&gt;D Layer&lt;/code&gt;，用来重写你不需要的逻辑。&lt;/p&gt;
&lt;p&gt;比如 B C 都是支付层，B 是支付宝支付，C 是微信支付。&lt;/p&gt;
&lt;p&gt;在主项目中使用 &lt;code&gt;/api/order/pay&lt;/code&gt; 接口。继承了 B 层时，调用此接口就是支付宝支付，继承 C 后就是微信支付。&lt;/p&gt;
&lt;p&gt;亦或是 B 的接口是 &lt;code&gt;/api/order/pay/zfb&lt;/code&gt; ，C 的接口是 &lt;code&gt;/api/order/pay/wx&lt;/code&gt;，主项目中的接口就可以用来自由控制使用哪种支付方式时调起对应的接口。&lt;/p&gt;
&lt;p&gt;对于自己的项目来说，这种分层方式，可以让你更加自由的控制代码开源程度以及自己多项目复用逻辑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于出售源码时，也只需要出售对应层的代码，组合起来即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对解决博客换主题的功能，更是不能再适合了，只要按开发的博客时使用的 Components，&lt;strong&gt;组件名一一对应就可以了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当然，如果实际应用到项目上，还有类似数据库、多环境等问题&lt;strong&gt;需要仔细斟酌&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但 Layer 的分层结构已经显而易见的更简单、更直接，开发体验也更好了。&lt;/p&gt;
&lt;h2&gt;潜在的问题&lt;/h2&gt;
&lt;p&gt;对于简单的前端项目，或是不涉及复杂的数据库架构，Layer 的方式无疑是更优雅的。&lt;/p&gt;
&lt;p&gt;既在文件结构上解耦了，同时继承（覆盖）时也很直接。&lt;/p&gt;
&lt;p&gt;但如果涉及到多个接口服务，或是多个数据库服务，甚至前端业务也比较复杂。还要是仔细测试其兼容性。&lt;/p&gt;
&lt;p&gt;毕竟虽然使用 &lt;code&gt;Layer&lt;/code&gt; 在文件结构上分开的，但实际打包后是在同一个 &lt;code&gt;Nuxt&lt;/code&gt; 服务里的，并不是类似微服务的那种形式。&lt;/p&gt;
&lt;p&gt;话说回来，大部分使用者应该只是看中了 SSR 能力，可能不会使用 Nitro 构建大型的应用，尤其是对于公司来说，也不太信任 Node 的服务能力。&lt;/p&gt;
&lt;p&gt;所以对于个人开发者来说，我觉得 &lt;code&gt;Layer&lt;/code&gt; 是个锦上添花的利器。因为大部分业务不会太复杂，而且偏前端更多一些。自己封装好的各种功能的 &lt;code&gt;Layer&lt;/code&gt; 信手拈来，开发效率大大提高，还不耽误二次开发。&lt;/p&gt;
&lt;p&gt;于是我在给自己的&lt;a href=&quot;https://blog.zzao.club&quot;&gt;博客&lt;/a&gt;尝试增加了一个权限层 &lt;a href=&quot;https://github.com/aatrooox/zc-auth-layer&quot;&gt;auth layer&lt;/a&gt; 后，才写下了这篇文章。&lt;/p&gt;
&lt;p&gt;期待后续 Layer 的大放异彩。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[2025 年的第 1 次复盘]]></title><description><![CDATA[从 2024/10/29 开始使用 Nuxt 的生态，掐指一算，已经有两个半月。]]></description><link>https://zzao.club/post/daily/2025-first-review</link><guid isPermaLink="true">https://zzao.club/post/daily/2025-first-review</guid><pubDate>Thu, 16 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从 2024/10/29 开始使用 Nuxt 的生态，掐指一算，已经有两个半月。&lt;/p&gt;
&lt;p&gt;开始做时没有细致的规划多长时间内开发多少内容，只有一个大概的方向：先是从博客入手，然后设计一个类似朋友圈动态功能，然后设计评论系统，最后再写一个点赞功能。&lt;/p&gt;
&lt;p&gt;陆陆续续提交了 300 多次代码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501171630255.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;就这样一直开发到感觉完善了 80% ～ 90% 左右的内容时，于是 1 月 10 号开始拆分功能。&lt;/p&gt;
&lt;h2&gt;一&lt;/h2&gt;
&lt;p&gt;在五天内拆出来两个仓库。&lt;/p&gt;
&lt;p&gt;一个是博客站 ，增强了文章的属性，默认的 md 文件都被解析为单篇的文章。而一个特殊配置的目录下可以解析成文档，就像是 vitepress 生成的文档类似，我起了个名叫：小册。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501171630257.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;开发下来，最大的阻碍就是 Nuxt 的生态教程太少，版本太落后。找资料比较费时间，有一些比较新的教程或知识点，都是在 &lt;code&gt;youtube&lt;/code&gt; 上，在 &lt;code&gt;github&lt;/code&gt; 里，还都是 Nuxt 生态的核心参与者自己出的教程或分享。&lt;/p&gt;
&lt;p&gt;所以我多发一些 &lt;code&gt;Nuxt&lt;/code&gt; 的内容应该是有一些意义的，然而这个意义取决于有多少人对 &lt;code&gt;Nuxt&lt;/code&gt; 感兴趣，至于以后能开什么花结什么果，只能过一段时间再回过头来看了。&lt;/p&gt;
&lt;p&gt;虽然可用教程少，但好处是外网发的内容准确度是很高。坏处还是我对它有所企图，生怕这个生态突然就嗝屁了，我的努力也付诸东流。&lt;/p&gt;
&lt;p&gt;毕竟大环境是面向 BOSS 编程，BOSS 招人要什么技术，我就用什么技术。我多少有点背道而驰了，以至于时不时焦虑一下，但好在定力还可以，热爱还是能盖过一切。&lt;/p&gt;
&lt;p&gt;另一个是 Nuxt 版的 &lt;code&gt;Flomo&lt;/code&gt;，我这么说应该比较好理解，毕竟 &lt;code&gt;Flomo&lt;/code&gt; 用的人多一些，我也起了个名叫 &lt;code&gt;Memoz&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501171630258.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;功能主要参考 &lt;code&gt;Flomo&lt;/code&gt; 和 &lt;code&gt;Memos&lt;/code&gt;，但也不能全抄，有些我自己用不到的或是太费时的，都不能下功夫开发，因为在没有用户的情况下，我更倾向于输出一些干货文章以及录一些视频，有了围观群众之后再细细优化。&lt;/p&gt;
&lt;p&gt;目前完成了 &lt;code&gt;30%&lt;/code&gt; 左右，完成了基本的 md 内容渲染，标签功能，热力图已经有了想法，还没实现。但已经把两个项目的源码放在了 &lt;code&gt;github&lt;/code&gt; 上，接下来也会多多公开编码。&lt;/p&gt;
&lt;p&gt;对于使用者（我）来说，最核心的问题还是在碎片化的记录之后如何搜索和归纳整理，为生成新内容所用。所以完成一个基础版本后，我就会先用起来，在实际的使用中看看有什么问题。&lt;/p&gt;
&lt;h2&gt;二&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;考虑到最可能的结果是大肆宣传也没什么人关注&lt;/strong&gt;，所以我需要尽快部署到更多的推广平台，混个眼熟。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;v2ex&lt;/code&gt; 也发了个帖子，&lt;code&gt;1770+&lt;/code&gt;点击，给博客站带来了&lt;code&gt;170+&lt;/code&gt;新用户的访问，挺不错的。虽然留不住几个人，但是我&lt;strong&gt;需要多多感受贴近流量的感觉&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;掘金&lt;/code&gt; 和 &lt;code&gt;CSDN&lt;/code&gt; 还是得继续发，虽然阅读量感人，但是也算是在中文互联网上的一个 SEO 途径。&lt;/p&gt;
&lt;p&gt;对比掘金、CSDN。公众号的大部分读者也不看技术相关的文章，还有的看了也就看了，没什么反馈。反正平台是不会给一个纯技术文大力推广的，除非在标题和封面上下功夫。&lt;/p&gt;
&lt;p&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;有些长期做能产生的价值，短期高质量的做也能产生相同的价值。&lt;/p&gt;
&lt;p&gt;所以一旦正在行动中，想法就会随着行动的推进发生变动。如果没有行动，就只能空谈自己认为的价值。&lt;/p&gt;
&lt;p&gt;说白了还是有点懒。&lt;/p&gt;
&lt;p&gt;但对比过去一年的话，这两个月也能称得上“稳中向好”了。&lt;/p&gt;
&lt;h2&gt;三&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1 - 2 个月复盘&lt;/strong&gt;要比&lt;strong&gt;年度复盘&lt;/strong&gt;要有用一些，可以迅速的调整状态。年度总结更多的表达对过去一年的遗憾以及对下一年的憧憬了。&lt;/p&gt;
&lt;p&gt;我也不知道我为什么要复盘，以前也没复盘过，就是突然停了下来，回想了一下最近做的事，然后库库一顿码字，有点神奇。&lt;/p&gt;
&lt;p&gt;接下来的要做的事，年前这几天应该是以整理文章、视频为主，每天有时间就开发一些新东西，毕竟快过年了，内心真的有点躁动了。&lt;/p&gt;
&lt;p&gt;年后先把 Memoz 猛推进一波，然后同时写点东西，录点视频。&lt;/p&gt;
&lt;p&gt;然后就开始开第三个坑： IMGX，一个极简、高效的内容美化工具，类似于生成卡片的 App 吧。&lt;/p&gt;
&lt;p&gt;在二月份如果顺利的话，后两个产品应该都能初步完成。不顺利的话，那估计就是连第二个都完成不了。&lt;/p&gt;
&lt;p&gt;反正不管开发什么产品，都有一些降级的收益：模板+文章+视频+教程。&lt;/p&gt;
&lt;p&gt;我隐隐感觉，要在 25 年第一季度（4～5月份）进行更高强度的输出来获取到一些正反馈，今年才有可能发生转机，不然最有希望的又是下一年了。&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:encoded></item><item><title><![CDATA[Nuxt3.15.2升级报告]]></title><description><![CDATA[Nuxt3.15.1 升级到 3.15.2 ，同时 nuxt/content@3.0.0-alpha.8 升级到了 3.0.0-alpha.9]]></description><link>https://zzao.club/post/nuxt/nuxt3.15.2-upgrade-report</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3.15.2-upgrade-report</guid><pubDate>Thu, 16 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nuxt3.15.1 升级到 3.15.2 ，同时 nuxt/content@3.0.0-alpha.8 升级到了 3.0.0-alpha.9&lt;/p&gt;
&lt;h2&gt;升级方式&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nuxi@latest upgrade --force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后 nuxt/content 会同步升级，慎重！&lt;/p&gt;
&lt;h2&gt;破坏性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;queryCollectionNavigation&lt;/strong&gt; 的查询结果发生了改变&lt;/p&gt;
&lt;p&gt;比如，我的这个collection定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;book: defineCollection({
    type: &apos;page&apos;,
    source: {
      include: &apos;book/**/*.md&apos;,
      exclude: [&apos;book/**/-*.md&apos;],
      repository: &apos;https://github.com/aatrooox/Blog&apos;,
      authToken: process.env.CONTENT_REPO_TOKEN
    },
    schema: z.object({
      date: z.date(),
      lastmod: z.date(),
      tags: z.array(z.string()),
      versions: z.array(z.string()),
    })
  }),

// book/nuxt-book1/install/demo.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;alpha.8 时， &lt;code&gt;queryCollectionNavigation&lt;/code&gt; 是不会包含最外层的目录的。数组直接以 &lt;code&gt;nuxt-book1&lt;/code&gt; 开始，现在会最顶层的 &lt;code&gt;book&lt;/code&gt; 也带上，导致我的博客小册相关的查询都要重构一下，这更新多少有点抽象了，不懂为什么改成这样的逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data: books } = await useAsyncData(&apos;navigation&apos;, () =&gt; {
  return queryCollectionNavigation(&apos;book&apos;, [&apos;date&apos;, &apos;path&apos;, &apos;id&apos;])
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以前 &lt;code&gt;books.value&lt;/code&gt;  就是 book 目录下的文件树，现在 &lt;code&gt;books&lt;/code&gt; 是包含了根节点的整棵树&lt;/p&gt;
&lt;p&gt;所以如何&lt;strong&gt;涉及到使用此导航树渲染的菜单都要改&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另外，由于查询默认带上了根节点，所以导致每个文件的 &lt;code&gt;path&lt;/code&gt; 也附带了完整的路径，从这个角度上看，这样改是合理的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果以前跳转时，自己拼接了前缀，也要去掉&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同时因为 &lt;code&gt;path&lt;/code&gt; 是完整的路径了，所以查询单个 &lt;code&gt;book&lt;/code&gt; 时, 直接用 &lt;code&gt;.path&lt;/code&gt; 就行了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data: page, error, refresh } = await useAsyncData(hash(route.path + &apos;page&apos;), () =&gt; {
  // 删掉前缀
  return queryCollection(&apos;book&apos;).path(route.path).first()
}, { watch: [route.query]})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上个版本应该是不行的，不然我应该不会用 id 查&lt;/p&gt;
&lt;p&gt;总之，查询单个md内容，直接用 &lt;code&gt;queryCollection().path()&lt;/code&gt; 就行&lt;/p&gt;
&lt;p&gt;而涉及到 &lt;code&gt;queryCollectionNavigation&lt;/code&gt;  的部分，总结就是不管怎么筛选和查询，他会始终附带根节点&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt Content v3 实现 RSS 订阅功能]]></title><description><![CDATA[Nuxt Content v3 实现 RSS 订阅功能]]></description><link>https://zzao.club/post/nuxt/nuxt-content-v3-rss-done</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt-content-v3-rss-done</guid><pubDate>Wed, 15 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这可能是对现在而言唯一一篇关于Content v3 版本的 RSS 订阅的文章了。我找遍了能搜出来的每篇文章，没有一个能用的。&lt;/p&gt;
&lt;p&gt;因为 &lt;code&gt;Nuxt Content&lt;/code&gt; v3 还没发布正式版，其相关生态的 &lt;code&gt;module&lt;/code&gt; 都没支持最新的 &lt;code&gt;Content&lt;/code&gt; , 感觉不是很复杂的功能，但是就不跟  &lt;code&gt;content v3&lt;/code&gt; 一起出个 &lt;code&gt;alpha&lt;/code&gt; 版，很无语。&lt;/p&gt;
&lt;p&gt;本文来带大家在 &lt;code&gt;Nuxt Content v3&lt;/code&gt; 里实现 RSS 订阅功能。&lt;/p&gt;
&lt;h2&gt;添加原始内容&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;content.config.ts&lt;/code&gt; 中 &lt;code&gt;rawbody&lt;/code&gt; 是一个特殊的 &lt;code&gt;schema&lt;/code&gt; ，配置后，将会把md原始内容存起来，在使用 &lt;code&gt;queryCollection&lt;/code&gt; 时，就可以查到 &lt;code&gt;rawbody&lt;/code&gt; 了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;content: defineCollection({
    type: &apos;page&apos;,
    source: {
      include: &apos;**/*.md&apos;,
      exclude: [&apos;**/-*.md&apos;, &apos;book/**/*.md&apos;],
      prefix: &apos;/post&apos;,
      repository: &apos;https://github.com/aatrooox/xxxx&apos;,
      authToken: process.env.CONTENT_REPO_TOKEN
    },
    schema: z.object({
      date: z.date(),
      lastmod: z.date(),
      tags: z.array(z.string()),
      versions: z.array(z.string()),
      rawbody: z.string()
    })
  }),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;server&lt;/code&gt; 中查询时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// @ts-ignore
  const posts = await queryCollection(event, &apos;content&apos;).order(&apos;date&apos;, &quot;DESC&quot;).all();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;queryCollection&lt;/code&gt; 是可以在直接在 &lt;code&gt;server&lt;/code&gt; 中使用的，不需要像 content v2中一样导入一个 &lt;code&gt;serverQueryContent&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;而且 &lt;code&gt;#content/server&lt;/code&gt; 这个导入方式在 v3 也不能用了&lt;/p&gt;
&lt;p&gt;使用时，会产生错误的类型提示，目前只能忽略它（不影响使用）。 可以查看 &lt;a href=&quot;https://github.com/nuxt/content/issues/2968#issuecomment-2589359589&quot;&gt;issue#2968&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;添加 feed.xml&lt;/h2&gt;
&lt;p&gt;找一个博客，点击他的订阅按钮，可以看到就是跳到一个 xml 页面上。&lt;/p&gt;
&lt;p&gt;所以我们也只需要实现一个 &lt;code&gt;feed.xml&lt;/code&gt; 即可，当然，一个网站也可以有多个 &lt;code&gt;rss&lt;/code&gt; 订阅源&lt;/p&gt;
&lt;p&gt;新建 &lt;strong&gt;server/routes/feed.xml.ts&lt;/strong&gt; ，因为是基于文件路径的路由，所以此路由就对应 &lt;code&gt;$baseUrl/feed.xml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在RSS阅读器上，也是通过输入这个地址来实现订阅。&lt;/p&gt;
&lt;h2&gt;添加相关依赖&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;rss&lt;/li&gt;
&lt;li&gt;unified&lt;/li&gt;
&lt;li&gt;remark-parse&lt;/li&gt;
&lt;li&gt;remark-gfm&lt;/li&gt;
&lt;li&gt;remark-breaks&lt;/li&gt;
&lt;li&gt;remark-frontmatter&lt;/li&gt;
&lt;li&gt;remark-directive&lt;/li&gt;
&lt;li&gt;remark-directive-rehype&lt;/li&gt;
&lt;li&gt;remark-rehype&lt;/li&gt;
&lt;li&gt;rehype-sanitize&lt;/li&gt;
&lt;li&gt;rehype-autolink-headings&lt;/li&gt;
&lt;li&gt;rehype-stringify&lt;/li&gt;
&lt;li&gt;hast-util-to-html&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i rss unified remark-parse remark-gfm remark-breaks remark-frontmatter remark-directive remark-directive-rehype remark-rehype rehype-sanitize rehype-autolink-headings rehype-stringify hast-util-to-html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些插件是围绕 markdown 和 html 的解析/转换相关的插件。&lt;/p&gt;
&lt;p&gt;如果你想了解他们之间是如何运作的，可以去看 &lt;a href=&quot;https://diygod.cc/unified-markdown&quot;&gt;DIYgod&lt;/a&gt; 的这篇文章。 以及这些插件在 &lt;a href=&quot;https://github.com/Crossbell-Box/xLog/blob/dev/src/markdown/index.ts&quot;&gt;xLog&lt;/a&gt; 中的具体用法。&lt;/p&gt;
&lt;p&gt;我再挨个罗列出来介绍一遍，画个图，感觉没什么必要了，都是非常稳定且已经是事实上的标准的插件。&lt;/p&gt;
&lt;p&gt;实际上 &lt;code&gt;nuxt/mdc&lt;/code&gt; 就是使用了一系列 &lt;code&gt;mdast&lt;/code&gt;、&lt;code&gt;hast&lt;/code&gt;、&lt;code&gt;remark&lt;/code&gt;、&lt;code&gt;rehype&lt;/code&gt; 的插件 ，但是可惜的是它没有开放出对应的接口。&lt;/p&gt;
&lt;p&gt;不然就不需要下载这么多插件了。&lt;/p&gt;
&lt;h2&gt;实现逻辑&lt;/h2&gt;
&lt;p&gt;直接放代码（忽略引入了，太长）：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/server/routes/feed.xml.ts&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineEventHandler(async (event) =&gt; {

  const config = useRuntimeConfig()
  // @ts-ignore
  const posts: any = await queryCollection(event, &apos;content&apos;).order(&apos;date&apos;, &quot;DESC&quot;).all();
  const feed = new RSS({
    title: &apos;早早集市&apos;,
    site_url: config.baseURL,
    feed_url: config.baseURL + &apos;/feed.xml&apos;,
  })

  for ( const post of posts) {
    const content = post.rawbody
    if (content) {
      const markdownContent = cleanInvalidChars(content);
      feed.item({
        title: post.title,
        url: `${config.baseURL}/${post.path}`,
        date: post.date,
        description: post.description,
        custom_elements: [
          {
            &apos;content:encoded&apos;: renderPageContent(markdownContent)
          }
        ]
      })
    }
  }

  const feedString = feed.xml();

  setResponseHeader(event, &apos;Content-Type&apos;, &apos;text/xml&apos;)

  return feedString

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先来说明一下每一段主要逻辑&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const config = useRuntimeConfig()&lt;/code&gt; 需要你在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 中配置如下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;runtimeConfig: {
	baseURL: &apos;your url&apos; // 或者使用环境变量覆盖
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;queryCollection&lt;/code&gt; 获取到所有原始的 md 内容&lt;/p&gt;
&lt;p&gt;我们的目的就是把每一篇文章都生成一个 &lt;code&gt;feed item&lt;/code&gt;， 所以循环所有文章，调用 &lt;code&gt;feed.item()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;此时出现了第一个问题： md原内容并不是等同于直接读取md文件，存在数据库中的原内容已经使&lt;code&gt; &lt;/code&gt; 变为了 &lt;code&gt;\ &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以为了使md能被正常解析，需要先清除一下没用的字符&lt;/p&gt;
&lt;p&gt;同文件下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function cleanInvalidChars(content:string) {
  return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, &apos;&apos;).replace(/\
/g, &apos;
&apos;).trim();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理好后，如果不写 &lt;code&gt;custom_elements&lt;/code&gt; ，这个 feed 也已经有效了，但缺点就是无法在 RSS 阅读器中直接阅读文章内容。&lt;/p&gt;
&lt;p&gt;所以刚才一堆插件，就是为了解析 &lt;code&gt;custom_elements&lt;/code&gt; 里要塞入的 &lt;code&gt;html&lt;/code&gt; 字符串&lt;/p&gt;
&lt;p&gt;同文件下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function renderPageContent(content: string) {
  const pipeline = unified()
  .use(remarkParse)
  .use(remarkBreaks)
  .use(remarkFrontmatter, [&quot;yaml&quot;])
  .use(remarkGfm, {  singleTilde: false })
  .use(remarkDirective)
  .use(remarkDirectiveRehype)
  .use(remarkRehype)
  .use(rehypeSanitize)
  .use(rehypeAutolinkHeadings)
  .use(rehypeStringify)

  const mdastTree = pipeline.parse(content)
  const hastTree = pipeline.runSync(mdastTree, content)
  return toHtml(hastTree)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，你可以在你的 &lt;code&gt;RSS&lt;/code&gt; 阅读器中订阅自己的博客，然后看看是否能展示正常的文章内容了，或者在发布之前，先在浏览器打开 &lt;code&gt;feed.xml&lt;/code&gt; ，观察 &lt;code&gt;xml&lt;/code&gt; 内的文章内容渲染是否正确。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;使用插件算是比较简单的实现方式了，搭配 AI 来手动写函数处理的话也是浪费了我不少时间，最后还有很多兼容问题，头铁的朋友可以试试&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;欢迎订阅我的博客：&lt;a href=&quot;https://blog.zzao.club/feed.xml&quot;&gt;RSS&lt;/a&gt; ，为你带来最新的 Nuxt 实战内容&lt;/p&gt;
&lt;p&gt;链接: &lt;a href=&quot;https://blog.zzao.club/feed.xml&quot;&gt;https://blog.zzao.club/feed.xml&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;插件库会随着时间逐步完善，所以本文同样具有时效性。但无论如何，文章开头的版本号代表本文的生效范围。&lt;/p&gt;
&lt;p&gt;欢迎在评论区留下你的疑问和高见~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[打造 Markdown 的绝美排版：@nuxtjs-mdc 使用指南]]></title><description><![CDATA[这是一份持续更新的@nuxtjs/mdc的使用说明书，扩充官方文档的同时，更正一些错误信息（因为官方更的不及时）。同时也会涵盖解析 Makdown 语法的使用说明。]]></description><link>https://zzao.club/post/nuxt/nuxtjs-mdc-docs</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxtjs-mdc-docs</guid><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一份持续更新的@nuxtjs/mdc的使用说明书，扩充官方文档的同时，更正一些错误信息（因为官方更的不及时）。同时也会涵盖解析 &lt;code&gt;Makdown&lt;/code&gt; 语法的使用说明。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nuxi@latest module add mdc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后 @nuxtjs/mdc 就会被自动添加到 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 的 modules 中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
  modules: [&apos;@nuxtjs/mdc&apos;]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组件&lt;/h2&gt;
&lt;p&gt;MDC 中提供了三个组件来渲染 markdown 内容&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MDC&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
const md = &quot;
	# h1 标题

	`代码快`

&quot;

&amp;#x3C;/script&gt;

&amp;#x3C;template&gt;
  &amp;#x3C;MDC :value=&quot;md&quot; tag=&quot;article&quot; /&gt;
&amp;#x3C;/template&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MDC&lt;/code&gt; 组件直接接受一个 &lt;code&gt;value&lt;/code&gt; prop，传入 &lt;code&gt;markdown&lt;/code&gt; 的&lt;strong&gt;原始内容&lt;/strong&gt;即可，&lt;code&gt;tag&lt;/code&gt; 属性可以决定渲染后的内容被什么标签包裹，类似于 &lt;code&gt;vue-router&lt;/code&gt; 的 &lt;code&gt;RouterLink&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MDCRenderer&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个组件依赖于 &lt;code&gt;parseMarkdown&lt;/code&gt; 函数提供的数据&lt;/p&gt;
&lt;p&gt;此函数需要从 &lt;code&gt;@nuxtjs/mdc/runtime&lt;/code&gt; 导入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { parseMarkdown } from &apos;@nuxtjs/mdc/runtime&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时可以像这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
import { parseMarkdown } from &apos;@nuxtjs/mdc/runtime&apos;

const { data: ast } = await useAsyncData(&apos;markdown&apos;, () =&gt; parseMarkdown(&apos;::alert
Missing markdown input
::&apos;))
&amp;#x3C;/script&gt;

&amp;#x3C;template&gt;
  &amp;#x3C;MDCRenderer :body=&quot;ast.body&quot; :data=&quot;ast.data&quot; /&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它还有第二个参数 &lt;a href=&quot;https://github.com/nuxt-modules/mdc/blob/main/src/runtime/types/parser.ts&quot;&gt;MDCParseOptions&lt;/a&gt;，可以用来控制解析起的行为。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111113759.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也可以在 nuxt.config.ts 中配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { defineNuxtConfig } from &apos;nuxt/config&apos;

export default defineNuxtConfig({
  modules: [&apos;@nuxtjs/mdc&apos;],
  mdc: {
    remarkPlugins: {
      plugins: {
        // Register/Configure remark plugin to extend the parser
      }
    },
    rehypePlugins: {
      options: {
        // Configure rehype options to extend the parser
      },
      plugins: {
        // Register/Configure rehype plugin to extend the parser
      }
    },
    headings: {
      anchorLinks: {
        // Enable/Disable heading anchor links. { h1: true, h2: false }
      }
    },
    highlight: false, // Control syntax highlighting
    components: {
      prose: false, // Add predefined map to render Prose Components instead of HTML tags, like p, ul, code
      map: {
        // This map will be used in `&amp;#x3C;MDCRenderer&gt;` to control rendered components
      }
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;点进去可以看到，这地址都 404 了，文件都删了，这就是为什么我要写这篇文章....&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;事实上，&lt;code&gt;mdc&lt;/code&gt; 底层用到的很多插件，都是和 &lt;code&gt;unified&lt;/code&gt; 的生态是一致的（都是基于&lt;code&gt;remarkPlugins&lt;/code&gt;、&lt;code&gt;rehypePlugins&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;但是 &lt;code&gt;mdc&lt;/code&gt; 搞的有点太封闭。没导出几个有用的Api，其实完全可以把关于 &lt;code&gt;markdown&lt;/code&gt;、&lt;code&gt;html&lt;/code&gt;、以及中间的 &lt;code&gt;hastTree&lt;/code&gt; 都开放出来。&lt;/p&gt;
&lt;p&gt;因为 &lt;code&gt;markdown&lt;/code&gt; 相关的内容，虽然没有官方的标准，但是因为使用范围很广，早就成了事实意义上的标准。有用的人自然会用了，不用的压根都不会看一眼。&lt;/p&gt;
&lt;p&gt;实际使用中，这种方式还没有找到使用场景（在内容渲染中），不管是自己本地的数据，还是从第三方 API获取到数据，直接扔给 &lt;code&gt;MDC&lt;/code&gt; 组件是最方便的，在数据中存储原始数据（&lt;code&gt;rawbody&lt;/code&gt;），在不同平台展示时自身处理渲染逻辑。&lt;/p&gt;
&lt;p&gt;PS：但要做 RSS 订阅就不得不把生成后的 &lt;code&gt;HTML&lt;/code&gt; 放在 &lt;code&gt;xml&lt;/code&gt; 中 ，这就是我上边为啥吐槽它太封闭。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MDCSlot&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个组件是为了替代 Vue 中的 &lt;code&gt;slot&lt;/code&gt; 组件，针对 &lt;code&gt;MDC&lt;/code&gt; 做了特殊处理，使用这个组件时你可以删除其包裹元素&lt;code&gt;p&lt;/code&gt;，（使用 &lt;code&gt;slot&lt;/code&gt; 时会默认渲染一个 &lt;code&gt;p&lt;/code&gt; 标签包裹文字内容）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;demo.md&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;ddddsadadasdasd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ProseP.vue&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;p&gt;
    &amp;#x3C;!-- MDCSlot will only render the actual text without the wrapping &amp;#x3C;p&gt; --&gt;
    &amp;#x3C;MDCSlot unwrap=&quot;p&quot; /&gt;
  &amp;#x3C;/p&gt;
&amp;#x3C;/template&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你输入两段纯文本，并且中间有一段空行时，这两段文本会分别被 p 标签包裹，做到换行的效果。&lt;/p&gt;
&lt;p&gt;而如果用上述的 &lt;code&gt;ProseP.vue&lt;/code&gt; 覆盖后，纯文本将不再被 &lt;code&gt;p&lt;/code&gt; 标签包裹，而是变成了 &lt;code&gt;span&lt;/code&gt;，也就是你在写 md 时，哪怕已经换了行，渲染后的内容也是连贯的排列在一起的。&lt;/p&gt;
&lt;p&gt;那 ProseComponent 是什么呢&lt;/p&gt;
&lt;h2&gt;Prose Component&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MDC&lt;/code&gt; 渲染 &lt;code&gt;markdown&lt;/code&gt; 内容时，使用了一套组件来渲染对应的 &lt;code&gt;markdown&lt;/code&gt; 语法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/CleanShot%202025-01-11%20at%2011.28.56.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同样的也支持你覆盖这些组件&lt;/p&gt;
&lt;p&gt;如果你使用 &lt;code&gt;nuxt3.15.1&lt;/code&gt; 并且开启了    &lt;code&gt;compatibilityVersion: 4&lt;/code&gt;，那你的 &lt;code&gt;components&lt;/code&gt; 路径应该是在 &lt;code&gt;app/components&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在此路径下新建目录 &lt;code&gt;mdc&lt;/code&gt; ，然后创建一个同名的 &lt;code&gt;vue&lt;/code&gt; 文件：&lt;code&gt;ProseA.vue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111114806.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我改写了其样式，并且把跳转默认为打开新标签页&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111114847.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到如上渲染内容&lt;/p&gt;
&lt;h2&gt;自定义组件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MDC&lt;/code&gt; 还支持在 &lt;code&gt;markdown&lt;/code&gt; 中写 &lt;code&gt;vue&lt;/code&gt; 组件，语法是这样的&lt;/p&gt;
&lt;p&gt;&lt;code&gt;demo.md&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;::component-name
This is an vue component
::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应 &lt;code&gt;app/components/mdc/ComponentName.vue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果你正在搭配 Nuxt Content 使用，则对应目录为 &lt;code&gt;app/components/content/ComponentName.vue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再来个更实际的例子&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;md 内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;下面是一个 CustomTag 组件

::custom-tag
内部内容演示
::

组件位于`app/components/mdc/CustomTag.vue`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CustomTag.vue&lt;/code&gt; 内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div class=&quot;text-center my-10&quot;&gt;
    &amp;#x3C;div
      class=&quot;text-black px-3 py-2 text-lg font-bold&quot;&gt;
      &amp;#x3C;slot/&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup lang=&quot;ts&quot;&gt;

&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染后的结果为：&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111185601.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种组件被称为 &lt;code&gt;Block Components&lt;/code&gt; ，和 &lt;code&gt;display: block&lt;/code&gt; 的意思相同，是个块级组件，单独占一行&lt;/p&gt;
&lt;p&gt;既然是Vue组件，也给它传 props&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;::custom-tag{type=&quot;warning&quot;}
内部内容演示
::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再把组件改一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div class=&quot;text-center my-10&quot;&gt;
    &amp;#x3C;div
      class=&quot;text-black px-3 py-2 text-lg font-bold&quot; :class=&quot;{ &apos;bg-yellow-200&apos;: props.type === &apos;warning&apos;, &apos;bg-blue-200&apos;: props.type === &apos;info&apos;, &apos;bg-green-200&apos;: props.type === &apos;success&apos;, &apos;bg-red-200&apos;: props.type === &apos;error&apos; }&quot;&gt;
      &amp;#x3C;slot/&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
const props = defineProps&amp;#x3C;{
  type?: &apos;warning’ | ‘info’ | ‘success’ | ‘error’&apos;
}&gt;()
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;`&lt;br&gt;
看下渲染的内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111185907.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也可以直接传 style&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;::custom-tag{type=&quot;warning&quot; style=&quot;margin-top:100px;&quot;} 
内部内容演示 
::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到有了一个很大的间距&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111190522.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;还支持使用 YAML method 的方式传入&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;::custom-tag{type=&quot;warning&quot; style=&quot;margin-top:100px;&quot;} 
---
desc: &quot;我是描述内容&quot;
---
::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把组件改为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div class=&quot;text-center my-10&quot;&gt;
    &amp;#x3C;div
      class=&quot;text-black px-3 py-2 text-lg font-bold&quot; :class=&quot;{ &apos;bg-yellow-200&apos;: props.type === &apos;warning&apos;, &apos;bg-blue-200&apos;: props.type === &apos;info&apos;, &apos;bg-green-200&apos;: props.type === &apos;success&apos;, &apos;bg-red-200&apos;: props.type === &apos;error&apos; }&quot;&gt;
      &amp;#x3C;div class=&quot;title&quot;&gt;
        {{ props.type }}
      &amp;#x3C;/div&gt;

      &amp;#x3C;div class=&quot;desc text-red-600&quot;&gt;
        {{ desc }}
      &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
const props = defineProps&amp;#x3C;{
  type?: &apos;warning&apos; | &apos;info&apos; | &apos;success&apos; | &apos;error&apos;,
  desc?: string
}&gt;()
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250111191131.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过这种方式，&lt;strong&gt;不能和 slot 混用&lt;/strong&gt;，渲染出来 slot 会把几个 props 都覆盖。&lt;/p&gt;
&lt;p&gt;实际使用时，&lt;strong&gt;不应该对一个内容写如此复杂的组件&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;换句话说，Vue 组件应该足够完善， 让你在 &lt;code&gt;markdown&lt;/code&gt; 中写足够少的信息，只传入必要的数据即可得到完美的展示才对。&lt;/p&gt;
&lt;p&gt;上面的 &lt;code&gt;Props&lt;/code&gt; 是我们自定义的组件提前写好的 &lt;code&gt;Props&lt;/code&gt; ，而内置的 Prose Components 也是一套 Vue 组件而已。&lt;/p&gt;
&lt;p&gt;所以除了在 &lt;code&gt;app/components/mdc/&lt;/code&gt; 下创建一个同名的 &lt;code&gt;Prose Component&lt;/code&gt; 覆盖原有组件，也可以直接给原组件传一些 &lt;code&gt;style&lt;/code&gt;，改变它的样式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;Attributes work on:

- ![favicon](/favicon.ico){style=&quot;display: inline; margin: 0;&quot;} image,
- [link](#attributes){style=&quot;background-color: pink;&quot;}, `code`{style=&quot;color: cyan;&quot;},
- _italic_{style=&quot;background-color: yellow; color:black;&quot;} and **bold**{style=&quot;background-color: lightgreen;&quot;} texts.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了使用一个 Vue 组件并给他传 Props，设置 style&lt;/p&gt;
&lt;p&gt;还能使用 &lt;code&gt;:ComponentName&lt;/code&gt;  的语法直接使用一个写好的组件，比如这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# Title

:banner
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Banner.vue&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;aside&gt;
    This component does not have any children.
  &amp;#x3C;/aside&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这在自己定制的平台上使用时，会很有用。 但同样的，如果你使用其他软件或 API 来获取 md，要考虑一下语法过多导致的各平台不兼容问题。&lt;/p&gt;
&lt;p&gt;PS: 这种md里写属性传值的方式并不是 &lt;code&gt;mdc&lt;/code&gt; 的原创，而是 &lt;code&gt;unified&lt;/code&gt; （remark/rehype）插件相关的生态，都是这样写的。&lt;/p&gt;
&lt;h2&gt;绑定数据&lt;/h2&gt;
&lt;p&gt;贴两个官方的例子，很好理解&lt;/p&gt;
&lt;p&gt;第一种是在 Markdown 的 YAML 中定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;---
title: &apos;Title of the page&apos;
description: &apos;meta description of the page&apos;
customVariable: &apos;Custom Value&apos;
---

# The Title is {{ $doc.title }} and customVariable is {{ $doc.customVariable || &apos;defaultValue&apos; }}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个用法很有局限，因为你用来写 md 文章的软件大概率不支持这个语法，或者你要同步到其他平台的时候其他平台也不会支持这个语法。&lt;/p&gt;
&lt;p&gt;但是如果你的用途很单一，说不定会比较有用&lt;/p&gt;
&lt;p&gt;第二种是定义在 Vue 组件中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;ContentRenderer :value=&quot;data&quot; :data=&quot;mdcVars&quot;/&gt;
    &amp;#x3C;button type=&quot;button&quot; v-on:click=&quot;mdcVars.name = &apos;Hugo&apos;&quot;&gt;Change name&amp;#x3C;/button&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
const { data } = await useAsyncData(() =&gt; queryCollection(&apos;content&apos;).path(&apos;/test&apos;).first());
const mdcVars = ref({ name: &apos;Maxime&apos;});
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;md 中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# Hello {{ $doc.name || &apos;World&apos; }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是那种话，定制的越多，越不可控。&lt;/p&gt;
&lt;p&gt;文章内容还是要以高质量的文字为准，自定义组件更多的是作为锦上添花，是离不开一个封闭的平台的。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;官方还给出了一种自定义组件的方式，就是在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 中配置 &lt;code&gt;prose: false&lt;/code&gt;，关闭 Prose Components 的渲染方式，自定义一个 map 指定组件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;mdc: {
    // components: {
    //   prose: false,
      // map: {
      //   &apos;a&apos;: &apos;MemoProseA&apos;
      // }
    // }
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但我觉得这种方式和直接在 mdc 目录在覆盖掉原组件的区别很小，这种方式可以做到只支持部分 md 语法的渲染，比如你只写一个 a ，那其他内容就是纯文本，只有 a 标签是通过自定义组件渲染出来的，不清楚什么场景下才会有这种选择～&lt;/p&gt;
&lt;h2&gt;tailwind CSS 主题&lt;/h2&gt;
&lt;p&gt;Prose Components 支持使用 &lt;a href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&gt;tailwindcss-typography&lt;/a&gt; 覆盖 html 排版&lt;/p&gt;
&lt;p&gt;这是我觉得比较实用的样式修改方式，因为 tailwindcss 足够通用，并且在全局的固定位置修改样式，便于管理&lt;/p&gt;
&lt;p&gt;Tailwind CSS Typography 提供了一组 prose class，可以给默认的 html 元素附加排版，➡️点击查看&lt;a href=&quot;https://play.tailwindcss.com/uj1vGACRJA?layout=preview&quot;&gt;演示&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;article class=&quot;prose lg:prose-xl&quot;&gt;{{ markdown }}&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;安装插件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install -D @tailwindcss/typography
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;新建或添加&lt;/strong&gt;到 &lt;code&gt;tailwind.config.js&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/** @type {import(&apos;tailwindcss&apos;).Config} */
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require(&apos;@tailwindcss/typography&apos;),
    // ...
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改灰度&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prose prose-gray(默认) prose-slate prose-zinc prose-neutral prose-stone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不管使用哪个，都要带有 &lt;code&gt;prose&lt;/code&gt; 这个基类&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文字整体尺寸&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prose prose-sm (14px) prose-base (默认16px) prose-lg (18px) prose-xl (20px) prose-2xl (24px)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和灰度一样，也要带有 &lt;code&gt;prose&lt;/code&gt; 这个基类，实际使用下来，还是 &lt;code&gt;prose-base&lt;/code&gt; 用的最多，可以在自己发文发帖的多个平台尝试不同字号&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适配深色模式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上面的几个调整灰度的主题，都有默认的深色模式版本，可以使用 prose-invert 来触发&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;article class=&quot;prose dark:prose-invert&quot;&gt;{{ markdown }}&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你使用了自定义组件，则需要自己使用 &lt;code&gt;dark:&lt;/code&gt; 修饰符适配一下深色模式&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Nuxt&lt;/code&gt; 中使用 &lt;code&gt;@nuxtjs/color-mode&lt;/code&gt; 来控制颜色模式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;精细化控制样式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除了全局设置默认的样式，也可以通过 &lt;code&gt;prose-xxx&lt;/code&gt; 来控制目标标签的样式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/picgo/Pasted%20image%2020250112101943.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;article class=&quot;prose prose-a:text-blue-600 hover:prose-a:text-blue-500&quot;&gt;{{ markdown }}&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，每个修饰符都为了保证内容的可读性，设置了最大宽度。 如果你希望内容能够填充其容器的宽度，可以使用 &lt;code&gt;max-w-none&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;article class=&quot;prose max-w-none&quot;&gt;{{ markdown }}&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;取消 prose 样式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;not-prose&lt;/code&gt; 标记一些元素，不使用 &lt;code&gt;prose&lt;/code&gt; 的样式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;article class=&quot;prose&quot;&gt;
  &amp;#x3C;h1&gt;My Heading&amp;#x3C;/h1&gt;
  &amp;#x3C;p&gt;...&amp;#x3C;/p&gt;

  &amp;#x3C;div class=&quot;not-prose&quot;&gt;
    &amp;#x3C;!-- Some example or demo that needs to be prose-free --&gt;
  &amp;#x3C;/div&gt;

  &amp;#x3C;p&gt;...&amp;#x3C;/p&gt;
  &amp;#x3C;!-- ... --&gt;
&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但无法继续在 &lt;code&gt;not-prose&lt;/code&gt; 里再嵌套 &lt;code&gt;prose&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自定义颜色主题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以在 &lt;code&gt;tailwindcss.config.js&lt;/code&gt; 中设置自定义的颜色主题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/** @type {import(&apos;tailwindcss&apos;).Config} */
module.exports = {
  theme: {
    extend: {
      typography: ({ theme }) =&gt; ({
        pink: {
          css: {
            &apos;--tw-prose-body&apos;: theme(&apos;colors.pink[800]&apos;),
            &apos;--tw-prose-headings&apos;: theme(&apos;colors.pink[900]&apos;),
            &apos;--tw-prose-lead&apos;: theme(&apos;colors.pink[700]&apos;),
            &apos;--tw-prose-links&apos;: theme(&apos;colors.pink[900]&apos;),
            &apos;--tw-prose-bold&apos;: theme(&apos;colors.pink[900]&apos;),
            &apos;--tw-prose-counters&apos;: theme(&apos;colors.pink[600]&apos;),
            &apos;--tw-prose-bullets&apos;: theme(&apos;colors.pink[400]&apos;),
            &apos;--tw-prose-hr&apos;: theme(&apos;colors.pink[300]&apos;),
            &apos;--tw-prose-quotes&apos;: theme(&apos;colors.pink[900]&apos;),
            &apos;--tw-prose-quote-borders&apos;: theme(&apos;colors.pink[300]&apos;),
            &apos;--tw-prose-captions&apos;: theme(&apos;colors.pink[700]&apos;),
            &apos;--tw-prose-code&apos;: theme(&apos;colors.pink[900]&apos;),
            &apos;--tw-prose-pre-code&apos;: theme(&apos;colors.pink[100]&apos;),
            &apos;--tw-prose-pre-bg&apos;: theme(&apos;colors.pink[900]&apos;),
            &apos;--tw-prose-th-borders&apos;: theme(&apos;colors.pink[300]&apos;),
            &apos;--tw-prose-td-borders&apos;: theme(&apos;colors.pink[200]&apos;),
            &apos;--tw-prose-invert-body&apos;: theme(&apos;colors.pink[200]&apos;),
            &apos;--tw-prose-invert-headings&apos;: theme(&apos;colors.white&apos;),
            &apos;--tw-prose-invert-lead&apos;: theme(&apos;colors.pink[300]&apos;),
            &apos;--tw-prose-invert-links&apos;: theme(&apos;colors.white&apos;),
            &apos;--tw-prose-invert-bold&apos;: theme(&apos;colors.white&apos;),
            &apos;--tw-prose-invert-counters&apos;: theme(&apos;colors.pink[400]&apos;),
            &apos;--tw-prose-invert-bullets&apos;: theme(&apos;colors.pink[600]&apos;),
            &apos;--tw-prose-invert-hr&apos;: theme(&apos;colors.pink[700]&apos;),
            &apos;--tw-prose-invert-quotes&apos;: theme(&apos;colors.pink[100]&apos;),
            &apos;--tw-prose-invert-quote-borders&apos;: theme(&apos;colors.pink[700]&apos;),
            &apos;--tw-prose-invert-captions&apos;: theme(&apos;colors.pink[400]&apos;),
            &apos;--tw-prose-invert-code&apos;: theme(&apos;colors.white&apos;),
            &apos;--tw-prose-invert-pre-code&apos;: theme(&apos;colors.pink[300]&apos;),
            &apos;--tw-prose-invert-pre-bg&apos;: &apos;rgb(0 0 0 / 50%)&apos;,
            &apos;--tw-prose-invert-th-borders&apos;: theme(&apos;colors.pink[600]&apos;),
            &apos;--tw-prose-invert-td-borders&apos;: theme(&apos;colors.pink[700]&apos;),
          },
        },
      }),
    },
  },
  plugins: [
    require(&apos;@tailwindcss/typography&apos;),
    // ...
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用自定义的色值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/** @type {import(&apos;tailwindcss&apos;).Config} */
module.exports = {
  theme: {
    extend: {
      typography: {
        DEFAULT: {
          css: {
            color: &apos;#333&apos;,
            a: {
              color: &apos;#3182ce&apos;,
              &apos;&amp;#x26;:hover&apos;: {
                color: &apos;#2c5282&apos;,
              },
            },
          },
        },
      },
    },
  },
  plugins: [
    require(&apos;@tailwindcss/typography&apos;),
    // ...
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过不建议在这里配置颜色相关的，因为提供了通过 prose-xxx 的形式灵活控制样式时，在这里在写一遍自定义的样式会难以覆盖，可以在这里设置一些间距类的样式。&lt;/p&gt;
&lt;p&gt;而颜色使用一个自定义的 class 去使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.mdc-page-prose {
  @apply prose prose-zinc prose-pre:bg-gray-100 dark:prose-pre:bg-zinc-400 dark:text-zinc-200 dark:prose-strong:text-zinc-200 prose-code:bg-zinc-200 dark:prose-code:bg-zinc-200 prose-code:text-zinc-800 dark:prose-blockquote:text-zinc-300 w-full max-w-full
}

.mdc-prose {
  @apply prose prose-zinc prose-pre:bg-gray-100 dark:prose-pre:bg-zinc-400 dark:text-zinc-200 dark:prose-strong:text-zinc-200 prose-code:bg-zinc-200 dark:prose-code:bg-zinc-200 prose-code:text-zinc-800 dark:prose-blockquote:text-zinc-300 w-full max-w-full
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，在一个项目中，出现两组或多组不同的 &lt;code&gt;prose&lt;/code&gt; 样式，就比较方便使用 &lt;code&gt;class&lt;/code&gt; 控制了，毕竟 &lt;code&gt;tailwindcss.config.js&lt;/code&gt; 只有一个，尽量设置一些通用的不常变化的属性。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;以上就是 &lt;code&gt;nuxtjs/mdc&lt;/code&gt; 的大部分使用场景了，通常这个库会在使用 &lt;code&gt;Nuxt Content&lt;/code&gt; 时使用，但也可以只使用它来支持多种来源，片段化的 &lt;code&gt;md&lt;/code&gt; 内容渲染。&lt;/p&gt;
&lt;p&gt;但是要注意，虽然支持自定义组件，但我还是不建议你的 &lt;code&gt;md 文章&lt;/code&gt;里不要包含太多的&lt;code&gt;魔法&lt;/code&gt;，&lt;strong&gt;在自己定制的平台上是魔法，在其他不支持的软件和 web 里就是麻瓜&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是自定义组件很适合用来&lt;strong&gt;支持自己的自建平台&lt;/strong&gt;，这也是我为什么会把 mdc 这个库拿出来单独使用。&lt;/p&gt;
&lt;p&gt;使用时注意文章开头的 &lt;code&gt;mdc&lt;/code&gt; 版本号，表示此文章的生效范围，后续更新只能在我的&lt;a href=&quot;https://blog.zzao.club&quot;&gt;博客站&lt;/a&gt;同步了&lt;/p&gt;
&lt;p&gt;如果你也是 &lt;code&gt;Nuxt&lt;/code&gt; 的使用者，或是 &lt;code&gt;Vue&lt;/code&gt; 使用者对 &lt;code&gt;Nuxt&lt;/code&gt; 感兴趣，欢迎在文末或博客站首页添加我的微信，一起交流，知无不言😎&lt;/p&gt;</content:encoded></item><item><title><![CDATA[博客站还有很多功能可以完善，但不得不进入优化期]]></title><description><![CDATA[目前博客站对于 Obsidian 的文章有了一个基本的渲染功能。]]></description><link>https://zzao.club/post/zzao/blog-site-is-pendding</link><guid isPermaLink="true">https://zzao.club/post/zzao/blog-site-is-pendding</guid><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目前博客站对于 &lt;code&gt;Obsidian&lt;/code&gt; 的文章有了一个基本的渲染功能。&lt;/p&gt;
&lt;p&gt;可以拉取指定Github仓库的文章，生成站点，也能配置某个目录下的文章为小册的展示方式。&lt;/p&gt;
&lt;p&gt;我知道还有很多功能不足，比如 &lt;code&gt;sitemap&lt;/code&gt;、&lt;code&gt;静态化&lt;/code&gt;、&lt;code&gt;robots.txt&lt;/code&gt;、&lt;code&gt;rss&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;@nuxtjs/seo&lt;/code&gt; 这个 &lt;code&gt;module&lt;/code&gt; 还未支持最新版的 &lt;code&gt;Nuxt Content&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以博客的内容只能先做减法，优化一些 UI 方面的细节&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
  modules: [&apos;@nuxtjs/mdc&apos;]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，因为我是先做了一个大的项目，然后又从中抽离出博客这个站点。&lt;/p&gt;
&lt;p&gt;所以，本项目上也存在不少冗余代码，最近有时间我就会清理一下。&lt;/p&gt;
&lt;p&gt;还有包括具体的使用教程，也会优先完善（如果有人用的话）。&lt;/p&gt;
&lt;p&gt;同时，也从 &lt;code&gt;Gitea&lt;/code&gt; 设置了 &lt;code&gt;Github&lt;/code&gt; 的镜像仓库，&lt;strong&gt;如果侥幸有人使用的话，能够反馈一些不足之处是最好的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/aatrooox/blog.zzao.club&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2025年01月14日11:34:17 更新&lt;/h2&gt;
&lt;p&gt;就在13号在 &lt;code&gt;sitemap&lt;/code&gt; 的 &lt;code&gt;github&lt;/code&gt; 提了 &lt;code&gt;issue&lt;/code&gt; 之后， 14号就发布了 v7.0.2 版本&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501141149021.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501141149022.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过倒不是支持了 &lt;code&gt;nuxt/content&lt;/code&gt;  而是检测如果使用了v3版本的content，则不会去自动引入content了&lt;/p&gt;
&lt;p&gt;所以，可以自己手动去生成 &lt;code&gt;sitemap&lt;/code&gt; 了&lt;/p&gt;</content:encoded></item><item><title><![CDATA[靠坚持做不成长期主义]]></title><description><![CDATA[做成一件长期的事前，有不止九九八十一难。]]></description><link>https://zzao.club/post/daily/can-not-make-long-termism-by-persistence</link><guid isPermaLink="true">https://zzao.club/post/daily/can-not-make-long-termism-by-persistence</guid><pubDate>Mon, 06 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;做成一件长期的事前，有不止九九八十一难。&lt;/strong&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;code&gt;坚持&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其他的收入方式，要么&lt;strong&gt;不适合&lt;/strong&gt;，要么&lt;strong&gt;不稳定&lt;/strong&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;code&gt;登录就送&lt;/code&gt;、&lt;code&gt;签到满x天就送x&lt;/code&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;code&gt;100%&lt;/code&gt;自驱，有时候能有&lt;code&gt;60%&lt;/code&gt;就有不错的效果，因为我要做的事也不是什么大事。&lt;/p&gt;
&lt;p&gt;我能给自己一些压力，但是不够我抵达目标。&lt;/p&gt;
&lt;p&gt;就好像一个上升的台阶，10米，就俩台阶，那我上不去。如果是5个台阶，我如果身体强壮的话，能试试。如果是10个台阶，那就很有信心登上去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那如果是九九八十一个台阶呢？&lt;/strong&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;放在做独立开发一个产品上，是为了差异化，为了增加用户粘性，让产品更有人情味。&lt;/p&gt;
&lt;p&gt;放在家庭教育上，谁来给？给多少？甜枣多了怕齁着，巴掌大了也怕打蒙了。&lt;/p&gt;
&lt;p&gt;一开始我想：&lt;strong&gt;这种事往深了想没有意义，因为不要求做到100分，考虑太细致反而受其乱。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是我此刻笃定极有意义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不过意义不是如何做到100分，而是如何把60分完成的游刃有余。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;毕竟对于一个差一步都“登顶”不了的普通人来说&lt;/p&gt;
&lt;p&gt;你怎么知道此刻的思考不是你的下一步台阶呢？&lt;/p&gt;
&lt;p&gt;碎碎念 END ....&lt;/p&gt;</content:encoded></item><item><title><![CDATA[2025 年，我打算 All in Nuxt]]></title><description><![CDATA[最近用 Nuxt 搭建了一个博客站，把 Obsidian 的文章无痛搬过去，可算是收了个小尾。]]></description><link>https://zzao.club/post/zzao/2024-all-in-nuxt</link><guid isPermaLink="true">https://zzao.club/post/zzao/2024-all-in-nuxt</guid><pubDate>Thu, 02 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近用 Nuxt 搭建了一个博客站，把 &lt;code&gt;Obsidian&lt;/code&gt; 的文章无痛搬过去，可算是收了个小尾。&lt;/p&gt;
&lt;p&gt;展示一下成果😎&lt;/p&gt;
&lt;p&gt;首页，展示个人信息、最近文章&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923577.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，也必须支持暗色&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923578.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;文章页，主要是支持分类查询和分页&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923579.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;小册，顾名思义：把文章整理成册就是小册，适合系统的整理某些资料&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923580.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;小册内根据 Obsidian 的目录结构来展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923581.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923582.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;也做了移动端适配&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202501021923583.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当然，审美这块儿，见仁见智....&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然后再说一下，目前关于写文章的相关工作流。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;目前使用 &lt;code&gt;Chrome&lt;/code&gt; 的 lighthouse 测试，性能 &lt;code&gt;98&lt;/code&gt; ，SEO &lt;code&gt;100&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;数据统计使用 &lt;code&gt;Google Analytics&lt;/code&gt;，手动插入 &lt;code&gt;scripts&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;Obsidian&lt;/code&gt; 的文章通过 &lt;code&gt;Github&lt;/code&gt; 进行多端同步，插件是 &lt;code&gt;Obsidian Git&lt;/code&gt;，设置定时 commit 定时 pull  定时 push。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;Obsidian&lt;/code&gt; 插件 &lt;code&gt;Linter&lt;/code&gt; 进行格式化 YAML 信息，自动添加 标题、描述、创建时间、修改时间等。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;Obsidian&lt;/code&gt; 插件 &lt;code&gt;image auto upload plugin&lt;/code&gt; 上传插入的图片到腾讯云 COS，上传后删除源文件，一般我的图片都是从本地复制过来的，留在仓库里也没什么用。&lt;/p&gt;
&lt;p&gt;腾讯云对象存储设置私有读写，套 CDN 和设置个人域名+SSL证书进行访问，CDN 设置流量上限，开启防盗链等，避免被人刷太多。&lt;/p&gt;
&lt;p&gt;写完就不用管了，自动上传到 Github，但是还没自动触发部署操作，这个后续再加吧～&lt;/p&gt;
&lt;p&gt;有“家”了之后，新的一年就好好整理一下屋里的东西（文章）吧&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最近的所有开发都围绕 Nuxt 展开，2025 年也打算深入投入到上面。但学习和开发过程中，发现可搜到的文章、视频都非常少（主要是看核心开发人员自己发的 youtube 视频），可交流的小伙伴也不多。&lt;/p&gt;
&lt;p&gt;所以，文章和视频部分，我准备自己发布一些。&lt;/p&gt;
&lt;p&gt;交流场所的话，就建个交流群吧。&lt;/p&gt;
&lt;p&gt;如果你也是 Vue / Nuxt 的使用者，想了解和使用 Nuxt ，欢迎加我好友👏。&lt;/p&gt;
&lt;p&gt;博客地址在消息菜单里可以找到&lt;/p&gt;</content:encoded></item><item><title><![CDATA[App 下滑上二楼到底是谁发明的]]></title><description><![CDATA[不知道从什么开始，很多 App 在首页加了个下滑上二楼的功能。]]></description><link>https://zzao.club/post/daily/app-2-floor</link><guid isPermaLink="true">https://zzao.club/post/daily/app-2-floor</guid><pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;不知道从什么开始，很多 App 在首页加了个下滑上二楼的功能。&lt;/p&gt;
&lt;p&gt;且先不说这功能好不好用。&lt;/p&gt;
&lt;p&gt;你说一个 App，肯定有个定位对吧。外卖的就是点外卖的，银行的就是转账的，打车的就是摇车的。&lt;/p&gt;
&lt;p&gt;底下三个入口都满足不了你是吧？&lt;/p&gt;
&lt;p&gt;第一个 Tab，点外卖，第二个 Tab 借钱，第三个 Tab 短视频，不是你真缺钱，你把贷款放在首页呗，怎么还藏着个点外卖的功能啊。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[nuxt/content运行或编译时报错unhandledRejection]]></title><link>https://zzao.club/post/issues/tar_bad_archive</link><guid isPermaLink="true">https://zzao.club/post/issues/tar_bad_archive</guid><pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt; ERROR  [unhandledRejection] TAR_BAD_ARCHIVE: Unrecognized archive format 09:45:32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此错误出现在使用了 nuxt/content 的 repository 配置。&lt;/p&gt;
&lt;p&gt;原因是 一般是 github 仓库拉取失败&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查是否是有效的 github 仓库地址&lt;/li&gt;
&lt;li&gt;检查该仓库是否配置了 accessToken&lt;/li&gt;
&lt;li&gt;检查 accessToken 的权限中是否包含&lt;code&gt;可读仓库&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自身网络是否有问题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;排查时，&lt;code&gt;npm install&lt;/code&gt; 有时也会报此错误，基本是因为网络问题，可以尝试挂一下代理。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[震惊，我在同时打三份工！]]></title><description><![CDATA[最近在同步开发两个私活，一个基于Flutter的App，一个基于Vue2的可视化大屏。再加上主业，竟然同时在搞三份工，纯牛马了属实是。]]></description><link>https://zzao.club/post/side-hustle/do-3-jobs-at-once</link><guid isPermaLink="true">https://zzao.club/post/side-hustle/do-3-jobs-at-once</guid><pubDate>Wed, 18 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在同步开发两个私活，一个基于&lt;code&gt;Flutter&lt;/code&gt;的App，一个基于&lt;code&gt;Vue2&lt;/code&gt;的可视化大屏。再加上主业，竟然同时在搞三份工，&lt;strong&gt;纯牛马&lt;/strong&gt;了属实是。&lt;/p&gt;
&lt;p&gt;App仅仅要求开发一个安卓App，没有要求开发使用的语言。&lt;/p&gt;
&lt;p&gt;本来我第一预期框架肯定是&lt;code&gt;uniapp&lt;/code&gt;的，但这个App工期比较松，于是想选个没用过的玩一下😏&lt;/p&gt;
&lt;p&gt;因为只要求安卓，于是我花了两个小时把&lt;code&gt;kotlin&lt;/code&gt;官方入门文档读了一遍，读完甚至觉得还挺简单，&lt;code&gt;TS&lt;/code&gt;类型折腾起来比它麻烦（我胡说的）。&lt;/p&gt;
&lt;p&gt;但是觉得&lt;code&gt;kotlin&lt;/code&gt;还是有些局限性（多端上），而且依赖Java库，又去&lt;code&gt;v2ex&lt;/code&gt;搜了下网友的建议，有些甚至建议先学Java。我对学Java很排斥，因为感觉遍地都是Java，我再怎么学也无法弥补相对于其他Javaer来说入行晚的巨大劣势，所以迅速抛弃了&lt;code&gt;kotlin&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那最后为什么转投了&lt;code&gt;flutter&lt;/code&gt;呢，很简单，打开招聘软件，搜索&lt;code&gt;uniapp&lt;/code&gt; / &lt;code&gt;flutter&lt;/code&gt;/ &lt;code&gt;RN&lt;/code&gt; 等关键词，哪个岗位多，薪资高，就选哪个。&lt;/p&gt;
&lt;p&gt;虽然我身在一个二线的省会城市，但是&lt;code&gt;Flutter&lt;/code&gt;相比其他俩来说在岗位数量和薪资上优势还是很大的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uniapp&lt;/code&gt; 不是不行，而是门槛太低，薪资太低，所有前端都能直接拿来使用，也是大部分小公司或传统企业要开发app、小程序的首选。但这玩意什么门槛都没有（我瞎说的），我没有要去做的动力。&lt;/p&gt;
&lt;p&gt;而且&lt;strong&gt;在用户和开发者的角度，我不认同损失性能和体积，去低成本的实现一个勉强能用的App的这种做法，这基本就是对用户在做服从性测试（反正难用你也得用，没得选）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但作为公司的角度，只有成本才是唯一的考量，我也非常理解。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;幸好用&lt;code&gt;Flutter&lt;/code&gt;开发完没让我失望，真的很丝滑，打包体积也只有&lt;code&gt;19m&lt;/code&gt;  后续还能编译到其他平台。&lt;/p&gt;
&lt;p&gt;说起来，学 &lt;code&gt;Flutter&lt;/code&gt; 的过程也有点跳跃。&lt;/p&gt;
&lt;p&gt;本来我打算先看一遍文档的，但是问了一嘴做过 &lt;code&gt;FlutterApp&lt;/code&gt; 的朋友，他随手甩了我一个链接，是一个 &lt;code&gt;Flutter&lt;/code&gt; 的佬（&lt;code&gt;小呆呆&lt;/code&gt;）写的关于 &lt;code&gt;Getx&lt;/code&gt; 的使用简介。&lt;/p&gt;
&lt;p&gt;于是我顺着他的文章读了几篇，大概摸索清楚了在 &lt;code&gt;Flutter&lt;/code&gt; 中如何使用 &lt;code&gt;Getx&lt;/code&gt; 管理数据及交互逻辑层，虽然此时我还完全不清楚Flutter的语法，但也不妨碍我理解 &lt;code&gt;Flutter&lt;/code&gt; 中的一些设计方式。&lt;/p&gt;
&lt;p&gt;于是在简单建立了整体结构、几个页面、和基本的交互逻辑后，我用 &lt;code&gt;Getx&lt;/code&gt; 小小的重构了一下，直接加深了不少理解。&lt;/p&gt;
&lt;p&gt;中间我又注重看了几个关于 &lt;code&gt;Flutter&lt;/code&gt; 中 Controller、 State、布局约束、ChangeNotifier的视频讲解，其中最推荐的是 B站：王叔不秃，讲的忒好了。&lt;/p&gt;
&lt;p&gt;看的同时，每天下手去开发，然后就发现最不熟悉的反而成了UI组件🥲，因为我一开始就默认了开发UI界面没有难度，所以优先去看了状态管理怎么做....&lt;/p&gt;
&lt;p&gt;由于UI比较简单，重构的也比较早，所以并没有看到所谓的“嵌套地狱”，整体开发体验（Android Studio）是很不错的。&lt;/p&gt;
&lt;p&gt;中间磕磕碰碰的都是些 Android 原生配置需要去搜，这玩意是真一点也看不懂。不过幸好，只要配好了，开发时是不需要管的。&lt;/p&gt;
&lt;p&gt;总之，在大概 3-4 人日的工作量下完成了这个Flutter App。&lt;/p&gt;
&lt;p&gt;至于Vue2的大屏，Vue的生态用的实在是太多次，所以没有什么意外。&lt;/p&gt;
&lt;p&gt;这次接下来就当是测了一下开发的极限速度，因为活很急，最终总共开发时间不到20小时。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;PS：大屏实在是太适合做低代码了，但是似乎没见过什么源码进源码出的低代码平台，可能是每个公司都有自己的业务需求，做出来很难通用吧，所以我简单花了个架构图，准备后面自己搞一下。&lt;/em&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;</content:encoded></item><item><title><![CDATA[nuxt-content v3 使用及迁移记录]]></title><description><![CDATA[nuxt-content v3 使用及迁移记录]]></description><link>https://zzao.club/post/nuxt/nuxt-content-v3-use-migrate</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt-content-v3-use-migrate</guid><pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一篇关于 &lt;code&gt;nuxt/content&lt;/code&gt; v3 版本的使用和迁移指南，分享一下我在使用 &lt;code&gt;nuxt/content&lt;/code&gt; 时的一些经验和问题。&lt;/p&gt;
&lt;p&gt;此前，我基于 v2 版本完成了博客内文章的渲染，在发布了 v3 版本后，我第一时间更新到了最新的版本。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202412261015007.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在之前的文章&lt;a href=&quot;https://zzao.club/post/nuxt/nuxt3-obsidian-build-your-blog&quot;&gt;《基于 Nuxt3 + Obsidian 搭建个人博客》&lt;/a&gt; 中，我已经分享了全部配置以及用法。&lt;/p&gt;
&lt;p&gt;大概思路是使用 &lt;code&gt;Obsidian&lt;/code&gt; 在本地来管理的文件，在发布时在本地打包，将打包后的 &lt;code&gt;.output&lt;/code&gt; 文件部署到服务器上。这样完全没有破坏以前的写文章路径，同时通过一个简单的插件给文章页面加上了一个复制到公众号的选项，做到了在公众号和个人博客站展示一致。&lt;/p&gt;
&lt;p&gt;之前存在一个问题，解析非英文路径时，会把非英文的部分直接丢弃掉（或识别不出来），在 v2 中解决办法是在 &lt;code&gt;server/plugins&lt;/code&gt; 中增加一个 &lt;code&gt;hook&lt;/code&gt; ： &lt;code&gt;content:file:beforeParse&lt;/code&gt; ，手动去处理文件的原始内容，插入一个 &lt;code&gt;_path&lt;/code&gt; 属性，以此保留了原始的中文路径，算是一个十分膈应的解决方式。&lt;/p&gt;
&lt;p&gt;但 v3 版本是破坏性升级，核心的部分也已经被重构了，&lt;del&gt;所以 v2 使用的 hook 也不存在了。&lt;/del&gt; 更正：文档突然冒出来的，真的！还有两个 &lt;a href=&quot;https://content3.nuxt.dev/docs/advanced/hooks&quot;&gt;hook&lt;/a&gt;  &lt;code&gt;content:file:beforeParse&lt;/code&gt; 、&lt;code&gt;content:file:afterParse&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;虽然还有，但是 v3 版本已经不需要使用此方式解决！&lt;/p&gt;
&lt;p&gt;我在 &lt;a href=&quot;https://github.com/nuxt/content/issues/2889&quot;&gt;issues#2889&lt;/a&gt; 提出了这个问题后，很快也得到了 &lt;a href=&quot;https://github.com/nuxt/content/pull/2898&quot;&gt;pull#2889&lt;/a&gt; 解决，所以你如果使用的是 &lt;code&gt;v3.0.0-alpha.8&lt;/code&gt; 及以后的版本，想必都是可以的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
  content: {
    build: {
      // 虽然官方没有写 markdown: {} ，实际 ts 提示这个配置是必须的
	  markdown: {},
      pathMeta: {
        slugifyOptions: {
          // Keep everything except invalid chars, this will preserve Chinese characters 
          remove: /[$*+~()&apos;&quot;!\-=#?:@]/g,
        }
      }
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外 &lt;code&gt;v3.0.0-alpha.7&lt;/code&gt; 连内容搜索时的分页 api 都没支持好，所以最起码也要用 &lt;code&gt;alpha.8&lt;/code&gt; 及以后的版本了。&lt;/p&gt;
&lt;p&gt;配置层面，现在拓展性更强了一些，并且提升到了一个新的 &lt;code&gt;content.config.ts&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { defineCollection, z } from &apos;@nuxt/content&apos;

export const collections = {
  content: defineCollection({
	// 表示文章和目录是一对一的关系，一个文件会生成一个路由
    type: &apos;page&apos;,
    source: {
      include: &apos;**/*.md&apos;,
      exclude: [&apos;**/-*.md&apos;],
      // 设置读取content的根目录
      cwd: &apos;/Users/aatrox/notion/blog&apos;,
    },
    schema: z.object({
      date: z.date(),
      tags: z.array(z.string()),
      versions: z.array(z.string()),
    })
  })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;包含哪些文件，忽略哪些文件可以自由配置，新增的 &lt;code&gt;front matter&lt;/code&gt; 属性，如 &lt;code&gt;versions&lt;/code&gt;，也可以自行添加。&lt;/p&gt;
&lt;p&gt;v2 版本时，content 是在运行时把文件存储在 cache 目录下。而 v3 版本彻底抛弃了 file 模式，直接转为了 &lt;code&gt;sqlite&lt;/code&gt; 存储，所以相对应的，内容相关的查询和渲染 API 也都进行了修改&lt;/p&gt;
&lt;p&gt;&lt;code&gt;queryCollection&lt;/code&gt; 的替换&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 分页查询
queryCollection(&apos;content&apos;).skip(skip).limit(10).all()
// 查数量
count.value = await queryCollection(&apos;content&apos;).count()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useContent()&lt;/code&gt; 被移除了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;ContentDoc&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;ContentList&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;ContentNavigation&gt;&lt;/code&gt; and &lt;code&gt;&amp;#x3C;ContentQuery&gt;&lt;/code&gt; 这个三个组件也被移除&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fetchContentNavigation()&lt;/code&gt; API 被替换成了新的 &lt;code&gt;queryCollectionNavigation()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;markdown 文本中的 &lt;code&gt;._path&lt;/code&gt; 被改为了 &lt;code&gt;.path&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;一些和旧版 content 配套使用的 &lt;code&gt;sitemap&lt;/code&gt; 逻辑也应该删除 &lt;code&gt;/server/routes/sitemap.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当在 &lt;code&gt;pages/xxx/[...slug].vue&lt;/code&gt; 渲染文章时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data: page } = await useAsyncData(route.path, () =&gt; {
    return queryCollection(&apos;content&apos;).path(decodeURI(route.path)).first()
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;需要使用 &lt;code&gt;decodeURI&lt;/code&gt; 才能在数据库中匹配到对应的文章。因为路径中包含中文&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更详细的迁移说明，可以移步&lt;a href=&quot;https://content3.nuxt.dev/docs/getting-started/migration&quot;&gt;官方迁移文档&lt;/a&gt;。（或者等我想起来，会更新在博客站中）&lt;/p&gt;
&lt;p&gt;现在使用 v3 开发模式时，会在根目录下创建一个&lt;code&gt;.data&lt;/code&gt; 目录，然后生成一个 &lt;code&gt;sqlite&lt;/code&gt; 文件用于存储文章内容。&lt;/p&gt;
&lt;p&gt;并且这个数据库不需要你来管理，只要配置好内容来源和规则。&lt;/p&gt;
&lt;p&gt;修改为最新版的 api 后，我又增加了一项改动，把从本地文件获取文章改为了从 github 仓库获取。&lt;/p&gt;
&lt;p&gt;原因是，以前我在公司和家里用的同一个笔记本，没有感知到什么问题。现在家里添置了一个新的 &lt;code&gt;macmini&lt;/code&gt; ，我在家里配置好了同样的环境后，两边带着个 &lt;code&gt;output&lt;/code&gt; 文件在 &lt;code&gt;git&lt;/code&gt; 上太诡异了，而且本地的路径也不一致。&lt;/p&gt;
&lt;p&gt;也就是需要修改一下 &lt;code&gt;content.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;source: {
      include: &apos;blog/**/*.md&apos;,
      exclude: [&apos;blog/**/-*.md&apos;],
      prefix: &apos;/post&apos;,
      // cwd: process.env.CONTENT_FS_PATH,
      repository: &apos;https://github.com/aatrooox/xxxxx&apos;,
      authToken: process.env.CONTENT_REPO_TOKEN
    },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置上自己的仓库地址，以及对应的 authToken.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;authToken 的配置在 github =&gt; 点击我的头像 =&gt; settings =&gt; 左侧最下方 &lt;code&gt;developer settings&lt;/code&gt; =&gt; Personal access tokens =&gt; fine-grained tokens&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202412261015008.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;为某一个库生成 token，同时不要忘记给他放开 read-only 权限&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202412261015009.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后生成是可以看到有哪些权限&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202412261015010.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你生成了 &lt;code&gt;token&lt;/code&gt;，但是没给权限，最后使用 &lt;code&gt;content&lt;/code&gt; 运行时报的错压根不会让你想到是 token 的权限问题。别问我怎么知道的。&lt;/p&gt;
&lt;p&gt;如果本地文件比较多的，建议给本地的知识库划分一下仓库。一是 obsidian 自己如果文件太多，也会有性能问题，二是 nuxt/content 要去拉取你的代码，太多了拉的也慢。三是划分好后，哪些文件时对外展示的也比较清晰。&lt;/p&gt;
&lt;p&gt;配置 &lt;code&gt;content&lt;/code&gt; 的 &lt;code&gt;repo&lt;/code&gt; 时，就配这个需要对外生成文章的仓库就好了。&lt;/p&gt;
&lt;p&gt;当运行 &lt;code&gt;dev&lt;/code&gt; 时，刚才提到的本地数据库目录 &lt;code&gt;.data&lt;/code&gt;，下会多出一个 &lt;code&gt;github-aatrooox-xxx-main&lt;/code&gt;，里面就是所有原始的文件。当应用执行第一个查询检索内容时，就会读取上一步已经生成的 &lt;code&gt;Dump&lt;/code&gt; 恢复到目标数据库中，同时有一套检查机制确保数据库中为最新的内容和避免重复导入。在客户端导航时，会在浏览器中初始化本地 SQLite，初始化后所有查询就是在本地进行了，所以 v3 的查询要比 v2 感觉上快很多。&lt;/p&gt;
&lt;p&gt;以上就是 v3 的使用指南，基于官方的迁移说明，并且还解决了一些中文互联网才会有问题。&lt;/p&gt;
&lt;p&gt;另外现在最新版处于不稳定状态，建议观望一下再升级，并且 nuxt/content 在网络上热度不高，出现奇怪的问题都没地儿找答案。&lt;/p&gt;
&lt;p&gt;如果你恰好也是 Nuxt 的使用者，欢迎关注我，一起来讨论。&lt;/p&gt;
&lt;p&gt;👋👋&lt;/p&gt;</content:encoded></item><item><title><![CDATA[如何防止别人把域名解析到自己的服务器公网 ip 上]]></title><description><![CDATA[我遇到了一个很奇葩的问题，以前没遇到过：就是有个网站解析到了我服务器 ip 上。]]></description><link>https://zzao.club/post/zzao/keep-domain-safe</link><guid isPermaLink="true">https://zzao.club/post/zzao/keep-domain-safe</guid><pubDate>Wed, 04 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我遇到了一个很奇葩的问题，以前没遇到过：就是有个网站解析到了我服务器 ip 上。&lt;/p&gt;
&lt;p&gt;我是怎么发现的呢，一开始我是在看 CDN 的监控指标，里面有个 referer 来源，本来应该都是我自己的域名，但是跑出来一个陌生的域名。&lt;/p&gt;
&lt;p&gt;于是我打开了这个域名，好嘛，这不就是我的网站么🥲&lt;/p&gt;
&lt;p&gt;这个域名还是个子域名，主域名上什么也没有，也看不到他的购买信息。本来想去问候一下。&lt;/p&gt;
&lt;p&gt;我就开始寻找解决办法，求助 AI，求助身边的运维朋友，可惜朋友没回...&lt;/p&gt;
&lt;p&gt;然后我就先把 cookie 的 samesite、domain、secure 设置好，确保接口不会被一直调用。但是我文章里的图片是个问题啊...  因为我把图片都存在了腾讯云上，他这个网站访问的时候，也会走我的流量，但是我当时又没找到办法解决，只好把主域名的 Nginx 配置先停掉了&lt;/p&gt;
&lt;p&gt;过了几天后，我又想起这个事儿，然后又换了个 AI 问了一遍，还是让我设置 Nginx 就行&lt;/p&gt;
&lt;p&gt;显示加了一个&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;add_header X-Frame-Options &quot;SAMEORIGIN&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个只是不允许其他网站嵌入&lt;/p&gt;
&lt;p&gt;然后又设置的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;server {
	listen 80 default_server;  # 这个 server 块是默认的
	server_name _;  # 匹配所有未定义的域名

	return 301 https://zzao.club$request_uri;  # 重定向到 HTTPS
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即 &lt;code&gt;80&lt;/code&gt; 端口上如果有我没配的域名，则重定向到我的域名&lt;/p&gt;
&lt;p&gt;当时尝试了，没成功，应该是因为加了 &lt;code&gt;https&lt;/code&gt;，导致规则没走这条。大意了！&lt;/p&gt;
&lt;p&gt;这次把 &lt;code&gt;https&lt;/code&gt; 换成 &lt;code&gt;http&lt;/code&gt;，发现其实是生效的&lt;/p&gt;
&lt;p&gt;于是照猫画虎，把 &lt;code&gt;443&lt;/code&gt; 端口也堵上&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;server {
	listen 443 default_server;  # 这个 server 块是默认的
	server_name _;  # 匹配所有未定义的域名
	
	ssl_certificate /etc/nginx/aaa.pem;  # 指定证书的位置，绝对路径
	ssl_certificate_key /etc/nginx/bbb.key;  # 绝对路径，同上

	return 301 https://zzao.club$request_uri;  # 重定向到 HTTPS
}
&lt;/code&gt;&lt;/pre&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;不过有了这次的经历，Nginx 的配置倒又学到了一点😛&lt;/p&gt;
&lt;p&gt;sss&lt;/p&gt;</content:encoded></item><item><title><![CDATA[新买了 Mac Mini !!]]></title><description><![CDATA[很久没有更新设备了，这次新买了最低配的 mac mini 16+256。教育优惠 3749 到手！]]></description><link>https://zzao.club/post/daily/got-mac-mini-m4</link><guid isPermaLink="true">https://zzao.club/post/daily/got-mac-mini-m4</guid><pubDate>Tue, 03 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很久没有更新设备了，这次新买了最低配的 &lt;code&gt;mac mini&lt;/code&gt; 16+256。教育优惠 &lt;code&gt;3749&lt;/code&gt; 到手！&lt;/p&gt;
&lt;p&gt;然后幸亏家里还有个一直没卖没扔的苹果鼠标，以为开机没蓝牙设备没法用呢！开机直接懵逼了。&lt;/p&gt;
&lt;p&gt;第二天才想过来，有扩展坞的话，直接用 usb 的键盘和鼠标连接也是可以的...&lt;/p&gt;
&lt;p&gt;然后在咸鱼花了 &lt;code&gt;100&lt;/code&gt; 元，配了个罗技 &lt;code&gt;K380&lt;/code&gt; + &lt;code&gt;M240&lt;/code&gt;，作为蓝牙设备使用。&lt;/p&gt;
&lt;p&gt;还没到货，后面会补图。&lt;/p&gt;
&lt;p&gt;之前同事用 k380 用了几年一直没换，淘宝京东的出货量也很大，想必质量应该是可以的。&lt;/p&gt;
&lt;p&gt;使用我的 M2Air 迁移助手迁移的时候，光用户文稿等信息就 200G+，直接迁不过去。&lt;/p&gt;
&lt;p&gt;索性就从头开始安装了，只把一些系统设置迁移过去了&lt;/p&gt;
&lt;p&gt;后续就作为在家里的主力机器使用，公司写了代码或者文章，没写完的再回家接着写。&lt;/p&gt;
&lt;p&gt;因为主机很小，线也不多，显得桌面也比较干净了。&lt;/p&gt;
&lt;p&gt;显示器的话用的是很多年前的微星 2k 显示器，估计近期也不会换了， 等有预算之后，可以换个 4K的红米显示器（如果支持雷电口的话）&lt;/p&gt;
&lt;p&gt;以前的冲动消费太多了，这次仔细思考之后，3749 真是很有性价比的生产力工具了。（玩炉石也是极😉）&lt;/p&gt;
&lt;p&gt;现在每天敲代码和写文章，基本也点习惯的痕迹了，希望能保持下去吧&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt3全栈开发 · $fetch、useFetch、useAsyncData 你用对了吗？]]></title><description><![CDATA[Nuxt3中 $fetch、useFetch、useAsyncData 的使用区别，各自的用法，以及最佳实践]]></description><link>https://zzao.club/post/nuxt/nuxt3-fetch-usefetch-useasyncdata</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3-fetch-usefetch-useasyncdata</guid><pubDate>Wed, 27 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nuxt3 中有三种获取数据的方式，看起来有点绕，那实际使用中有什么区别，应该怎样使用呢？&lt;/p&gt;
&lt;h2&gt;$fetch&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;$fetch&lt;/code&gt; 基于 &lt;code&gt;ofetch&lt;/code&gt; ，&lt;code&gt;ofetch&lt;/code&gt; 是一个类似 &lt;code&gt;axios&lt;/code&gt; 的请求库，可以运行在 node、浏览器、workers 上。&lt;/p&gt;
&lt;p&gt;所以它的用法类似原生 fetch 、axios，在 Nuxt3 中全局可用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 app 中直接向 server 内的 api 发起请求&lt;/li&gt;
&lt;li&gt;在 app 中向其他服务发出请求&lt;/li&gt;
&lt;li&gt;在 server 的一个接口中向另一个接口请求&lt;/li&gt;
&lt;li&gt;在 server 的一个接口中向其他服务发出请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，是一个底层的请求库。&lt;/p&gt;
&lt;p&gt;用它是最简单的方法。&lt;/p&gt;
&lt;p&gt;但 Nuxt 是一个可以在服务器和客户端两个环境下运行同构代码的框架，如果在 setup 中使用 &lt;code&gt;$fetch&lt;/code&gt; 来获取数据，可能会导致执行两次，一次在服务器上由 nitro 渲染 html 时，另一次是在客户端水合时。&lt;/p&gt;
&lt;p&gt;所以水合是一个必须要理解的过程：&lt;/p&gt;
&lt;p&gt;当在浏览器打开一个页面时，首先会在服务端渲染（SSR)，渲染后的&lt;strong&gt;完整 html 代码&lt;/strong&gt;会被发送到客户端。此时已经在服务端拿到请求的数据了，所以客户端收到 HTML后，用户已经能够看到内容。&lt;/p&gt;
&lt;p&gt;对比单页应用（SPA），浏览器只是加载了一个 带有根元素的的 html 页面，所以此时页面是空白的，还需要在加载完相关 JS文件后，由 JS 去插入内容。所以单页应用只要在根元素 &lt;code&gt;div id = app&lt;/code&gt; 里写点内容（如loading、骨架），就会早于实际内容出现，达到不白屏的效果。&lt;/p&gt;
&lt;p&gt;回到 &lt;code&gt;Nuxt&lt;/code&gt; ，用户看到内容后，此时页面还无法交互，因为仅仅是渲染了 &lt;code&gt;HTML&lt;/code&gt;，&lt;code&gt;Vue&lt;/code&gt; 相关的逻辑还在 &lt;code&gt;JS&lt;/code&gt; 里，需要下载和执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下载和执行时被 Vue 接管 HTML 的过程就叫水合&lt;/strong&gt;，水合后界面就可以响应用户的交互了。&lt;/p&gt;
&lt;p&gt;有了渲染方式的差异，才会有其他的请求方式来契合这种渲染方式。&lt;/p&gt;
&lt;h2&gt;useAsyncData&lt;/h2&gt;
&lt;p&gt;从 &lt;code&gt;vue2&lt;/code&gt; 到 &lt;code&gt;vue3&lt;/code&gt;，我们都知道多了个选项式和组合式的区别。&lt;/p&gt;
&lt;p&gt;选项式中通常把功能集中在一个组件或函数中，每个组件都有 &lt;code&gt;data&lt;/code&gt;、&lt;code&gt;methods&lt;/code&gt;，组件内定义属性都会暴漏在函数内部的 &lt;code&gt;this&lt;/code&gt; 上，指向当前实例。它的特点是，不关心响应式细节，强制按照选项来组织代码，你的后端同事看了 &lt;code&gt;Vue&lt;/code&gt; 之后都表示很简单。&lt;/p&gt;
&lt;p&gt;而组合式（&lt;code&gt;composable&lt;/code&gt;）的核心思想是直接在函数作用于内定义响应式变量，要比选项式自由和高效的多。&lt;/p&gt;
&lt;p&gt;所以 Vue3 的代码中，经常可以看到 &lt;code&gt;UseXXXX&lt;/code&gt; 这类的函数，其内部就包含响应式变量，也就类似选项式代码中的 &lt;code&gt;data&lt;/code&gt; 和 &lt;code&gt;method&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以其内部的响应式变量发生变化时，通常会有一个对应的逻辑随之发生变化，可能是另一个响应式变量，也可能是与之对应的&lt;code&gt;Template&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;useAsyncData&lt;/code&gt; 这个 &lt;code&gt;composable&lt;/code&gt; ，也有类似的功能和效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data, error, clear, status, refresh } = await useAsyncData(&apos;users&apos;, () =&gt; myGetFunction(&apos;users&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useAsyncData&lt;/code&gt; 第一个参数是唯一键，用于&lt;strong&gt;缓存&lt;/strong&gt;第二个参数的&lt;strong&gt;响应&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以用 &lt;code&gt;useAsyncData&lt;/code&gt; 时，在 &lt;code&gt;SSR&lt;/code&gt; 时和在 &lt;code&gt;水合&lt;/code&gt; 时，不会发生两次重复的请求。可以保证渲染的一致性。&lt;/p&gt;
&lt;p&gt;其次，因为第二个参数只是一个获取数据的匿名函数，所以你可以用它来请求任意的服务，比如你用了其他 CMS服务来管理数据，这时候就应该使用 &lt;code&gt;useAsyncData&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这东西竟然有五个返回值，看看都有啥用，怎么就响应式了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;error&lt;/code&gt;、&lt;code&gt;status&lt;/code&gt; 这三个值，都是 Vue 的引用（ Vue refs accessible），也就是说类似于你用 &lt;code&gt;ref&lt;/code&gt; 提前定义好了一样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const data = ref()
const error = ref()
const status = ref(&apos;idle&apos;) // 还没请求

const fetchData = async () =&gt; {
	status.value = &apos;pendding&apos;
	const data = await fetch(&apos;xxxx&apos;).catch( err =&gt; {
		error.value = err
		status.value = &apos;error&apos;
	})
	...
	data.value = res.data
	status.value = &apos;success&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样看能明白了吧。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;data&lt;/code&gt; 就是我们接收返回后的数据的  &lt;code&gt;Ref&lt;/code&gt; 引用，&lt;code&gt;error&lt;/code&gt; 同理，&lt;code&gt;status&lt;/code&gt; 则是类似于在 Vue2 中使用一个 &lt;code&gt;isFetch&lt;/code&gt; 变量去管理 &lt;code&gt;loading&lt;/code&gt; 状态。&lt;/p&gt;
&lt;p&gt;也可以直接给 data 重命名一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data: userList, error, clear, status, refresh } = await useAsyncData(&apos;users&apos;, () =&gt; myGetFunction(&apos;users&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过要注意，这个 &lt;code&gt;data.value&lt;/code&gt; 是什么，和第二个参数返回的数据是什么有关，比如你的接口固定返回：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;{
	code: 200,
	data: [],
	msg: &apos;ok&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那要想取得列表渲染要用的数据，应该是 &lt;code&gt;userList.value?.data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因为渲染方式的特点，&lt;code&gt;useAsyncData&lt;/code&gt; 还可以传入第三个参数 &lt;code&gt;options&lt;/code&gt; 来控制其行为。&lt;/p&gt;
&lt;h3&gt;lazy&lt;/h3&gt;
&lt;p&gt;默认情况下，页面使用了 &lt;code&gt;useAsyncData&lt;/code&gt; 来获取数据，这个 &lt;code&gt;composable&lt;/code&gt; 会等待异步函数的解析，然后再导航到新的页面。&lt;/p&gt;
&lt;p&gt;解析会耗时，所以你的导航动作就会有延迟，给人一种：&lt;strong&gt;点了，但是没立马动起来的迟滞感&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lazy&lt;/code&gt; 选项可以使其忽略异步函数的解析。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data: userList, error, clear, status, refresh } = await useAsyncData(&apos;users&apos;, () =&gt; myGetFunction(&apos;users&apos;), { lazy: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，当你点击进入一个新的页面时，导航不会被阻塞，但进入后内容可能还没拿到，所以需要使用 &lt;code&gt;status&lt;/code&gt; 来加载 &lt;code&gt;loading&lt;/code&gt;、&lt;code&gt;骨架组件&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我觉得应该没人想在点击后阻塞导航吧，所以这个 &lt;code&gt;lazy&lt;/code&gt; 建议一直开启。&lt;/p&gt;
&lt;h3&gt;server&lt;/h3&gt;
&lt;p&gt;上一篇关于在 Nuxt 中使用 Prisma 的文章里，我提到了本地有个 &lt;code&gt;dev.db&lt;/code&gt; ，线上也有一个 &lt;code&gt;prod.db&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;两个数据库里存的东西肯定是不一样嘛，因为本地需要测试。所以我本地的 &lt;code&gt;userList&lt;/code&gt; 里是 &lt;strong&gt;张三&lt;/strong&gt;，线上的 &lt;code&gt;userList&lt;/code&gt; 是&lt;strong&gt;李四&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但在 &lt;code&gt;Nuxt&lt;/code&gt; 打包时，会 &lt;code&gt;prerender&lt;/code&gt;，预渲染！（我需要本地打包的）&lt;/p&gt;
&lt;p&gt;也就是先把接口请求一遍，把真实的 &lt;code&gt;HTML&lt;/code&gt; 先给组装好，并且还有缓存。因为是为了 SEO，方便搜索引擎快速抓取到页面的内容。&lt;/p&gt;
&lt;p&gt;于是，在线上打开用户列表页时，我的张三被显示出来了，因为他就是 HTML 里的内容。&lt;/p&gt;
&lt;p&gt;这个时候就需要另一个选项： &lt;code&gt;server&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当设置 &lt;code&gt;server: false&lt;/code&gt; 时，第一次渲染就不会去请求数据，也就是会渲染出一个空的用户列表页。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data: userList, error, clear, status, refresh } = await useAsyncData(&apos;users&apos;, () =&gt; myGetFunction(&apos;users&apos;), { lazy: true, server: false })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;watch&lt;/h3&gt;
&lt;p&gt;看到这，我也没看出来 useAsyncData 响应个啥了啊，不就是帮我省下了创建接收数据的 &lt;code&gt;ref&lt;/code&gt; 和 管理状态的 &lt;code&gt;ref&lt;/code&gt; 的功夫？&lt;/p&gt;
&lt;p&gt;那我们再把&lt;strong&gt;获取列表这个场景&lt;/strong&gt;丰富一下。&lt;/p&gt;
&lt;p&gt;用户多了，有分页了，应该怎么处理？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const page = ref(1)
const changePage = () =&gt; {
	myFetchData()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果再加个类型的筛选，日期的筛选等等一切和重新获取数据相关的响应式变量，都要写同样的代码。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;useAsyncData&lt;/code&gt; 支持直接 &lt;code&gt;watch&lt;/code&gt; 响应式变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const page = ref(1)
const { data: userList, error, clear, status, refresh } = await useAsyncData(&apos;users&apos;, () =&gt; myGetFunction(&apos;users&apos;), { lazy: true, watch: [page, tags] })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;page&lt;/code&gt; 发生变化时，&lt;code&gt;useAsyncData&lt;/code&gt; 就会重新执行它的 &lt;code&gt;handler&lt;/code&gt; ，做到刷新数据。&lt;/p&gt;
&lt;p&gt;代码量又减少了不少&lt;/p&gt;
&lt;h3&gt;其他选项&lt;/h3&gt;
&lt;p&gt;和 &lt;code&gt;vue&lt;/code&gt; 的 &lt;code&gt;watch&lt;/code&gt; 相比，&lt;code&gt;useAsyncData&lt;/code&gt; 也有一个 &lt;code&gt;immediate&lt;/code&gt; 选项，只不过它默认是 &lt;code&gt;true&lt;/code&gt; ，你可以设置为 &lt;code&gt;false&lt;/code&gt; 来阻止立即触发请求。&lt;/p&gt;
&lt;p&gt;还有一种场景，使用一个接口获取一组比较大的数据比如文章时，有时候不需要文章的内容，每次都传输内容的话，数据量太大了，影响传输速度。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;code&gt;pick&lt;/code&gt; 选项，选取指定的键：&lt;code&gt;pick: [&quot;title&quot;, &quot;description&quot;]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;还有 &lt;code&gt;deep&lt;/code&gt; 选项，默认为 &lt;code&gt;true&lt;/code&gt; ，如果不需要深度深度响应时，可以设置为 &lt;code&gt;false&lt;/code&gt; 以提高性能。&lt;/p&gt;
&lt;p&gt;其他选项就不一一介绍了，我直接放在这里，后续发现比较有用的场景，再来分享。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type AsyncDataOptions&amp;#x3C;DataT&gt; = {
  server?: boolean
  lazy?: boolean
  immediate?: boolean
  deep?: boolean
  dedupe?: &apos;cancel&apos; | &apos;defer&apos;
  default?: () =&gt; DataT | Ref&amp;#x3C;DataT&gt; | null
  transform?: (input: DataT) =&gt; DataT | Promise&amp;#x3C;DataT&gt;
  pick?: string[]
  watch?: WatchSource[]
  getCachedData?: (key: string, nuxtApp: NuxtApp) =&gt; DataT
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以确定，&lt;code&gt;useAsyncData&lt;/code&gt; 可以帮助缓存数据，防止多次请求，保证渲染的一致性。提供响应式的 &lt;code&gt;data&lt;/code&gt;、&lt;code&gt;error&lt;/code&gt;、&lt;code&gt;status&lt;/code&gt; 来完成页面的渲染，同时提供一些选项来控制其行为。&lt;/p&gt;
&lt;p&gt;最后再来捋一捋什么场景下使用什么选项的问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411271747999.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PS：这图我在 Youtube 刷到的，但是没截屏，所以凭借印象又画了一遍&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那 &lt;code&gt;useFetch&lt;/code&gt; 还有什么活能整吗？&lt;/p&gt;
&lt;h2&gt;useFetch&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;useFetch&lt;/code&gt; 的整活就像是：&lt;/p&gt;
&lt;p&gt;虽然在 &lt;code&gt;script&lt;/code&gt; 需要使用 &lt;code&gt;.value&lt;/code&gt; 获取响应式数据，但 &lt;code&gt;template&lt;/code&gt; 中不需要，因为 &lt;code&gt;Vue&lt;/code&gt; 帮你处理了。&lt;/p&gt;
&lt;p&gt;以前 &lt;code&gt;props&lt;/code&gt; 解构会失去响应式，现在（3.5+）解构也能直接用了。&lt;/p&gt;
&lt;p&gt;是的，它就是个语法糖一样，是 &lt;code&gt;useAsyncData&lt;/code&gt; 和 &lt;code&gt;$fetch&lt;/code&gt; 的包装器。&lt;/p&gt;
&lt;p&gt;它不用传第一个 &lt;code&gt;key&lt;/code&gt; 值，会根据 url 和选项自动生成，并且可以推断 API 的响应类型。&lt;/p&gt;
&lt;p&gt;同时有着和 useAsyncData 一样的第三个参数（选项），但做了一些增强。&lt;/p&gt;
&lt;p&gt;还是拿刚才获取用户列表的场景举例，我们监听 &lt;code&gt;page&lt;/code&gt;、&lt;code&gt;tags&lt;/code&gt;，变化时会再次请求。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;useFetch&lt;/code&gt; 中可以再进一步：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const page = ref(1)
const { data: userList, error, clear, status, refresh } = await useFetch(&apos;/api/user/list&apos;, { page }, { lazy: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们只需要传入 &lt;code&gt;page&lt;/code&gt; 这个响应式变量，&lt;code&gt;useFetch&lt;/code&gt; 就会追踪到这个响应式变量。&lt;/p&gt;
&lt;p&gt;类似 &lt;code&gt;watch&lt;/code&gt; 和 &lt;code&gt;watchEffect&lt;/code&gt;。一个需要显式的传入，一个会自动的追踪。&lt;/p&gt;
&lt;p&gt;但是这里要注意：&lt;code&gt;{ page }&lt;/code&gt; 等价于 &lt;code&gt;{ page: page }&lt;/code&gt;， 而不是 &lt;code&gt;{ page: page.value }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;page.value&lt;/code&gt; 只是一个值！&lt;/strong&gt; 所以要传入的是 &lt;code&gt;page&lt;/code&gt; 这个 &lt;code&gt;ref对象&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用 &lt;code&gt;useFetch&lt;/code&gt; 后代码被进一步简化。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以在思考使用 &lt;code&gt;useFetch&lt;/code&gt; 还是 &lt;code&gt;$fetch&lt;/code&gt;  来进行接口请求时就十分明了了：&lt;/p&gt;
&lt;p&gt;一般基于用户的交互才去做出反应的，就用 &lt;code&gt;$fetch&lt;/code&gt; 就可以。&lt;/p&gt;
&lt;p&gt;如果基于页面状态需要重复获取数据的，就用 &lt;code&gt;useFetch&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;但刚才也强调了，传入的是一个 &lt;code&gt;ref对象&lt;/code&gt; 才会时 &lt;code&gt;useFetch&lt;/code&gt; 正常的响应式的自动请求，所以如果是&lt;strong&gt;想统一请求方式&lt;/strong&gt;，也可以选择直接传值给 &lt;code&gt;useFetch&lt;/code&gt; ，这样他就像一个普通的增强版 &lt;code&gt;$fetch&lt;/code&gt; 一样了。&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;useFetch&lt;/code&gt; 返回的值还是个响应式的，使用 &lt;code&gt;$fetch&lt;/code&gt; 的场景很多都不需要返回值再触发其他响应，所以显得有些&quot;多余&quot;。&lt;/p&gt;
&lt;p&gt;怎么取舍就看自己了。&lt;/p&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;p&gt;我在项目中以使用 &lt;code&gt;useFetch&lt;/code&gt;为主 , 那怎么封装 &lt;code&gt;useFetch&lt;/code&gt; 用起来才最顺手呢？&lt;/p&gt;
&lt;p&gt;经过我自己的使用，以及搜索多个关于 &lt;code&gt;useFetch&lt;/code&gt; 的使用。最后在最少三个博客站内发现了同样的封装代码，究竟谁是作者我也不清楚，代码里也没有说明，这里直接贴给大家，再分析如何使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;composables/useHttp.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import type {FetchError, FetchResponse, SearchParameters} from &apos;ofetch&apos;;
import {hash} from &apos;ohash&apos;;
import type {AsyncData, UseFetchOptions} from &apos;#app&apos;;
import type {KeysOf} from &apos;#app/composables/asyncData&apos;;
type UrlType = string | Request | Ref&amp;#x3C;string | Request&gt; | (() =&gt; string | Request);

type HttpOption&amp;#x3C;T&gt; = UseFetchOptions&amp;#x3C;ResOptions&amp;#x3C;T&gt;, T, KeysOf&amp;#x3C;T&gt;, any&gt;;
interface ResOptions&amp;#x3C;T&gt; {
    data: T;
    code: number;
    message: boolean;
    err?: string[];
}

function handleError&amp;#x3C;T&gt;(
    _method: string | undefined,
    _response: FetchResponse&amp;#x3C;ResOptions&amp;#x3C;T&gt;&gt; &amp;#x26; FetchResponse&amp;#x3C;any&gt;,
) {
    // Implement error handling logic here
    if (_response?._data?.statusCode === 401) { 
      // setUser(&apos;&apos;)
    }
    console.error(`[useHttp] [error] ${_method}:`, _response);
}

function checkRef(obj: Record&amp;#x3C;string, any&gt;) {
    return Object.keys(obj).some(key =&gt; isRef(obj[key]));
}

function fetch&amp;#x3C;T&gt;(url: UrlType, opts: HttpOption&amp;#x3C;T&gt;): AsyncData&amp;#x3C;ResOptions&amp;#x3C;T&gt;, FetchError&amp;#x3C;ResOptions&amp;#x3C;T&gt;&gt;&gt; {
    // Check the `key` option
    const { key, params, watch } = opts;
    if (!key &amp;#x26;&amp;#x26; ((params &amp;#x26;&amp;#x26; checkRef(params)) || (watch &amp;#x26;&amp;#x26; checkRef(watch))))
        console.error(&apos;\x1B[31m%s\x1B[0m %s&apos;, &apos;[useHttp] [error]&apos;, &apos;The `key` option is required when `params` or `watch` has ref properties, please set a unique key for the current request.&apos;);

    const options = opts as UseFetchOptions&amp;#x3C;ResOptions&amp;#x3C;T&gt;&gt;;
    options.lazy = options.lazy ?? true;

    // const { baseUrl } = useRuntimeConfig().public;

    return useFetch&amp;#x3C;ResOptions&amp;#x3C;T&gt;&gt;(url, {
        // Request interception
        onRequest({ options }) {
            // options.baseURL = baseUrl;
            // Set the base URL
        },
        // Response interception
        onResponse(_context) {
            // Handle the response
        },
        // Error interception
        onResponseError({ response, options: { method } }) {
            handleError&amp;#x3C;T&gt;(method, response);
        },
        // Set the cache key
        key: key ?? hash([&apos;api-fetch&apos;, url, JSON.stringify({ method: options.method, params: options.params })]),
        // Merge the options
        ...options,
    }) as AsyncData&amp;#x3C;ResOptions&amp;#x3C;T&gt;, FetchError&amp;#x3C;ResOptions&amp;#x3C;T&gt;&gt;&gt;;
}

export const $http = {
    get: &amp;#x3C;T&gt;(url: UrlType, params?: SearchParameters, option?: HttpOption&amp;#x3C;T&gt;) =&gt; {
        return fetch&amp;#x3C;T&gt;(url, { method: &apos;get&apos;, params, ...option });
    },

    post: &amp;#x3C;T&gt;(url: UrlType, body?: RequestInit[&apos;body&apos;] | Record&amp;#x3C;string, any&gt;, option?: HttpOption&amp;#x3C;T&gt;) =&gt; {
        return fetch&amp;#x3C;T&gt;(url, { method: &apos;post&apos;, body, ...option });
    }
};

export default function UseHttp() {
    return {
        $http,
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总体封装的很简单，基本没啥看不懂的点，细节还是要自己去扩充。&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;ResOptions&lt;/code&gt; 需要根据自己的返回值类型修改&lt;/p&gt;
&lt;p&gt;&lt;code&gt;handleError&lt;/code&gt; 负责在 &lt;code&gt;onResponseError&lt;/code&gt; 时处理错误。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;onRequest&lt;/code&gt; 可以自行添加比如 &lt;code&gt;baseUrl&lt;/code&gt; 、自定义 &lt;code&gt;header&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lazy&lt;/code&gt; 默认开启是最好用的，loading（status）状态自己维护&lt;/p&gt;
&lt;p&gt;使用时的几种情况大概是这样：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基于请求传入的 ref 对象，自动重新请求&lt;/li&gt;
&lt;li&gt;watch 其他 ref 对象，以重新请求&lt;/li&gt;
&lt;li&gt;不需要马上在页面中展示的，&lt;code&gt;server: false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;除此之外基本用默认配置就可以了&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// hash(&apos;memo-list-search&apos;)
const { data: memoList, error, status } = await $http.get(&apos;/api/v1/memo/list&apos;, { page: 1, size: 100 }, { key: hash(&apos;memo-list-search&apos;), server: false, watch: [user, refreshKey] })

const { data: memoList, error, status } = await $http.get(&apos;/api/v1/memo/list&apos;, { page, size: 100 }, { key: hash(&apos;memo-list-search&apos;), server: false })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点是 &lt;code&gt;key&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果需要响应式请求，则&lt;strong&gt;必须传入 key 值&lt;/strong&gt;，传入的对象会被 &lt;code&gt;checkRef(params)&lt;/code&gt; 检测是不是 &lt;code&gt;Ref&lt;/code&gt; ，其他选项（options）还是可以正常传入。&lt;/p&gt;
&lt;p&gt;虽然代码里有个默认的 key 值，只是会打印一个错误，但这个 key 还是&lt;strong&gt;建议手动传入&lt;/strong&gt;，以免造成key值重复时，发生错误的缓存问题。&lt;/p&gt;
&lt;p&gt;虽然这里封装了 &lt;code&gt;handleError&lt;/code&gt; ，但实际上我没发现没什么用，原因是我用的 &lt;code&gt;primevue/toast&lt;/code&gt; 目前不能套在 &lt;code&gt;useFetch&lt;/code&gt; 这个 &lt;code&gt;composable&lt;/code&gt; 中使用。我基本尝试了 &lt;code&gt;github&lt;/code&gt; 和 &lt;code&gt;stackoverflow&lt;/code&gt; 能搜到的几种方式都没有奏效。&lt;/p&gt;
&lt;p&gt;而发生错误时提示出来基本是必做的一个动作，所以我只能把 &lt;code&gt;show error toast&lt;/code&gt; 这个动作，放在了另一个 &lt;code&gt;composable&lt;/code&gt; （useErrorDispose）中，在使用 $http 的 vue 文件中，使用这个 &lt;code&gt;useErrorDispose&lt;/code&gt; 处理 &lt;code&gt;error.value&lt;/code&gt;，以及本地的缓存信息。也算是完成了我的需求。&lt;/p&gt;
&lt;p&gt;另外，请求时，（公司中）一般会设置 header 中携带 token，但实际上没有比 cookie 更好用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;{
    httpOnly: true,
    sameSite: isProd ? &apos;strict&apos; : &apos;lax&apos;,
    maxAge: 2592000, // maxAge 优先级高， expires 受客户端时间的影响
    secure: true,
    domain: &apos;abc.com&apos;,
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样设置后，也能节省不少（操作 localstorage 的）代码，还比使用 &lt;code&gt;localstorage&lt;/code&gt; 存储 &lt;code&gt;token&lt;/code&gt; 再放在 &lt;code&gt;header&lt;/code&gt; 上更安全一些。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;nuxt&lt;/code&gt;（nitro）中操作 &lt;code&gt;Cookie&lt;/code&gt; 也是十分简单的，可以自行了解一下。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是本人在使用 &lt;code&gt;$fetch&lt;/code&gt;、&lt;code&gt;useFetch&lt;/code&gt;、&lt;code&gt;useAsyncData&lt;/code&gt;时的一些经验分享，希望能够帮助到你。&lt;/p&gt;
&lt;p&gt;同时，如果你有更多的经验，也希望在评论区指正文中的错误，让大家学到更多&lt;/p&gt;
&lt;p&gt;更多 Nuxt 最新的全栈开发内容，欢迎关注「&lt;strong&gt;早早集市&lt;/strong&gt;」&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt3全栈开发 · 如何使用Prisma+Sqlite]]></title><description><![CDATA[Nuxt3全栈开发时，如何使用Prisma来管理Sqlite、Mysql等数据库]]></description><link>https://zzao.club/post/nuxt/nuxt3-full-stack-prisma-sqlite</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3-full-stack-prisma-sqlite</guid><pubDate>Tue, 26 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;想要在 &lt;code&gt;Nuxt3&lt;/code&gt; 中使用 &lt;code&gt;Sqlite&lt;/code&gt; 非常简单，但重点是如何管理表结构的变更。&lt;/p&gt;
&lt;p&gt;Nuxt3官方提供了配置，可以直接开启数据库，默认就是&lt;code&gt;sqlite&lt;/code&gt;，数据库文件会存储在&lt;code&gt;.data/&lt;/code&gt; 目录下。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://db0.unjs.io/connectors/sqlite&quot;&gt;https://db0.unjs.io/connectors/sqlite&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;nitro: {
    experimental: {
      database: true
    },
    database: {
      default: {
        connector: &apos;sqlite&apos;,
        options: {
          path: &apos;/blog&apos;,
          name: &apos;blog.db&apos;
        }
      }
    },
    devDatabase: {
      default: {
        connector: &apos;sqlite&apos;,
        options: {
          path: &apos;/Users/your_name/code/abc/databases/blog&apos;,
          name: &apos;blog.db&apos;
        }
      }
    }
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是后续的表结构的更新、新增、删除等操作如何和线上同步就是一个大问题了。&lt;/p&gt;
&lt;p&gt;所以对于单机部署的博客来说，还是用上&lt;code&gt;Prisma&lt;/code&gt;省心。&lt;/p&gt;
&lt;h2&gt;安装及配置&lt;/h2&gt;
&lt;p&gt;没有用到官方的 &lt;code&gt;@prisma/nuxt&lt;/code&gt; ，因为一开始装上报错了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Error: 
The &quot;path&quot; argument must be of type string. Received undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后来单独安装了 &lt;code&gt;prisma&lt;/code&gt; 和 &lt;code&gt;@prisma/client&lt;/code&gt; 之后，发现有同样的问题。我也懒得再来一遍来，就按 &lt;code&gt;prisma&lt;/code&gt; 文档里流程接入。 总之，运行 &lt;code&gt;prisma&lt;/code&gt; 相关命令时添加上 &lt;code&gt;npx prisma&lt;/code&gt; 就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i prisma -D
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i @prisma/client
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化完成后，会生成一个 &lt;code&gt;prisma&lt;/code&gt; 目录，里面有一个 &lt;code&gt;dev.db&lt;/code&gt; 和 &lt;code&gt;schema.prisma&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dev.db&lt;/code&gt; 是本地的数据库文件。如果你恰好也用 &lt;code&gt;VS Code&lt;/code&gt; 的话，可以用 &lt;code&gt;SQLite3 Editor&lt;/code&gt;  这个免费的插件管理&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/yy0931/sqlite3-editor&quot;&gt;https://github.com/yy0931/sqlite3-editor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;好用的话别忘了给人家点个赞&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411261618324.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;schema.prisma&lt;/code&gt; 就是我们代码和数据库之间的“桥梁”，里面定义了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;client：客户端配置&lt;/li&gt;
&lt;li&gt;db：数据库配置，使用什么数据库， 链接地址&lt;/li&gt;
&lt;li&gt;model：所有表结构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;client&lt;/code&gt; 里的 &lt;code&gt;provider&lt;/code&gt; 是一个固定的值 &lt;code&gt;prisma-client-js&lt;/code&gt; ，还有一个 &lt;code&gt;binaryTargets&lt;/code&gt; 比较有用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;generator client {
  provider      = &quot;prisma-client-js&quot;
  binaryTargets = [&quot;native&quot;, &quot;debian-openssl-3.0.x&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;native&lt;/code&gt; 就是本机的环境，第二个值我配的是服务器上的环境，配好这个参数后，&lt;code&gt;prisma/client&lt;/code&gt; 里会生成对应的二进制文件，会被打包上去。（因为我这个项目是本地打包）如果你是线上打包的话，那 &lt;code&gt;native&lt;/code&gt; 其实就是你得线上服务器环境了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411261618325.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;db&lt;/code&gt; 主要是指定使用的数据库，以及地址。&lt;/p&gt;
&lt;p&gt;这里我用 &lt;code&gt;Sqlite&lt;/code&gt;，所以我这样配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;datasource db {
  provider = &quot;sqlite&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;env(&quot;DATABASE_URL&quot;)&lt;/code&gt; 是要读取你根目录下的 &lt;code&gt;.env&lt;/code&gt; 文件里的 &lt;code&gt;DATABASE_URL&lt;/code&gt;，所以说在部署时，要把 &lt;code&gt;env文件&lt;/code&gt; 也发上去。&lt;/p&gt;
&lt;p&gt;但 Nuxt3 里不推荐用 &lt;code&gt;env文件&lt;/code&gt; 管理环境变量，因为它本身要支持更多的部署环境，所以这里可以用其他方式来设置。&lt;/p&gt;
&lt;p&gt;如果用 &lt;code&gt;pm2&lt;/code&gt; 启动 &lt;code&gt;node&lt;/code&gt; 服务（Nuxt output），那就可以在 &lt;code&gt;ecosystem.config.cjs&lt;/code&gt;，设置环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = {
  apps: [
    {
      name: &apos;Blog&apos;,
      port: &apos;4577&apos;,
      exec_mode: &apos;fork&apos;,
      // instances: &apos;max&apos;,
      script: &apos;./server/index.mjs&apos;,
      // interpreter: &apos;~/.bun/bin/bun&apos;,
      env: {
        NODE_ENV: &apos;production&apos;,
      },
      env_production: {
        NODE_ENV: &apos;production&apos;,
        DATABASE_URL: &quot;file:/your_path/data1/dbs/blog.db&quot;
      }
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用 &lt;code&gt;docker&lt;/code&gt; / &lt;code&gt;docker-compose&lt;/code&gt; 同样的也是在 &lt;code&gt;Dockerfile&lt;/code&gt;、&lt;code&gt;docker-compose.yml&lt;/code&gt; 里配置好。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;model&lt;/code&gt; 里就是定义表结构。以一个 &lt;code&gt;user&lt;/code&gt; 表为例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;model User {
  id           Int           @id @default(autoincrement())
  email        String?       @unique
  username     String        @unique
  nickname     String?
  password     String
  avatar_url   String?
  articles     Articles[]

  @@map(&quot;b_users&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;model User&lt;/code&gt; ，这里的 &lt;code&gt;User&lt;/code&gt; 会在其他的 model 中使用，如 &lt;code&gt;user_info  User&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;model Articles {
  id         Int        @id @default(autoincrement())
  uid        String     @unique
  title      String
  tags       String     @default(&quot;[]&quot;)
  create_ts  DateTime   @default(now())
  updated_ts DateTime   @updatedAt
  user_id    Int
  user_info  User       @relation(fields: [user_id], references: [id], onDelete: NoAction)

  @@map(&quot;b_articles&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段、类型、说明这些规则就不说了，看看文档就能懂。这里 &lt;code&gt;user&lt;/code&gt; 和 &lt;code&gt;article&lt;/code&gt; 是一对多关系，每个 &lt;code&gt;article&lt;/code&gt; 只有一个 &lt;code&gt;user&lt;/code&gt;，一个 &lt;code&gt;user&lt;/code&gt; 可以有多个 &lt;code&gt;article&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这个 &lt;code&gt;user_info&lt;/code&gt; 在表结构中不会存在，只是 prisma 会用到&lt;/strong&gt;，所以表里的字段是 &lt;code&gt;user_info&lt;/code&gt; 上面的那些。&lt;/p&gt;
&lt;p&gt;这个 &lt;code&gt;User&lt;/code&gt;，同样也会在使用 &lt;code&gt;prisma&lt;/code&gt; 进行查询时使用，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await prisma.user.findMany(...some options )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要给 &lt;code&gt;User&lt;/code&gt; 重新定义个名字，就使用 &lt;code&gt;@@map&lt;/code&gt; ，这个名字（&lt;code&gt;b_users&lt;/code&gt;）就是数据库真正的表名&lt;/p&gt;
&lt;h2&gt;初始化和使用&lt;/h2&gt;
&lt;p&gt;定义好自己配置、表结构后，我们希望把定义好的结构同步到 &lt;code&gt;dev.db&lt;/code&gt; 里去，此时 &lt;code&gt;dev.db&lt;/code&gt; 是空的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate dev --name init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;migrate&lt;/code&gt; 是迁移的命令，&lt;code&gt;dev&lt;/code&gt; 是在本地开发时表结构发生表更时生成一个迁移文件的命令。&lt;/p&gt;
&lt;p&gt;生成的迁移文件会在 &lt;code&gt;prisma/migrations&lt;/code&gt; 中 &lt;code&gt;2024111100000_init/migration.sql&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;生成会自动执行一下 &lt;code&gt;prisma generate&lt;/code&gt;，此命令会安装 &lt;code&gt;@prisma/client&lt;/code&gt;并根据我们定义的模型，生成 &lt;code&gt;client API&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;这就是为什么刚才的代码能顺利运行，并且提示的信息还特别全的原因（提供了完整的 TS 代码提示）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await prisma.user.findMany(...some options )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411261623713.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时我们就可以使用 &lt;code&gt;prisma/client&lt;/code&gt; 的 API 在代码中进行增删改查操作了。&lt;/p&gt;
&lt;p&gt;在此之前，如果还没有一个 prisma 实例，可以像我一样在 &lt;code&gt;server&lt;/code&gt; 目录中创建一个 &lt;code&gt;prisma.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PrismaClient } from &apos;@prisma/client&apos;

const prismaClientSingleton = () =&gt; {
  return new PrismaClient()
}

declare const globalThis: {
  prismaGlobal: ReturnType&amp;#x3C;typeof prismaClientSingleton&gt;;
} &amp;#x26; typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== &apos;production&apos;) globalThis.prismaGlobal = prisma
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;server/api/v1/user&lt;/code&gt; 中（没有的话需要创建），新建一个接口文件 &lt;code&gt;login.post.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;此时 &lt;code&gt;Nuxt&lt;/code&gt; 中会出现一个 POST 接口：&lt;code&gt;/api/v1/user/login&lt;/code&gt; ,（你可以在页面中或其他 api工具中调用测试一下）&lt;/p&gt;
&lt;p&gt;然后在 &lt;code&gt;login.post.ts&lt;/code&gt; 中引入 &lt;code&gt;prisma&lt;/code&gt; 进行使用 。当然，如果你用了官方的 module，这里直接使用 &lt;code&gt;prisma&lt;/code&gt; 实例就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
import prisma from &apos;~/server/prisma&apos;

export default defineEventHandler(async (event) =&gt; {
	// useSafeValidatedBody
  const body = await readBody(event)
  const { nuxtSecretKey, jwtSecret } = useRuntimeConfig(event)
  const { username, password } = body
  const secret = new TextEncoder().encode(jwtSecret)
  const user = await prisma.user.findUnique({
    where: {
      username
    }
  })

  return {
    data: user,
    msg: &apos;登录成功&apos;
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里仅仅演示一下 &lt;code&gt;client api&lt;/code&gt; 的使用。然后在 nuxt3 中，使用 &lt;code&gt;$fetch&lt;/code&gt; 、&lt;code&gt;useAsyncData&lt;/code&gt; 、&lt;code&gt;useFetch&lt;/code&gt; 进行调用即可。&lt;/p&gt;
&lt;h2&gt;表结构变更和同步&lt;/h2&gt;
&lt;p&gt;在开发阶段，表结构很有可能会变动。&lt;/p&gt;
&lt;p&gt;这种情况下，只需要修改 &lt;code&gt;prisma.schema&lt;/code&gt; 的 &lt;code&gt;model&lt;/code&gt; 定义，然后再使用 &lt;code&gt;migrate dev&lt;/code&gt; ，生成一条迁移文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我的初始 &lt;code&gt;User&lt;/code&gt; 是这样的 （init/migration.sql）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- CreateTable
CREATE TABLE &quot;User&quot; (
    &quot;id&quot; INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    &quot;email&quot; TEXT NOT NULL,
    &quot;name&quot; TEXT
);

-- CreateIndex
CREATE UNIQUE INDEX &quot;User_email_key&quot; ON &quot;User&quot;(&quot;email&quot;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我发现字段不够，或者要改，于是修改 model 后，运行 &lt;code&gt;migrate dev&lt;/code&gt; , 又生成了一条 &lt;code&gt;migration.sql&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;/*
  Warnings:

  - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.

*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE &quot;new_User&quot; (
    &quot;id&quot; INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    &quot;email&quot; TEXT NOT NULL,
    &quot;name&quot; TEXT,
    &quot;password&quot; TEXT NOT NULL,
    &quot;role&quot; TEXT NOT NULL DEFAULT &apos;user&apos;
);
INSERT INTO &quot;new_User&quot; (&quot;email&quot;, &quot;id&quot;, &quot;name&quot;) SELECT &quot;email&quot;, &quot;id&quot;, &quot;name&quot; FROM &quot;User&quot;;
DROP TABLE &quot;User&quot;;
ALTER TABLE &quot;new_User&quot; RENAME TO &quot;User&quot;;
CREATE UNIQUE INDEX &quot;User_email_key&quot; ON &quot;User&quot;(&quot;email&quot;);
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我又加了 &lt;code&gt;password&lt;/code&gt;、&lt;code&gt;role&lt;/code&gt;，它的做法是新建一个 &lt;code&gt;new_user&lt;/code&gt; 表，把原来的表的内容插入进去，然后把新的表名改一下。同时上方有提醒 我加了一个 &lt;code&gt;password&lt;/code&gt; ，但又&lt;strong&gt;不能为空&lt;/strong&gt;，如果原表有数据的话，就会有问题了。这个需要注意一下。&lt;/p&gt;
&lt;p&gt;此时，把新的表结构同步到线上的命令，使用的是：&lt;code&gt;prisma migrate deploy&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果数据库是空的、新的，那本地一直用 &lt;code&gt;migrate dev&lt;/code&gt; 生成变更，线上一直用 &lt;code&gt;migrate deploy&lt;/code&gt; 就能一直同步（目前我没发现问题）&lt;/p&gt;
&lt;p&gt;但有一些情况是，项目可能是老的，数据库也已经存在。此时就需要 prisma 的其他命令，如 &lt;code&gt;push&lt;/code&gt; 、&lt;code&gt;pull&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;设置基线&lt;/h2&gt;
&lt;p&gt;如果已经有数据库了，数据库无法被重置。&lt;/p&gt;
&lt;p&gt;就要用到 &lt;strong&gt;基线&lt;/strong&gt; 。只要不是刚接入数据库，可以都算这种情况。&lt;/p&gt;
&lt;p&gt;先把现有的表结构拉下来。官方称之为 内省/反省 （==Introspection==）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;prisma db pull
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉下来的结果，就和初始化时自己写 &lt;code&gt;model&lt;/code&gt; 一样，prisma.schema 里会生成一堆和现有数据库对应的表结构。&lt;/p&gt;
&lt;p&gt;如果已经有了 &lt;code&gt;prisma/migrations&lt;/code&gt; 文件夹，请删除、移动、重命名或备份此文件夹&lt;/p&gt;
&lt;p&gt;新建&lt;code&gt;migrations&lt;/code&gt;文件夹，新增一个 &lt;code&gt;0_init&lt;/code&gt; 目录，注意：&lt;strong&gt;前缀是必须的&lt;/strong&gt;！,正常使用时是按时间戳排序的，这里需要重建一个&lt;strong&gt;基线&lt;/strong&gt;，所以以 &lt;strong&gt;0_&lt;/strong&gt; 开头。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkdir -p prisma/migrations/0_init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;migrate diff&lt;/code&gt;生成一份&lt;code&gt;migration.sql&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;个人理解：生成一份和当前数据库差异&lt;code&gt;sql&lt;/code&gt;，也就是运行这个&lt;code&gt;sql&lt;/code&gt;后可以达到目前这个数据库的结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate diff \  
--from-empty \  
--to-schema-datamodel prisma/schema.prisma \  
--script &gt; prisma/migrations/0_init/migration.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;resolve 应用这个差异&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;个人理解：应用意为已经执行过了，相当于&lt;strong&gt;抹平了历史记录&lt;/strong&gt;，从现在开始的改动就是基于当前数据库结构的变更了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate resolve --applied 0_init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线上数据库同理。&lt;/p&gt;
&lt;p&gt;后续有表结构的改动，就使用&lt;code&gt;migrate dev&lt;/code&gt;来维护就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署到服务器上时， 我是把 &lt;code&gt;prisma&lt;/code&gt; 也直接&lt;code&gt;push&lt;/code&gt; (&lt;strong&gt;git&lt;/strong&gt;)到项目打包后的目录下了，同步表结构只需要:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知道关于这部分，我有没有讲清楚？&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是在 &lt;code&gt;Nuxt3&lt;/code&gt; 中使用 &lt;code&gt;prisma&lt;/code&gt; 管理 &lt;code&gt;Sqlite&lt;/code&gt; 的一些经验分享。&lt;/p&gt;
&lt;p&gt;后续还会有深入和具体的 Nuxt3 全栈项目内的使用，完全使用 Nuxt 的 Server 能力开发接口等内容还在编写中～&lt;/p&gt;
&lt;p&gt;欢迎关注「&lt;strong&gt;早早集市&lt;/strong&gt;」&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt3全栈开发 · 配置篇]]></title><description><![CDATA[Nuxt3全栈开发个人博客，会用到哪些插件和包，以及它们的详细配置]]></description><link>https://zzao.club/post/nuxt/nuxt3-full-stack-config</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3-full-stack-config</guid><pubDate>Tue, 19 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在用Nuxt3全栈开发个人博客，踩了不少小坑，这篇文章总结一下。&lt;/p&gt;
&lt;h2&gt;依赖库及博客主要功能&lt;/h2&gt;
&lt;p&gt;先来介绍一下我用到了哪些 &lt;code&gt;Nuxt3&lt;/code&gt; 的相关生态及对应的功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@nuxtjs/color-mode&lt;/code&gt; 颜色模式：白天（light）、黑夜（dark）、系统（system）三者切换&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nuxt/content&lt;/code&gt; 展示文章，基于&lt;code&gt;mdc&lt;/code&gt;，可以使用自定义组件渲染&lt;code&gt;markdown&lt;/code&gt;，支持 &lt;code&gt;front matter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nuxtjs/tailwindcss&lt;/code&gt; 样式。以及配合 &lt;code&gt;@tailwindcss/typography&lt;/code&gt; 自定义markdown主题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@primevue/nuxt-module&lt;/code&gt; 组件库。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nuxt/image&lt;/code&gt; 图片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nuxt/icon&lt;/code&gt; 图标。配合 &lt;code&gt;iconify&lt;/code&gt; ，我目前用的图标主要是 &lt;code&gt;@iconify-json/icon-park-outline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nuxt/robots&lt;/code&gt; SEO&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@nuxt/mdc&lt;/code&gt; 解析动态（类型Memos/朋友圈/X）展示。和文章有一致的表现，也可以通过&lt;code&gt;tailwindcss&lt;/code&gt;自定义样式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prisma&lt;/code&gt; 管理数据库（sqlite3）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gitea&lt;/code&gt; 管理代码仓库（私有）。以及使用&lt;code&gt;workflows&lt;/code&gt;自动部署&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于这些库逐步使用和功能的逐渐实现，分享一下使用经验。&lt;/p&gt;
&lt;p&gt;如果没有刻意提到的安装方式，则默认都是用 &lt;code&gt;npx nuxi@latest module add xxxx&lt;/code&gt; 进行安装。&lt;/p&gt;
&lt;p&gt;如果没有表明在何处配置，则默认是在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 的顶级&lt;/p&gt;
&lt;p&gt;如果代码中变量明显没有引入，则是使用了 &lt;code&gt;Nuxt3&lt;/code&gt; 的 &lt;code&gt;auto imports&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;颜色模式&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;colorMode: {
    preference: &apos;system&apos;, // default value of $colorMode.preference
    fallback: &apos;light&apos;, // fallback value if not system preference found
    // hid: &apos;nuxt-color-mode-script&apos;,
    // globalName: &apos;__NUXT_COLOR_MODE__&apos;,
    // componentName: &apos;ColorScheme&apos;,
    // classPrefix: &apos;&apos;,
    // classSuffix: &apos;-mode&apos;,
    // storage: &apos;localStorage&apos;, // or &apos;sessionStorage&apos; or &apos;cookie&apos;
    // storageKey: &apos;nuxt-color-mode&apos;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有三种模式：&lt;code&gt;light&lt;/code&gt; &lt;code&gt;dark&lt;/code&gt; &lt;code&gt;system&lt;/code&gt; ，默认为 &lt;code&gt;system&lt;/code&gt; 根据系统模式来自动设置浅色或深色&lt;/p&gt;
&lt;p&gt;切换模式时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const colorMode = useColorMode()
const index = ref(modes.indexOf(colorMode.preference))
// 用来显示不同图标
const modes = [&apos;system&apos;, &apos;light&apos;, &apos;dark&apos;]

const modeIcon = computed( () =&gt; {
	switch ...
	case ...
})

function toggleColorMode() {
  colorMode.preference = modes[(++index.value) % modes.length]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合组件库 &lt;code&gt;primevue&lt;/code&gt; 的配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;primevue: {
    importTheme: { from: &apos;~/primevue/theme.ts&apos; },
    // usePrimeVue: false
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;theme.ts&lt;/code&gt; 如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;
import { definePreset } from &apos;@primevue/themes&apos;;
import Aura from &apos;@primevue/themes/aura&apos;;


const Noir = definePreset(Aura, {
  semantic: {
      primary: {
	      ...
      },
      colorScheme: {
        light: { ... }
        dark: { ... }
      }
  },
  components: {
    button: {
      ...
    }
  }
});


export default {
    preset: Noir,
    options: {
        darkModeSelector:&apos;.dark-mode&apos;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置 &lt;code&gt;darkModeSelector&lt;/code&gt; 为 &lt;code&gt;.dark-mode&lt;/code&gt;。 使用colorMode切换时，会自动切换 &lt;code&gt;html&lt;/code&gt; 的 &lt;code&gt;class&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;解析Markdown文件&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;content: {
    documentDriven: {
      injectPage: false
    },
    highlight: {
      theme: &apos;github-light&apos;,
      langs: [&apos;typescript&apos;, &apos;vue&apos;, &apos;javascript&apos;, &apos;go&apos;, &apos;shell&apos;, &apos;bash&apos;, &apos;yaml&apos;, &apos;markdown&apos;, &apos;json&apos;, &apos;html&apos;, &apos;ts&apos;, &apos;js&apos;]
    },
    sources: {
      obsidian: {
        prefix: &apos;/obsidian&apos;, // All contents inside this source will be prefixed with `/fa`
        driver: &apos;fs&apos;,
        base: `/Users/your_name/code/notion/blog` // Path for source directory
      },
    }
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;documentDriven&lt;/code&gt; 的 &lt;code&gt;injectPage&lt;/code&gt; 是为了解决一个警告信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;[@nuxt/content 09:52:13] Using &amp;#x3C;NuxtLayout&gt; inside app.vue will cause unwanted layout shifting in your application.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是，原代码从 &lt;code&gt;pages/[slug].vue&lt;/code&gt; 改为 &lt;code&gt;pages/post/[slug].vue&lt;/code&gt; 导致报错。&lt;/p&gt;
&lt;p&gt;以下是搜索时找到的相关issue&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nuxt/nuxt/issues/15240&quot;&gt;NuxtLayout warn vs documentation #15240&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dan-bowen/nuxt-blog-starter/pull/9&quot;&gt;nuxt-blog-starter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;highlight&lt;/code&gt; 是配置代码块高亮的，内部使用的是 &lt;a href=&quot;https://github.com/shikijs/shiki&quot;&gt;Shiki&lt;/a&gt;，同时和 &lt;code&gt;color-mode&lt;/code&gt; 兼容，可以查看 &lt;a href=&quot;https://content.nuxt.com/get-started/configuration#highlight&quot;&gt;更多官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sources&lt;/code&gt; 是核心配置，&lt;/p&gt;
&lt;p&gt;官方的默认配置是 &lt;code&gt;base: resolve(__dirname, &apos;content&apos;)&lt;/code&gt; , 即从当前项目下的content内读取md文件，我直接改成了自己本地的一个目录。&lt;/p&gt;
&lt;p&gt;启动项目时，会&lt;strong&gt;读取并监听&lt;/strong&gt;该目录下的所有md文件，并有一个忽略规则（开头为 &lt;code&gt;.&lt;/code&gt; 或 &lt;code&gt;-&lt;/code&gt; 的 ），然后会解析并缓存到 &lt;code&gt;.nuxt&lt;/code&gt; 内，&lt;code&gt;dev&lt;/code&gt; 模式下就是从 &lt;code&gt;.nuxt&lt;/code&gt; 中直接拿缓存数据，所以有一些奇怪的问题可以通过删除 &lt;code&gt;.nuxt&lt;/code&gt; 并重新运行可以解决。&lt;/p&gt;
&lt;p&gt;当然这个配置也决定了必须带着 &lt;code&gt;.nuxt&lt;/code&gt; 目录才能正常打包。&lt;/p&gt;
&lt;p&gt;只靠 &lt;code&gt;@nuxt/content&lt;/code&gt; 解析出的文章还没眼看，需要借助 &lt;code&gt;@tailwindcss/typography&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用前：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241106171191.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用（并自定义）后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241119101171.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;markdown&lt;/code&gt; 被解析为 &lt;code&gt;p&lt;/code&gt; 、&lt;code&gt;a&lt;/code&gt; 、&lt;code&gt;code&lt;/code&gt; 、&lt;code&gt;h1&lt;/code&gt; 、&lt;code&gt;h2&lt;/code&gt;、&lt;code&gt;img&lt;/code&gt;、&lt;code&gt;strong&lt;/code&gt; 等这些标签，而在 &lt;code&gt;@nuxt/content&lt;/code&gt; 中，使用对应的 &lt;code&gt;ProseA&lt;/code&gt;、&lt;code&gt;ProseH1&lt;/code&gt; 组件进行渲染。&lt;/p&gt;
&lt;p&gt;并且支持自己编写然后覆盖这些组件预设，在 &lt;code&gt;components/content&lt;/code&gt; 目录下新建一个同名的组件，如 &lt;code&gt;ProseA.vue&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;NuxtLink :href=&quot;props.href&quot; :target=&quot;props.target&quot;
    class=&quot;font-bold border-b-2 border-dashed border-zinc-600 hover:border-solid hover:border-zinc-900 dark:border-zinc-300 dark:hover:border-zinc-100&quot;&gt;
    &amp;#x3C;slot /&gt;
  &amp;#x3C;/NuxtLink&gt;
&amp;#x3C;/template&gt;

&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
import type { PropType } from &apos;vue&apos;

const props = defineProps({
  href: {
    type: String,
    default: &apos;&apos;
  },
  target: {
    type: String as PropType&amp;#x3C;&apos;_blank&apos; | &apos;_parent&apos; | &apos;_self&apos; | &apos;_top&apos; | (string &amp;#x26; object) | null | undefined&gt;,
    default: &apos;_blank&apos;,
    required: false
  }
})
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我把他的默认打开方式改为了 &lt;code&gt;_blank&lt;/code&gt; ，并自定义了功能和样式。其他组件同理，都是可以自定义的。 &lt;a href=&quot;https://content.nuxt.com/components/prose#prosea&quot;&gt;查看NuxtContent中支持的组件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;同样的可以基于 &lt;code&gt;typography&lt;/code&gt; 在顶层修改其样式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import typography from &apos;@tailwindcss/typography&apos;
/** @type {import(&apos;tailwindcss&apos;).Config} */
export default {
  content: [],
  plugins: [typography()],
  theme: {
    extend: {
      typography: (theme) =&gt; ({
        DEFAULT: {
          css: {
            code: {
              // backgroundColor: theme(&apos;colors.gray.100&apos;),
              // color: theme(&apos;colors.orange.400&apos;),
              fontWeight: &apos;normal&apos;,
              marginLeft: theme(&apos;spacing.1&apos;),
              marginRight: theme(&apos;spacing.1&apos;),
              paddingLeft: theme(&apos;spacing.2&apos;),
              paddingRight: theme(&apos;spacing.2&apos;),
              paddingTop: &apos;1px&apos;,
              paddingBottom: &apos;1px&apos;,
              borderRadius: &apos;2px&apos;,
              &apos;&amp;#x26;::before&apos;: {
                content: `&apos;&apos;!important`
              },
              &apos;&amp;#x26;::after&apos;: {
                content: `&apos;&apos;!important`
              }
            },
            p: {
              lineHeight: theme(&apos;lineHeight.loose&apos;)
            },
            pre: {
              // paddingBottom: 0,
              // paddingTop: 0,
              &apos;&amp;#x26; &gt; code&apos;: {
                color: theme(&apos;colors.gray.900&apos;),
                backgroundColor: &apos;transparent&apos;
              }
            },
            a: {
              textDecoration: &apos;none&apos;
            },
            img: {
              marginTop: 0,
              marginBottom: 0,
            }
          }
        }
      })
    },
  },
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我&lt;strong&gt;建议只改大小间距等属性&lt;/strong&gt;，颜色相关的我放在了其他地方管理，比如 &lt;code&gt;assets/tailwind.css&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 针对page的prose颜色配置 */
.mdc-page-prose {
  @apply prose prose-zinc prose-pre:bg-gray-100 dark:prose-pre:bg-zinc-400 dark:text-zinc-200 dark:prose-strong:text-zinc-200 prose-code:bg-zinc-200 dark:prose-code:bg-zinc-200 prose-code:text-zinc-800 dark:prose-blockquote:text-zinc-300
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为后面还涉及到动态的展示，动态也是基于mdc渲染的，也共用一套样式，那我再定义一个 &lt;code&gt;.mac-memo-prose&lt;/code&gt; 可能会更灵活一些。&lt;/p&gt;
&lt;h2&gt;解析Markdown字符串&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@nuxtjs/mdc&lt;/code&gt; 提供了 &lt;code&gt;MDC&lt;/code&gt; 组件来渲染md字符串， 添加此模块后即可使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;MDC :value=&quot;content&quot; tag=&quot;section&quot; class=&quot;mdc-memo-prose prose&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一开始我是没发现mdc可以直接使用。在搜github的issue时，早期的nuxt版本中，大家都是手动引入包内的解析函数😏 这就是用的晚的好处吧 ～&lt;/p&gt;
&lt;p&gt;样式表现和文章解析出来一模一样，如果想自定义，就用 &lt;code&gt;mdc-memo-prose&lt;/code&gt; 去添加。&lt;/p&gt;
&lt;p&gt;如果要使用一个自定义组件（&lt;code&gt;Mtag.vue&lt;/code&gt;）时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;::mtag
是实打实
::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;components/global&lt;/code&gt; 目录下新建 &lt;code&gt;Mtag.vue&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;Tag class=&quot;h-6 mr-2&quot;&gt;&amp;#x3C;slot&gt;&amp;#x3C;/slot&gt;&amp;#x3C;/Tag&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此 &lt;code&gt;Mtag&lt;/code&gt; 中使用的是 &lt;code&gt;primevue&lt;/code&gt; 中的 &lt;code&gt;Tag&lt;/code&gt; 组件，这也就意味着仅靠输入一些简单的语法，就实现了无限的组件呈现&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241119111158.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;图片、图标、SEO&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;图片使用 &lt;code&gt;@nuxt/image&lt;/code&gt; 模块&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果仅使用 &lt;code&gt;src&lt;/code&gt; 属性，NuxtImg 会输出原始的 img 标签。&lt;/p&gt;
&lt;p&gt;它提供了 sizes、placeholder（占位符）、preset、format（指定格式）、quality（图片质量）、loading（懒加载）、preload（预加载） 等非常多的配置，非常省事、好用。&lt;/p&gt;
&lt;p&gt;这里没有什么特殊用法，所以可以直接&lt;a href=&quot;https://image.nuxt.com/usage/nuxt-img#usage&quot;&gt;查看所有配置&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图片使用 &lt;code&gt;@nuxt/icon&lt;/code&gt; 模块&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;搭配 icon 库(&lt;code&gt;@iconify-json/icon-park-outline&lt;/code&gt;)使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i -D @iconify-json/icon-park-outline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;Icon name=&quot;icon-park-outline:wechat&quot;&gt;&amp;#x3C;/Icon&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改大小 ( 修改颜色直接改 style color: xxx )：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;Icon name=&quot;icon-park-outline:wechat&quot; size=&quot;1.5em&quot;&gt;&amp;#x3C;/Icon&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要替换掉 &lt;code&gt;primevue&lt;/code&gt; 的 &lt;code&gt;icon&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;Button severity=&quot;parimary&quot; size=&quot;small&quot;&gt;
	&amp;#x3C;Icon name=&quot;icon-park-outline:wechat&quot; slot=&quot;icon&quot;&gt;&amp;#x3C;/Icon&gt;
&amp;#x3C;/Button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SEO相关&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最直接的办法就是，打开控制台，找到 &lt;code&gt;Lighthouse&lt;/code&gt; , 开始分析即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241119111114.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;看看哪里加载慢，SEO里提示什么可以优化，比如没有 &lt;code&gt;robots&lt;/code&gt;，那就加入模块 &lt;code&gt;@nuxt/robots&lt;/code&gt; 就会自动帮你做好了。&lt;/p&gt;
&lt;p&gt;其他的就是注意 &lt;code&gt;img&lt;/code&gt; 的 &lt;code&gt;alt&lt;/code&gt; 有没有写，第三方 js/css 设置 &lt;code&gt;sync&lt;/code&gt; &lt;code&gt;defer&lt;/code&gt;，页面绘制时偏移等等&lt;/p&gt;
&lt;p&gt;header 信息，可以用 &lt;code&gt;useHead&lt;/code&gt; 轻松设置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;useHead({
  title: &apos;早早集市｜博客站&apos;,
  meta: [
    {
      name: &apos;description&apos;,
      content: &apos;https://blog.zzao.club&apos;,
    },
    {
      name: &apos;keywords&apos;,
      content: &apos;&apos;,
    },
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;prisma 和 gitea&lt;/h2&gt;
&lt;p&gt;这两篇太长了，我决定分出去两篇，下次再发！&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;作为一个展示为主的博客，前端使用这些模块、库已经够用了，但作为一个全栈框架，后端 &lt;code&gt;Nitro&lt;/code&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;图片上传（cos）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中涉及到大量对 Nitro 的探索，鉴权、中间件、数据库等等。这也是后面文章输出的重点方向，即 Nuxt3 的全栈开发。&lt;/p&gt;
&lt;p&gt;👏👏欢迎关注 「早早集市」&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nuxt3中使用prisma binaryTargets多环境配置]]></title><description><![CDATA[Nuxt3中使用prisma binaryTargets多环境配置]]></description><link>https://zzao.club/post/nuxt/nuxt3-prisma-binarytargets</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3-prisma-binarytargets</guid><pubDate>Fri, 15 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;场景： 本地使用macos开发，服务器是Debian12，打包时需要本地打包再上传&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;schema.prisma&lt;/code&gt; 配置 &lt;code&gt;binaryTargets&lt;/code&gt; 字段，本地正常使用，打包后会自动区分环境&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;generator client {
  provider      = &quot;prisma-client-js&quot;
  binaryTargets = [&quot;native&quot;, &quot;debian-openssl-3.0.x&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 &lt;code&gt;generate&lt;/code&gt; 时生成的目录下有俩 &lt;code&gt;.node&lt;/code&gt; 二进制包，分别对应 &lt;code&gt;binaryTargets&lt;/code&gt; 两个环境&lt;/p&gt;
&lt;p&gt;另外：不要把output指定到node_modules外面，&lt;strong&gt;使用默认配置即可&lt;/strong&gt;，这样打包后的文件里使用了 &lt;code&gt;__dirname&lt;/code&gt; 导致会报错。 此条解决办法来自&lt;code&gt;github issues&lt;/code&gt; ，有更好的办法，欢迎留言&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;[nuxt] [request error] [unhandled] [500] __dirname is not defined in ES module scope
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[版本管理工具]]></title><description><![CDATA[目前我的全部项目的版本管理都在使用 unjs/changelogen]]></description><link>https://zzao.club/post/nuxt/nuxt3-auto-update-version</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3-auto-update-version</guid><pubDate>Thu, 14 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目前我的全部项目的版本管理都在使用 &lt;code&gt;unjs/changelogen&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;无需安装，也无需配置，只需要增加几个 &lt;code&gt;scripts&lt;/code&gt; 即可&lt;/p&gt;
&lt;p&gt;具体细节和版本号声明请看：&lt;a href=&quot;https://blog.zzao.club/post/knows/semantic-versioning-changelogen&quot;&gt;语义化版本控制说明&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[基于Hono和Satori的后端生成SVG图片简易方案]]></title><description><![CDATA[Satori （Vercel的）是一个可以把HTML+CSS生成SVG的一个库。]]></description><link>https://zzao.club/post/imgx/hono-satori-svg-creator</link><guid isPermaLink="true">https://zzao.club/post/imgx/hono-satori-svg-creator</guid><pubDate>Thu, 07 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Satori&lt;/code&gt; （Vercel的）是一个可以把&lt;code&gt;HTML+CSS&lt;/code&gt;生成&lt;code&gt;SVG&lt;/code&gt;的一个库。&lt;/p&gt;
&lt;p&gt;通常提到把HTML转为图片，都会想到 &lt;code&gt;html2canvas&lt;/code&gt; 、&lt;code&gt;html-to-image&lt;/code&gt;这类的库，但这类库需要借助浏览器环境，比如各种卡片类网站的导出功能（css特性支持有限）。但如果是多端都有生成需求，或者要实现更便捷的获取方式，就得考虑放在后端去实现。&lt;/p&gt;
&lt;p&gt;而Satori只需要接收&lt;strong&gt;JSX元素&lt;/strong&gt;就可以计算得出&lt;code&gt;SVG&lt;/code&gt;内容，不需要在前端就可以实现。重要的是，文字的字体也会保留，文字直接被解析成了&lt;code&gt;path&lt;/code&gt;！&lt;/p&gt;
&lt;p&gt;虽然&lt;code&gt;Satori&lt;/code&gt;不保证&lt;code&gt;SVG&lt;/code&gt;和浏览器呈现的 &lt;code&gt;HTML&lt;/code&gt; 100%匹配，但我觉得仅是脱离浏览器和保留了相当一部分&lt;code&gt;css&lt;/code&gt;属性的支持，就足够产生无限的想象。&lt;/p&gt;
&lt;p&gt; &lt;img src=&quot;https://imgx.zzao.club/api/img/001/%E6%88%91%E4%B8%8D%E5%BE%97%E4%B8%8D%E5%91%8A%E8%AF%89%E4%BD%A0%E7%9A%84+deepseek-r1%E7%9A%84%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;功能构思&lt;/h2&gt;
&lt;p&gt;要实现的功能很简单，前端网站上有个文字转卡片的界面，支持保存主题和样式的预设到后端，然后用户在其他地方调接口就能到图片。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;前端页面上可以自定义一套样式，包括背景，渐变，flex布局，阴影等等一切 &lt;code&gt;Satori&lt;/code&gt; 支持的&lt;code&gt;css&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;前端的html框架和后端jsx的框架保持一致&lt;/strong&gt;。比如一张卡片就是套三个div，最外层负责渐变色，中间层负责半透明+磨砂效果，最内层div负责展示文字。那hono中也用jsx定义好一样的结构，并在前端维护好三个&lt;code&gt;style对象&lt;/code&gt;，调用接口把样式存起来，比如和用户id挂钩。或者把页面结构也存起来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户传文本过来，拿到对应的结构和样式，把文本塞进去，用&lt;code&gt;Satori&lt;/code&gt;生成&lt;code&gt;SVG&lt;/code&gt;，返回给用户&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了防止消耗大量资源，限流一下，比如每分钟xx次&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hono中直接使用TSX&lt;/h2&gt;
&lt;p&gt;关于&lt;code&gt;Hono&lt;/code&gt;项目的搭建、部署，我已经写过一个简易的流程了，可以自行翻阅，这部分就跳过了。&lt;/p&gt;
&lt;p&gt;直接在项目内新建一个目录 &lt;code&gt;src/imgx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;初始化该子模块下的路由 &lt;code&gt;src/imgx/index.tsx&lt;/code&gt;并在根路由下挂载&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const imgx = new Hono&amp;#x3C;{ Variables: Variables }&gt;();

imgx.post(&quot;/gen&quot;, zvalidator(&apos;json&apos;, textGenSchema), async (c) =&gt; {
  const { text } = c.req.valid(&apos;json&apos;)

  const svg = await renderSVG(c, &amp;#x3C;&gt;&amp;#x3C;div&gt;{text}&amp;#x3C;/div&gt;&amp;#x3C;/&gt;)
  c.header(&apos;Content-Type&apos;, &apos;image/svg+xml&apos;);
  c.header(&apos;Content-Disposition&apos;, &apos;attachment; filename=&quot;imgx.svg&quot;&apos;);
  return c.body(svg)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为要直接写&lt;code&gt;JSX&lt;/code&gt;，所以直接把文件名后缀改为.&lt;code&gt;tsx&lt;/code&gt;即可。&lt;code&gt;tsx&lt;/code&gt; 的内容还是按正常的写法，只不过它支持&lt;code&gt;JSX&lt;/code&gt;了，如果用到类型的话，可以在 &lt;code&gt;hono/jsx&lt;/code&gt; 中导出 &lt;code&gt;{ FC, JSX }&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;结构参数用 &lt;code&gt;zvalidator&lt;/code&gt; 校验一下，或者把此接口白名单去掉，需要登录后才能使用。&lt;/p&gt;
&lt;p&gt;关于怎么存样式和HTML框架就不写了，随意怎么存都行，我这里直接存个json文件做演示。&lt;/p&gt;
&lt;p&gt;当收到请求时并通过校验后，先去读取对应的样式，当然也有可能读不到&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;try {
    style = fs.readFileSync(path.resolve(process.cwd(), &quot;style.json&quot;));
  } catch(err) {
    c.set(&apos;errMsg&apos;, &apos;不存在预设的样式文件, 请联系管理员处理&apos;)
    throw new HTTPException(400)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把样式里的各种信息解析出来&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  const { bgStyle, innerStyle, textStyle, imgSize } = JSON.parse(style)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再传给 Satori 处理就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { fonts } from &apos;../common/fonts&apos;

const svg = await satori(
    &amp;#x3C;div
    style={{ 
      ...bgStyle,
      ...textStyle
    }}
&gt;   
&amp;#x3C;div style={{ ...innerStyle }}&gt;
{ element }
&amp;#x3C;/div&gt;
    
&amp;#x3C;/div&gt; ,
    {
      width: imgSize.width,
      height: imgSize.height,
      fonts: fonts
    }

  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我这个是文字生成图片，只要存在文字，&lt;code&gt;Satori&lt;/code&gt; 就一定要显式的传入字体，也就是上边的&lt;code&gt;fonts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而字体库，可以自己维护在服务器上，应该用到的也不是很多，&lt;code&gt;Satori&lt;/code&gt; 支持 &lt;code&gt;ttf&lt;/code&gt; 、&lt;code&gt;oft&lt;/code&gt; 、 &lt;code&gt;woff&lt;/code&gt; 这三种格式的字体。要把字体数据作为 ArrayBuffer 或 Buffer 传递。&lt;/p&gt;
&lt;p&gt;我用的Bun运行Hono项目，所以可以这样处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import type { FontStyle, FontWeight } from &quot;satori&quot;;
import path from &apos;path&apos;
const YouSheBiaoTiHei = Bun.file(path.resolve(process.cwd(), &quot;fonts&quot;, &quot;YouSheBiaoTiHei-2.ttf&quot;));
export const fonts: Array&amp;#x3C;{
  name: string;
  data: ArrayBuffer;
  weight: FontWeight;
  style: FontStyle;
}&gt; = [
    {
      name: &quot;YouSheBiaoTiHei&quot;,
      data: await YouSheBiaoTiHei.arrayBuffer(),
      weight: 500,
      style: &apos;normal&apos;
    }
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后不要忘了处理&lt;code&gt;header&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;c.header(&apos;Content-Type&apos;, &apos;image/svg+xml&apos;);
c.header(&apos;Content-Disposition&apos;, &apos;attachment; filename=&quot;imgx.zzao.club.svg&quot;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;res.body&lt;/code&gt; 返回就可以了&lt;/p&gt;
&lt;p&gt;然后可以用 &lt;code&gt;hono-rate-limiter&lt;/code&gt; 做一下限流&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { rateLimiter } from &quot;hono-rate-limiter&quot;;

const limiter = rateLimiter({
  windowMs: 1 * 60 * 1000, // 1分钟
  limit: 50, // Limit each IP to 50 requests per `window` (here, per 1 minutes).
  standardHeaders: &quot;draft-6&quot;, // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
  keyGenerator: (c) =&gt; c.req.url, // Method to generate custom identifiers for clients.
  // store: ... , // Redis, MemoryStore, etc. See below.
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;limiter&lt;/code&gt; 是一个中间价，可以直接在全局启用，也可以单独在某一个路由上使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;imgx.post(&quot;/gen&quot;, limiter, zvalidator(&apos;json&apos;, textGenSchema), async (c) =&gt; {
	...
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当来自某个ip的请求，在1分钟内超过了50次，就会直接返回HTTP错误，提示请求了太多次（too many request ... ）&lt;/p&gt;
&lt;h2&gt;进一步处理&lt;/h2&gt;
&lt;p&gt;生成了SVG， 可以用前端通过 &lt;code&gt;post&lt;/code&gt; 请求，并设置 &lt;code&gt;responseType: &apos;blob&apos;&lt;/code&gt; ，拿到数据，然后配合a标签直接进行下载。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const a = document.createElement(&apos;a&apos;)
const dataUrl = URL.createObjectURL(response.data)
a.href = dataUrl
a.download = &apos;image.svg&apos;
a.click()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这样就和直接在前端生成图片比，看起来没有优势了。当然也可以拿到svg再用其他canvas插件处理一下，二次编辑一下。&lt;/p&gt;
&lt;p&gt;或者直接在后端使用 &lt;code&gt;Resvg&lt;/code&gt; 来生成&lt;code&gt;PNG&lt;/code&gt;，&lt;code&gt;Resvg&lt;/code&gt;是&lt;code&gt;rust&lt;/code&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;毕竟一个卡片网站有再多的主题，自己常用的其实就1-2个，而每次文字要分享的文字是不一样。&lt;/p&gt;
&lt;p&gt;所以我觉得在变化的地方起手是比较舒适的操作。&lt;/p&gt;
&lt;p&gt;不知道你意下如何？&lt;/p&gt;</content:encoded></item><item><title><![CDATA[浅谈卡片类应用的最后一步]]></title><description><![CDATA[最近一年出现了很多大同小异的文字生成卡片类的App和网站。]]></description><link>https://zzao.club/post/imgx/card-app-the-last-step</link><guid isPermaLink="true">https://zzao.club/post/imgx/card-app-the-last-step</guid><pubDate>Wed, 06 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近一年出现了很多大同小异的文字生成卡片类的App和网站。&lt;/p&gt;
&lt;p&gt;主要功能是可以在一个精美的页面上定制自己的内容，然后保存为图片，比如这样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/%E6%B5%81%E5%85%89%E5%8D%A1%E7%89%87_tempC_20241106_1544.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;看上去十分精美，也有很多种主题和样式可调，非常适合在各种社交平台发一些自己的感悟，或者在自媒体平台发一些文字类的帖子。&lt;/p&gt;
&lt;p&gt;但不管谁去开发网站、App都不是做慈善的，大部分是奔着收费去的，做免费也得掂量掂量自己的服务器开销。所以说收费很合理，只要对用户来说好用，愿意掏钱买单就可以了。&lt;/p&gt;
&lt;p&gt;我在简单使用后发现了几个小问题（水印问题不算），似乎可以自己优化一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主题很多，但常用的就1-2个&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;哪怕是有再多的主题和配色，用户都是为了去发表内容才会去使用，而同一个用户发内容，大概率不会一直换自己的主题和配色。&lt;/p&gt;
&lt;p&gt;所以已经需要登录、购买会员才能去掉水印或解锁功能的App，也许可以把挑选和使用分为两个场景：初次或前期使用，可以去挑选喜欢的主题配色，去实验；在进行了某个如保存或设为常用的操作后，下次用户进来也许可以忽略掉周围包裹的各种配置参数。这样使用起来会更清爽一些。&lt;/p&gt;
&lt;p&gt;当然，这是放在开发者的角度，要想在已有诸多竞品的情况下再切入这个赛道，也只能绞尽脑汁优化了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以其他方式提供服务&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按照我的主观想法，前面已经优化到经常使用的用户进到App里，直接编辑一下就可以导出了。&lt;/p&gt;
&lt;p&gt;那还有什么办法可以再简化这个操作呢？&lt;/p&gt;
&lt;p&gt;我觉得可以尝试一下&lt;strong&gt;不进入App就可以使用&lt;/strong&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;</content:encoded></item><item><title><![CDATA[基于 Nuxt3 + Obsidian 搭建个人博客]]></title><description><![CDATA[Nuxt是一个用Vue来编写的，可用来创建类型安全、高性能和生产级全栈 Web 应用程序和网站的全栈框架。后端是 Nitro，一个可以被单独使用的Web服务端框架。]]></description><link>https://zzao.club/post/nuxt/nuxt3-obsidian-build-your-blog</link><guid isPermaLink="true">https://zzao.club/post/nuxt/nuxt3-obsidian-build-your-blog</guid><pubDate>Wed, 06 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Nuxt&lt;/code&gt;是一个用Vue来编写的，可用来创建类型安全、高性能和生产级全栈 Web 应用程序和网站的全栈框架。后端是 &lt;code&gt;Nitro&lt;/code&gt;，一个可以被单独使用的Web服务端框架。&lt;/p&gt;
&lt;p&gt;作为一个全栈框架，不仅具备了比使用Vue开发SPA客户端&lt;strong&gt;更好的开发体验&lt;/strong&gt;，还能享受服务端渲染带来的&lt;strong&gt;SEO优化&lt;/strong&gt;，同时Node服务可以实现帮你实现&lt;strong&gt;更多的可能性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;之所以基于Nuxt从零搭建，一是为了选全栈，二是发现了&lt;code&gt;nuxt/content&lt;/code&gt;可以读取本地的&lt;code&gt;markdown&lt;/code&gt;文件，三是选的模板达不到想要的效果，四是过程可以作为写文章的素材。&lt;/p&gt;
&lt;h2&gt;初始化Nuxt项目&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;nuxt/content&lt;/code&gt;初始化项目，因为这个插件是博客的核心插件，所以初始化时直接装上就可以&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nuxi@latest init content-app -t content
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择&lt;code&gt;npm&lt;/code&gt;、&lt;code&gt;pnpm&lt;/code&gt;、&lt;code&gt;yarn&lt;/code&gt;作为包管理器后，会生成项目目录，下载依赖，我选了&lt;code&gt;npm&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241106171162.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时直接运行 &lt;code&gt;npm run dev&lt;/code&gt; 就可以启动项目了&lt;/p&gt;
&lt;p&gt;启动后会有如下页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241106171191.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;code&gt;app.vue&lt;/code&gt;作为页面的入口文件，里面只有一个简单对的 &lt;code&gt;NuxtPage&lt;/code&gt; ，作用类似于 &lt;code&gt;Vue&lt;/code&gt; 中的 &lt;code&gt;RouterView&lt;/code&gt; ，实际上它就是 &lt;code&gt;RouterView&lt;/code&gt; 的包装。&lt;/p&gt;
&lt;p&gt;同样的包装还有：&lt;code&gt;NuxtLink&lt;/code&gt; 、&lt;code&gt;NuxtImg&lt;/code&gt; 等等。 更多 &lt;a href=&quot;https://nuxt.com/docs/api/components/client-only&quot;&gt;Nuxt Component&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;#x3C;template&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;NuxtPage /&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它用来显示 &lt;code&gt;pages/&lt;/code&gt; 下的页面&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pages/&lt;/code&gt; 是Nuxt生成路由的一个约定目录，其内的文件会自动生成路由。&lt;/p&gt;
&lt;p&gt;如 &lt;code&gt;pages/a.vue&lt;/code&gt; 会生成 &lt;code&gt;/a&lt;/code&gt; 路由&lt;/p&gt;
&lt;p&gt;如果想接收url参数可以这样写： &lt;code&gt;/pages/a/[id].vue&lt;/code&gt; 或 &lt;code&gt;/pages/a-[group]/[id].vue&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在文件内使用这种方式来获取参数，总之 &lt;code&gt;[xxx]&lt;/code&gt; 是它的路由规则&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;&amp;#x3C;script setup lang=&quot;ts&quot;&gt;
const route = useRoute()
console.log(route.params.id, route.params.group)
&amp;#x3C;/script&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想匹配改路径下的所有路由，可以这样写：&lt;code&gt;/pages/a/[...slug].vue&lt;/code&gt;，此时可以再去看一下&lt;br&gt;
&lt;code&gt;route.params.slug&lt;/code&gt; 的值是什么。&lt;/p&gt;
&lt;p&gt;查看更多关于 &lt;a href=&quot;https://nuxt.com/docs/guide/directory-structure/pages&quot;&gt;Nuxt Pages&lt;/a&gt; 👈&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;Pages&lt;/code&gt;不仅仅是简化了&lt;code&gt;Router&lt;/code&gt;的配置，也是为了服务端渲染，更好的SEO。做博客的话，当然希望别人搜索某些关键字可以直接搜到自己的文章页面。而不是全部内容都在一个单页面内，由浏览器渲染。&lt;/p&gt;
&lt;p&gt;所以自带的两个页面是怎么渲染的就清楚了。&lt;/p&gt;
&lt;p&gt;你可以看到上面我并没有引入&lt;code&gt;useRoute&lt;/code&gt;，但也可以在&lt;code&gt;vue&lt;/code&gt;文件中直接使用，因为Nuxt提前做好了&lt;code&gt;自动import&lt;/code&gt; 的配置，它自动导入组件、可组合项、辅助函数和 &lt;code&gt;Vue API&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;项目根目录下的  &lt;a href=&quot;https://nuxt.com/docs/guide/directory-structure/components&quot;&gt;&lt;code&gt;components/&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://nuxt.com/docs/guide/directory-structure/composables&quot;&gt;&lt;code&gt;composables/&lt;/code&gt;&lt;/a&gt; , &lt;a href=&quot;https://nuxt.com/docs/guide/directory-structure/utils&quot;&gt;&lt;code&gt;utils/&lt;/code&gt;&lt;/a&gt; 都可以直接使用，无需手动导入。&lt;/p&gt;
&lt;p&gt;如果你想手动导入此类API，可以使用&lt;code&gt;#imports&lt;/code&gt;， 如 &lt;code&gt;import { ref, computed } from &apos;#imports&apos;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果使用了第三方的包，也想支持自动导入，可以在 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 中配置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineNuxtConfig({
  imports: {
    presets: [
      {
        from: &apos;vue-i18n&apos;,
        imports: [&apos;useI18n&apos;]
      }
    ]
  }
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，&lt;code&gt;nuxt.config.ts&lt;/code&gt;还可以配置 &lt;code&gt;sourcemap&lt;/code&gt; ，全局css引入：&lt;code&gt;css&lt;/code&gt; ，&lt;code&gt;tailwindcss&lt;/code&gt; ，以及 &lt;code&gt;nuxt/content&lt;/code&gt; 等等。&lt;/p&gt;
&lt;p&gt;Nuxt的文档和Vue的类似，都非常全面，读过一遍后会对这个框架有比较清晰的了解，所以起步阶段还是&lt;strong&gt;建议先读文档&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;NuxtContent&lt;/h2&gt;
&lt;p&gt;项目搭建好了，它默认加载了两个页面，也就是两个md文件，是&lt;code&gt;/content&lt;/code&gt;目录下的 &lt;code&gt;index.md&lt;/code&gt; 和 &lt;code&gt;about.md&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;这不是重点，因为我也不会在这个项目目录下管理我的文章。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://content.nuxt.com/get-started/configuration#sources&quot;&gt;NuxtContent&lt;/a&gt; 支持配置多种文件的来源，看一下官方的配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { resolve } from &quot;node:path&quot;;

export default defineNuxtConfig({
  content: {
    sources: {
      // overwrite default source AKA `content` directory
      content: {
        driver: &apos;fs&apos;,
        prefix: &apos;/docs&apos;, // All contents inside this source will be prefixed with `/docs`
        base: resolve(__dirname, &apos;content&apos;)
      },
      // Additional sources
      fa: {
        prefix: &apos;/fa&apos;, // All contents inside this source will be prefixed with `/fa`
        driver: &apos;fs&apos;,
        // ...driverOptions
        base: resolve(__dirname, &apos;content-fa&apos;) // Path for source directory
      },
      github: {
        prefix: &apos;/blog&apos;, // Prefix for routes used to query contents
        driver: &apos;github&apos;, // Driver used to fetch contents (view unstorage documentation)
        repo: &quot;&amp;#x3C;owner&gt;/&amp;#x3C;repo&gt;&quot;,
        branch: &quot;main&quot;,
        dir: &quot;content&quot;, // Directory where contents are located. It could be a subdirectory of the repository.
        // Imagine you have a blog inside your content folder. You can set this option to `content/blog` with the prefix option to `/blog` to avoid conflicts with local files.
      },
    }
  }
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到不仅能用&lt;code&gt;content&lt;/code&gt;目录，还能用&lt;code&gt;content-fa&lt;/code&gt;目录，还能用&lt;code&gt;github&lt;/code&gt;拉取。&lt;/p&gt;
&lt;p&gt;而目录下&lt;code&gt;base&lt;/code&gt;配置传入的是一个文件夹路径，所以我们这里&lt;strong&gt;直接写上自己经常用来写文章的一个目录绝对路径&lt;/strong&gt;就可以了。&lt;/p&gt;
&lt;p&gt;在目录下所有文章的&lt;strong&gt;改动会被监听&lt;/strong&gt;，也就是在写文章时有什么改动会实时更新在本地服务的页面上，非常方便。&lt;/p&gt;
&lt;p&gt;我建议在一个比较高级别的目录下分出几个单独的文件夹，往博客上发的全都放在一个比如&lt;code&gt;blog&lt;/code&gt;的目录下，其他可能不是文章的，就还是该咋写咋写。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;blog&lt;/code&gt;下的文章，有时候也不一定都能当天写完，&lt;code&gt;nuxt/content&lt;/code&gt;支持使用 &lt;code&gt;.&lt;/code&gt; 和 &lt;code&gt;-&lt;/code&gt; 作为文件的前缀时&lt;strong&gt;忽略此文章&lt;/strong&gt;，不会被处理。&lt;code&gt;Obsidian&lt;/code&gt;不支持使用&lt;code&gt;.&lt;/code&gt;作为前缀，所以我用的&lt;code&gt;-&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;blog&lt;/code&gt;下的文章可以随意划分文件夹，&lt;code&gt;blog&lt;/code&gt;上不受影响，因为博客上的文章其实还是要根据&lt;code&gt;tag&lt;/code&gt;或&lt;code&gt;category&lt;/code&gt;用代码逻辑划分，和本地的文件夹没什么关系。&lt;/p&gt;
&lt;p&gt;所以此时我的配置就是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;content: {
	sources: {
		obsidian: {
        prefix: &apos;/obsidian&apos;, // All contents inside this source will be prefixed with `/fa`
        driver: &apos;fs&apos;,
        // ...driverOptions
        base: `/xxx/xxx/notion/blog` // Path for source directory
      },
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要想正常的使用本地文件，还需要做一些改动。&lt;/p&gt;
&lt;p&gt;之前在Obsidian写东西，文件都是中文名，而content在处理中文名时会自动忽略，&lt;code&gt;我搜了下各种issue&lt;/code&gt; 其他语言也存在这种问题。好在content处理前后分别给了一个钩子函数，不然这博客直接夭折了&lt;/p&gt;
&lt;p&gt;在这里写一个处理函数 &lt;code&gt;/server/plugins/content.ts&lt;/code&gt; （没有的目录要手动新建）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// @ts-ignore
export default defineNitroPlugin((nitroApp) =&gt; {
  nitroApp.hooks.hook(&apos;content:file:beforeParse&apos;, (file: { body: string }) =&gt; {
    // 匹配markdown文件内的元信息
    const match = file.body.match(/---
([\s\S]+?)
---
([\s\S]*)/);
    if (match) {
      let frontMatter = match[1];
      const mainContent = match[2];
      // 如果不包含_path字段, 则使用title字段和一个前缀来生成_path
      if (!frontMatter.includes(&apos;_path:&apos;)) {
        // 提取 title 字段的值
        const titleMatch = frontMatter.match(/title:\s*(.+)/);
        if (titleMatch &amp;#x26;&amp;#x26; titleMatch.length &gt; 1) {
            const titleValue = titleMatch[1].trim();
            const pathValue = `/post/${titleValue}`;
            // 将 _path 插入到 front-matter 中
            frontMatter = `_path: ${pathValue}
` + frontMatter;
        } else {
          return;
        }
    }
    // 重新组合文件内容
    const newContent = `---
${frontMatter}
---
${mainContent}`;
    file.body = newContent;
    }
    // 如果页面内没有 _path 属性, 则自动添加为 /blog/ + 文件名
    
  });
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;content&lt;/code&gt;拿到本地文件后，会编译一遍，然后放在 &lt;code&gt;.nuxt&lt;/code&gt; 缓存，其处理后的文件内容，带有一个 &lt;code&gt;_path&lt;/code&gt; 属性，这个属性就是页面上对应的文章的地址。&lt;/p&gt;
&lt;p&gt;前面说中文地址会丢掉，也是这个&lt;code&gt;_path&lt;/code&gt;出了问题，因为它默认是自己根据文件名生成的。&lt;/p&gt;
&lt;p&gt;所以这里处理逻辑就是，处理原始内容前，给原始内容加上一个&lt;code&gt;_path&lt;/code&gt;。当原始内容里带了&lt;code&gt;_path&lt;/code&gt;，它就会优先用设置好的，不自动生成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以你如果愿意手动给自己的每个md文件都手动加上一个&lt;code&gt;_path&lt;/code&gt;的话，也可以不用这个钩子。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以前还要注意一个问题：&lt;code&gt;const pathValue = /post&lt;/code&gt; 这行代码，相当于写死了每篇文章的前缀是 &lt;code&gt;/post&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;所以说 &lt;code&gt;pages/&lt;/code&gt; 下必须要有这样的结构 &lt;code&gt;/pages/post/[...slug].vue&lt;/code&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;用&lt;code&gt;Obsidian&lt;/code&gt;的小伙伴，可以用&lt;code&gt;Linter&lt;/code&gt;这个插件格式化&lt;code&gt;YAML属性&lt;/code&gt; ，如&lt;code&gt;date&lt;/code&gt;、&lt;code&gt;lastmod&lt;/code&gt;、&lt;code&gt;title&lt;/code&gt;，这三个是自动生成而且都是必用的。&lt;code&gt;tags&lt;/code&gt;、&lt;code&gt;category&lt;/code&gt; 会用作分类和筛选。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/1-img-20241106181119.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;那文章有了，在哪里点进去呢？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;content&lt;/code&gt; 提供了 &lt;code&gt;queryContent&lt;/code&gt; 来查询内容， 你可以这样来查询：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const contentQuery = queryContent(&apos;post&apos;)
// 总文章数 去除了被忽略的文章
const count = await queryContent(&apos;post&apos;).count()

const contentQuery2 = queryContent(&apos;post&apos;)
// 按时间倒序以及分页后的数据
const pages await contentQuery2.sort({ date: -1 }).skip(skip).limit(pageSize.value).find()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;skip&lt;/code&gt; &lt;code&gt;limit&lt;/code&gt; &lt;code&gt;sort&lt;/code&gt; 用这几个就能完成大部分操作，同时还支持 &lt;code&gt;where&lt;/code&gt; 过滤内容，实现更多的分类查询功能。&lt;/p&gt;
&lt;p&gt;关于&lt;code&gt;queryContent&lt;/code&gt;的&lt;a href=&quot;https://content.nuxt.com/composables/query-content&quot;&gt;更多API&lt;/a&gt;👈&lt;/p&gt;
&lt;p&gt;到这里，基于本地文件有了，也没破坏本地的写作流程，各种分类、搜索功能有了，作为一个最简博客已经五脏俱全。&lt;/p&gt;
&lt;p&gt;后续我会继续分享我自己的博客建设过程。&lt;/p&gt;
&lt;p&gt;最后一步就是发布到服务器&lt;/p&gt;
&lt;h2&gt;打包和部署&lt;/h2&gt;
&lt;p&gt;如果此时你没有初始化git，可以先初始化一下。&lt;/p&gt;
&lt;p&gt;然后我要先说一个Nuxt和其他Node服务的不同点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它打包后的文件内已经包含了&lt;code&gt;node_modules&lt;/code&gt;&lt;/strong&gt; ，也就是说打包后的output文件就是它的完全体，不需要再 &lt;code&gt;npm install&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;而博客的文章，是本地某个目录下写的，用的别的软件如&lt;code&gt;Obsidian&lt;/code&gt; 来管理。&lt;/p&gt;
&lt;p&gt;这样就有三个问题：&lt;/p&gt;
&lt;p&gt;1️⃣：打包要在本地打。因为博客文件在本地，其实也是在&lt;code&gt;.nuxt&lt;/code&gt;内，但我们也不会把&lt;code&gt;.nuxt&lt;/code&gt; 传到git上去。所以只能在本地打完了再传上去。&lt;/p&gt;
&lt;p&gt;2️⃣：要修改&lt;code&gt;.gitignore&lt;/code&gt;。这里要去掉两个 一个是 &lt;code&gt;node_modules&lt;/code&gt; 一个是 &lt;code&gt;dist&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我前期因为只去了 &lt;code&gt;node_modules&lt;/code&gt; 落下了 &lt;code&gt;dist&lt;/code&gt;，还让我一度怀疑是Nuxt的重大bug。因为我在服务器配了&lt;code&gt;gitea&lt;/code&gt;的&lt;code&gt;action&lt;/code&gt;，git push上去时，&lt;strong&gt;需要把output整个都扔上去&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而扔上去之后因为某些&lt;code&gt;node_module&lt;/code&gt;插件少了dist目录，会导致有些导出引用不到，我在排查pm2日志时懒得cat整个errlog，直接用的&lt;code&gt;pm2 logs Blog --lines 30&lt;/code&gt;，恰好没发现dist丢了的问题。&lt;/p&gt;
&lt;p&gt;然后还去github不停的搜问题，还真被我搜出一些 &lt;code&gt;vite&lt;/code&gt; 和 &lt;code&gt;nuxt&lt;/code&gt; 的早期bug，误以为现在还存在。于是就用重新&lt;code&gt;npm install&lt;/code&gt;的方式暂时解决了问题。&lt;/p&gt;
&lt;p&gt;直到我近几天才发现报错路径上有个dist，然后猛然想起 &lt;code&gt;.gitignore&lt;/code&gt;里忽略里dist。我滴个老天爷。&lt;/p&gt;
&lt;p&gt;3️⃣：要配个软件来写&lt;code&gt;markdown&lt;/code&gt;。如果已经有了还好，但是可能有人用的&lt;code&gt;vscode&lt;/code&gt;之类的东西来写md，我不确定方不方便管理 &lt;code&gt;front matter&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有了以上几点，基本就是本地写文章，写完跑一下&lt;code&gt;build&lt;/code&gt;，然后push上去就完成部署了&lt;/p&gt;
&lt;p&gt;是不是还挺丝滑的？比项目内管理MD文件要舒服很多吧&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;以上就是用&lt;code&gt;Nuxt&lt;/code&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;</content:encoded></item><item><title><![CDATA[理想的博客站]]></title><description><![CDATA[建博客就好比装修房子]]></description><link>https://zzao.club/post/zzao/ideal-blog</link><guid isPermaLink="true">https://zzao.club/post/zzao/ideal-blog</guid><pubDate>Wed, 06 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;建博客就好比装修房子&lt;/h2&gt;
&lt;p&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;博客也要开始技术选型，或直接选模板，只要满足自己的需求，怎么简单怎么来。&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;strong&gt;不像富哥，先花几十万全屋定制一套，后面不喜欢什么了再换，再砸，再买。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;博客也是先搞必要的功能，一下子弄太多的话，一个是没时间，再个弄乱了容易烂尾。&lt;/p&gt;
&lt;p&gt;页面要追求简洁，看到花里胡哨的就上，阴影渐变搞一大堆，很容易审美疲劳。&lt;/p&gt;
&lt;p&gt;更何况闲着也是闲着，产生不了其他价值，自己边摸索边学习，挺好的。&lt;/p&gt;
&lt;h2&gt;理想中的博客站&lt;/h2&gt;
&lt;p&gt;第一，以前写好的东西，不要搬家就能直接用。要不然写的文章，这里一份，那里一份，维护和修改都十分费劲。&lt;/p&gt;
&lt;p&gt;第二，文件能很好的分类和忽略，因为要被直接搬到网站上去，有些草稿肯定是不希望去发布。分类的话还是按文件夹自己分，到了网站上其实还需要用标签等方式分一下类。&lt;/p&gt;
&lt;p&gt;第三，&lt;strong&gt;文章开头必须带框架@版本&lt;/strong&gt;。谁会希望把文章看完了才发现版本和自己用的差了十万八千里呢。要集成这个小组件，博客要支持&lt;code&gt;mdx&lt;/code&gt;/&lt;code&gt;mdc&lt;/code&gt;，能在&lt;code&gt;md&lt;/code&gt;的&lt;code&gt;front matter&lt;/code&gt;中拿到类似tags的信息（或其他方式），去展示在文章列表或文章开头，避免已过时的信息浪费时间。（我真的反感这一点很久了）&lt;/p&gt;
&lt;p&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;所以针对以上几点慢慢维护，同时上线其他网站，看看能碰撞出什么😯&lt;/p&gt;</content:encoded></item><item><title><![CDATA[基于原生 DOM 实现Markdown复制样式到公众号]]></title><description><![CDATA[很多人选择了markdown语法来写文章，因为它可以在纯文字的基础上添加少量的语法，就能渲染出更美观的样式，并且可以自己扩展样式。]]></description><link>https://zzao.club/post/zzao/copy-md-styles-to-wx</link><guid isPermaLink="true">https://zzao.club/post/zzao/copy-md-styles-to-wx</guid><pubDate>Sun, 03 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很多人选择了&lt;code&gt;markdown&lt;/code&gt;语法来写文章，因为它可以在纯文字的基础上添加少量的语法，就能渲染出更美观的样式，并且可以自己扩展样式。&lt;/p&gt;
&lt;p&gt;更重要的是它的生态十分丰富，基本上所有平台、框架都支持markdown语法，再加上开源插件的协助，可以满足绝大部分展示需求。&lt;/p&gt;
&lt;p&gt;以前我写文章的流程是这样的：先在本地的&lt;strong&gt;某个写作App&lt;/strong&gt;上把文章写完，确认没问题后，再打开&lt;strong&gt;某个自己还算信赖的markdown转换网站/App&lt;/strong&gt;，一键复制内容，然后打开目标平台编辑器，粘贴进去，看看样式有没有问题，然后点击预览、发布。&lt;/p&gt;
&lt;p&gt;直接有几次半夜写完了文章想发布的时候，&lt;strong&gt;markdown转换失败了&lt;/strong&gt;，要么是代码丢了高亮，要么是有些样式错乱了。而我别无他法，只能再找另一个网站去转换，但是经常写文章的话，某些样式可能是自己自定义的，某些主题别的网站可能还没集成，非常无奈。&lt;/p&gt;
&lt;p&gt;我一直觉得这是个问题，但碍于我也没解决，讲出来顶多是大家一起吐槽一下，所以就不了了之。&lt;/p&gt;
&lt;p&gt;直到前一阵，我又又又开始自建博客站，这次没有找现成的模板，因为我想&lt;strong&gt;基于本地文件直接生成博客文章&lt;/strong&gt;，之前搬家搬的真的累，这次要一举让我写文章这个工作流达到完美。&lt;/p&gt;
&lt;p&gt;基于本地文件生成文章，我用&lt;code&gt;nuxt/content&lt;/code&gt;实现了，于是问题来到「如何一键复制到公众号」上。&lt;/p&gt;
&lt;p&gt;要实现这个功能，有几点需要明确，思路才能理顺。&lt;/p&gt;
&lt;h2&gt;复制的内容是什么&lt;/h2&gt;
&lt;p&gt;我要想保持各平台样式一致，肯定是从自己博客上复制内容+样式，然后到其他平台上。&lt;/p&gt;
&lt;p&gt;所以，那些markdown转换的网站，他们复制的是什么内容？为什么可以带有样式？&lt;/p&gt;
&lt;p&gt;打开以前用过的网站，写一段markdown，然后点复制。粘贴进&lt;code&gt;VSCODE&lt;/code&gt;看看到底是啥。&lt;/p&gt;
&lt;p&gt;如果能正常粘贴出来的话，你可能会看到如下内容（以下是我在一个mdx编辑器内粘贴出来的内容）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/1-img-20241104141167.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;而这是它的渲染结果是这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/1-img-20241103171168.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以粘贴进其他编辑器的内容是什么？ &lt;strong&gt;html&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;更准确的说是&lt;strong&gt;具有内联样式的html&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;那为什么可能你复制完再去粘贴，可能看不到这个&lt;strong&gt;html内容&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;这是因为&lt;code&gt;navigator.clipboard&lt;/code&gt; 同时设置了两种类型的文本，你在支持富文本的编辑器内粘贴，就会使用html内容，你在不支持富文本的文件内粘贴，就会只保留文本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const htmlData = new Blob([yourHTML], { type: &apos;text/html&apos; })
const textData = new Blob([yourText], { type: &apos;text/plain&apos; })
const clipboardItem = new ClipboardItem({ &apos;text/html&apos;: htmlData, &apos;text/plain&apos;: textData})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上都是&lt;strong&gt;浏览器原生对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后使用&lt;code&gt;navigator.clipboard&lt;/code&gt;写入到粘贴板：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await navigator.clipboard.write([clipboardItem])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从使用技术手段实现的角度：&lt;code&gt;navigator.clipboard.write&lt;/code&gt;可以把带有内联样式的html代码写入到粘贴板，目标编辑器就可以粘贴出带有样式的内容。&lt;/p&gt;
&lt;p&gt;所以问题变成了：&lt;strong&gt;怎么把自己的博客里的文章转换为带有内联样式的html代码&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;如何获取到文章的样式&lt;/h2&gt;
&lt;p&gt;获取文章样式这个操作，让任何一个前端都能写出来，但这里明显不能用简单的&lt;code&gt;style属性&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因为影响样式的css，可能是内联样式，也可能是通过外部引入的css。&lt;/p&gt;
&lt;p&gt;所以这里我用了 &lt;code&gt;getComputedStyle&lt;/code&gt; 这个方法，传入&lt;code&gt;DOM&lt;/code&gt;，它可以获取到&lt;strong&gt;DOM元素最终的样式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么意思？ 意思就是仅靠这一个方法就可能实现这个功能！&lt;/p&gt;
&lt;p&gt;所以方案就很明确了：用&lt;code&gt;getComputedStyle&lt;/code&gt;获取到样式，转换为&lt;code&gt;style=&quot;xxx&quot;&lt;/code&gt; 这样的内联样式，插入到原有的&lt;code&gt;html&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;这一步转换有没有插件？有，我搜罗了几个开源项目，基本都是使用了 &lt;code&gt;juice&lt;/code&gt; 这个插件。&lt;/p&gt;
&lt;p&gt;它可以让你传入&lt;code&gt;html&lt;/code&gt;，在传入&lt;code&gt;css&lt;/code&gt;，然后帮你拼接成&lt;code&gt;具有内联样式的html&lt;/code&gt;。所以它适合在你知道了自己的css在哪里的场景，也就是一个在线的markdown编辑器里。&lt;/p&gt;
&lt;p&gt;我要是解决的就是脱离在线编辑器，所以肯定是不能走这个路子。&lt;/p&gt;
&lt;p&gt;虽然&lt;code&gt;getComputedStyle&lt;/code&gt;获取到样式有几百个至多，而又有那么多的元素，直接原封不动的拼接，内容肯定是太多太大了。&lt;/p&gt;
&lt;p&gt;但好在用markdown写文章的人，一般追求的都是&lt;strong&gt;简洁&lt;/strong&gt;、&lt;strong&gt;大气&lt;/strong&gt;、&lt;strong&gt;低调&lt;/strong&gt;、&lt;strong&gt;极客&lt;/strong&gt;，对吧？彦祖。&lt;/p&gt;
&lt;p&gt;所以平时用到的markdow语法，其实也是有限的几种。&lt;/p&gt;
&lt;p&gt;而渲染后的文章，通常也只有这几个元素：&lt;code&gt;p&lt;/code&gt;、&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;span&lt;/code&gt;、&lt;code&gt;blockquote&lt;/code&gt;、&lt;code&gt;strong&lt;/code&gt;、&lt;code&gt;code&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;它们分别对应了：段落、超链接、代码块、标注、加粗等。&lt;/p&gt;
&lt;p&gt;所以只需要把影响样式的样式属性限制一下，从&lt;code&gt;getComputedStyle&lt;/code&gt;里只取这几个！&lt;/p&gt;
&lt;h2&gt;通过调试得到样式的全部覆盖&lt;/h2&gt;
&lt;p&gt;先思考一下那些样式影响了文章的样式，列出来：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 对元素有影响的属性
export const EffectCssAttrs = [
  // &apos;fontFamily&apos;,
  &apos;fontSize&apos;,
  &apos;fontWeight&apos;,
  &apos;color&apos;,
  &apos;textAlign&apos;,
  &apos;lineHeight&apos;,
  &apos;whiteSpace&apos;,
  &apos;textSizeAdjust&apos;,
  &apos;overflowX&apos;,
  &apos;padding&apos;,
  &apos;paddingTop&apos;,
  &apos;paddingBottom&apos;,
  &apos;paddingLeft&apos;,
  &apos;paddingRight&apos;,
  ...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在通过 &lt;code&gt;getComputedStyle&lt;/code&gt; 获取到dom的全部样式时，使用此列表过滤：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;	const computedCssStyles = getComputedStyle(childDom, null)
    // console.log(`computedCssStyles`, computedCssStyles)
    const _effectCssAttrs = pointCssAttrs.length &gt; 0 ? pointCssAttrs : EffectCssAttrs
    _effectCssAttrs.forEach( cssAttr =&gt; {
          const value = computedCssStyles[cssAttr]
          if (value) {
            curCssStyles[cssAttr] = value
          }
        })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们只需要拿到文章最外层的&lt;code&gt;Dom&lt;/code&gt;，循环所有子元素，获取到其有效样式，组合成内联样式&lt;/p&gt;
&lt;p&gt;然后再把全部&lt;code&gt;Dom&lt;/code&gt;整合起来，就得到了一个&lt;strong&gt;带有样式的html字符串&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然后再衔接上一小节的&lt;code&gt;navigator.clipboard&lt;/code&gt; Api，就已经实现了功能。&lt;/p&gt;
&lt;p&gt;但是测试下来，还是有很多需要填补和优化的地方。&lt;/p&gt;
&lt;p&gt;比如影响样式的属性列举的不太全，导致有些渲染的不对劲；&lt;/p&gt;
&lt;p&gt;比如&lt;code&gt;fontFamily&lt;/code&gt;这个明显不需要每个元素都获取一遍的样式需要单独处理；&lt;/p&gt;
&lt;p&gt;比如文章太长时，元素太多，复制出来的内容太大，也许精简一下也能得到相同的效果；&lt;/p&gt;
&lt;p&gt;比如代码块 &lt;code&gt;pre&lt;/code&gt; 元素内，每个&lt;code&gt;span&lt;/code&gt;其实只需要&lt;code&gt;color&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;比如博客自定义了图片组件用于放大查看，其他展示平台只需要&lt;code&gt;img&lt;/code&gt;单个标签等等类似的问题。&lt;/p&gt;
&lt;p&gt;比如在A平台有效，在B平台有些样式不支持，需要单独处理。&lt;/p&gt;
&lt;p&gt;这些问题列出来看着有点多，但基本都是先把主要功能打通后，逐个解决的，问题不大。&lt;/p&gt;
&lt;p&gt;最后再来理一遍思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拿到文章最外层的元素，循环处理&lt;/li&gt;
&lt;li&gt;封装一个整合单个元素的&lt;code&gt;递归函数（getDomCssStyle)&lt;/code&gt;，放在循环内，获取到处理后的带有内联样式的html字符串
&lt;ul&gt;
&lt;li&gt;处理各种特殊情况：无dom、忽略某些nodeType、忽略某些无用的标签（tagName）、忽略某些无用的class（classList）&lt;/li&gt;
&lt;li&gt;特殊处理某些组件，如图片 &lt;code&gt;img&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置缓存，优化代码。（针对文章又长又复杂时）&lt;/li&gt;
&lt;li&gt;深度优先，有子元素时，先去递归组装好全部子元素&lt;/li&gt;
&lt;li&gt;组装最终的dom字符串，return&lt;/li&gt;
&lt;li&gt;优化：抽离函数、常量等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;ClipboardItem&lt;/code&gt; （两种类型 &lt;code&gt;text/plain&lt;/code&gt; 、&lt;code&gt;text/html&lt;/code&gt; ）创建实例&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;navigator.clipboard.write&lt;/code&gt; 实例&lt;/li&gt;
&lt;li&gt;粘贴进其他编辑器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上全部语法均基于&lt;strong&gt;原生DOM&lt;/strong&gt;、&lt;strong&gt;浏览器原生对象&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;以上就是我为了脱离第三方markdown编辑网站而做出的一个小小功能，没有依赖任何第三方插件，目前已经应用在了我的博客站上，用来往公众号同步。但我的博客站还没搞完，所以先不贴出来了。&lt;/p&gt;
&lt;p&gt;功能比较简单，相信大部分人都能实现，但我就是没搜到有类似的插件。大家都是画地为牢，做了一个个功能完全一样的markdown编辑网站...&lt;/p&gt;
&lt;p&gt;幸好我解决了这个问题，再也不用发愁啦！&lt;/p&gt;
&lt;p&gt;对自建博客或是此插件感兴趣，欢迎关注～&lt;/p&gt;</content:encoded></item><item><title><![CDATA[基于本地文件搭建个人博客站：无缝配合Obsidian]]></title><description><![CDATA[使用 Obsidian 的Git仓库作为文章数据源，配合Nuxt搭建个人博客]]></description><link>https://zzao.club/post/nuxt/file-based-blog-by-obsidian</link><guid isPermaLink="true">https://zzao.club/post/nuxt/file-based-blog-by-obsidian</guid><pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;搞博客站一直有些痛点：文件不停的搬家、需要多处同步修改、无法复制到其他平台、界面同质化严重。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在别人的框架里，很难实现所有的个性化需求，所以我想试试能不能通过自建来达到一个近似完美的状态。&lt;/p&gt;
&lt;h2&gt;先有文章再有博客&lt;/h2&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;blockquote&gt;
&lt;p&gt;PS: 从文章到博客的使用场景有点类似学英语。其对诸如程序员等人群来说有很大帮助，但仅&lt;br&gt;
用于看和写上，我实在想不出普通打工仔的听和说的场景在哪里，但现在更多的App是让用户去&lt;br&gt;
听、说、记，也许会有所帮助，但我总觉得相较普通人来说不是最好的方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了文章就要发出去给别人看，因为发出去不仅仅是表达自己的观点或发泄，也是和别人的观点产生碰撞从而修正自己的某些错误。待在本地笔记本里只能没事拿出来emo一下。&lt;/p&gt;
&lt;p&gt;大部分人发文章都追求的是简便，比如直接发在微信公众号等自媒体平台。自建的话用类似WordPress、Hexo、Halo、Hugo、Typecho等现成的框架。&lt;/p&gt;
&lt;p&gt;不管在哪里写，我通常都不会直接在发文的平台下直接写。一般会先在本地写个草稿或大纲，在一天或者几天内把它写完。因为写的可能比较零碎，后续也得自己整合。&lt;/p&gt;
&lt;p&gt;写完后如果要发到多个平台，我希望他们在各家的App上打开时，&lt;strong&gt;尽量在样式保持一致&lt;/strong&gt;，这非常合理，但也有一些门槛：&lt;strong&gt;在本地记录的格式要兼容各家平台&lt;/strong&gt;。（没样式相当于自嗨，把平台当成自己的日记本）&lt;/p&gt;
&lt;p&gt;所以从我的角度来讲，&lt;code&gt;markdown&lt;/code&gt;是不二之选。但有的平台他就是不支持&lt;code&gt;markdown&lt;/code&gt;，支持&lt;code&gt;markown&lt;/code&gt;的也可能不让你自己修改样式。所以文章保持全平台一致是解决不了的，只能保持部分平台一致（转为富文本）。&lt;/p&gt;
&lt;p&gt;另外文章的内容是受平台方监管的，比如你在A平台的文章里不能出现B平台的联系方式、要控制自己不能发一些不该发的言论等等。&lt;/p&gt;
&lt;p&gt;你的文章推荐机制也是由各平台自己决定，比如公众号的文章你在搜索平台上搜不到的，只能微信内部搜。一些社区或平台要看你文章打开率、点赞率、评论数等指标来决定是不是要给你推流等等。&lt;/p&gt;
&lt;p&gt;没办法，谁让用户在他们手里呢。&lt;/p&gt;
&lt;p&gt;当你感觉到膈应了，又看到别人精美的博客时，很容易产生自己建站的冲动。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前期其实可以0成本选择一些现成的平台去发文，如果满足不了自己，再去研究自建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;博客框架的「框架」&lt;/h2&gt;
&lt;p&gt;自建博客有很多&lt;strong&gt;框架&lt;/strong&gt;可选：WordPress、Hexo、Halo、Hugo、Typecho、Astro、Next、开源仓库等等。&lt;/p&gt;
&lt;p&gt;但他们都有自己的&lt;strong&gt;框架&lt;/strong&gt;。以下痛点也来自于我不接受这些“框架”。&lt;/p&gt;
&lt;p&gt;框架本身有自己组织文章文件的方式，一般是让你放在某个（&lt;code&gt;/content&lt;/code&gt;）目录下自行编译或是提供你一套编辑器和后台管理系统，然后通过一个或多个配置文件进行站点内容的自定义，再通过开源插件或付费插件实现更换主题的功能。&lt;/p&gt;
&lt;p&gt;但是前面提到了，我本身有固定的App（Obsidian）去写文章，要想把文章生成站点，还要再复制到框架项目的指定文件夹下。如果已经发布的文章更新了、删除了，同步过去是个问题。每次写完了还要再塞过去也是个问题。并且框架对于文章内容和头部元信息的处理方式还可能和自己平时的习惯相冲突。&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;strong&gt;所有的问题都来自于自己喜欢折腾&lt;/strong&gt;，人家嫌麻烦的直接发朋友圈都没觉得有啥问题。&lt;/p&gt;
&lt;p&gt;不过也没关系，喜欢折腾的人总会吸引到其他喜欢折腾的人，咱们继续往下折腾！&lt;/p&gt;
&lt;h2&gt;自建博客&lt;/h2&gt;
&lt;p&gt;在说自建前，补充一点：&lt;/p&gt;
&lt;p&gt;上面的痛点，其实能提供云服务的笔记App（类Notion）已经解决了。&lt;/p&gt;
&lt;p&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;那你猜我为啥用&lt;code&gt;Obsidian&lt;/code&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;strong&gt;我自建的挂了，我不会骂自己。但是我花钱买你的App却用不了，甭管谁的问题都得骂。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再说我的笔记都在本地，自建的只是个展示平台，数据安稳的很。&lt;code&gt;Github&lt;/code&gt;这类平台可以作为同步和备份使用。&lt;/p&gt;
&lt;p&gt;也正是因为我的笔记是本地的&lt;code&gt;markdown&lt;/code&gt;文件，所以框架必须得&lt;strong&gt;支持我拿本地的数据进行编译&lt;/strong&gt;，再部署到服务器上。能&lt;strong&gt;直接&lt;/strong&gt;做到这一点的好像真的不多，我目前发现了&lt;code&gt;nuxt/content&lt;/code&gt; 可以做到这一点（有用过其他的希望能在评论区补充一下😘），所以我开启了一个基于&lt;code&gt;Nuxt&lt;/code&gt;从零搭建博客的计划。&lt;/p&gt;
&lt;p&gt;首先本地文件还是用以前的方式写，不需要搬家，只需要把本地的文件整理好，然后开放给框架一个读取的目录即可，因为可以直接读取后本地编译。备份方式还是用自己习惯的方式，没有侵入性。元信息还是用&lt;code&gt;Obsidian&lt;/code&gt;的&lt;code&gt;Linter&lt;/code&gt;插件维护。&lt;/p&gt;
&lt;p&gt;其次页面样式靠自己的“阅历”，多看优秀的博客设计或是界面动效，组合到自己的博客上，「撞站」大概率不会。&lt;/p&gt;
&lt;p&gt;往其他平台如公众号发布的时候，我希望是直接在自己的站点上集成这个功能，写完文章，博客站本地编译好自动部署，我直接点击按钮能把html内容复制出来最好。这样能保证文章的样式是一致的。&lt;/p&gt;
&lt;p&gt;现在的流程是把markdown内容复制到另一个工具网站里，然后再复制生成好的html内容，粘贴到公众号编辑器内。像是掘金的话，直接把markdown源码复制过去，他是支持md编辑器的。其他平台也要么是支持md，要么支持富文本。&lt;/p&gt;
&lt;p&gt;这个功能还在思考中，因为虽然&lt;code&gt;nuxt/content&lt;/code&gt;负责渲染&lt;code&gt;markdown&lt;/code&gt;，但是没法直接拿到&lt;strong&gt;具有内联样式的html内容&lt;/strong&gt;，实际内容是元素带有className，再通过css渲染样式。而像md转公众号的工具站点核心是把编辑器内渲染的&lt;code&gt;html&lt;/code&gt;和固定的&lt;code&gt;css&lt;/code&gt;通过&lt;code&gt;juice&lt;/code&gt;(插件)渲染为一段内联样式的html内容，然后再把剪贴板的类型设置为&lt;code&gt;text/html&lt;/code&gt;，粘贴到公众号编辑器中时就会带着样式了。&lt;/p&gt;
&lt;p&gt;在解决掉这个问题后，我的本地写作流程是这样的：&lt;/p&gt;
&lt;p&gt;打开obsidian&lt;strong&gt;开始写东西&lt;/strong&gt;，写完后，去博客项目目录下运行一行命令&lt;code&gt;npm run build&lt;/code&gt;，博客会通过&lt;code&gt;gitea action&lt;/code&gt;自动部署上去，如果要发布到其他平台，打开博客站域名找到文章，点击复制，再去对应平台粘贴。&lt;/p&gt;
&lt;p&gt;感觉挺带劲儿的，几乎只剩下了「写」这一件事。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;这个基于&lt;code&gt;Nuxt&lt;/code&gt;从零开发的博客，我会全程记录文章然后分享出来。&lt;/p&gt;
&lt;p&gt;目前进度可能有&lt;code&gt;30%&lt;/code&gt;？评论、搜索、分类、类朋友圈等功能都是自己开发或集成，速度可能不会特别快（少量涉及后端&lt;code&gt;HonoJS&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;后续代码也会开源，有兴趣的可以自己去搭建。&lt;/p&gt;
&lt;p&gt;如果你有相同方向的需求，不管有没有开发能力，都可以在评论区/私信留一下言，&lt;strong&gt;满20人的话我拉个小群&lt;/strong&gt;，方便随时更新进度，或收集一些需求，后续提前开放源码或帮助大家也建起站来。&lt;/p&gt;
&lt;p&gt;以上就是关于我的「理想博客站」的全部内容啦~&lt;/p&gt;
&lt;p&gt;👋🏻👋🏻&lt;/p&gt;</content:encoded></item><item><title><![CDATA[程序员探索副业一年了，收益有多少 ]]></title><description><![CDATA[从23年的11月份左右，我开始尝试在主业外寻找其他能获取收益的方式（以下统称为副业），到目前为止有一年了，所以这篇文章简单总结一些小感悟。]]></description><link>https://zzao.club/post/side-hustle/independent-developer-one-year-zero-money</link><guid isPermaLink="true">https://zzao.club/post/side-hustle/independent-developer-one-year-zero-money</guid><pubDate>Wed, 30 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;从23年的11月份左右，我开始尝试在主业外寻找其他能获取收益的方式（以下统称为副业），到目前为止有一年了，所以这篇文章简单总结一些小感悟。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之所以是感悟，是因为没有通过副业获取到利润，虽然也没有赔。所以喜欢直接看数字来刺激自&lt;br&gt;
己的可以划走了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;社群的好坏&lt;/h2&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;strong&gt;会拓展眼界&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;说白了，就是我以前只知道给公司干活，拿工资，和同事间交流、扯皮、撕逼，对于项目是怎么谈成的，怎么推广的，推广给谁了，费用是怎么定价的，钱又被谁分走了多少，甚至我自己应该怎么多拿一部分钱都几乎没有自己的认知。（只要眼界/认知够低，总会得到拓展）&lt;/p&gt;
&lt;p&gt;而加入一个社群，不管他发的「&lt;strong&gt;日入xxxx元，我是怎么做到的&lt;/strong&gt;」的文章/项目真实性如何，其中必有我未曾了解过的领域。所以我也是抱着试一试的心态去尝试每个小项目，去了解一下：从撮合一个项目，到推广、引流，以及抽成等方方面面的经验。&lt;/p&gt;
&lt;p&gt;但是，对于一些小的好上手的项目，基本都是只参与引流这一步，然后拿一笔提前讲好的抽成。大概100人的群里，有5个人左右，会在短期获取到看似比较客观的收入（因为后续的收入我无法印证）。这个比例在每个引流相关的项目里都差不多。但讲真的，这和被公司雇佣其实没有区别，大家可能也是现在私活越来越少，被迫尝试各种领域的收入吧。&lt;/p&gt;
&lt;p&gt;有好处就一定有坏处，对吧。&lt;/p&gt;
&lt;p&gt;坏处就是：&lt;strong&gt;前期你不容易察觉到这个社群的调性，而这个调性可能会导致你偏离初心。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;意思就是，你从完全不了解，到新加入一个以搞钱为目的的社群，很快就会被各种信息狂轰乱炸，然后一边觉得很有用说的很对，一边又疲于尝试，然后没好结果就用没执行力来PUA自己。&lt;/p&gt;
&lt;p&gt;从你的角度是进入了一个副业圈子，努力寻找能搞钱的项目，积极参与，每天多挣一点。在社群圈子的或者圈子里拥有定价权的人来说，任何来的人都是要被淘洗一遍，最后选出的你也许不过是个好用的劳动力。真正有价值的人，会被吸收为人合伙人/投资人。当然，占比最高的还是毫无进展的人。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;大量尝试是必须的，但其中的影响可能要等到你尝试后才能发觉。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有的尝试不过是从旋涡的中心努力的往更外围移动。你在动，别人也在动，旋涡自己也在动。&lt;/p&gt;
&lt;h2&gt;时间、精力、正反馈&lt;/h2&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;p&gt;现阶段的原因有很多&lt;/p&gt;
&lt;p&gt;比如平时就是很懒，有人劝我学，有人劝我躺，我本能的选择了舒适的一方。&lt;/p&gt;
&lt;p&gt;比如杂事缠身：被领导批了，绩效低了，工作累了，孩子病了，老婆怒了，心情差了，代码出BUG卡住了，出新游戏了，游戏出新DLC了，旧游戏又回归了，股票涨了，股票跌了... 等等&lt;/p&gt;
&lt;p&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;我的计划包含&lt;code&gt;输入&lt;/code&gt;和&lt;code&gt;输出&lt;/code&gt;两部分&lt;/p&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;/ul&gt;
&lt;p&gt;&lt;strong&gt;我的出发点永远是精进自己的技能，因为我要靠这个赚钱&lt;/strong&gt;，虽然并不是只能靠这个，但是肯定没理由放弃自己最擅长的部分。&lt;/p&gt;
&lt;p&gt;其他知识的获取，主要是为了增加自己的厚度，看的远了，眼前的鸡毛蒜皮就不算什么了。其次，我要首先理解世界，才能告诉下一代我的理解。&lt;/p&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;/ul&gt;
&lt;p&gt;总结：&lt;strong&gt;抓住尽量多可用时间，去执行自己的计划&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然不可能利用好一切时间，只要能达到一个相对满意的程度就可以了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关于精力，我只能说：早睡早起，锻炼身体！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;关于正反馈，我指的是&lt;strong&gt;产出被别人看到，受到鼓舞，从而推动自己进行更多输入到输出的转换&lt;/strong&gt;。而不是那种企图获取自己家人的鼓励的正反馈，因为这件事在一个无法提供丝毫情绪价值的典型中式家庭里显得十分愚蠢。我对此无限悲观。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用充足的精力维持输入和输出，同时输出带来的正反馈，再持续强化这一过程。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这算是我近一年的最大感受了。但是请注意，这些放在整个生活里仍然是一小部分。&lt;/p&gt;
&lt;h2&gt;目前的打算&lt;/h2&gt;
&lt;p&gt;经过一年的摸索，现在我可以比较确定，我会把主要精力放在技术和技术相关的产出上。&lt;/p&gt;
&lt;p&gt;其他的副业，有机会就尝试，没有就算了，还没到刀架脖子上的程度。&lt;/p&gt;
&lt;p&gt;产出主要是文章和App。&lt;/p&gt;
&lt;p&gt;App可以是网页、客户端、插件、脚本，这些都算，但我会先以串联自己的工作流为目标，先让这个「集市」有个样子，而不是空想一些无法落地的功能，这样也能使我不至于一天啥也没干出来。&lt;/p&gt;
&lt;p&gt;文章则主要是学习技术和做App的伴生品，我会记录开发教程、灵感、经验、踩坑等等。可能还会有一些关于日常生活的主观分享、旅行记录等等。载体的话，一是发在各平台，比如公众号：「早早集市」，二是自建平台，比如博客、工具站、客户端工具。&lt;/p&gt;
&lt;p&gt;至于有没有用，我真的没法告诉你，因为&lt;strong&gt;这不是一条能够被快速验证的路，只能看自己是否乐在其中。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后，来回答一下标题，收益有多少？对于金钱来说，是负的🥲。&lt;/p&gt;
&lt;p&gt;但对于&lt;strong&gt;前进方向和自身的把控&lt;/strong&gt;上来讲，清晰了很多，已经很知足了。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[2024年，想入坑前端or后端开发需要学多少才算够用？]]></title><description><![CDATA[你是否好奇，如果要在前端或后端领域深耕，需要具备哪些技术要点才算足够?]]></description><link>https://zzao.club/post/daily/2024-front-end-jishuzhan</link><guid isPermaLink="true">https://zzao.club/post/daily/2024-front-end-jishuzhan</guid><pubDate>Mon, 19 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你是否好奇，如果要在前端或后端领域深耕，需要具备哪些技术要点才算足够?&lt;/p&gt;
&lt;p&gt;Github上有一个开源项目 &lt;code&gt;developer-roadmap&lt;/code&gt;，提供了一份完整的技术路线规划。在这里和大家分享一下&lt;/p&gt;
&lt;h2&gt;前端开发路线&lt;/h2&gt;
&lt;p&gt;![[1-img-20241119141198.png]]&lt;br&gt;
国内其实用&lt;code&gt;Vue&lt;/code&gt;偏多，但国外是&lt;code&gt;React&lt;/code&gt; , 生态也各不相同&lt;/p&gt;
&lt;h2&gt;后端开发路线&lt;/h2&gt;
&lt;p&gt;实际上我在网站上看到的，后端比较推荐&lt;code&gt;JavaScript&lt;/code&gt; 和 &lt;code&gt;Go&lt;/code&gt;，但下载下来的pdf没有提供他的&lt;code&gt;Personal Recommendation / Opinion&lt;/code&gt; 🤔。&lt;/p&gt;
&lt;p&gt;![[1-img-20241119141173.png]]&lt;/p&gt;
&lt;p&gt;相信这个对很多想跨入另一个领域，但不知道什么才是这个领域常见的技术路线的开发者朋友来说，会一些帮助和启发。&lt;/p&gt;
&lt;p&gt;如果你对其他领域，如Android、AI、大数据、游戏开发等等，也感兴趣，可以自行访问：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://roadmap.sh/&quot;&gt;https://roadmap.sh/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;国内国外的技术环境不能一概而论。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;足够与否完全取决于你所在的或长期居住的城市中对互联网技术的包容度。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但这不妨碍我们自由探索真正开放、包容、有深度的技术知识。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;希望对你有所帮助和启发，👋🏻👋🏻👋🏻&lt;/p&gt;</content:encoded></item><item><title><![CDATA[【contentlayer】兼容不同的md文件metadata的字段]]></title><description><![CDATA[markdown顶部可以设置一组元数据，用于解析成作者，发布日期，简介等信息]]></description><link>https://zzao.club/post/frame/contentlayer-md-metadata</link><guid isPermaLink="true">https://zzao.club/post/frame/contentlayer-md-metadata</guid><pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;markdown顶部可以设置一组元数据，用于解析成作者，发布日期，简介等信息&lt;/p&gt;
&lt;p&gt;使用过多种Markdown编辑器以及多种博客框架的人都知道，每个框架使用的字段有可能不太一样。&lt;/p&gt;
&lt;p&gt;比如简介。有的用的是&lt;code&gt;description&lt;/code&gt;，有的用的是&lt;code&gt;summary&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果你在一款md编辑器中写好了一篇文章，这篇文章被你使用Astro的某模板进行开发和部署，后面你又换了其他的框架，比如NextJS，结果新框架使用的matedata是不一样的，而你又有那么多的文章需要搬运。这无疑是一件很头疼的事。&lt;/p&gt;
&lt;p&gt;虽然作为一个程序员，可以自己写一个脚本，批量的把matadata修改一下，再放在某一个文件夹内，但这就明显会造成一种心智负担：很快你就会混淆，一篇文章的简介到底该写&lt;code&gt;desc&lt;/code&gt;，还是&lt;code&gt;description&lt;/code&gt;，还是&lt;code&gt;summary&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以，我希望可以是不关心文章的简介用什么字段表示，只管写文章就好了。&lt;/p&gt;
&lt;p&gt;于是乎，我就调研了一下&lt;code&gt;contentlayer&lt;/code&gt;的文档，把自己手头的模板稍稍改动一下，用来支持多种字段表示同一个信息&lt;/p&gt;
&lt;p&gt;我目前在用的&lt;code&gt;NextJS&lt;/code&gt;博客模板是&lt;code&gt;Tailwind Nextjs Starter Blog&lt;/code&gt;，所以我以这个为例&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;contentlayer.config.ts&lt;/code&gt;中，有一个&lt;code&gt;computedFields&lt;/code&gt;的配置&lt;/p&gt;
&lt;p&gt;假如我有些文章发布时期用的 &lt;code&gt;date&lt;/code&gt;， 有些用的 &lt;code&gt;published&lt;/code&gt;，那就可以这样配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const computedFields: ComputedFields = {
    // ...
    showDate: {
        type: &apos;date&apos;,
        resolve: (doc) =&gt; doc.date || doc.published,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，在Fileds里加入这两种字段&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const Blog = defineDocumentType(() =&gt; ({
    name: &apos;Blog&apos;,
    filePathPattern: &apos;blog/**/*.mdx&apos;,
    contentType: &apos;mdx&apos;,
    fields: {
        title: { type: &apos;string&apos;, required: true },
        date: { type: &apos;date&apos;, required: true },
        published: { type: &apos;date&apos;, required: false },
        // ...
    },
    computedFields: {
        // ...
    },
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再去修改date的文件里，可以边看页面边调试&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把相应的 &lt;code&gt;post.date&lt;/code&gt; 改为 &lt;code&gt;post.showDate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;const { date } = post&lt;/code&gt; 改为 &lt;code&gt;const { showDate } = post&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后再把&lt;code&gt;date&lt;/code&gt;插入的内容处，替换为&lt;code&gt;showDate&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;确保替换完&lt;/strong&gt;上述内容之后，你就可以随意在其他md编辑器中使用你设置好的多种字段了&lt;/p&gt;
&lt;p&gt;有任何问题，欢迎咨询。&lt;/p&gt;
&lt;p&gt;👋🏻👋🏻&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Memos Apis]]></title><description><![CDATA[注意：Memos版本为： V0.22.4]]></description><link>https://zzao.club/post/memos/memos-apis</link><guid isPermaLink="true">https://zzao.club/post/memos/memos-apis</guid><pubDate>Thu, 15 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;注意：Memos版本为： &lt;code&gt;V0.22.4&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;GET: /api/v1/memos?filter=&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Spelling the string directly may not succeed. I suggest requesting in the following way.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const filterValue = &quot;creator==&apos;users/1&apos;&amp;#x26;&amp;#x26;tag_search==[&apos;Blog&apos;]&amp;#x26;&amp;#x26;visibilities==[&apos;PUBLIC&apos;, &apos;PROTECTED&apos;]&amp;#x26;&amp;#x26;limit==30&quot;;
const encodedFilterValue = encodeURIComponent(filterValue);
const url = `https://example.com/api/v1/memos?filter=${encodedFilterValue}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;it&apos;s all about query params ↓&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var MemoFilterCELAttributes = []cel.EnvOption{
	cel.Variable(&quot;content_search&quot;, cel.ListType(cel.StringType)),
	cel.Variable(&quot;visibilities&quot;, cel.ListType(cel.StringType)),
	cel.Variable(&quot;tag_search&quot;, cel.ListType(cel.StringType)),
	cel.Variable(&quot;order_by_pinned&quot;, cel.BoolType),
	cel.Variable(&quot;display_time_before&quot;, cel.IntType),
	cel.Variable(&quot;display_time_after&quot;, cel.IntType),
	cel.Variable(&quot;creator&quot;, cel.StringType),
	cel.Variable(&quot;uid&quot;, cel.StringType),
	cel.Variable(&quot;row_status&quot;, cel.StringType),
	cel.Variable(&quot;random&quot;, cel.BoolType),
	cel.Variable(&quot;limit&quot;, cel.IntType),
	cel.Variable(&quot;include_comments&quot;, cel.BoolType),
	cel.Variable(&quot;has_link&quot;, cel.BoolType),
	cel.Variable(&quot;has_task_list&quot;, cel.BoolType),
	cel.Variable(&quot;has_code&quot;, cel.BoolType),
	cel.Variable(&quot;has_incomplete_tasks&quot;, cel.BoolType),
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[Next x Memos 懒人建站]]></title><description><![CDATA[我觉得博客站里的内容主要有两种展现形态。]]></description><link>https://zzao.club/post/memos/next-memos-quick-start-website</link><guid isPermaLink="true">https://zzao.club/post/memos/next-memos-quick-start-website</guid><pubDate>Tue, 13 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我觉得博客站里的内容主要有两种展现形态。&lt;/p&gt;
&lt;p&gt;一种是文章，有头有尾，长篇大论，输出观点，阐述事实。&lt;/p&gt;
&lt;p&gt;一种是动态，即兴发挥，图文为主，类似朋友圈动态，碎片化。&lt;/p&gt;
&lt;p&gt;文章好说，所有框架都能满足，无非就是把文章文件（markdown）放在项目里，然后编译出来，再部署。&lt;/p&gt;
&lt;p&gt;把动态加入进去，也许用Memos（指和Memos类似的开源作品）作为数据来源是个不错的选择。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;那既然能做博客上的动态页，是不是也能给其他站点作为数据源呢&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;起初我想选一个框架&lt;/h2&gt;
&lt;p&gt;都说前端卷，天天造轮子，天天撕逼，堪比娱乐圈。&lt;/p&gt;
&lt;p&gt;我学的速度还没框架更新速度快。&lt;/p&gt;
&lt;p&gt;那卷之后有什么好处呢？&lt;/p&gt;
&lt;p&gt;我觉得最大的好处就是有&lt;strong&gt;工具、插件、模板&lt;/strong&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;2024年了，能建博客站点的框架实在是太多。每个框架的模版又非常多。&lt;/p&gt;
&lt;p&gt;要是真挨个挑选下来，我估计我这文章也不用写了，时间全浪费在 选择 上了。&lt;/p&gt;
&lt;p&gt;所以我也不推荐什么框架了，萝卜青菜各有所爱，选了之后还是要写文章才有意义。&lt;/p&gt;
&lt;p&gt;但是呢，这个非常繁琐的选择过程，让我产生了一个想法。&lt;/p&gt;
&lt;h2&gt;然后想做一个选框架的导航站&lt;/h2&gt;
&lt;p&gt;刚才说到，前端框架卷的狠，产出多。&lt;/p&gt;
&lt;p&gt;所以现在就有了一个问题：只了解了&lt;code&gt;Vue&lt;/code&gt;、&lt;code&gt;React&lt;/code&gt;、&lt;code&gt;Node&lt;/code&gt;，这不足以让我直接做出东西来。&lt;/p&gt;
&lt;p&gt;就必须还得有个开发模板，有人帮我用&lt;code&gt;Webpack&lt;/code&gt;、&lt;code&gt;Vite&lt;/code&gt; 搭建好了项目，再帮我配置好一些基本的功能，像什么前端的&lt;code&gt;router&lt;/code&gt;、&lt;code&gt;store&lt;/code&gt;、&lt;code&gt;Tailwindcss&lt;/code&gt;，Node的&lt;code&gt;middleware&lt;/code&gt;、&lt;code&gt;orm&lt;/code&gt;、&lt;code&gt;jwt&lt;/code&gt; 等等。&lt;/p&gt;
&lt;p&gt;配好了还只是第一步，Node版本合不适合，框架版本新不新，数据库用的啥等等类似问题还有很多。&lt;/p&gt;
&lt;p&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;所以，我在准备开始这个导航站前，还是先捋了捋功能点（前端页面），捋了捋需要哪些接口（后端接口），怎么管理这些数据（管理系统）。&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&gt;用偷懒的方式实现&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;我用我当前的认知继续思考。&lt;/strong&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;code&gt;Memos&lt;/code&gt; 貌似能满足我的要求。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Memos&lt;/code&gt;是我用&lt;code&gt;docker&lt;/code&gt;直接部署到云服务器上的，现成的开源项目，我不用写代码。&lt;/p&gt;
&lt;p&gt;也提供了一个前端界面，可以录入文本，可以发图片，可以生成可管理的&lt;code&gt;Access Tokens&lt;/code&gt;用于接口请求。&lt;/p&gt;
&lt;p&gt;这就相当于，&lt;strong&gt;我在&lt;code&gt;Memos&lt;/code&gt;写东西，就是在给前端提供数据&lt;/strong&gt;，只要前端通过接口拿到一条Memo后，能根据某种规则解析出内容即可。&lt;/p&gt;
&lt;p&gt;我在Memos打个&lt;code&gt;导航&lt;/code&gt;的Tag开始发上几条Memo，然后前端获取&lt;code&gt;导航&lt;/code&gt;相关Tag的Memos，再把内容以另一种形式呈现出来。这不就是我想要的效果么？&lt;/p&gt;
&lt;p&gt;甚至我在Memos再发点有关其他品类的Memo，比如植物，那前端再换一种方式展示出来，这不又是一个养花养草的科普站点？&lt;/p&gt;
&lt;p&gt;果然，前端页面也是个换皮的游戏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一套数据，换个皮，换个交互，就是另一个App了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最终姿势&lt;/h2&gt;
&lt;p&gt;博客用&lt;code&gt;NextJS&lt;/code&gt;的模板搭建。模板是&lt;code&gt;tailwind-nextjs-starter-blog&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;博客的文章部分&lt;/strong&gt;，还是用传统的方式，把&lt;code&gt;md文件&lt;/code&gt;放在项目中，然后&lt;code&gt;build&lt;/code&gt;，&lt;code&gt;docker&lt;/code&gt;部署&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动态部分&lt;/strong&gt;，自己新开一个页面，用&lt;code&gt;Fetch&lt;/code&gt;去请求自己Memos服务的接口，筛选出想要展示的Memo。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;导航站&lt;/strong&gt;（如果还做的话），也是用Memos录入，打上指定Tag和权限，在前端获取后，写好一套解析规则，比如Markdown的里一级标题就是&lt;code&gt;title&lt;/code&gt;字段，内容标签也区分开来等等。&lt;/p&gt;
&lt;p&gt;这样，Memos就是我的数据录入平台+接口服务+后台管理系统，不需要开发，0成本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memos的使用方面&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在电脑端，用Memos的前端页面进行记录。&lt;/p&gt;
&lt;p&gt;在手机端，我喜欢在微信里打开H5页面，然后把页面添加到浮窗里。感觉要比使用Memos的App或者小程序要舒服很多。（不过要用https，不然时间长了可能会禁止访问）&lt;/p&gt;
&lt;p&gt;最后，要生产其他展示类的站点，就可以直接使用Memos里的数据，先把网站做出来，后续觉得有搞头的话，再把必要的后端服务具体实现出来。没人访问的话，只建一个前端也花不了几天，成本比较小了。&lt;/p&gt;
&lt;p&gt;然后&lt;strong&gt;涉及到数据&lt;/strong&gt;，记得&lt;strong&gt;时常备份&lt;/strong&gt;，被人攻击就重启+恢复备份。&lt;/p&gt;
&lt;p&gt;或者再给Memos限制一下只接受来自 47.xx.xx.xx 等ip的请求，也可以避免一下被人操作Memos，因为Memos的Api是公开的，别人很容易就知道你创建和删除Memos的接口。&lt;/p&gt;
&lt;p&gt;或者，在本地把Memos请求下来保存到文件里，直接把页面用Next编译成静态网站再发布，对于数据更新不太频繁的网站也是可以的。&lt;/p&gt;
&lt;p&gt;或者，自己在服务器加一层中间层，请求方式改为请求中间层，这样别人就不知道你创建和删除接口是怎么调用的了。&lt;/p&gt;
&lt;p&gt;当然，如果定时备份的话，自己再恢复也可以的。&lt;/p&gt;
&lt;h2&gt;后续优化&lt;/h2&gt;
&lt;p&gt;做好基本的基建工作后，后续优化我觉得就是&lt;strong&gt;把记录这件事再简化，做到随手可记&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;作为手动码字录入，Memos已经是挺方便了，我打开微信就能记录。&lt;/p&gt;
&lt;p&gt;进一步增加录入方式，大概是再加个浏览器插件，在网上冲浪的时候，看到想要记录的内容，选中后弹出个表单，简单填写后，直接通过Memos的接口录入进去。&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;hr&gt;
&lt;p&gt;ok，这就是全部内容啦。&lt;/p&gt;
&lt;p&gt;不知道对你有没有帮助呢？&lt;/p&gt;
&lt;p&gt;欢迎一起来讨论~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Memos自建服务入门指南]]></title><description><![CDATA[基于某个版本魔改自己的Memos]]></description><link>https://zzao.club/post/memos/memos-self-build-quick-start</link><guid isPermaLink="true">https://zzao.club/post/memos/memos-self-build-quick-start</guid><pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Memos是一个&lt;strong&gt;开源免费的私人版/家庭版推特&lt;/strong&gt;。适合可以在私人服务器、家庭Nas上自建服务，适合喜欢折腾的小伙伴。&lt;/p&gt;
&lt;p&gt;本文内容主要有三部分：&lt;/p&gt;
&lt;p&gt;1、从原作者仓库拉取Memos代码，并本地运行项目&lt;/p&gt;
&lt;p&gt;2、通过&lt;code&gt;docker&lt;/code&gt;发布自己的版本到&lt;code&gt;docker hub&lt;/code&gt; 和 &lt;code&gt;云服务器容器服务&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;3、在自己的服务器上&lt;strong&gt;运行和更新Memos&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;选择合适版本&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;首先选择一个版本&lt;/strong&gt;，克隆到自己本地，或者直接&lt;strong&gt;fork到自己的github仓库&lt;/strong&gt;再拉下来。&lt;/p&gt;
&lt;p&gt;因为每个版本的功能不太一致，可以先用 &lt;code&gt;docker desktop&lt;/code&gt; 跑一下各个版本，本地看看效果，了解一下哪个版本比较好用。&lt;/p&gt;
&lt;p&gt;也可以在本地用 &lt;code&gt;docker build&lt;/code&gt;命令运行当前拉下来的版本，再去&lt;code&gt;docker desktop&lt;/code&gt;跑起来&lt;/p&gt;
&lt;p&gt;注意有几个参数，我在图里做了标识。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/1-img-20240725180755.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我这里使用的是&lt;code&gt;0.22.3&lt;/code&gt;版本，早期版本我没接触过，所以直接用的最新版，看了下界面和功能完全够我使用了。&lt;/p&gt;
&lt;p&gt;注意选择后，咱们的版本就固定了，大概率不再跟随原作者更新，除非有非常好用的功能，再进行手动合并&lt;/p&gt;
&lt;p&gt;代码到本地后，首先要保证自己有运行环境。&lt;/p&gt;
&lt;p&gt;不懂&lt;code&gt;Node&lt;/code&gt;或&lt;code&gt;Go&lt;/code&gt;如何安装，直接问&lt;code&gt;AI&lt;/code&gt;就可以了，现在&lt;code&gt;AI&lt;/code&gt;的辅助能力其实要比大部分半吊子开发者要强得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 我用的豆包&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;安装依赖和运行项目&lt;/h2&gt;
&lt;p&gt;首先是&lt;code&gt;Go&lt;/code&gt;，根目录下就有&lt;code&gt;go.mod&lt;/code&gt;，里面是项目的依赖。&lt;strong&gt;不懂就直接问AI，可以解决80%的问题&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go mod tidy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是web，注意要 &lt;code&gt;cd ./web&lt;/code&gt;，他这里用的&lt;code&gt;react&lt;/code&gt;，不过框架对于老前端来说影响不大，基本都能很快上手&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是想看一下界面，可以先用docker跑起来&lt;/p&gt;
&lt;p&gt;打包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build ./ -t memoz --load  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包后就可以在你的 &lt;code&gt;docker desktop&lt;/code&gt; 里看到镜像了，直接点击运行即可，也可以用命令运行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /memos/:/var/opt/memos memoz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;memoz&lt;/code&gt;是我起的名字， &lt;code&gt;/memos&lt;/code&gt; 是我的挂载目录，最后的&lt;code&gt;memoz&lt;/code&gt;是我刚才打包好的镜像。这些要视情况更改。&lt;/p&gt;
&lt;p&gt;运行成功就可以打开 &lt;a href=&quot;http://localhost:5230&quot;&gt;http://localhost:5230&lt;/a&gt; 看到界面了。&lt;/p&gt;
&lt;p&gt;如果想要本地边开发边调试，可以分别运行go和web&lt;/p&gt;
&lt;p&gt;停掉docker运行的容器后，在项目根目录下先运行go服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go run bin/memos/main.go       
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后 &lt;code&gt;cd ./web&lt;/code&gt; 到前端目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后前端项目就会运行在 &lt;code&gt;http://localhost:3001&lt;/code&gt; ，此时再从浏览器访问就可以了。&lt;/p&gt;
&lt;h2&gt;发布到docker hub&lt;/h2&gt;
&lt;p&gt;memos推荐我们直接用作者发布好的包进行部署，像是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v ~/memos/:/var/opt/memos usememos/memos:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那如果我们想拉取自己的版本，就要像作者一样把自己的版本发布上去&lt;/p&gt;
&lt;p&gt;首先你要&lt;strong&gt;注册&lt;/strong&gt;好自己的账号（docker hub）&lt;/p&gt;
&lt;p&gt;然后要在本地登录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后给刚才&lt;code&gt;build的镜像&lt;/code&gt;打上标签，不写具体tag，默认就是&lt;code&gt;latest&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker tag &amp;#x3C;image_name&gt; &amp;#x3C;dockerhub_username&gt;/&amp;#x3C;repository_name&gt;:&amp;#x3C;tag&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后给打好标签的镜像推送上去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker push &amp;#x3C;dockerhub_username&gt;/&amp;#x3C;repository_name&gt;:&amp;#x3C;tag&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录到docker hub网站上，就可以看到自己发布的镜像了。&lt;/p&gt;
&lt;p&gt;然后在服务器上把自己发布的包跑起来就行了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v ~/memos/:/var/opt/memos &amp;#x3C;dockerhub_username&gt;/&amp;#x3C;repository_name&gt;:&amp;#x3C;tag&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;然后你就会发现，根本拉不下来&lt;/strong&gt;，哪怕是配置了代理，也没法顺利拉取&lt;/p&gt;
&lt;p&gt;所以我们还是要把自己的镜像发布到对应的云服务器厂商自己的容器服务上，由于我的服务器是&lt;code&gt;阿里云&lt;/code&gt;，所以此处我用阿里云演示&lt;/p&gt;
&lt;h2&gt;发布到阿里云容器服务&lt;/h2&gt;
&lt;p&gt;发布过程和&lt;code&gt;docker hub&lt;/code&gt;是一样的 ，只不过登录的是阿里云容器服务的账号密码，推送的目标也是阿里云的容器服务&lt;/p&gt;
&lt;h3&gt;登录&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo docker login --username=&amp;#x3C;your username&gt; registry.cn-beijing.aliyuncs.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：后面的地址&lt;code&gt;registry.cn-beijing.aliyuncs.com&lt;/code&gt;，可以在你的阿里云后台找到&lt;/p&gt;
&lt;h3&gt;打包&lt;/h3&gt;
&lt;p&gt;打包的时候也有一个坑，就是要注意服务器的&lt;code&gt;platform&lt;/code&gt;，在本地打包和push前就要加上参数。 比如我这里是&lt;code&gt;linux/amd64&lt;/code&gt;，你可以先不管，等在服务器上运行时，会提示你报错信息，然后再根据提示加上&lt;code&gt;platform&lt;/code&gt;参数重新打包发布也可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker buildx build ./ -t memoz --load --platform linux/amd64 (对应阿里云ubuntu服务器)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打Tag&lt;/h3&gt;
&lt;p&gt;打标签前，需要在阿里云控制台里，先创建好自己的命名空间，我这里是&lt;code&gt;zzstudio0&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker tag memoz registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;发布&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker push --platform linux/amd64 registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;此时本地操作已经完成，登录到云服务器再去拉取和运行发布好的镜像&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;运行和更新&lt;/h2&gt;
&lt;h3&gt;拉取&lt;/h3&gt;
&lt;p&gt;拉取前也是得登录，参考上边&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull --platform linux/amd64 registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运行&lt;/h3&gt;
&lt;p&gt;运行的镜像名称可以通过  &lt;code&gt;docker images&lt;/code&gt; 查看，也就是 &lt;code&gt;registry.cn-beijing.aliyuncs.com/zzstudi0/memoz&lt;/code&gt; 部分换成你自己的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /home/memoz/:/var/opt/memos registry.cn-beijing.aliyuncs.com/zzstudi0/memoz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后通过 &lt;code&gt; docker ps&lt;/code&gt;，可以看到正在运行在&lt;code&gt;5230&lt;/code&gt;端口下，如果想直接通过5230端口访问，只要在阿里云&lt;code&gt;控制台-云服务器-安全组&lt;/code&gt;中打开端口即可&lt;/p&gt;
&lt;p&gt;当自己在本地新增或者修改了某些功能后，&lt;strong&gt;再按照上述的步骤，重新发布一个新版本的镜像&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后在更新和重新运行镜像&lt;/p&gt;
&lt;h3&gt;更新&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;container_id&lt;/code&gt; 可以通过 &lt;code&gt;docker ps&lt;/code&gt; 查看到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;停止目前运行的容器
docker stop container_id
删除容器（为了避免名称冲突）
docker rm container_id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后拉取新推送的版本， 比如&lt;code&gt;1.1.0&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull --platform linux/amd64 registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:1.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再重新运行新版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name memoz -p 5230:5230 -v /home/memoz/:/var/opt/memos registry.cn-beijing.aliyuncs.com/zzstudi0/memoz:1.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再从外网访问，检查页面是否有所变化&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文介绍了如何本地运行&lt;code&gt;Memos&lt;/code&gt;，以及如何发布和部署在自己的云服务器上。&lt;/p&gt;
&lt;p&gt;因为Web部分只是普通的&lt;code&gt;React+Vite&lt;/code&gt;，对大部分人没什么难度，所以后面修改细节就不水文了，功能方面的改造会水一水。&lt;/p&gt;
&lt;p&gt;说到底，这只是一个普通的内容工具，关键还是真的有内容要发布才行。&lt;/p&gt;
&lt;p&gt;文章发布时，作者已经更新了&lt;code&gt;0.22.4&lt;/code&gt;版本，修改了标签和分类的筛选机制（&lt;code&gt;0.22.3&lt;/code&gt;版本比较难用），把热力图也挪到了首页，所以推荐新入坑的小伙伴直接上最新的版本。&lt;/p&gt;
&lt;p&gt;以上就是全部内容啦👏&lt;/p&gt;
&lt;p&gt;对Memos感兴趣的话欢迎私聊讨论~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[推荐几款程序员常用的笔记App]]></title><description><![CDATA[热爱就是唯一的意义]]></description><link>https://zzao.club/post/daily/developer-useful-apps</link><guid isPermaLink="true">https://zzao.club/post/daily/developer-useful-apps</guid><pubDate>Fri, 19 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如果你也有记录笔记的习惯，可以看看我推荐的这几款App，&lt;strong&gt;从开箱即用到支持复杂定制&lt;/strong&gt;，&lt;strong&gt;从云端同步到本地存储&lt;/strong&gt;，总有一款适合你。&lt;/p&gt;
&lt;h2&gt;飞书&lt;/h2&gt;
&lt;p&gt;程序员一般记录的都是自己的技术心得，实践历程，别的App还得下载，如果你正在飞书办公的话，直接打开飞书文档就可以写起来了。&lt;/p&gt;
&lt;p&gt;打开知识库新建文档，还有很多模版可以选择，所以对于单单说“记录”这个需求，飞书是肯定够用的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429816.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;并且写完之后，可以设置成公开，直接分享给其他人，也能获得其他人的收藏和评论，查看被多少人查看等等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429817.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你想导出的话，飞书&lt;strong&gt;只支持导出Word、PDF&lt;/strong&gt;，对于程序员最需要的Mardown格式反而没有，如果呼声够大的话，应该也会加上。&lt;/p&gt;
&lt;p&gt;数据存储在飞书的服务器上。&lt;/p&gt;
&lt;h2&gt;语雀&lt;/h2&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://www.yuque.com/download&quot;&gt;https://www.yuque.com/download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;语雀是阿里巴巴出品的产品，支持IOS、安卓、MacOS、Windows、iPad。详细信息大家可以去官网查看一下。&lt;/p&gt;
&lt;p&gt;语雀使用起来感官上要比飞书舒服很多，很干净。&lt;/p&gt;
&lt;p&gt;App操作逻辑也比较清晰，如果是日常记录，非常够用了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429819.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同样也支持非常&lt;strong&gt;丰富的模板&lt;/strong&gt;，对于普通人来说，写文章、写心得、记录旅游、记账、分享，这几样都能满足。&lt;/p&gt;
&lt;p&gt;不断的换App的最大问题恐怕还是颜值，其次就是数据存储上免费版够不够用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429820.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种支持多端文档同步的App，不可能不收费，不然就活不下来，这几天程序员圈子里闹的沸沸扬扬的老牌博客站「博客园」就是一个例子。&lt;/p&gt;
&lt;p&gt;所以大家看自己的情况，免费版够用就用免费版，付费会员看自己接受程度，我90%的人估计都是轻度用户。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;导出支持的格式比较多 ： word、paf、markdown、jpg、语雀文档&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然，你的数据也是存在它自己的服务器上。&lt;/p&gt;
&lt;h2&gt;我来（wolai）、Notion&lt;/h2&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://www.wolai.com/&quot;&gt;https://www.wolai.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于Notion我只是轻度试用了一下，没有太深入使用。所以这里着重介绍一下wolai&lt;/p&gt;
&lt;p&gt;（我知道有很多人把Notion玩出了花，一边用作知识库，一边还能直接建站。）&lt;/p&gt;
&lt;p&gt;wolai，我用起来就类似一个国内版的Notion。&lt;/p&gt;
&lt;p&gt;这是它的界面，左侧是文件列表，右侧是文件内容&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429821.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;支持块级双链，方便你串联知识，不过我对这个块级双链用的不太好，没有那么多东西可以串联🥲&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429822.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是这也是它的一大特色，每一段文字、图片、表格等等都会视为一个块，可以充分发挥你的想象。但也是要求你有比较多的积累素材才能把它真正用起来。&lt;/p&gt;
&lt;p&gt;同样也可以生成分享链接，设置权限等等。&lt;/p&gt;
&lt;p&gt;也支持绑定微信服务号，给服务号发消息即可保存在wolai中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429823.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;颜值方面和丝滑度方面，个人感觉比语雀略逊一筹。&lt;/p&gt;
&lt;p&gt;同样&lt;strong&gt;数据存储在云端&lt;/strong&gt;，多端同步。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS: 上述几款App，差不多都是向Notion看齐。但由于Notion服务器问题，国内使用不是很方面，所以受众有限。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flomo  浮墨笔记&lt;/h2&gt;
&lt;p&gt;下载地址： &lt;a href=&quot;https://flomoapp.com/&quot;&gt;https://flomoapp.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;和上边介绍的几个不同，Flomo更加轻量化，记录的内容也更加碎片化，适合你突然来的灵感，或者一时的感悟。（记录长的肯定也可以&lt;/p&gt;
&lt;p&gt;界面长这样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429824.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是我的Mac端&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429825.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你平时不是要写那种长篇大论的文章，或是记录庞大的知识体系，Flomo的使用场景真的非常适合。&lt;/p&gt;
&lt;p&gt;类似你随手发了个朋友圈一样，简单快捷，不拘泥于语法，内容至上。回顾自己的笔记时，靠左侧的标签分类来查找，标签支持多级。&lt;/p&gt;
&lt;p&gt;笔记之间也支持链接，但需要pro会员才可以。因为我没有尝试，所以不知道是不是双向的链接，不过猜测是的。&lt;/p&gt;
&lt;p&gt;绑定Flomo的微信服务号，可以通过给微信的Flomo服务号发消息来进行快速记录，这一点和wolai是一样的。&lt;/p&gt;
&lt;p&gt;这是他们的付费标准&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429826.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Obsidian&lt;/h2&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://obsidian.md/&quot;&gt;https://obsidian.md/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果你想找到一个功能齐全，同时又完全把数据保存在本地的App，那一定绕不开Obsidian。&lt;/p&gt;
&lt;p&gt;这是它的初始界面&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191429827.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时也有非常多的主题可以下载，可以自由选择各种markdown配色，所以说它的颜值不是问题，&lt;strong&gt;问题在于你能不能接受它的繁琐&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果你不使用插件，其实也能满足你绝大部分的场景，除了链接分享。因为它的文件都是在你的本地。&lt;/p&gt;
&lt;p&gt;在我使用起来，Obsidian还是更偏向于文章类的长篇的记录，如果记录的片段过于琐碎，只会让左侧的文件目录更加庞大。（当然有类似插件如Thino，支持你记录碎片内容）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429828.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，它就类似于vscode，有非常活跃的插件社区，基本上你能想到的笔记功能，都能在里面实现。但这同样也是它的缺点，因为对于不想折腾的人来说，或者是非程序员，看起来有一些上手难度。&lt;/p&gt;
&lt;p&gt;比如你要实现使用github来进行笔记的云端同步，那就得使用Obsidian Git插件，同时还要知道github的使用方法，这无疑都是门槛。&lt;/p&gt;
&lt;p&gt;如果你愿意折腾一下的话，Obsidian绝对会越用越顺手。基本的写作还是要求你懂一些Markdown语法来达到最好的效果。当然，完全不懂的话，也不是什么问题，就只管写就好了。&lt;/p&gt;
&lt;p&gt;如果说你觉得Obsidian太过于繁琐，上手难度太高，我还有另一个轻量化的App推荐给你：Typora&lt;/p&gt;
&lt;h2&gt;Typora&lt;/h2&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://typora.io/&quot;&gt;https://typora.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;typora也是一个markdown编辑器和阅读器，我在它的很早期的bate版本就使用过。&lt;/p&gt;
&lt;p&gt;它和Obsidian一样，都支持markdown语法的实时渲染。&lt;/p&gt;
&lt;p&gt;同时它更加的轻巧，不像Obsdian一样有很多插件需要研究，看起来是一个让你专注于写作的好帮手。&lt;/p&gt;
&lt;p&gt;这是它的界面，同样有很多主题可以选择。&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191429829.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;它的官网下载应用包只有Mac和windows，所以我也不清楚它有没有移动端App。&lt;/p&gt;
&lt;p&gt;支持Markdown文件的导入和导出，导出格式也比较丰富。&lt;/p&gt;
&lt;p&gt;我不再使用的原因，就是因为它过于简洁，不太符合我的需求。我还是希望多多少少可以有一些辅助功能，让记录的文字更加灵活和有序一些。&lt;/p&gt;
&lt;p&gt;不过现在又多了一个理由：它开始收费了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429830.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
不过它有15天的试用期，你可以下载尝试一下。&lt;/p&gt;
&lt;h2&gt;Memos&lt;/h2&gt;
&lt;p&gt;官网地址： &lt;a href=&quot;https://www.usememos.com/&quot;&gt;https://www.usememos.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最后一个要介绍的是可以自建服务的笔记App。&lt;/p&gt;
&lt;p&gt;这里推荐的是Memos，但不仅限于Memos，还有一些其他非常优秀的开源项目。&lt;/p&gt;
&lt;p&gt;这是Memos的界面，有没有感觉有点熟悉，他早一些的版本功能其实和Flomo是基本一致的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191429831.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Memos后端使用的是Go，前端使用的是React，完全开源，支持Docker部署，目前（2024-07-15）版本是V0.22.3。&lt;/p&gt;
&lt;p&gt;作者的更新内容也比较有争议，砍掉了很多大家觉得好的功能，也有删掉又加回来的功能。&lt;/p&gt;
&lt;p&gt;所以非常建议大家自己部署时，锁定版本，只要够用就行。毕竟更新再频繁，到底还是一个笔记App，核心还是记录。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后面如何部署和二开，我会单独再写一篇&lt;/strong&gt;，这里只介绍部署后的功能点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据存储问题&lt;/strong&gt;。可以部署在云服务器，本地内网环境，家庭Nas环境里，数据存储全看你的盘大小。不过云服务器访问速度看你的服务器配置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Api功能&lt;/strong&gt;。这一点放在Flomo里也是收费功能，Memos可以生成Token，你可以在外部调用自己的接口，进行读写操作。也可以配置webhook，用来通知飞书、钉钉、微信等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分享&lt;/strong&gt;。点击分享可以直接生成一个链接，这个链接就是你部署后的地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;双向链接&lt;/strong&gt;。发布一条笔记时，可以链接另一条笔记，两个笔记之前就可以相互点击跳转，其实这个链接就是上边分享用的链接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主题&lt;/strong&gt;。它的主题是需要你自己写入css文件，目前我还没发现有写好的别的主题，不过它自带的样式其实还算简洁，这也是我选择它的一个原因。&lt;/p&gt;
&lt;p&gt;最最重要的，就是&lt;strong&gt;定制/魔改&lt;/strong&gt;。如果不是为了定制，其实完全都不用选memos，浪费服务器钱，浪费时间，最后只换来了其他App一样的效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过定制可以实现独一无二的UI界面，更多的接口功能。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也可以同时学习React和Go，在实践中提升自己的技术，因为这算是一个有着非常明确需求的项目，你做什么功能完全比着Flomo抄就可以了，同时技术栈还比较新，比什么用xx前端框架复刻一个饿了么这种项目要有用的多。&lt;/p&gt;
&lt;p&gt;那你可能就要问了，我废了这么多劲儿，最后得到的不还是一个普普通通的笔记App吗？有什么意义吗？&lt;/p&gt;
&lt;p&gt;首先能问出这个问题，肯定是已经在技术上丧失热情了，把研究技术当成费力不讨好，想必咱们之间也没什么好说的。&lt;/p&gt;
&lt;p&gt;这个问题就类似有人问：为什么你一个钓鱼佬，十次有九次空军，还有一次钓到地球，还花那么多钱去买装备？半夜还要去钓鱼？这有什么意思吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这就是热爱，你他妈的懂不懂啊😤&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;首先我们排除一个连免费版都没有的：Typora。&lt;/p&gt;
&lt;p&gt;前提是平时有写作习惯。如果都没有写文章记录的习惯，直接使用微信朋友圈、小x书这类App就可以了。&lt;/p&gt;
&lt;p&gt;如果你懒得折腾，但需要分享给朋友或社群看，喜欢写长文的话，推荐飞书、语雀、wolai这一类的App，写完了直接就可以发链接。喜欢分享感悟，碎片笔记，可以选择Flomo，写完后导出图片进行分享。&lt;/p&gt;
&lt;p&gt;如果你热衷于折腾，平时也有大量写作和记录的需求，又想要一个完全离线版的笔记App，Obsidian是值得一试的。&lt;/p&gt;
&lt;p&gt;而Memos适合你喜欢研究前端和Go，并且记录和分享的内容也是一些碎片笔记。&lt;/p&gt;
&lt;p&gt;甚至不是因为你多么需要笔记App，而是单纯的为了把玩一个有趣的开源作品。&lt;/p&gt;
&lt;p&gt;这就是唯一有意义的意义。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;还有很多App，像是印象笔记、思源、为知、oneNote、有道云笔记等等等等，太多了。&lt;/p&gt;
&lt;p&gt;如果你有推荐的神器，欢迎在评论区给大家指路👏👏👏&lt;/p&gt;</content:encoded></item><item><title><![CDATA[本地版微博?开源版Flomo？服务器自建Memos有什么好处]]></title><description><![CDATA[折腾一下Memos，这是部署篇]]></description><link>https://zzao.club/post/memos/local-weibo-folomo-memos</link><guid isPermaLink="true">https://zzao.club/post/memos/local-weibo-folomo-memos</guid><pubDate>Thu, 11 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一般写博客，都是长篇文章，类似我现在这篇，是一个有头有尾有内容的长文。&lt;/p&gt;
&lt;p&gt;但是如果是灵光乍现，或是一时的牢骚，或是转瞬即逝的感慨，这种片段内容你们会发在哪里呢？&lt;/p&gt;
&lt;p&gt;朋友圈肯定是个首要的选择，微博好像也可以，小红书也不错。但是你发的内容如果更加私人化，不想被别人看到，或是你无数灵感中的一个，后续还要整合起来，这种公共平台可不会支持你导入导出。&lt;/p&gt;
&lt;p&gt;app图标图&lt;/p&gt;
&lt;p&gt;所以很自然的就会诞生一些针对短文的卡片笔记App，比如Flomo。非常适合随手记录，可以和你的微信进行联动，并且有热力图，可以设置标签分类。&lt;/p&gt;
&lt;p&gt;但是只要是公司出的App，就不会完全任由你白嫖，别人也要养活员工的不是，所以就会有会员或者买断机制。并且为了多端同步，云服务器的开销肯定也要你出一份力。&lt;/p&gt;
&lt;p&gt;要花钱是一方面，另一方面很多人是不愿意自己的文字或灵感被其他App上传到云端的，不管他到底有没有扫描你的内容。&lt;/p&gt;
&lt;p&gt;比如我本地使用的Obsidian，完全本地，多端同步靠私有的Github仓库，有丰富的插件可以折腾，绝对的够用，非常适合用Markdown的写作者。&lt;/p&gt;
&lt;p&gt;那Memos有哪些特殊的优势呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开源、免费。这是最重要的，意味着你可以随意定制属于自己的Memos。&lt;/li&gt;
&lt;li&gt;不依赖Markdown。可以用，但不用也没事，就和发朋友圈一样，写字+配图&lt;/li&gt;
&lt;li&gt;数据本地化。写了一堆心得、感悟，随时可以导出Markdown&lt;/li&gt;
&lt;li&gt;Api访问。支持通过自己生成指定过期时间的token来自定义其他的玩法，比如从微信直接录入到Memos中&lt;/li&gt;
&lt;li&gt;可私有，可公开。人难免需要把自己的想法分享给他人获得共鸣，Memos也支持你分享某一条&lt;code&gt;朋友圈&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如上则是一些比较大众的优势&lt;/p&gt;
&lt;p&gt;在我看来，Memos可以试做一个小型的知识库。同时，你的知识片段需要时常去分享出去验证，而不是一直躺在本地，此时Memos就显得十分便利。&lt;/p&gt;
&lt;p&gt;在分享的过程中，别人首先会对你的内容感兴趣，其次也会好奇你的分享方式。最后说不定也能发展成为小群体之间的知识库，比如面向小区的植物类科普空间，大家可以分享下自己的日常干货。&lt;/p&gt;
&lt;p&gt;日常使用时，PC端就使用Web页面。手机端我喜欢使用微信里直接打开链接，然后添加到浮窗。个人感觉这样要比App（Meo Memos）要好用一些，App里更倾向于早期一些基础功能，和现版本的Memos有点脱节。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[找个长期的事做]]></title><description><![CDATA[当前任何的焦虑都来源于金钱]]></description><link>https://zzao.club/post/daily/find-something-todo</link><guid isPermaLink="true">https://zzao.club/post/daily/find-something-todo</guid><pubDate>Tue, 09 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&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;p&gt;除了赚钱，其实最长期的事儿就是锻炼身体，这个是你不管平时工作收入如何，心情如何，只要去做了， 就会实打实的反馈给自己的一件事。&lt;/p&gt;
&lt;p&gt;但人都是懒的，运动有点反人性，能坚持去锻炼真的不容易。尤其是有孩子的情况下，还要找个合适的时间去。&lt;/p&gt;
&lt;p&gt;目前看来，已知的是闲鱼可以倒卖电子产品，货来自于中间商，中间商依靠我们这些下线，在老板那边低价拿货（承诺每个月帮老板卖xxxx台），拿了货再发给我们下线去卖，利润归下线，下线一次性最少拿xx台。&lt;/p&gt;
&lt;p&gt;相当于老板是一点都不担风险的，出了货就拿到了利润。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;中间商&lt;/code&gt;负责的是教下线如何闲鱼卖货，&lt;code&gt;吃一笔一次性学费&lt;/code&gt;（就算是不教你，其实他自己也要卖货，但你无法获得老板这样的货源），也相当于&lt;code&gt;给自己以后其他的副业项目背书&lt;/code&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;</content:encoded></item><item><title><![CDATA[花时间研究赚不到钱的副业，还是花时间做个没人用的产品]]></title><description><![CDATA[做内容！做内容！做内容！做内容！做内容！做内容！]]></description><link>https://zzao.club/post/side-hustle/do-some-sidehustle-or-do-some-code</link><guid isPermaLink="true">https://zzao.club/post/side-hustle/do-some-sidehustle-or-do-some-code</guid><pubDate>Wed, 03 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141184.png&quot; alt=&quot;&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;花时间在副业上，没赚到钱，但是能提升对副业的敏感度，不知道什么时候就上了道了。&lt;/p&gt;
&lt;p&gt;花时间在产品，没人用，但是能提升技术之外的譬如架构、设计、推广之类的经验，说不定什么时候也能火一把。&lt;/p&gt;
&lt;p&gt;真是难评。（当然，这里副业指的是网赚的那种）&lt;/p&gt;
&lt;p&gt;比如我前一段时间。&lt;/p&gt;
&lt;p&gt;起初，是有要还原一下小红书web端的想法。当然，只是为了在技术上表示自己能实现（最后也实现了他的效果。&lt;/p&gt;
&lt;p&gt;先用uniapp里实现了一下两列的瀑布流，又用NextJS实现了动态列的瀑布流，又想用Vue实现一遍，我真是疯了。。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141108.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;恰好，前一阵又和老婆谈论起孩子周岁请摄影师的问题。&lt;/p&gt;
&lt;p&gt;家庭摄影师上门拍摄，一套是&lt;code&gt;599&lt;/code&gt; ~ &lt;code&gt;899&lt;/code&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;strong&gt;真的，我真是一个标准的程序员，每个想法的背后都直通内个大道：「我能不能自己实现呢」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;老想着搞一个本地化的类似小红书用于承载内容，而实际上内容还没开始。😮‍💨&lt;/p&gt;
&lt;p&gt;先把内容充实起来，技术方面的东西其实很快就出来，内容是最难打磨的。&lt;/p&gt;
&lt;p&gt;而且在没有足够多的内容之前，其实是不能确定要如何呈现内容的，所以又回到了最初的问题：「&lt;strong&gt;怎样积累自己的内容&lt;/strong&gt;」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/1-img-20240830210833.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;虽然已经这样想了，但总觉得某些会有使用场景，会忍不住去把他做出来，就是不知道场景在哪里～～&lt;/p&gt;
&lt;p&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;&lt;strong&gt;这样看的话，其实真不如把它做出来。&lt;/strong&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;而且习惯了被公司安排具体的活干，和自己想出一个能解决别人需求的产品，完全是两个方向的思路。其中唯一不用担心的就是技术问题。&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;strong&gt;研究新技术、看底层原理、造轮子会产生一种负罪感。&lt;/strong&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;</content:encoded></item><item><title><![CDATA[打工仔请睁眼看世界]]></title><description><![CDATA[闭眼工作的时候世界是那么的美好，睁眼看世界的时候人人都想赚我的钱。]]></description><link>https://zzao.club/post/side-hustle/please-open-your-eyes</link><guid isPermaLink="true">https://zzao.club/post/side-hustle/please-open-your-eyes</guid><pubDate>Tue, 07 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;闭眼工作的时候世界是那么的美好，睁眼看世界的时候人人都想赚我的钱。&lt;/p&gt;
&lt;h2&gt;1&lt;/h2&gt;
&lt;p&gt;我是一个打工仔。&lt;/p&gt;
&lt;p&gt;我的工作就是完成老板安排好的任务。&lt;/p&gt;
&lt;p&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;完成的好，甚至还会多给我钱。&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;strong&gt;老板发什么我都只需要发大拇指👍🏻和收到。&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141195.png&quot; alt=&quot;&quot;&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;strong&gt;横竖都是一天，只要时间一过，工资就会到手。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141174.png&quot; alt=&quot;&quot;&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;/p&gt;
&lt;p&gt;可以买一张最新的显卡；&lt;/p&gt;
&lt;p&gt;可以买 steam 新出的游戏；&lt;/p&gt;
&lt;p&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;于是我开始为自己打工&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&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;p&gt;赚钱是少数者的游戏，是 &lt;strong&gt;（努力+执行+数量 + 质量+ 头脑+视野+变通）x 赛道加持 x运气&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我懵了，超过一个未知数的算术题，我向来是不擅长的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141181.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在一天24小时都变成了自己的时间。&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;img src=&quot;https://img.zzao.club/article/1-img-20241119141149.png&quot; alt=&quot;&quot;&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;/p&gt;
&lt;p&gt;&lt;strong&gt;我去菜市场花一块钱买菜，卖菜大妈都不会给我一个烂叶子。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想了想，算了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我删掉了不少于200字的不重复的问候。&lt;/strong&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;于是我在群里发了一个大拇指👍🏻，并顺手把群折叠了一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141102.png&quot; alt=&quot;&quot;&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;strong&gt;赚钱使我日夜不安&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我的灵魂里好像也有了一个恶魔。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它不停的对我说：人生不可空过。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可是人生，尤其是我的人生就要空过了，简直让人发狂。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;还不如让我和以前一样心安理得的过日子&lt;/strong&gt;。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[30岁的身体，18岁的灵魂，60岁的心态]]></title><description><![CDATA[这一届的年轻人格外拉跨]]></description><link>https://zzao.club/post/daily/30-year-old-bold-18-year-old-soul-60-year-old-inside</link><guid isPermaLink="true">https://zzao.club/post/daily/30-year-old-bold-18-year-old-soul-60-year-old-inside</guid><pubDate>Mon, 08 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;进可攻，退可守&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我有一种感觉，到了 30 岁，人生才刚刚开始&lt;/p&gt;
&lt;h2&gt;1&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;30 只是个数字，因人而异，我的 30 岁也可以是你的 25 岁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对我而言，30 岁意味着经历了学业、恋爱、工作、结婚、生子这几件人生大事，同样也意味着以后肯定也还有很多我不知道的大事&lt;/p&gt;
&lt;p&gt;现在看来学业和恋爱作为时间上占比 70% 的基础积累，仅靠义务教育远远不够。更不要说，甚至好多人 22 岁以前主线任务只有学业&lt;/p&gt;
&lt;p&gt;要在剩下的8年里，完成情商、沟通、财商、情绪管理、时间管理这些重要素质的铺垫，还要外加对家庭的感悟，对教育孩子的探索等等，我觉得非常难。&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;strong&gt;要靠平时看的书，认识到的人&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ok，接触到了，算是第一步。&lt;/p&gt;
&lt;p&gt;但&lt;strong&gt;生活不是考试&lt;/strong&gt;，学习期间看的是考试相关的书，练的是考试相关的题，考起试也都是相关的知识，好好学是可以拿高分的&lt;/p&gt;
&lt;p&gt;沟通，理财，时间管理这些我怎么去刻意练习呢？&lt;/p&gt;
&lt;p&gt;&lt;em&gt;沟通就是挨骂，挨批评&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;理财就是拿着饭钱不吃饭，省下来偷偷买点玩具，买点零食&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;时间管理就是晚上挑灯看小说，第二天白天照上课不误&lt;/em&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;strong&gt;所有的技能都要靠言传身教&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;怎么理财，怎么沟通，怎么管理情绪这都是要手把手教的&lt;/p&gt;
&lt;p&gt;&lt;em&gt;没见过钱，没花过钱的人怎么理财？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;只看到父母天天大吼大叫，不开心了就发泄，怎么管理情绪？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;有问题不说，问就是我的错，谁会有沟通的欲望？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;刻意练习达到的效果被其他人看到后就变成了一句夸奖。&lt;/p&gt;
&lt;p&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;更有意思的是，就算重新编排了过去的 30 年，大概率此刻应该还会后悔&lt;/p&gt;
&lt;p&gt;人生不如意事十有八九，此刻能认识到，还不去尝试，就是未来的遗憾&lt;/p&gt;
&lt;p&gt;所以 30 岁只是一个开始，是意识到自己有那么多不足后，又重整旗鼓的开端。&lt;/p&gt;
&lt;p&gt;最最重要的是，我此刻意识到的不足并想办法学习，思考，总结成自己的东西。对于孩子来说，从他开始懂事，我就是拥有某些长处的家长了，就可以在某些方面作一个榜样了。&lt;/p&gt;
&lt;p&gt;我的过往虽不完美，但对他来说并不可见&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;18 岁代表了欲望，是源动力&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;今年是工作第 7 年了&lt;/p&gt;
&lt;p&gt;现在每天上班的节奏就是，把工作安排好，然后尽快完成手中的活，腾出的时间来去学习和探索&lt;/p&gt;
&lt;p&gt;因为工作时间比较久了，Vue 已经用了 7 年，比较熟了，在项目上基本上属于帮同事们解决疑难问题的状态&lt;/p&gt;
&lt;p&gt;普通的业务需求很短时间就开发完了，搞一搞有难度的才能让我兴奋起来&lt;/p&gt;
&lt;p&gt;这几年从前端学到了后端，云服务器也买过了不少，也学会了用 docker 去部署，自己搞个项目用一用已经不是什么问题&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;strong&gt;我也想用上班时间上几把分，下几把铲铲，可惜情况不允许啊&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;目前感受下来，状态要比 17 年刚入行的自己要好很多。既有了一些技术储备，还保持着热情。回看前两年也成长了不少，和自己比能保持一些进步，这就足够了，多的也奢求不来。&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&gt;3&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;60 岁代表了心态，是动态的佛系&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;p&gt;如果是想正经搞个副业的话，先尽可能的多看多搜，相信也能了解个七七八八&lt;/p&gt;
&lt;p&gt;最简单的，去某鱼某多买点那种打包的副业资料，自己花点时间通读一遍，先把副业的玩法了解个大概，没有必要去网上找一个毫不相干的陌生人&lt;/p&gt;
&lt;p&gt;你可以把副业当成一个项目上的业务一样去看待，要立项，要调研，要优化，要落地，要有 deadline，然后你就会发现，谁会买和怎么告诉别人来买的问题，顺着问题一路解决下去就可以了&lt;/p&gt;
&lt;p&gt;还有的朋友，非常害怕自己要发展长期主义的话，几年下去看不到结果，怕浪费了时间和精力之后无法变现，就没有意义了&lt;/p&gt;
&lt;p&gt;首先我觉得，如果要追求变现的话，为什么要选择做长期主义呢，短期有那么多变现的办法，花点门票钱，进一个社群，各种小项目挺多的呀。&lt;/p&gt;
&lt;p&gt;其次，我理解的长期主义意味着我本身才是最有价值的产品，别人买单也是因为我是个靠谱的人才买单，要不怎么叫&lt;code&gt;个人IP&lt;/code&gt;呢？ 每一天投入在提升自己的技能点、认知的精力，每一次更新的动态、输出的能量能够对他人有所帮助或鼓舞，长期积累起来才慢慢体现出来我的价值。所以大可不必担心投入几年没成效的问题，压根没那么容易坚持住，投入三个月不间断已经打败了99%的人了。所以，每一次对自己的投入都是有意义的，但这些意义确实需要时间去体现。&lt;/p&gt;
&lt;p&gt;最后，没有收益这件事人人都怕。不是人人都能发现，不是人人都能坚持，也不是人人都能幸运。 这都筛选了多少层了，我被筛选掉也很正常。&lt;/p&gt;
&lt;p&gt;60岁的心态就是为日常的各种失败兜底的，当个普通人也没有错&lt;/p&gt;
&lt;p&gt;守住主业，发挥优势，多去尝试，拥抱失败。这四条我觉得很重要，顺序也很重要。很多转型的大佬都是挂着以前的Title，大家选择起来也更愿意相信这些已经在某些领域做出过成绩的人。&lt;/p&gt;
&lt;p&gt;另外，烦心事多着呢，犯不着在一个事儿上纠结，再说人生就这几十年，忍一忍就死了个屁的了&lt;/p&gt;
&lt;p&gt;还有，以前总听说万事开头难，实际上“事“以亿计，不难的事多的很。不要被思维惯性骗了，只是咱接触的少，不知道而已。&lt;/p&gt;
&lt;p&gt;有好心态才方便变通：“破守成规”&lt;/p&gt;
&lt;h2&gt;4&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;不动如山，动如雷霆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;30岁的过往已经翻篇，代表不了以后&lt;/p&gt;
&lt;p&gt;保持热情，摆正心态，多和&lt;strong&gt;在同样道路上觉得比自己做的好的&lt;/strong&gt;人交流&lt;/p&gt;
&lt;p&gt;不管是多少岁，都是开始&lt;/p&gt;
&lt;p&gt;干就完了!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[“无薪加班，最为致命”]]></title><description><![CDATA[早早下班]]></description><link>https://zzao.club/post/daily/no-money-work-is-not-work</link><guid isPermaLink="true">https://zzao.club/post/daily/no-money-work-is-not-work</guid><pubDate>Tue, 02 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;“我知道你们都很辛苦，但是项目交付在即，大家为了公司再加把劲！！！”&lt;/p&gt;
&lt;h2&gt;1&lt;/h2&gt;
&lt;p&gt;相信各行各业都有加班这种现象&lt;/p&gt;
&lt;p&gt;就拿我所在的互联网行业为例吧，我工作已经有7年左右，加班、出差、项目通宵上线这些都不知道经历过多少次了&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;img src=&quot;https://img.zzao.club/article/202411191421705.png&quot; alt=&quot;&quot;&gt;&lt;br&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;img src=&quot;https://img.zzao.club/article/202411191421708.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不是，领导，我那3k的月薪怎么说？？&lt;/p&gt;
&lt;p&gt;你给我3w试试来，我直接住在公司，每天鞭策自己。这还没完，我还要鞭打同事，让他们也努力，也感受感受你的福报&lt;/p&gt;
&lt;p&gt;领导在吗，怎么已读不回&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&gt;
&lt;p&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;&lt;del&gt;因为你的领导喜欢你加班的样子&lt;/del&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;img src=&quot;https://img.zzao.club/article/202411191421709.png&quot; alt=&quot;&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;&lt;strong&gt;还有的，是单纯的不想让你比领导走的早&lt;/strong&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;img src=&quot;https://img.zzao.club/article/202411191421710.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我明白了，这是让咱提前体验体验体制内的生活呢😅&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&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;都行，你TM别什么都不给啊&lt;/p&gt;
&lt;p&gt;白天顶着身体压力，坐一整天，腰酸背痛，没空提肛&lt;/p&gt;
&lt;p&gt;晚上还要上点精神压力，血压反复过山车，一边心里痛骂，一边自我开导&lt;/p&gt;
&lt;p&gt;让本就不堪大用的身体雪上加霜&lt;/p&gt;
&lt;p&gt;这下好了，不用担心35岁失业了，我活到这个岁数都够呛&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191421711.png&quot; alt=&quot;&quot;&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;strong&gt;太惨了家人们，越是牛马，越被人当牛马，停下来还要被抽两下&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;4&lt;/h2&gt;
&lt;p&gt;95后身上栓的东西太多了&lt;/p&gt;
&lt;p&gt;整顿职场还得看后浪&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191421712.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;入职前我唯唯诺诺，入职后我重拳出击！&lt;/p&gt;
&lt;p&gt;平时我们看的都是《xx从入门到精通》《程序员腰椎康复指南》&lt;/p&gt;
&lt;p&gt;而人家看的是...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191421713.png&quot; alt=&quot;&quot;&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;img src=&quot;https://img.zzao.club/article/202411191421714.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&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;&lt;img src=&quot;https://img.zzao.club/article/202411191421715.png&quot; alt=&quot;&quot;&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;img src=&quot;https://img.zzao.club/article/202411191421717.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这位更是人间清醒，已经是10后了，不是00后了&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;人家00后，10后有事是真整顿&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;strong&gt;没想到自己也要成了被整顿的对象喽&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[当一个程序员看到小米汽车发布后，竟然产生了这样的连锁反应]]></title><description><![CDATA[...]]></description><link>https://zzao.club/post/daily/when-a-developer-saw-the-mi-car</link><guid isPermaLink="true">https://zzao.club/post/daily/when-a-developer-saw-the-mi-car</guid><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1&lt;/h2&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&gt;2&lt;/h2&gt;
&lt;p&gt;虽然小米汽车热度很高，但程序员嘛，在公司摸鱼还是以摸文字版的信息为主，不敢太过造次&lt;/p&gt;
&lt;p&gt;再个，程序员摸鱼的论坛，多少还是和微博这类阵营有点区别的，也不仅仅是热点新闻&lt;/p&gt;
&lt;p&gt;这不，前几天我就看到了一个关于小米汽车的3D网站，打开网站一看，好家伙， 我直呼好家伙&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191425001.png&quot; alt=&quot;&quot;&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;strong&gt;仿佛鸣人在变身九尾&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;艾伦在变身巨人&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大古在变身奥特曼&lt;/strong&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;我必须马上开始学习webGL!&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&gt;
&lt;p&gt;说搞就搞，我立马就打开了vscode，准备开始我的webGL学习之旅&lt;/p&gt;
&lt;p&gt;目前用的最多的就是Three.js这个库了，那我就从它开始搞起&lt;/p&gt;
&lt;p&gt;先找到官网，快速过一遍Api，了解一下基本概念&lt;/p&gt;
&lt;p&gt;过完了，直接开始手搓!&lt;/p&gt;
&lt;p&gt;不会也没有关系，现在有了AI辅助，在完全不懂的情况下，完成一些demo也不是问题，这里我用的是月之暗面的Kimi&lt;/p&gt;
&lt;p&gt;先给AI设定一个背景，然后表达出自己具体的需求，比如，我就是想用threejs加载一个3d模型，然后给3d模型尝试换色&lt;/p&gt;
&lt;p&gt;先根据官网，把项目搭建起来，然后根据Kimi的提示，一步步的完善代码&lt;/p&gt;
&lt;p&gt;很多像是做着色器/uv/shader这些名词，不懂也不用先深究，目的是先搞个3d模型出来让自己爽一爽&lt;/p&gt;
&lt;p&gt;看文档得知，3d模型推荐使用glsl格式的模型，于是我先去找一个3d模型下载到本地来&lt;/p&gt;
&lt;p&gt;找到了一个 &lt;a href=&quot;https://www.turbosquid.com/&quot;&gt;3d模型网站&lt;/a&gt;，然后选择免费的模型，大佬搞个小米su7，咱也别太差，弄个奔驰大G就行&lt;/p&gt;
&lt;p&gt;然后把尝试在代码里把模型加载进来&lt;/p&gt;
&lt;p&gt;当然，这期间不是这么顺利的&lt;/p&gt;
&lt;p&gt;其中很多坐标系问题，模型加载不顺利，灯光忘记加了，摄像机位置不对 这类问题就一一对照官网文档和AI的提示，也没有花费太多时间就搞出来了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191425002.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;模型加载出来之后，我先尝试给模型换一个颜色，在AI一遍一遍的提示下，始终无法做到像是真实世界汽车改色一样的效果&lt;/p&gt;
&lt;p&gt;我就怀疑是不是我的模型有问题，因为我看到3d模型的文件里，只有一个mesh，只要一改颜色，就整体全部都改了，改完的颜色就像是在ps里给他硬生生叠加了个颜色上去一样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191425003.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是我又对3d模型里的各种专业名词不太了解，只靠自己猜测，恐怕解决不了这个问题&lt;/p&gt;
&lt;p&gt;而且用AI把控大方向的编码是没问题的，太具体的需求它也解决不了，除非把所有关联的上下文全都喂给他，但是这样又不太现实&lt;/p&gt;
&lt;p&gt;于是我就找了个搞3d的老哥请教了一下，大概了解到了，如果要精细的实现控制只在某些部位比如车身上变换颜色或者痛车，模型就得分为多个mesh才行&lt;/p&gt;
&lt;p&gt;而要实现精细的特效，多半要自己手动实现shader，只用threejs可以实现一些的效果，但达不到自定义的高度&lt;/p&gt;
&lt;p&gt;好吧，看来是要继续深入探究了，不过这不也是开了个头嘛~&lt;/p&gt;
&lt;h2&gt;4&lt;/h2&gt;
&lt;p&gt;既然需要shader，我就去了解了一些shader的概念，以及一些shader的教程，先让自己对shader有个大体了解&lt;/p&gt;
&lt;p&gt;一顿恶补之后，好歹知道了顶点着色器以及片段着色器的概念，这时才知道Kimi给我写的材质里传入的那一堆代码是什么东西!&lt;/p&gt;
&lt;p&gt;但这类野文十分稀少，连掘金小册都只有一本&lt;/p&gt;
&lt;p&gt;为了便于让自己更好的学习shader，我就花了十来块钱，买了小册，利用上班时间和业余时间，闲里偷忙，开始跟着作者实践起来&lt;/p&gt;
&lt;p&gt;学习新东西，还是尽量找到一些别人总结好的教程，不管是付费的还是免费的，只要是总结成体系的，肯定是比刷野文要吸收的好一些&lt;/p&gt;
&lt;p&gt;大概花了两天的空闲时间，我刷完了这本小册，对于一个数学已经忘得一干二净的人来说，还是十分烧脑的&lt;/p&gt;
&lt;p&gt;但我还是硬着头皮把它看完了，倒不是说完全学会了其中的各种函数，这毕竟是有数学门槛的，但我只需要理解什么时候要用，能实现什么效果即可&lt;/p&gt;
&lt;p&gt;而且作者的实战项目里，还会教一些他在用的框架，vscode插件等等，这些东西如果靠自己搜，估计很难搜到&lt;/p&gt;
&lt;p&gt;所以说，找一份别人提炼过的教程来学习，事半功倍，能接触到不少相关的库和技巧&lt;/p&gt;
&lt;p&gt;小册最后，作者也推荐很多shader的相关学习资源，正好我还不理解的部分也想着继续深入学习一下，于是我就一一打开看了看，尝试了一下工具，收藏起来&lt;/p&gt;
&lt;p&gt;然后我就发现了Essense of Linear Algebra 这个视频，b站叫 &lt;a href=&quot;https://www.bilibili.com/video/BV1ys411472E/?vd_source=cf9b3c8faa56d0c64b09a55be382ef80&quot;&gt;线性代数的本质&lt;/a&gt;，因为shader计算的背后是线性代数这门学科，这个教程以可视化的方式很形象的阐述了线代的主要知识点(向量/矩阵等)&lt;/p&gt;
&lt;p&gt;本来我没想着从头再学一遍线代，但是我还是打开看了看&lt;/p&gt;
&lt;p&gt;这一上来还没看到内容，弹幕的夸赞就让我知道，可能是捡到宝了!&lt;/p&gt;
&lt;h2&gt;5&lt;/h2&gt;
&lt;p&gt;我的目的是理解线代和计算机图形学的关系，同时帮助我加深理解那些sdf函数是如何产生神奇的效果的，而非真的去学习&lt;code&gt;计算&lt;/code&gt;的那部分&lt;/p&gt;
&lt;p&gt;这个教程真的很符合我的需求，让计算机去做计算那部分，我又不需要考试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191425004.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191425005.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;目前这个系列我还在不断地学习中， 作者的教学及动画也正如他所说的， 让我对于这些概念有了非常直观的概念。&lt;/p&gt;
&lt;p&gt;视频真的非常优秀，感觉刚上大学，或者上大学前，非常有必要提前看一看这个视频，对学习会很有帮助&lt;/p&gt;
&lt;p&gt;结合之前在shader小册里学的东西，能够更好的理解shader中这些数字的运算是如何对应到屏幕上的颜色的，以及为什么产生了这样的效果&lt;/p&gt;
&lt;h2&gt;6&lt;/h2&gt;
&lt;p&gt;btw，整个连锁反应，没有超过三天，并且还是在工作日。&lt;/p&gt;
&lt;p&gt;因为一个炫酷的网站，真的学到了很多自己未知领域的东西，技术果然还是一如既往的好玩！想必这就是最初做技术的初心和乐趣所在吧。&lt;/p&gt;
&lt;p&gt;但是话又说回来，这三天的收获也可以说什么都没有。&lt;/p&gt;
&lt;p&gt;因为threejs只是开了个新建了个项目，shader也玩不了6，线代也考不了高分。&lt;/p&gt;
&lt;p&gt;Math.floor(技术方面)确实是毫无收获。&lt;/p&gt;
&lt;p&gt;但是我又一想，也许这种学习和挖掘的能力对我来说更重要，之所以感觉没用，是还没把它用在更多或更该用的地方。&lt;/p&gt;
&lt;p&gt;简单来讲，就是还没靠技能赚到钱，钱对人的激励永远是最大的。&lt;/p&gt;
&lt;p&gt;那就继续探索和努力吧。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[做程序员 7 年了，你改变了多少]]></title><description><![CDATA[从入门到入土]]></description><link>https://zzao.club/post/daily/developer-7-change-something</link><guid isPermaLink="true">https://zzao.club/post/daily/developer-7-change-something</guid><pubDate>Sat, 23 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191433571.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;研究表明&lt;/strong&gt;：如果感到文章不符合自己的认知、价值观，或许是阅读姿势导致的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;专家建议&lt;/strong&gt;：将手机朝南倒立后，撕掉防窥膜，在其上方57°方向俯视阅读，方能感到舒适。 不方便将手机倒立的，也可以自己倒立。&lt;/p&gt;
&lt;h2&gt;1&lt;/h2&gt;
&lt;p&gt;作为一个程序员，工作之余，难免会产生要做一个项目的想法。&lt;/p&gt;
&lt;p&gt;因为技术本身上限极高，我们时常会看到某某同学因为技术优秀，进了某某大厂。某某大佬又出了一个什么牛逼的开源框架，短短时间已经上千或上万star。另一位大佬独立开发，一人企业，已经实现财务自由。&lt;/p&gt;
&lt;p&gt;还在苦逼搬砖的我们，也会有搞个大的出来的想法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;虽然大部分时候都是拉了坨大的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/1-img-20241119141102.png&quot; alt=&quot;0.37&quot;&gt;&lt;/p&gt;
&lt;p&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;领导只关心能否按时交付项目，拿到回款？&lt;/p&gt;
&lt;p&gt;再回想一下，你在面试的时候有没有在介绍项目的时候告诉面试官说，这是你自己独立开发的项目，实现了什么什么功能。而面试官只关心这是不是在公司正式环境中使用的，有多少盈利，你说不是，没有，是自己实现着玩的。然后就没有后续了&lt;/p&gt;
&lt;p&gt;所以，这个项目是不是自己一行一行敲出来的，或许没那么重要。&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&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;strong&gt;需要自己有点东西&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191433572.png&quot; alt=&quot;0.40&quot;&gt;&lt;/p&gt;
&lt;p&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;当然，绝对不能排除这个世界比你癫狂后还要魔幻的许多。&lt;/p&gt;
&lt;p&gt;也会有你要空手套老板，而老板or领导也不关心你是什么状态，只要能把业务兜住就皆大欢喜的机会。&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;strong&gt;只要你今天能从自己嘴里讲出来，那别人就不关心你是几年就会学会的，还是刚刚学会的&lt;/strong&gt;。甚至还会自己脑补你肯定已经厉害很久了。&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&gt;
&lt;p&gt;换个位置来想，如果我是领导或老板，我当然希望自己是个善解人意、有领导力、有情商、有技术的好领导好老板。但这恐怕都是建立在公司能平稳运行，收入不愁的情况下。如果说是艰难时期，我的基本任务恐怕是要保证自己有收入，保证底下一群人有饭吃。&lt;/p&gt;
&lt;p&gt;假如说费了九牛二虎之力和客户谈到了项目，我会希望项目能顺利交付。如果说底下有个刺头，在项目攻坚期间，就是不服从管理，还私下在同事间传播消极思想，恐怕我也会把他优化掉，因为这种情况下首先就是考验自己的管理能力。再个，搞不定的话自己都没饭吃了，谁还会关心被裁的人呢？技术换谁来做都可以，但这个项目必须得有人能谈成和推进才行。&lt;/p&gt;
&lt;p&gt;再换回位置&lt;/p&gt;
&lt;p&gt;“我在工作岗位上矜矜业业，公司竟然还让我无偿加班，还有那个啥* 领导这么多功能，就给这么点时间。就这还不一定有作为一个有骨气的技术人，这能忍？ 直接到某金的发一个《点赞xx，我提桶跑路》”&lt;/p&gt;
&lt;p&gt;“别说了，每次接口都调不通，我还得跟着加班调，哎（此处包含30+程序员的一生”&lt;/p&gt;
&lt;p&gt;“别提了，这年头能发工资就不错了，你没听xx公司，裁员裁了这么多人吗”&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&gt;4&lt;/h2&gt;
&lt;p&gt;那这个项目到底还做不做啦？&lt;/p&gt;
&lt;p&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;那我觉得还不如去接单，给别人写代码，用时间和精力换钱。&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;img src=&quot;https://img.zzao.club/article/202411191421710.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;h2&gt;5&lt;/h2&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;</content:encoded></item><item><title><![CDATA[2024年了，副业可能还是要慢慢做]]></title><description><![CDATA[想急也急不得]]></description><link>https://zzao.club/post/daily/2024-find-a-side-hustle-slowly</link><guid isPermaLink="true">https://zzao.club/post/daily/2024-find-a-side-hustle-slowly</guid><pubDate>Fri, 22 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1&lt;/h2&gt;
&lt;p&gt;2024年了，这两年大家可能都不太好过&lt;/p&gt;
&lt;p&gt;因为好过的人他可能是忙于工作或赚钱，没空在各大平台矫情，也没空听别人矫情&lt;/p&gt;
&lt;p&gt;什么？你说你是上班时间没事干才刷的文章，请不要在评论区告诉我，谢谢您嘞&lt;/p&gt;
&lt;p&gt;当然，我特指的是互联网企业，其他行业我也不太清楚&lt;/p&gt;
&lt;p&gt;毕竟我妈前几天坐滴滴顺风车，随便碰到一个乘客，说自己是个销售，江苏人，月入5w，每个月天南海北的旅游，最发愁的事情是去了一个地方后要玩什么才好玩。&lt;/p&gt;
&lt;p&gt;虽然我也惊到了，但我内心毫无波澜。但我妈十分吃一套，这个事到后面少说要被她再翻来覆去说几遍（同款老妈，请举手）&lt;/p&gt;
&lt;p&gt;这两年互联网行业，铺天盖地的都是&lt;code&gt;降本增效&lt;/code&gt;，一开始这个词还没辐射到二线三线城市，或者是还没辐射到中小厂，但到了今年，应该大部分需要自己努力谈项目的公司都把这个词列为了指导思想。&lt;/p&gt;
&lt;p&gt;运气差点的兄弟，可能已经经历过了1-2次裁员，运气差点的里的运气好点的，还能拿个N+1。&lt;/p&gt;
&lt;p&gt;运气好点的，也每天被网上的各种焦虑气氛搅乱了心神，生怕有一天会一刀砍到自己头上。 所以不得不开始研究起了网赚的副业，给自己多留一条后路，顺便幻想一下靠副业大富大贵的那一天。&lt;/p&gt;
&lt;p&gt;反正我现在是看到一个什么帖子，名字叫xx副业，月入xx，很难忍住不点进去，万一能搞呢？ 万一成了呢？是幻想也是希望。&lt;/p&gt;
&lt;h2&gt;2&lt;/h2&gt;
&lt;p&gt;在搞副业的道路上，花了有几个月的时间，接触到了不少副业方向，现在体会下来，大部分其实和做技术是一个道理。&lt;/p&gt;
&lt;p&gt;作为一个入行时间有快7年的前端，只用vue，你要说问我react好不好学，好不好找工作，我也会告诉你，只要有一定的学习能力，先看看react文档，再自己多下手去敲几个项目，上手很快的，找个工作也不是问题，问题是给你开的薪资多与少。&lt;/p&gt;
&lt;p&gt;副业也是一样的，你让一个入行很多年，已经有一些成功经验的师傅，给你讲副业怎么赚钱，怎么操作，师傅直接给你出一个极为详细的sop，你看后直呼nb。然后师傅再告诉你，师父领进门，修行看个人，你要有实操，要把手弄脏，要有执行力。再对比我给你的sop文档，赚钱不是问题，问题就是你赚的钱多与少。&lt;/p&gt;
&lt;p&gt;当然更重要的是，你和你的师傅只是网友，他成没成功、是不是真的有经验你也不知道，sop出于谁手也有待商榷，有些师傅也不愿或不能公开他的账号供你参考，全靠自己自适应，靠信仰的力量，你只要相信相信的力量....&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&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;兜兜转转一圈后，还是写写文章吧，多看多写，想起什么来写点什么，记录一下这几年技术方面的积累，记录一下自己的所思所想，也顺便蹭点流量主的收益。也尝试了AI写作，写的是快，量大了也能时不时爆个10w+（约50元/1w），就是没什么营养，我自己是不会看这种文章的。并且我极少主动取关一个公众号，除非他一直发不对我胃口的文章。&lt;/p&gt;
&lt;p&gt;一开始给自己定的是一周两更，因为当时工作屁事没有，全是空闲时间。大把时间花在了搞搞新奇的项目和写文章总结上，但是后来白天工作量一上来，晚上还要看看孩子，再加上自己很懒，执行力不高，过完年后基本就断更了。现在又调整了一下，重新写起来了，扩大一下选题，克服一下心理洁癖，多存几篇存货，反正都断更了，也不差这几天了。&lt;/p&gt;
&lt;h2&gt;4&lt;/h2&gt;
&lt;p&gt;当然，如果不是开始探索副业，找到了一个圈子，并且有人愿意交流，或者你能从别人的交流里得到有用信息，也确实不好给自己一个定位。因为这个定位是在有压力的环境下，有目的的给自己确立的目标，不同于在舒适安逸环境下的无意识输出和积累。所以花点小钱，加入圈子，是我走的第一步，到目前看来，没有踩雷，也没有被割。（有需要的，可以加我V&lt;code&gt;zzdaddy577&lt;/code&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;我纯纯不合理猜测啊，你随便看看。&lt;/p&gt;
&lt;p&gt;一是项目扩张有了阻碍，无法再更进一步。&lt;/p&gt;
&lt;p&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;个体间的参差，让钱流动了起来。&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;strong&gt;当然，慢，只是出结果慢，不代表你此刻什么都不做&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;未经探索的副业，也许表面看起来都不靠谱，因为有人已经在里面赚钱了&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[30岁程序员，已经在副业的海洋里遨游了]]></title><link>https://zzao.club/post/daily/30-year-old-developer-find-out-side-hustle</link><guid isPermaLink="true">https://zzao.club/post/daily/30-year-old-developer-find-out-side-hustle</guid><pubDate>Mon, 18 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191423718.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;1&lt;/h2&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&gt;2&lt;/h2&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;打开App，推送的全是《2024年了，我劝大家多做两手准备》、《我，xx岁，xx副业，月入xk》、《大家做好3年内失业的准备》&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191423719.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;很好，里应外合，继续噶掉我这所剩无几的韭菜！&lt;/p&gt;
&lt;p&gt;让我来看看怎么回事吧，先加个群，跟着搞一下，反正才9.9&lt;/p&gt;
&lt;p&gt;好家伙&lt;/p&gt;
&lt;p&gt;100个人里有一个人跑通了副业，然后剩下的99人都在各种付费课程里徘徊。&lt;/p&gt;
&lt;p&gt;主打一个先付带动后付&lt;/p&gt;
&lt;p&gt;什么？你还没找到适合自己的副业？&lt;/p&gt;
&lt;p&gt;没关系，我这里还有一个帮你开拓认知的大师，你只需付费xxxx，就能加入他的陪伴群，还能获得xx%的分销权利。&lt;/p&gt;
&lt;p&gt;要知道，人很难赚到自己认知以外的钱，所以提升认知是很重要的。&lt;/p&gt;
&lt;p&gt;当你不知道投资什么的时候，投资自己就对了。&lt;/p&gt;
&lt;p&gt;另外，你不买没关系，几天后，价格就会升到xxxx。&lt;/p&gt;
&lt;p&gt;我：卧槽，说的太对了，早买我还捡便宜了呢&lt;/p&gt;
&lt;p&gt;赶紧掏出我15号刚发的窝囊费，狠狠的投资上自己一笔。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191423720.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;30年河东，30年河西，莫欺少年穷。&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;img src=&quot;https://img.zzao.club/article/202411191423721.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;很快啊，大师也出了一个训练营，仅需xx，因为我是尊贵的陪伴群成员，只需要9.9就能加入，还能获得100%的分销权利&lt;/p&gt;
&lt;p&gt;我去，真的良心，9.9还没到一杯奶茶的钱。&lt;/p&gt;
&lt;p&gt;搞起来。&lt;/p&gt;
&lt;h2&gt;3&lt;/h2&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;p&gt;苦口婆心，没办法，谁让我是真心经历过呢，那真是实打实的获得了好处&lt;/p&gt;
&lt;p&gt;我现在是纯粹的利他，顺便还能利己啊&lt;/p&gt;
&lt;p&gt;最后，成功拉新x人，获取一笔佣金。&lt;/p&gt;
&lt;h2&gt;4&lt;/h2&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;br&gt;
...&lt;br&gt;
...&lt;/p&gt;
&lt;p&gt;等等&lt;/p&gt;
&lt;p&gt;这个人怎么有点熟悉。。。&lt;/p&gt;
&lt;h2&gt;5&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191423722.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你已经忘了自己是要去码头整点薯条，至少还是要有在人们头上拉屎的乐趣&lt;/p&gt;
&lt;p&gt;另外，赚钱就是这样，任何方式，只要合法都不磕碜，之所以觉得难以逾越心里的障碍，是这几十年环境对人潜移默化的影响，可以类比各类丑陋习俗。&lt;/p&gt;
&lt;p&gt;如果直接给你500w，你一顿规划之后也是要回归平静、安逸、祥和的隐居生活。(注意，是真的有了500w之后的假设，而不是空想)&lt;/p&gt;
&lt;p&gt;那么恭喜你，你现在只需要把心态调整到极致，就已经在过拥有了500w之后一样的生活了。（就现在，马上去人们头上拉屎！）&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Astro建站入门指南]]></title><description><![CDATA[Astro初体验]]></description><link>https://zzao.club/post/frame/astro-quick-start</link><guid isPermaLink="true">https://zzao.club/post/frame/astro-quick-start</guid><pubDate>Mon, 05 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;为什么要自己建站呢？&lt;/strong&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;三、信念感：建了站后，相当于画了一个自己的地盘。往里面发文章，那叫充实自己。网站名称，那叫个人品牌。网站 LOGO ，那叫个人形象。网站内容，那叫个人 IP。想想就有一种无比充盈的信念感，让人忍不住就往下跟着建站了。&lt;/p&gt;
&lt;p&gt;说起信念，正好啊，快新年了，提前给大年拜个年，祝大家新年快乐👏👏👏&lt;/p&gt;
&lt;h2&gt;框架选择&lt;/h2&gt;
&lt;p&gt;建站有非常多的选择，如：WordPress、Hugo、Hexo、vuepress、jekyll、halo等等，我用过其中的部分，但今天选择的是 Astro。也不要问我为什么，想选哪个选哪个。&lt;/p&gt;
&lt;p&gt;一般建的都是博客站，用来放一些自己的文章之类的。但作为一个程序员，还是多少希望他能玩点花样，但也希望他可以开箱即用，开箱即用基本上大部分框架都能做到，但是每个框架的花样确是不同。&lt;/p&gt;
&lt;p&gt;Astro 开创的群岛架构，可以很好的满足我的需求，在不想折腾的时候，我就找个好用的模板开箱即用。想自定义的时候，就直接现代前端框架 Vue、React 、SolidJS、Svelte都整上。&lt;/p&gt;
&lt;p&gt;以下是群岛架构的官方解释：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;“群岛” 架构的总体思想看似简单：
在服务器上渲染 HTML 页面，并在高度动态的区域周围注入占位符或插槽 […] 
这些区域随后可以在客户端 “激活” 成为小型独立的小部件，
重用它们服务器渲染的初始 HTML。 
— Jason Miller, Preact 的创造者
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**在 Astro 中，“岛屿”指的是页面上的任何交互式 UI 组件，知道这一点即可。&lt;/p&gt;
&lt;p&gt;在 Astro 中，每个岛屿都是一个&lt;code&gt;.astro 文件&lt;/code&gt;，有很多模板可以使用，而且官方文档支持中文，且很详细。所以这里我直接跳过了新建项目部分，可以去&lt;a href=&quot;https://docs.astro.build/zh-cn/getting-started/&quot; title=&quot;官网&quot;&gt;官网&lt;/a&gt;👈上了解Astro&lt;/p&gt;
&lt;h2&gt;模版选择&lt;/h2&gt;
&lt;p&gt;使用 Astro 需要 Node 环境支持，这一部分也需要自己去下手准备！！！&lt;/p&gt;
&lt;p&gt;由于我们想要的是搭建起一个博客站，如果起步过于繁琐，很容易劝退，所以我们优先找一个好用的模板来快速启动，毕竟不是每个人都是开发者。&lt;/p&gt;
&lt;p&gt;官网上列举了全部的 &lt;a href=&quot;https://astro.build/themes/&quot; title=&quot;Theme&quot;&gt;Theme&lt;/a&gt; 👈，大概有一两百个。我没有全部看下来，因为如果全看下来之后再选一个好看的，大概一天的摸鱼时间都不一定够，那第二天还会不会接着搞都不一定了。&lt;/p&gt;
&lt;p&gt;于是我从免费的第一页里挑一个中意的，原则就是 ：简洁，但该有的都要有，也不能太简洁。&lt;/p&gt;
&lt;p&gt;最后我选择了&lt;a href=&quot;https://astro.build/themes/details/astrowind/&quot; title=&quot;AstroWind&quot;&gt;AstroWind&lt;/a&gt;👈 这个模板。由于语法上有一些差别，再加上全英文内容，还是要花一点时间改造一下。&lt;/p&gt;
&lt;h2&gt;模版修改&lt;/h2&gt;
&lt;h4&gt;全局配置&lt;/h4&gt;
&lt;p&gt;全局相关的配置都在 &lt;code&gt;src/config.yaml&lt;/code&gt; 里，分为了几个部分，还都挺有用的，挨个来看一下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;site：主要是用来设置你的站点名称、地址&lt;/li&gt;
&lt;li&gt;metadata：SEO相关的&lt;/li&gt;
&lt;li&gt;i18n： language需要改成zh&lt;/li&gt;
&lt;li&gt;apps：是关于 blog 也就是文章渲染相关的，不改动也可以用，后续可以自己微调。&lt;/li&gt;
&lt;li&gt;ui：主题可以切换 light or dark，主题的颜色变量也可以自己配置。直接拿来使用的话，可以不改。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;导航栏&lt;/h4&gt;
&lt;p&gt;跑起来项目肯定第一时间是点一点导航栏，看看都有什么内容。配置文件在&lt;code&gt;src/navigation.js&lt;/code&gt; 里，&lt;/p&gt;
&lt;p&gt;配置内容我已经改成了中文，这样看起来会比较好理解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/image_tx6oa4Qrms.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里的扩展下拉菜单的配置位置，会对 &lt;code&gt;src/pages/landing &lt;/code&gt;产生影响，原因是landing 里的页面是通过 &lt;code&gt;src/layouts/LandingLayout.astro&lt;/code&gt; 渲染出来的，这个页面打开后会有一个新的导航栏，下拉菜单作者默认取的索引为 &lt;code&gt;2 &lt;/code&gt;的菜单，所以需要手动改一下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/image_2thqa4jFcg.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;底部Footer&lt;/h4&gt;
&lt;p&gt;底部的修改主要集中在 &lt;code&gt;src/components/widgets/Footer.astro &lt;/code&gt;中，比如也许不需要很多 links，看起来像是企业的官网似的，就可以打开这个页面自己注释一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/image_YZjpteEe-v.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;文章内容&lt;/h4&gt;
&lt;p&gt;文章放在&lt;code&gt; src/content/post &lt;/code&gt;下，一般是 md ，也支持 mdx。&lt;/p&gt;
&lt;p&gt;md 文件中顶部会在三个横线中声明一些元信息，这些信息会在网站中用到，并渲染出来。下边是我复制的项目里的 md 文件的元信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
publishDate: 2023-07-17T00:00:00Z
author: 枣把儿
title: AstroWin123131
excerpt: While easy to get started, Astrowind is quite complex internally.  This page provides documentation on some of the more intricate parts.
image: https://images.unsplash.com/photo-1534307671554-9a6d81f4d629?ixlib=rb-4.0.3&amp;#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&amp;#x26;auto=format&amp;#x26;fit=crop&amp;#x26;w=1651&amp;#x26;q=80
category: Vue
tags:
  - astro
  - tailwind css
  - front-end
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;publishDate&lt;/code&gt;会用作排序，因为首页有一个功能，会展示最近的几篇文章，是取的这个时间。&lt;/p&gt;
&lt;p&gt;作者会显示在文章列表和文章详情页，分类和标签也同样会显示。&lt;/p&gt;
&lt;h4&gt;文章分类&lt;/h4&gt;
&lt;p&gt;在文章内使用category可以给文章分类，同时在导航栏中可以直接选择某一个分类进行展示&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;src/navigation.js&lt;/code&gt; 里可以配置导航栏，比如想配置一个页面，放全部的和 Vue 相关的文章&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/image_uFdvBw8lR3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击此下拉选项后，就可以打开一个只有 Vue 分类的文章列表页&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/image_sGqeY9QHVp.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同样的，上上图中的  &lt;code&gt;href: getPermalink(&apos;astro&apos;, &apos;tag&apos;)&lt;/code&gt; 就是打开一个 tag 为 astro 的文章列表页。&lt;/p&gt;
&lt;p&gt;有了分类和标签功能，这个网站承载文章基本就足够了。&lt;/p&gt;
&lt;h4&gt;使用图标&lt;/h4&gt;
&lt;p&gt;这个项目的&lt;a href=&quot;https://icon-sets.iconify.design/tabler/&quot; title=&quot;图标库&quot;&gt;图标库&lt;/a&gt;👈，使用的是&lt;code&gt;@iconify-json/tabler&lt;/code&gt;，是安装依赖时已经下载好的。&lt;/p&gt;
&lt;p&gt;使用时可以在 &lt;code&gt;pages/index.astro&lt;/code&gt;里找到一个 Icon的注释。 name 就从图标库里复制出来直接使用，class 支持 &lt;code&gt;tailwindcss&lt;/code&gt; 控制大小，color 控制颜色。如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;&amp;#x3C;Icon name=&quot;tabler:alert-square-filled&quot; color=&quot;blue&quot; class=&quot;text-md&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要其他图标库的，也可以自己加装。&lt;/p&gt;
&lt;h4&gt;其他&lt;/h4&gt;
&lt;p&gt;其他配置，比如右上角的按钮、底部的版权声明等，我也都在代码中用了 &lt;code&gt;TODO注释&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;大家可以打开在源码，再借助 Vscode的TODO tree插件，快速查看所有标注点，根据自己的喜好进行修改。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzstudio.cn/image_X7uMvU0Gix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;打包和部署&lt;/h2&gt;
&lt;p&gt;打包命令和其他前端项目一样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包后会生成一个 dist 文件夹，里面是打包后的静态文件。页面被编译成了单独的 html 文件，图片文件已经被转成了 &lt;code&gt;webp&lt;/code&gt; 格式。&lt;/p&gt;
&lt;p&gt;可以扔进自己任意服务器里，也可以选择其他免费的托管服务。&lt;/p&gt;
&lt;p&gt;比如Readme 里的 &lt;code&gt;Netlify&lt;/code&gt;和 &lt;code&gt;Vercel&lt;/code&gt;，或者 &lt;code&gt;Github&lt;/code&gt; 本身，这部分内容就不展开讲了。&lt;/p&gt;
&lt;h2&gt;源码地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zzdaddy/astro-onwidget-zz&quot; title=&quot;模板&quot;&gt;模板&lt;/a&gt;自取👈 源码中有部分中文标注，可以帮助你理解项目结构。&lt;/p&gt;
&lt;p&gt;另外对模板的自定义程度有限，可以理解为本土化注释了一遍，因为再继续改造也不一定所有人都喜欢了。如果你也正在使用 Astro 建站，有困难的话可以在文末联系我。&lt;/p&gt;
&lt;p&gt;以上就是全部内容啦👏&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;本文是对 Astro 建站的一个不入门指南，相当于推荐了一下Astro，以及我自己在用的模板，目的是为了方便非前端出身的朋友快速上手。&lt;/p&gt;
&lt;p&gt;如果对你有帮助的话，欢迎点赞和关注我～&lt;/p&gt;
&lt;p&gt;我是枣把儿，一个正在扩充全栈知识库「早早集市」的开发者。欢迎在公众号「早早集市」找我交流～&lt;/p&gt;</content:encoded></item><item><title><![CDATA[使用puppeteer爬取掘金个人信息]]></title><description><![CDATA[使用puppeteer爬取掘金个人信息]]></description><link>https://zzao.club/post/spider/puppeteer-jujin-user-info</link><guid isPermaLink="true">https://zzao.club/post/spider/puppeteer-jujin-user-info</guid><pubDate>Mon, 29 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;上一篇文章《使用puppeteer爬取掘金热榜》里，用了puppeteer的一些基础语法就完成了数据的爬取，这种可见即可爬的方式对于普通的使用者在感觉上来说，还是非常可靠和实用的。这次依旧是选择爬掘金的个人信息，但绕开了最麻烦的一步。&lt;/p&gt;
&lt;h2&gt;绕过登录&lt;/h2&gt;
&lt;p&gt;登录里比较麻烦就是验证码，有滑块、数字、数学计算等等多种多样的。&lt;/p&gt;
&lt;p&gt;由于爬取的目的一般来说只是不太方便打开目标网站，或者要关注的网站太多，需要聚合一下每天刷一刷，所以对爬取的速度并没有太多要求。比较重要的是拿到数据后，如何进行可视化，所以就又回到了前端界面优化的问题上了。&lt;/p&gt;
&lt;p&gt;所以我这里选择手动登录，并且之前使用的&lt;code&gt;puppeteer&lt;/code&gt;这个库，现在换成了&lt;code&gt;puppeteer-core&lt;/code&gt;，我用它来控制现有的chrome浏览器。&lt;/p&gt;
&lt;p&gt;还有一个隐形的好处。比如有一个好朋友也有类似的需求，而他不懂技术，把程序打包成二进制给他之后，他能看着浏览器一步步的操作，反而会感觉更安心一些🤔&lt;/p&gt;
&lt;p&gt;开始改造一下上次写的代码。&lt;/p&gt;
&lt;h2&gt;连接现有浏览器&lt;/h2&gt;
&lt;p&gt;因为不用默认内置的浏览器了，所以需要先打开自己的chrome浏览器，然后获取到浏览器的调试信息，再进行连接&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const browser = await puppeteer.connect({
      slowMo: 50,
      browserWSEndpoint: address,
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取address前，需要先用debug模式启动chrome，以macos为例，启动一个9222的端口号&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome  --remote-debugging-port=9222 --no-first-run --no-default-browser-check --user-data-dir=$(mktemp -d -t &apos;your_chrome_data_dir&apos;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是windows，dir路径需要是一个存在的路径&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;文件路径/chrome.exe --remote-debugging-port=9222 --user-data-dir=&quot;your_chrome_data_dir&quot;


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动成功后可以通过&lt;code&gt;GET&lt;/code&gt;  &lt;a href=&quot;http://127.0.0.1:9222/json/version&quot; title=&quot;http://127.0.0.1:9222/json/version&quot;&gt;http://127.0.0.1:9222/json/version&lt;/a&gt; 这个地址获取到&lt;code&gt;webSocketDebuggerUrl&lt;/code&gt;，也就是上边的&lt;code&gt;address&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const data = await axios
      .get(&apos;http://127.0.0.1:9222/json/version&apos;)
      .catch((err) =&gt; {
        this.logger.error(`未找到已启动的chrome浏览器1`);
      });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先获取webSocketDebuggerUrl， 如果获取成功，使用puppeteer-core连接，如果获取失败，提示出来，不再执行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;slowMo&lt;/code&gt;可以控制脚本操作chrome的时候慢一些，方便观察。&lt;/p&gt;
&lt;h2&gt;自行登录&lt;/h2&gt;
&lt;p&gt;连接成功后，自己可以使用任意方式登录。&lt;/p&gt;
&lt;p&gt;但是有一个很大的问题，启动后的&lt;strong&gt;命令行界面和浏览器，都不能关闭&lt;/strong&gt;。不然登录状态就没了，再重新打开后又得登录一遍。如果爬取的大部分网站不需要登录还好，如果都要登录的话，那还是得想办法自动登录一下。&lt;/p&gt;
&lt;p&gt;打码平台有很多，云码、超级鹰等都可以自己对接，不过大部分都是要钱的。也可以自己找找github上有没有开源项目，接入一下。或者等我找到之后，再来看我的😎&lt;/p&gt;
&lt;p&gt;最小化窗口没有影响，跑起来还是会自己打开。&lt;/p&gt;
&lt;h2&gt;开始爬取&lt;/h2&gt;
&lt;p&gt;以下内容，和爬取热榜大同小异，有需要的自取。 &lt;/p&gt;
&lt;p&gt;获取css selector的方式也是用的上篇文章的方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 等待数据展示区域展示出来
    await page.waitForSelector(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.minor-area &gt; div &gt; div.stat-block.block.shadow &gt; div.block-body &apos;,
    );
    // 等待头像加载出来, 头像出来了, 右侧信息肯定都有了
    await page.waitForSelector(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.major-area &gt; div.user-info-block.block.shadow &gt; div.avatar.jj-avatar &gt; img&apos;,
    );
    // 文章被点赞数
    const articleUpvote = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.minor-area &gt; div &gt; div.stat-block.block.shadow &gt; div.block-body &gt; div:nth-child(1) &gt; span &gt; span&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`文章被点赞数`, articleUpvote);
    // 文章被阅读数
    const articleViewNumber = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.minor-area &gt; div &gt; div.stat-block.block.shadow &gt; div.block-body &gt; div:nth-child(2) &gt; span &gt; span&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`文章被阅读数`, articleViewNumber);
    // 文章被阅读数
    const articleJueNumber = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.minor-area &gt; div &gt; div.stat-block.block.shadow &gt; div.block-body &gt; a &gt; span &gt; span&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`掘力值`, articleJueNumber);
    // 获取头像
    const avatarUrl = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.major-area &gt; div.user-info-block.block.shadow &gt; div.avatar.jj-avatar &gt; img&apos;,
      (el) =&gt; el.getAttribute(&apos;src&apos;),
    );
    console.log(`头像地址`, avatarUrl);
    // 获取用户名
    const userName = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.major-area &gt; div.user-info-block.block.shadow &gt; div.info-box.info-box &gt; div.top &gt; div.left &gt; h1 &gt; span&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`用户名`, userName);
    const position = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.major-area &gt; div.user-info-block.block.shadow &gt; div.info-box.info-box &gt; div.introduction &gt; div.left &gt; div.position &gt; span &gt; span:nth-child(1)&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`职位`, position);
    const company = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.major-area &gt; div.user-info-block.block.shadow &gt; div.info-box.info-box &gt; div.introduction &gt; div.left &gt; div.position &gt; span &gt; span:nth-child(3)&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`公司`, company);
    const intro = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.major-area &gt; div.user-info-block.block.shadow &gt; div.info-box.info-box &gt; div.introduction &gt; div.left &gt; div.intro &gt; span&apos;,
      (el) =&gt; el.textContent,
    );
    console.log(`个人简介`, intro);
    let fansNumber = await page.$eval(
      &apos;#juejin &gt; div.view-container &gt; main &gt; div &gt; div.minor-area &gt; div &gt; div.follow-block.block.shadow &gt; a:nth-child(2) &gt; div.item-count&apos;,
      (el) =&gt; el.textContent,
    );

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;信息拿到之后，重点是要干啥用。&lt;/p&gt;
&lt;h2&gt;信息处理&lt;/h2&gt;
&lt;p&gt;我提供几种思路，大家酌情考虑。&lt;/p&gt;
&lt;h4&gt;webhook通知&lt;/h4&gt;
&lt;p&gt;用nestjs里的定时任务，每天自动跑一下，拿到数据之后，用钉钉/飞书的webhook直接发出来，早上看看有无数据的变动或者消息。&lt;/p&gt;
&lt;h4&gt;数据趋势可视化&lt;/h4&gt;
&lt;p&gt;可以把每天的文章数据、粉丝数据、阅读量等记录一下，生成趋势图，看起来更直观一些。&lt;/p&gt;
&lt;p&gt;也可以看到自己关注的博主最新更新的信息，计算一下他有几天没有更新了🤔&lt;/p&gt;
&lt;p&gt;思路再打开一些，不一定是技术站点，其他任何自己感兴趣的网站都可以爬下来，做一些直观又好看的趋势图。&lt;/p&gt;
&lt;h4&gt;名片类可视化&lt;/h4&gt;
&lt;p&gt;可以把自己的数据，和平时自己看的动漫、电影、游戏等面板结合起来。&lt;/p&gt;
&lt;p&gt;比如我能想到的就是JOJO的面板，搜了一圈，目前还没啥可用的网站。&lt;/p&gt;
&lt;p&gt;由于游戏已经很久没玩了，从最开始的英雄联盟、守望先锋、PUBG后来就几乎不玩了，灵感已经枯竭。这两天帕兽大火，我才开了一下机，发现steam密码早忘了，然后账号密码找回，让我贴一张支付截图，我支付宝账单都找到17年去了🥲&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;![[1-img-20241119141155.png]]&lt;/p&gt;
&lt;p&gt;认知这个框的，估计也快30了吧😏&lt;/p&gt;
&lt;p&gt;以上就是全部内容啦，没啥新鲜东西，有时间再慢慢做吧。&lt;/p&gt;
&lt;p&gt;最近摸鱼环境不容乐观，闲里偷忙的机会越来越少啦！！&lt;/p&gt;
&lt;p&gt;我是枣把儿，欢迎关注我的公众号：早早集市，来找我玩耍🥳&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Nest从TypeORM到Prisma：迁移记录]]></title><description><![CDATA[Prisma初体验]]></description><link>https://zzao.club/post/nest/nest-from-typeorm-to-prisma</link><guid isPermaLink="true">https://zzao.club/post/nest/nest-from-typeorm-to-prisma</guid><pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近把typeORM换成了prisma，主要是想用prisma的migrate功能，于是记录了一下其中发现的问题。&lt;/p&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;全局安装prisma&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i -g prisma
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目安装@prisma/client&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm add @prisma/client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在nest根目录初始化prisma，初始化后会在根目录创建一个prisma目录以及一个&lt;code&gt;.env&lt;/code&gt;，目录里面有个&lt;code&gt;schema.prisma&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nest创建一个新的模块prisma&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nest g resource prisma
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用&lt;code&gt;@Global&lt;/code&gt;把这个模块注册成全局模块。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;service部分&lt;/code&gt;可以参考nest官网的推荐或者其他野文，基本上都一样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnApplicationBootstrap, OnApplicationShutdown
{
  constructor() {
    super();
  }
  async onApplicationBootstrap() {
    await this.$connect();
  }

  async onApplicationShutdown() {
    await this.$disconnect();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把之前typeORM相关的，find、save等等方法一一替换成prisma。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async findUserById(id: bigint) {
    return this.prisma.user.findUnique({
      where: {
        id,
      },
    });
  } 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确定没有问题之后，看一下prisma的常用命令。这部分也可以先跳过，migrate dev命令放在了正确的迁移这一小节中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;db pull &lt;/code&gt;&lt;br&gt;
基于数据库schema生成prisma schema&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prisma generate&lt;/code&gt;&lt;br&gt;
生成client代码，不会同步数据库&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prisma db push&lt;/code&gt;&lt;br&gt;
用于将本地的表结构改动同步到数据库。&lt;/p&gt;
&lt;p&gt;如果表里已经有了字段和数据，再新增字段时，务必设置为允许为null的可选字段，如&lt;code&gt;String?&lt;/code&gt;，否则会清空表结构。虽然会提示你，也不排除很多人会不看英文提示。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt; username   String?  @db.VarChar(255) ✅
 username2  String  @db.VarChar(255)  ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是删除了字段。运行后会同步到mysql数据库中。同时不会删除已有的数据，只会删除对应的字段。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-resolve&quot; title=&quot;Prisma CLI reference&quot;&gt;&lt;strong&gt;Prisma CLI reference&lt;/strong&gt;&lt;/a&gt; 👈此处有所有命令，还是建议看看官方文档。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意此操作不会像migrate一样会被记录&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;正确的迁移&lt;/h3&gt;
&lt;p&gt;迁移过程也有&lt;a href=&quot;https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project&quot; title=&quot;官方文档&quot;&gt;官方文档&lt;/a&gt;👈，这里相当于我实践后又讲述了一遍。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先设置好开头&lt;code&gt;init&lt;/code&gt;好的&lt;code&gt;schema.prisma&lt;/code&gt;，如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;datasource db {
  provider = &quot;mysql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用dotenv重新配置一下env文件，和项目现有的env文件结合起来，如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install -g dotenv-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;package.json&lt;/code&gt;配置一下常用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  &quot;migrate:dev&quot;: &quot;npx dotenv -e .dev.env -- prisma migrate dev&quot;,
    &quot;migrate:deploy&quot;: &quot;npx dotenv -e .prod.env -- prisma migrate deploy&quot;,
    &quot;prisma:generate:dev&quot;: &quot;npx dotenv -e .dev.env -- prisma generate&quot;,
    &quot;prisma:generate:prod&quot;: &quot;npx dotenv -e .prod.env -- prisma generate&quot;,
    &quot;db:push:dev&quot;: &quot;npx dotenv -e .dev.env -- prisma db push&quot;,
    &quot;db:push:prod&quot;: &quot;npx dotenv -e .prod.env -- prisma db push&quot;,
    &quot;db:pull:dev&quot;: &quot;npx dotenv -e .dev.env -- prisma db pull&quot;,
    &quot;db:pull:prod&quot;: &quot;npx dotenv -e .prod.env -- prisma db pull&quot;,
    &quot;db:seed:dev&quot;: &quot;npx dotenv -e .dev.env -- prisma db seed&quot;,
    &quot;db:seed:prod&quot;: &quot;npx dotenv -e .prod.env -- prisma db seed&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把prisma默认的env里配置的&lt;code&gt;DATABASE_URL&lt;/code&gt;挪到&lt;code&gt;.dev.env&lt;/code&gt; 和&lt;code&gt; .prod.env&lt;/code&gt;里去，在不同环境测试一下能否正确连接数据库。&lt;/p&gt;
&lt;p&gt;由于数据库里已有表结构和数据，prisma属于后来者，我不想丢失数据和结构，所以先使用db pull 生成prisma schema&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm db:pull:dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最关键的一点来了。migrate dev会记录迁移的过程，如果直接使用migrate dev的话，他会清空数据，并生成一个migration和建表的sql，这样显然不符合预期。所以先手动创建一个migration&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkdir -p prisma/migrations/0_init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用prisma migrate diff 输出一个sql脚本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script &gt; prisma/migrations/0_init/migration.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，检查一下生成sql语句确保没有任何问题，然后把这个migration标记为已应用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx prisma migrate resolve --applied 0_init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候，去修改prisma schema后再使用migrate dev，就会生成基于已有数据库后的一个变更了。这个变更就不会影响已有数据了。&lt;/p&gt;
&lt;p&gt;下边我也基于migrate dev的操作做了一些场景重现，感兴趣的可以瞅瞅。&lt;/p&gt;
&lt;h3&gt;migrate dev 操作演示&lt;/h3&gt;
&lt;p&gt;假设我不懂如何在两个环境之间迁移，只是在本地把玩prisma，直到我开始想到和线上数据库要同步一下。&lt;/p&gt;
&lt;p&gt;运行&lt;code&gt;pnpm migrate:dev&lt;/code&gt;会在prisma目录下生成一个&lt;code&gt;migrations&lt;/code&gt;目录，并生成本次操作的sql。&lt;/p&gt;
&lt;p&gt;现在本地数据库已经有了一堆表，刚刚接入了prisma，或者本地乱搞的时候已经不知道是什么状态了。我通过&lt;code&gt;init&lt;/code&gt;后用&lt;code&gt;db puill &lt;/code&gt;生成了&lt;code&gt;prisma schame&lt;/code&gt; 然后我在&lt;code&gt;prisma schema&lt;/code&gt;中删除了一个&lt;code&gt;model&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;删除表&lt;/h4&gt;
&lt;p&gt;然后运行&lt;code&gt;pnpm migrate:dev&lt;/code&gt; 并在提示下命名为&lt;code&gt;delete_table1&lt;/code&gt;。它会提示你的数据库架构与迁移历史不一致，然后继续执行。因为此时我本地的状态已经混乱，migrations记录已经不在了，所以他从头开始给我记录了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;Drift detected: Your database schema is not in sync with your migration history.
The following is a summary of the differences between the expected database schema given your migrations files, and the actual schema of the database.
It should be understood as the set of changes to get from the expected schema to the actual schema.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时可以看到目录下生成对应的sql，&lt;strong&gt;里面是一堆创建表的SQL语句&lt;/strong&gt;。再去本地数据库看的时候，数据都被清除了，并且自动执行了&lt;code&gt;seed.ts&lt;/code&gt; 插入了假数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191448363.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后再删除一张表&lt;code&gt;admin_user&lt;/code&gt;，然后再运行一下&lt;code&gt;migrate:dev&lt;/code&gt;，这次命名为&lt;code&gt;delete_table2&lt;/code&gt;，可以看到这次没有提示错误，只是单纯的删除了一张表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191448364.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;DROP TABLE `admin_user`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;增加表&lt;/h4&gt;
&lt;p&gt;这个没有什么可说的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;CREATE TABLE `color2` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `desc` VARCHAR(20) NULL,
    `desc2` VARCHAR(20) NULL,
    `desc3` VARCHAR(20) NULL,
    `value` VARCHAR(255) NOT NULL,
    `createTime` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `updateTime` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `userId` BIGINT NOT NULL,

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除字段&lt;/h4&gt;
&lt;p&gt;然后我找了一张表&lt;code&gt;color&lt;/code&gt;，给他直接加了一条数据，然后在&lt;code&gt;model&lt;/code&gt;里把其中一个字段&lt;code&gt;desc&lt;/code&gt;删掉，再去运行&lt;code&gt;pnpm migrate:dev&lt;/code&gt;，这次生成的SQL里也只是删除了一个字段，对数据没有影响。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;ALTER TABLE `color` DROP COLUMN `desc`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;增加字段&lt;/h4&gt;
&lt;p&gt;还是这张表&lt;code&gt;color&lt;/code&gt;，再加两个必填字段&lt;code&gt;desc&lt;/code&gt;、&lt;code&gt;desc2&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;desc       String   @db.VarChar(20)
desc2      String   @db.VarChar(20)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为这俩字段都是必填的，所以直接把表清空了，这次也没提示会清空表（？）。&lt;/p&gt;
&lt;p&gt;再去数据造一条数据，然后再把这俩字段改成可选，再次执行&lt;code&gt;pnpm migrate:dev&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;desc       String?   @db.VarChar(20)
desc2      String?   @db.VarChar(20)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次没有数据没有被清，只是字段被设置了可以为null&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;-- AlterTable
ALTER TABLE `color` MODIFY `desc` VARCHAR(20) NULL,
    MODIFY `desc2` VARCHAR(20) NULL;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接添加一个可选字段&lt;code&gt;desc3&lt;/code&gt;，这次只是单纯增加了一个可以为null的desc3字段&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;-- AlterTable
ALTER TABLE `color` ADD COLUMN `desc3` VARCHAR(20) NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常情况下，已经有数据的表，不可能再增加一个必填字段了，因为已有字段就都不符合条件了。我这里只是演示一下，对于一个没接触过后端和数据库的前端来说，这个可能也是需要考虑的......&lt;/p&gt;
&lt;h4&gt;同步到其他环境的数据库&lt;/h4&gt;
&lt;p&gt;如果本地有docker容器的端口会冲突的话，先stop一下，然后把nest项目本地用docker-compose跑起来。&lt;/p&gt;
&lt;p&gt;如何使用docker-compose 部署nest项目可以看下我这篇文章，里面有详细解释 👉&lt;a href=&quot;https://juejin.cn/post/7316202589603807259&quot; title=&quot;使用 Docker Compose 部署 Nest 应用&quot;&gt;使用 Docker Compose 部署 Nest 应用&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;docker-compose up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包完成后，来到nest服务内部，这里我用的Docker Desktop 演示。此时，正式数据库还是没有&lt;code&gt;color2&lt;/code&gt;这个表，color表里也没有&lt;code&gt;desc2&lt;/code&gt; 和&lt;code&gt;desc&lt;/code&gt;的。然后运行一下，为了保证他运行不报错，我先把20240117080859_delete_table1删了，因为这个sql里全是建表的语句。后面会重新来一下这个过程来弥补这个错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;/app # pnpm migrate:deploy

&gt; master@0.0.1 migrate:deploy /app
&gt; npx dotenv -e .prod.env -- prisma migrate deploy

Prisma schema loaded from prisma/schema.prisma
Datasource &quot;db&quot;: MySQL database &quot;zzstudio&quot; at &quot;zz_mysql:3306&quot;

6 migrations found in prisma/migrations

Applying migration `20240117080859_delete_table2`
Applying migration `20240117082030_delete_field1`
Applying migration `20240117082351_add_desc2`
Applying migration `20240117082752_optional1`
Applying migration `20240117083056_add_desc3`
Applying migration `20240117083359_addtablecolor2`

The following migrations have been applied:

migrations/
  └─ 20240117080859_delete_table2/
    └─ migration.sql
  └─ 20240117082030_delete_field1/
    └─ migration.sql
  └─ 20240117082351_add_desc2/
    └─ migration.sql
  └─ 20240117082752_optional1/
    └─ migration.sql
  └─ 20240117083056_add_desc3/
    └─ migration.sql
  └─ 20240117083359_addtablecolor2/
    └─ migration.sql
      
All migrations have been successfully applied.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然我在本地开发时，先增加了两个必填字段desc、desc2，导致了当时数据被删除，后面又增加了desc、desc2、desc3三个可选字段，在正式环境的数据库中，数据并没有丢失，这三个可选字段也被加上了。为了验证修改也有数据的字段会不会产生影响，我把正式库和本地库的desc、desc2、desc3三个可选字段都造上数据，然后再去代码里把desc3改为desc4。&lt;/p&gt;
&lt;p&gt;SQL如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;ALTER TABLE `color` DROP COLUMN `desc3`,
    ADD COLUMN `desc4` VARCHAR(20) NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再模拟一下部署上线，再次运行deploy。&lt;/p&gt;
&lt;p&gt;然后在正式环境运行deploy没有问题。但在本地执行上边那一步时，会检查我的migration记录，导致我跑不起来，所以我在不知道如何操作的情况下，只能又从垃圾桶里把删除的文件还原。&lt;/p&gt;
&lt;p&gt;然后把全部migrations部署到正式上去运行deploy时，也会提示我&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;A migration failed to apply. New migrations cannot be applied before the error is recovered from. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve

Migration name: 20240117075835_delete_table1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据提示的地址，然后运行了，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;/app # npx dotenv -e .prod.env -- prisma migrate resolve --rolled-back &quot;20240117075835_delete_table1&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提示如下。把20240117075835_delete_table1这条记录回滚了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;Migration 20240117075835_delete_table1 marked as rolled back.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次deploy时，因为20240117075835_delete_table1的sql里和正式数据库有冲突（重复创建了表），所以还是报错了。然后我就把20240117075835_delete_table1删了再重新部署上去deplpy，此时提示， 这个migrate在之前的一个时刻失败了。因为prisma有一个自己表，会记录整个迁移的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;The `20240117075835_delete_table1` migration started at 2024-01-17 09:13:27.542 UTC failed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是我又重新运行了一下resolve&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;/app # npx dotenv -e .prod.env -- prisma migrate resolve --rolled-back &quot;20240117075835_delete_table1&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再次deploy，可以了。刷新了一下表里数据，desc3被删了，desc4也加上了。&lt;/p&gt;
&lt;h4&gt;开始修复&lt;/h4&gt;
&lt;p&gt;但是由于一顿操作，搞的两边的prisma migrations不太同步了，具体哪里不一样我也忘了。而我只想跳过第一步会给我清空表数据再重新建表的migration。&lt;/p&gt;
&lt;p&gt;于是我把本地&lt;code&gt;reset&lt;/code&gt;一下，把&lt;code&gt;migrations&lt;/code&gt;全删掉，也不管他是啥状态了，先按照正确的迁移的做法建好一个&lt;code&gt;0_init&lt;/code&gt;，标记为&lt;code&gt;applied&lt;/code&gt;，然后继续修改一些model字段，然后再使用&lt;code&gt;migrate dev&lt;/code&gt;生成了一条migration记录。重新“部署上线”&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;docker-compose up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在docker内部尝试一下deploy&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;/app # pnpm migrate:deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;A migration failed to apply. New migrations cannot be applied before the error is recovered from. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve

Migration name: 0_init

Database error code: 1050
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个错已经在本地看了无数遍了。好解决！把它标记为已应用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;/app # npx dotenv -e .prod.env -- prisma migrate resolve --applied 0_init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功后再次使用deploy。可以看到没问题了，只执行了0_init之后的两个migration记录。&lt;/p&gt;
&lt;h3&gt;附录&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wolai.com/dZFPf36pn52aSaFT1TcvDr#qkBYymtEjsbxhg95MxLmn&quot; title=&quot;Troubleshooting&quot;&gt;Troubleshooting&lt;/a&gt; 包含了各种migration的操作&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-resolve&quot; title=&quot;Prisma CLI reference&quot;&gt;&lt;strong&gt;Prisma CLI reference&lt;/strong&gt;&lt;/a&gt;  包含所有prisma cli 命令&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project&quot; title=&quot;Add to existing project&quot;&gt;Add to existing project&lt;/a&gt; 将prisma添加到现有项目中&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;以上就是从typeorm到prisma的变更过程及遇到的问题。&lt;/p&gt;
&lt;p&gt;如果对你有帮助的话，希望可以点个关注支持一下 (๑•̀ㅂ•́)و✧&lt;/p&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;p&gt;Nest搭建： &lt;a href=&quot;https://juejin.cn/post/7313418992310616101&quot; title=&quot;一个产品要有一个“好底子”：Nest项目搭建&quot;&gt;一个产品要有一个“好底子”：Nest项目搭建&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Nest部署：&lt;a href=&quot;https://juejin.cn/post/7316202589603807259&quot; title=&quot;使用 Docker Compose 部署 Nest 应用&quot;&gt;使用 Docker Compose 部署 Nest 应用&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[当一个程序员开始做了一款产品]]></title><description><![CDATA[生活里的方方面面是一个大桶里的小桶，某个方面做的够好（加水），大桶里的水位也会跟着上涨]]></description><link>https://zzao.club/post/daily/when-a-developer-start-a-project</link><guid isPermaLink="true">https://zzao.club/post/daily/when-a-developer-start-a-project</guid><pubDate>Thu, 28 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;从23.12.11开始下手去做产品和写文章，到现在不到一个月的时间，现在到了第一篇文章立下的flag -- 总结篇了👏。&lt;/p&gt;
&lt;h2&gt;方向&lt;/h2&gt;
&lt;p&gt;从写第一篇文章开始，我给自己立的目标是周更两篇。现在是第四周，除了每周更的时间不太固定，还算在正常更新。因为写的不是水文，不是流水账，也不是广告文，所以平时需要大量的时间去推进自己的产品，去推进自己的学习进度，以便能输出不那么水的文章。倒不是说要教会谁学前端，去炫技，更多的是还是记录真实的自己。因为24年的我在技术方面的追求上和17年刚开始工作的我没有什么大的区别，依然热爱和相信技术。&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;/ol&gt;
&lt;p&gt;第一点算是被时代的发展和自身的发展同时推动的，不管以后要发展视频、文字、技术，都离不开写作能力。自己能会做，和能讲出来是两回事，和讲和能写又是两回事，所以写作能力还是比较重要的。而且在写技术文章的时候需要查阅大量文档来确保准确性，对技术也是一种反哺。&lt;/p&gt;
&lt;p&gt;第二点是我把技术当做终生爱好来看。所以我把技术和工作分开看待，生活需要钱，现在只有工作能满足搞钱的量和稳定性，而且大概率以后也是要靠工作，所以只要是在工作时间内，什么改需求这那的，我都毫无感觉。而且我时不时会想用什么工具链能提升一下工作的效率，挤出一些时间来，如果公司明显支持不起来，那就算了。所以有技术上的提升带来的一些自信，我对换工作没有太多的焦虑，实在不行就降降薪嘛~  作为一个爱好，要么是真的是好玩爱玩，要么就是要能持续的提供正反馈。技术明显是属于后者，所以就慢慢有了整理的念头。&lt;/p&gt;
&lt;h2&gt;下场&lt;/h2&gt;
&lt;p&gt;于是23年7月、8月左右，开始思考如何把脑子中的想法构思一个个完整的产品，然后又演变成了如何把这一个个的产品串起来。8月底左右，有了一个大体的结构，我把生活抽象成了一个“集市”，单个产品抽象成了“摊位”，因为所有产品都是从我的生活或者我周围人的生活中得到的灵感，所有他们在一定程度上可以打通。我把产品大体分成了两类，一类用于“陈列”，一类用于“服务”，类似工厂和门店的关系。&lt;/p&gt;
&lt;p&gt;思路有了，开始下手做起来，开了好头之后，开始思考怎么用文章输出。这中间也加了不少大佬、群、星球，开始观察别人怎么操作，因为我是带着目标来学习，所以这个过程也确确实实学到了一些东西，也花钱买了一些小册帮助自己入门，然后开始在公众号、掘金这种平台去发一些文章试试水。&lt;/p&gt;
&lt;p&gt;于是就有了第一篇文章☝️： &lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;当一个程序员突然想做一款产品&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;当然发第一篇文章之前我也在纠结以什么口吻，用什么类型的文字去写，然后就按两种风格写出来给老婆看了看，pass掉了那个读起来像是高高在上教育别人的那一篇。还没有任何经验的情况下这种议论文很难准确表达出来，还是得从叙事的角度开始写起。&lt;/p&gt;
&lt;p&gt;第一篇的文章的正反馈很足，我在没有任何推广的情况下，发在了公众号和掘金，第一周内公众号的阅读量达到了&lt;code&gt;1w&lt;/code&gt;，掘金大概&lt;code&gt;2k~3k&lt;/code&gt;，导致我第一周过于兴奋，半夜要是突然醒来，就彻底睡不着了，脑子在疯狂的转，时不时看手机，阅读数有没有涨，有没有新的朋友关注🥲。不过到了第二周随着热度下降，也逐渐回归理智，不再过度关注这些数据，而是思考如何分配可支配时间。&lt;/p&gt;
&lt;h2&gt;时间&lt;/h2&gt;
&lt;p&gt;在写作上分配太多时间的话，产品和学习进度就会落下，会导致写不出有用的东西。倒也不是非要写出多么有用的东西，而且写的时候感觉状态不好，不顺畅，一旦有这种心理，我大概率会选择不写。&lt;/p&gt;
&lt;p&gt;所以我考虑将50%的时间用于开发产品，15%的时间的用于输出文章，35%的时间用于学习。&lt;/p&gt;
&lt;p&gt;至于一个人一天有多少可支配时间，很难说。要看工作忙不忙，家里事多不多。8月初我刚升级当了爸爸，现在孩子还没满6个月，家里的时间还需要不断磨合出来。家庭是要比其他的方面更需要时间打磨的，需要用心经营。所以在公司一般我都是选择“提效”的方式来提升自己的时薪，由于我的时薪增加，身价在涨，公司竟然也不肯为我多付钱，所以我把多出来的时间当成自己的可支配时间，很合理😎。&lt;/p&gt;
&lt;p&gt;写作的风格或者能力也是做产品一样，光在脑子里想，很美好，下场去做，全是问题。能力就是在解决问题中提升的，尝试新的领域，解决新的问题，也会让我很兴奋。&lt;/p&gt;
&lt;h2&gt;图腾&lt;/h2&gt;
&lt;p&gt;头像和游戏ID、网名一样，是一个图腾，是一个代号。需要给人留下一点印象，这意味着不能太复杂，也不能太肤浅。&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;ul&gt;
&lt;li&gt;前端搭建： &lt;a href=&quot;https://mp.weixin.qq.com/s/_XUUL1HR60Zfu3xjKFeZ_w&quot;&gt;Vue3项目实战：像素风LOGO编辑器 Pixeled Pic Pro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;前端迭代（一）： &lt;a href=&quot;https://mp.weixin.qq.com/s/n8E_clCeQl8Zu2nLg0lHbQ&quot;&gt;iKun集合！Pixeled Pic Pro 前端迭代篇（一）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;后端搭建： &lt;a href=&quot;https://mp.weixin.qq.com/s/GnYy_Ym3Z7jlVRfsjGLMUA&quot;&gt;一个产品要有一个“好底子”：Nest项目搭建&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然这些文章明显是进入到细分的领域了，不那么宽泛，不能引起共鸣了，所以数据上只有第一篇文章的&lt;code&gt;1%~2%&lt;/code&gt;左右，但不影响我的两个核心目标，数据只是附带的。&lt;/p&gt;
&lt;p&gt;最开始这个产品我只想做生成头像、LOGO，但是随着我自己的使用，我发现它还可以大有可为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以当成自己的“物料站”，用来生产ICON、头像，而不用担心版权问题。&lt;/li&gt;
&lt;li&gt;在玩了一天sketch之后，我发现有个叫颜色变量的东西，可以用在我的产品上，这样就会产生类似“掘金蓝”、“微信绿”的配色方案，再配合预设功能，可以衍生出一个负责陈列配色方案或者作品的“摊位”。两个摊位就可以产生联动。&lt;/li&gt;
&lt;li&gt;因为底层基于&lt;a href=&quot;https://www.leaferjs.com/ui/&quot;&gt;Leafer&lt;/a&gt; 这个UI框架，我还可以抽离出来，做一个低端低代码平台，通过简单的拖拽，生成js、html、css，完成&lt;code&gt;那种大国企客户&lt;/code&gt;需要做的&lt;code&gt;那种大屏系统&lt;/code&gt;，你懂得，重复度又高，代码又原始。&lt;/li&gt;
&lt;li&gt;也可以基于类似低代码平台的思路，做一个可视化的cli基建工具，配合一个模版仓库，实现快速启动另一个“摊位”&lt;/li&gt;
&lt;li&gt;....&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看出，产品的路线不是线性的，不是从&lt;code&gt;开头&lt;/code&gt;开始的，而是不知道从哪里开始，又像一个网状一样，连带出很多优化点和其他产品。&lt;/p&gt;
&lt;h2&gt;自驱&lt;/h2&gt;
&lt;p&gt;把产品的蓝图画好之后，下一步就是如何一步步去实现了，因为是为爱发电，所以本身没有要求有多少回报。但是如果因为爱好正在做的事，恰好又能提供收益，那这就是最强的驱动力，但是太过依赖这种驱动力，一旦收益下降或者停止，这个心理上的落差也要提前考虑一下，所以我把预期放在最低，仅以学习为目的推动自己继续前进，定期复盘一下，如果什么都没学到，就调整节奏。&lt;/p&gt;
&lt;p&gt;当然有一部分原因是我自己不是那种喜欢一直闲着的人，并且对技术有很强的好奇心。&lt;/p&gt;
&lt;p&gt;以前也会时不时有，在公司就很焦虑，觉得公司不合适自己，不清楚要做什么的感觉。但放在7年这个时间维度上，感觉自己还是在分阶段的突破，虽然也会三天打鱼两天晒网，但是我三天打一次鱼，一直不停地打，也会比一直晒网的强了。只要是在公司没碰到的业务，我拿到自己的产品上来去实践和探索，感觉非常有意思，这算是我的源动力了。&lt;/p&gt;
&lt;p&gt;大环境变差也会有推动作用，当然对有些人也会是反向推动。我不觉得躺平是不正常的事，我觉得不正常的是，你明明在躺平，但是你不认可自己的躺平行为，很拧巴。一边躺，一边又觉得别人学也对，也想使劲。为什么我会有这种体会？因为我也是个拧巴的人，我家里也天生带着拧巴的属性，所以我之前也在很长时间里陷入的思维的怪圈里。有时候可能不是想的太少了，而是想的太多，做的太少了，在实践中得到的“真知”太少，无法得到有力的回应。&lt;strong&gt;所以，停止思考，马上去做。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在哪个方面开始去做？我觉得都可以，虽然生活被分成了工作、学习、家庭、夫妻、子女、亲情等等很多方面，但是我发现你在任何一个方向用积极的心态去思考去做，都会带动其他方面变好。本来我以为生活像个木桶，要么提升长板（✅），要么弥补短板（❌），但是我现在觉得生活倒有点像是桶中桶，生活里的方方面面是一个大桶里的小桶，某个方面做的够好（加水），大桶里的水位也会跟着上涨。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;玄乎，太玄乎了&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以，自驱，是一种心态，会影响你的方方面面。你要做的产品也许不能带来什么收入，但也是你心境的一种折射。&lt;/p&gt;
&lt;h2&gt;未来&lt;/h2&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;ol&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;/ol&gt;
&lt;p&gt;收入方面，如果有了新的突破，也会写出来和大家分享一下！&lt;/p&gt;
&lt;p&gt;ok，全文结束。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;从23.12.11写下第一篇文章，到今天的总结篇，算是完成了一个小闭环，成就感满满🥳，度过了新手期。&lt;/p&gt;
&lt;p&gt;接下来就继续按照自己的节奏输出。&lt;/p&gt;
&lt;p&gt;第一个小产品：像素厂Pixeled Pic Pro，也已经更新在了&lt;a href=&quot;https://zzstudio.cn/stall/pixeled-pic-pro/#/home&quot;&gt;在线体验&lt;/a&gt; ，现在纯前端已经开始限制功能的发展了，不日就会加入一些后端功能！&lt;/p&gt;
&lt;p&gt;有任何问题或者兴趣也可以在公众号：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;早早集市&lt;/a&gt; 找到我。&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[产品进度：像素风编辑器从Konva到Leafer]]></title><description><![CDATA[leafer初体验]]></description><link>https://zzao.club/post/pixel/zzao-club-konva-leafer</link><guid isPermaLink="true">https://zzao.club/post/pixel/zzao-club-konva-leafer</guid><pubDate>Thu, 28 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;哈喽，大家好，我是枣把儿。&lt;/p&gt;
&lt;p&gt;前两周我开发了一个像素风图片编辑器，正当我在逐渐完善功能的时候，我老婆过来看了看我做的功能。于是我在12x12的画板上给她画了两下，然后她说，你弄个120x120的给我看看，再写个字。结果我在生成14400个矩形的时候浏览器就卡死了😅，数值调小一些后，勉强可以生成出来，但是拖动和缩放都很卡，我尝试画了两下，断触也很严重，基本是不能用的状态。果然，一个真实的用户随便提的要求，都和自己构思的千差万别。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191435994.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;无奈之下，我又重新调研了一些框架，把核心功能从Konva换成了Leafer，Leafer是一个23年6月才发布1.0.0-beta的新框架，但是我使用起来感受还是比较丝滑的，他们自带的图形编辑器样式比起Konva也好不少（很多）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191435995.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;整体的设计也可以用分层的方式，分为ground（背景层）、tree（内容层）、sky（变化层） 三层，当然也可以再自己添加自定义的层。&lt;/p&gt;
&lt;p&gt;对SVG、动画等也支持，可以说再复刻一遍我的这个工具是没啥问题的（开源协议是MIT），而且还有node端可以使用。&lt;/p&gt;
&lt;p&gt;然后就用Leafer把原来的功能都复刻了一遍&lt;/p&gt;
&lt;p&gt;【此处的图丢了】&lt;/p&gt;
&lt;p&gt;后面再加一下动态减少行和列，自己编辑的功能基本就差不多了。 重构之余，我也有了如何自动生成像素图的一些新思路。&lt;/p&gt;
&lt;p&gt;像素格子为&lt;code&gt;10px&lt;/code&gt;的话，我导入一个图片为&lt;code&gt;200x200&lt;/code&gt;，那我只需要把我的像素画板画成20x20，然后去图片里取色即可。取色也比较简单，依靠Canvas可以直接拿到&lt;code&gt;rgba&lt;/code&gt;的色值，而且不用担心png的透明背景，或取了超出图片的颜色。&lt;/p&gt;
&lt;p&gt;我先按一个&lt;code&gt;10x10&lt;/code&gt;的区域只取一个点，也就是坐标&lt;code&gt;(5,5)&lt;/code&gt;所在的像素点，把这个色值填充到像素画板对应的方格上。&lt;/p&gt;
&lt;p&gt;按照这个思路，我实现了一下功能，再稍作调整后，我把自己的头像复刻了出来。&lt;/p&gt;
&lt;p&gt;左侧是我导入的图片(&lt;code&gt;297x297&lt;/code&gt;)， 右侧是生成的像素画布(&lt;code&gt;30x30&lt;/code&gt;)，每个方格是&lt;code&gt;10x10&lt;/code&gt;，因为它本身就是像素图，所以正好可以检验我这个思路有没有问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191435996.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;还可以调小参数（&lt;code&gt;5x5&lt;/code&gt;）后继续调试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191435997.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，&lt;code&gt;秋衣部分&lt;/code&gt;的比例更贴近原图了，但因为我取样次数（只取一次）太少，某些特征部分正好没卡在中间位置，就会有些怪异...&lt;/p&gt;
&lt;p&gt;而且，5x5的时候是 3600个矩形块，在手动涂抹像素画布的时候，Leafer依然不卡。&lt;/p&gt;
&lt;p&gt;转换操作用时&lt;code&gt;88ms&lt;/code&gt;， 这个我做了一点优化，本来是先把所有色值计算出来，然后去Leafer里用&lt;code&gt;findOne&lt;/code&gt; 去找到对应的矩形块， 然后填色。我改成了先把色值数组改成&lt;code&gt;Map&lt;/code&gt;，然后&lt;code&gt;findOne&lt;/code&gt;方法替换为&lt;code&gt;children&lt;/code&gt;属性，用属性直接拿到所有子元素，避免循环查找，然后给每个子元素&lt;code&gt;fill&lt;/code&gt;属性赋值成算出来的色值即可。&lt;/p&gt;
&lt;p&gt;接下来肉眼可见的问题就是调整取样次数了，从一个格子里取一个点，改成按某种规律取多个点，然后判断一下该用哪个，这个还需要我去多多调试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191435998.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;还有个问题就是图片风格化。 我直接拿我的生活照去生成，看起来基本就是和打了马赛克一个原理。而像素化，我理解的应该是先提取图片的特征，特征里最主要的应该就是轮廓和五官，因为一个像素小人，怎么也得有鼻子有眼的，才能看着是一个小人。而有些线条类的图片，他的黑色或其他颜色的描边，对应到像素小人上就是一圈深色的色块把自己包围起来，就类似我上边的头像图片。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191435999.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;想到这里，我对我大学完全没有学高数感到很后悔，总觉得有个算法会直接解决我的问题，但我又不知道是什么，所以我只能先粗糙的实现这个“算法”，如果有大佬恰好看到，恰好知道的话，恳请评论区指点一下我orz。&lt;/p&gt;
&lt;p&gt;想到上边，我脑子突然脑补到了我爸妈看到上边这句话，会对我说：&quot;你看我就说xxxx吧，当初不好好学，现在后悔了，你要是xxxx，你现在xxxx&quot;。&lt;/p&gt;
&lt;p&gt;我觉得我能产生这个想法，实在是太悲哀了，像巴浦洛夫的狗一样。我无法和自己的原生家庭倾诉，因为他们总是不去思考，只是习惯性的否定和消极。&lt;/p&gt;
&lt;p&gt;同时，我在30岁的时候还能焕发起要远远大于22岁时候的技术热情，另一半的治愈效果可想而知。所以不要排斥谈恋爱、结婚，如果合适，这对你真的很有用，也许你排斥的只是双方原生家庭可能会隐隐带来的问题。&lt;/p&gt;
&lt;p&gt;当然我现在说这些话相当的心平气和，因为我自己认知也在不断地突破，也有了自己的家庭，没有必要故步自封。让自己不断努力，也是争取让自己的孩子拥有更高的眼界，看事情看的更清晰。&lt;/p&gt;
&lt;p&gt;ok，回到正题。 接下来准备完善的两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;增加取样频率，做成可配置的，观察一下最佳数量&lt;/li&gt;
&lt;li&gt;看看自己能不能先风格化图片。
&lt;ol&gt;
&lt;li&gt;思路就是，有多个色值都是属于红色，那就当成一种颜色处理。这样应该可以把鼻孔、眼睛区分出来&lt;/li&gt;
&lt;li&gt;深色做一下特殊处理，有些描边画出来的图，深色的优先级大于其他颜色，比如取色五个[ 黄，这黄，那黄，黑，白] 黑色权重高一些，因为这说不定是个眼睛&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;另外，个人网站支付这块我也做了一些调研，有一些第三方平台可以接入。等后续用到的话再来和大家汇报一下。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这就是本篇文章的全部内容了，目前后端部分已经打通，也开发了一些接口，后续我准备快速把像素厂的前端迭代一下，按照正经的产品去发展！&lt;/p&gt;
&lt;p&gt;从12.11准备周更2篇到现在是第3周，感觉技术文章受众确实是少一些。以后汇报产品进度的话，我就当闲聊来写了，和一些比较干的技术内容区分开。等到这个产品正式v1.0.0版本，我再来把整个流程的感悟做一下总结。&lt;/p&gt;
&lt;p&gt;做产品还是走在写文章前面的，所以接下来我可能会调整一下节奏，把每天的可控时间再重新规划一下比重，产出效率再高一些，也会尝试用图文的形式去更新自己的进度~&lt;/p&gt;
&lt;p&gt;有任何问题也可以在公众号：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;早早集市&lt;/a&gt; 找到我。&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[使用 Docker Compose 部署 Nest 应用]]></title><link>https://zzao.club/post/nest/docker-compose-deploy-nest</link><guid isPermaLink="true">https://zzao.club/post/nest/docker-compose-deploy-nest</guid><pubDate>Sun, 24 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;最近把像素厂（&lt;a href=&quot;https://mp.weixin.qq.com/s/_XUUL1HR60Zfu3xjKFeZ_w&quot;&gt;PixeledPicPro&lt;/a&gt;）的前端又翻新了一遍，也把 Nest应用部署到了云服务器，下面分享一下使用 docker-compose 的部署流程，保证你可以按步骤完成 Nest 项目（小水管服务器的）部署工作。&lt;/p&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;本地和服务器是两个环境，所以难免会涉及配置文件问题，这里一并讲了。然后按本地模拟部署和服务器两部分演示&lt;/p&gt;
&lt;h3&gt;本地部署&lt;/h3&gt;
&lt;p&gt;首先保证你的 Nest 服务在本地 dev 环境下是正常可用的。有些同学不要一上来就拿不知道哪年的项目开始练手，结果项目本身就有问题。&lt;/p&gt;
&lt;p&gt;因为本地和线上是两个不同环境，mysql、redis等很多需要区分环境的属性值都需要统一配置。Nest提供了&lt;code&gt;@nest/config&lt;/code&gt;这个包，可以用来读取环境变量文件，并注入一个 service 到所有 module 里使用。&lt;/p&gt;
&lt;p&gt;环境变量文件的类型有很多，可以是.env 文件，也可以是.yaml，也可以是.js, .ts。这里我使用的是.env，下面就以此为例。&lt;/p&gt;
&lt;p&gt;.env 文件放在根目录下，如果你是有多个微服务，这个 .env文件默认读取的也是根目录。&lt;/p&gt;
&lt;p&gt;在目录中位置如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191445665.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;在app.module.ts 中配置 ConfigModule，同样的，在这个文件中把 TypeOrmModule 的配置改成 env里的配置项&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Module({
	imports: [
		ConfigModule.forRoot({
	      isGlobal: true,
	      envFilePath:
	        process.env.NODE_ENVIRONMENT === &apos;production&apos;
	          ? path.join(process.cwd(), &apos;.prod.env&apos;)
	          : path.join(process.cwd(), &apos;.dev.env&apos;),
	    }),
		TypeOrmModule.forRootAsync({
	      imports: [ConfigModule],
	      useFactory(configService: ConfigService) {
	        return {
	          type: &apos;mysql&apos;,
	          host: configService.get(&apos;mysql_server_host&apos;),
	          port: configService.get(&apos;mysql_server_port&apos;),
	          username: configService.get(&apos;mysql_server_username&apos;),
	          password: configService.get(&apos;mysql_server_password&apos;),
	          database: configService.get(&apos;mysql_server_database&apos;),
	          # 可以同步表结构，先开着
	          synchronize: true,
	          logging: true,
	          entities: [
	            你的 entities
	          ],
	          poolSize: 10,
	          connectorPackage: &apos;mysql2&apos;,
	          extra: {
	            authPlugin: &apos;sha256_password&apos;,
	          },
	        };
	      },
	      inject: [ConfigService],
	    }),
	]
})
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;redis配置同理，也把之前写死的属性值改成 env 文件里的配置项&lt;/p&gt;
&lt;p&gt;redis.module.ts&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Module({
  controllers: [RedisController],
  providers: [
    RedisService,
    {
      provide: &apos;REDIS_CLIENT&apos;,
      async useFactory(configService: ConfigService) {
        console.log(`redis 打印`, configService.get(&apos;redis_server_port&apos;));
        const client = createClient({
          socket: {
            host: configService.get(&apos;redis_server_host&apos;),
            port: configService.get(&apos;redis_server_port&apos;),
          },
          database: configService.get(&apos;redis_server_db&apos;),
        });
        await client.connect();
        return client;
      },
      inject: [ConfigService],
    },
  ],
  imports: [],
  exports: [RedisService],
})

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更改完成后，先启动一下本地服务，看看能不能正常访问，如果可以再进行下一步！这一步卡住也没事，慢慢 Google 一下，基本都是常见问题。出现问题再解决才能变成自己的东西。&lt;/p&gt;
&lt;p&gt;本地开发的时候，我是用Docker Desktop单独跑了两个容器：mysql、redis。他们的 data 也挂载到了我本地目录上。而上线的时候我需要用 docker-compose 编排 nest、mysql、redis 他们三个，并且 nest 依赖于 mysql 和 redis，下面直接列一下我本地部署时的 docker-compose.yml。我直接把注释写在里面，免去上下滑动对比着看了&lt;/p&gt;
&lt;p&gt;有些名称、端口我故意没有起成一样的，方便形成对比&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &apos;1.0&apos;
services:
 # 我的nest服务名称
  nest-master:
	  # 指定了容器名， 这个容器名会用于容器间的通信， 比如你本地 mysql 的 host 是 localhost，那线上就用 zz_master 访问。 不声明的话就是‘nest-master’
    container_name: &apos;zz_master&apos;
    # nest项目打包时，用的是他自己目录下的 Dockerfile
    build:
      context: ./
      dockerfile: ./apps/master/Dockerfile
    # 依赖于另外两个 service，另外两个 service 会先被构建
    depends_on:
      - zz-mysql-7
      - zz-redis-7
    # 端口映射，左边是宿主机上的端口 7008，也就是你打开浏览器http://localhost:7008 可以访问到 nest 的接口，而在容器内部他是运行在 7007 上
    ports:
      - &apos;7008:7007&apos;
    # 失败时重启，有时候 mysql 没启动起来，nest 已经完事了，就会连不上 mysql，所以一直重启，知道 mysql 启动成功
    # 不过你的项目如果有bug，他就会无限重启，所以要自己注意了
    restart: on-failure
    # 声明他们在zzstudio-server 这个网络中，可以用container_name进行访问
    # 不声明的话，也会在同一个网络中，名称默认是 项目_default， 比如我这个项目叫 zz-nest, 默认的网络名称就是 zz-nest_default
    networks:
      - zzstudio-server
  # 我的 mysql 服务的名称
  zz-mysql-7:
    # 我指定的容器名，当 nest服务（也就是上边的 nest-master）要访问 mysql 时，mysql的 host 就配置为 zz_mysql
    container_name: &apos;zz_mysql&apos;
    image: mysql
    # 端口映射，当你从外部访问 3307 时会被映射到容器内部的 3306 上。
    # 因为这里我们三个服务在同一个网络下，所以我们prod.env 使用的应该是 3306
    ports:
      - &apos;3307:3306&apos;
    # 挂载的本地目录
    volumes:
      - /我的本地目录/mysql:/var/lib/mysql
      # 可以用于初始化时执行一些 sql，我查阅的野文里，有的用来解决数据库没有被创建，或用来表结构初始化，有需要的自行尝试
      # - ./init.sql/:/docker-entrypoint-initdb.d/init.sql

    # 相关的环境变量，密码应该是必须要设置的，忘了咋回事了
    environment:
      # 如果你的 mysql 是 8.x 不要指定 MYSQL_USER=root，会报错
      # 在指定了MYSQL_DATABASE后，会自动创建这个数据库！
      MYSQL_DATABASE: zzstudio 
      MYSQL_ROOT_PASSWORD: 123456
    # 同样，显式的声明在一个网络下
    networks:
      - zzstudio-server
  # 我的redis服务名称
  zz-redis-7:
    # 我指定的容器名称，当 nest 应用要连接 redis 时，redis server host 直接写这个‘zz_redis’ 即可
    container_name: &apos;zz_redis&apos;
    image: redis
    # 端口映射，道理和 mysql 一样。可以看下上边的描述
    ports:
      - &apos;6378:6379&apos;
    volumes:
      - /我的本地目录/redis:/data
    networks:
      - zzstudio-server
# 声明网络，和上边所有的 server 下边的 networks 相对应
networks:
  zzstudio-server:
    driver: bridge

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再对比看一下本地和线上的环境配置，我&lt;strong&gt;放在了一起演示&lt;/strong&gt;，可以自己体会一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis_server_host=localhost #dev
redis_server_host=zz_redis #prod 对应 docker-compose.yml 里的 redis 的 container_name

redis_server_port=6379 #dev
redis_server_port=6379 #prod  ports 配的是 6378：6379，这里用的是 6379，因为他们已经在同一个网络里

redis_server_db=1

mysql_server_host=localhost # dev
mysql_server_host=zz_mysql # prod 对应 docker-compose.yml 里的 msyql 的 container_name
mysql_server_port=3307# dev 我本地 docker单独启用 mysql 时， 使用的 ports 是 3307:3306，所以我 nest 访问 mysql是用 3307 访问，从外部访问。同理 navicat 这种软件访问也需要从 3307
mysql_server_port=3306# prod 线上ports 配的也是 3307:3306，这里用的是 3306，因为他们已经在同一个网络里
#mysql_server_username=用户名
#mysql_server_password=密码
#mysql_server_database=数据库名称
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nest 应用的 Dockerfile 如下，要注意的是我单独把 &lt;code&gt;prod.env&lt;/code&gt; 复制进去了，不然不会扔进去。你也可以先自己试试，看看报错信息，思考一下是哪里出了问题，再回过头来按我这个流程排查。这样也能加深记忆。&lt;/p&gt;
&lt;p&gt;这个 Dockfile 我也是参考了很多野文，这方面不再赘述了，能跑起来就行！&lt;/p&gt;
&lt;p&gt;&lt;code&gt;要注意：master 是我的服务的文件名，要记得换成自己的&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM node:18.18.2-alpine3.18 as build-stage

WORKDIR /app

RUN npm install -g pnpm

COPY package.json .

RUN pnpm install

COPY . .

RUN pnpm build:master 

FROM node:18.18.2-alpine3.18 as production-stage 

COPY --from=build-stage /app/dist/apps /app/apps
COPY --from=build-stage /app/.prod.env /app/.prod.env
COPY --from=build-stage /app/package.json /app/package.json

WORKDIR /app

RUN npm install -g pnpm

ENV NODE_ENVIRONMENT=production

RUN pnpm install --production

CMD [ &quot;node&quot;, &quot;./apps/master/main.js&quot; ]

EXPOSE 7007

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写好了Dockerfile、 docker-compose.yml 和本地 env，我们先把本地当线上跑一下，跑之前先看看自己本地的docker 运行情况，不要冲突了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跑完后，出现下图，即为成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191445666.png&quot; alt=&quot;1.00&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果出现了类似以下报错，可能是&lt;strong&gt;因为没建成数据库&lt;/strong&gt;，也可能是你挂载的本地目录&lt;code&gt;/你的目录/mysql&lt;/code&gt;&lt;strong&gt;不为空，导致初始化失败&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也可能会出现 178.x.x.x：端口号 访问不到的报错，一般是你配置文件里 &lt;code&gt;port&lt;/code&gt; 配错了，应该配成容器的 &lt;code&gt;network&lt;/code&gt; 的 &lt;code&gt;bridge&lt;/code&gt; 的 &lt;code&gt;Gateway&lt;/code&gt; 地址&lt;/p&gt;
&lt;p&gt;可以往上滑一下日志，看看具体报错信息，如有不知名报错，也可以在公众号：早早集市 找到我，我再尝试复现一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zz_master  | Error: getaddrinfo EAI_AGAIN zz_mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们拿浏览器测试一下 Get接口即可，能返回数据，终端里有输出日志就行啦&lt;/p&gt;
&lt;p&gt;然后开始部署到云服务器！&lt;/p&gt;
&lt;h3&gt;服务器部署&lt;/h3&gt;
&lt;p&gt;往服务器部署前还需要改一下 &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;mysql、redis的 &lt;code&gt;volumes&lt;/code&gt; 改为服务器上的地址，没有的话先新建一下&lt;/p&gt;
&lt;p&gt;然后代码传到服务器上，可以用各种方式，如 git、ftp、jenkins等。&lt;/p&gt;
&lt;p&gt;然后我们通过 ssh 连一下云服务器，然后进入到项目的根目录下， 运行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的服务器上只安装了 docker 没有 docker-compose，可以使用以下命令先安装一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo curl -L &quot;https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

docker-compose --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在经过几次 retry之后，可以看到服务成功启动!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zz_master  | [Nest] 1  - 12/24/2023, 12:32:59 PM   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (4)...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;docker-compose down&lt;/code&gt; 停止，然后使用&lt;code&gt;docker-compose up -d&lt;/code&gt;，在后台启动即可&lt;/p&gt;
&lt;p&gt;然后使用&lt;code&gt;docker ps&lt;/code&gt; ，查看已经在运行的容器～&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker-compose down&lt;/code&gt; 运行后也可以看到，启动的是三个 Container，一个 Network，Network 就是上边指定的网络&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[+] Running 4/4
 ⠿ Container zz_master             Removed                                                                                                              10.4s
 ⠿ Container zz_redis              Removed                                                                                                                 0.4s
 ⠿ Container zz_mysql              Removed                                                                                                                 1.1s
 ⠿ Network server_zzstudio-server  Removed    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ok，服务已经启动，接下来我用 Ngxin 做代理，把接口代理到 nest 服务上（容器的对外端口上）&lt;/p&gt;
&lt;p&gt;nginx 也是一个 docker 单独启动的容器，目前用来挂载一个前端应用的目录，靠&lt;code&gt;Termius&lt;/code&gt;的 &lt;code&gt;SFTP&lt;/code&gt; 部署前端，因为Jenkins 太吃内存，后面我再调研一下有没有平替的方案。运维方面也不着急优化。&lt;/p&gt;
&lt;h2&gt;Nginx 转发到 Nest&lt;/h2&gt;
&lt;p&gt;一开始我是这样配置的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; location ^~ /api/ {
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_pass http://localhost:7007/; 
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在服务器里使用 &lt;code&gt;curl&lt;/code&gt; 是可以调通Nest的接口的，但是用 &lt;code&gt;Apifox&lt;/code&gt; 调不同，会报 502，查看错误日志&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2023/12/24 08:04:24 [warn] 24#24: *4 upstream server temporarily disabled while connecting to upstream, client: xxxx, server: zzstudio.cn, request: &quot;GET /api/user/aaa HTTP/1.1&quot;, upstream: &quot;http://127.0.0.1:7007/user/aaa&quot;, host: &quot;zzstudio.cn&quot;
2023/12/24 08:04:25 [error] 24#24: *4 no live upstreams while connecting to upstream, client: xxx, server: zzstudio.cn, request: &quot;GET /api/user/aaa HTTP/1.1&quot;, upstream: &quot;http://localhost/user/aaa&quot;, host: &quot;zzstudio.cn&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把配置里的 localhost 改成自己服务器的公网 ip，发现还是不行&lt;/p&gt;
&lt;p&gt;然后又查看了一下 nginx 容器里的 ip&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 我的nginx容器名称叫nginx
docker inspect nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉到最后，看到 &lt;code&gt;Networks&lt;/code&gt;里，&lt;code&gt;bridge&lt;/code&gt; 的 &lt;code&gt;Gateway&lt;/code&gt; 地址，把上边 &lt;code&gt;location&lt;/code&gt; 里的 &lt;code&gt;proxy_pass&lt;/code&gt;  改成 &lt;code&gt;http://Gateway 地址: 端口号/&lt;/code&gt;， 再去 Apifox 尝试下自己的接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; location ^~ /api/ {
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_pass http://Gateway 地址:7007/; 
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功了👏&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;以上就是我使用 docker-compose 部署 Nest 应用的全部过程，如果对你有帮助的话，还望点个关注、点赞支持一下～&lt;/p&gt;
&lt;p&gt;也欢迎关注公众号：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;早早集市&lt;/a&gt;，第一时间围观我继续产出其他产品和文章～&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;
&lt;h2&gt;相关文章推荐&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;产品构思： &lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;当一个程序员突然想做一款产品&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前端搭建： &lt;a href=&quot;https://mp.weixin.qq.com/s/_XUUL1HR60Zfu3xjKFeZ_w&quot;&gt;Vue3项目实战：像素风LOGO编辑器 Pixeled Pic Pro&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前端迭代（一）：&lt;a href=&quot;https://mp.weixin.qq.com/s/n8E_clCeQl8Zu2nLg0lHbQ&quot;&gt;iKun集合！Pixeled Pic Pro 前端迭代篇（一）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后端搭建：&lt;a href=&quot;https://mp.weixin.qq.com/s/GnYy_Ym3Z7jlVRfsjGLMUA&quot;&gt;一个产品要有一个“好底子”：Nest项目搭建&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;后记 2023年12月26日17:59:16&lt;/h2&gt;
&lt;p&gt;nest项目通过docker-compose更新&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 先停止
docker-compose down 
# 然后后台启动的时候重新build，mysql和redis自动越过了，重新打包了nest服务
docker-compose up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo tee /etc/docker/daemon.json &amp;#x3C;&amp;#x3C;-EOF { &quot;registry-mirrors&quot;: [ &quot;https://1js6gccw.mirror.aliyuncs.com&quot;, &quot;https://dockerproxy.com&quot;, &quot;https://mirror.baidubce.com&quot;, &quot;https://docker.m.daocloud.io&quot;, &quot;https://docker.nju.edu.cn&quot;, &quot;https://docker.mirrors.sjtug.sjtu.edu.cn&quot; ] } EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/docker 
sudo tee /etc/docker/daemon.json &amp;#x3C;&amp;#x3C;-EOF 
{ &quot;registry-mirrors&quot;: [ 
	&quot;https://1js6gccw.mirror.aliyuncs.com&quot;,
	&quot;https://dockerproxy.com&quot;, 
	&quot;https://mirror.baidubce.com&quot;, 
	&quot;https://docker.m.daocloud.io&quot;, 
	&quot;https://docker.nju.edu.cn&quot;,
	&quot;https://docker.mirrors.sjtug.sjtu.edu.cn&quot; 
	] 
} 
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
--init \
--name memoz \
--publish 5230:5230 \
--volume /home/memoz/:/var/opt/memos \
neosmemo/memos:0.22.3
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[iKun集合！Pixeled Pic Pro 前端迭代篇（一）]]></title><description><![CDATA[像素厂 Pixed Pic Pro 迭代记录]]></description><link>https://zzao.club/post/pixel/ikun-pixeled-pic-pro</link><guid isPermaLink="true">https://zzao.club/post/pixel/ikun-pixeled-pic-pro</guid><pubDate>Thu, 14 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;上篇《&lt;a href=&quot;https://juejin.cn/post/7311697095505149990&quot;&gt;Vue3项目实战：像素风LOGO编辑器 Pixeled Pic Pro&lt;/a&gt;》，我完成了V0.6.0版本，实现了基础功能。本来打算先写完后续的Nest篇、部署篇、总结篇，途中再自己默默更新到V1.0.0。但是写着写着，发现了些刺&lt;em&gt;&lt;strong&gt;激&lt;/strong&gt;&lt;/em&gt;的玩法，导致打乱了我的计划。&lt;/p&gt;
&lt;p&gt;真是计划赶不上变化啊，无奈之下我决定加更一篇，来分享一下我又新加了什么有趣的功能！&lt;/p&gt;
&lt;h2&gt;功能一览&lt;/h2&gt;
&lt;h4&gt;V0.7.0&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;自定义行x列生成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本来是v0.6.0的功能，偷懒了，放到了这个版本补上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191438063.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
2.  Toast组件&lt;/p&gt;
&lt;p&gt;原因：Konva生成过多矩形后，页面会有明显卡段，所以需要一个提示组件，提醒一下用户。&lt;/p&gt;
&lt;p&gt;基于daisyUI封装了一下Toast小组件，目前只支持同时显示一条消息，重复的消息会重复利用这个元素&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191438064.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Tab键切换颜色调整为空格键&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为Tab键和浏览器默认行为有冲突，页面有表单里元素，会自动focus，使用起来不太方便，所以调整为空格键了&lt;/p&gt;
&lt;h4&gt;V0.8.0&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;预设功能！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我在自己把玩自己的网站的时候，发现自己写死的几个颜色，有时候不够用，或者说我也不知道用什么颜色比较好。&lt;/p&gt;
&lt;p&gt;于是我就想要不我用吸取颜色的软件，找几个高大上的图，吸几组配色，再放上去够大家选择。于是我寻思先在电脑里开始找几个图吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191438065.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
很突然，我就发现了iKun的图片（侵删），马上就来了兴致，先画个iKun玩玩再说！&lt;br&gt;
画完了之后，导出，直接发到&quot;回家吃饭&quot;的群里！&lt;/p&gt;
&lt;p&gt;果然啊，大家对我的绘画水平表示了强烈的认可。&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191438066.png&quot; alt=&quot;&quot;&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;ol&gt;
&lt;li&gt;从吸取颜色到配置数量，太麻烦，太模糊。不像我这样擅长画画的话，很难一步到位&lt;/li&gt;
&lt;li&gt;画着画着，格子不够了怎么办&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;于是我灵机一动，我把我现在的配置保存成一个“预设”，别人想用的时候我就发给他。&lt;/p&gt;
&lt;p&gt;能用文本传播，很自然就能想到用json，而且用剪贴板的话，不懂代码的人也不必关心复制的是个什么玩意。用的时候再从一个入口粘贴进去，我这边自动给他加上，这样就完成了和上一个创作者相同的前置条件！&lt;/p&gt;
&lt;p&gt;不擅长画画也是个好事，不光好的画受欢迎，只要&lt;em&gt;&lt;strong&gt;你&lt;/strong&gt;&lt;/em&gt;的画够抽象，相信喜欢的人也是非常多的。因为互联网的好处就是你可以从中找到任何奇怪爱好、奇怪角度的同行者。&lt;/p&gt;
&lt;p&gt;为了解决第一个问题，我梳理了以下功能点， 然后趁着热乎劲儿火速把他们实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;预设的内容为：方格大小和数量配置，颜色组，名称&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    name: &quot;IKUN&quot;,
    cellConfig: {
      size: 5,
      border: 1,
      xCount: 15,
      yCount: 24,
    },
    // 从kunkun身上吸取的几个颜色
    colors: [&quot;#564A54&quot;, &quot;#DDDBED&quot;, &quot;#1C1A25&quot;, &quot;#DDB3C0&quot;, &quot;#908B96&quot;, &quot;#CC7A76&quot;, &quot;#ffffff&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;可以一键导出自己的预设到剪贴板&lt;br&gt;
粘贴板功能用的vue-use，&lt;a href=&quot;https://juejin.cn/post/7311697095505149990&quot;&gt;搭建项目的时候&lt;/a&gt;已经内置好了&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { useClipboard } from &quot;@vueuse/core&quot;;

const { isSupported, copy } = useClipboard();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191438067.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
3. 可以一键导入别人的模版&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191438068.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
4. 选择预设后，会切换当前配置为预设内容&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191438069.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;预设内容存在了LocalStorage，加载页面时会在本地缓存中加载预设，导入预设的时候，也会往本地缓存同步一下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 加载本地预设
...
const localPresets = localStorage.getItem(&quot;ZZSTUDIO_PPP_PRESETS&quot;);
  if (localPresets) {
    try {
      const presets = JSON.parse(localPresets);
      // 检测合法性, 没问题的话, 直接应用
      if (presets.every((preset: any) =&gt; checkPreset(preset))) {
        // 去重, name一样会被去掉, 本地优先
        const newPresets = filterNoRepeatPresets(presets.concat(awsomePreset.value));
        awsomePreset.value = newPresets.concat();
      }
    } catch (err) {
      toast.value.show({
        msg: &quot;本地预设加载失败&quot;,
        type: &quot;error&quot;,
      });
      return false;
    }
  } else {
    logger.info(&quot;无本地预设&quot;);
  }
...
// 导出预设
... 
if (isSupported) {
    copy(copyPreset.value);
    toast.value.show({
      type: &quot;success&quot;,
      msg: &quot;已复制到剪切板!&quot;,
    });
  }
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完之后，开始测试，随手画几个蒙娜丽莎这种作品，发现了几个bug，马上修复。&lt;/p&gt;
&lt;p&gt;由于点「导出图片」按钮次数&lt;em&gt;&lt;strong&gt;太&lt;/strong&gt;&lt;/em&gt;多，感觉有点迷瞪了，容易和「生成」按钮混了，于是我把「生成」按钮挪到了最上边，防止误触。&lt;/p&gt;
&lt;p&gt;又导出了几张大作并且分享之后，有用户给我提出了建议：导出之后的边框太粗了，有点影响视觉。&lt;/p&gt;
&lt;p&gt;我觉得很道理，安排！于是加了一个「导出去边框」的Toggle，测试了一下，确实是有内味了。&lt;/p&gt;
&lt;p&gt;目前操作栏如下：&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191438070.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;以上就是这两次小版本迭代新增的功能，边写边使用边改bug，闲里偷忙，只隔了一天就搞完啦~&lt;/p&gt;
&lt;h2&gt;作品演示&lt;/h2&gt;
&lt;p&gt;只复刻iKun肯定不是真粉丝，于是我把自己也打扮成了他的模样 ！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191438071.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;请把 「&lt;strong&gt;优秀&lt;/strong&gt;」打在评论区！！&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;本次带来了两个小版本更新，以及我的传世之画作，希望大家喜欢！&lt;/p&gt;
&lt;p&gt;从最初架构中，我并没有构思出这些功能或问题，而是在开发、使用的过程中，不断打磨、&lt;em&gt;&lt;strong&gt;美&lt;/strong&gt;&lt;/em&gt;化，发现问题，解决问题，&lt;strong&gt;从使用者的角度增加功能&lt;/strong&gt;。可见，最重要的还是抛除杂念，然后下手去做。&lt;/p&gt;
&lt;p&gt;而且！现在还只是纯前端功能，相信有了后端的加入，能够实现更多的有意思的功能~&lt;/p&gt;
&lt;p&gt;看到这里，如果你已经提起了兴趣，可以去回顾我的上一篇《&lt;a href=&quot;https://juejin.cn/post/7311697095505149990&quot;&gt;Vue3项目实战：像素风LOGO编辑器 Pixeled Pic Pro&lt;/a&gt;》，然后行动起来吧~&lt;/p&gt;
&lt;p&gt;如果你对这个产品感兴趣或者有好的功能建议要告诉我，欢迎关注公众号：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;早早集市&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[🚀提升效率！早早下班！Sharp+Picgo实现压缩后上传并替换外链的命令行工具]]></title><description><![CDATA[sharp压缩图片，picgo自动上传]]></description><link>https://zzao.club/post/cli/sharp-picgo-cli-tool</link><guid isPermaLink="true">https://zzao.club/post/cli/sharp-picgo-cli-tool</guid><pubDate>Wed, 13 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;大家好啊， 我是枣把儿！&lt;/p&gt;
&lt;p&gt;最近写作比较多，而要发平台又好几个，插入图片的时候就很不方便。一开始我是白嫖掘金的图片地址，但是每个平台都会自动打个水印上去...&lt;/p&gt;
&lt;p&gt;于是我又把之前写的命令行工具拿出来翻新了一些，加入了Sharp图片压缩、Picgo上传图床、markdown文件内容替换这三大内容，极大的改善了我的发文体验，并且感觉在日常中也比较实用。和大家分享一下~&lt;/p&gt;
&lt;h2&gt;功能一览&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;国际化文件翻译 - &lt;strong&gt;translate&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;目前用于公司内部批量翻译国际化文件, 所以需要遵循一定的规则&lt;/li&gt;
&lt;li&gt;基于百度翻译, 需要自定义自己的appId和key&lt;/li&gt;
&lt;li&gt;支持指定单个文件翻译&lt;/li&gt;
&lt;li&gt;支持指定文件夹, 批量翻译所有文件&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;压缩文件 - &lt;strong&gt;tiny&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基于&lt;a href=&quot;https://sharp.pixelplumbing.com/install#custom-libvips&quot;&gt;sharp&lt;/a&gt;, sharp支持的文件都可以压缩&lt;/li&gt;
&lt;li&gt;输出目录: 所有参数下, 压缩后文件都会输出到同级目录中&lt;/li&gt;
&lt;li&gt;输出时显示大小及压缩比例&lt;br&gt;
如: ✔ 190.4 KB =&gt; 115.2 KB (↓39.52%)【 demo1-zz-tiny-1702631665830.gif 】&lt;/li&gt;
&lt;li&gt;支持自定义名称输出 --name=xxx.png&lt;/li&gt;
&lt;li&gt;支持自定义压缩质量 --quality=70 (1-100)&lt;/li&gt;
&lt;li&gt;支持单个文件压缩 --file=xxx.png&lt;/li&gt;
&lt;li&gt;支持批量文件压缩
&lt;ul&gt;
&lt;li&gt;指定文件夹 --dir=./demo (基于当前命令运行的目录)
&lt;ul&gt;
&lt;li&gt;支持相对路径&lt;/li&gt;
&lt;li&gt;支持绝对路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;指定文件名 --condition=abc
&lt;ul&gt;
&lt;li&gt;模糊匹配 所有包含abc且支持的文件类型都会被压缩&lt;/li&gt;
&lt;li&gt;如果没有指定--dir , 则--condition会在当前目录下查找&lt;/li&gt;
&lt;li&gt;模糊匹配到的内容, 会标红处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;若批量压缩时, 指定了name(第四条), 则自定义名称后会自动拼接一个序号, 避免覆盖&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持和picgo联动, 压缩完后直接上传到图床&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;需要先安装picgo, 启动, 并配置好图床配置&lt;/li&gt;
&lt;li&gt;限制文件大小, 超过 max 的文件不会被上传&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 Picgo 上传到图床 - &lt;strong&gt;picgo&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持指定文件 -f 上传&lt;/li&gt;
&lt;li&gt;支持指定文件夹 -d 批量上传全部&lt;/li&gt;
&lt;li&gt;支持限制大小 -m 默认只上传60kb以内的图片&lt;/li&gt;
&lt;li&gt;支持模糊匹配 -co 文件名中含有co的图片, 且满足大小限制, 都会被上传&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;究极联动： &lt;strong&gt;tiny压缩 -&gt; 自动传给picgo -&gt; 自动把obsidian里的本地图片链接替换为上传后的url&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;先安装&lt;a href=&quot;https://sharp.pixelplumbing.com/install#custom-libvips&quot;&gt;sharp&lt;/a&gt;的两个前置依赖包。@img/sharp-darwin-arm64@0.33.0 、 @img/sharp-win32-x64@0.33.0&lt;/p&gt;
&lt;p&gt;我的系统是macos M2，所以需要安装@img/sharp-darwin-arm64&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i -g @img/sharp-darwin-arm64@0.33.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;windows10/11 (win32-x64)用户请安装  @img/sharp-win32-x64@0.33.0&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; npm i -g @img/sharp-win32-x64@0.33.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后安装工具本体&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i -g zzoffduty-cli@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 -h 看看是否成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zz -h
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tiny命令 实现压缩&lt;/h2&gt;
&lt;p&gt;使用help命令查看所有支持的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zz tiny --help

  -f, --file &amp;#x3C;file&gt;             要压缩的图片文件 (default: null)
  -d, --dir &amp;#x3C;dir&gt;               压缩文件夹内所有文件 (default: null)
  -co, --condition &amp;#x3C;condition&gt;  压缩文件夹内所有名称包含[--condition]的图片文件 (default: null)
  -q, --quality &amp;#x3C;quality&gt;       压缩质量(1-100) (default: 75)
  -c, --colours &amp;#x3C;colours&gt;       GIF色彩保留(2-256) (default: 128)
  -n, --name &amp;#x3C;name&gt;             指定文件名输出 (default: &quot;&quot;)
  -m, --max &amp;#x3C;max&gt;               限制要上传的文件大小(kb)(仅当开启 --picgo 时会用到) (default: 50)
  --picgo [type]                调用picgo (无参数) (default: null)
  --no-picgo [type]             不调用picgo (无参数) (default: null)
  -h, --help                    display help for command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;压缩功能演示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指定目录压缩，并指定名称输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442697.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;picgo命令 实现上传图床&lt;/h2&gt;
&lt;p&gt;PicGo在&lt;code&gt;2.2.0&lt;/code&gt;版本开始内置了一个小型的服务器，用于接收来自其他应用的HTTP请求来上传图片。&lt;/p&gt;
&lt;p&gt;默认监听地址： &lt;code&gt;127.0.0.1&lt;/code&gt;，默认监听端口：&lt;code&gt;36677&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;以POST请求去上传&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;method: &lt;code&gt;POST&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;url: &lt;code&gt;http://127.0.0.1:36677/upload&lt;/code&gt; （此处以默认配置为例）&lt;/li&gt;
&lt;li&gt;request body: &lt;code&gt;{list: [&apos;xxx.jpg&apos;]}&lt;/code&gt; 必须是JSON格式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;{
  &quot;success&quot;: true, // or false
  &quot;result&quot;: [&quot;url&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以&lt;strong&gt;此功能通过Http请求的方式调用Picgo Server, 所以需要本地已经安装并启动Picgo, 并已经配置好了图床&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果使用picgo-core的话，手动配置json文件我觉得更痛苦一些。所以还是基于已经有了Picgo客户端的情况下，简化一些流程&lt;/p&gt;
&lt;p&gt;使用help命令查看所有支持的功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Options:
  -f, --file &amp;#x3C;file&gt;             要上传的图片文件 (default: null)
  -d, --dir &amp;#x3C;dir&gt;               上传文件夹内所有图片文件 (default: null)
  -co, --condition &amp;#x3C;condition&gt;  上传文件夹内所有名称包含[--condition]的图片文件 (default: null)
  -m, --max &amp;#x3C;max&gt;               大于指定大小(kb)的图片不会被上传 (default: 50)
  -h, --help                    display help for command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上传文件夹内所有大小符合限制的图片文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442699.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上传所有名称中含有指定文字且大小符合限制的图片文件。没有匹配到会有提示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442700.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;tiny 联动 picgo&lt;/h2&gt;
&lt;p&gt;在tiny命令后使用--picgo 开启压缩后调用picgo&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;压缩单个图片文件 并 通过picgo上传 （这里也校验了max）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;zz tiny -f ./demo/demo2.png --picgo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442701.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;压缩文件夹内所有图片文件，并通过picgo上传所有符合大小限制的图片文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;zz tiny -d ./demo --picgo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442702.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;压缩文件夹内名称含有指定文字的，且符合大小限制的图片，并通过picgo上传&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;zz tiny -d ./demo -co mo2 --picgo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442703.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;压缩文件夹内名称含有指定文字的，且符合大小限制的图片，并指定输出的文件名，并通过picgo上传&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ps: 因为名称含有指定文字这个指令是个批量操作，所以输出文件名必定会带一个后缀防止重复&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zz tiny -d ./demo -co mo2 -n 自定义 --picgo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442704.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;究极联动，一条龙服务&lt;/h2&gt;
&lt;p&gt;压缩 =&gt; 上传 =&gt; 自动替换markdown图片链接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./src/index.js tiny -d ./demo/md/配图 --picgo --replace -ref ./demo/md/demo4.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442705.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此命令目前支持替换Obsidian里的Wiki链接， 即 &lt;code&gt;![[demo2.png]]&lt;/code&gt;&lt;br&gt;
如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442706.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;被压缩的图片里只要有demo2.png 就会替换成demo2.png压缩后的，上传到图床的url。&lt;br&gt;
&lt;code&gt;![[demo2.png]]&lt;/code&gt;  替换为 &lt;code&gt;![](http://www.baidu.com/demo2.png)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特别注意，有一些Obsidian配置的前置条件：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;需要开启Wiki链接。因为目前版本我&lt;strong&gt;只实现了替换wiki链接对应的文本&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191442707.png&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;Obsidian默认情况下，粘贴来的图片会显示成 paste img 2023123123.png 这种格式。所以建议安装 paste image rename 这个插件。原因是：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;因为它名称自带空格，和命令行工具无法兼容！如果你批量上传过程中，有一个文件不符合大小限制，没有被上传，也没被替换。此时你想使用&lt;code&gt;zz tiny -f ./paste img 2023123123.png  -m 100 --picgo --repalce -ref ../xxx.md&lt;/code&gt; 继续放大限制，上传+替换时，文件就无法被解析了！&lt;/li&gt;
&lt;li&gt;而且本身的名字比较长，不好找&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下是我的插件配置&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191442708.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我截图后粘贴到md中时显示如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442709.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在粘贴图片时，文件存放路径配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442710.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在目录中展示为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191442711.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;ok，以上就是我的obsidian配置，及我是如何使用zzoffduty-cli的全部内容！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;
zz tiny -d ./ -q 60 -m 100 --picgo --replace -ref ../🚀提升效率  ！早早下班！Sharp+Picgo实现压缩后上传并替换外链的命令行工具.md

✔ 正在压缩:.DS_Store
✖ 不支持此文件类型[.DS_Store]!
✔ 正在压缩:1-img.png
✔ 65.0 KB =&gt; 14.0 KB (↓78.42%)【 1-img-zz-tiny-1703041107406.png 】
✔ 正在压缩:2-img.png
✔ 423.4 KB =&gt; 95.9 KB (↓77.35%)【 2-img-zz-tiny-1703041107433.png 】
✔ 正在压缩:3-img.png
✔ 62.4 KB =&gt; 14.5 KB (↓76.76%)【 3-img-zz-tiny-1703041107602.png 】
✔ 正在压缩:4-img.png
✔ 88.1 KB =&gt; 17.9 KB (↓79.65%)【 4-img-zz-tiny-1703041107623.png 】
✔ 正在压缩:5-img.png
✔ 33.1 KB =&gt; 7.2 KB (↓78.23%)【 5-img-zz-tiny-1703041107660.png 】
✔ 正在压缩:img1.png
✔ 153.2 KB =&gt; 18.2 KB (↓88.14%)【 img1-zz-tiny-1703041107673.png 】
✔ 正在压缩:img10.png
✔ 100.1 KB =&gt; 37.9 KB (↓62.12%)【 img10-zz-tiny-1703041107703.png 】
✔ 正在压缩:img11.png
✔ 42.5 KB =&gt; 9.1 KB (↓78.61%)【 img11-zz-tiny-1703041107756.png 】
✔ 正在压缩:img2.png
✔ 139.4 KB =&gt; 32.2 KB (↓76.9%)【 img2-zz-tiny-1703041107779.png 】
✔ 正在压缩:img3.png
✔ 128.0 KB =&gt; 29.8 KB (↓76.75%)【 img3-zz-tiny-1703041107821.png 】
✔ 正在压缩:img4.png
✔ 79.1 KB =&gt; 17.5 KB (↓77.88%)【 img4-zz-tiny-1703041107867.png 】
✔ 正在压缩:img5.png
✔ 242.6 KB =&gt; 57.5 KB (↓76.31%)【 img5-zz-tiny-1703041107896.png 】
✔ 正在压缩:img6.png
✔ 94.7 KB =&gt; 21.3 KB (↓77.53%)【 img6-zz-tiny-1703041107974.png 】
✔ 正在压缩:img7.png
✔ 110.5 KB =&gt; 25.1 KB (↓77.31%)【 img7-zz-tiny-1703041108013.png 】
✔ 正在压缩:img8.png
✔ 110.6 KB =&gt; 24.4 KB (↓77.91%)【 img8-zz-tiny-1703041108058.png 】
✔ 正在压缩:img9.png
✔ 262.4 KB =&gt; 65.7 KB (↓74.98%)【 img9-zz-tiny-1703041108106.png 】
✔ 压缩成功 16 个
✔ 上传成功 16 个!
✔ 上传地址:
http://img.zzstudio.cn/1-img-zz-tiny-1703041107406.png
http://img.zzstudio.cn/2-img-zz-tiny-1703041107433.png
http://img.zzstudio.cn/3-img-zz-tiny-1703041107602.png
http://img.zzstudio.cn/4-img-zz-tiny-1703041107623.png
http://img.zzstudio.cn/5-img-zz-tiny-1703041107660.png
http://img.zzstudio.cn/img1-zz-tiny-1703041107673.png
http://img.zzstudio.cn/img10-zz-tiny-1703041107703.png
http://img.zzstudio.cn/img11-zz-tiny-1703041107756.png
http://img.zzstudio.cn/img2-zz-tiny-1703041107779.png
http://img.zzstudio.cn/img3-zz-tiny-1703041107821.png
http://img.zzstudio.cn/img4-zz-tiny-1703041107867.png
http://img.zzstudio.cn/img5-zz-tiny-1703041107896.png
http://img.zzstudio.cn/img6-zz-tiny-1703041107974.png
http://img.zzstudio.cn/img7-zz-tiny-1703041108013.png
http://img.zzstudio.cn/img8-zz-tiny-1703041108058.png
http://img.zzstudio.cn/img9-zz-tiny-1703041108106.png
✔ [🚀提升效率！早早下班！Sharp+Picgo实现压缩后上传并替换外链的命令行工具.md]图片链接替换完成!请前往检查!

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后直接复制，粘贴到各大平台即可！&lt;/p&gt;
&lt;p&gt;&lt;code&gt;使用前请仔细阅读readme.md，及最后的免责声明（害怕）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;这次上传的图片太多，真的怕七牛云空间不够了。。&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;下一步计划&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;提高兼容性！&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;准备把&lt;strong&gt;文本替换功能&lt;/strong&gt;抽离出来，现在只是满足了自己的需求。&lt;/li&gt;
&lt;li&gt;可以再加一个掘金文章里用的图片缩放功能，或者自定义replace函数、正则表达式等。&lt;/li&gt;
&lt;li&gt;收集更多其他人日常使用方式，融合进来&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这是一个我在为了简化日常工作和生活中一些复杂的、繁琐的、可联动的操作而开发的命令行工具，是一个纯粹为了效率的实用工具。&lt;/p&gt;
&lt;p&gt;后续随着我自己的使用或其他人的反馈，我会加入更多的功能把它一步步的完善。同时一些适合可视化操作的功能，也会拆成web、客户端、小程序等形态，都是我的&quot;预备摊位&quot;！！&lt;/p&gt;
&lt;p&gt;如果你对这个产品感兴趣或者有好的功能建议要告诉我，代码在&lt;a href=&quot;https://github.com/zzdaddy/zzoffduty-cli&quot;&gt;Github&lt;/a&gt;，也欢迎关注公众号：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;早早集市&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[一个产品要有一个“好底子”：Nest项目搭建]]></title><description><![CDATA[Nest 项目搭建]]></description><link>https://zzao.club/post/nest/nest-project-quick-start</link><guid isPermaLink="true">https://zzao.club/post/nest/nest-project-quick-start</guid><pubDate>Wed, 13 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;大家好，我是枣把儿。&lt;/p&gt;
&lt;p&gt;上周搞了一个前端小项目：Pixeled Pic Pro， 是一个用来制作像素风格LOGO的Canvas编辑器。&lt;/p&gt;
&lt;p&gt;同样，它的后端也是&lt;strong&gt;本着能用就行，先把功能搞出来&lt;/strong&gt;的原则。我来分享一下后端实现过程以及发生的故事~&lt;/p&gt;
&lt;h2&gt;“底子”&lt;/h2&gt;
&lt;p&gt;我一开始要做的项目起名叫：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;早早集市&lt;/a&gt;。这是一个从不知道什么时候到23年8月份左右完成构思，在方向上开始清晰起来（当时是这么认为的）的项目。&lt;/p&gt;
&lt;p&gt;原因就是，平时脑子里想法太多，多到必须下来，越写越多之后，我就在想怎么把他们搞出来，并且搞的有点联系。&lt;/p&gt;
&lt;p&gt;然后再去了几次夜市吃喝之后，我就有了点灵感：我要不整个电子集市吧！&lt;/p&gt;
&lt;p&gt;和大家去赶大集一样，每个产品相当于一个摊位，可以在一个入口里，看到所有在营业的&quot;摊位&quot;，并且像大集里的摊主一样，每个产品也是在向互联网用户提供服务或商品。&lt;/p&gt;
&lt;p&gt;我仔细想想之后啊，感觉真不错。摊位五花八门，没有限制，我的想法也是天马行空，指不定想做什么，可以取悦自己；有的摊位提供“&lt;strong&gt;商品&lt;/strong&gt;”；有的摊位提供“&lt;strong&gt;服务&lt;/strong&gt;”；还有的摊位给别的摊位提供商品，个体也可以直接去他那“&lt;strong&gt;进货&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;打通了自己的想法之后，就越想越顺，也越想越复杂。生怕架构层面无法满足自己的设想。&lt;/p&gt;
&lt;p&gt;搜索了很久微服务架构相关文章，问了下前同事（Java、运维）相关的思路（你问我为什么不问现同事？也问了，大部分表示就是用Spring全家桶，也知道微服务，也知道消息队列，也知道k8s。问怎么实现的、怎么设计的。不知道），最后也是给自己泼了泼冷水。&lt;/p&gt;
&lt;p&gt;算了，不整那么复杂了，本来搞个项目，也是为了万一35岁以后真不干前端了，给自己留点&quot;互联网遗产&quot;，证明自己来过。别还没开始就给自己折腾&quot;死了&quot;。&lt;/p&gt;
&lt;p&gt;冷却下来之后，我还发现，这玩意搞好了是个集市，搞不好不就是个工具站吗，网上一搜一大堆！&lt;/p&gt;
&lt;p&gt;你看，果然还是打退堂鼓的时候思路更清晰一些。&lt;/p&gt;
&lt;p&gt;但好在这次的构思过程足够深入，冷静下来之后还是让我感觉值得做下去，所以还是继续开始了这个故事。&lt;/p&gt;
&lt;p&gt;所以，Pixeled Pic Pro 也是其中一个“摊位”，摊主(名字待定)提供的正是“服务”。&lt;/p&gt;
&lt;p&gt;听完了故事，那就一起开始摆摊吧。&lt;/p&gt;
&lt;h2&gt;新建Nest项目&lt;/h2&gt;
&lt;p&gt;nest提供了 @nestjs/cli 这个包，先来安装一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i -g @nestjs/cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;已经安装完了的话，可以升级一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm update -g @nestjs/cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成之后可以使用 ==nest -h== 查看有哪些命令，后续会经常用到&lt;/p&gt;
&lt;p&gt;其中 ==--no-spec== 可以指定不生成测试文件，后面会用到&lt;/p&gt;
&lt;p&gt;这里我把服务分为两个，一个gateway服务，用来对外实现api接口及鉴权。 一个主服务，实现所有业务。除非不能满足业务，否则不再拆分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PS: 拆出gateway只是为了在实际业务中感受它的好处和坏处，大家自行甄别、自由选择&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;开始创建项目！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nest new 项目名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191446171.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;选择pnpm后等待安装完成，完成后已经可以运行， 进入项目根目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm start:dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开网站localhost:3000，可以看到Hello World！字样&lt;/p&gt;
&lt;p&gt;因为我这里需要有另一个网关服务，所以我再新建一个app，通过monorepo的方式管理&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# generator 可以缩写为 g
nest g app gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时可以看到app gateway 已经被创建， 同时自动创建了一个apps文件夹，里面包含两个app&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191446172.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;安装微服务需要的包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# add会安装在dependencies  加参数 -D 会安装在 devDependencies
pnpm add @nestjs/microservices
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后改造一下server部分，因为gateway在前，server在后，所以server需要改成微服务，通过TCP和gateway通信&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const app = await NestFactory.createMicroservice&amp;#x3C;MicroserviceOptions&gt;(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        port: 7577,
      },
    },
  );
  await app.listen();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在app.controller.ts里改一下接口，一会用来测试一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@MessagePattern(&apos;hello&apos;)
  getHello(): string {
    return &apos;hello by zzstudio-server&apos;;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来到gateway这边， gateway.module.ts里同样也要注册一下微服务，端口号和上面对应起来&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Module({
  imports: [
    ClientsModule.register([
      {
        name: &apos;ZZSTUDIO_SERVER&apos;,
        transport: Transport.TCP,
        options: {
          port: 7577,
        },
      },
    ]),
  ],
  controllers: [GatewayController],
  providers: [GatewayService],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在gateway.controller.ts里也写个方法测试一下&lt;br&gt;
对了，先用@Inject注入刚才注册的。可以看到有个ts报错，我们回到server那边&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Controller()
export class GatewayController {
  @Inject(&apos;ZZSTUDIO_SERVER&apos;)
  private serverClient: ClientProxy;
  constructor(private readonly gatewayService: GatewayService) {}

  @Get()
  getHello(): string {
    return this.gatewayService.getHello();
  }

  @Get(&apos;app&apos;)
  getServerHello(): unknown {
    return this.serverClient.send(&apos;hello&apos;, &apos;hello&apos;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把两个项目跑起来测试一下， 因为我们用的zzstudio-server新建的项目，所以跑dev默认启动的是zzstudio-server。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm start:dev
pnpm start:dev gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后浏览器输入localhost:3000/app，可以看到hello by zzstudio-server，通了。&lt;/p&gt;
&lt;p&gt;然后再试试打包&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 这会打包zzstuido-server服务
pnpm build

# 这会打包gateway服务
pnpm build gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打包完成后，可以看到dist里分别产生了各自服务的文件夹&lt;/p&gt;
&lt;h2&gt;功能梳理&lt;/h2&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;首先gateway部分。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对外提供接口，可以起一个公共的前缀，比如/api/v1。&lt;/li&gt;
&lt;li&gt;如果前端发生了改动，则去修改gateway里的请求逻辑，主服务不需要变&lt;/li&gt;
&lt;li&gt;实现鉴权。jwt 双token，前端无感刷新，过滤掉没权限的请求。&lt;/li&gt;
&lt;li&gt;如果主服务发生了改动，则去修改gateway里的请求逻辑，前端不需要变&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接口目前很简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;登录注册
&lt;ol&gt;
&lt;li&gt;先用用户名密码+邮箱验证码注册&lt;/li&gt;
&lt;li&gt;后续再添加关注公众号注册之类的操作&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;导出功能
&lt;ol&gt;
&lt;li&gt;次数统计 看看有多少人使用了导出。 相当于埋点了&lt;/li&gt;
&lt;li&gt;导出并压缩 （这是一个不着急实现的公共功能，可以预见其他的产品也会有这个功能）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;保存预设，json&lt;/li&gt;
&lt;li&gt;保存图片，以一种字符串或者json的形式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中3，4都不是必须的，先放一放。只要实现了框架结构和基本功能，后续按功能再加就很快了&lt;/p&gt;
&lt;h2&gt;功能实现&lt;/h2&gt;
&lt;p&gt;实现之前先用图来串一串思路。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用的Obsidian的Excalidraw画的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191446173.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后开始按照这个思路去实现功能，这里我只演示几个关键点。同样代码贴在文末，免费、开源&lt;/p&gt;
&lt;h3&gt;JWT模块注册&lt;/h3&gt;
&lt;p&gt;安装 @nestjs/jwt&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm add @nestjs/jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gateway.module.ts里注册&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Module({
  imports: [
    ClientsModule.register([
      {
        name: &apos;ZZSTUDIO_SERVER&apos;,
        transport: Transport.TCP,
        options: {
          port: 7577,
        },
      },
    ]),
    JwtModule.register({
        global: true,
        secret: &apos;zzdaddy&apos;,
        signOptions: {
          expiresIn: &apos;1d&apos;,
        },
      }),
  ],
  controllers: [GatewayController],
  providers: [GatewayService],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义decorator&lt;/h3&gt;
&lt;p&gt;我想设置一个开关，标识哪个接口可以不需要登录就访问，没有这个标识的就都需要鉴权&lt;/p&gt;
&lt;p&gt;先自定义一个装饰器，用于设置接口是否是公开的（true），没设置就是false&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 在gateway服务下，新建了一个custom文件夹，里面有一个custom.decorator.ts
nest g decorator custom --project=gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现装饰器 custom.decorator.ts&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const setPublicRoute = () =&gt; SetMetadata(&apos;isPublicRoute&apos;, true);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义Guard&lt;/h3&gt;
&lt;p&gt;按照上图的思路，现在应该写一个Guard，用来控制权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 生成后自己改个名， 我改成了LoginGuard
nest g guard globalGuard --project=gateway --no-spec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在生成的guard里实现&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;canActivate(
    context: ExecutionContext,
  ): boolean | Promise&amp;#x3C;boolean&gt; | Observable&amp;#x3C;boolean&gt; {
    const request: Request = context.switchToHttp().getRequest();

    const isPublicRoute = this.reflector.getAllAndOverride(&apos;isPublicRoute&apos;, [
      context.getClass(),
      context.getHandler(),
    ]);
    if (isPublicRoute) {
      return true;
    }

    const authorization = request.headers.authorization;

    if (!authorization) {
      throw new UnauthorizedException(&apos;用户未登录&apos;);
    }

    try {
      const token = authorization.split(&apos; &apos;)[1];
      const data = this.jwtService.verify(token);
      // 这里会报没有user, 可以用declare module 给上边的 Request 在类型空间定义一下user
      request.user = data.user;
      return true;
    } catch (e) {
      throw new UnauthorizedException(&apos;token 失效，请重新登录&apos;);
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;guard要想生效，还要在gateway.module.ts里注册一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt; providers: [
    {
      provide: APP_GUARD,
      useClass: LoginGuard,
    },
    GatewayService,
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时注册完成后，再去浏览器请求一下 /app 接口，可以发现已经被拦截住了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;{
  message: &quot;用户未登录&quot;,
  error: &quot;Unauthorized&quot;,
  statusCode: 401
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义Filter&lt;/h3&gt;
&lt;p&gt;拦截住之后，问题就来了，貌似公司里Java接口，返回的都是内种的格式，我怎么自定义自己的返回格式&lt;br&gt;
再回顾上图，实现一个过滤器，因为401是抛出了一个错误，会被filter捕捉到&lt;br&gt;
再从custom里建一个filter吧，建完了把名字改改&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nest g filter custom --project=gateway --no-spec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现一下功能，因为他会捕捉所有错误，也就是你其他地方抛出来的不是HttpException的错误也会在这里捕捉到，所以要判断一下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Catch()
export class HttpCatchFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const http = host.switchToHttp();
    const response = http.getResponse&amp;#x3C;Response&gt;();

	// 我把自己代码写错导致的错误都返回500
    const statusCode =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;
    // 使用exception的message 也可能是 exception.message.message 或 exception.message.error
    let message = exception.message;
    // 使用了参数校验之后，多个参数校验不通过，会返回一个数组，所以这里合并了一下，优化展示
    if (exception instanceof HttpException) {
      let res = exception.getResponse() as { message: string[] };
      message = res?.message?.join
        ? res?.message?.join(&apos;,&apos;)
        : exception.message;
    }
    // 这里json的格式、字段、内容，自己随便写
    response.status(statusCode).json({
      code: statusCode,
      message,
      error: &apos;Bad Request&apos;,
    });
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完同样需要在gateway.module.ts里的providers下注册（其他全局注册方式建议自行查阅）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;providers: [
    {
      provide: APP_GUARD,
      useClass: LoginGuard,
    },
    {
        provide: APP_FILTER,
        useClass: CommonErrorCatchFilter,
      },
    GatewayService,
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再回到浏览器看一下/app接口, 在校验失败的情况下，返回结果已经变成了我们想要的结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  code: 401,
  message: &quot;用户未登录&quot;,
  error: &quot;Bad Request&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再把刚才写的setPublicRoute给/app这个接口用一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Get(&apos;app&apos;)
@setPublicRoute()
getServerHello(): Observable&amp;#x3C;any&gt; {
  return this.serverClient.send(&apos;hello&apos;, &apos;hello&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再去浏览器看一下，hello by zzstudio-server，这个内容又出来了。&lt;/p&gt;
&lt;p&gt;写完了别忘了保存啊。我都忘了，还以为哪里写错了呢。&lt;/p&gt;
&lt;p&gt;然后再实现一个post接口试试看&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Post(&apos;login&apos;)
@setPublicRoute()
login(): Observable&amp;#x3C;any&gt; {
  return this.serverClient.send(&apos;login&apos;, { username: 1, password: 2 });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在我的主服务里，接受这个请求，然后返回俩token&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@MessagePattern(&apos;login&apos;)
  login(): object {
    return {
      access_token: &apos;123456&apos;,
      refresh_token: &apos;123456&apos;,
    };
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再拿postman、postwoman、apifox去请求试一试，我用的apifox&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191446174.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，拿到了数据，但明显还不是我们想要的结构。&lt;/p&gt;
&lt;h3&gt;自定义interceptor&lt;/h3&gt;
&lt;p&gt;所以我们再回顾一下上图，可以在interceptor里去处理一下next.handle() 之后的数据&lt;br&gt;
再回顾上图，实现一个拦截器&lt;br&gt;
再从custom里建一个吧，建完了把名字改改&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nest g interceptor custom --project=gateway --no-spec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现一下功能&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Injectable()
export class HttpCommonInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;#x3C;any&gt; {
    const response = context.switchToHttp().getResponse&amp;#x3C;Response&gt;();
    // 201时返回200
    if (response.statusCode === HttpStatus.CREATED)
      response.status(HttpStatus.OK);
    return next.handle().pipe(
      map((data) =&gt; {
        return {
          code: 200,
          data,
          message: &apos;ok&apos;,
        };
      }),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也需要在gateway.module.ts里注册一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;providers: [
    {
      provide: APP_GUARD,
      useClass: LoginGuard,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: HttpCommonInterceptor,
    },
    {
      provide: APP_FILTER,
      useClass: CommonErrorCatchFilter,
    },
    GatewayService,
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再去apifox请求看一下&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191446175.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;是我们想要的格式了。&lt;/p&gt;
&lt;h3&gt;权限校验部分&lt;/h3&gt;
&lt;p&gt;我新建一个auth模块， 在里面实现login、refreshToken接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nest g resource auth --project=gateway --no-spec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录接口，返回两个token，给到前端之后，前端请求时需要在headers里携带access_token，当提示前端已过期时，前端再用refresh_token去请求refresh接口。refresh接口则会再返回两个新的token。以此达到无限续签。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Post(&apos;login&apos;)
  async login(
    @Body() user: LoginDto,
    @Res({ passthrough: true }) res: Response,
  ): Promise&amp;#x3C;any&gt; {
    let userInfo = await this.authService.login(user);
    if (userInfo) {
      const access_token = this.jwtService.sign(
        {
          user: {
           ...
          },
        },
        {
          expiresIn: &apos;60m&apos;,
        },
      );
      const refresh_token = this.jwtService.sign(
        {
          userId: userInfo.id,
        },
        {
          expiresIn: &apos;7d&apos;,
        },
      );

      res.setHeader(&apos;token&apos;, access_token);
      return {
        access_token,
        refresh_token,
      };
    }
  }

   @Get(&apos;refresh&apos;)
  async refresh(@Query() refreshParams: refreshDto) {
    try {
      const data = this.jwtService.verify(refreshParams.refreshToken);

      const user = await this.authService.findUserById(data.userId);

      const access_token = this.jwtService.sign(
        {
          ...
        },
        {
          expiresIn: &apos;60m&apos;,
        },
      );

      const refresh_token = this.jwtService.sign(
        {
          userId: user.id,
        },
        {
          expiresIn: &apos;7d&apos;,
        },
      );

      return {
        access_token,
        refresh_token,
      };
    } catch (e) {
      throw new UnauthorizedException(&apos;token 已失效，请重新登录&apos;);
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用typeorm链接mysql&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm add typeorm @nestjs/typeorm mysql2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在gateway.module.ts里注册。synchronize: true时 user表会自动创建。不过这里只是为了演示，实际上我是在主服务里维护的user模块&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;TypeOrmModule.forRootAsync({
      useFactory() {
        return {
          type: &apos;mysql&apos;,
          host: &apos;localhost&apos;,
          port: 3306,
          username: &apos;root&apos;,
          password: &apos;123456&apos;,
          database: &apos;zzstudio&apos;,
          synchronize: true,
          logging: true,
          entities: [User],
          poolSize: 10,
          connectorPackage: &apos;mysql2&apos;,
          extra: {
            authPlugin: &apos;sha256_password&apos;,
          },
        };
      },
    }),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，实现了基本的接口之后，再配合上边写的自定义Guard，就可以实现对权限的校验了。细节部分，我就不展开了，对大家意义也不大。&lt;/p&gt;
&lt;p&gt;本地开发的话，我是用的docker desktop，先跑一个mysql，这样感觉比较省事。后续上线的话，我用的是docker-compose，把服务+mysql+redis 一起编排上线&lt;/p&gt;
&lt;p&gt;ok。结束&lt;/p&gt;
&lt;p&gt;PS：后续更新的方向，将会按照故事的发展进行，但每一个系列我也会尽快收尾。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这次分享了一下我最初构思的那个项目的大概背景和设定。以及从零搭建一个Nest项目，按照上边流程，搭建出来是没问题的。&lt;/p&gt;
&lt;p&gt;因为我自己的服务早就写完了，这次我专门从头建了一个项目，又码了一遍，后面可能鉴权，token方面写的不太细致。因为感觉这种教程应该是一搜一大堆了，我再重新来一遍意义不大。&lt;/p&gt;
&lt;p&gt;代码我还是会尽快放在&lt;a href=&quot;https://github.com/zzdaddy/nestjs-template-zz&quot;&gt;&lt;strong&gt;Github&lt;/strong&gt;&lt;/a&gt;中，作为v0.1.0版本，后续会把鉴权、日志、文件、邮箱、支付等等一些公共的模块会更新在这个仓库里，需要的同学可以pull下来，再自己改改，作为项目的启动模版。&lt;/p&gt;
&lt;p&gt;当然，有任何问题也可以在公众号：&lt;a href=&quot;https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A&quot;&gt;&lt;strong&gt;早早集市&lt;/strong&gt;&lt;/a&gt; 找到我。后面我会持续分享早早集市里的每一个“摊位”的诞生和迭代过程，但&lt;strong&gt;不构成&lt;/strong&gt;对大家技术栈、代码规范、命名方式、架构层面合理性等等见仁见智的角度的&lt;strong&gt;任何建议&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;大家就当听个故事，新手的话顺便还能入个门，半路进厂想做全栈的也可以参考一下我的思路，也欢迎来找我一起交流 ~&lt;/p&gt;
&lt;p&gt;感谢阅读，我是枣把儿 ~&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Vue3项目实战：像素风LOGO编辑器 Pixeled Pic Pro]]></title><description><![CDATA[一个基于 LeaferUI 的像素风编辑器]]></description><link>https://zzao.club/post/pixel/vue3-logo-creator-ppp</link><guid isPermaLink="true">https://zzao.club/post/pixel/vue3-logo-creator-ppp</guid><pubDate>Mon, 11 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;上篇文章《&lt;a href=&quot;https://juejin.cn/post/7310786618391167013&quot;&gt;当一个程序员突然想做一款产品&lt;/a&gt;》说到，我在做自己的产品的过程中，萌生了自己做一个像素风LOGO编辑器的想法，并设计了产品的整体功能、技术栈选择、服务器部署全部流程。&lt;/p&gt;
&lt;p&gt;出于一个开发人员本能的对产品排期的排斥，我非常想向身为产品的自己提出一点时间上的吐槽。&lt;/p&gt;
&lt;p&gt;但是我很快就忍住了，因为我想到xw了更好的处理办法。&lt;/p&gt;
&lt;h2&gt;版本划分&lt;/h2&gt;
&lt;p&gt;产品的核心功能是：生成任意行和列的方格子，按自己的喜好进行涂抹上色，创作完自己想要的图片后，导出图片即可。&lt;/p&gt;
&lt;p&gt;所以我把核心功能定为V1版本：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个可拖动的画布，一个可绘制的区域，区域内由带或不带黑色边框的单元格组成，并且可缩放可拖动。&lt;/li&gt;
&lt;li&gt;单元格的形态为正方形格子。可以设置数量，如：100x100，单个方格可以设置大小，如格尺寸为：5x5&lt;/li&gt;
&lt;li&gt;操作模式分为两种： 1. 鼠标模式 2. 绘图模式&lt;/li&gt;
&lt;li&gt;鼠标模式下，可以自由拖拽&lt;/li&gt;
&lt;li&gt;绘图模式下，鼠标可在绘图区域的小方格上点击并滑动，小方格会被设置对应的颜色
&lt;ol&gt;
&lt;li&gt;颜色可配置，并且方便切换：比如tab键自动轮换预制的几种颜色&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;绘制后，图片可导出&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我打算这样制定版本号规则：x.y.z&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;z：通常在未上线阶段或上线后小范围迭代使用，如样式修改、代码优化等不涉及功能的改动后，此版本号加1。&lt;/li&gt;
&lt;li&gt;y：通常为新增功能，bug修复等功能的迭代版本，如我这次开发的产品，我打算以v0.1.0开始，在每完成一个上述的独立功能123456后，此版本号加1，直到产品闭环。&lt;/li&gt;
&lt;li&gt;x：通常为一个产品的稳定版本，此时产品的功能已经完成闭环，与上一个版本比，在功能、操作、逻辑上有重大更新或修复致命bug时，此版本号加1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样，我就可以在有限的时间内推进项目的正常开发工作，让大家看到我这段时间的成果，后续还可以掏出时间来持续的迭代产品。显得有条不紊，尽显老鸟风范，皆大欢喜。&lt;/p&gt;
&lt;h2&gt;项目搭建&lt;/h2&gt;
&lt;p&gt;首先安装&lt;a href=&quot;https://github.com/Rich-Harris/degit&quot;&gt;degit&lt;/a&gt;这个工具，它可以用来安装&lt;a href=&quot;https://github.com/vitejs/awesome-vite#templates&quot;&gt;社区模版&lt;/a&gt;里的优秀模版，以此快速启动我们的项目&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm install -g degit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在社区模版里挑选过后，我选择了&lt;a href=&quot;https://github.com/kirklin/boot-vue/blob/master/README.zh-CN.md&quot;&gt;boot-vue&lt;/a&gt;。作者这样说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⚡ &lt;a href=&quot;https://github.com/kirklin/boot-vue#readme&quot;&gt;闪电般的速度&lt;/a&gt;：使用 Vue 3、Vite 和 pnpm 构建，速度飞快 🔥&lt;/li&gt;
&lt;li&gt;💪 &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;强类型&lt;/a&gt;：使用 TypeScript 💻&lt;/li&gt;
&lt;li&gt;🔥 &lt;a href=&quot;https://github.com/vuejs/rfcs/pull/227&quot;&gt;最新语法&lt;/a&gt;：使用新的 script setup 语法 🆕&lt;/li&gt;
&lt;li&gt;📦 &lt;a href=&quot;https://chat.openai.com/chat/src/components&quot;&gt;自动导入组件&lt;/a&gt;：自动导入组件 🚚&lt;/li&gt;
&lt;li&gt;📥 &lt;a href=&quot;https://github.com/antfu/unplugin-auto-import&quot;&gt;自动导入 API&lt;/a&gt;：使用 unplugin-auto-import 直接导入 Composition API 和其他 API 📨&lt;/li&gt;
&lt;li&gt;🎨 &lt;a href=&quot;https://unocss.dev/&quot;&gt;UnoCSS&lt;/a&gt; - 瞬间响应式 CSS 引擎，提供轻量级和快速的样式应用方式。&lt;/li&gt;
&lt;li&gt;🌼 &lt;a href=&quot;https://daisyui.com/&quot;&gt;Daisy&lt;/a&gt; - 免费开源的 Tailwind CSS 组件库&lt;/li&gt;
&lt;li&gt;💡 &lt;a href=&quot;https://router.vuejs.org/&quot;&gt;官方路由器&lt;/a&gt;：使用 Vue Router v4 🛣️&lt;/li&gt;
&lt;li&gt;🎉 &lt;a href=&quot;https://github.com/rstacruz/nprogress&quot;&gt;加载反馈&lt;/a&gt;：使用 NProgress 提供页面加载进度反馈 🔄&lt;/li&gt;
&lt;li&gt;🍍 &lt;a href=&quot;https://pinia.esm.dev/&quot;&gt;状态管理&lt;/a&gt;：使用 Pinia 进行状态管理 🗃️&lt;/li&gt;
&lt;li&gt;📜 &lt;a href=&quot;https://github.com/kirklin/unocss-preset-chinese&quot;&gt;中文字体预设&lt;/a&gt;：包含中文字体预设 🇨🇳&lt;/li&gt;
&lt;li&gt;🌍 &lt;a href=&quot;https://chat.openai.com/chat/src/locales&quot;&gt;国际化就绪&lt;/a&gt;：使用本地化准备好国际化 🌎&lt;/li&gt;
&lt;li&gt;☁️ &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify 就绪&lt;/a&gt;：可在 Netlify 上零配置部署 ☁️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，它的功能已经非常齐全，并且没有太多多余的业务类代码，拿来作为项目的启动模版是非常香的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx degit kirklin/boot-vue pixeled-pic-pro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到一个叫&lt;strong&gt;pixeled-pic-pro&lt;/strong&gt;的项目已经被创建，进入项目目录，开始安装依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 没有安装pnpm的话，先自行安装一下
pnpm i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，运行一下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到页面已经运行在了8888端口下，并且自动开启了一个__inspect的页面&lt;/p&gt;
&lt;p&gt;打开浏览器，发现还有一个切换主题皮肤的功能（&lt;a href=&quot;https://daisyui.com/&quot;&gt;Daisy&lt;/a&gt;），可以尝试后选择一个比较严肃和正经的皮肤&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191440537.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;为了以防开发完了才发现别人的库有打包的问题，我们可以先运行一下打包命令，看看是否正常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;good，没有问题。&lt;/p&gt;
&lt;h2&gt;梳理项目结构&lt;/h2&gt;
&lt;p&gt;看看这个项目里的东西有没有需要删除或修改的地方&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;router&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;路由钩子&lt;/strong&gt;里已经结合&lt;a href=&quot;https://ricostacruz.com/nprogress/&quot;&gt;NProgress&lt;/a&gt;，做了加载的进度反馈，后续如果有其他需要结合路由来判断的状态，也会继续添加在这里。&lt;/p&gt;
&lt;p&gt;模式是 Hash模式，是用createWebHashHistory()创建的，可以看到url上会有一个#标志。&lt;/p&gt;
&lt;p&gt;如果使用createWebHistory()，则创建的是Html5模式，这种模式的url看起来会比较“正常”，但需要服务器做一些配置工作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;#nginx部分配置
location / {
  try_files $uri $uri/ /index.html;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;store&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过pinia进行状态管理，已经有了一个小例子，后续使用的话可以仿照着改造一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pages&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;页面部分。 它有两个页面。为了保持简单，我就只用这两个页面，一个写欢迎页，一个写功能页。 标题栏也直接拿来用，不要浪费。&lt;/p&gt;
&lt;p&gt;欢迎页比较简单，我准备放一个功能演示的GIF，配一个大大的按钮【开始创作】&lt;/p&gt;
&lt;p&gt;把首页里左上角名称改成&lt;strong&gt;Pixeled Pic Pro&lt;/strong&gt;和作者相关的Footer注释掉，github地址换成自己的。这部分内容在&lt;strong&gt;layouts&lt;/strong&gt;里。&lt;/p&gt;
&lt;p&gt;注意：此项目使用的是基于&lt;a href=&quot;https://www.tailwindcss.cn/docs/installation&quot;&gt;Tailwind CSS&lt;/a&gt; 的UI组件库 &lt;a href=&quot;https://daisyui.com/docs/themes/&quot;&gt;daisyUI&lt;/a&gt;，而Tailwind CSS是通过&lt;a href=&quot;https://unocss.dev/&quot;&gt;UnoCSS&lt;/a&gt;来控制引入的，所以开发时需要打开它们的文档，方便查阅。&lt;/p&gt;
&lt;p&gt;此时页面长这个样子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191440539.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;再稍微调整一下样式，我先设置了一个flex布局，画布页面直接flex-1占满&lt;/p&gt;
&lt;p&gt;layouts/index.vue&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;template&gt;
&amp;#x3C;div class=&quot;font-chinese antialiased&quot;&gt;
	&amp;#x3C;div class=&quot;min-h-screen flex flex-col&quot;&gt;
		&amp;#x3C;Navbar /&gt;
		&amp;#x3C;RouterView /&gt;
	&amp;#x3C;/div&gt;
	&amp;#x3C;!-- &amp;#x3C;Footer /&gt; --&gt;
&amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;画布页面StoreTest.vue初始状态如下，文件名字可以改一改，但是我不改&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191440540.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;还需要一个操作栏，放置一些常用配置和功能&lt;/p&gt;
&lt;p&gt;我选择把操作栏放在左侧，并且加入几个功能按钮：生成、导出、颜色配置等，按钮可以边写边加，先按主要功能一步一步的来实现。&lt;/p&gt;
&lt;p&gt;设置到了这里，我发现了一个小问题，作者似乎对html使用了scrollbar-gutter：stable这个属性，导致我页面没有滚动条的情况下，在右侧也出现了gutter&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191440541.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是乎我把这个属性覆盖了一下，一切就正常了起来~&lt;/p&gt;
&lt;p&gt;src/styles/main.css&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt; html {
    	scrollbar-gutter: auto
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有内味儿了吧？接下来开始实现主体功能&lt;/p&gt;
&lt;h2&gt;画布相关功能&lt;/h2&gt;
&lt;p&gt;安装konva、axios(备用)依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm i konva axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合之前多次头脑风暴的经验，canvas的可玩性是非常高的，可以实现&lt;strong&gt;很多&lt;/strong&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;如果另一个新的产品也和画布有关，那我们可以进一步把他抽离，发布到npmjs上。就如同这个boot-vue的作者自己内置的@kirklin/logger、@kirklin/reset-css一样。&lt;/p&gt;
&lt;p&gt;回到正题。&lt;/p&gt;
&lt;p&gt;把这个类随便取一个名字：AppStage，Konva里已经有个Stage的概念了，所以我这里稍微改改。&lt;br&gt;
回顾一下需要实现的功能，按照思路，罗列出AppStage要实现哪些方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化画布 initStage&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class AppStage {
	constructor(
		ref: Ref,
		options: AppStageConfig = {
			isAllowMouseSelectShapes: true,
			isInitKeyboardEvents: true,
			mouseMode: &quot;basic&quot;,
			scale: false,
			scaleMember: null,
		}
	)
	...
	
	init(options: AppStageConfig) {
		console.log(`初始化容器为: ${this.canvasWindow.width} x ${this.canvasWindow.height}`
		);
		this.stage = new Konva.Stage({
			container: this.containerRef,
			width: this.canvasWindow.width,
			height: this.canvasWindow.height,
			id: &quot;baseStage&quot;,
			draggable: options.scale
		});
	}
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;设置是否可缩放 set&lt;/li&gt;
&lt;li&gt;设置可缩放的对象 (macos上触摸板两指上下滑动等同于鼠标放大缩小)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Konva的stage可以监听鼠标滚轮事件
this.stage.on(&quot;wheel&quot;, (e) =&gt; {
	// 通过wheelDelta判断，是在放大还是缩小
	if (evt.wheelDelta &gt; 0) {
          // 放大
          if (this.scaleMember.scaleX() &amp;#x3C; max) {
            this.scaleMember.scaleX(this.scaleMember.scaleX() + step);
            this.scaleMember.scaleY(this.scaleMember.scaleY() + step);
            // this.scaleMember.move({ x: -offsetX, y: -offsetY }) // 跟随鼠标偏移位置
          }
        } else {
          // 缩小
          ...
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;绑定鼠标事件（落下、移动、抬起）（移动过程中，划过的元素进行填充颜色）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type MouseMode = &quot;basic&quot; | &quot;draw&quot; | &quot;clip&quot; | &quot;fill&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;解绑鼠标事件（有绑定就有解绑）&lt;/li&gt;
&lt;li&gt;添加图形&lt;/li&gt;
&lt;li&gt;批量添加图形（因为方格要被统一的缩放，并且只有方格可以交互，所以批量添加时需要加入到一个&lt;a href=&quot;http://192.241.202.210/docs/groups_and_layers/Groups.html&quot;&gt;Group&lt;/a&gt;中, 方便管理）&lt;/li&gt;
&lt;li&gt;设置鼠标模式（切换绘图、拖拽模式，类似ps、ai里的操作）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;switchMouseMode(mouseMode: MouseMode) {
	this.mouseMode = mouseMode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;查找图形（比如查找没有上色的图形等功能可以用到）&lt;/li&gt;
&lt;li&gt;导出图片 （回到缩放前的状态，然后导出）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于具体的业务代码对大家可能并无卵用，细节代码就不展开了，代码链接放在文末的小结中。&lt;/p&gt;
&lt;h2&gt;生成格子&lt;/h2&gt;
&lt;p&gt;由于生成12x12这个格子属于这个产品的业务功能，我这里没有选择把他放进类的方法里，选择在自己的组件里实现&lt;/p&gt;
&lt;p&gt;单元格配置先给几个默认的预设，方便在不配置的情况下也能正常使用，视频里用Adobe AI来实现的时候使用了十二根线交错，所以我这里先默认给个12x12的方格布局。并且生成的单元格，默认是正方形，不排除后续加入三角形、圆形。所以我先留个口子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const basicCellConfig = reactive({
	size: 5, // 单个格子宽高
	border: 1, // 边框宽度
	xCount: 12, // 横向有几个
	yCount: 12, // 纵向有几个
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成12x12单元格方法，这里我们先把所有rect生成好，然后传给AppStage类里按Group生成图形&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const genPixelBoxCells = () =&gt; {
  let { x, y, strokeWidth: border } = PixelRect.value?.getAttrs()
  let cells = []
  for (let xIndex = 0; xIndex &amp;#x3C; basicCellConfig.xCount; xIndex++) {
    for (let yIndex = 0; yIndex &amp;#x3C; basicCellConfig.yCount; yIndex++) {
      let attrs = {
        x: x + border + basicCellConfig.size * xIndex,
        y: y + border + basicCellConfig.size * yIndex,
        width: basicCellConfig.size,
        height: basicCellConfig.size,
        strokeWidth: basicCellConfig.border,
        stroke: &apos;black&apos;,
        fill: &apos;white&apos;,
        name: `fillnode-${xIndex}-${yIndex}`,
        draggable: false,
      }
      let rect = new Konva.Rect(attrs)
      cells.push(rect)
    }
  }
  // 把所有格子放进一个组里，方便同时管理
  Stage.value.createShapesByGroup(PixelRectGroup.value, cells)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，页面如下：&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191440542.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;鼠标模式、鼠标事件、键盘事件&lt;/h2&gt;
&lt;p&gt;监听鼠标事件，然后实现涂色功能，先默认一个颜色，把功能做出来，然后再实现切换颜色的功能&lt;/p&gt;
&lt;p&gt;在AppStage.ts里实现监听和上色功能&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;listenAndFillRect() {
	// 监听前，需要设置一个绘图对象，涂色时只对这个对象有效
    if (!this.drawTaget) {
      console.error(`未设置绘图对象 drawTarget : this.drawTarget(target:any)`);
      return;
    }

    this.drawTaget.on(&quot;mousedown&quot;, (e: any) =&gt; {
    // 如果不是自己的模式，就不执行
	  if (this.mouseMode !== &quot;fill&quot;) {
        this.drawTaget.off(&apos;mousedown&apos;)
        return;
      };
      // 绘图时禁止冒泡, 防止拖拽
      e.cancelBubble = true;
      this.fillStatus = &quot;filling&quot;;
      e.target.fill(this.fillConfig?.color);

      this.drawTaget.on(&quot;mousemove&quot;, (e: any) =&gt; {
        if (this.fillStatus === &quot;filling&quot;) {
          e.target.fill(this.fillConfig?.color);
        }
      });

      this.drawTaget.on(&quot;mouseup&quot;, () =&gt; {
        this.drawTaget.off(&quot;mousemove&quot;);
        this.drawTaget.off(&quot;mouseup&quot;);
        this.fillStatus = &quot;done&quot;;
      });
    });
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在组件内实现切换模式功能，这个功能同样封装在AppStage里，两种模式切换的时候对应两套鼠标动作，所以还需要一个listenAndAssignTask功能，来switch case一下模式，对应不同的操作逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const changeMode = () =&gt; {
  Stage.value.switchMouseMode(mode.value)
  Stage.value.listenAndAssignTask()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再加一个切换颜色的功能，写几个div，背景色就是配置的颜色，横向排列，因为我们这里是方格，所以展示颜色的div也显示成方格，如下图&lt;br&gt;
&lt;img src=&quot;https://img.zzao.club/article/202411191440543.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个切换颜色的小组件，还可以继续优化一下，做一个动画，点击了谁就跳到第一个位置上。这里&lt;strong&gt;先记下，后续再做&lt;/strong&gt;，继续推进功能，避免中途加太多东西，导致延期。&lt;/p&gt;
&lt;p&gt;鼠标一个一个的点，明显不方便，我再加一个&lt;strong&gt;tab键切换颜色&lt;/strong&gt;的功能&lt;/p&gt;
&lt;p&gt;StoreTest.vue&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const bindKeyboardEvent = () =&gt; {
    window.addEventListener(&quot;keydown&quot;, (e) =&gt; {
      if (e.code === &quot;Tab&quot;) {
        let index = colorConfig.value.findIndex(color =&gt; color === selectColor.value)
        index = index &gt;= colorConfig.value.length - 1 ? 0 : index+1
        changeColor(colorConfig.value[index])
      }
      e.preventDefault();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前，页面已经实现了以下效果&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191440544.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后一步，实现导出图片功能&lt;/p&gt;
&lt;h2&gt;导出功能&lt;/h2&gt;
&lt;p&gt;Konva内置了toDataURL功能，可以自定义导出的区域&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt; // 转dataURL 用于导出
  toDataURL(options = {}) {
    return this.stage.toDataURL(options);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再封装一个用a标签下载图片的功能&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export function downloadPNGForCanvas(
  dataURL: string,
  filename: string = (+new Date()).toString(),
) {
  const a = document.createElement(&apos;a&apos;)
  a.download = filename
  a.href = dataURL
  document.body.appendChild(a)
  a.click()
  a.remove()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ok, 可以实现导出功能了。&lt;br&gt;
创建方格时，我给他们套了一个Rect，方便获取导出时width、height，xy坐标是Group的坐标。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const exportImage = () =&gt; {
  // 缩放回原始大小
  PixelRectGroup.value.scaleX(1)
  PixelRectGroup.value.scaleY(1)
  Stage.value.batchDraw()
  nextTick(() =&gt; {
    // 获取位置
    let { x, y } = PixelRectGroup.value.absolutePosition()
    let { width, height } = PixelRect.value.getAttrs()
    let dataURL = Stage.value.toDataURL({
      x,
      y,
      width,
      height,
      pixelRatio: window.devicePixelRatio, 
    })
    downloadPNGForCanvas(dataURL, &apos;测试&apos;)
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后用完成的功能画一个图试试看。&lt;br&gt;
很明显啊，这是个&lt;strong&gt;掘金&lt;/strong&gt;的标志，但是略微有些抽象，如果格子多一些后会好很多。&lt;br&gt;
不过用手画还是略微有点慢呀，看来V2版本的&lt;strong&gt;导入图片自动绘制像素风&lt;/strong&gt;要提上日程啦。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191440545.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;版本迭代&lt;/h2&gt;
&lt;p&gt;Pixeled Pic Pro的&lt;strong&gt;V1版本&lt;/strong&gt;已经按照自己规划的内容完成了。本篇的代码，我会尽快更新到&lt;a href=&quot;https://github.com/zzdaddy/PixeledPicPro&quot;&gt;Github&lt;/a&gt;中。这次的版本定为v0.6.0，我会打一个tag标记。&lt;/p&gt;
&lt;p&gt;后续有优化、功能迭代的话我都会按照文章所述的版本命名方式继续在&lt;a href=&quot;https://github.com/zzdaddy/PixeledPicPro&quot;&gt;Github&lt;/a&gt;中更新，感兴趣的朋友可以去&lt;a href=&quot;https://github.com/zzdaddy/PixeledPicPro&quot;&gt;Github&lt;/a&gt; 拉代码玩一玩，考虑到这个产品本体功能可能对大部分同学都不适用，所以大家仅参考我的开发过程和思考方式即可，前端或后端的&lt;strong&gt;V2版本&lt;/strong&gt;，我会在整个产品的V1版本闭环后再&lt;strong&gt;酌情更新&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;后续文章为： Nest实战篇、前端+后端部署篇、总结篇。 总计五篇文章。我会收录在我的专栏/分类里方便大家查阅。&lt;/p&gt;
&lt;h2&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;/li&gt;
&lt;li&gt;碰到可复用的代码，先拆出来&lt;/li&gt;
&lt;li&gt;收尾后及时总结归纳，加深印象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上就是本篇全部的内容，文中有&lt;strong&gt;不正确、不清晰&lt;/strong&gt;的地方，欢迎在评论区指出，我会尽快更正（不优雅不算）。&lt;/p&gt;
&lt;p&gt;如果对大家能有一点点帮助，这将是我继续写下去的最大动力。&lt;/p&gt;
&lt;p&gt;喜欢的朋友可以点个关注，继续追更后续的更多内容。有任何前端问题想要咨询的同学，也欢迎加VX：zzdaddy7，我会尽力为你解答。&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;
&lt;p&gt;（丑陋的彩蛋...）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191440546.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[当一个程序员突然想做一款产品]]></title><description><![CDATA[你一定有很多瞬间，脑子诞生了一个有趣的产品雏形，但因为种种原因搁浅，最终没有落地。]]></description><link>https://zzao.club/post/daily/when-a-developer-want-to-dosomething</link><guid isPermaLink="true">https://zzao.club/post/daily/when-a-developer-want-to-dosomething</guid><pubDate>Wed, 06 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你一定有很多瞬间，脑子诞生了一个有趣的产品雏形，但因为种种原因搁浅，最终没有落地。&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;在一个普普通通的周二，我又㕛叒一次萌生了和许多程序员一样的想法：做一款自己的产品。&lt;/p&gt;
&lt;p&gt;鉴于以前的想法 99% 都变成了一个一个不能用的 demo，还有 1% 在反复起步，这次我痛定思痛，决心一定要好好“架构”一番！！&lt;/p&gt;
&lt;h2&gt;起步&lt;/h2&gt;
&lt;p&gt;说干就干。我熟练的打开Obsidian，打开Excalidraw 新建绘图，开始设计网站的草图，然后在一旁开始罗列想用的技术栈、表结构、功能点。一阵奋笔疾书后，一个网站的大概样子已经尽收我眼底！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191426107.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;正当我心中一阵窃喜时，我突然意识到我还缺一个Logo。&lt;/p&gt;
&lt;p&gt;是的，这是一个非常严重的问题。虽然我还没有新建文件夹，但是这种建成后必定一流的网站要有一个配得上它的一流Logo才对。不行，我一定要为它找一个称心如意的Logo才行。但是今天已经快下班了，我只能明天再搞了。&lt;/p&gt;
&lt;p&gt;ok，愉快的第二天马上就来了。&lt;/p&gt;
&lt;p&gt;我开始从AIGC和自主设计两个方向寻找适合我的Logo，很快啊，AIGC被我排除了，因为它感觉就像一个不合格的Tony老师一样，不适合我这种精益求精的态度。&lt;/p&gt;
&lt;p&gt;于是我打开b站，开始观摩几个设计领域的大up主，看看能不能吸取到一星半点的灵感（当然是只感受到了nb二字）。突然，我发现了一个&lt;a href=&quot;https://www.bilibili.com/video/BV13U4y1u7zB/?spm_id_from=333.999.0.0&amp;#x26;vd_source=cf9b3c8faa56d0c64b09a55be382ef80&quot;&gt;搬运自YouTube的视频&lt;/a&gt;，再结合之前我在&lt;a href=&quot;https://www.pinterest.com/&quot;&gt;pinterest&lt;/a&gt;看到的让我眼前一亮的像素风格，就决定是你了！！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191426108.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;再起炉灶&lt;/h2&gt;
&lt;p&gt;我打开了我珍藏已久的Adobe AI , 想尝试按视频里的操作自己设计一番，但无奈它提示我因为某些原因不能继续使用了，并且只给了我退出软件和购买软件两个选项。我在微信提示余额不足后咬牙选择了退出。&lt;/p&gt;
&lt;p&gt;既然如此，我只能自己先实现一个生成LOGO的工具了。（是的，像擦出爱情的火花一样突然）&lt;/p&gt;
&lt;p&gt;（至于最开始是想做什么产品，我先按下不表）&lt;/p&gt;
&lt;h2&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;小方格可以设置数量，如：100x100，单个方格可以设置大小，如格尺寸为：5x5&lt;/li&gt;
&lt;li&gt;绘图模式下，鼠标可在绘图区域的小方格上点击并滑动，小方格会被设置对应的颜色
&lt;ul&gt;
&lt;li&gt;颜色可配置，并且方便切换：比如tab键自动轮换预制的几种颜色&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&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;li&gt;如果自己手写一个分析像素值的功能，如何设计采样的步长最合理，最高效？&lt;/li&gt;
&lt;li&gt;&lt;del&gt;不会已经有AI可以直接替代我这个产品了吧？&lt;/del&gt;（想到这里，我狠狠的打了自己一个大嘴巴子！想起了以前各种没出世的小产品，我决不能再找到一个理由不去完成这个产品）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构思到这里，我已经可以在脑子里完整的走完【生成像素LOGO】这个功能。&lt;/p&gt;
&lt;p&gt;这算是一个产品经理立项的过程了，先聆听客户需求-&gt; 再分析和拆解-&gt; 以一个产品的形态实现客户的需求-&gt; 梳理成文档发给研发部门或开项目启动会&lt;/p&gt;
&lt;p&gt;后面可能还会有交底、反交底、UI评审、测试用例评审等等会议，这里出于产品本人即是UI又是开发，在大家的一致赞同下就跳过了。&lt;/p&gt;
&lt;h2&gt;技术栈选择&lt;/h2&gt;
&lt;p&gt;围绕着这些要实现的功能，我作为一个开发，首先想到了两种方向：&lt;/p&gt;
&lt;p&gt;第一个是原生dom。&lt;/p&gt;
&lt;p&gt;没错，在这个卷虚拟dom卷的没边儿的年代，我还能想到用原生dom来实现这个功能，实在是不忘初心，值得一个点赞和关注。&lt;/p&gt;
&lt;p&gt;绘制方格、鼠标事件、控制涂色、导出图片，这些看起来都不是问题。&lt;/p&gt;
&lt;p&gt;问题是：怎么控制这多dom一起缩放和拖拽？&lt;/p&gt;
&lt;p&gt;由于感觉实现起来比较繁琐，不够优雅，加上这方面没有经验，以免被不能解决的问题卡住。&lt;/p&gt;
&lt;p&gt;我开始思考第二个方向：Canvas。&lt;/p&gt;
&lt;p&gt;先找别人做好的轮子。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fabric&lt;/li&gt;
&lt;li&gt;Konva&lt;/li&gt;
&lt;li&gt;Pixi&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在经过不长时间的google之后，我分别尝试了这三个插件，且看到了一个关于这种canvas库的性能对比网站。最终选择了&lt;a href=&quot;http://konvajs-doc.bluehymn.com/docs/index.html&quot;&gt;Konva&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;因为它内置了图层、图形、选择、变形、拖转、鼠标事件、导出图片/json等常用功能，比较是最贴合我的需求的，并且中文文档和英文的Api文档还算友好。&lt;/p&gt;
&lt;p&gt;关于性能，这是一个在技术选型时经常提到的问题。但事实就是，不是每个产品的量级都会发展到那么大，你在考虑几万、几十万、几千万用户或者多么多么复杂的渲染场景的时候，却连第一个真实用户都还没有？更何况我这是个没人用的工具(⊙︿⊙)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为不存在的用户绞尽脑汁的进行性能优化，不如先为第一个用户实现主体功能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;毕竟后续的优化也是KPI嘛&lt;/p&gt;
&lt;p&gt;解决了核心库的选择，剩下的就是围绕项目搭建所用的技术了&lt;/p&gt;
&lt;h3&gt;前端&lt;/h3&gt;
&lt;p&gt;前端技术的选择，秉承着用新不用旧的原则，全部用最新的技术栈，也不考虑兼容问题。所以我选择了以下技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.vuejs.org/&quot;&gt;Vue&lt;/a&gt; 3.x&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://router.vuejs.org/zh/&quot;&gt;Vue-router&lt;/a&gt; 4.x&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.vitejs.dev/guide/&quot;&gt;Vite&lt;/a&gt;4.x&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pinia.vuejs.org/&quot;&gt;Pinia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tailwindcss.cn/docs/installation&quot;&gt;TailwindCss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://konvajs-doc.bluehymn.com/docs/index.html&quot;&gt;Konva&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;后端&lt;/h3&gt;
&lt;p&gt;既然是大前端，肯定优先选择Node，尽量把JS玩明白。&lt;/p&gt;
&lt;p&gt;因为它的应用场景不光是页面或者Api服务，还包括客户端、命令行、各类编辑器插件、某些写作软件的插件、浏览器插件、爬虫、自动化等等。熟练后，使用范围非常广，可玩性非常强。&lt;/p&gt;
&lt;p&gt;并且，司以为，业务层注重的应该是业务逻辑、思维逻辑，并不是你选了什么编程语言做后端，就可以避免屎山代码，就比别的语言优雅。&lt;/p&gt;
&lt;p&gt;所以Node作为一个链接数据库和前端/客户端，提供一定量的稳定服务的编程语言，对我来说足矣。&lt;/p&gt;
&lt;p&gt;再插一嘴选TS还是JS的问题，除了核心问题和选什么语言一样外。在工作上保持一定的挑战性对个人的成长很有帮助，所以建议还想成长的前端er，无脑选TS，&lt;strong&gt;走在薪资的前沿总是没有错的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Node版本选择：&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;我目前是16.20.2 ，16的稳定版， 18、 20 也是可以的，我记得Vite5已经不支持低于18的版本，所以家里有条件的可以直接从20用起了。（偶数版本是稳定版）&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Node框架选择：&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.nestjs.cn/&quot;&gt;Nest&lt;/a&gt; ：Node框架的不二之选。搭配&lt;a href=&quot;https://www.tslang.cn/&quot;&gt;TypeScript&lt;/a&gt;使用，既能学习到后端业务如何设计，又能参考目录接口如何设计，又能感受和使用TypeScript，一石三鸟。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.zzao.club/article/202411191426109.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;服务器&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.aliyun.com/daily-act/ecs/activity_selection?spm=5176.28508143.J_ahRFo5CaAe_asSOaCgS4J.7.5421154akYLvYx&amp;#x26;scm=20140722.M_169282029.P_163.MO_2275-ID_10071131-MID_10071131-CID_30876-ST_9367-V_1&quot;&gt;阿里云服务器&lt;/a&gt;，前一阵搞活动99元/年，还是很划算的。&lt;/p&gt;
&lt;p&gt;牵扯到的备案、域名、Nginx+Https，后续上线篇会展开讲讲。&lt;/p&gt;
&lt;h3&gt;部署方式&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.docker.com/&quot;&gt;Docker&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;用docker跑nginx、redis、mysql，这样本地和服务器环境能尽量保持一致，还能提前验证问题。不至于本地开发完没问题，部署上去又出现各种问题&lt;/p&gt;
&lt;p&gt;至此，一个从产品立项、到技术选型、最后到运维部署的流程已经梳理完成。下一步就是具体实现各个功能了！&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;源于一个突发的灵感，在纠结了它到底对谁有用之后，我决定先上手实现它。&lt;/p&gt;
&lt;p&gt;也许功能对其他人没用，但记录和写作的过程对我很有用。&lt;/p&gt;
&lt;p&gt;如果对新入行的同学，也能有一点点启发和带动作用，这将是我继续写下去的最大动力。&lt;/p&gt;
&lt;p&gt;喜欢的朋友可以点个关注，继续追更后续的技术篇。有任何前端问题想要咨询的同学，也欢迎加VX：523748995，我会尽力为你解答。&lt;/p&gt;
&lt;p&gt;感谢你的阅读，我是枣把儿~&lt;/p&gt;</content:encoded></item></channel></rss>