<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>CodeXun</title>
    <link>https://codexun.com</link>
    <description>汇集编程、AI、产品、科技，探索编程和技术未来</description>
    <language>zh-CN</language>
    <atom:link href="https://codexun.com/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>20+个谷歌高级搜索指令助你轻松开发客户，了解市场趋势</title>
      <link>https://codexun.com/posts/google-advanced-search-tips-foreign-trade</link>
      <guid isPermaLink="true">https://codexun.com/posts/google-advanced-search-tips-foreign-trade</guid>
      <description>精准查找目标客户 1.site: 指令 该指令可将搜索范围限定在特定网站，能帮你精准查找特定网站下的相关信息。 流程：使用site:域名指令，查找特定网站下的内容，如site:linkedin.com 外贸，获取相关公司信息。 2.inurl: 指令 通过此指令可找到网址中包含特定词汇的页面，有助于快速定位相关业务的网</description>
      <content:encoded><![CDATA[<h2><strong>精准查找目标客户</strong></h2><p>1.site: 指令</p><p>该指令可将搜索范围限定在特定网站，能帮你精准查找特定网站下的相关信息。</p><p>流程：使用site:域名指令，查找特定网站下的内容，如site:linkedin.com 外贸，获取相关公司信息。</p><p>2.inurl: 指令</p><p>通过此指令可找到网址中包含特定词汇的页面，有助于快速定位相关业务的网站。</p><p>流程：使用inurl:关键字，查找网址中包含特定词汇的页面，如inurl:export，找到相关出口公司。</p><p>3.intitle: 指令</p><p>能让你搜索到标题中包含特定词汇的页面，对于查找特定主题的信息非常有效。</p><p>流程：使用intitle:关键字，查找标题中包含特定词汇的页面，如intitle:"importer"，获取进口商信息。</p><p>4.filetype: 指令</p><p>专门用于搜索特定文件类型，方便你获取特定格式的文档、报告等资料。</p><p>流程：使用filetype:pdf 关键字，查找特定文件类型，如filetype:pdf "market report"，获取行业报告。</p><h2><strong>邮件和联系方式获取</strong></h2><p>1.“关键词” + “邮箱” 指令</p><p>帮助你搜索与关键词相关的邮件地址，是获取潜在客户联系方式的一种途径。</p><p>流程：使用"外贸" + "email"，查找相关邮件地址。</p><p>2.“公司名” + “联系” 指令</p><p>介绍：快速找到公司的联系方式，方便你与潜在客户或合作伙伴进行沟通。</p><p>流程：输入"公司名" + "contact"，获取公司的联系方式。</p><h2><strong>获取行业数据与趋势</strong></h2><p>1.related: 指令</p><p>可查找与某个网站相关的其他网站，助你发现潜在的竞争对手或合作伙伴。</p><p>流程：使用related:域名，查找与某个网站相关的其他网站，如related:alibaba.com，获取潜在竞争对手。</p><p>2.cache: 指令</p><p>能查看某个网站的快照，方便你获取该网站过往的信息，对于了解网站的历史变化有帮助。</p><p>流程：使用cache:域名，查看某个网站的快照，以获取过往信息。</p><p>3.“关键词” + “国家” 指令</p><p>输入特定产品关键词和国家，可快速获取特定国家对该产品的需求信息，利于针对性开发市场。</p><p>流程：输入"产品" + "国家"，获取特定国家的产品需求信息，如"电子产品" + "Germany"。</p><h2><strong>社交媒体与论坛</strong></h2><p>1.“关键词” + site: 指令</p><p>结合社交媒体网站，能帮你在特定社交平台上查找与关键词相关的讨论，有助于了解市场动态和客户需求。</p><p>流程：结合社交媒体，使用"外贸" site:facebook.com，查找相关社交讨论。</p><p>2.“关键词” + forum 指令</p><p>可快速找到与产品相关的行业论坛，在论坛上你能获取到专业的行业信息和客户反馈。</p><p>流程：使用"产品" + forum，查找与产品相关的行业论坛，如"手工艺品" + forum。</p><p>3.“产品” + “评论” 指令</p><p>通过查找产品的用户评论，能让你了解客户对产品的看法和需求，为产品改进和市场开发提供参考。</p><p>流程：查找产品的用户评论，输入"产品" + "review"，获取客户反馈。</p><h2><strong>了解竞争对手</strong></h2><p>1.“竞争对手名” + “价格” 指令</p><p>可查看竞争对手的产品定价，帮助你制定更有竞争力的价格策略。</p><p>流程：查找竞争对手的定价策略，输入"竞争对手名" + "price"。</p><p>2.“竞争对手名” + “客户评价” 指令</p><p>了解竞争对手的客户反馈，有助于你发现竞争对手的优势和不足，从而优化自己的产品和服务。</p><p>流程：了解竞争对手的客户反馈，输入"竞争对手名" + "reviews"。</p><h2><strong>所有客户的 WhatsApp号码</strong></h2><p>假如你从事假睫毛的行业，想要开拓欧美市场（以美国为例，区号1），按照这个情形，我们来演示一下：</p><p><strong>技巧一：网站上的 WhatsApp。</strong></p><p>使用搜索指令 “intitle:"eyelash" whatsapp+1（区号）”，从标题含关键词 “eyelash” 和 “whatsapp” 以及美国区号 “+1” 的网页中寻找潜在客户的 WhatsApp 号码。</p><p><strong>技巧二：官网上的 WhatsApp。</strong></p><p>“inurl:contact"eyelash" whatsapp+1”，通过这个指令在含关键词 “eyelash”、“whatsapp” 和 “+1” 的 contact 网页中进行搜索。</p><p><strong>技巧三：Facebook 上的 WhatsApp。</strong></p><p>“site:facebook.com"eyelash" whatsapp+1”，在标题含相关关键词和国家区号的 Facebook 网页中挖掘客户信息。</p><p><strong>技巧四：LinkedIn 上的 WhatsApp。</strong></p><p>“site:linkedin.com"eyelash" whatsapp+1”，从 LinkedIn 上的相关网页中寻找目标。</p><p>注意：</p><p>inurl 语法用来搜索包含关键词关键词的相关链接</p><p>intitle是指标题中含有相关关键词。</p><p>site指令可以定位到任何的网址上。</p><p>其他的平台也可以这样使用，另外大家也可以把WhatsApp换成mail，找相关的邮箱地址，进行开发信的发送，利用开发信区开发客户</p><p>此类搜索指令还有很多高级用法，有兴趣的朋友可以进一步探索。</p><p><strong>技巧五：使用谷歌地图</strong></p><p>首先定位到目标国家和城市，定位到你的目标市场。</p><p>然后输入你的行业关键字，比如你做睫毛产品，就可以输入 “eyelash” 等相关关键词。</p><p>地图会为你展示该区域内相关的公司、店铺信息，经过整理验证，就能轻松获取 WhatsApp 号码。</p><p>这个时候信息搜出来可能会比较多，再利用相关的刮刀插件，比如Instant Data ScraperInstant Data Scraper，进行搜刮自动整理也是可以的，很方便。小太阳外贸笔记</p><h2><strong>行业报告与市场调研</strong></h2><p>1.“市场报告” + “产品” 指令</p><p>能快速找到与产品相关的行业报告，为你的市场调研提供有力的数据支持。</p><p>流程：获取行业报告，输入"market report" + "产品"。</p><p>2.“产品” + “趋势” 指令</p><p>助你了解产品的市场趋势，为产品研发和市场推广提供方向。</p><p>流程：查找市场趋势，输入"产品" + "trends"。</p><h2><strong>利用图片和视频</strong></h2><p>1.images: 指令</p><p>使用该指令可搜索相关产品图片，直观地了解产品的外观和特点，也可用于产品推广。</p><p>流程：使用images:关键字，查找相关产品图片，如images:手工艺品，吸引客户。</p><p>2.“关键词” + site:youtube.com 指令</p><p>在视频平台上查找与产品相关的视频资源，视频能更生动地展示产品，有助于吸引客户。</p><p>流程：查找视频资源，输入"产品" + site:youtube.com，获取产品视频介绍。</p><h2><strong>综合搜索技巧</strong></h2><p>1.组合搜索</p><p>将多种指令组合使用，能进一步提高搜索的精准度和效率，满足更复杂的搜索需求。</p><p>流程：结合多种指令，如site:linkedin.com "外贸" "中国"，精确查找相关人士。</p><p>2.利用引号和减号</p><p>引号可确保搜索词的精确匹配，减号能排除不相关的结果，使搜索结果更符合你的需求。</p><p>流程：使用引号"确保搜索词的精确匹配，使用减号-排除不相关的结果，如"外贸" -公司名。</p><p>通过以上 20 种谷歌高级搜索指令，我们可以更加高效地开发国外客户，获取市场信息，优化自己的客户开发策略。</p><p>希望这些方法能为你的外贸工作提供新的思路和灵感！</p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Claude Code 最佳实践指南</title>
      <link>https://codexun.com/posts/claude-code-best-practices-guide</link>
      <guid isPermaLink="true">https://codexun.com/posts/claude-code-best-practices-guide</guid>
      <description>前言 本文档的核心目标：帮助团队成员高效使用 Claude Code。 内容以 官方最佳实践 为主线，穿插 Claude Code 之父 Boris Cherny 在 X 上分享的真实工作流（标注为 Boris Pro Tip），并从团队实践中提取实用模板。 一个核心约束贯穿全文：Claude 的上下文窗口（~200K</description>
      <content:encoded><![CDATA[<h2><strong>前言</strong></h2>
<p>本文档的核心目标：帮助团队成员<strong>高效使用 Claude Code</strong>。</p>
<p>内容以 <a href="https://code.claude.com/docs/en/best-practices">官方最佳实践</a> 为主线，穿插 Claude Code 之父 <strong>Boris Cherny</strong> 在 X 上分享的真实工作流（标注为 <strong>Boris Pro Tip</strong>），并从团队实践中提取实用模板。</p>
<p><strong>一个核心约束贯穿全文</strong>：Claude 的上下文窗口（~200K tokens）会快速填满，而填满后性能会下降。几乎所有最佳实践都围绕这个约束展开。</p>
<h2><strong>1. Claude Code 的核心交互模型 🟢</strong></h2>
<p>当你给 Claude 一个任务时，它会经历三个阶段：<strong>收集上下文</strong>、<strong>采取行动</strong>和<strong>验证结果</strong>。这些阶段相互融合。Claude 始终使用工具，无论是搜索文件以了解你的代码、编辑以进行更改，还是运行测试以检查其工作。</p>
<p><strong>你的角色</strong>：</p>
<table>
<thead>
<tr>
<th><strong>你做什么</strong></th>
<th><strong>Claude 做什么</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>通过 @ 引用文件，帮 Claude 看见正确的上下文</td>
<td>阅读文件、搜索代码、理解架构</td>
</tr>
<tr>
<td>用自然语言描述需求，帮 Claude 思考正确的方向</td>
<td>分析问题、规划方案、评估风险</td>
</tr>
<tr>
<td>配置权限，让 Claude 能够行动</td>
<td>编辑文件、运行测试、执行命令</td>
</tr>
</tbody>
</table>
<h3><strong>@ 和 ! 速览</strong></h3>
<table>
<thead>
<tr>
<th><strong>符号</strong></th>
<th><strong>作用</strong></th>
<th><strong>示例</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>@</td>
<td>感知：将文件/资源注入上下文</td>
<td>解释 @src/auth.ts 的逻辑</td>
</tr>
<tr>
<td>!</td>
<td>行动：在提示框中直接执行 Shell</td>
<td>! git log --oneline -5（结果注入上下文）</td>
</tr>
</tbody>
</table>
<pre><code class="language-shell"># @ 的常见用法
&gt; 解释 @src/auth.ts 的逻辑              # 引用单个文件
&gt; 对比 @old-api.ts 和 @new-api.ts       # 引用多个文件
&gt; @src/components 的结构是什么？          # 引用目录

# ! 的常见用法
&gt; ! git diff --stat                       # 执行命令，结果注入上下文
&gt; ! npm test 2&gt;&amp;1 | tail -20              # 运行测试，截取尾部
</code></pre>
<h2><strong>2. Effort Level 🟢</strong></h2>
<p>Opus 4.6 引入了 <strong>Adaptive Thinking</strong>（自适应思考）——Claude 会根据任务复杂度自动决定是否以及多少使用深度推理。你通过 <strong>effort level</strong> 来控制推理深度，而不再需要在提示词中写 <code>think hard</code> 或 <code>ultrathink</code> 等关键词。</p>
<p><strong>三个等级：</strong></p>
<table>
<thead>
<tr>
<th><strong>Effort Level</strong></th>
<th><strong>推理行为</strong></th>
<th><strong>适用场景</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>High（默认）</td>
<td>Claude 几乎总是进行深度思考</td>
<td>复杂架构设计、多文件重构、疑难 Bug</td>
</tr>
<tr>
<td>Medium</td>
<td>适度思考，简单问题可能跳过</td>
<td>日常编码、跨文件修改、中等复杂度任务</td>
</tr>
<tr>
<td>Low</td>
<td>最小化思考，优先速度</td>
<td>简单问答、格式化、小修改</td>
</tr>
</tbody>
</table>
<blockquote>
<p>💡 API 层面还支持 <strong>max</strong> 等级（仅 Opus 4.6），思考无上限。Claude Code CLI 目前暴露 low/medium/high 三级。</p>
</blockquote>
<p><strong>配置方式（三选一）：</strong></p>
<pre><code class="language-shell"># 方式 1：在 /model 菜单中用 ← → 箭头键调节滑块
/model

# 方式 2：环境变量
export CLAUDE_CODE_EFFORT_LEVEL=low    # low | medium | high

# 方式 3：settings.json
{
  &quot;effortLevel&quot;: &quot;high&quot;
}
</code></pre>
<h2><strong>3. 项目目录结构全景 🟢</strong></h2>
<p>一个完整的 Claude Code 项目配置结构：</p>
<pre><code class="language-shell">your-project/
├── CLAUDE.md                    # 📋 项目级指令（团队共享，提交到 Git）
├── CLAUDE.local.md              # 👤 个人项目偏好（自动 gitignore）
├── .claude/
│   ├── settings.json            # ⚙️ 项目设置（团队共享）
│   ├── settings.local.json      # 👤 个人项目设置（gitignore）
│   ├── CLAUDE.md                # 📋 等效于根目录 CLAUDE.md
│   ├── rules/                   # 📏 模块化规则文件
│   │   ├── code-style.md        #    代码风格
│   │   ├── testing.md           #    测试规范
│   │   └── security.md          #    安全要求
│   ├── agents/                  # 🤖 自定义子代理
│   │   ├── code-reviewer.md
│   │   └── debugger.md
│   ├── skills/                  # ⚡ 自定义技能
│   │   └── fix-issue/
│   │       └── SKILL.md
│   └── worktrees/               # 🌳 Git Worktree 目录（加入 .gitignore）
├── .mcp.json                    # 🔌 项目级 MCP 服务器配置
└── .github/
    └── workflows/
        └── claude.yml           # 🔄 Claude Code GitHub Actions
</code></pre>
<blockquote>
<p><strong>新手提示</strong>：刚开始只需要 <code>CLAUDE.md</code> 和 <code>.claude/settings.json</code>。其他配置随着需求逐步添加。</p>
</blockquote>
<h2><strong>4. 快速验证配置 🟢</strong></h2>
<pre><code class="language-shell">cd your-project
claude

# 在 Claude Code 中运行
&gt; /init          # 自动生成 CLAUDE.md
&gt; /config        # 查看/修改全局配置
&gt; /permissions   # 查看/修改权限规则
&gt; /cost          # 查看当前会话 token 用量
&gt; /context       # 查看上下文消耗分布
</code></pre>
<h2><strong>5. 你的第一次对话 🟢</strong></h2>
<p>安装完成后，试试这些命令快速上手：</p>
<pre><code class="language-shell"># 探索项目（最安全的开始方式）
&gt; 给我一个这个代码库的概览

# 理解代码
&gt; 解释 @src/main.ts 的主要逻辑

# 做一个小修改
&gt; 把 @src/utils/format.ts 中的 formatDate 函数改为支持 ISO 8601 格式

# 验证修改
&gt; 运行测试确认修改没有破坏任何东西
</code></pre>
<h2><strong>6. 提示词结构图 🟡</strong></h2>
<p>一个高质量提示词的结构：</p>
<pre><code class="language-shell">┌─────────────────────────────────────────────┐
│  1. 任务描述                                 │
│     做什么？（一句话清晰描述）                 │
├─────────────────────────────────────────────┤
│  2. 上下文                                   │
│     相关文件：@path/to/files                  │
│     参考模式：@path/to/example                │
│     背景信息：为什么要做这个                    │
├─────────────────────────────────────────────┤
│  3. 约束                                     │
│     不能做什么 / 必须满足什么                   │
│     &quot;不引入新依赖&quot;、&quot;保持向后兼容&quot;              │
├─────────────────────────────────────────────┤
│  4. 验证标准                                  │
│     怎么确认做对了？                           │
│     &quot;运行 npm test&quot;、&quot;截图对比&quot;               │
└─────────────────────────────────────────────┘
</code></pre>
<h2><strong>7. 结构化提示模板 🟡</strong></h2>
<h3><strong>功能开发模板</strong></h3>
<pre><code class="language-shell">实现 [功能描述]。

上下文：
- 相关文件：@path/to/relevant/files
- 参考已有模式：@path/to/similar/implementation

要求：
1. [具体要求 1]
2. [具体要求 2]
3. [具体要求 3]

验证：
- 运行 `npm test` 确保所有测试通过
- 运行 `npm run typecheck` 确保无类型错误
</code></pre>
<h3><strong>Bug 修复模板</strong></h3>
<pre><code class="language-shell">修复 [问题描述]。

复现步骤：
1. [步骤 1]
2. [步骤 2]
3. [出现错误]

错误信息：
[粘贴完整错误信息或堆栈跟踪]

期望行为：[描述正确行为]

请：
1. 找到根因
2. 写一个能复现问题的失败测试
3. 修复问题
4. 确认测试通过
</code></pre>
<h3><strong>代码审查模板</strong></h3>
<pre><code class="language-shell">审查 @path/to/file 的以下方面：
- 安全漏洞（注入、XSS、认证问题）
- 边界情况处理
- 性能问题
- 与项目现有模式的一致性

对每个问题给出：
1. 问题严重度（Critical / Warning / Suggestion）
2. 具体位置（文件名和行号）
3. 修复建议
</code></pre>
<h2><strong>8. 采访模式：让 Claude 采访你 🟡</strong></h2>
<p>对于大型功能，让 Claude 先采访你以明确需求，而不是一开始就写代码。</p>
<h3><strong>AskUserQuestion：采访模式的秘密武器</strong></h3>
<p><code>AskUserQuestion</code> 是 Claude Code 内置的一个交互工具。当 Claude 需要你做决策时，它会弹出<strong>结构化的选择题界面</strong>——不需要你打字组织语言，只需点击选项即可。</p>
<p>这个工具有时会被 Claude 自动触发，但你也可以<strong>显式要求</strong>使用它。</p>
<h3><strong>实战演示：用苏格拉底式提问对齐需求</strong></h3>
<p>假设你要做一个「与众不同的小游戏」，在 Claude Code 中输入：</p>
<pre><code class="language-shell">我想开发一款独特的小游戏，但具体做什么、怎么做还没想好。

请你作为游戏策划顾问，用苏格拉底式提问法帮我从零厘清思路。要求：

 - 必须使用 AskUserQuestion 工具向我提问，不要用纯文字提问
 - 每轮提问后，根据我的回答总结洞察，再发起下一轮提问
 - 至少覆盖以下维度：游戏类型、核心玩法、美术风格、目标平台、技术方案
 - 灵活运用 single_select、multi_select、rank_priorities 三种题型
 - 3-5 轮提问结束后，输出一份「游戏设计一页纸」

从第一轮开始吧。
</code></pre>
<h2><strong>9. 编写有效的 <a href="http://CLAUDE.md">CLAUDE.md</a> 🟢</strong></h2>
<p><a href="http://CLAUDE.md">CLAUDE.md</a> 是一个特殊文件，Claude 在每次会话开始时读取它。写入 Bash 命令、代码风格、工作流规则等<strong>Claude 无法从代码中推断</strong>的信息。</p>
<h3><strong>什么该写，什么不该写</strong></h3>
<table>
<thead>
<tr>
<th><strong>该写 ✅</strong></th>
<th><strong>不该写 ❌</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude 猜不到的 Bash 命令</td>
<td>Claude 读代码就能知道的信息</td>
</tr>
<tr>
<td>与默认不同的代码风格规则</td>
<td>标准语言规范（Claude 已知）</td>
</tr>
<tr>
<td>测试指令和首选测试框架</td>
<td>详细的 API 文档（链接即可）</td>
</tr>
<tr>
<td>仓库约定（分支命名、PR 格式）</td>
<td>频繁变化的信息</td>
</tr>
<tr>
<td>项目特有的架构决策</td>
<td>长篇教程或解释</td>
</tr>
<tr>
<td>开发环境怪癖（必需的环境变量）</td>
<td>逐文件的代码库描述</td>
</tr>
<tr>
<td>常见陷阱和非显而易见的行为</td>
<td>&quot;写干净的代码&quot;之类的废话</td>
</tr>
</tbody>
</table>
<p><strong>格式自由但保持精炼</strong>，例如：</p>
<pre><code class="language-shell"># Code style
- Use ES modules (import/export), not CommonJS (require)
- Destructure imports when possible

# Workflow
- Be sure to typecheck when you're done making a series of code changes
- Prefer running single tests, not the whole test suite, for performance
</code></pre>
<h3><strong><a href="http://CLAUDE.md">CLAUDE.md</a> 模板集 🟢</strong></h3>
<h4><strong>最小可用模板</strong></h4>
<pre><code class="language-shell"># 构建
- 安装：`npm install`
- 测试：`npm test`
- Lint：`npm run lint`

# 规范
- TypeScript strict 模式
- 提交消息使用 Conventional Commits
</code></pre>
<h4><strong>通用项目模板</strong></h4>
<pre><code class="language-shell"># 项目：[项目名]
[一句话描述]。技术栈：[列出关键技术]。

# 构建与测试
- 安装：`pnpm install`
- 开发：`pnpm dev`
- 构建：`pnpm build`
- 测试全部：`pnpm test`
- 测试单个：`pnpm test -- path/to/test`
- 类型检查：`pnpm typecheck`
- Lint：`pnpm lint`

# 代码规范
- 使用 ES modules（import/export）
- 函数参数 &gt;3 个时使用对象参数
- 错误处理使用 AppError 类（@src/lib/errors.ts）
- API 路径 kebab-case，JSON 属性 camelCase

# 架构
- 状态管理：Zustand（不是 Redux）
- ORM：Drizzle（不是 Prisma）
- API：tRPC

# 工作流
- **IMPORTANT**: 修改代码后运行 `pnpm typecheck`
- **NEVER**: 不要修改 migrations/ 下的已有文件
- 提交遵循 Conventional Commits

# 压缩指令
When compacting, preserve:
- 修改过的文件完整列表
- 测试命令和结果
- 未完成的任务
</code></pre>
<h4><strong>前端项目模板</strong></h4>
<pre><code class="language-shell"># 构建命令
- 开发：`pnpm dev`（端口 3000）
- 构建：`pnpm build`
- 测试：`pnpm test`（Vitest）
- E2E：`pnpm e2e`（Playwright）

# 代码规范
- 函数式组件 + hooks
- Tailwind CSS（不用 CSS modules）
- 导入顺序：React → 第三方 → 本地 → 类型 → 样式

# 组件结构
- 页面：`src/app/`（Next.js App Router）
- 组件：`src/components/`
- 参考：`src/components/ui/Button.tsx`

# 测试
- 优先运行单个测试文件
- UI 变更后截图对比验证
</code></pre>
<h2><strong>10. 配置权限 🟡</strong></h2>
<p>默认情况下，Claude Code 对可能修改系统的操作请求权限。这很安全但频繁打断你。</p>
<h3><strong>权限允许列表</strong></h3>
<pre><code class="language-shell">{
  &quot;permissions&quot;: {
    &quot;allow&quot;: [
      &quot;Bash(npm run lint)&quot;,
      &quot;Bash(npm run test *)&quot;,
      &quot;Bash(git commit *)&quot;,
      &quot;Bash(git push *)&quot;
    ],
    &quot;ask&quot;: [
      &quot;Bash(git push --force *)&quot;
    ],
    &quot;deny&quot;: [
      &quot;Read(./.env)&quot;,
      &quot;Read(./.env.*)&quot;,
      &quot;Read(./secrets/**)&quot;,
      &quot;Bash(curl *)&quot;,
      &quot;Bash(rm -rf *)&quot;
    ]
  }
}
</code></pre>]]></content:encoded>
      <category>AI</category>
      <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>OpenClaw Windows 环境部署完整教程</title>
      <link>https://codexun.com/posts/openclaw-windows-setup-guide</link>
      <guid isPermaLink="true">https://codexun.com/posts/openclaw-windows-setup-guide</guid>
      <description>本教程详细介绍如何在 Windows 系统上本地部署 OpenClaw（原 Clawdbot/Moltbot）AI 助手。 项目地址 OpenClaw 是 2026 年 GitHub 上增长最快的开源 AI 助手项目，核心能力包括： 读写本地文件、执行终端命令、运行脚本（PowerShell/Batch）；Chrome</description>
      <content:encoded><![CDATA[<p>本教程详细介绍如何在 Windows 系统上本地部署 OpenClaw（原 Clawdbot/Moltbot）AI 助手。</p><p></p><img src="/api/images/image/2026/07/f78e6e55a44246c5-image.png" alt="image.png" width="50%" data-align="center" data-width-percent="50" style="display: block; max-width: 100%; width: 50%; margin-left: auto; margin-right: auto;"><h2><strong>项目地址</strong></h2><p><strong>OpenClaw</strong>&nbsp;是 2026 年 GitHub 上增长最快的开源 AI 助手项目，核心能力包括：</p><p>读写本地文件、执行终端命令、运行脚本（PowerShell/Batch）；Chrome/Edge 自动化、网页截图、数据提取；Outlook 邮件读取、日历事件管理、待办事项；Office等办公软件操作；WhatsApp、Telegram、Discord、Slack、企业微信、飞书、钉钉等多渠道交互</p><ul class="tight" data-tight="true"><li><p><strong>项目地址</strong>：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/openclaw/openclaw">https://github.com/openclaw/openclaw</a></p></li><li><p><strong>官网</strong>：<a target="_blank" rel="noopener noreferrer nofollow" href="https://openclaw.ai">https://openclaw.ai</a></p></li></ul><h2><strong>WSL2 + Ubuntu 部署（强烈推荐）</strong></h2><p>这是官方推荐的 Windows 部署方式，提供最完整的 Linux 环境支持。</p><h3><strong>1. 启用 WSL2</strong></h3><p><strong>以管理员身份打开 PowerShell</strong>，执行：</p><pre><code class="language-powershell"># 启用 WSL 功能
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

# 设置 WSL 2 为默认版本
wsl --set-default-version 2
</code></pre><p><strong>重启计算机</strong>。</p><h3><strong>2. 安装 Ubuntu</strong></h3><p><strong>方法 ：Microsoft Store 安装（推荐）</strong></p><ol class="tight" data-tight="true"><li><p>打开 Microsoft Store</p></li><li><p>搜索「Ubuntu 22.04 LTS」或「Ubuntu 24.04 LTS」</p></li><li><p>点击「获取」并安装</p></li><li><p>首次启动设置用户名和密码</p></li></ol><p>安装完成后会自动打开 Ubuntu 终端，按提示设置用户名和密码。</p><h3><strong>3. 更新 Ubuntu 系统</strong></h3><p>在 Ubuntu 终端中执行：</p><pre><code class="language-shell"># 更新软件包列表
sudo apt update &amp;&amp; sudo apt upgrade -y

# 安装基础工具
sudo apt install -y curl git wget build-essential
</code></pre><h3><strong>4. 安装 Node.js 22+</strong></h3><pre><code class="language-shell"># 添加 NodeSource 仓库
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -

# 安装 Node.js
sudo apt install -y nodejs

# 验证版本（必须 ≥ 22.x）
node -v
npm -v

</code></pre><h3><strong>5. 安装 OpenClaw</strong></h3><p><strong>方法 A：一键脚本安装</strong></p><pre><code class="language-shell">curl -fsSL https://openclaw.ai/install.sh | bash
</code></pre><h3><strong>6. 验证安装</strong></h3><pre><code class="language-shell"># 查看版本
openclaw --version

# 查看帮助
openclaw --help

# 查看系统状态
openclaw status
</code></pre><h3><strong>7. 配置 Windows 访问 WSL2 服务</strong></h3><p>由于 OpenClaw 运行在 WSL2 中，需要配置端口转发以便 Windows 访问。</p><p><strong>创建启动脚本</strong>&nbsp;<code>start-openclaw.bat</code>：</p><pre><code class="language-shell">@echo&nbsp;off
echo&nbsp;Starting OpenClaw Gatewayin&nbsp;WSL2...
wsl -d Ubuntu-22.04 -u root service openclaw start
timeout /t 3
start http://localhost:18789
</code></pre><p>或直接在 WSL2 中启动：</p><pre><code class="language-shell"># 在 WSL2 Ubuntu 终端中
openclaw gateway run --port 18789
</code></pre><p>然后在 Windows 浏览器访问&nbsp;<code>http://localhost:18789</code></p><h2><strong>PowerShell 原生部署</strong></h2><p>适合不想使用 WSL2 的纯 Windows 用户。</p><h3><strong>1. 安装 Node.js 22+</strong></h3><p><strong>方法 ：官网下载安装</strong></p><ol class="tight" data-tight="true"><li><p>访问 <a target="_blank" rel="noopener noreferrer nofollow" href="https://nodejs.org/zh-cn">https://nodejs.org/zh-cn</a></p></li><li><p>下载 Windows 安装包（LTS 版本 22.x）</p></li><li><p>运行安装程序，勾选「自动安装必要的工具」</p></li></ol><h3><strong>2. 验证 Node.js 安装</strong></h3><pre><code class="language-shell"># 打开 PowerShell
node -v
npm -v
</code></pre><h3><strong>3. 以管理员身份安装 OpenClaw</strong></h3><p><strong>重要</strong>：必须以<strong>管理员身份</strong>运行 PowerShell。</p><pre><code class="language-shell"># 安装最新稳定版
npm install -g openclaw@latest

# 或安装汉化版
npm install -g @qingchencloud/openclaw-zh@latest

</code></pre><h3><strong>4. 解决安装权限问题</strong></h3><p>如果遇到权限错误：</p><pre><code class="language-shell"># 方法 A：启用 PowerShell 脚本执行
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

# 方法 B：修改 npm 安装目录
npm configset&nbsp;prefix"C:\npm"
npm configset&nbsp;cache"C:\npm-cache"

# 将目录添加到 PATH
[Environment]::SetEnvironmentVariable("Path",$env:Path +";C:\npm","User")
</code></pre><h3><strong>5. 验证安装</strong></h3><pre><code class="language-plain">openclaw --version
openclaw --help
</code></pre><h3><strong>6. 解决常见问题</strong></h3><p><strong>问题：sharp 模块加载失败</strong></p><pre><code class="language-plain"># 清理 npm 缓存
npm cache clean --force

# 重新安装
npm install -g openclaw@latest --force
</code></pre><p><strong>问题：Windows Defender 阻止</strong></p><p>将 OpenClaw 安装目录添加到 Windows Defender 排除项：</p><pre><code class="language-shell">C:\Users\你的用户名\AppData\Roaming\npm
C:\Users\你的用户名\.openclaw
</code></pre><h2><strong>初始化配置</strong></h2><p>安装完成后，需要运行初始化向导。</p><h3><strong>启动初始化向导</strong></h3><pre><code class="language-shell">openclaw onboard --install-daemon
</code></pre><h3><strong>配置 AI 模型提供商</strong></h3><p>OpenClaw 需要对接 AI 模型才能工作。</p><p><strong>以配置Anthropic Claude（推荐）为例：</strong></p><pre><code class="language-plain"># WSL2 或 PowerShell
openclaw models auth add anthropic

# 输入 API Key: sk-ant-xxx
</code></pre><h3><strong>绑定消息渠道</strong></h3><p><strong>1. Telegram创建 Bot</strong>：</p><ol class="tight" data-tight="true"><li><p>在 Telegram 搜索&nbsp;<code>@BotFather</code></p></li><li><p>发送&nbsp;<code>/newbot</code>&nbsp;创建机器人</p></li><li><p>保存 Bot Token</p></li></ol><p><strong>配置</strong>：</p><pre><code class="language-shell">openclaw channels add telegram
openclaw configset&nbsp;channels.telegram.botToken"your-bot-token"
openclaw gateway restart
</code></pre><p><strong>2. WhatsApp</strong></p><pre><code class="language-plain"># 登录 WhatsApp（显示二维码）
openclaw channels login whatsapp

# 用手机 WhatsApp 扫码
</code></pre><p><strong>3. 企业微信（国内推荐）</strong></p><pre><code class="language-plain"># 安装企业微信插件
openclaw plugins install @m1heng-clawd/wework

# 配置
openclaw configset&nbsp;channels.wework'{"enabled":true,"corpId":"xxx","agentSecret":"xxx"}'&nbsp;--json

</code></pre><p><strong>4. 飞书（国内推荐）</strong></p><pre><code class="language-plain"># 安装飞书插件
openclaw plugins install @m1heng-clawd/feishu

# 配置
openclaw configset&nbsp;channels.feishu'{"enabled":true,"appId":"cli_xxx","appSecret":"xxx"}'&nbsp;--json
</code></pre><h2><strong>常用命令速查</strong></h2><h3><strong>系统管理</strong></h3><table class="tiptap-table" style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>命令</strong></p></th><th colspan="1" rowspan="1"><p><strong>功能</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw --version</code></p></td><td colspan="1" rowspan="1"><p>查看版本</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw status</code></p></td><td colspan="1" rowspan="1"><p>查看系统状态</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw health</code></p></td><td colspan="1" rowspan="1"><p>健康检查</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw update</code></p></td><td colspan="1" rowspan="1"><p>更新 OpenClaw</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw doctor</code></p></td><td colspan="1" rowspan="1"><p>诊断系统问题</p></td></tr></tbody></table><h3><strong>配置管理</strong></h3><table class="tiptap-table" style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>命令</strong></p></th><th colspan="1" rowspan="1"><p><strong>功能</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw onboard</code></p></td><td colspan="1" rowspan="1"><p>初始化向导</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw configure</code></p></td><td colspan="1" rowspan="1"><p>交互式配置</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw config get &lt;key&gt;</code></p></td><td colspan="1" rowspan="1"><p>查看配置项</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw config set &lt;key&gt; &lt;value&gt;</code></p></td><td colspan="1" rowspan="1"><p>修改配置项</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>openclaw config unset &lt;key&gt;</code></p></td><td colspan="1" rowspan="1"><p>删除配置项</p></td></tr></tbody></table>]]></content:encoded>
      <category>AI</category>
      <pubDate>Fri, 06 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Antigravity tools 使用教程，反代理免费使用Claude Code 模型</title>
      <link>https://codexun.com/posts/antigravity-tools-usage-guide</link>
      <guid isPermaLink="true">https://codexun.com/posts/antigravity-tools-usage-guide</guid>
      <description>Google Antigravity（Google AI Pro），出手相当阔绰：免费给 Opus 4.5 和 Gemini 3.0 Pro 的额度。唯一的不足是——谷歌把这些模型锁在自家的网页端 IDE 里，只能在反重力 IDE 中使用。 下面介绍以下通过“反向代理”，把谷歌网页端的免费额度，伪装成标准 API 给本</description>
      <content:encoded><![CDATA[<p>Google Antigravity（Google AI Pro），出手相当阔绰：免费给 Opus 4.5 和 Gemini 3.0 Pro 的额度。唯一的不足是——谷歌把这些模型锁在自家的网页端 IDE 里，只能在反重力 IDE 中使用。</p>
<p>下面介绍以下通过“反向代理”，把谷歌网页端的免费额度，伪装成标准 API 给本地工具用。还能多账号轮询。</p>
<h2>Antigravity Tools工具介绍</h2>
<p>这是一个把 Antigravity 里面的模型转成标准 API，给 Claude Code 等 Coding Agent 接入的智能代理项目。提供多账号管理、协议转换和智能请求调度等功能，让你能稳定、低成本地在 Claude Code、Codex 中使用 gemini / claude 系列模型。</p>
<p>项目地址：‣</p>
<h2>如何使用？</h2>
<ol>
<li>访问 Antigravity Tools 代码仓库，按照指示安装 Antigravity Tools 桌面软件</li>
<li>在 Antigravity Tools 桌面软件添加账号，打开浏览器通过谷歌账号登录 Antigravity</li>
</ol>
<p><img src="/api/images/image/2026/07/681326f37916dd18-image.png" alt="image.png"></p>
<ol start="3">
<li>在终端配置环境变量，让 Claude Code 使用自定义的 API 端点
在 API 反代的 tab 界面，找到 API 密钥。然后配置到claude code中。</li>
</ol>
<p><img src="/api/images/image/2026/07/3e3d706714902b67-image.png" alt="image.png"></p>
<p>export ANTHROPIC_API_KEY=&quot;sk-xxx&quot;
export ANTHROPIC_BASE_URL=&quot;<a href="http://127.0.0.1:8045">http://127.0.0.1:8045</a>&quot;</p>
<ol start="4">
<li>打开 Claude Code 发送指令，开始使用 Claude Code</li>
</ol>
<h2>配合 cc-switch 工具简单配置使用</h2>
<p>cc-switch是一款跨平台的 Claude Code / Codex / Gemini CLI 辅助工具，核心功能包括：一键切换 API 供应商配置、速度测试、MCP服务器管理、Skills 技能管理、Prompts 提示词预设切换。</p>
<p>是当前比较方便管理Claude Code / Codex / Gemini CLI 这些 vibe coding 配置的图形工具。</p>
<p>项目地址：‣</p>
<p><img src="/api/images/image/2026/07/7d181c137a2e5467-image.png" alt="image.png"></p>
<p>最后，Antigravity Tools 这个项目目前只提供桌面版软件，不支持 Web 应用，不能通过服务器部署做 API 中转站，仅供自己在本地使用，一定程度降低了对 Claude、ChatGPT 的账号依赖。</p>
<p>当然，近量还是使用官方或一些第三方的 Claude code API，毕竟这个方法还是有一些“邪修”，稳定性和账号安全不能够保证，可以作为一个临时的应急方法。</p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Thu, 05 Feb 2026 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>vscode系列编辑器的窗口字体大小设置</title>
      <link>https://codexun.com/posts/vscode-window-font-size-settings</link>
      <guid isPermaLink="true">https://codexun.com/posts/vscode-window-font-size-settings</guid>
      <description>记录一些vscode 中的设置。 一、调整滚轮的灵敏度 VS Code 中确实可以调整滚轮的灵敏度。你可以按照以下步骤来把这一项调慢： 打开设置： 在 Mac 上按 Cmd + , (逗号)。 或者直接点击左下角的齿轮图标 -&gt; Settings (设置)。 搜索配置项： 在搜索框中输入 scroll sensitiv</description>
      <content:encoded><![CDATA[<p>记录一些vscode 中的设置。</p>
<p><img src="/api/images/image/2026/07/67a02c3022fcc171-image.png" alt="image.png"></p>
<h2>一、调整滚轮的灵敏度</h2>
<p>VS Code 中确实可以调整滚轮的灵敏度。你可以按照以下步骤来把这一项调慢：</p>
<ol>
<li><strong>打开设置</strong>：
<ul>
<li>在 Mac 上按 Cmd + , (逗号)。</li>
<li>或者直接点击左下角的齿轮图标 -&gt; <strong>Settings</strong> (设置)。</li>
</ul>
</li>
<li><strong>搜索配置项</strong>：
<ul>
<li>在搜索框中输入 <strong>scroll sensitivity</strong>。</li>
</ul>
</li>
<li><strong>调整数值</strong>：
<ul>
<li>找到 <strong>&quot;Editor: Mouse Wheel Scroll Sensitivity&quot;</strong> (编辑器：鼠标滚轮滚动灵敏度)。</li>
<li>默认值通常是 1。</li>
<li>想要<strong>调慢</strong>，可以将其设置为小于 1 的小数，例如 0.8 。你可以在编辑器里试着滚动一下，找到适合你的速度。</li>
</ul>
</li>
</ol>
<p>(顺便提一下，如果你是指按住 Alt 或 Option 键时的快速滚动，也可以搜 fast scroll 来调整那个特定的的倍率。)</p>
<h2>二、调整文件底部的空白区域</h2>
<p>在 VS Code 中，调整文件底部的空白区域主要涉及到两个设置。你可以根据自己的喜好来组合使用：</p>
<p><strong>方法一：开关“无限滚动”（彻底去掉或是保留大片空白）</strong></p>
<p>这是最常用的设置。默认情况下，VS Code 允许你把最后一行代码滚动到屏幕的最顶端，这会导致底部出现大片空白。</p>
<ol>
<li>打开设置 Cmd + , (逗号)。</li>
<li>搜索 <code>**scroll beyond**</code>。</li>
<li>找到 <strong>&quot;Editor: Scroll Beyond Last Line&quot;</strong> (编辑器：滚动越过最后一行)。
<ul>
<li><strong>取消勾选</strong>：滚动条到底时，最后一行代码会停在屏幕<strong>最底部</strong>（没有大片空白）。</li>
<li><strong>勾选（默认）</strong>：许滚动超过最后一行，直到最后一行显示在窗口顶部。</li>
</ul>
</li>
</ol>
<p><strong>方法二：微调底部留白大小（精确控制像素）</strong></p>
<p>如果你选择了<strong>取消勾选</strong>上面的选项，但觉得代码贴着底边太挤，或者你想在这个基础上增加一点点固定的缓冲区，可以用这个设置：</p>
<ol>
<li>打开设置 Cmd + , (逗号)。</li>
<li>搜索 <code>**padding bottom**</code>。</li>
<li>找到 <strong>&quot;Editor › Padding: Bottom&quot;</strong>。</li>
<li>输入一个数值（例如 10、50 或 100）。
<ul>
<li>这会在文件内容的最后一行下方增加指定像素的空白区域。</li>
</ul>
</li>
</ol>
<p>总结建议： 如果你想要“适度”的空白，建议取消勾选 Scroll Beyond Last Line，然后将 Padding: Bottom 设置为 100 左右。这样既不会无限滚动，也不会让代码贴底。</p>
<h2>三、VS Code <strong>设置</strong>侧边栏字体大小</h2>
<p>VS Code <strong>不支持单独设置</strong>侧边栏（资源管理器）的字体大小。它的设计逻辑是整个窗口（UI）的缩放是统一的。</p>
<p>不过，你可以通过<strong>组合调整</strong>来达到想要的效果：</p>
<p><strong>方法：缩放整个界面 + 调小编辑器字体</strong></p>
<p>如果你觉得侧边栏字太小，可以先把<strong>整个 VS Code 界面放大</strong>，然后再把<strong>代码编辑区的字体单独调小</strong>回你习惯的大小。</p>
<ol>
<li><strong>放大整个界面（包括侧边栏）</strong>：
<ul>
<li>快捷键： Cmd + , (逗号)。</li>
<li>或者在设置里搜索 window.zoomLevel，将其设置为 1 或更高。</li>
<li><em>此时你会发现侧边栏变大了，但写代码的地方也变大了。</em></li>
</ul>
</li>
<li><strong>把代码字体调回去</strong>：
<ul>
<li>打开设置  Cmd + , (逗号)。</li>
<li>搜索 editor.fontSize。</li>
<li>将数值<strong>调小</strong>（比如你放大了一级界面，可能需要把原本 14px 的字体改成 12px 左右），直到代码看起来顺眼为止。</li>
</ul>
</li>
</ol>
<p>这样就能实现“侧边栏字变大，代码字不变”的效果了。</p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Sat, 20 Dec 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>2025 年出海独立开发者穷鬼套餐【建议保存】，用免费增值（Freem)ium）撬动全球市场</title>
      <link>https://codexun.com/posts/2025-indie-developer-freemium-global-toolkit</link>
      <guid isPermaLink="true">https://codexun.com/posts/2025-indie-developer-freemium-global-toolkit</guid>
      <description>从今年年初至今，一直在实践面向海外的独立开发。 对于预算有限、单枪匹马闯荡全球市场的独立开发者而言，“穷鬼套餐”并非意味着牺牲品质，而是精明地利用生态系统中成熟的免费SaaS（软件即服务）和基础设施，实现“零成本”启动和低成本运营。 核心构成： 这套方案的核心思想是：在产品获得市场验证和稳定收入之前，将固定成本降至无限</description>
      <content:encoded><![CDATA[<p>从今年年初至今，一直在实践面向海外的独立开发。</p>
<p>对于预算有限、单枪匹马闯荡全球市场的独立开发者而言，“穷鬼套餐”并非意味着牺牲品质，而是精明地利用生态系统中成熟的免费SaaS（软件即服务）和基础设施，实现“零成本”启动和低成本运营。</p>
<h2><strong>核心构成：</strong></h2>
<p>这套方案的核心思想是：在产品获得市场验证和稳定收入之前，将固定成本降至无限接近于零。</p>
<h3><strong>1、前端与托管：</strong></h3>
<p>全球化部署的首选是 Vercel 或 Netlify。</p>
<p>它们的免费套餐对于托管静态网站、提供了开箱即用的全球CDN加速、自动HTTPS加密和持续集成/持续部署（CI/CD）流程。</p>
<p>你只需专注于代码，无需为服务器运维和全球访问速度操心。</p>
<h4><strong>部署：Vercel/Cloudflare Pages</strong></h4>
<p><img src="/api/images/image/2026/07/6e418d06d707bdb9-1757750463552-0ee65dd6-b1c8-4668-90d4-06dc529c3cce.png" alt="image"></p>
<p><em>“极速部署，免费托管，轻松上线！”</em></p>
<p>Vercel 官网: <a href="https://vercel.com/">https://vercel.com/</a></p>
<p>Cloudflare Pages 官网: <a href="https://pages.cloudflare.com/">https://pages.cloudflare.com/</a></p>
<h4><strong>域名购买：Spaceship</strong></h4>
<p><img src="/api/images/image/2026/07/16ecbc52a333ac30-1757749363478-6e14608f-72b0-4e01-944a-d37586fd25b0.png" alt="image"></p>
<p><em>“低价域名，轻松启航你的在线业务！”</em></p>
<p>官网链接: <a href="https://spaceship.com/">https://spaceship.com/</a></p>
<h3><strong>2、后端与数据库：</strong></h3>
<p>“后端即服务”（BaaS）是省钱利器。Supabase 或 Firebase 的免费层是绝佳选择。</p>
<p>它们集成了用户认证、对象存储、实时数据库和无服务器函数（Serverless Functions）等核心功能。</p>
<h4><strong>数据库：Supabase</strong></h4>
<p><img src="/api/images/image/2026/07/f0ada996ee793069-1757749373094-ac9974de-d22f-4c15-a9ca-6784eab4baf1.png" alt="image"></p>
<p><em>“开源数据库，免费额度助力快速开发！”</em></p>
<p>官网链接: <a href="https://supabase.io/">https://supabase.io/</a></p>
<h4><strong>缓存：Upstash</strong></h4>
<p><img src="/api/images/image/2026/07/7ba5a180fc6f09d9-1757749385173-e651f759-3510-40d7-af44-34c67d5bf7bd.png" alt="image"></p>
<p><em>“Serverless Redis，免费额度助力高性能缓存！”</em></p>
<p>官网链接: <a href="https://upstash.com/">https://upstash.com/</a></p>
<h4><strong>CMS：Sanity/Keystatic</strong></h4>
<p><img src="/api/images/image/2026/07/04d8d022a914cc48-1757749424545-7424e269-82c2-4677-98e8-0e468e997c7f.png" alt="image"></p>
<p><em>“灵活内容管理，免费额度助力内容创作！”</em></p>
<p>Sanity 官网: <a href="https://www.sanity.io/">https://www.sanity.io/</a></p>
<p>Keystatic 官网: <a href="https://keystatic.com/">https://keystatic.com/</a></p>
<h3><strong>3、无服务器计算：</strong></h3>
<p>各大云厂商的Serverless产品是关键。Cloudflare Workers 凭借其慷慨的免费额度和全球边缘节点的低延迟特性，成为处理API请求、运行定时任务或集成第三方服务的首选。</p>
<h4><strong>域名解析：Cloudflare Workers</strong></h4>
<p><img src="/api/images/image/2026/07/74bbcebb99928432-1757749368328-1f0102a8-f261-4ff0-ae14-bbecbb1facc6.png" alt="image"></p>
<p><em>“全球加速，免费解析，安全无忧！”</em></p>
<p>官网链接: <a href="https://www.cloudflare.com/">https://www.cloudflare.com/</a></p>
<h4><strong>认证：Clerk/Supabase</strong></h4>
<p><img src="/api/images/image/2026/07/914c1d807250d55e-1757749377498-8274020d-9e37-4592-9ad9-c07a5920e54f.png" alt="image"></p>
<p><em>“一键集成认证，安全便捷，开发者首选！”</em></p>
<p>Clerk 官网: <a href="https://clerk.dev/">https://clerk.dev/</a></p>
<h4><strong>域名邮件：Resend + Cloudflare 转发</strong></h4>
<p><img src="/api/images/image/2026/07/064f45928fec5b12-1757749399896-045046c4-1e51-4ac4-bf1f-8307de0503d4.png" alt="image"></p>
<p><em>“专业邮件服务，免费转发，沟通无忧！”</em></p>
<p>Resend 官网: <a href="https://resend.com/">https://resend.com/</a></p>
<p>Cloudflare 官网: <a href="https://www.cloudflare.com/">https://www.cloudflare.com/</a></p>
<h3><strong>4、数据分析与监控：</strong></h3>
<p>要想了解海外用户行为，数据分析必不可少。Google Analytics 4 依然是功能最强大且完全免费的选择。</p>
<p>而对于应用性能监控和错误追踪，Sentry 或 UptimeRobot 的免费套餐能帮你及时发现并修复线上问题，保证用户体验。</p>
<h4><strong>SEO：Google Trends/Ahrefs/Semrush</strong></h4>
<p><img src="/api/images/image/2026/07/5c7c675363756758-1757749492309-f02739d4-a4fc-43c9-88a3-fe7baba7692c.png" alt="image"></p>
<p><em>“免费 SEO 工具，洞察趋势，优化搜索排名！”</em></p>
<p>Google Trends 官网: <a href="https://trends.google.com/">https://trends.google.com/</a></p>
<p>Ahrefs 官网: <a href="https://ahrefs.com/">https://ahrefs.com/</a></p>
<p>Semrush 官网: <a href="https://www.semrush.com/">https://www.semrush.com/</a></p>
<h4><strong>统计：Google Analytics/Openpanel/Posting</strong></h4>
<p><img src="/api/images/image/2026/07/1fd6cfd0d4bb9b45-1757749405070-23c64ed5-24ac-407a-b693-721f076e3e34.png" alt="image"></p>
<p><em>“深度数据分析，免费工具助力用户洞察！”</em></p>
<p>Google Analytics 官网: <a href="https://analytics.google.com/">https://analytics.google.com/</a></p>
<p>Openpanel 官网: <a href="https://openpanel.dev/">https://openpanel.dev/</a></p>
<p>Posting 官网: <a href="https://posthog.com/">https://posthog.com/</a></p>
<h4><strong>监控：Sentry</strong></h4>
<p><img src="/api/images/image/2026/07/fd6157a37175fb99-1757749409907-0f9f5387-a76f-48c6-9c6e-747acd7b0417.png" alt="image"></p>
<p><em>“实时错误监控，快速定位问题，提升应用稳定性！”</em></p>
<p>官网链接: <a href="https://sentry.io/">https://sentry.io/</a></p>
<h3><strong>5、项目管理与协作：</strong></h3>
<p>Notion、Trello 或 GitHub Projects 的免费版本提供了强大的任务管理、文档撰写和路线图规划功能。</p>
<h4><strong>笔记：Obsidian/Notion/飞书</strong></h4>
<p><img src="/api/images/image/2026/07/431992c8f1a8313a-1757749475716-965fffc5-e692-4e9c-8f76-32061b02f3c4.png" alt="image"></p>
<p><em>“多平台笔记工具，免费使用，知识管理无忧！”</em></p>
<p>Obsidian 官网: <a href="https://obsidian.md/">https://obsidian.md/</a></p>
<p>Notion 官网: <a href="https://www.notion.so/">https://www.notion.so/</a></p>
<p>飞书官网: <a href="https://www.feishu.cn/">https://www.feishu.cn/</a></p>
<h4><strong>任务管理：Notion/飞书</strong></h4>
<p><img src="/api/images/image/2026/07/28bc10da15c5401d-1757749486459-155ce898-74b6-4534-a15e-292dbc82f064.png" alt="image"></p>
<p><em>“多功能任务管理，免费工具提升团队效率！”</em></p>
<p>Notion 官网: <a href="https://www.notion.so/">https://www.notion.so/</a></p>
<p>飞书官网: <a href="https://www.feishu.cn/">https://www.feishu.cn/</a></p>
<p>总结而言，2025 产品出海，前期先以极低的经济风险去试水全球市场。</p>
<p>从前端部署到后端逻辑，再到数据分析和用户管理，每一个环节都有稳定可靠的免费方案。</p>
<p>这使得独立开发者可以将全部精力投入到最核心的价值创造——也就是打磨产品本身，直到找到自己的第一批付费用户。</p>
<p>大家快去试试吧～</p>]]></content:encoded>
      <category>开源</category>
      <pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>一步教你配置Qwen3-Coder + Claude Code辅助完成代码编写</title>
      <link>https://codexun.com/posts/configure-qwen3-coder-with-claude-code</link>
      <guid isPermaLink="true">https://codexun.com/posts/configure-qwen3-coder-with-claude-code</guid>
      <description>Qwen3-Coder 模型是前不久阿里巴巴旗下的通义千问正式发布了全新的 AI 编程大模型。 作为千问系列模型中首个采用混合专家 MoE 架构的代码模型，Qwen3-Coder 总参数达 480B，但在运行时仅激活 350 亿参数，实现了 “大容量、高效率” 的完美平衡。与传统的稠密模型不同，MoE 架构通过动态路由</description>
      <content:encoded><![CDATA[<p>Qwen3-Coder 模型是前不久阿里巴巴旗下的通义千问正式发布了全新的 AI 编程大模型。</p>
<p>作为千问系列模型中首个采用混合专家 MoE 架构的代码模型，Qwen3-Coder 总参数达 480B，但在运行时仅激活 350 亿参数，实现了 “大容量、高效率” 的完美平衡。与传统的稠密模型不同，MoE 架构通过动态路由机制，每个 token 仅由少数最擅长的 “专家” 处理。</p>
<p>Qwen3-Coder 原生就支持 256K token 的上下文窗口，通过 YaRN 技术，更可扩展至 1M token，这意味着它能处理更庞大、更复杂的代码任务。</p>
<p><img src="/api/images/image/2026/07/5140fa8f2601091b-1757241291156-4eaaba36-50c1-4dff-a09a-d8b99bee7eaa.png" alt="image"></p>
<p>定价仅为Claude4的1/3（输入4元/百万tokens，输出16元/百万tokens），且生成速度达160字/秒，显著降低企业与开发者使用门槛。</p>
<p>在开源社区和工业场景中，Qwen3-Coder已验证其代码生成质量（平均200行代码/次，注释率13%），并通过持续迭代优化实际表现。</p>
<p>官方推出 Qwen Code 工具驱动Claude Code 进行编程。</p>
<p>当然，如果你更熟悉Claude Code，也可以通过简单配置驱动Qwen3-Coder 来运行 Claude Code！只需在 阿里云 Model Studio 平台 申请 API 密钥，并安装 Claude Code 工具，配置也比较简单，步骤如下：</p>
<h2>获取阿里云百炼API Key</h2>
<p>首先是登录阿里云百炼大模型服务平台 <a href="https://bailian.console.aliyun.com/%E3%80%82%E9%9C%80%E8%A6%81%E5%BC%80%E9%80%9A%E9%98%BF%E9%87%8C%E4%BA%91%E7%99%BE%E7%82%BC%E7%9A%84%E6%A8%A1%E5%9E%8B%E6%9C%8D%E5%8A%A1%EF%BC%8C%E5%A6%82%E6%9E%9C%E6%98%AF%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%B3%A8%E5%86%8C%EF%BC%8C%E8%BF%98%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%BE%97%E5%85%8D%E8%B4%B9%E9%A2%9D%E5%BA%A6%E3%80%82">https://bailian.console.aliyun.com/。需要开通阿里云百炼的模型服务，如果是第一次注册，还可以获得免费额度。</a></p>
<p>通过阿里云百炼获取API Key，具体在密钥管理页面，在API-Key页签（下图位置①）下单击创建API-KEY（下图位置②）。</p>
<h2>安装claude code工具</h2>
<p>以在 mac 中举例，在终端中执行下列命令，安装 Claude Code。</p>
<pre><code class="language-plain">npm install -g @anthropic-ai/claude-code

</code></pre>
<h2>配置环境变量</h2>
<p>将阿里云百炼提供的 Base URL 和 API Key 设置为环境变量。在 mac 中，默认的 Shell 类型是 zshr。</p>
<pre><code class="language-plain"># 用您的百炼API KEY代替YOUR_DASHSCOPE_API_KEY
echo 'export ANTHROPIC_BASE_URL=&quot;&lt;https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy&gt;&quot;' &gt;&gt; ~/.zshrc
echo 'export ANTHROPIC_AUTH_TOKEN=&quot;YOUR_DASHSCOPE_API_KEY&quot;' &gt;&gt; ~/.zshrc

</code></pre>
<p>在终端中执行下列命令，使环境变量生效。</p>
<pre><code class="language-plain">source ~/.zshrc

</code></pre>
<p>打开一个新的终端，执行下列命令，查看环境变量是否生效。</p>
<pre><code class="language-plain">echo $ANTHROPIC_BASE_URL
echo $ANTHROPIC_AUTH_TOKEN

</code></pre>
<h2>运行Claude Code</h2>
<p>当前仅支持 qwen3-coder-plus模型，所以暂不用配置 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL这两个环境变量。</p>
<p>在终端中执行以下命令，运行 Claude Code，效果如图所示：</p>
<p><img src="/api/images/image/2026/07/4f42532a124f661d-1757242245572-13b9c593-5c9c-4d05-99bf-6293201f175f.png" alt="image"></p>
<p>如果你之前有使用过 Claude code，配置过用国内大模型驱动，实际上这个配置过程是类似，比较简单的</p>
<p>配置完成，就可以愉快使用qwen3-coder模型了～</p>
<p>下面是使用提示词生成的打地鼠游戏，实际操作完，速度还是非常快的、</p>
<p><img src="/api/images/image/2026/07/b50576f426ce1eab-1757242758114-3bc89ba8-3da2-428d-af1f-822c3efc3c92.png" alt="image"></p>]]></content:encoded>
      <category>AI</category>
      <pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Perplexity Pro免费送会员了，只要你有 Paypal 账号</title>
      <link>https://codexun.com/posts/get-perplexity-pro-free-subscription-using-paypal</link>
      <guid isPermaLink="true">https://codexun.com/posts/get-perplexity-pro-free-subscription-using-paypal</guid>
      <description>Perplexity AI 介绍 Perplexity AI 是一款结合了对话式交互和搜索引擎功能的 AI 答案引擎。 Perplexity AI 并非旨在完全取代 Google 等传统搜索引擎，而是作为其补充，为用户提供一种 更高效、更整合的信息获取和知识管理方式，特别适合那些希望快速获得可靠、有据可查的答案，并减少</description>
      <content:encoded><![CDATA[<h2><strong>Perplexity AI 介绍</strong></h2>
<p>Perplexity AI 是一款结合了对话式交互和搜索引擎功能的 <strong>AI 答案引擎</strong>。</p>
<p>Perplexity AI 并非旨在完全取代 Google 等传统搜索引擎，而是作为其补充，为用户提供一种 <strong>更高效、更整合</strong>的信息获取和知识管理方式，特别适合那些希望快速获得可靠、有据可查的答案，并减少手动筛选信息时间的用户。</p>
<p><img src="/api/images/image/2026/07/bc3e42e879cadbf2-image.png" alt="image.png"></p>
<p>之前 Perplexity 之前好像搞了很多次通过优惠码免费领 pro 会员的活动，没过多久就回收了，纯纯玩不起！</p>
<p>不过这两天，PayPal 和 Perplexity 又整了一个 活动，搞了一个新的 AI 浏览器——Comet，还赠送 12 个月的 Perplexity pro 会员，这次官方有正式通告，真的不会再鸽了～</p>
<p>经过简单操作，已经领取成功了，如下图示:</p>
<p><img src="/api/images/image/2026/07/085835f55ef444ba-image.png" alt="image.png"></p>
<h2><strong>领取步骤</strong></h2>
<p>话不多说，领取步骤也很简单，只要有 Paypal 账户，如果没有的话可以自行注册。</p>
<p>并且 Paypal 账号没有美区、国区的限制，我测试使用国区的 Paypal 账号也领取成功了，并且 Perplexity 的账号可以跟 Paypal 账号的邮箱不一致。</p>
<h3><strong>1、访问领取订阅</strong>：</h3>
<p><a href="https://www.perplexity.ai/join/p/paypal-subscription">https://www.perplexity.ai/join/p/paypal-subscription</a></p>
<p>上面链接，进入到活动页面，使用一个新账号登录perplexity。</p>
<p><img src="/api/images/image/2026/07/8a21e8e179c8ac06-image.png" alt="image.png"></p>
<p>登录之后会显示下面界面，让重定向到 Paypal 账号登录，在新的登录界面登录你 Paypal 账号，paypal 账号可以使用跟perplexity不同的邮箱。</p>
<p>确认使用这个 Paypal 账号用于订阅perplexity，后面可以<strong>取消自动扣款</strong>。</p>
<p><img src="/api/images/image/2026/07/4da107e8b95f6765-image.png" alt="image.png"></p>
<p>然后接受优惠，就可以免费领取到Perplexity AI 的 Pro 账号了。</p>
<p><img src="/api/images/image/2026/07/e0aa71d222ea3f68-image.png" alt="image.png"></p>
<h3><strong>2、取消自动扣款</strong>：</h3>
<p><a href="https://www.paypal.com/myaccount/autopay/">https://www.paypal.com/myaccount/autopay/</a></p>
<p>为了防止到期后自动续订 Pro 账号的扣费，可以点击上述链接，取消掉自动续订的续订的perplexity。【<strong>类似支付宝订阅的自动扣款</strong>】</p>
<p>下图是我已经取消自动续订之后，已经显示无效了。可以点击左侧“显示有效”这里，perplexity取消相应的订阅。</p>
<p><img src="/api/images/image/2026/07/21f849d2af97a4c8-image.png" alt="image.png"></p>
<p>以上操作完成，就可以开始愉快使用Perplexity Pro 账号了～</p>
<p><img src="/api/images/image/2026/07/9ec3dec3682c483c-image.png" alt="image.png"></p>]]></content:encoded>
      <category>AI</category>
      <pubDate>Fri, 05 Sep 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Android 开发中 applicationId、namespace 以及 package 属性区别和联系</title>
      <link>https://codexun.com/posts/android-applicationid-namespace-package-differences-connections</link>
      <guid isPermaLink="true">https://codexun.com/posts/android-applicationid-namespace-package-differences-connections</guid>
      <description>applicationId、namespace 以及 AndroidManifest.xml 中的 package 是 Android 开发中涉及应用标识和代码组织的重要概念。 它们之间既有联系也有区别，尤其是在现代 Android Gradle 插件 (AGP) 版本中，其行为和推荐用法有所演进。 以下是对这三者的详</description>
      <content:encoded><![CDATA[<p><code>applicationId</code>、<code>namespace</code> 以及 <code>AndroidManifest.xml</code> 中的 <code>package</code> 是 Android 开发中涉及应用标识和代码组织的重要概念。</p>
<p>它们之间既有联系也有区别，尤其是在现代 Android Gradle 插件 (AGP) 版本中，其行为和推荐用法有所演进。</p>
<p>以下是对这三者的详细解析：</p>
<h2>1. <code>applicationId</code></h2>
<p>applicationId，在模块的 <code>build.gradle</code> 文件中定义</p>
<ul>
<li><strong>定义</strong>：这是您的应用在 Android 设备上和 Google Play 商店中的<strong>最终且唯一的标识符</strong>。</li>
<li><strong>重要性</strong>：一旦您发布了应用（即使是内部测试版），就**绝对不应该再更改 **<code>**applicationId**</code>。更改它会导致 Google Play 和设备将其视为一个全新的应用，用户将无法通过更新现有安装来获取新版本，而是会安装一个新应用。</li>
<li><strong>用途</strong>：
<ul>
<li>设备操作系统使用它来唯一识别已安装的每个应用程序。</li>
<li>Google Play 商店使用它来跟踪和管理您的应用列表。</li>
<li>应用间通信（例如，通过 Intent 显式启动其他应用时，可以指定目标应用的 <code>applicationId</code>）。</li>
<li>内部数据存储路径等也可能基于此 ID。</li>
</ul>
</li>
<li><strong>独立性</strong>：<code>applicationId</code> 可以与您的项目源代码的包名（Java/Kotlin 代码所在的目录结构）完全不同。这种分离提供了灵活性，允许您在不影响已发布应用的情况下重构内部代码包。</li>
<li><strong>配置位置</strong>：在模块级别（通常是 <code>app</code> 模块）的 <code>build.gradle</code> 文件中，位于 <code>defaultConfig</code> 闭包内，或者可以针对不同的 <code>productFlavors</code> 进行配置。</li>
</ul>
<pre><code class="language-groovy">android {
    defaultConfig {
        applicationId &quot;com.example.myapp.uniqueid&quot; // 这是最终的应用 ID
        // ...
    }
}
</code></pre>
<h2><strong>2、</strong><code>**namespace**</code>** **</h2>
<p><code>namespace</code>，在模块的 <code>build.gradle</code> 文件中定义 - AGP 7.0+ 核心配置</p>
<ul>
<li><strong>定义</strong>:<code>namespace</code> 是在模块级别 <code>build.gradle</code> 文件的 <code>android</code> 闭包中定义的属性。它指定了该 Gradle 模块中由构建系统生成的关键代码（如 <code>R</code> 类和 <code>BuildConfig</code> 类）的 Java/Kotlin 包名。</li>
<li><strong>主要用途与职责</strong>:
<ol>
<li><code>**R**</code>** 类包名**: 决定了包含所有资源 ID 的 <code>R.java</code> 文件的包名。例如，如果 <code>namespace &quot;com.example.myapp&quot;</code>，则 <code>R</code> 类将是 <code>com.example.myapp.R</code>。这对于在代码中访问资源（如 <code>R.string.app\_name</code> 或 <code>R.layout.activity\_main</code>）至关重要。</li>
<li><code>**BuildConfig**</code>** 类包名**: 决定了包含构建时常量（如 <code>DEBUG</code>, <code>VERSION\_NAME</code>, <code>FLAVOR</code> 等）的 <code>BuildConfig.java</code> 文件的包名。</li>
<li><code>**applicationId**</code>** 的默认值 (仅限应用模块)**: 对于应用模块（<code>com.android.application</code> 插件），如果 <code>build.gradle</code> 的 <code>defaultConfig</code> 或 <code>productFlavors</code> 中没有显式设置 <code>applicationId</code>，则 <code>namespace</code> 的值将自动被用作该应用的 <code>applicationId</code>。</li>
<li>对于库模块，<code>namespace</code> 定义了其 R 类的包名，以便其他模块可以正确引用该库的资源。</li>
</ol>
</li>
<li><strong>解耦:</strong>
<code>namespace</code> 的引入主要是为了将应用的 Java/Kotlin 包结构与最终的 <code>applicationId</code> 解耦。您可以拥有一个内部代码组织包名（由 <code>namespace</code> 定义），同时拥有一个独立的、稳定的 <code>applicationId</code>。</li>
<li><strong>配置位置</strong>:
在模块级别（例如 <code>app/build.gradle</code> 或 <code>mylibrary/build.gradle</code>）的 <code>android</code> 闭包内直接声明。</li>
</ul>
<pre><code class="language-groovy">android {
    compileSdk 34
    namespace &quot;com.example.myapp.codepackage&quot; // R类 和 BuildConfig 的包名
    defaultConfig {
        // applicationId &quot;com.example.myapp.uniqueid&quot; // 可以与 namespace 不同
        // ...
    }
}

</code></pre>
<p>如果您没有显式设置 applicationId，那么 namespace 的值将被用作 applicationId。</p>
<h2>3、<code>package</code></h2>
<p>package，在 <code>AndroidManifest.xml</code> 文件中定义</p>
<ul>
<li><strong>定义</strong>: <code>package</code> 是 <code>AndroidManifest.xml</code> 文件根 <code>&lt;manifest&gt;</code> 元素的一个属性。它声明了一个 Java/Kotlin 风格的包名，作为该清单文件内容的特定上下文。</li>
<li><strong>历史版本 (AGP 7.0 之前)</strong>:
在旧版本的 Android Gradle 插件 (AGP) 中，<code>AndroidManifest.xml</code> 中的 <code>package</code> 属性承担了多个关键角色：
<ol>
<li><code>**applicationId**</code>** 的来源**: 它直接决定了应用的最终 <code>applicationId</code>。</li>
<li><code>**R**</code>** 类包名**: 它也用于确定生成的 <code>R</code> 类的包名。</li>
<li><strong>相对类名解析</strong>: 用于解析清单文件中声明的所有相对类名（例如，<code>&lt;activity android:name=&quot;.MyActivity&quot;/&gt;</code> 会被解析为 <code>\[package属性值\].MyActivity</code>）。
这种多重职责有时会导致混淆，尤其是在需要重构代码包名或为不同构建变体管理不同的 <code>applicationId</code> 时。</li>
</ol>
</li>
<li><strong>最新版本 (AGP 7.0+ 及以后，当 <strong><code>**build.gradle**</code></strong> 中使用了 <strong><code>**namespace**</code></strong>)</strong>:
随着 <code>namespace</code> 属性在 <code>build.gradle</code> 文件中的引入和推广，<code>AndroidManifest.xml</code> 中 <code>package</code> 属性的职责已显著简化：
<ol>
<li><strong>清单文件内部相对类名的解析基础</strong>: 其最主要且几乎唯一的作用是作为当前 <code>AndroidManifest.xml</code> 文件内部声明的相对类名（如 Activity、Service、Receiver、Provider 组件的 <code>android:name</code> 属性值如果以 <code>.</code> 开头）的解析基础。
例如：如果 <code>package=&quot;com.example.myapp&quot;</code> 且有一个 <code>&lt;activity android:name=&quot;.ui.MainActivity&quot;/&gt;</code>，它会被解析为 <code>com.example.myapp.ui.MainActivity</code>。</li>
<li>**不再决定 **<code>**applicationId**</code>: 应用的最终 <code>applicationId</code> 完全由 <code>build.gradle</code> 文件中的 <code>applicationId</code> 字段（如果显式设置）或 <code>namespace</code> 字段（如果 <code>applicationId</code> 未设置）控制。</li>
<li><strong>不再决定 <strong><code>**R**</code></strong> 类的包名</strong>: <code>R</code> 类的包名完全由 <code>build.gradle</code> 文件中的 <code>namespace</code> 字段控制。</li>
<li><strong>默认值:</strong> 如果 <code>AndroidManifest.xml</code> 中的 <code>package</code> 属性没有显式设置，它的值会默认为 <code>build.gradle</code>中定义的 <code>namespace</code>。构建工具会确保这一点。</li>
</ol>
</li>
<li><strong>与 <strong><code>**namespace**</code></strong> (在 <strong><code>**build.gradle**</code></strong> 中) 的关系</strong>:
<ul>
<li><strong>权威性来源</strong>: <code>build.gradle</code> 中的 <code>namespace</code> 属性是 <code>AndroidManifest.xml</code> 中 <code>package</code> 属性值的权威来源。</li>
<li><strong>同步要求</strong>: Android Gradle 插件 (AGP) <strong>强烈建议或要求</strong> <code>AndroidManifest.xml</code> 中的 <code>package</code> 属性值与 <code>build.gradle</code> 中的 <code>namespace</code> 值<strong>保持一致</strong>。</li>
<li><strong>构建行为</strong>: 在构建过程中，AGP 会确保最终打包到 APK 中的 <code>AndroidManifest.xml</code> 文件（即所谓的 &quot;merged manifest&quot;）的 <code>package</code> 属性值与 <code>build.gradle</code> 中定义的 <code>namespace</code> 值相符。如果 manifest 中的 <code>package</code> 属性最初未设置或与 <code>namespace</code> 不匹配，AGP 通常会使用 <code>namespace</code> 的值进行覆盖或填充，并可能发出警告。</li>
</ul>
</li>
<li><strong>配置示例</strong>:</li>
</ul>
<pre><code class="language-xml">&lt;manifest xmlns:android=&quot;&lt;http://schemas.android.com/apk/res/android&gt;&quot;
    package=&quot;com.example.myapp.manifestpackage&quot;&gt; &lt;!-- 主要用于解析此文件内的相对类名 --&gt;

    &lt;application ...&gt;
        &lt;activity android:name=&quot;.MainActivity&quot; ... /&gt;
        &lt;!--  会被解析为 com.example.myapp.manifestpackage.MainActivity --&gt;
    &lt;/application&gt;
&lt;/manifest&gt;

</code></pre>
<h2>总结与最佳实践 (AGP 7.0+)</h2>
<ol>
<li><code>**applicationId**</code>** (in <strong><code>**build.gradle**</code></strong>)**:
<ul>
<li><strong>最重要</strong>: 这是您的应用在设备和商店上的<strong>唯一ID</strong>。发布后不要更改。</li>
<li>可以独立于您的代码包结构。</li>
</ul>
</li>
<li><code>**namespace**</code>** (in <strong><code>**build.gradle**</code></strong>)**:
<ul>
<li><strong>代码结构基础</strong>: 定义您的 <code>R</code> 类和 <code>BuildConfig</code> 类的包名。</li>
<li>建议与您的主要源代码包结构保持一致。</li>
<li>如果 <code>applicationId</code> 未设置，它将成为 <code>applicationId</code> 的默认值。</li>
</ul>
</li>
<li><code>**package**</code>** (in <strong><code>**AndroidManifest.xml**</code></strong>)**:
<ul>
<li><strong>清单文件内部解析</strong>: 主要用于解析清单文件内部的相对类名。</li>
<li><strong>建议</strong>: 为了清晰和避免混淆，建议将 <code>AndroidManifest.xml</code> 中的 <code>package</code> 属性的值设置为与 <code>build.gradle</code> 中的 <code>namespace</code> 相同的值。虽然构建系统在很多情况下会处理不一致的情况，但保持一致性是好习惯。事实上，AGP 会警告（或在未来版本可能报错）如果它们不匹配，除非有特殊理由。</li>
</ul>
</li>
</ol>
<p>核心区别：</p>
<ul>
<li><code>applicationId</code> 是<strong>外部标识</strong>。</li>
<li><code>namespace</code> 是模块<strong>内部代码生成（如R类）的包名基准</strong>。</li>
<li><code>AndroidManifest.xml</code> 中的 <code>package</code> 在现代 AGP 中主要服务于<strong>清单文件自身的类名解析</strong>，其权威性已被 <code>namespace</code> 和 <code>applicationId</code> 取代。</li>
</ul>]]></content:encoded>
      <category>安卓</category>
      <pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>【开源】 Bing 壁纸数据爬虫和文档生成</title>
      <link>https://codexun.com/posts/bing-wallpaper-data-scraper-and-doc-generator</link>
      <guid isPermaLink="true">https://codexun.com/posts/bing-wallpaper-data-scraper-and-doc-generator</guid>
      <description>因为平时使用 bing 搜索比较多，都知道必应（Bing）首页，每日会更换精美的壁纸，然后想着是否可以写一个数据爬取程序，保存每日的壁纸，并用网站来呈现。 正好之前使用 kiro 可以试用的时候，通过不断对话交互，一方面可以试一下 kiro 的交互使用，另一方面简单实现了一个 bing 壁纸数据爬取的程序。 下面将进行</description>
      <content:encoded><![CDATA[<p>因为平时使用 bing 搜索比较多，都知道必应（Bing）首页，每日会更换精美的壁纸，然后想着是否可以写一个数据爬取程序，保存每日的壁纸，并用网站来呈现。</p>
<p>正好之前使用 kiro 可以试用的时候，通过不断对话交互，一方面可以试一下 kiro 的交互使用，另一方面简单实现了一个 bing 壁纸数据爬取的程序。</p>
<p>下面将进行总结一下，通过必应的非官方 API 以编程方式访问每日壁纸，构建一个强大的 Python 爬虫来下载图片和元数据，并利用 GitHub Actions 完全免费地自动化整个流程。</p>
<p><strong>可以查看项目开源地址，以及 Bing 壁纸网站。</strong></p>
<p>1、壁纸数据爬取项目<a href="https://github.com/axe433/bing_wallpaper_data/">Github地址 </a></p>
<p>2、最终可以在 <a href="https://bing.codexun.com/cn">Bing 壁纸</a> 这个网站上查看</p>
<p><img src="/api/images/image/2026/07/7a8f75534000d4ff-image.png" alt="image.png"></p>
<h2>项目：<a href="https://github.com/axe433/bing_wallpaper_data/">壁纸档案馆</a></h2>
<p>在深入技术细节之前，先看下这个项目最终实现了什么。它不是一个简单的脚本，而是一个全自动、自维护的生态系统：</p>
<ul>
<li><strong>全球覆盖</strong>：自动抓取包括中国、美国、日本等 <strong>34个国家和地区</strong> 的每日壁纸数据。</li>
<li><strong>每日自动更新</strong>：通过 GitHub Actions ，整个流程（抓取、下载、生成文档、提交代码）每天自动运行，无需任何人工干预。</li>
<li><strong>数据完整性</strong>：保存完整的壁纸元数据，包括标题、版权信息、描述等</li>
<li><strong>超清画质</strong>：智能下载每张壁纸的 <strong>UHD（超高清）</strong> 版本，确保最佳视觉体验。</li>
<li><strong>智能管理</strong>：通过MD5哈希值进行 <strong>增量下载和自动去重</strong>，避免存储空间浪费。</li>
<li><strong>自动化文档</strong>：为每个国家生成 <strong>精美的Markdown文档</strong>，包含今日壁纸、最近30天预览和按年月归档的完整历史记录。</li>
</ul>
<p><em>项目生成的文档会像这样每日更新，展示精美的今日壁纸</em></p>
<h2>分析：壁纸 API 深度解析</h2>
<p>虽然 Bing 从未公开过 API 接口，但是可以通过搜索，监测到访问 bing 首页的时候隐形调用的 api 接口。</p>
<p>驱动必应壁纸功能的核心是一个非官方的 <code>HTTP GET</code> 接口：<code>https://www.bing.com/HPImageArchive.aspx</code> 。尽管微软并未提供关于此接口的官方文档，但其稳定的结构和在众多开源项目中的广泛应用，使其成为一个相当可靠的数据来源 。</p>
<p>该接口通过 <code>format</code> 参数控制返回数据的格式。对于开发者而言，最友好的选项是 <code>format=js</code>，因为它会返回一个结构清晰的 JSON 对象 。</p>
<h2>数据爬取：Bing 壁纸<strong>数据抓取脚本</strong></h2>
<p>项目的强大功能由<code>crawl/</code>目录下的几个核心Python脚本驱动，它们各司其职，又紧密协作。</p>
<h3>1. <code>bing\_data.py</code> - 数据抓取脚本</h3>
<p>这是所有工作的起点。它负责调用Bing的官方API，获取壁纸的元数据。</p>
<ul>
<li><strong>工作原理</strong>：脚本会遍历一个包含34个国家代码的列表，向Bing API发送请求，获取每个国家当天的壁纸JSON数据。</li>
<li><strong>智能更新</strong>：它会将最新的数据 <strong>增量添加</strong> 到对应国家的 <code>jsonc/{country}/bing.jsonc</code> 文件中，并保持时间倒序，确保数据不重复、不丢失。</li>
</ul>
<h3>2. <code>download\_wallpapers\_action.py</code> - 壁纸下载脚本</h3>
<p>获取元数据后，就轮到下载图片了。这个脚本的亮点在于其“智能”。</p>
<ul>
<li><strong>构建UHD链接</strong>：它会从JSON数据中的 <code>urlbase</code> 字段构建出真正的超高清壁纸下载链接，格式通常是 <code>https://www.bing.com{urlbase}\_UHD.jpg</code>。</li>
<li><strong>MD5指纹</strong>：下载图片后，脚本会计算文件的MD5哈希值。这个哈希值有两个妙用：
<ol>
<li><strong>作为文件名</strong>：将图片重命名为 <code>{md5}.jpg</code>，天然避免了重名问题。</li>
<li><strong>作为下载凭证</strong>：将MD5值写回JSON数据中。下次运行时，脚本只需检查JSON项中是否存在 <code>md5</code> 字段，就能知道这张图片是否已下载，从而实现 <strong>增量下载</strong>。</li>
</ol>
</li>
<li><strong>自动去重</strong>：在下载前，脚本还会检查目标文件是否已存在，进一步确保了不会下载重复的图片。</li>
</ul>
<h3>3. <code>generate\_markdown.py</code> &amp; <code>generate\_readme.py</code> - 文档生成器</h3>
<p>将爬取的Bing壁纸数据转化为了美观、易于浏览的网页。</p>
<ul>
<li><strong>数据驱动</strong>：脚本读取 <code>jsonc/</code> 中的数据和 <code>images/</code> 中的图片，动态生成每个国家的Markdown文档。</li>
<li><strong>丰富功能</strong>：生成的文档包含“今日壁纸”大图展示、“最近30天”响应式网格布局以及“按年份归档”的导航按钮，用户体验极佳。</li>
<li><strong>模板化</strong>：<code>generate\_readme.py</code> 使用 <code>templates/</code> 目录下的模板文件来生成项目主页的<code>README.md</code>，做到了内容与样式的分离。</li>
</ul>
<h2>自动化：使用GitHub Actions每日自动执行</h2>
<p>只需在 <code>.github/workflows/python-app.yml</code> 文件中进行简单配置，就能让所有任务自动执行。</p>
<pre><code class="language-yaml">name: Generate Bing Wallpaper Json Auto
on:
  schedule:
    - cron: '1 23 * * *'  # 每天23:01 UTC执行
  workflow_dispatch:      # 允许手动触发

jobs:
  download_and_push:
    runs-on: ubuntu-latest
    steps:
      # ... 检出代码、安装依赖 ...
      - name: 抓取数据
        run: python crawl/bing_data.py
      - name: 下载图片
        run: python crawl/download_wallpapers_action.py
      - name: 生成文档
        run: python crawl/generate_markdown.py
      - name: 生成README
        run: python crawl/generate_readme.py
      - name: 提交并推送
        run: |
          git config --global user.name 'github-actions'
          git add .
          git commit -m &quot;Update wallpaper data and markdown at $(date)&quot;
          git push
</code></pre>
<p>这段配置定义了一个在每天特定时间（UTC 23:01）自动运行的任务。它会依次执行我们上面介绍的四个核心脚本，最后自动将所有更新（新的JSON数据、新的图片、新的Markdown文档）提交回GitHub仓库。整个过程完全自动化</p>
<p>对了，之前有详细总结过 Github Action 的有关使用，具体可以参考之前的文章：</p>
<p><strong><a href="/posts/github-actions-workflow-with-inputs">GitHub Actions：带有输入的调度和计划工作流程</a></strong></p>
<p><strong><a href="/posts/github-action-permission-and-personal-access-token">Github 仓库操作— token使用，Action 操作权限</a></strong></p>
<p><strong><a href="/posts/automating-vercel-deployment-with-github-actions">使用 Github Action自动执行Vercel 部署</a></strong></p>
<p><strong><a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">使用 Github Action自动执行 Netlify 部署</a></strong></p>]]></content:encoded>
      <category>开源</category>
      <pubDate>Sat, 30 Aug 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>如何查看Google账号地区以及如何更改Google账号地区</title>
      <link>https://codexun.com/posts/how-to-check-change-google-account-country</link>
      <guid isPermaLink="true">https://codexun.com/posts/how-to-check-change-google-account-country</guid>
      <description>之前写过一篇 如何切换 google play 账号到美区，锁定美区实现ChatGPT应用无障碍下载** **介绍了如何切换 gp 账号到美区，突破一些 app 的下载限制。 其实除了 gp 账号的地区限制之外，google 本身账号也有地区属性，并且可以和Google Play 账号的地区不一致。 例如，你的 Goo</description>
      <content:encoded><![CDATA[<p>之前写过一篇 <strong><a href="/posts/google-play-region-lock-solution-chatgpt-download">如何切换 google play 账号到美区，锁定美区实现ChatGPT应用无障碍下载</a></strong>** **介绍了如何切换 gp 账号到美区，突破一些 app 的下载限制。</p>
<p>其实除了 gp 账号的地区限制之外，google 本身账号也有地区属性，并且可以和Google Play 账号的地区不一致。</p>
<p>例如，你的 Google 账号可能是在香港地区创建的，但您可以将 Google Play 地区设置为美国。另外，Google账号地区不是一成不变的，可能随着你常用IP地址变动，Google账号地区也会随之变化。</p>
<h2><strong>如何查看Google账号地区</strong></h2>
<p>1.登录Google账号</p>
<p>2.点击服务条款或者直接登录网站：<strong><a href="https://policies.google.com/terms"><u>https://policies.google.com/terms</u></a></strong></p>
<p><img src="/api/images/image/2026/07/7713f2bc929924f1-image.png" alt="image.png"></p>
<p>3.可以看到国家/地区版本：XX地区（如下图）</p>
<p><img src="/api/images/image/2026/07/7c8775acb9d73746-image.png" alt="image.png"></p>
<h2>如何<strong>更改Google账号地区</strong></h2>
<p>Google账号所属的国家或地区不是一成不变的，开始是某个地区的，等你长期用某地的IP登录后，被系统检测到，系统会自动帮你更改成该地区的Google账号。</p>
<p>当然，短时间内想快速修改账号地区的话，可以按照如下操作步骤，自行修改Google账号所属的国家或地区。</p>
<p>1、用希望修改的国家或地区IP登录Google账号，最好是用该IP连续登录一周左右。
2、修改路径在常见问题解答页面，也可以用登录的账号直接打开网站：<a href="https://policies.google.com/country-association-form">https://policies.google.com/country-association-form</a>
按下图填写提交即可。</p>
<p><img src="/api/images/image/2026/07/3e3df99f7c3444be-image.png" alt="image.png"></p>
<p>3、填写要求改目的的地区，最好和你科学上网的 ip 地址匹配，理由的话可以自己写的真诚一些。</p>
<p><img src="/api/images/image/2026/07/abadf75211dfd7dc-image.png" alt="image.png"></p>
<p>4、大概2-3天后系统会给该邮箱告知申请的结果，可能会失败，也可能会成功。失败的原因大概是使用目标IP登录该Google账号时间过短。</p>
<h2>为何要更改Google账号的地区</h2>
<p>很多喜欢玩外服的游戏玩家都知道，Google是分国家地区或地区的，而Google比如谷歌礼品卡，有美国谷歌礼品卡、香港谷歌礼品卡，日本谷歌礼品卡，韩国谷歌礼品卡等。自从谷歌修锁区以来，导致很多游戏玩家不能成功兑换谷歌礼品卡。想要成功兑换礼品卡，必须要当地的谷歌账号和礼品卡以及IP。</p>
<p>另外，还有某些地区账号的地理套利，比如之前Google One如果采用土耳其邮箱，就可以很简单的就薅到土耳其Google One的低价羊毛，不知道怎么操作的小伙伴。</p>
<p>再比如YouTube Premium在个别国家的个人会员的价格，因为汇率的原因，可以做到远远低于美国地区的价格进行订阅。</p>
<table>
<thead>
<tr>
<th></th>
<th>订阅费USD/月（当地货币）</th>
<th>差额USD/月</th>
<th>参考汇率汇率换算</th>
</tr>
</thead>
<tbody>
<tr>
<td>美国</td>
<td>$13.99</td>
<td>–</td>
<td>–</td>
</tr>
<tr>
<td>尼日利亚</td>
<td>$0.86（1,100 NGN）</td>
<td>-$13.13</td>
<td>1 USD =1,285 NGN</td>
</tr>
<tr>
<td>阿根廷</td>
<td>$1.14(389[ARS])</td>
<td>-$12.85</td>
<td>$1=340.00[ARS]</td>
</tr>
</tbody>
</table>]]></content:encoded>
      <category>记录</category>
      <pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Java中单例模式的几种实现方式总结</title>
      <link>https://codexun.com/posts/java-singleton-pattern-implementations</link>
      <guid isPermaLink="true">https://codexun.com/posts/java-singleton-pattern-implementations</guid>
      <description>单例模式（Singleton Pattern）是Java中最基本和最常用的设计模式之一。该模式确保一个类只有一个实例，并提供一个全局访问点来访问该实例。在需要频繁创建和销G毁对象或者需要一个全局唯一的对象（例如，线程池、缓存、日志对象等）的场景下，单例模式尤为重要。 以下总结了Java中几种常见的单例模式实现方式，并分</description>
      <content:encoded><![CDATA[<p>单例模式（Singleton Pattern）是Java中最基本和最常用的设计模式之一。该模式确保一个类只有一个实例，并提供一个全局访问点来访问该实例。在需要频繁创建和销G毁对象或者需要一个全局唯一的对象（例如，线程池、缓存、日志对象等）的场景下，单例模式尤为重要。</p>
<p>以下总结了Java中几种常见的单例模式实现方式，并分析了各自的优缺点。</p>
<h2>1. 饿汉式（Eager Initialization）</h2>
<p>饿汉式在类加载时就立即创建实例，因此是线程安全的。</p>
<pre><code class="language-java">public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}
</code></pre>
<p><strong>优点:</strong></p>
<ul>
<li>实现简单，代码可读性高。</li>
<li>在类加载时就完成了实例化，避免了多线程同步问题，是线程安全的。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>在类加载时就创建实例，即使后续从未使用过该实例，也会占用内存空间，可能造成资源浪费。</li>
</ul>
<h2>2. 懒汉式（Lazy Initialization）</h2>
<p>懒汉式在第一次调用<code>getInstance()</code>方法时才创建实例。</p>
<h3>2.1 基础懒汉式（非线程安全）</h3>
<pre><code class="language-java">public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
</code></pre>
<p><strong>优点:</strong></p>
<ul>
<li>实现了懒加载，只有在需要时才创建实例，节约了资源。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>在多线程环境下是非线程安全的。如果多个线程同时进入<code>if (instance == null)</code>判断，并且此时<code>instance</code>都为<code>null</code>，那么将会有多个线程创建出多个实例，违背了单例模式的初衷。</li>
</ul>
<h3>2.2 线程安全的懒汉式（同步方法）</h3>
<p>通过在<code>getInstance()</code>方法上添加<code>synchronized</code>关键字，可以保证线程安全。</p>
<pre><code class="language-java">public class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton instance;

    private SynchronizedLazySingleton() {}

    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedLazySingleton();
        }
        return instance;
    }
}
</code></pre>
<p><strong>优点:</strong></p>
<ul>
<li>解决了多线程环境下的线程安全问题。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>每次调用<code>getInstance()</code>方法时都需要进行同步，即使实例已经被创建。这会导致不必要的性能开销，尤其是在高并发场景下。</li>
</ul>
<h2>3. 双重检查锁定（Double-Checked Locking）</h2>
<p>双重检查锁定是对线程安全的懒汉式的一种优化，旨在减少不必要的同步开销。</p>
<pre><code class="language-java">public class DoubleCheckedSingleton {
    private static volatile DoubleCheckedSingleton instance;

    private DoubleCheckedSingleton() {}

    public static DoubleCheckedSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedSingleton();
                }
            }
        }
        return instance;
    }
}
</code></pre>
<p><strong>关键点:</strong></p>
<ul>
<li>使用<code>volatile</code>关键字修饰<code>instance</code>变量，确保其可见性和禁止指令重排。</li>
<li>进行两次<code>if (instance == null)</code>检查，第一次检查是为了避免在实例已存在的情况下进入同步代码块，从而提高性能。第二次检查是为了在同步代码块内部再次确认实例是否被创建，防止多个线程同时通过第一次检查后重复创建实例。</li>
</ul>
<p><strong>优点:</strong></p>
<ul>
<li>兼顾了线程安全和懒加载的性能。只有在第一次创建实例时才会进入同步代码块，后续的调用将直接返回已创建的实例，性能较高。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>实现相对复杂，容易出错。<code>volatile</code>关键字是必不可少的，否则可能由于指令重排导致获取到未完全初始化的对象。</li>
</ul>
<h2>4. 【推荐】静态内部类（Static Inner Class）</h2>
<p>这是目前被广泛推荐的一种实现方式，它利用了JVM类加载机制来保证线程安全和懒加载。</p>
<pre><code class="language-java">public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {}

    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
</code></pre>
<p><strong>原理:</strong></p>
<ul>
<li>当<code>StaticInnerClassSingleton</code>类被加载时，其静态内部类<code>SingletonHolder</code>并不会被加载。</li>
<li>只有当第一次调用<code>getInstance()</code>方法时，JVM才会加载<code>SingletonHolder</code>类，并初始化<code>INSTANCE</code>。</li>
<li>JVM在加载类时是线程安全的，因此保证了只有一个线程可以初始化<code>INSTANCE</code>。</li>
</ul>
<p><strong>优点:</strong></p>
<ul>
<li>实现了懒加载。</li>
<li>由JVM保证线程安全，无需任何同步代码，性能高。</li>
<li>实现简单，代码清晰。</li>
</ul>
<h2>5. 枚举（Enum）</h2>
<p>这是《Effective Java》作者Joshua Bloch推荐的最佳实现方式。</p>
<pre><code class="language-java">public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        System.out.println(&quot;do something...&quot;);
    }
}
</code></pre>
<p><strong>优点:</strong></p>
<ul>
<li>实现最简单。</li>
<li>天然线程安全。</li>
<li>能有效防止通过反射和反序列化创建新的对象，从而破坏单例。</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>不具备懒加载的特性。</li>
</ul>
<h4>总结</h4>
<table>
<thead>
<tr>
<th>实现方式</th>
<th>线程安全</th>
<th>懒加载</th>
<th>推荐指数</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>饿汉式</strong></td>
<td>是</td>
<td>否</td>
<td>⭐⭐⭐</td>
<td>实现简单，但可能造成资源浪费。</td>
</tr>
<tr>
<td><strong>懒汉式（非线程安全）</strong></td>
<td>否</td>
<td>是</td>
<td>⭐</td>
<td>仅限单线程环境使用。</td>
</tr>
<tr>
<td><strong>懒汉式（同步方法）</strong></td>
<td>是</td>
<td>是</td>
<td>⭐⭐</td>
<td>性能开销较大，不推荐。</td>
</tr>
<tr>
<td><strong>双重检查锁定</strong></td>
<td>是</td>
<td>是</td>
<td>⭐⭐⭐⭐</td>
<td>实现较复杂，需正确使用<code>volatile</code>。</td>
</tr>
<tr>
<td><strong>静态内部类</strong></td>
<td>是</td>
<td>是</td>
<td>⭐⭐⭐⭐⭐</td>
<td>兼具懒加载和线程安全，实现简单，推荐使用。</td>
</tr>
<tr>
<td><strong>枚举</strong></td>
<td>是</td>
<td>否</td>
<td>⭐⭐⭐⭐⭐</td>
<td>最简洁、最安全的实现方式，能防止反射和反序列化攻击。</td>
</tr>
</tbody>
</table>
<p>在实际开发中，<strong>静态内部类</strong>和<strong>枚举</strong>是两种最被推崇的单例模式实现方式。如果需要懒加载，静态内部类是绝佳的选择；如果不需要懒加载，或者对防止反射和反序列化有强烈的需求，枚举则是最简单且最安全的方式。</p>]]></content:encoded>
      <category>安卓</category>
      <pubDate>Wed, 20 Aug 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Next.js 项目中 i18n 多语言实现详解</title>
      <link>https://codexun.com/posts/nextjs-i18n-implementation-guide</link>
      <guid isPermaLink="true">https://codexun.com/posts/nextjs-i18n-implementation-guide</guid>
      <description>在现代 Web 开发中，国际化（i18n）已成为构建全球化应用不可或缺的一部分。 Next.js 相关架构下，一般可以基于 next-intl 的国际化实现方式，本文将从代码结构、配置方式到实际使用场景，全面介绍 i18n的使用，包括语言配置、翻译消息加载、路由支持、组件使用等核心知识点。 一、i18n 实现基础 1.</description>
      <content:encoded><![CDATA[<p>在现代 Web 开发中，国际化（i18n）已成为构建全球化应用不可或缺的一部分。</p>
<p>Next.js 相关架构下，一般可以基于 next-intl 的国际化实现方式，本文将从代码结构、配置方式到实际使用场景，全面介绍 i18n的使用，包括语言配置、翻译消息加载、路由支持、组件使用等核心知识点。</p>
<h2><strong>一、i18n 实现基础</strong></h2>
<h3><strong>1.1 使用 next-intl 库实现国际化</strong></h3>
<p>用 <strong><a href="https://next-intl.dev/">next-intl</a></strong> 库来实现国际化功能。该库为 Next.js 提供了完整的国际化解决方案，包括翻译、日期格式化、数字格式化等功能。</p>
<h3><strong>1.2 安装与依赖管理</strong></h3>
<p>项目使用 <code>pnpm</code> 作为包管理器，安装 next-intl 及其相关依赖非常简单：</p>
<pre><code class="language-shell">pnpm add next-intl
</code></pre>
<h3><strong>1.3 语言配置文件</strong></h3>
<p>语言配置主要定义在 <code>/src/config/website.tsx</code> 文件中，包含默认语言和所有支持的语言列表：</p>
<pre><code class="language-typescript">// /src/config/website.tsx
export const websiteConfig = {
  i18n: {
    defaultLocale: 'en',
    locales: {
      en: { flag: '🇺🇸', name: 'English' },
      zh: { flag: '🇨🇳', name: '中文' }
    }
  }
};
</code></pre>
<p>此配置不仅用于控制应用的默认语言，还被多个组件引用，如语言切换器、页面标题生成等。</p>
<h2><strong>二、翻译消息文件</strong></h2>
<h3><strong>2.1 翻译消息文件结构</strong></h3>
<p>翻译消息存储在 <code>/messages/&lt;locale&gt;.json</code> 文件中，按模块划分，例如 &quot;Common&quot;、&quot;PricingPage&quot; 等：</p>
<pre><code class="language-json">{
  &quot;Common&quot;: {
    &quot;login&quot;: &quot;Log in&quot;,
    &quot;logout&quot;: &quot;Log out&quot;,
    &quot;language&quot;: &quot;Switch language&quot;
  },
  &quot;PricingPage&quot;: {
    &quot;title&quot;: &quot;Pricing&quot;,
    &quot;description&quot;: &quot;Choose the plan that works best for you&quot;
  }
}
</code></pre>
<h3><strong>2.2 在客户端组件中使用翻译</strong></h3>
<p>在客户端组件中，使用 <code>useTranslations</code> 钩子获取翻译内容：</p>
<pre><code class="language-javascript">import { useTranslations } from 'next-intl';

export default function DashboardPage() {
  const t = useTranslations();
  return (
    &lt;h1&gt;{t('Dashboard.dashboard.title')}&lt;/h1&gt;
  );
}
</code></pre>
<h3><strong>2.3 在服务端组件中使用翻译</strong></h3>
<p>在服务器端组件中，使用 <code>getTranslations</code> 获取翻译内容：</p>
<pre><code class="language-javascript">export async function generateMetadata({
  params,
}: {
  params: Promise&lt;{ locale: Locale }&gt;;
}): Promise&lt;Metadata | undefined&gt; {
  const { locale } = await params;
  const pt = await getTranslations({ locale, namespace: 'AuthPage.login' });
  return constructMetadata({
    title: pt('title'),
  });
}
</code></pre>
<h2><strong>三、路由国际化配置</strong></h2>
<h3><strong>3.1 动态路由 <strong><code>**\[locale\]**</code></strong> 支持</strong></h3>
<p>Next.js 的 App Router 支持通过动态路由 <code>\[locale\]</code> 来处理不同语言版本的页面请求。路由配置文件位于 <code>/src/i18n/routing.ts</code>，定义了支持的语言列表和默认语言：</p>
<pre><code class="language-javascript">// /src/i18n/routing.ts
export const routing = defineRouting({
  locales: LOCALES,
  defaultLocale: DEFAULT_LOCALE,
  localeDetection: false,
  localeCookie: {
    name: LOCALE_COOKIE_NAME,
  },
  localePrefix: 'as-needed',
});
</code></pre>
<h3><strong>3.2 自动检测语言与 Cookie 记忆</strong></h3>
<p>设置 <code>localeDetection: false</code> 禁用浏览器自动检测语言的功能。用户语言偏好通过 cookie (<code>NEXT\_LOCALE</code>) 记忆。</p>
<h2><strong>四、语言切换组件与导航链接</strong></h2>
<h3><strong>4.1 语言切换组件</strong></h3>
<p>可以实现多个语言切换组件，如 <code>LocaleSwitcher</code> 和 <code>LocaleSelector</code>，允许用户手动切换语言：</p>
<pre><code class="language-javascript">// /src/components/layout/locale-switcher.tsx
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/router';

export default function LocaleSwitcher() {
  const router = useRouter();
  const { locales } = router;
  const t = useTranslations('Common');

  return (
    &lt;select
      value={router.locale}
      onChange={(e) =&gt; router.push(router.asPath, router.asPath, { locale: e.target.value })}
    &gt;
      {locales?.map((locale) =&gt; (
        &lt;option key={locale} value={locale}&gt;
          {t(`language.${locale}`)}
        &lt;/option&gt;
      ))}
    &lt;/select&gt;
  );
}
</code></pre>
<h3><strong>4.2 带翻译的导航链接</strong></h3>
<p>使用自定义的 <code>LocaleLink</code> 组件生成带语言前缀的链接：</p>
<pre><code class="language-javascript">import { LocaleLink } from '@/i18n/navigation';

&lt;LocaleLink href=&quot;/dashboard&quot;&gt;Dashboard&lt;/LocaleLink&gt;
</code></pre>
<h2><strong>五、TypeScript 类型定义与类型安全</strong></h2>
<p>为了确保类型安全并提供更好的开发体验，项目中使用全局类型定义文件 <code>global.d.ts</code> 扩展 <code>AppConfig</code> 接口，提供 <code>Locale</code> 和 <code>Messages</code> 类型定义：</p>
<pre><code class="language-javascript">// global.d.ts
import type { routing } from '@/i18n/routing';
import type messages from './messages/en.json';

declare module 'next-intl' {
  interface AppConfig {
    Locale: (typeof routing.locales)[number];
    Messages: typeof messages;
  }
}
</code></pre>
<p>该类型定义文件通常位于项目根目录，并需在 <code>tsconfig.json</code> 中显式包含以确保被识别。</p>
<p>此外，还可以通过 <strong>fumadocs-ui</strong> 提供的 i18n 工具自动生成 <code>Translations</code> 类型定义，确保类型安全和自动补全支持。</p>
<h2><strong>六、新增语言的正确做法</strong></h2>
<p>当你需要扩展新的语言或新增翻译字段时，应遵循以下步骤：</p>
<ol>
<li><strong>添加新语言 JSON 文件</strong> 到 <code>/messages/</code> 目录，如 <code>es.json</code>。</li>
<li><strong>在 <strong><code>**website.tsx**</code></strong> 中添加该语言配置</strong>，提供国旗、名称等元信息。</li>
<li><strong>在 <strong><code>**routing.ts**</code></strong> 中添加语言支持</strong>，同步更新语言列表。</li>
<li>页面中使用 <code>useTranslations('Namespace.key')</code> 即可生效。</li>
</ol>
<p>这样可以保证国际化系统的一致性和可维护性。</p>
<h2><strong>七、国际化布局与上下文注入机制</strong></h2>
<h3><strong>7.1 LocaleLayout 组件的作用</strong></h3>
<p>在 Next.js 的 App Router 中，<code>\[locale\]</code> 路由下的 <code>LocaleLayout</code> 是整个国际化的<strong>根布局组件</strong>。它的主要职责包括：</p>
<ul>
<li><strong>加载当前语言的翻译消息文件</strong>（如 <code>/messages/en.json</code> 或 <code>/messages/zh.json</code>）</li>
<li><strong>注入国际化上下文</strong>，通过 <code>NextIntlClientProvider</code></li>
<li><strong>设置 HTML 标签的语言属性</strong>，确保 SEO 和无障碍支持</li>
<li><strong>包裹页面内容</strong>，使所有子组件都能基于正确的语言环境渲染</li>
</ul>
<p>示例代码片段：</p>
<pre><code class="language-javascript">export default async function LocaleLayout({
  children,
  params: Promise&lt;{ locale: string }&gt;
}) {
  // 加载对应语言的消息文件
  const messages = (await import(`@/messages/${locale}.json`)).default;

  return (
    &lt;html suppressHydrationWarning lang={locale}&gt;
      &lt;body {...props}&gt;
        &lt;NextIntlClientProvider messages={messages} locale={locale}&gt;
          &lt;Providers&gt;{children}&lt;/Providers&gt;
        &lt;/NextIntlClientProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<blockquote>
<p>⚠️ 注意：虽然目前项目中未显式导入消息文件，但 NextIntlClientProvider 默认会从 /messages/<locale>.json 自动加载翻译消息。</p>
</blockquote>
<h3><strong>7.2 NextIntlClientProvider 的作用</strong></h3>
<p><code>NextIntlClientProvider</code> 是 <code>next-intl</code> 提供的核心组件之一，它通过 React Context 将翻译数据传递给所有客户端组件。</p>
<p><strong>主要功能：</strong></p>
<table>
<thead>
<tr>
<th><strong>功能</strong></th>
<th><strong>描述</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>翻译注入</td>
<td>将 <code>messages</code> 注入 React 上下文，使 <code>useTranslations</code> 可用</td>
</tr>
<tr>
<td>客户端支持</td>
<td>所有使用 <code>useTranslations</code> 的组件均可访问翻译键值</td>
</tr>
<tr>
<td>本地化格式化</td>
<td>支持日期、时间、数字等本地化格式输出</td>
</tr>
</tbody>
</table>
<p><strong>示例用法：</strong></p>
<pre><code class="language-javascript">import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage.hero');
  return &lt;h1&gt;{t('title')}&lt;/h1&gt;;
}
</code></pre>
<blockquote>
<p>📌 提示：如果你希望手动控制消息加载方式，可以显式传入 messages 属性给 NextIntlClientProvider。</p>
</blockquote>
<h3><strong>7.3 整体流程图解</strong></h3>
<pre><code class="language-javascript">[LocaleLayout]
   ↓
加载 messages/locale.json
   ↓
包裹 &lt;NextIntlClientProvider&gt; 注入翻译数据
   ↓
客户端组件通过 useTranslations 获取翻译内容
   ↓
渲染多语言 UI
</code></pre>
<h3><strong>4. 最佳实践建议</strong></h3>
<ul>
<li>✅ 新增语言时只需添加 <code>/messages/&lt;new-locale&gt;.json</code> 文件即可生效</li>
<li>✅ 在 <strong>website.tsx</strong> 中注册语言元信息</li>
<li>✅ 在 <strong>routing.ts</strong> 中同步更新语言列表</li>
<li>❗ 不建议修改 <code>LocaleLayout</code> 的结构，除非你有特殊需求</li>
</ul>
<p>通过以上机制，可以实现了简洁、可维护且高效的国际化系统，适合大型 SaaS 应用的多语言需求。</p>
<h2><strong>八、翻译消息加载机制详解</strong></h2>
<h3><strong>8.1 为什么没有显式导入翻译消息？</strong></h3>
<p>在 <code>/src/app/\[locale\]/layout.tsx</code> 中，你<strong>不会看到类似 <strong><code>**import messages from '@/messages/zh.json'**</code></strong> 的代码片段</strong>，这是因为 <strong>next-intl</strong> 提供了<strong>自动加载翻译消息</strong>的能力。</p>
<p><strong>✅ 自动加载机制</strong></p>
<p><code>NextIntlClientProvider</code> 默认会从 <code>/messages/&lt;locale&gt;.json</code> 路径加载对应语言的消息文件。只要项目结构符合预期（如 <code>/messages/en.json</code> 和 <code>/messages/zh.json</code> 存在），就可以无需手动导入。</p>
<p>示例代码如下：</p>
<pre><code class="language-plain">&lt;NextIntlClientProvider&gt;
  &lt;Providers&gt;{children}&lt;/Providers&gt;
&lt;/NextIntlClientProvider&gt;
</code></pre>
<blockquote>
<p>📌 注意：这里并没有传入 messages 属性，表示使用默认的自动加载方式。</p>
</blockquote>
<p><strong>⚠️ 手动加载的适用场景</strong></p>
<p>如果你需要自定义路径、延迟加载或调试，可以显式导入并传入 <code>messages</code> 属性：</p>
<pre><code class="language-typescript">const messages = (await import(`@/messages/${locale}.json`)).default;

return (
  &lt;html suppressHydrationWarning lang={locale}&gt;
    &lt;body {...props}&gt;
      &lt;NextIntlClientProvider messages={messages} locale={locale}&gt;
        &lt;Providers&gt;{children}&lt;/Providers&gt;
      &lt;/NextIntlClientProvider&gt;
    &lt;/body&gt;
  &lt;/html&gt;
);
</code></pre>
<p>但目前项目中不推荐这样做，因为结构和配置已完全符合自动加载的要求。</p>
<h3><strong>8.2 翻译消息文件路径约定</strong></h3>
<ul>
<li>消息文件必须存放在 <code>/messages/&lt;locale&gt;.json</code>。</li>
<li>示例：
<ul>
<li>/messages/en.json</li>
<li>/messages/zh.json</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 建议：新增语言时只需添加对应的 JSON 文件即可生效，无需修改 layout.tsx。</p>
</blockquote>
<h3><strong>8.3 加载方式对比</strong></h3>
<table>
<thead>
<tr>
<th><strong>方式</strong></th>
<th><strong>是否需要手动导入</strong></th>
<th><strong>使用场景</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>✅ 自动加载</td>
<td>否</td>
<td>标准项目结构、消息文件在 <code>/messages/&lt;locale&gt;.json</code></td>
</tr>
<tr>
<td>🔁 手动加载</td>
<td>是</td>
<td>自定义消息路径、动态加载、服务器渲染等高级用法</td>
</tr>
</tbody>
</table>
<h3><strong>8.4 最佳实践建议</strong></h3>
<ul>
<li>✅ 新增语言时只需添加 <code>/messages/&lt;new-locale&gt;.json</code> 文件即可生效</li>
<li>✅ 在 website.tsx 中注册语言元信息</li>
<li>✅ 在 routing.ts 中同步更新语言列表</li>
<li>❗ 不建议修改 <code>LocaleLayout</code> 的结构，除非你有特殊需求</li>
</ul>
<p>通过以上机制，MkSaaS 模板实现了简洁、可维护且高效的国际化系统，适合大型 SaaS 应用的多语言需求。</p>
<h2><strong>九、语言配置的职责划分：website.tsx 与 routing.ts 的作用解析</strong></h2>
<p>在 MkSaaS 模板中，国际化（i18n）的语言配置分别定义在两个文件中：</p>
<ul>
<li><strong>website.tsx</strong>：用于提供语言元信息和 UI 显示；</li>
<li><strong>routing.ts</strong>：用于控制路由系统中的多语言行为。</li>
</ul>
<h3><strong>9.1 website.tsx：UI 层语言配置</strong></h3>
<p><strong>路径</strong>: /src/config/website.tsx</p>
<p><strong>示例代码：</strong></p>
<pre><code class="language-javascript">i18n: {
  defaultLocale: 'en',
  locales: {
    en: { flag: '🇺🇸', name: 'English' },
    zh: { flag: '🇨🇳', name: '中文' }
  }
}
</code></pre>
<p><strong>作用：</strong></p>
<ul>
<li>定义网站支持的语言列表及默认语言；</li>
<li>提供语言元信息（如国旗、名称），用于 UI 显示；</li>
<li>被多个组件引用，例如语言切换器、页面标题生成等。</li>
</ul>
<blockquote>
<p>📌 这是面向前端 UI 的语言配置，主要用于控制语言选择器、显示标签等。</p>
</blockquote>
<h3><strong>9.2 routing.ts：框架层语言路由配置</strong></h3>
<p><strong>路径</strong>: <strong>/src/i18n/routing.ts</strong></p>
<p><strong>示例代码：</strong></p>
<pre><code class="language-javascript">export const routing = defineRouting({
  locales: LOCALES,
  defaultLocale: DEFAULT_LOCALE,
  localeDetection: false,
  localeCookie: { name: LOCALE_COOKIE_NAME },
  localePrefix: 'as-needed'
});
</code></pre>
<p><strong>作用：</strong></p>
<ul>
<li>配置 App Router 中的国际化路由规则；</li>
<li>控制 URL 是否添加语言前缀（如 <code>/en/about</code> vs <code>/about</code>）；</li>
<li>设置语言检测方式（浏览器自动检测 or cookie）；</li>
<li>定义多语言 Cookie 名称（如 <code>NEXT\_LOCALE</code>）。</li>
</ul>
<blockquote>
<p>📌 这是面向 Next.js 路由系统的语言配置，主要用于控制语言感知的路由行为和请求处理。</p>
</blockquote>
<h3><strong>9.3 为什么不统一到一个地方？</strong></h3>
<p>虽然从表面上看将语言配置合并到一个文件中似乎更简洁，但实际原因如下：</p>
<table>
<thead>
<tr>
<th><strong>维度</strong></th>
<th><strong>website.tsx</strong></th>
<th><strong>routing.ts</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>用途</strong></td>
<td>提供语言元信息（UI 层）</td>
<td>控制路由行为（框架层）</td>
</tr>
<tr>
<td><strong>使用者</strong></td>
<td>前端组件、翻译钩子</td>
<td>Next.js App Router、中间件</td>
</tr>
<tr>
<td><strong>功能</strong></td>
<td>显示语言选项、翻译内容</td>
<td>多语言 URL、Cookie 存储、路由匹配</td>
</tr>
<tr>
<td><strong>是否可合并？</strong></td>
<td>❌ 不建议</td>
<td>❌ 不建议</td>
</tr>
</tbody>
</table>
<p><strong>✅ 正确做法：同步更新两处配置</strong></p>
<p>当你新增一种语言（如西班牙语 <code>es</code>）时，应执行以下步骤：</p>
<p><strong>1、 在 website.tsx 添加语言配置</strong></p>
<pre><code class="language-javascript">i18n: {
  defaultLocale: 'en',
  locales: {
    en: { flag: '🇺🇸', name: 'English' },
    zh: { flag: '🇨🇳', name: '中文' },
    es: { flag: '🇪🇸', name: 'Español' } // 新增语言
  }
}
</code></pre>
<p><strong>2、 在 routing.ts 更新语言列表</strong></p>
<pre><code class="language-javascript">ts // 自动从 websiteConfig 获取语言列表 export const LOCALES = Object.keys(websiteConfig.i18n.locales); // 包含 ['en', 'zh', 'es']
</code></pre>
<p><strong>3、 创建对应语言的消息文件</strong></p>
<p><code>/messages/es.json</code></p>
<p><strong>✅ 总结</strong></p>
<table>
<thead>
<tr>
<th><strong>问题</strong></th>
<th><strong>回答</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>为什么要分开配置？</strong></td>
<td>因为 website.tsx 提供 UI 元信息，routing.ts 控制路由行为，二者职责不同。</td>
</tr>
<tr>
<td><strong>能否统一？</strong></td>
<td>不建议，违反单一职责原则且可能导致类型冲突。</td>
</tr>
<tr>
<td><strong>新增语言应怎么做？</strong></td>
<td>在 website.tsx 添加语言元信息，在 routing.ts 同步更新，并创建对应 JSON 消息文件。</td>
</tr>
</tbody>
</table>
<p>这样设计确保了国际化系统的灵活性和可维护性，是符合最佳实践的结构.</p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Sun, 13 Jul 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>使用 Content Collections 高效管理和渲染文档、博客文章等结构化内容</title>
      <link>https://codexun.com/posts/efficient-content-management-with-content-collections</link>
      <guid isPermaLink="true">https://codexun.com/posts/efficient-content-management-with-content-collections</guid>
      <description>Content Collections 是一个用于管理内容的模块化工具，常用于从 content/ 目录中提取和处理 .mdx 和 .json 文件，并将其转换为结构化的数据集合。 它非常适合用于文档、博客文章、静态页面等内容驱动型模块的数据建模。 一、安装依赖 确保你已经安装了必要的依赖包，如： pnpm add @</description>
      <content:encoded><![CDATA[<p>Content Collections 是一个用于管理内容的模块化工具，常用于从 <code>content/</code> 目录中提取和处理 <code>.mdx</code> 和 <code>.json</code> 文件，并将其转换为结构化的数据集合。</p>
<p>它非常适合用于文档、博客文章、静态页面等内容驱动型模块的数据建模。</p>
<h2><strong>一、安装依赖</strong></h2>
<p>确保你已经安装了必要的依赖包，如：</p>
<pre><code class="language-shell">pnpm add @content-collections/core @fumadocs/content-collections
</code></pre>
<h2><strong>二、配置路径别名（tsconfig.json）</strong></h2>
<p>在项目根目录下的 tsconfig.json 中添加路径映射，以便在代码中直接引用 content-collections：</p>
<pre><code class="language-json">{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;content-collections&quot;: [&quot;./content-collections&quot;]
    }
  }
}
</code></pre>
<p>这样你就可以通过以下方式导入内容集合：</p>
<pre><code class="language-typescript">import { allPosts } from 'content-collections';
</code></pre>
<h2><strong>三、创建 content-collections.ts 配置文件</strong></h2>
<p>在项目根目录创建或修改 <code>content-collections.ts</code>，定义内容集合。例如：</p>
<pre><code class="language-typescript">// content-collections.ts
import { defineCollection, defineConfig } from '@content-collections/core';
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing';
import {
  createDocSchema,
  createMetaSchema,
  transformMDX,
} from '@fumadocs/content-collections/configuration';

const docs = defineCollection({
  name: 'docs',
  directory: 'content/docs',
  include: '**/*.mdx',
  schema: (z) =&gt; ({
    ...createDocSchema(z),
    preview: z.string().optional(),
    index: z.boolean().default(false),
  }),
  transform: transformMDX,
});

export default defineConfig({
  collections: [docs],
});

</code></pre>
<h3>1、配置的作用</h3>
<p>这个配置文件做了以下几件事：</p>
<ul>
<li><strong>定义集合</strong>：使用 <code>defineCollection</code> 定义了一个名为 <code>docs</code> 的集合。</li>
<li><strong>指定目录</strong>：告诉程序去 <code>content/docs</code> 目录下查找 <code>.mdx</code> 文件。</li>
<li><strong>国际化支持</strong>：导入国际化相关的语言配置（默认语言和所有支持的语言列表）。</li>
<li><strong>设置 schema</strong>：定义 frontmatter 元信息的字段类型和默认值。</li>
<li><strong>数据转换</strong>：使用 <code>transformMDX</code> 将 <code>.mdx</code> 文件正文解析为 HTML 并生成目录（TOC）。</li>
</ul>
<p>在 content-collections.ts 文件中，schema 和 transform 是两个非常关键的配置项，它们分别用于定义内容集合的数据结构和数据转换逻辑。</p>
<h3><strong>2、schema 的作用</strong></h3>
<p>用途: 定义每个集合中内容的字段类型、默认值和验证规则。 处理的数据: 每个 .mdx 或 .json 文件中的 frontmatter（文件头部的元信息）。</p>
<p>实现方式: 使用 Zod 库来创建类型安全的 schema。 确保读取的内容具有预期的字段，并对字段进行类型校验。 可以设置默认值或可选字段，以提高灵活性。</p>
<h3><strong>3、transform 的作用</strong></h3>
<p>用途: 对集合中的每一条内容进行自定义转换，生成更复杂或更适用于前端展示的数据结构。</p>
<p>处理的数据: 包括文件路径、frontmatter 元信息以及文件正文内容。</p>
<p>实现方式: 接收原始内容对象 (data) 和上下文 (context) 返回一个经过处理的新对象 可以访问其他集合的内容（如作者、分类），进行关联处理</p>
<h2><strong>四、创建内容文件</strong></h2>
<p>在 <code>content/docs</code> 目录下创建一些 <code>.mdx</code> 文件，例如：</p>
<p>示例：content/docs/introduction.mdx</p>
<pre><code class="language-markdown">---
title: Introduction
description: Welcome to the documentation.
date: 2025-07-10T10:00:00Z
published: true
---

# Introduction

Welcome to the documentation of this project.
</code></pre>
<h2><strong>五、在组件中使用内容集合</strong></h2>
<p>现在你可以直接在页面组件中导入并使用这些内容：</p>
<p>示例：src/app/docs/page.tsx</p>
<pre><code class="language-typescript">import { allDocs } from 'content-collections';

export default function DocsPage() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Documentation&lt;/h1&gt;
      &lt;ul&gt;
        {allDocs.map((doc) =&gt; (
          &lt;li key={doc.slug}&gt;
            &lt;h2&gt;{doc.title}&lt;/h2&gt;
            &lt;p&gt;{doc.description}&lt;/p&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h2><strong>六、构建与运行</strong></h2>
<p>确保你的开发服务器正在运行：</p>
<pre><code class="language-shell">pnpm dev
</code></pre>
<p>访问 <code>/docs</code> 路由，你应该能看到所有 <code>.mdx</code> 文件的内容被正确渲染出来。</p>
<p>在 package.json 中配置以下命令：</p>
<pre><code class="language-json">&quot;dev&quot;: &quot;concurrently \&quot;content-collections watch\&quot; \&quot;next dev\&quot;&quot;,
&quot;build&quot;: &quot;content-collections build &amp;&amp; next build&quot;
</code></pre>
<ul>
<li><code>dev</code>: 同时启动内容监听与 Next.js 开发服务器，实现热更新。</li>
<li><code>build</code>: 先构建内容集合数据，再构建整个 Next.js 应用，确保部署前数据同步完成。</li>
</ul>
<h2><strong>七、编译输出命名规范</strong></h2>
<p>在使用 Content Collections 管理内容的项目中，所有集合的完整数据输出文件采用统一命名格式：<code>all&lt;CollectionName&gt;.js</code>（如 <code>allPosts.js</code>, <code>allDocs.js</code>）。该命名方式表示“包含某类内容的全部数据”，具有以下特点：</p>
<ul>
<li><strong>语义清晰</strong>：便于开发者快速理解文件用途。</li>
<li><strong>约定优于配置</strong>：提升项目一致性，避免歧义。</li>
<li><strong>易于检索</strong>：在大型项目中更方便查找特定集合的全部数据。</li>
<li><strong>构建流程集成</strong>：由 <code>content-collections.ts</code> 中定义的集合自动构建生成，结合 schema 校验和 transform 处理后输出结构化数据。</li>
</ul>
<p>建议保持默认命名风格以确保一致性与兼容性，除非构建工具明确支持自定义输出格式。</p>
<h2><strong>八、总结</strong></h2>
<p>总之，content-collections 提供了一种高效的方式来管理和渲染文档、博客文章等结构化内容。通过合理配置，可以大幅提升开发效率和内容维护能力。</p>
<ul>
<li>所有内容文件放在 <code>content/</code> 目录下按类别组织</li>
<li>schema 中使用 Zod 进行类型校验，确保数据一致性</li>
<li>transform 函数用于生成路由、关联其他集合数据、计算阅读时间等</li>
<li>每个集合需定义 <code>name</code>, <code>directory</code>, <code>include</code>, <code>schema</code>, <code>transform</code></li>
<li>集合名称建议使用复数形式（如 <code>posts</code>, <code>docs</code>）；输出文件自动命名为 <code>all&lt;CollectionName&gt;.js</code>（如 <code>allPosts.js</code>）</li>
<li>可通过封装中间层统一导出所有集合数据：<code>@/lib/content/index.ts</code></li>
<li>确保 <code>.mdx</code> 文件 frontmatter 符合 schema 定义的字段类型</li>
<li>多语言支持可通过文件名后缀识别（如 <code>file.en.mdx</code>, <code>file.zh.mdx</code>）</li>
</ul>]]></content:encoded>
      <category>前端</category>
      <pubDate>Sat, 12 Jul 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Android 中监听 Home 键与 Overview 键的实现原理</title>
      <link>https://codexun.com/posts/android-home-overview-keys-monitoring-implementation</link>
      <guid isPermaLink="true">https://codexun.com/posts/android-home-overview-keys-monitoring-implementation</guid>
      <description>在 Android 开发中，出于安全和隐私考虑，系统默认不会为 Home 键或 Overview 键（最近任务键）发送全局广播。然而，在某些场景下（如埋点、行为统计、通知触发等），我们仍然需要感知这些按键事件的发生。 本文将详细讲解如何通过 ACTION\_CLOSE\_SYSTEM\_DIALOGS 广播来间接监听这</description>
      <content:encoded><![CDATA[<p>在 Android 开发中，出于安全和隐私考虑，<strong>系统默认不会为 Home 键或 Overview 键（最近任务键）发送全局广播</strong>。然而，在某些场景下（如埋点、行为统计、通知触发等），我们仍然需要感知这些按键事件的发生。</p>
<p>本文将详细讲解如何通过 <code>ACTION\_CLOSE\_SYSTEM\_DIALOGS</code> 广播来间接监听这些按键，并对比 <strong>动态注册广播接收器</strong> 与 <strong>静态注册广播接收器</strong> 。</p>
<hr>
<h2>**一、实现原理：监听 **<code>**ACTION\_CLOSE\_SYSTEM\_DIALOGS**</code></h2>
<h3><strong>1.1 原理概述</strong></h3>
<p>虽然不能直接监听 Home 键或 Overview 键的按下动作，但可以通过监听系统广播 <code>Intent.ACTION\_CLOSE\_SYSTEM\_DIALOGS</code> 来“推测”用户是否按下了这些按键。</p>
<p>该广播会在以下情况被触发：</p>
<ul>
<li>用户按下 <strong>Home 键</strong></li>
<li>用户按下 <strong>Overview 键（Recent Apps）</strong></li>
</ul>
<h3><strong>1.2 获取关闭原因字段</strong></h3>
<p>在接收到广播后，通过以下方式获取关闭对话框的原因：</p>
<pre><code class="language-java">String reason = intent.getStringExtra(&quot;reason&quot;);
</code></pre>
<p>常见值包括：</p>
<table>
<thead>
<tr>
<th><strong>值</strong></th>
<th><strong>含义</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&quot;homekey&quot;</code></td>
<td>用户按下了 Home 键</td>
</tr>
<tr>
<td><code>&quot;recentapps&quot;</code></td>
<td>用户按下了 Overview 键</td>
</tr>
</tbody>
</table>
<h3><strong>1.3 示例代码</strong></h3>
<p>通过下面的代码，注册广播接收器，监听系统广播，达到识别用户按钮的目的。</p>
<pre><code class="language-java">public class TouchKeyReceiver extends BroadcastReceiver {
    private static final String SYSTEM_DIALOG_REASON_KEY = &quot;reason&quot;;
    private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = &quot;recentapps&quot;;
    private static final String SYSTEM_DIALOG_REASON_HOME = &quot;homekey&quot;;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
            String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
            if (SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)) {
                // Overview 键
            } else if (SYSTEM_DIALOG_REASON_HOME.equals(reason)) {
                // Home 键
            }
        }
    }
}
</code></pre>
<h2><strong>二、广播注册方式对比：动态 vs 静态</strong></h2>
<p>Android 提供了两种注册广播接收器的方式：<strong>动态注册</strong> 和 <strong>静态注册</strong>。它们在生命周期、使用场景、兼容性等方面有明显差异。</p>
<h3><strong>2.1 动态注册和静态注册的特点对比：</strong></h3>
<table>
<thead>
<tr>
<th><strong>特性</strong></th>
<th>动态注册</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>注册位置</td>
<td>Java/Kotlin 代码中（如 <code>onCreate()</code> 方法内）</td>
<td>在 <code>AndroidManifest.xml</code> 文件中声明</td>
</tr>
<tr>
<td>生命周期</td>
<td>依赖于组件（如 Activity 或 Application）</td>
<td>应用安装后即可接收广播</td>
</tr>
<tr>
<td>触发条件</td>
<td>应用运行时才能接收广播</td>
<td>即使应用未运行，系统也会尝试唤醒进程处理广播</td>
</tr>
<tr>
<td>灵活性</td>
<td>可根据需要动态添加/移除监听</td>
<td>Android 8.0+ 对隐式广播限制较多</td>
</tr>
<tr>
<td>权限控制</td>
<td>支持 <code>Context.RECEIVER\_EXPORTED</code> 控制外部访问</td>
<td>默认 <code>exported=false</code>，需显式设置才可接收外部广播</td>
</tr>
</tbody>
</table>
<h3><strong>2.2 动态示例代码</strong></h3>
<pre><code class="language-java">IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
registerReceiver(taskKeyReceiver, filter, Context.RECEIVER_EXPORTED);
</code></pre>
<h3><strong>2.3 静态注册示例</strong></h3>
<pre><code class="language-xml">&lt;receiver android:name=&quot;.TouchKeyReceiver&quot;&gt;
    &lt;intent-filter&gt;
        &lt;action android:name=&quot;android.intent.action.CLOSE_SYSTEM_DIALOGS&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/receiver&gt;
</code></pre>
<h2><strong>三、Android 8.0+ 的广播限制</strong></h2>
<table>
<thead>
<tr>
<th><strong>广播类型</strong></th>
<th><strong>是否支持静态注册</strong></th>
<th><strong>备注</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ACTION\_CLOSE\_SYSTEM\_DIALOGS</code></td>
<td>❌ 不支持</td>
<td>包括 Home 键和 Recent Apps 键事件</td>
</tr>
<tr>
<td><code>ACTION\_USER\_PRESENT</code>（解锁）</td>
<td>✅ 支持</td>
<td>可以静态注册</td>
</tr>
<tr>
<td>自定义广播</td>
<td>✅ 支持</td>
<td>需要配置 <code>exported=true</code> 和权限</td>
</tr>
</tbody>
</table>
<blockquote>
<p>⚠️ 随着 Android 系统版本升级，Google 对后台广播的限制越来越多，尤其是系统广播。从 Android 8.0 开始，很多系统广播（如 ACTION_CLOSE_SYSTEM_DIALOGS）不再支持静态注册，比如上面的关播接收器需要进行动态注册。</p>
</blockquote>
<h2><strong>四、扩展建议</strong></h2>
<ul>
<li>对于需要“持久化”监听广播的场景，可以结合 <code>Service</code> 或 <code>JobScheduler</code> 实现保活；</li>
<li>使用 <code>WorkManager</code> 替代传统广播机制，适应 Android 10+ 的后台限制；</li>
<li>对于自定义广播，建议使用 <code>LocalBroadcastManager</code> 提高安全性与性能；</li>
<li>如果广播逻辑复杂，建议封装成独立模块并配合 <code>LiveData</code> 或 <code>EventBus</code> 使用。</li>
</ul>]]></content:encoded>
      <category>安卓</category>
      <pubDate>Fri, 11 Jul 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Github 仓库操作— token使用，Action 操作权限</title>
      <link>https://codexun.com/posts/github-action-permission-and-personal-access-token</link>
      <guid isPermaLink="true">https://codexun.com/posts/github-action-permission-and-personal-access-token</guid>
      <description>由于最近有在 mac 上操作多个 Github 账号代码库的情况。 记得之前操作的方式，是配置 ssh key等操作权限，需要在 mac 隐藏目录.ssh下面的known_hosts文件进行编辑，将相应 key 填写进去。 或者使用用户名密码操作的方式，不过随着 GitHub 对用户安全要求的提升，GitHub 已经停</description>
      <content:encoded><![CDATA[<p>由于最近有在 mac 上操作多个 Github 账号代码库的情况。</p>
<p>记得之前操作的方式，是配置 ssh key等操作权限，需要在 mac 隐藏目录.ssh下面的known_hosts文件进行编辑，将相应 key 填写进去。</p>
<p>或者使用用户名密码操作的方式，不过随着 GitHub 对用户安全要求的提升，GitHub 已经停止了对基于用户名和密码的访问认证，改为使用 <strong>Personal Access Token（个人访问令牌）</strong> 来进行身份认证。</p>
<h2>1、使用 **Personal Access Token（个人访问令牌）**操作不同账号的代码库</h2>
<p>又比如在使用GitHub的时候, 提交命令 <code>git push</code> 时，可能会遇到以下错误提示：</p>
<pre><code class="language-shell">$ git push
Usernamefor 'https://github.com': Username
Passwordfor 'https://Username@github.com':
remote: Invalid username or password.
fatal: Authentication failedfor 'https://github.com/username/repository.git/'
</code></pre>
<p>解决办法就是使用最新的 **Personal Access Token (PAT) **的方式操作代码仓库。</p>
<h3>1.1 创建 **Personal Access Token **</h3>
<p>登录 GitHub 账号，进入设置中心，左侧菜单底部进入到开发者设置（ <strong>Developer settings</strong>），进到页面里如图创建点击 <strong>Tokens (classic)</strong>，创建传统访问令牌。</p>
<p><img src="/api/images/image/2026/07/d13b88cee34ce59b-image.png" alt="image.png"></p>
<p>创建令牌 Token的页面，输入 name、选择过期时间、仓库权限等等，填好之后最下面点击<strong>Generate token（生成令牌）</strong> 按钮，生成新的访问令牌。</p>
<p><img src="/api/images/image/2026/07/b5b9f1a65f4e3827-image.png" alt="image.png"></p>
<p><strong>注意</strong>：令牌生成后只会显示一次，请立即复制并妥善保存。此令牌将用于后续的 <a href="https://codexun.com/tags/github">Git </a>操作。</p>
<p><img src="/api/images/image/2026/07/2777d9f3c6f0994b-image.png" alt="image.png"></p>
<h3>1.2 使用令牌进行 Git 操作</h3>
<p>当你下次运行 <code>git push</code> 或其他需要身份认证的 Git 操作时，使用生成的访问令牌作为密码即可。示例操作：</p>
<pre><code class="language-shell">$ git push
Usernamefor 'https://github.com': your_username
Passwordfor 'https://github.com': &lt;生成的Personal Access Token&gt;
</code></pre>
<p>或者更简单的，将 token 填入到远程仓库 url 路径里**【不要忘记@符号】**，比如操作：</p>
<pre><code class="language-shell">拉取github仓库：
git clone https://[token]@github.com/username/xxx.git

添加远程仓库：
git remote add origin https://[token]@github.com/username/xxx.git
</code></pre>
<p>如果是配合 sourcetree 进行操作，可以在仓库设置里，远程仓库进行编辑，添加 **[token] @ **到远程仓库URL 路径里，这样就不用每次都输入访问令牌的密码了。</p>
<p><img src="/api/images/image/2026/07/3b77d30d4b1f6557-image.png" alt="image.png"></p>
<h2>2、给Github Action 相关权限</h2>
<p>之前写过几期使用 Github Action 自动执行任务、部署的教程，比如定时部署到 Vercle、Netlify上，就是定时执行 hook 地址，就可以触发一次部署了，没有涉及到 action 修改代码提交的情况。</p>
<p><strong><a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">使用 Github Action自动执行 Netlify 部署</a></strong></p>
<p><strong><a href="/posts/automating-vercel-deployment-with-github-actions">使用 Github Action自动执行Vercel 部署</a></strong></p>
<p><strong><a href="/posts/github-actions-deploy-vercel">使用自定义 GitHub Actions CI/CD 部署到 Vercel</a></strong></p>
<p><strong><a href="/posts/github-actions-workflow-with-inputs">GitHub Actions：带有输入的调度和计划工作流程</a></strong></p>
<p>不过最近写了一个 <a href="https://bing.codexun.com/">bing 壁纸预览和下载</a> 的学习网站，有每天定时获取最新 bing wallpaper 数据，提交到代码仓库中，然后再进行部署。</p>
<p>在调试的过程中，开始失败就是忘记了设置代码库的 github action 权限，这里记录一下。</p>
<pre><code class="language-shell">错误日志：
ERROR - Write access to repository not granted

remote: Write access to repository not granted.
fatal: unable to access 'https://github.com/xxx/xxx/': The requested URL returned error: 403
Error: Process completed with exit code 128.
</code></pre>
<p>设置路径在：你的代码仓库 → Settings -&gt; Actions -&gt; General</p>
<p><img src="/api/images/image/2026/07/b4b8aa9b4ba11dae-image.png" alt="image.png"></p>
<p>给定权限之后，再进行执行 github action 编写的 workflow 就没有错误问题了。</p>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Fri, 04 Jul 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>4.0 版本之后-在Mac上通过脚本实现微信双开[最新4.0.6.17版本]</title>
      <link>https://codexun.com/posts/wechat-open-on-macos-after-version-40</link>
      <guid isPermaLink="true">https://codexun.com/posts/wechat-open-on-macos-after-version-40</guid>
      <description>之前[4.0 版本之前—在Mac上通过脚本实现微信双开] 随着 mac 版本和微信版本的更新，已经失效。 下面以 双开4.0.6.17版（最详细教程） 举例。4.0.4、4.0.4、4.0.5 版本同理。 1、原理 复制微信应用并修改签名，使系统识别为不同应用。 用此方法也不影响微信输入法的正常使用。 2、适配版本 已</description>
      <content:encoded><![CDATA[<p>之前<a href="/posts/wechat-open-on-macos-before-version-40">[4.0 版本之前—在Mac上通过脚本实现微信双开]</a> 随着 mac 版本和微信版本的更新，已经失效。</p>
<p>下面以 <strong>双开4.0.6.17版</strong>（最详细教程） 举例。4.0.4、4.0.4、4.0.5 版本同理。</p>
<p><img src="/api/images/image/2026/07/f070582bbf80955e-image.png" alt="image.png"></p>
<h2><strong>1、原理</strong></h2>
<p>复制微信应用并修改签名，使系统识别为不同应用。 用此方法也不影响微信输入法的正常使用。</p>
<h2><strong>2、适配版本</strong></h2>
<p>已测试可支持的，适配微信版本（MAC）</p>
<ul>
<li>4.0.5.12（2025-05-15 已验证）</li>
<li>4.0.3.93（2025-04-16 已验证）</li>
</ul>
<h2><strong>3、操作步骤</strong></h2>
<h3>**第一步：**创建微信的「分身」</h3>
<p><strong>执行如下命令，复制微信应用</strong></p>
<pre><code class="language-plain">sudo cp -R /Applications/WeChat.app /Applications/WeChat2.app
</code></pre>
<p>会要求输入密码，直接输入即可。</p>
<h3>**第二步：**给 WeChat2.app </h3>
<p><strong>修改BundleID，防止微信检测为同一应用</strong></p>
<pre><code class="language-plain">sudo /usr/libexec/PlistBuddy -c &quot;Set :CFBundleIdentifier com.tencent.xinWeChat2&quot; /Applications/WeChat2.app/Contents/Info.plist
</code></pre>
<p>就是通过 <code>Set CFBundleIdentifier com.tencent.xinWeChat2</code> 鉴权的。</p>
<h3><strong>第三步：重新签名应用</strong></h3>
<p>第二步的延续，签名应用。输入如下命令，如果提示输入密码，输入即可。</p>
<pre><code class="language-plain">sudo codesign --force --deep --sign - /Applications/WeChat2.app
</code></pre>
<h3><strong>第四步：启动双开</strong></h3>
<p>手动打开第一个微信（原应用）；在终端执行如下命令，运行第二个微信，创建方法体。</p>
<pre><code class="language-plain">nohup /Applications/WeChat2.app/Contents/MacOS/WeChat &gt;/dev/null 2&gt;&amp;1 &amp;
</code></pre>
<h3><strong>第五步：常驻程序坞</strong></h3>
<p>第二个终端应用启动后，可以选择右键在程序坞中保留，这样下次直接点程序坞的程序就可以运行了，不需要再次使用终端命令打开第二个微信。</p>
<p><img src="/api/images/image/2026/07/48f759fed008f331-image.png" alt="image.png"></p>
<p>至此，我们的微信双开就完成了，有需要三开的，重复第二到第八步，将步骤中所有的wechat2.app都改成wechat3.app，就可以实现3开了。</p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Thu, 03 Jul 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>4.0 版本之前—在Mac上通过脚本实现微信双开</title>
      <link>https://codexun.com/posts/wechat-open-on-macos-before-version-40</link>
      <guid isPermaLink="true">https://codexun.com/posts/wechat-open-on-macos-before-version-40</guid>
      <description>注意：下面这种方法只适用于 mac 微信4.0 版本之前。验证Version. 3.8.10 (28632)是可以的。 可以参见**4.0 版本之后-在Mac上通过脚本实现微信双开[最新4.0.6.17版本]**** **最新微信版本上实现双开的处理办法。 本文主要介绍了如何在Mac上通过脚本实现微信双开，并通过自动操</description>
      <content:encoded><![CDATA[<p>注意：下面这种方法只适用于 mac 微信4.0 版本之前。验证Version. 3.8.10 (28632)是可以的。</p>
<p>可以参见**<a href="/posts/wechat-open-on-macos-after-version-40">4.0 版本之后-在Mac上通过脚本实现微信双开[最新4.0.6.17版本]</a>**** **最新微信版本上实现双开的处理办法。</p>
<p><img src="/api/images/image/2026/07/58d756aa09a0496e-image.png" alt="image.png"></p>
<p>本文主要介绍了如何在Mac上通过脚本实现微信双开，并通过自动操作创建应用程序，修改图标后直接当作第二个微信使用，无需打开终端输入代码，且双开后无终端出现。</p>
<h2><strong>1 、基本原理</strong></h2>
<blockquote>
<p>如果您不想理解原理，或者不懂Shell，可以直接跳转到最后章节。</p>
</blockquote>
<p>使用Linux nohup命令实现，将微信主程序输出到一个“黑洞”里，即使此时关掉终端也不会影响双开微信的运行。</p>
<p>将以下代码复制到终端（Teriminal）运行，即可开启第二个微信，开启后，关闭终端也可以正常运行。</p>
<pre><code class="language-plain">nohup /Applications/WeChat.app/Contents/MacOS/WeChat &gt; /dev/null 2&gt;&amp;1
</code></pre>
<p>弊端是每次都需要复制此代码到终端，非常不优雅，接下来的章节介绍如何使用自动操作打包。</p>
<h2><strong>2 、使用自动操作打包脚本</strong></h2>
<p>自动操作（Automator）是Mac电脑上自带的一款软件，通过设置，可以实现电脑上的大部分操作自动进行，可以简单理解为一个更加硬核的快捷指令（Shortcuts）。</p>
<ol>
<li>在启动台找到<strong>自动操作（Automator）</strong>，点开打开。</li>
</ol>
<p><img src="/api/images/image/2026/07/69316c5b68a022ed-image.png" alt="image.png"></p>
<ol>
<li>选择<strong>新建文稿</strong></li>
</ol>
<p><img src="/api/images/image/2026/07/cfc38376ce377d7c-image.png" alt="image.png"></p>
<ol>
<li>选择文稿类型为<strong>应用程序</strong>。</li>
</ol>
<p><img src="/api/images/image/2026/07/358fb59a3931f0d5-image.png" alt="image.png"></p>
<ol>
<li>找到<strong>实用工具——运行Shell脚本</strong>，双击或者拖拽至右侧空白处。</li>
</ol>
<p><img src="/api/images/image/2026/07/bf600ab6f56ba8f9-image.png" alt="image.png"></p>
<ol>
<li><strong>复制代码</strong>至文本框，Shell类型<strong>默认/bin/zsh或/bin/bash</strong>即可。</li>
</ol>
<pre><code class="language-plain">nohup /Applications/WeChat.app/Contents/MacOS/WeChat &gt; /dev/null 2&gt;&amp;1
</code></pre>
<p><img src="/api/images/image/2026/07/01ca716851dfa7ff-image.png" alt="image.png"></p>
<ol>
<li>保存文件（Cmd+S），名称随意，位置选择<strong>应用程序（Applications）</strong>，文件格式选择<strong>应用程序</strong>，存储即可。</li>
</ol>
<p><img src="/api/images/image/2026/07/48c95b2f0fee4d1b-image.png" alt="image.png"></p>
<ol>
<li>打开<strong>启动台</strong>，图标出现，点击即可启动第二个微信。</li>
</ol>
<p><img src="/api/images/image/2026/07/103fa6ca3ea3528e-image.png" alt="image.png"></p>
<h2><strong>3、 嫌丑换图标</strong></h2>
<p>这个图标只是脚本启动APP的图标，脚本执行之后出现的第二个微信还是原版本的，如果还是嫌丑就改图标，自己设计或者去网上找一个心仪的图标。我喜欢这个黑白的，因为有植物大战僵尸里面模仿者（Imitater）的感觉。</p>
<ol>
<li>复制（Cmd+C）下载好的图标文件。</li>
</ol>
<p><img src="/api/images/image/2026/07/4dcd7476c42378dc-image.png" alt="image.png"></p>
<ol>
<li>在应用程序（Applications）文件夹里找到刚保存的脚本程序，<strong>右击图标，点击显示简介</strong>。</li>
</ol>
<p><img src="/api/images/image/2026/07/c36d19f4a63de258-image.png" alt="image.png"></p>
<ol>
<li><strong>点击右上方的小图标</strong>，周围变蓝即可，然后粘贴（Cmd+V），即可完成。</li>
</ol>
<p><img src="/api/images/image/2026/07/c35e7ac3f5fa9e4b-image.png" alt="image.png"></p>
<ol>
<li>效果展示及懒人包</li>
</ol>
<p>把双开启动软件放在dock栏，点击之后就可以实现微信双开，而且全程无终端打开，也不会出现其他方法中关闭终端，第二个微信就关闭的情况。</p>
<p><img src="/api/images/image/2026/07/8ec4169d2156df5b-image.png" alt="image.png"></p>
<p>同样的，如果更改代码，也可以实现其他软件的双开，可以自行探索。</p>
<p>如果你实在太懒了，不想操作，那么点击下面链接，下载我修改好的可以直接使用。</p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>阿里网盘—若依系列框架视频教程大全</title>
      <link>https://codexun.com/posts/resource-aliyun-ruoyi-document-videos</link>
      <guid isPermaLink="true">https://codexun.com/posts/resource-aliyun-ruoyi-document-videos</guid>
      <description>RuoYi 是一个 Java EE 企业级快速开发平台，基于经典技术组合（Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap），内置模块如：部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。在线定时任务配置；支持集群，支持多数据</description>
      <content:encoded><![CDATA[<p><a href="https://ruoyi.vip/">RuoYi </a>是一个 Java EE 企业级快速开发平台，基于经典技术组合（Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap），内置模块如：部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。在线定时任务配置；支持集群，支持多数据源，支持分布式事务。</p>
<p><img src="/api/images/image/2026/07/f8d5e501feac9180-image.png" alt="image.png"></p>
<h2>1、若依框架系列视频教程（RuoYi 前端不分离版本）</h2>
<p>视频教程（共115节）</p>
<p>RuoYi是一个后台管理系统，基于经典技术组合（Spring Boot、Apache Shiro、MyBatis、Thymeleaf）主要目的让开发者注重专注业务，降低技术难度，从而节省人力成本，缩短项目周期，提高软件安全质量。</p>
<p>引导你逐步理解若依框架的核心功能和应用技巧。</p>
<blockquote>
<p>教程内容
├── 1 项目概述
├── 2 环境部署
├── 3 war包部署到tomcat
├── 4 文件结构介绍
├── 5 配置文件介绍
├── 6 SpringBoot简介
├── 7 服务器配置
├── 8 多环境使用
├── 9 配置文件获取
├── 10 自定义资源映射
├── 11 缓存使用
├── 12 异步调用
├── 13 定时任务
├── 14 热部署
├── 15 Shiro简介
├── 16 登录实现
├── 17 退出实现
├── 18 记住我实现
├── 19 验证码实现
├── 20 权限控制实现
├── 21 会话实现</p>
<p>├──。。。
├── 114 档案系统部署使用
├── 115 4.8.1更新介绍</p>
</blockquote>
<p>若依框架系列视频教程 - 前端不分离版本 - 带源码课件
<a href="https://www.alipan.com/s/ijsSiNUf1vt">https://www.alipan.com/s/ijsSiNUf1vt</a>
提取码: k94j
点击链接保存，或者复制本段内容，打开「阿里云盘」APP ，无需下载极速在线查看，视频原画倍速播放。</p>
<h2>2、若依框架系列视频教程（RuoYi-Vue 前后端分离版本）。</h2>
<p>视频教程（共137节）
RuoYi-Vue 是一个 Java EE 企业级快速开发平台，基于经典技术组合（Spring Boot、Spring Security、MyBatis、Jwt、Vue），内置模块如：部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。在线定时任务配置，支持集群，支持多数据源，支持分布式事务等。</p>
<p>专为前端开发者设计的Vue.js集成教程，深入讲解如何与后端若依框架无缝对接。</p>
<blockquote>
<p>教程内容
├── 01 项目概述
├── 02 项目运行
├── 03 构建发布到Linux
├── 04 系统菜单介绍
├── 05 开发环境运行
├── 06 表结构介绍
├── 07 目录文件介绍
├── 08 主配置文件介绍
├── 09 Spring Boot 简介
├── 10 Spring Security 简介
├── 11 Spring Security 配置介绍
├── 12 Spring Security 密码加密
├── 13 Spring Security 退出配置
├── 14 Spring Security 登录配置
├── 15 Spring Security 权限讲解
├── 16 Spring Security 权限注解
├── 17 JWT 介绍
├── 18 JWT 实现
├── 19 JWT 过滤器</p>
<p>├── 。。。
├── 136 3.8.9版本更新介绍
├── 137 3.9.0版本更新介绍
~~</p>
</blockquote>
<p>若依框架系列视频教程 - 前后端分离版本 - 带源码课件
<a href="https://www.alipan.com/s/fcKDRjwY8rX">https://www.alipan.com/s/fcKDRjwY8rX</a>
提取码: 0ue1
点击链接保存，或者复制本段内容，打开「阿里云盘」APP ，无需下载极速在线查看，视频原画倍速播放。</p>
<h2>3、若依框架系列视频教程（RuoYi-Cloud微服务版本）。</h2>
<p>视频教程（共146节）</p>
<p>针对微服务架构爱好者，、涵盖从微服务架构设计到具体实现的全过程。</p>
<p><strong>RuoYi-Cloud</strong> 是一个 Java EE 分布式微服务架构平台，基于经典技术组合（Spring Boot、Spring Cloud &amp; Alibaba、Vue、Element），内置模块如：部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、代码生成等。在线定时任务配置；支持集群，支持多数据源。</p>
<blockquote>
<p>教程内容</p>
<p>├── 01 项目概述
├── 02 微架构概念
├── 03 微架构及选型
├── 04 目录文件介绍
├── 05 项目运行
├── 06 构建发布到Linux
├── 07 开发环境运行
├── 08 服务网关介绍
├── 09 创建使用服务网关
├── 10 网关路由规则
├── 11 网关路由配置
├── 12 网关限流配置
├── 13 网关熔断降级
├── 14 网关跨域配置
├── 15 网关黑名单配置
├── 16 网关白名单配置
├── 17 网关全局过滤器
├── 18 网关实现Sentinel限流
├── 19 Sentinel分组限流
├── 20 Sentinel自定义异常</p>
<p>├──。。。。。
├── 132 定时任务相关使用
├── 133 定时任务实现详解</p>
</blockquote>
<p>若依框架系列视频教程 - 微服务版本
<a href="https://www.alipan.com/s/unQTyDeSEU2">https://www.alipan.com/s/unQTyDeSEU2</a>
提取码: 3uf1
点击链接保存，或者复制本段内容，打开「阿里云盘」APP ，无需下载极速在线查看，视频原画倍速播放。</p>]]></content:encoded>
      <category>开源</category>
      <pubDate>Tue, 10 Jun 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>git仓库同步两个远程仓库</title>
      <link>https://codexun.com/posts/synchronize-two-remote-git-repositories</link>
      <guid isPermaLink="true">https://codexun.com/posts/synchronize-two-remote-git-repositories</guid>
      <description>假设有两个存储库 URL，并希望同步它们以便它们包含相同的内容。 必须在 Git 中配置一个指向上游仓库的远程仓库，以便将您在分支中所做的更改同步到原始仓库。这也允许您将原始仓库中的更改同步到分支中。 若需双向同步（配置远程仓库 origin、upstream），举例拉取 unpstream 最新代码，同步到 mast</description>
      <content:encoded><![CDATA[<p><img src="/api/images/image/2026/07/071297876b68762b-image.png" alt="image.png"></p>
<p>假设有两个存储库 URL，并希望同步它们以便它们包含相同的内容。</p>
<p>必须在 Git 中配置一个指向上游仓库的远程仓库，以便将您在分支中所做的更改同步到原始仓库。这也允许您将原始仓库中的更改同步到分支中。</p>
<p>若需双向同步（配置远程仓库 origin、upstream），举例拉取 unpstream 最新代码，同步到 master 分支，可采用以下方法：</p>
<h2><strong>1. 配置双远程仓库</strong></h2>
<p>查看你的 fork 当前配置的远程仓库地址：</p>
<pre><code class="language-shell">$ git remote -v
 origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
 origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)
</code></pre>
<p>指定当前 fork 将要同步的上游远程仓库地址：</p>
<pre><code class="language-shell">$ git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITO
</code></pre>
<h2>2.验证一下你刚指定的上游仓库地址：</h2>
<pre><code class="language-shell">$ git remote -v
 origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
 origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)
 upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (fetch)
 upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (push)
</code></pre>
<h2><strong>3. 拉取所有远程更新</strong></h2>
<p>从上游仓库拉取分支及其对应的提交记录。对于 master 的提交会保存在一个本地分支 upstream/master 里。</p>
<pre><code class="language-shell">$ git fetch upstream
 remote: Counting objects: 75, done.
 remote: Compressing objects: 100% (53/53), done.
 remote: Total 62 (delta 27), reused 44 (delta 9)
 Unpacking objects: 100% (62/62), done.
 From https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY
 * [new branch] master -&gt; upstream/master
</code></pre>
<h2><strong>4. 合并并推送</strong></h2>
<p>签出你的 fork 的本地 master 分支</p>
<pre><code class="language-shell">$ git checkout master
 Switched to branch ‘master’
</code></pre>
<p>把 upstream/master 中的变更合并到本地的 master 分支里。这样你的 fork 的 master 分支就和上游仓库同步了，也不会丢失本地的更改。</p>
<pre><code class="language-shell">$ git merge upstream/master
 Updating a422352..5fdff0f
 Fast-forward
 README | 9 — — — -
 README.md | 7 ++++++
 2 files changed, 7 insertions(+), 9 deletions(-)
 delete mode 100644 README
 create mode 100644 README.md
</code></pre>
<p>把更改推送到服务器：</p>
<pre><code class="language-shell">$ git push
</code></pre>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Fri, 16 May 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>通过llms.txt文件让AI更容易的收录你的网站内容</title>
      <link>https://codexun.com/posts/how-to-make-ai-index-your-website-content-via-llms-txt-file</link>
      <guid isPermaLink="true">https://codexun.com/posts/how-to-make-ai-index-your-website-content-via-llms-txt-file</guid>
      <description>llms.txt 是一个专为大型语言模型（Large Language Models，简称 LLMs）设计的文本文件，类似于 robots.txt，但目的不同。robots.txt 告诉搜索引擎爬虫哪些页面可以爬取，而 llms.txt 则是为 AI 工具提供网站内容的结构化信息，帮助 AI 更好地理解和索引网站内容。</description>
      <content:encoded><![CDATA[<p>llms.txt 是一个专为大型语言模型（Large Language Models，简称 LLMs）设计的文本文件，类似于 robots.txt，但目的不同。robots.txt 告诉搜索引擎爬虫哪些页面可以爬取，而 llms.txt 则是为 AI 工具提供网站内容的结构化信息，帮助 AI 更好地理解和索引网站内容。</p>
<p>llms.txt 的名称中的 &quot;llms&quot; 代表 &quot;Large Language Models&quot;，表明这个文件是专门为大型语言模型设计的。</p>
<hr>
<h2>一、llms.txt 的由来</h2>
<p>随着 ChatGPT、Claude 等 AI 工具的普及，开发者希望这些 AI 工具能够更好地理解和索引他们的文档。然而，AI 工具在抓取和理解网站内容时面临着一些挑战：</p>
<ol>
<li>网站结构复杂，AI 难以理解内容之间的关系</li>
<li>网站内容可能分散在多个页面，AI 难以获取完整信息</li>
<li>网站可能包含动态内容，AI 难以抓取</li>
</ol>
<p>为了解决这些问题，一些开发者和 AI 公司开始探索一种标准化的方式，让网站所有者能够主动向 AI 工具提供网站内容的结构化信息。这就是 llms.txt 的由来。</p>
<p><strong>相关标准对比</strong></p>
<table>
<thead>
<tr>
<th><strong>文件类型</strong></th>
<th><strong>主要功能</strong></th>
<th><strong>目标用户</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>robots.txt</td>
<td>控制爬虫访问权限</td>
<td>搜索引擎爬虫</td>
</tr>
<tr>
<td>sitemap.xml</td>
<td>提供可索引页面列表</td>
<td>搜索引擎</td>
</tr>
<tr>
<td>llms.txt</td>
<td>优化内容结构呈现</td>
<td>AI系统</td>
</tr>
</tbody>
</table>
<h2><strong>二、llms.txt 的规范</strong></h2>
<p>标准定义了两类文件：</p>
<ol>
<li>基础文件(/llms.txt)：作为核心导航指南，概述网站主要内容框架</li>
<li>扩展文件(/llms-full.txt)：可选补充文件，包含完整内容细节供深度查询使用</li>
</ol>
<p>机器人协议配置 在robots.txt中添加AI爬虫专用规则</p>
<pre><code class="language-python"># 基础访问规则
User-Agent: *
Allow: /

# AI爬虫特殊处理
User-Agent: GPTBot
Allow: /llms.txt
Disallow: /

User-Agent: anthropic-ai
Allow: /llms.txt
Disallow: /

# 搜索引擎爬虫
User-Agent: Googlebot
Allow: /

# AI内容引导
LLM-Content: https://yourdomain.com/llms.txt
LLM-Full-Content: https://yourdomain.com/llms-full.txt
</code></pre>
<h3>1、基础文件(<code>llms.txt</code>)：</h3>
<p>作为核心导航指南，概述网站主要内容框架</p>
<p>llms.txt 文件通常放置在网站的根目录下，例如 <code>https://example.com/llms.txt</code>。它的内容格式比较自由，但通常包含以下几个部分：</p>
<ol>
<li><strong>网站标题和描述</strong>：简要介绍网站的主题和内容</li>
<li><strong>内容目录</strong>：列出网站的主要页面和链接</li>
<li><strong>内容摘要</strong>：提供网站主要内容的摘要</li>
</ol>
<p>下面是一个简单的示例：</p>
<pre><code class="language-python"># 网站标识
[网站名称]

&gt; 价值主张：用简洁语句说明网站核心价值

## 内容架构
- [分类1](链接)：内容特性说明
- [分类2](链接)：内容特性说明
- [文章专栏](链接)：内容形式描述

## 关键页面
- [核心页面1](链接)：功能概述
- [核心页面2](链接)：功能概述

## 近期更新
- [新内容1](链接)：更新时间
- [新内容2](链接)：更新时间
</code></pre>
<h3>2、扩展版本 <code>llms-full.txt</code></h3>
<p>此外，还有一个扩展版本 <code>llms-full.txt</code>，它包含网站的完整内容，让 AI 工具能够获取网站的所有信息。下面是一个简单的示例：</p>
<pre><code class="language-python"># 网站全景介绍

&gt; 详细说明（100-150字）：目标用户、核心优势、差异化特点

## 建站理念
阐述创办初衷和发展愿景（200-300字）

## 内容体系
### [主分类1](链接)
深入说明内容特色和用户收益（150-200字）

### [主分类2](链接)
深入说明内容特色和用户收益（150-200字）

## 用户获益
具体描述能解决哪些问题（200-300字）

## 内容更新
### [最新文章1](链接)
核心观点和价值提炼（100-150字）

### [最新文章2](链接)
核心观点和价值提炼（100-150字）
</code></pre>
<h2>三、<strong>llms.txt 的作用</strong></h2>
<p>llms.txt 是放置在网站根目录下的 Markdown 文件，包含结构化内容导航和核心信息摘要。其标准由 <a href="http://Answer.AI">Answer.AI</a> 联合创始人 Jeremy Howard 提出，目的是解决传统网页（如复杂 HTML 元素）对 AI 处理效率的干扰。主要作用如下：</p>
<ol>
<li>优化 AI 内容处理
<ul>
<li>过滤冗余信息：去除导航栏、JavaScript 等非必要元素，直接为 AI 提供“纯净”文本。</li>
<li>结构化呈现：通过标题（H1/H2）和列表划分内容模块，帮助 AI 快速理解网站架构。</li>
<li>上下文管理：适应 AI 的有限上下文窗口，优先提供关键内容。</li>
</ul>
</li>
<li>控制 AI 访问权限
<ul>
<li>指定允许抓取的页面路径（如 <code>/docs</code>），排除敏感或无关内容（如 <code>/admin</code>）。</li>
<li>声明内容使用规则（如引用授权、禁止商用）。</li>
</ul>
</li>
<li>提升生成结果质量
<ul>
<li>标注关键词和术语，增强 AI 对专业领域内容的理解。</li>
<li>通过 <code>llms-full.txt</code> 提供完整文档细节，供 AI 深度查询。</li>
</ul>
</li>
</ol>
<h2>四、llms.txt 的生成工具与实操指南</h2>
<h3><strong>4.1、自动化工具</strong></h3>
<h4>**4.1.**1、Firecrawl LLMs.txt Generator：</h4>
<p><strong><a href="https://llmstxt.firecrawl.dev/">Firecrawl LLMs.txt Generator</a></strong><a href="https://llmstxt.firecrawl.dev/"> </a>支持输入网址自动生成文件，提供 API 接口，适合技术文档站。</p>
<h4>**4.1.**2、SiteSpeakAI：</h4>
<p><strong><a href="https://sitespeak.ai/tools/llms-txt-generator">SiteSpeakAI LLMs.txt Generator</a></strong><a href="/301e3f3674f9818aa7c4c890604a3133?pvs=25"> </a>免费在线工具，一键抓取网站内容并生成标准化文件。</p>
<h4>**4.1.**3、Mintlify：</h4>
<p><strong><a href="https://mintlify.com/docs/settings/llms">Mintlify</a></strong> 托管文档平台（如技术文档）可自动生成并部署 <code>llms.txt</code> 和 <code>llms-full.txt</code>。</p>
<h3>**4.2、开发插件：**vitepress-plugin-llms</h3>
<p>vitepress-plugin-llms 是一个专为 VitePress 文档网站设计的插件，它可以自动生成 llms.txt 和 llms-full.txt 文件，让你的 VitePress 文档更好地被 AI 工具理解和索引。</p>
<p>尤雨溪推荐的 VitePress 插件，安装后自动生成文件并集成到文档站。</p>
<pre><code class="language-shell">npm install vitepress-plugin-llms -D
</code></pre>
<p>配置 VitePress 后，文件将自动生成至根目录。</p>
<h3><strong>4.3、快速生成方法</strong></h3>
<p>URL 转换工具，访问 <code>https://llmstxt.new/你的网站域名</code>（如 <code>llmstxt.new/https://example.com</code>），系统自动抓取并生成 Markdown 格式文件。</p>
<h2>五、注意事项</h2>
<ul>
<li>内容质量优先：需配合原创优质内容，仅技术优化无法替代内容价值。</li>
<li>持续更新：定期更新 <code>llms.txt</code> 可建立 AI 信任度，提升收录优先级。</li>
<li>兼容性验证：使用工具生成后，建议通过 LLMs TXT Hub 校验文件格式。</li>
</ul>
<p>通过上述工具和方法，开发者可快速为网站生成符合标准的 <code>llms.txt</code> 文件，显著提升 AI 对内容的理解与应用效率。</p>
<h2>实用工具推荐</h2>
<p>[1] <strong>Firecrawl LLMs.txt Generator</strong> :<a href="https://llmstxt.firecrawl.dev/">https://llmstxt.firecrawl.dev/</a></p>
<p>[2] <strong>SiteSpeakAI LLMs.txt Generator</strong> : <em><a href="https://sitespeak.ai/tools/llms-txt-generator">https://sitespeak.ai/tools/llms-txt-generator</a></em></p>
<p>[3] <strong>apify/actor-llmstxt-generator</strong> : <em><a href="https://github.com/apify/actor-llmstxt-generator">https://github.com/apify/actor-llmstxt-generator</a></em></p>
<p>[4] <strong>WordLift AI-Powered SEO Generator</strong> : <em><a href="https://wordlift.io/generate-llms-txt/">https://wordlift.io/generate-llms-txt/</a></em></p>
<p>[5] <strong>Mintlify</strong> : <em><a href="https://mintlify.com/docs/settings/llms">https://mintlify.com/docs/settings/llms</a></em></p>
<h2>总结</h2>
<p>llms.txt 为 AI 工具提供了一种统一的方式来获取和理解网站内容，而 vitepress-plugin-llms 插件则让 VitePress 文档网站轻松支持这一标准。通过在 Wot Design Uni 项目中引入这一插件，我们让 AI 工具能够更好地理解和索引我们的文档内容，为用户提供更准确、更有价值的信息。</p>
<p>随着 AI 技术的不断发展，llms.txt 标准可能会得到更广泛的支持和应用。作为开发者，我们可以通过引入这一标准，让我们的文档在 AI 时代保持良好的可访问性和可发现性。</p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>基于 Cloudflare 创建无限多企业邮箱</title>
      <link>https://codexun.com/posts/create-unlimited-corporate-mailboxes-based-on-cloudflare</link>
      <guid isPermaLink="true">https://codexun.com/posts/create-unlimited-corporate-mailboxes-based-on-cloudflare</guid>
      <description>Cloudflare是一家提供CDN、网络安全、DDos防御和域名服务的公司。人称大善人，赛博活佛，爬爬虾之前介绍过cloudflare的DNS，内网穿透等免费服务。 今天这里主要介绍如何从Cloudflare 白嫖无限多个企业级邮箱，瞬间获得无限多个免费邮箱，这些邮箱既可以收邮件，也可以发邮件。 首先域名邮箱，可以用</description>
      <content:encoded><![CDATA[<p>Cloudflare是一家提供CDN、网络安全、DDos防御和域名服务的公司。人称大善人，赛博活佛，爬爬虾之前介绍过cloudflare的DNS，内网穿透等免费服务。</p>
<p>今天这里主要介绍如何从Cloudflare 白嫖无限多个企业级邮箱，瞬间获得无限多个免费邮箱，这些邮箱既可以收邮件，也可以发邮件。</p>
<p>首先域名邮箱，可以用来接收网站验证码，注册账户的时候就可以很方便的注册一大堆小号。 还可以把他们当成临时邮箱，与人通信，避免暴露自己真实的邮箱，保护个人隐私。</p>
<h2>1、域名</h2>
<p>使用Cloudflare的前提是要有一个域名。</p>
<p>有了域名以后，把它托管到Cloudflare上。登录到Cloudflare之后，右上角点击添加站点，输入你的域名。</p>
<p><img src="/api/images/image/2026/07/992ee3b71651d56b-image.png" alt="image.png"></p>
<p>然后继续，这里有一些付费的，我们都不要，直接找下面这个免费的，点击继续。</p>
<p><img src="/api/images/image/2026/07/25548d7cedffa2d8-image.png" alt="image.png"></p>
<p>一路点击下一步。这步是重点，更改名称服务器。找到这两个名称服务器。</p>
<p><img src="/api/images/image/2026/07/98f3d16b8ab853e8-image.png" alt="image.png"></p>
<p>我们需要去刚才买域名的网站更改一下，这里有一个Name Server。就把这里改一下，把原来的这3条全部删掉，然后填上刚才网站给我分配的这两个，点击保存。</p>
<p><img src="/api/images/image/2026/07/c6509bb062c1fa8f-image.png" alt="image.png"></p>
<p>我们再回到Cloudflare，点击立即检查，然后点击继续。这里什么都不用动，直接开始使用就可以了。</p>
<p>左上角点击Cloudflare的图标，回到首页，域名这里打上一个对勾，那就可以开始直接使用了。</p>
<p><img src="/api/images/image/2026/07/e04d04c2e28a3fe7-image.png" alt="image.png"></p>
<h2>2、收邮件</h2>
<p><a href="http://xn--tech-shrimp-2u0ri05cgtmnv8resg.com">这里以域名tech-shrimp.com</a>，我们先点击这个域名，进来以后选择左侧电子邮件，选择电子邮件路由。</p>
<p>第一次进来以后，会有一个新手设置，点击取消新手设置，点击启用电子邮件路由。</p>
<p><img src="/api/images/image/2026/07/c7ebdc86fe4b166e-image.png" alt="image.png"></p>
<p>这里面有4个DNS值，我们点击添加记录并启用，Cloudflare就会自动添加上这4个DNS记录。</p>
<p><img src="/api/images/image/2026/07/efbf6f970abcedfa-image.png" alt="image.png"></p>
<p>弄好以后，我们再点击路由规则，这里有一个catch-all地址，我们点击右侧的编辑，</p>
<p><img src="/api/images/image/2026/07/e816cf74cc0e4502-image.png" alt="image.png"></p>
<p>点击发送到电子邮件。这里填写一个自己的电子邮箱，然后点击保存。</p>
<p><img src="/api/images/image/2026/07/21a5e212fe147b4f-image.png" alt="image.png"></p>
<p>保存好以后，这里会出现一个待验证。</p>
<p><img src="/api/images/image/2026/07/88d43093747b6ca3-image.png" alt="image.png"></p>
<p>Cloudflare会发送一封验证邮件到这个地址，我们来接收一下。刷新一下页面，打上一个对勾，那就配置完成了。</p>
<p><img src="/api/images/image/2026/07/b915c7396c78c704-image.png" alt="image.png"></p>
<p><strong>这个配置的意思是所有发送到tech-shrimp.com域名下面的邮件都会自动转到163的邮箱</strong> 。这样就实现了无限多个邮箱可以收邮件。</p>
<h2>3、发邮件</h2>
<p>使用邮箱发送邮件，我使用的服务是**<a href="http://resend.com/">resend.com</a>** ，这也是一个免费的服务。没有账号的话就自己注册一个。</p>
<p>选择左侧的API Keys，点击create API Key，名字随便填一个，下面两个保持默认，点击添加。</p>
<p><img src="/api/images/image/2026/07/df3324376a67279b-image.png" alt="image.png"></p>
<p><img src="/api/images/image/2026/07/2d018b544344df8b-image.png" alt="image.png"></p>
<p>就会获得一个API Key，把它保存好。这里有一个域名，我们需要把域名也添加到这个网站上，就是刚才托管到Cloudflare的那个域名。</p>
<p><img src="/api/images/image/2026/07/3f0621f27c134032-image.png" alt="image.png"></p>
<p><strong>好，这一步是重点</strong> 。这里给了3个DNS记录，我们需要把这个记录填回Cloudflare。</p>
<p><img src="/api/images/image/2026/07/31ce34789ad99433-image.png" alt="image.png"></p>
<p>回到cloudflare。</p>
<p><img src="/api/images/image/2026/07/af103b1474c77c1f-image.png" alt="image.png"></p>
<p><img src="/api/images/image/2026/07/91cd1604177c495f-image.png" alt="image.png"></p>
<p>我们回到resend，有一个按钮verify DNS records，我们点击一下，它检查我们刚才的配置是否正确。等校验完，页面标成绿色，那就配置完成了。</p>
<p><img src="/api/images/image/2026/07/b7519efa3cb3fad3-image.png" alt="image.png"></p>
<h4>使用Python发邮件</h4>
<pre><code class="language-python"># 先安装依赖# pip install resend
import resend
# 这里换成自己的resend API Key
resend.api_key = &quot;re_xxxxxxxxxxxxxxxxxxxxxxx&quot;

params: resend.Emails.SendParams = {
# 发件人可以是自己域名下的任何一个人&quot;from&quot;: &quot;sdkjfskdhf@你的域名&quot;,
  &quot;to&quot;: [&quot;tech-shrimp@outlook.com&quot;],
  &quot;subject&quot;: &quot;hi&quot;,
  &quot;html&quot;: &quot;&lt;strong&gt;hello, world!&lt;/strong&gt;&quot;
}

email = resend.Emails.send(params)
print(email)
</code></pre>
<h4>使用cURL发邮件</h4>
<p>Authorization 换成自己的resend API Key</p>
<p>发件人可以是自己域名下的任何一个人</p>
<pre><code class="language-python">curl -X POST 'https://api.resend.com/emails' \
 -H 'Authorization: Bearer re_VxeNCEn1_6w4bYF93xQKgKGFYRxNK2D3J' \
 -H 'Content-Type: application/json' \
 -d
```{
  &quot;from&quot;: &quot;Acme &lt;onboarding@你的域名&gt;&quot;,
  &quot;to&quot;: [&quot;tech-shrimp@outlook.com&quot;],
  &quot;subject&quot;: &quot;hello world&quot;,
  &quot;text&quot;: &quot;it works!&quot;
}'
</code></pre>]]></content:encoded>
      <category>工具</category>
      <pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>如何切换 google play 账号到美区，锁定美区实现ChatGPT应用无障碍下载</title>
      <link>https://codexun.com/posts/google-play-region-lock-solution-chatgpt-download</link>
      <guid isPermaLink="true">https://codexun.com/posts/google-play-region-lock-solution-chatgpt-download</guid>
      <description>由于 ChatGPT 在某些国家无法使用， 在 google play 应用商店上对于某些地区也无法下载。一般显示类似“此商品无法在您所在的国家/地区购买或下载”的提示。 这时候你可能想从一些第三方应用商店，比如 APKPure、apkmirror上搜索下载 apk 进行安装，虽然能够成功安装了，但是无法打开运行，打开</description>
      <content:encoded><![CDATA[<p>由于 ChatGPT 在某些国家无法使用， 在 google play 应用商店上对于某些地区也无法下载。一般显示类似“<strong>此商品无法在您所在的国家/地区购买或下载</strong>”的提示。</p>
<p>这时候你可能想从一些第三方应用商店，比如 APKPure、apkmirror上搜索下载 apk 进行安装，虽然能够成功安装了，但是无法打开运行，打开后会跳转到 google 应用商店，然后显示“<strong>无法识别您设备上安装的应用，该应用可能会损害您的设备</strong>”的提示。</p>
<p><img src="/api/images/image/2026/07/9b36f7be4fdec4f8-image.png" alt="image.png"></p>
<p>以上这些，根本原因还是该应用没有在你 google play 账号的地区上架，所以无法下载和运行。</p>
<p>要解决这个问题，需要将你 google play 账号锁定到一个合理的地区，比如美区，一般所有的应用都能够下载安装和运行。</p>
<p>下面教你如何将你 google play 账号锁定到美区：</p>
<h2>1、查看 google play 账号所在地区</h2>
<p>通过 google play 右上角点击个人头像，依次按照如下顺序，进入到“<strong>账号和设备偏好设置</strong>”中，查看到当前账号所属的国家或地区。</p>
<p>如果为空，说明当前账号未确定到详细地区，可以通过设置付款信息绑定到固定地区。</p>
<p><img src="/api/images/image/2026/07/25b4dd5bc0ed6c44-image.png" alt="image.png"></p>
<h2>2、绑定美国信用卡账号锁定到美区</h2>
<p>最简单的方法就是账号绑定美国信用卡，可以将 gp 账号锁定到美区。</p>
<p>不过美国信用卡一般人无法满足条件，不过可以使用虚拟信用卡，这类网上也有很多提供这类服务的，经常做出海服务的肯定知道**<a href="https://yeka.ai/i/PREMIUM">wildcard这个平台，也称为野卡</a><strong>，注册充值之后可以为你生成美国信用卡，平常用于绑定国外的一些服务，向ChatGPT Plus/Midjourney等海外服务，一般是不支持国内的信用卡的，这时候我们就可以使用</strong><a href="https://yeka.ai/i/PREMIUM">wildcard</a>**这个为我们生成的虚拟信用卡来进行绑定。</p>
<p>具体**<a href="https://yeka.ai/i/PREMIUM">wildcard这个平台</a>**的介绍，可以参见之前的这篇文章：</p>
<p><strong><a href="/posts/wildcard-virtual-card-guide-chatgpt-plus-midjourney">野卡WildCard虚拟信用卡全攻略：无需海外卡轻松订阅ChatGPT Plus/Midjourney等50+海外服务</a></strong></p>
<p>这里 gp 账号在第一步中，账号设置的地方，点击“付款和订阅”，然后绑定申请到美国虚拟信用卡，即可将 gp 账号绑定到美区。</p>
<blockquote>
<p>说明：绑定信用卡并不实际扣费，绑定完之后 gp 账号锁定到美区，可以把付款信用卡再解绑即可。</p>
</blockquote>
<h2>小技巧：</h2>
<p>命令行快速打开 google play 应用，例如[<strong>xxx 替换为要打开的 google play 应用的正式包名</strong>]：</p>
<pre><code class="language-javascript">adb shell am start https://play.google.com/store/apps/details?id=xxx
</code></pre>]]></content:encoded>
      <category>安卓</category>
      <pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>野卡WildCard虚拟信用卡全攻略：无需海外卡轻松订阅ChatGPT Plus/Midjourney等50+海外服务</title>
      <link>https://codexun.com/posts/wildcard-virtual-card-guide-chatgpt-plus-midjourney</link>
      <guid isPermaLink="true">https://codexun.com/posts/wildcard-virtual-card-guide-chatgpt-plus-midjourney</guid>
      <description>一、虚拟信用卡：数字时代的安全支付新选择 虚拟信用卡是专为在线交易设计的数字支付工具，通过生成可自定义使用次数的虚拟卡号，完美解决海外订阅场景中的信用卡信息泄露风险。其具备实体卡同等功能（含CVV安全码/有效期），特别适配ChatGPT Plus、MidJourney等热门AI工具的订阅需求。 虚拟信用卡的三大核心优势</description>
      <content:encoded><![CDATA[<h3>一、虚拟信用卡：数字时代的安全支付新选择</h3>
<p>虚拟信用卡是专为在线交易设计的<strong>数字支付工具</strong>，通过生成可自定义使用次数的虚拟卡号，完美解决海外订阅场景中的信用卡信息泄露风险。其具备实体卡同等功能（含CVV安全码/有效期），特别适配ChatGPT Plus、MidJourney等热门AI工具的订阅需求。</p>
<h4>虚拟信用卡的三大核心优势</h4>
<ul>
<li><strong>金融安全保障</strong>：动态卡号隔离真实账户，防范钓鱼网站与交易欺诈</li>
<li><strong>跨境支付利器</strong>：支持全球200+国家/地区的数字服务订阅</li>
<li><strong>智能风控管理</strong>：单次/多次使用模式自由切换，单日消费额度自定义</li>
</ul>
<hr>
<h3>二、<a href="https://s2ink.com/yeka">WildCard虚拟信用卡</a>：中国用户专属的海外支付解决方案</h3>
<h4>为什么选择<a href="https://s2ink.com/yeka">WildCard</a>？</h4>
<p>作为专为中文用户打造的虚拟信用卡平台，WildCard已服务超10万+用户完成海外AI服务订阅，特别针对OpenAI/ChatGPT的支付场景进行深度优化。</p>
<p><img src="/api/images/image/2026/07/e27d52b860acd4be-image.png" alt="image.png"></p>
<p><strong>平台亮点功能：</strong></p>
<ul>
<li>国内手机号直注册，支持支付宝/微信即时充值</li>
<li>5分钟完成ChatGPT Plus升级，成功率高达99%</li>
<li>无需海外身份认证，中文界面零门槛操作</li>
</ul>
<hr>
<h3>三、3分钟完成注册：<a href="https://s2ink.com/yeka">WildCard虚拟信用卡</a>开通全流程</h3>
<h4>新手必看操作指南</h4>
<ol>
<li><strong>官网注册入口</strong>：<a href="https://yeka.ai/i/PREMIUM">立即开通WildCard</a>
（使用邀请码<strong>PREMIUM</strong>立享开卡费5折+首年0手续费）</li>
<li><strong>选择服务周期</strong>：推荐2年套餐享最高性价比（现价$19.99/年）</li>
<li><strong>绑定支付渠道</strong>：支持支付宝扫码充值，实时到账无延迟</li>
</ol>
<hr>
<h3>四、WildCard六大核心功能解析</h3>
<h4>差异化竞争优势</h4>
<ul>
<li><strong>10秒极速开卡</strong>：全程中文引导，无需信用卡申请经验</li>
<li><strong>AI生态全覆盖</strong>：ChatGPT/MidJourney/Claude等主流平台即绑即用</li>
<li><strong>零隐藏收费</strong>：消费手续费全免，汇率实时透明可查</li>
</ul>
<h4>特色功能详解</h4>
<ol>
<li><strong>ChatGPT Plus一键升级</strong>：突破国内信用卡支付限制</li>
<li><strong>智能API路由</strong>：自动选择最优网络节点，订阅成功率提升80%</li>
<li><strong>海外验证码服务</strong>：支持美国/英国虚拟号码接收短信</li>
<li><strong>多账户管理</strong>：单账号可创建5张独立虚拟卡</li>
<li><strong>交易实时提醒</strong>：微信/短信双重通知机制</li>
</ol>
<p><img src="/api/images/image/2026/07/79655c15fb80f44c-image.png" alt="image.png"></p>
<hr>
<h3>五、费用说明与使用技巧</h3>
<h4>2024最新资费标准</h4>
<ul>
<li><strong>基础年费</strong>：首年$9.99（原价$11.99，输入邀请码立减$2）</li>
<li><strong>充值费率</strong>：支付宝充值3.5%手续费，推荐单次大额充值更划算</li>
</ul>
<h4>高效使用贴士</h4>
<ul>
<li><strong>余额预存建议</strong>：建议预留$20以上余额保障订阅连续性</li>
<li><strong>账单邮编填写</strong>：使用系统自动生成的美国邮编（如10001）</li>
<li><strong>订阅失败处理</strong>：切换浏览器隐私模式重新绑定</li>
</ul>
<hr>
<h3>六、高频问题解决方案库</h3>
<h4>ChatGPT订阅专项答疑</h4>
<ul>
<li><strong>自动续费关闭指南</strong>：
ChatGPT账户 → Billing → Cancel Subscription → 确认关闭</li>
<li><strong>支付失败处理方案</strong>：
使用WildCard专用<a href="https://s2ink.com/yeka">支付通道</a>，成功率提升至95%</li>
</ul>
<h4>账户安全类问题</h4>
<ul>
<li><strong>风险IP检测</strong>：避免使用公共机场节点，推荐WildCard专属加速器</li>
<li><strong>交易异常处理</strong>：联系在线客服（工作日9:00-21:00）即时冻结卡片</li>
</ul>
<hr>
<h3>七、为什么80%的AI从业者选择WildCard？</h3>
<p>作为国内虚拟信用卡领域的头部服务商，WildCard凭借以下优势成为技术从业者的首选：</p>
<ul>
<li>✅ 日均处理OpenAI订阅请求3000+</li>
<li>✅ 7×12小时中文客服即时响应</li>
<li>✅ 行业最低年费套餐（平均每天仅$0.03）</li>
</ul>
<p>立即通过<a href="https://s2ink.com/yeka">WildCard官网</a>开通服务，输入邀请码<strong>PREMIUM</strong>还可额外获得$5充值优惠券！</p>
<p><img src="/api/images/image/2026/07/4e85141436654e5c-image.png" alt="image.png"></p>]]></content:encoded>
      <category>记录</category>
      <pubDate>Fri, 18 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Next.js 系列之的SSG、SSR、ISR、CSR 的详细介绍和区别</title>
      <link>https://codexun.com/posts/nextjs-series-difference-between-ssg-ssr-isr-and-csr-in-nextjs</link>
      <guid isPermaLink="true">https://codexun.com/posts/nextjs-series-difference-between-ssg-ssr-isr-and-csr-in-nextjs</guid>
      <description>Next.js 提供了多种渲染策略（SSG、SSR、ISR、CSR），每种策略适用于不同的场景。以下是它们的核心区别、实现方式及最佳实践： 1. SSG（Static Site Generation，静态生成） 核心特点 生成时机：在构建时（next build）生成静态 HTML。 数据获取：通过 getStatic</description>
      <content:encoded><![CDATA[<p>Next.js 提供了多种渲染策略（SSG、SSR、ISR、CSR），每种策略适用于不同的场景。以下是它们的核心区别、实现方式及最佳实践：</p>
<hr>
<h2><strong>1. SSG（Static Site Generation，静态生成）</strong></h2>
<h3><strong>核心特点</strong></h3>
<ul>
<li><strong>生成时机</strong>：在构建时（<code>next build</code>）生成静态 HTML。</li>
<li><strong>数据获取</strong>：通过 <code>getStaticProps</code> 获取数据，支持预渲染所有页面。</li>
<li><strong>适用场景</strong>：内容不经常变化的页面（如博客、文档、营销页）。</li>
<li><strong>优点</strong>：加载最快、SEO 友好、CDN 缓存友好。</li>
<li><strong>缺点</strong>：无法实时更新内容，需重新构建才能更新。</li>
</ul>
<h3><strong>实现代码</strong></h3>
<pre><code class="language-javascript">// pages/blog/[slug].js
export async function getStaticPaths() {
  // 预生成所有可能的路径（如博客文章列表）
  const posts = await fetch('&lt;https://api.example.com/posts&gt;');
  return {
    paths: posts.map(post =&gt; ({ params: { slug: post.slug } })),
    fallback: false, // 未预生成的路径返回 404
  };
}

export async function getStaticProps({ params }) {
  // 根据 slug 获取单篇文章内容
  const post = await fetch(`https://api.example.com/posts/${params.slug}`);
  return { props: { post } };
}

export default function BlogPost({ post }) {
  return &lt;article&gt;{post.content}&lt;/article&gt;;
}

</code></pre>
<hr>
<h2><strong>2. SSR（Server-Side Rendering，服务端渲染）</strong></h2>
<h3><strong>核心特点</strong></h3>
<ul>
<li><strong>生成时机</strong>：每次页面请求时在服务端动态生成 HTML。</li>
<li><strong>数据获取</strong>：通过 <code>getServerSideProps</code> 在请求时获取最新数据。</li>
<li><strong>适用场景</strong>：需要实时数据的页面（如用户仪表盘、实时股价）。</li>
<li><strong>优点</strong>：内容始终最新，支持个性化渲染（如基于 Cookie 的权限验证）。</li>
<li><strong>缺点</strong>：TTFB（首字节时间）较慢，服务端压力大。</li>
</ul>
<h3><strong>实现代码</strong></h3>
<pre><code class="language-javascript">// pages/dashboard.js
export async function getServerSideProps(context) {
  // 从请求中获取 Cookie 或用户信息
  const user = await getUserFromCookie(context.req.cookies);
  const data = await fetch(`https://api.example.com/dashboard?user=${user.id}`);
  return { props: { data } };
}

export default function Dashboard({ data }) {
  return &lt;div&gt;{data.stats}&lt;/div&gt;;
}

</code></pre>
<hr>
<h2><strong>3. ISR（Incremental Static Regeneration，增量静态再生）</strong></h2>
<h3><strong>核心特点</strong></h3>
<ul>
<li><strong>生成时机</strong>：SSG 的扩展，允许在运行时按需重新生成静态页面。</li>
<li><strong>数据获取</strong>：在 <code>getStaticProps</code> 中设置 <code>revalidate</code> 参数（秒），触发条件：
• 初次访问时生成静态页面。
• 后续访问时，若超过 <code>revalidate</code> 时间，后台重新生成页面。</li>
<li><strong>适用场景</strong>：内容更新频率适中的页面（如电商产品页、新闻列表）。</li>
<li><strong>优点</strong>：兼具 SSG 的速度和 SSR 的更新能力，减轻服务端压力。</li>
<li><strong>缺点</strong>：再生成期间用户可能看到旧数据。</li>
</ul>
<h3><strong>实现代码</strong></h3>
<pre><code class="language-javascript">// pages/products/[id].js
export async function getStaticPaths() {
  return { paths: [], fallback: 'blocking' }; // 动态生成所有路径
}

export async function getStaticProps({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`);
  return {
    props: { product },
    revalidate: 3600, // 每 1 小时重新验证一次
  };
}

export default function Product({ product }) {
  return &lt;div&gt;{product.name}&lt;/div&gt;;
}

</code></pre>
<hr>
<h2><strong>4. CSR（Client-Side Rendering，客户端渲染）</strong></h2>
<h3><strong>核心特点</strong></h3>
<ul>
<li><strong>生成时机</strong>：在客户端浏览器中通过 JavaScript 渲染内容。</li>
<li><strong>数据获取</strong>：通过 <code>useEffect</code> 或 <code>SWR</code> 在组件挂载后获取数据。</li>
<li><strong>适用场景</strong>：交互性强、无需 SEO 的页面（如用户后台、管理面板）。</li>
<li><strong>优点</strong>：减少服务端负载，支持动态交互。</li>
<li><strong>缺点</strong>：首屏加载慢，SEO 不友好，依赖客户端 JavaScript。</li>
</ul>
<h3><strong>实现代码</strong></h3>
<pre><code class="language-javascript">// pages/profile.js
import { useEffect, useState } from 'react';

export default function Profile() {
  const [user, setUser] = useState(null);

  useEffect(() =&gt; {
    // 组件挂载后获取用户数据
    fetch('/api/user')
      .then(res =&gt; res.json())
      .then(data =&gt; setUser(data));
  }, []);

  if (!user) return &lt;div&gt;Loading...&lt;/div&gt;;
  return &lt;div&gt;{user.name}&lt;/div&gt;;
}

</code></pre>
<hr>
<h2><strong>5. 对比表格</strong></h2>
<table>
<thead>
<tr>
<th><strong>特性</strong></th>
<th><strong>SSG</strong></th>
<th><strong>SSR</strong></th>
<th><strong>ISR</strong></th>
<th><strong>CSR</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>生成时机</strong></td>
<td>构建时</td>
<td>每次请求时</td>
<td>构建时 + 按需再生</td>
<td>客户端运行时</td>
</tr>
<tr>
<td><strong>数据实时性</strong></td>
<td>低（需重新构建）</td>
<td>高（实时）</td>
<td>中等（可配置更新间隔）</td>
<td>高（动态获取）</td>
</tr>
<tr>
<td><strong>SEO 支持</strong></td>
<td>✅ 最佳</td>
<td>✅ 良好</td>
<td>✅ 良好</td>
<td>❌ 差（需额外处理）</td>
</tr>
<tr>
<td><strong>首屏加载速度</strong></td>
<td>⚡️ 最快</td>
<td>⏳ 较慢（依赖服务端）</td>
<td>⚡️ 快（缓存优先）</td>
<td>⏳ 慢（需加载 JS）</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>静态内容</td>
<td>实时数据</td>
<td>频繁更新但非实时内容</td>
<td>交互复杂、无需 SEO</td>
</tr>
<tr>
<td><strong>服务端压力</strong></td>
<td>无</td>
<td>高</td>
<td>低</td>
<td>无</td>
</tr>
</tbody>
</table>
<hr>
<h2><strong>6. 混合使用策略</strong></h2>
<p>Next.js 允许在同一个项目中灵活组合多种渲染方式：
• <strong>SSG + CSR</strong>：首屏静态渲染，交互部分客户端加载（如博客的评论模块）。
• <strong>SSR + ISR</strong>：动态页面初次 SSR，后续通过 ISR 缓存（如新闻详情页）。
• <strong>App Router 优化</strong>：在 <code>app</code> 目录中，使用 React Server Components 减少客户端 JS 体积。</p>
<h3><strong>示例：混合 SSG 和 CSR</strong></h3>
<pre><code class="language-javascript">// pages/index.js（SSG 主页面）
export async function getStaticProps() {
  const posts = await fetch('&lt;https://api.example.com/posts&gt;');
  return { props: { posts } };
}

export default function Home({ posts }) {
  // 主内容静态生成，评论模块客户端渲染
  return (
    &lt;div&gt;
      {posts.map(post =&gt; &lt;Post key={post.id} {...post} /&gt;)}
      &lt;ClientOnly&gt;
        &lt;Comments /&gt;
      &lt;/ClientOnly&gt;
    &lt;/div&gt;
  );
}

// 客户端组件（仅在浏览器加载）
function Comments() {
  const [comments, setComments] = useState([]);
  useEffect(() =&gt; {
    fetch('/api/comments').then(res =&gt; res.json()).then(setComments);
  }, []);
  return &lt;div&gt;{comments.map(comment =&gt; &lt;div&gt;{comment.text}&lt;/div&gt;)}&lt;/div&gt;;
}

</code></pre>
<hr>
<h2><strong>7. 选择策略的决策树</strong></h2>
<ol>
<li><strong>是否需要 SEO？</strong>
• ✅ 是 → 使用 SSG、SSR 或 ISR。
• ❌ 否 → 使用 CSR。</li>
<li><strong>数据是否频繁更新？</strong>
• ✅ 是 → 使用 SSR 或 ISR。
• ❌ 否 → 使用 SSG。</li>
<li><strong>是否有个性化内容？</strong>
• ✅ 是 → 使用 SSR。
• ❌ 否 → 使用 SSG 或 ISR。</li>
</ol>
<hr>
<h2><strong>总结</strong></h2>
<ul>
<li><strong>SSG</strong>：优先用于内容稳定、SEO 关键的页面。</li>
<li><strong>SSR</strong>：用于实时数据或个性化内容。</li>
<li><strong>ISR</strong>：平衡速度与更新频率，适合大规模内容站点。</li>
<li><strong>CSR</strong>：用于交互复杂、无需 SEO 的功能模块。</li>
<li><strong>混合使用</strong>：结合不同策略，最大化性能与功能灵活性。</li>
</ul>
<h2>系列文章</h2>
<p><strong><a href="/posts/nextjs-series-difference-between-client-components-and-server-components">Next.js 系列之客户端组件（Client Components）和服务端组件（Server Components）的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-introduction-about-serverless-architecture">Next.js 系列之Serverless（无服务器架构）介绍</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-app-router-and-page-router">Next.js 系列之 app router 和 page router 的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-ssg-ssr-isr-and-csr-in-nextjs">Next.js 系列之的SSG、SSR、ISR、CSR 的详细介绍和区别</a></strong></p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Next.js 系列之 app router 和 page router 的区别</title>
      <link>https://codexun.com/posts/nextjs-series-difference-between-app-router-and-page-router</link>
      <guid isPermaLink="true">https://codexun.com/posts/nextjs-series-difference-between-app-router-and-page-router</guid>
      <description>Next.js 中的 App Router（应用路由）和 Page Router（页面路由）是两种不同的路由架构，主要区别在于 目录结构、功能特性、数据加载方式及布局管理。以下是详细对比： 1. 目录结构与路由定义 特性 App Router (Next.js 13+) Page Router (Next.js 12 </description>
      <content:encoded><![CDATA[<p>Next.js 中的 App Router（应用路由）和 Page Router（页面路由）是两种不同的路由架构，主要区别在于 <strong>目录结构、功能特性、数据加载方式及布局管理</strong>。以下是详细对比：</p>
<hr>
<h2><strong>1. 目录结构与路由定义</strong></h2>
<table>
<thead>
<tr>
<th><strong>特性</strong></th>
<th><strong>App Router</strong> (Next.js 13+)</th>
<th><strong>Page Router</strong> (Next.js 12 及更早)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>目录位置</strong></td>
<td><code>app</code> 目录</td>
<td><code>pages</code> 目录</td>
</tr>
<tr>
<td><strong>路由生成方式</strong></td>
<td>基于文件夹路径定义路由（如 <code>app/about/page.js</code> → <code>/about</code>）</td>
<td>基于文件名自动生成路由（如 <code>pages/about.js</code> → <code>/about</code>）</td>
</tr>
<tr>
<td><strong>动态路由</strong></td>
<td>文件夹名用 <code>\[param\]</code> 标记（如 <code>app/posts/\[id\]/page.js</code>）</td>
<td>文件名用 <code>\[param\]</code> 标记（如 <code>pages/posts/\[id\].js</code>）</td>
</tr>
<tr>
<td><strong>API 路由</strong></td>
<td>支持 API 路由（如 <code>app/api/route.ts</code>）</td>
<td>使用 <code>pages/api</code> 目录</td>
</tr>
</tbody>
</table>
<hr>
<h2><strong>2. 核心功能差异</strong></h2>
<h3><strong>（1）布局与嵌套路由</strong></h3>
<ul>
<li><strong>App Router</strong></li>
</ul>
<p>• <strong>嵌套布局</strong>：通过 <code>layout.js</code> 文件定义层级布局，自动包裹子页面（如 <code>app/dashboard/layout.js</code> 包裹 <code>app/dashboard/page.js</code>）。</p>
<p>• <strong>模板与错误处理</strong>：支持 <code>template.js</code>（局部刷新布局）和 <code>error.js</code>（错误边界）。</p>
<p>• <strong>流式加载</strong>：通过 <code>loading.js</code> 定义页面加载时的骨架屏。</p>
<ul>
<li><strong>Page Router</strong></li>
</ul>
<p>• <strong>全局布局</strong>：需通过 <code>\_app.js</code> 和 <code>\_document.js</code> 自定义全局布局，无法自动嵌套。</p>
<p>• <strong>手动处理嵌套</strong>：需在组件内显式引入布局（如 <code>&lt;Layout&gt;&lt;Page /&gt;&lt;/Layout&gt;</code>）。</p>
<h3><strong>（2）数据加载</strong></h3>
<ul>
<li><strong>App Router</strong></li>
</ul>
<p>• <strong>服务端数据获取</strong>：组件可直接使用 <code>async/await</code> 获取数据（默认为 React Server Component）。</p>
<p>• <strong>并行加载</strong>：通过 <code>&lt;Suspense&gt;</code> 分块加载多个数据源。</p>
<p>• <strong>动态 SEO</strong>：通过导出 <code>metadata</code> 对象动态设置页面标题、描述等。</p>
<pre><code class="language-javascript">// App Router 页面示例（app/page.js）
export default async function Page() {
  const data = await fetchData(); // 直接异步获取数据
  return &lt;div&gt;{data}&lt;/div&gt;;
}

export const metadata = {
  title: 'Home Page',
};

</code></pre>
<ul>
<li><strong>Page Router</strong></li>
</ul>
<p>• <strong>数据方法</strong>：需使用 <code>getStaticProps</code>（SSG）、<code>getServerSideProps</code>（SSR）或 <code>getInitialProps</code>（混合渲染）。</p>
<p>• <strong>SEO 静态化</strong>：需手动在 <code>\_document.js</code> 或页面组件内设置 <code>&lt;Head&gt;</code> 标签。</p>
<pre><code class="language-javascript">// Page Router 页面示例（pages/index.js）
export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}

export default function Page({ data }) {
  return &lt;div&gt;{data}&lt;/div&gt;;
}
</code></pre>
<hr>
<h2><strong>3. 渲染模式与性能</strong></h2>
<table>
<thead>
<tr>
<th><strong>特性</strong></th>
<th><strong>App Router</strong></th>
<th><strong>Page Router</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>默认渲染模式</strong></td>
<td>支持 React Server Components（服务端组件优先）</td>
<td>默认客户端渲染（需手动选择 SSG/SSR）</td>
</tr>
<tr>
<td><strong>流式渲染（Streaming）</strong></td>
<td>支持分块传输内容，优先显示关键部分</td>
<td>需自行实现（如使用 <code>React.lazy</code>）</td>
</tr>
<tr>
<td><strong>静态生成（SSG）</strong></td>
<td>通过 <code>generateStaticParams</code> 预生成动态路由</td>
<td>通过 <code>getStaticPaths</code> 预生成路由</td>
</tr>
</tbody>
</table>
<hr>
<h2><strong>4. 混合使用与迁移</strong></h2>
<ul>
<li><strong>共存支持</strong>：Next.js 13+ 允许 <code>app</code> 和 <code>pages</code> 目录共存，可逐步迁移旧项目。</li>
<li><strong>优先级</strong>：若同一路由在 <code>app</code> 和 <code>pages</code> 中均有定义，<code>app</code> 目录的页面优先。</li>
<li><strong>适配性</strong>：部分第三方库（如 <code>next-auth</code>）可能需调整配置以兼容 App Router。</li>
</ul>
<hr>
<h2><strong>5. 适用场景对比</strong></h2>
<table>
<thead>
<tr>
<th><strong>场景</strong></th>
<th><strong>App Router 优势</strong></th>
<th><strong>Page Router 优势</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>新项目</strong></td>
<td>✅ 推荐使用（功能更现代，性能更优）</td>
<td>❌ 不推荐</td>
</tr>
<tr>
<td><strong>旧项目维护</strong></td>
<td>⚠️ 需逐步迁移</td>
<td>✅ 适合保留原有结构</td>
</tr>
<tr>
<td><strong>复杂布局需求</strong></td>
<td>✅ 嵌套布局和模板支持完善</td>
<td>❌ 需手动管理嵌套</td>
</tr>
<tr>
<td><strong>服务端组件需求</strong></td>
<td>✅ 默认支持服务端渲染和数据获取</td>
<td>❌ 需通过 <code>getServerSideProps</code> 实现</td>
</tr>
</tbody>
</table>
<hr>
<h2><strong>总结</strong></h2>
<ul>
<li><strong>App Router</strong> 是 Next.js 的未来方向，提供更强大的布局管理、流式渲染和服务端组件支持，适合追求高性能和现代特性的项目。</li>
<li><strong>Page Router</strong> 适合维护旧项目或简单应用，但功能扩展性较弱。</li>
<li>建议新项目直接采用 App Router，旧项目可逐步迁移以利用新特性。</li>
</ul>
<h2>系列文章</h2>
<p><strong><a href="/posts/nextjs-series-difference-between-client-components-and-server-components">Next.js 系列之客户端组件（Client Components）和服务端组件（Server Components）的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-introduction-about-serverless-architecture">Next.js 系列之Serverless（无服务器架构）介绍</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-app-router-and-page-router">Next.js 系列之 app router 和 page router 的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-ssg-ssr-isr-and-csr-in-nextjs">Next.js 系列之的SSG、SSR、ISR、CSR 的详细介绍和区别</a></strong></p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Sat, 05 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Next.js 系列之Serverless（无服务器架构）介绍</title>
      <link>https://codexun.com/posts/nextjs-series-introduction-about-serverless-architecture</link>
      <guid isPermaLink="true">https://codexun.com/posts/nextjs-series-introduction-about-serverless-architecture</guid>
      <description>Serverless（无服务器架构）是一种云计算模型，其核心思想是开发者无需关注底层服务器的运维、扩容和资源分配，只需专注于编写业务代码。云服务商（如 AWS、腾讯云、阿里云）会动态管理计算资源的分配，按需执行代码并自动伸缩。在前端开发中，Serverless 主要用于解决后端服务部署、API 管理和静态资源托管等问题</description>
      <content:encoded><![CDATA[<p>Serverless（无服务器架构）是一种云计算模型，其核心思想是开发者无需关注底层服务器的运维、扩容和资源分配，只需专注于编写业务代码。云服务商（如 AWS、腾讯云、阿里云）会动态管理计算资源的分配，按需执行代码并自动伸缩。在前端开发中，Serverless 主要用于解决后端服务部署、API 管理和静态资源托管等问题，尤其适合结合 SSR（服务端渲染）技术使用。以下是具体解析：</p>
<hr>
<h2><strong>1. Serverless 的核心特点</strong></h2>
<ul>
<li><strong>无服务器管理</strong>：开发者无需自行配置服务器或维护基础设施，云平台自动处理资源调度。</li>
<li><strong>事件驱动</strong>：代码通过 HTTP 请求、数据库变更等事件触发执行（如用户访问页面触发 SSR 渲染）。</li>
<li><strong>按需付费</strong>：根据实际代码执行时间和资源消耗计费，空闲时不产生费用。</li>
<li><strong>自动伸缩</strong>：流量激增时自动扩容，避免因服务器性能不足导致服务中断。</li>
</ul>
<hr>
<h2><strong>2. 前端开发中 Serverless 的典型应用场景</strong></h2>
<h3><strong>（1）服务端渲染（SSR）</strong></h3>
<ul>
<li><strong>优化首屏加载与 SEO</strong>：通过 Serverless 函数（如 AWS Lambda、腾讯云 SCF）动态生成 HTML，提升页面加载速度和搜索引擎友好性。
<em>示例</em>：Next.js 框架的 SSR 页面可部署为云函数，每个页面路由对应一个独立的函数。</li>
<li><strong>与静态资源分离</strong>：将渲染逻辑放在云函数中，静态资源（如图片、CSS）托管至对象存储（如 COS、S3），通过 CDN 加速。</li>
</ul>
<h3><strong>（2）API 服务</strong></h3>
<ul>
<li><strong>轻量级后端</strong>：前端开发者可用 Node.js 编写 API（如 Express、Koa），直接部署为 Serverless 函数，替代传统后端服务器。</li>
<li><strong>快速集成云服务</strong>：通过 Serverless 组件直接调用数据库、消息队列等云服务，无需自行搭建中间件。</li>
</ul>
<h3><strong>（3）静态网站托管</strong></h3>
<ul>
<li><strong>自动化部署</strong>：Vercel 等平台支持将 Next.js、Nuxt.js 等前端框架的构建产物自动托管到全球 CDN，并绑定自定义域名。</li>
<li><strong>CI/CD 集成</strong>：关联代码仓库后，每次 Git Push 自动触发构建和部署流程。</li>
</ul>
<hr>
<h2><strong>3. Serverless 对前端的核心价值</strong></h2>
<ul>
<li><strong>降低运维成本</strong>：无需学习 Linux 或 Nginx 配置，专注业务代码。</li>
<li><strong>快速迭代</strong>：通过 Serverless Framework 或 Vercel CLI 实现一键部署，缩短开发周期。</li>
<li><strong>成本可控</strong>：小型项目可享受免费额度（如腾讯云每月 40 万 GBs 函数资源）。</li>
<li><strong>生态兼容性</strong>：支持主流框架（Next.js、Nuxt.js）、语言（Node.js、Python）和云平台（AWS、阿里云、腾讯云）。</li>
</ul>
<hr>
<h2><strong>4. 实践注意事项</strong></h2>
<ul>
<li><strong>冷启动问题</strong>：函数首次调用时需初始化环境，可能导致延迟（可通过预置并发缓解）。</li>
<li><strong>代码体积限制</strong>：云函数代码包通常需压缩至 50MB 以内，需剔除开发依赖（如 <code>npm install --omit=dev</code>）。</li>
<li><strong>环境适配</strong>：部分云平台默认 Node.js 版本较低，需自行升级或通过 Layer 功能绑定运行时。</li>
<li><strong>本地调试</strong>：使用 Serverless Framework 或 IDE 插件模拟云环境进行测试。</li>
</ul>
<hr>
<h2><strong>5. 主流部署方案示例</strong></h2>
<p>以 Next.js 应用为例：</p>
<ol>
<li><strong>Vercel 托管</strong>：零配置部署，自动识别路由和 API，适合个人项目。</li>
<li><strong>腾讯云 Web 函数</strong>：通过 <code>serverless.yml</code> 配置入口文件和资源，支持自定义域名和 CDN。</li>
<li><strong>AWS Lambda</strong>：利用 <code>@vercel/next</code> 构建器将页面拆分为独立函数，优化性能和资源利用率。</li>
</ol>
<hr>
<h2><strong>总结</strong></h2>
<p>Serverless 为前端开发提供了“无运维化”的可能性，尤其适合需要快速迭代、低成本运维的 SSR 应用和轻量级 API 服务。尽管存在冷启动等挑战，但随着云平台优化和工具链完善（如 Cloud Studio 的一键部署），它已成为现代前端工程化的重要方向。</p>
<h2>系列文章</h2>
<p><strong><a href="/posts/nextjs-series-difference-between-client-components-and-server-components">Next.js 系列之客户端组件（Client Components）和服务端组件（Server Components）的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-introduction-about-serverless-architecture">Next.js 系列之Serverless（无服务器架构）介绍</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-app-router-and-page-router">Next.js 系列之 app router 和 page router 的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-ssg-ssr-isr-and-csr-in-nextjs">Next.js 系列之的SSG、SSR、ISR、CSR 的详细介绍和区别</a></strong></p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Next.js 系列之客户端组件（Client Components）和服务端组件（Server Components）的区别</title>
      <link>https://codexun.com/posts/nextjs-series-difference-between-client-components-and-server-components</link>
      <guid isPermaLink="true">https://codexun.com/posts/nextjs-series-difference-between-client-components-and-server-components</guid>
      <description>Next.js 中客户端组件（Client Components）和服务端组件（Server Components）的核心区别在于运行环境、功能特性及适用场景，以下是详细对比： 1. 运行环境与渲染方式 服务端组件 • 环境：仅在服务器端运行，生成静态 HTML 发送到客户端。 • 渲染：支持静态渲染（默认）、动态渲染</description>
      <content:encoded><![CDATA[<p>Next.js 中客户端组件（Client Components）和服务端组件（Server Components）的核心区别在于运行环境、功能特性及适用场景，以下是详细对比：</p>
<hr>
<h2><strong>1. 运行环境与渲染方式</strong></h2>
<ul>
<li><strong>服务端组件</strong></li>
</ul>
<p>• <strong>环境</strong>：仅在服务器端运行，生成静态 HTML 发送到客户端。</p>
<p>• <strong>渲染</strong>：支持静态渲染（默认）、动态渲染（需调用 <code>fetch</code> 等 API）和流式渲染（分块加载内容）。</p>
<p>• <strong>示例代码</strong>：直接获取数据并返回 HTML，无需客户端交互逻辑。</p>
<pre><code class="language-javascript">// 服务端组件示例：直接获取数据并生成静态 HTML
export default async function ServerComponent() {
  const data = await fetch('&lt;https://api.example.com/data&gt;');
  return &lt;div&gt;{data}&lt;/div&gt;;
}

</code></pre>
<ul>
<li><strong>客户端组件</strong></li>
</ul>
<p>• <strong>环境</strong>：在浏览器中运行，支持交互逻辑和浏览器 API（如 <code>window</code>、<code>localStorage</code>）。</p>
<p>• <strong>渲染</strong>：首次加载时由服务端生成 HTML 预览，随后通过客户端 JavaScript 进行 Hydration（水合）以实现交互性。</p>
<p>• <strong>示例代码</strong>：使用 <code>useState</code> 管理状态并响应用户操作。</p>
<pre><code class="language-javascript">'use client';
import { useState } from 'react';
export default function Counter() {
  const [count, setCount] = useState(0);
  return &lt;button onClick={() =&gt; setCount(count + 1)}&gt;{count}&lt;/button&gt;;
}

</code></pre>
<hr>
<h2><strong>2. 功能特性</strong></h2>
<table>
<thead>
<tr>
<th><strong>特性</strong></th>
<th><strong>服务端组件</strong></th>
<th><strong>客户端组件</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>数据获取</strong></td>
<td>可直接访问数据库、API 或敏感数据。</td>
<td>需通过客户端请求（如 <code>fetch</code>），数据可能暴露。</td>
</tr>
<tr>
<td><strong>React Hooks</strong></td>
<td>不支持 <code>useState</code>、<code>useEffect</code> 等。</td>
<td>支持所有 React Hooks。</td>
</tr>
<tr>
<td><strong>浏览器 API</strong></td>
<td>无法访问。</td>
<td>可访问 <code>window</code>、<code>document</code> 等。</td>
</tr>
<tr>
<td><strong>性能优化</strong></td>
<td>减少客户端 JS 体积，提升 SEO 和首屏加载速度。</td>
<td>增加客户端 JS 体积，但支持动态交互。</td>
</tr>
<tr>
<td><strong>嵌套规则</strong></td>
<td>可包含客户端组件。</td>
<td>不可嵌套服务端组件（仅能通过 Props 传递数据）。</td>
</tr>
</tbody>
</table>
<hr>
<h2><strong>3. 适用场景</strong></h2>
<ul>
<li><strong>服务端组件优先使用的情况</strong>：</li>
</ul>
<p>• 静态内容展示（如博客文章、产品列表）。</p>
<p>• 需要直接访问数据库或敏感数据的场景。</p>
<p>• SEO 优化需求高的页面。</p>
<ul>
<li><strong>客户端组件优先使用的情况</strong>：</li>
</ul>
<p>• 用户交互（如表单提交、按钮点击）。</p>
<p>• 动态状态管理（如购物车、计数器）。</p>
<p>• 依赖浏览器 API 的功能（如动画、地理位置）。</p>
<hr>
<h2><strong>4. 混合使用与数据传递</strong></h2>
<ul>
<li><strong>组合方式</strong>：服务端组件可通过 Props 将数据传递给客户端组件。例如：</li>
</ul>
<pre><code class="language-javascript">// 服务端组件（传递数据）
export default async function ServerParent() {
  const data = await fetchData();
  return &lt;ClientChild data={data} /&gt;;
}

// 客户端组件（接收数据）
'use client';
export default function ClientChild({ data }) {
  const [filter, setFilter] = useState('');
  // 根据 data 进行过滤等操作
}

</code></pre>
<ul>
<li><strong>限制</strong>：客户端组件无法直接导入服务端组件，需通过 Props 或占位符传递。</li>
</ul>
<hr>
<h2><strong>5. 性能与安全</strong></h2>
<ul>
<li><strong>服务端优势</strong>：</li>
</ul>
<p>• 缓存渲染结果，减少重复请求。</p>
<p>• 敏感逻辑（如数据库查询）保留在服务端，避免泄露。</p>
<ul>
<li><strong>客户端劣势</strong>：</li>
</ul>
<p>• 增加客户端 JS 体积，可能影响加载速度。</p>
<p>• 需谨慎处理用户输入和敏感数据。</p>
<hr>
<h2><strong>总结</strong></h2>
<p>服务端组件和客户端组件在 Next.js 中形成互补：</p>
<p>• <strong>服务端组件</strong> 侧重于性能优化、数据安全与静态内容渲染。</p>
<p>• <strong>客户端组件</strong> 专注于交互性和动态功能。</p>
<p>合理混合使用两者（如服务端获取数据后传递给客户端处理交互），可构建高效且用户友好的应用。</p>
<h2>系列文章</h2>
<p><strong><a href="/posts/nextjs-series-difference-between-client-components-and-server-components">Next.js 系列之客户端组件（Client Components）和服务端组件（Server Components）的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-introduction-about-serverless-architecture">Next.js 系列之Serverless（无服务器架构）介绍</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-app-router-and-page-router">Next.js 系列之 app router 和 page router 的区别</a></strong></p>
<p><strong><a href="/posts/nextjs-series-difference-between-ssg-ssr-isr-and-csr-in-nextjs">Next.js 系列之的SSG、SSR、ISR、CSR 的详细介绍和区别</a></strong></p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Tue, 01 Apr 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>个税年度汇算时，申报的劳务报酬总收入小于本年度代开的劳务发票总金额，该如何处理？</title>
      <link>https://codexun.com/posts/resolve-individual-income-tax-declaration-review</link>
      <guid isPermaLink="true">https://codexun.com/posts/resolve-individual-income-tax-declaration-review</guid>
      <description>2024年度个税汇算退税，从 3月21 号开始不需要预约了，然后抓紧时间对 2024 年的个税进行汇算申报，说不定还能退点税呢。 1、全年一次性奖金计税方式的选择 趁着周末的时间，打开了个税 APP进行了一番操作，其中 24 年因为有一项是奖金，可以尝试选择 **计税方式 **，两种计税方式对比一下，在最后一步查看退税</description>
      <content:encoded><![CDATA[<p>2024年度个税汇算退税，从 3月21 号开始不需要预约了，然后抓紧时间对 2024 年的个税进行汇算申报，说不定还能退点税呢。</p>
<p><img src="/api/images/image/2026/07/51fb7c62918cfd57-image.png" alt="image.png"></p>
<h2>1、全年一次性奖金计税方式的选择</h2>
<p>趁着周末的时间，打开了个税 APP进行了一番操作，其中 24 年因为有一项是奖金，可以尝试选择 **计税方式 **，两种计税方式对比一下，在最后一步查看退税或者需要补税的金额大小，选择最优解。</p>
<p><img src="/api/images/image/2026/07/3a11e8633ee75519-531f5532-b99c-45e5-aeb2-706c16141ac3.png" alt="image.png"></p>
<h2>2、税务审核不通过</h2>
<p>昨天收到短信通知，告知审核不通过。查看个税 APP 审核状态，有如下信息：“<strong>由于您申报的收入存在待核实的事项，且纳税人年度汇算时申报的劳务报酬总收入小于本年度代开的劳务发票（不含汇总代开发票）总金额，故不予同意本次退税申请。</strong>”</p>
<p><img src="/api/images/image/2026/07/c1a38ceca7035729-image.png" alt="image.png"></p>
<p>搜索有效信息，审核不通过的这个原因的意思是：2024年度代开了劳务发票，但是扣缴义务人未代扣代缴或少代扣代缴了劳务报酬所得的个人所得税，所以需要您更正年度申报，自行添加未申报或少申报的劳务报酬收入。</p>
<p>仔细查看劳务报酬所得，大概率是 <strong>京粉返利</strong> 导致的数据有出入了。</p>
<p>有其他人遇到了同样的情况，具体就是 “<strong>相关情况为23年个税记录申报后，发票延期至24年开具导致。因为开票与申报时间不匹配的原因导致出现此提醒，实际对应金额的收入平台前期均已正常申报。当前可反馈兆盈侧协助提供说明文件，待出具后您可将说明文件上传至个税APP推送补正材料入口(无入口可联系主管税局沟通推送补正材料入口)。如您需兆盈侧协助提供相应说明材料信息， 可填写下方问卷信息以便协助处理</strong>。”</p>
<p>京东返利 23 年的发票延期到 24 年开具导致的。</p>
<h2>3、税务审核不通过解决办法</h2>
<p>如果 <strong>申报的劳务报酬总收入</strong>** <strong>和</strong> ****本年度代开的劳务发票总金额 **差别不大的话，可以如下解决。</p>
<h3>3.1、查看<strong>本年度代开的劳务发票总金额</strong></h3>
<p>登录个人所得税APP，点击【办&amp;查】，下滑到【发票管理】模块，选择【我的票夹】。</p>
<p>点击【我销售的】—【筛选】，开票日期选择【自定义】，填写汇算年度（一定要自定义选择时间，2024-01-01—2024-12-31），发票类型分别选择【数电票】和【税控发票】，其他默认【全部】，点击【确定】后进行查询，统计代开发票总金额。</p>
<p><img src="/api/images/image/2026/07/1fd62b0e06e6494c-image.png" alt="image.png"></p>
<p>统计本年度代开发票总金额可以有个技巧，在我的票夹下选择批量管理，把所有的全选，左下角显示即是 24 年度代开发票总金额。</p>
<p><img src="/api/images/image/2026/07/748813c964dbdc3d-image.png" alt="image.png"></p>
<h3>3.2、查看<strong>申报的劳务报酬总收入</strong></h3>
<p><strong>申报的劳务报酬总收入 <strong>这个比较容易看到，在进行申报的时候点进劳务报酬选项内，有金额合计即是 24 年申报的劳务报酬总收入</strong>。</strong></p>
<p><img src="/api/images/image/2026/07/37bfdc29790e163c-image.png" alt="image.png"></p>
<h3>3.3、新增一笔劳务报酬，重新申报</h3>
<p>第一次申报审核没有通过的原因，就是<strong>申报的劳务报酬总收入【78.06元】小于本年度代开的劳务发票总金额【122. 83 元】</strong>，也就是这两个金额对不上。</p>
<p>要解决这个问题，可以在劳务报酬这个页面中，右上角点击<strong>新增</strong>，点击新增一项劳务报酬所得，新增的金额为 44.77 元【122.83-78.06】，<strong>这个根据自己实际情况填写</strong>。</p>
<p><img src="/api/images/image/2026/07/8c59f2f092940370-image.png" alt="image.png"></p>
<p>新增页面的备注可以参考如下填写：</p>
<blockquote>
<p>💡
购物返利未申报部分差额
支付人：宿迁兆盈商服科技有限公司
支付地址：江苏省，宿迁市
价税合计122.83元
已申报金额78.06元
差额44.77元
识别号：91321311MA1X716Q06</p>
</blockquote>
<h3>3.4、第二天成功审核通过</h3>
<p>将以上的重新申报提交之后，第二天查看申报记录，已经成功审核通过啦，下一步就是美美的等着退税到账即可。</p>
<p><img src="/api/images/image/2026/07/5dcd11ddb9917f4f-image.png" alt="image.png"></p>]]></content:encoded>
      <category>记录</category>
      <pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>为博客集成utterances 评论组件</title>
      <link>https://codexun.com/posts/integrate-utterances-comment-component-in-blog</link>
      <guid isPermaLink="true">https://codexun.com/posts/integrate-utterances-comment-component-in-blog</guid>
      <description>utterances 是一个轻量级的评论插件 Utterances，基于 GitHub Issues 构建，无广告、无跟踪、开源且永久免费。它通过 GitHub OAuth 流程授权用户发表评论，所有数据存储在 GitHub 中，支持多种页面与 Issue 的映射方式，并提供多种主题自定义选项。 1、utterance</description>
      <content:encoded><![CDATA[<p>utterances 是一个轻量级的评论插件 Utterances，基于 GitHub Issues 构建，无广告、无跟踪、开源且永久免费。它通过 GitHub OAuth 流程授权用户发表评论，所有数据存储在 GitHub 中，支持多种页面与 Issue 的映射方式，并提供多种主题自定义选项。</p>
<h2>1、utterances工作原理</h2>
<p>utterances 是基于 GitHub issues 构建的轻量级评论小部件，所有的评论信息也即是存储在 Github issues 中的，使用 GitHub issues管理博客的评论等。</p>
<p>当博客页面中 utterances 的组件加载时，GitHub issues搜索API将根据<code>url</code>，<code>pathname</code>或<code>title</code>找到与页面关联的 issues。如果我们找不到与该页面匹配的issue，那么utterances 机器人会在第一次发表评论时自动创建 issue。</p>
<p>用户要发表评论，必须使用Github Oauth授权登录，用户在博客下面发布评论，实质上是在对应的GitHub issue上发表评论。</p>
<h2>2、utterances 优点</h2>
<p>最主要的优点还是轻量接入，utterances插件通过 GitHub OAuth 授权用户发表评论，或者用户可以直接在 GitHub Issue 上评论。</p>
<p>另外，还有如下特点：</p>
<ul>
<li>支持多种页面与 Issue 的映射方式，如根据页面路径名、URL、标题或 Open Graph 标题等自动匹配或创建 Issue。</li>
<li>可配置特定 Issue 编号或自定义标题关键词以匹配 Issue，同时支持为创建的 Issue 添加标签。</li>
<li>提供多种主题选项，可以定制样式，并通过简单的 script 标签将 Utterances 集成到博客中。</li>
</ul>
<h2>3、utterances 接入步骤</h2>
<h3>3.1、创建仓库并安装 utterances</h3>
<p>1、然后需要创建一个公开的 github repo，接入utterances之后该仓库的 issues 用于存储博客的评论。</p>
<p>2、在你 github repo 中安装 ‣ ，点击左边的链接安装到你 账号中，安装之后可以在用</p>
<p>户设置的 Application 中查看已经安装。</p>
<p><img src="/api/images/image/2026/07/63f8ddce049adc47-image.png" alt="安装utterances"></p>
<p><img src="/api/images/image/2026/07/a071caaafe6984fa-image.png" alt=" 安装成功，并进行配置"></p>
<p>然后点击 Configure 配置按钮，确保第一步中的 github repo打开了对 **utterances **的操作权限。</p>
<h3>3.2、代码配置到 blog 文件中</h3>
<p>打开 ‣，在configuration 中配置 github repo 地址。</p>
<blockquote>
<p>💡
注意配置的格式是 owner/repo，其中 owner 是 account name，也就是你 github url中的name。</p>
</blockquote>
<p><img src="/api/images/image/2026/07/cd5fb25f161ee406-image.png" alt="image.png"></p>
<p>然后会得到要嵌入到 blog 系统中的一段代码，根据blog 不同的技术实现，粘贴到相应的博客模板位置即可。</p>
<p>使用<code>.utterances</code>和 <code>.utterances-frame</code>选择器自定义布局和样式。</p>
<pre><code class="language-javascript">&lt;script src=&quot;https://utteranc.es/client.js&quot;
        repo=&quot;[ENTER REPO HERE]&quot;
        issue-term=&quot;pathname&quot;
        theme=&quot;github-light&quot;
        crossorigin=&quot;anonymous&quot;
        async&gt;
&lt;/script&gt;
</code></pre>
<h2>参考资料</h2>
<p><a href="https://utteranc.es/">https://utteranc.es/</a></p>
<p><a href="https://github.com/utterance/utterances">https://github.com/utterance/utterances</a></p>
<p><a href="https://bruceit.com/p/AGDc6W">使用 utterances 作为博客评论组件</a></p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Fri, 21 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>XAPK是什么文件，如何在安卓上安装XAPK？</title>
      <link>https://codexun.com/posts/how-to-install-xapk-apk-file</link>
      <guid isPermaLink="true">https://codexun.com/posts/how-to-install-xapk-apk-file</guid>
      <description>什么是XAPK? XAPK文件是一种文件扩展格式，它与APK格式有些相似，但也有所不同。 可以这么理解： XAPK文件= APK文件+ OBB数据文件。（OBB（Opaque Binary Blob）文件格式，是安卓游戏通用数据包）包含了单独的APK文件和OBB cache assets文件。 Google Play </description>
      <content:encoded><![CDATA[<h2>什么是XAPK?</h2>
<p>XAPK文件是一种文件扩展格式，它与APK格式有些相似，但也有所不同。</p>
<p>可以这么理解： <strong>XAPK文件= APK文件+ OBB数据文件</strong>。（<strong>OBB</strong>（Opaque Binary Blob）<strong>文件</strong>格式，是<strong>安卓</strong>游戏通用数据包）包含了单独的APK文件和OBB cache assets文件。</p>
<p>Google Play 商店要求压缩APK大小不超过100 MB。用户通过Google Play Console每次上传一个APK时可以选择附带一到两个.obb格式的文件，这种.obb文件可以是大小不超过2 GB的任何格式（ZIP，PDF，MP4等）。</p>
<p>XAPK格式确保用户可以顺利下载大型文件避免中断或失败。</p>
<h2><strong>为什么使用</strong>XAPK?</h2>
<p><strong>APK文件</strong>是在<strong>Android智能手机</strong>上安装应用程序的标准格式。由于大小原因，通常必须单独获取OBB文件。</p>
<p>XAPK是一种zip格式，可将所有应用程序数据保存在一个文件中，以方便安装。如果您要下载XAPK文件，则该文件同时包含OBB和APK部分，以及同一存储库中的缓存，图标和其他信息。</p>
<p>但是，XAPK文件不像APK文件那样，可以通过手机上的默认安装器直接安装到安卓手机上。需要找一些三方工具，比如 XAPK Installer，或者 APKPure。</p>
<h2>安装XAPK的方法和工具</h2>
<h3>1、工具 xapk installer</h3>
<p><img src="/api/images/image/2026/07/ea39d076a9c121c6-image.png" alt="image.png"></p>
<p>可以到 Google Play 上搜索 xapk installer，安装好之后扫描xapk 的文件，或者手动选择到 xapk 的路径，然后打开即可安装。</p>
<h3>2、工具Install Buddy: XAPK, APKs</h3>
<p><img src="/api/images/image/2026/07/c783b7bb0280866d-image.png" alt="image.png"></p>
<p>同上，可以在 google 应用商店搜索该应用。</p>
<h3>3、工具ApkPure</h3>
<p>APKPure XAPK 安装器可以通过最便捷的方式帮你一键安装和管理安卓设备上的APK和XAPK文件。</p>
<p>可以帮你集中浏览和管理所有XAPK和APK文件，同样也可以通过最简单的方式一键更新或者卸载任意XAPK和APK文件。</p>
<p><img src="/api/images/image/2026/07/289c0ba61c3212b4-image.png" alt="image.png"></p>
<blockquote>
<p>💡
需要注意的是，要为 xapk installer 等这些工具打开文件访问、应用安装（未知来源）的权限。</p>
</blockquote>]]></content:encoded>
      <category>工具</category>
      <pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>使用自定义 GitHub Actions CI/CD 部署到 Vercel</title>
      <link>https://codexun.com/posts/github-actions-deploy-vercel</link>
      <guid isPermaLink="true">https://codexun.com/posts/github-actions-deploy-vercel</guid>
      <description>除了上一篇介绍的 使用 Github Action自动执行Vercel 部署** **使用Deploy Hooks 的方式进行部署之外，还可以有更复杂的场景。 比如可以将 Vercel 与 GitHub Actions 一起使用作为您的 CI/CD 提供程序，在每次推送代码时生成预览部署，并在代码合并到main分支时部</description>
      <content:encoded><![CDATA[<p>除了上一篇介绍的 <strong><a href="/posts/automating-vercel-deployment-with-github-actions">使用 Github Action自动执行Vercel 部署</a></strong>** **使用Deploy Hooks 的方式进行部署之外，还可以有更复杂的场景。</p>
<p>比如可以将 Vercel 与 GitHub Actions 一起使用作为您的 CI/CD 提供程序，在每次推送代码时生成预览部署，并在代码合并到<code>main</code>分支时部署到生产环境。</p>
<p>这种则需要使用到 Vercel 的访问令牌 access_token 等。</p>
<h2>1、获取相关访问令牌</h2>
<p>获取<code>VERCEL\_TOKEN</code>、<code>VERCEL\_ORG\_ID</code>和<code>VERCEL\_PROJECT\_ID</code> 相关token。</p>
<h3>1.1、<a href="/301e3f3674f981688810f6912660d524?pvs=25">获取 Vercel api 访问令牌</a>VERCEL_TOKEN</h3>
<p>在 个人账号设置 → Token 菜单下，创建 api 访问令牌，也即是<code>VERCEL\_TOKEN</code>。</p>
<p><img src="/api/images/image/2026/07/bd42918a3b17d535-image.png" alt="image.png"></p>
<h3>1.2、获取VERCEL_PROJECT_ID</h3>
<p>在Vercel 具体项目下， 项目设置 下找到 Project Id。</p>
<p><img src="/api/images/image/2026/07/879a56ba183efc1e-image.png" alt="image.png"></p>
<h3>1.3、获取VERCEL_ORG_ID</h3>
<ul>
<li>使用命令行<code>npm i -g vercel</code>安装Vercel CLI并运行<code>vercel login</code></li>
<li>在新建文件夹中，运行<code>vercel link</code>以创建一个新的 Vercel 项目，或者是已有的Git 项目，会提示 <code>✅  Linked to ``**xxx-projects/next-starter-example**`` (created .vercel)</code> 并生成.vercel 文件夹。</li>
<li>在生成的.vercel文件夹中，从project.json<code>{&quot;projectId&quot;:&quot;prj\_xxx&quot;,&quot;orgId&quot;:&quot;team\_xxx&quot;}</code>保存projectId和orgId。</li>
</ul>
<blockquote>
<p>💡
在Account Setting→General 菜单下，有<code>Vercel ID\[This is your user ID within Vercel.\]</code> API Token，是否可以作为<code>VERCEL\_ORG\_ID</code>？<strong>[未验证]</strong></p>
<p><img src="/api/images/image/2026/07/ea37b6244a84d388-image.png" alt="image.png"></p>
</blockquote>
<h3>1.4、配置到 Github 中</h3>
<p>将以上<code>VERCEL\_TOKEN</code>、<code>VERCEL\_ORG\_ID</code>和<code>VERCEL\_PROJECT\_ID</code>添加到 GitHub 项目的 actions secret 配置中。</p>
<p><img src="/api/images/image/2026/07/8943188bfb14ac84-image.png" alt="image.png"></p>
<h2>2、配置 Github Actions</h2>
<p>当代码推送到非 main 分支时，生成预览部署：</p>
<pre><code class="language-yaml">name: GitHub Actions Vercel Preview Deployment
env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
  push:
    branches-ignore:
      - main
jobs:
  Deploy-Preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Vercel CLI
        run: npm install --global vercel@canary
      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
      - name: Build Project Artifacts
        run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
</code></pre>
<p>当代码推送到 main 分支时，生成正式部署：</p>
<pre><code class="language-yaml">name: GitHub Actions Vercel Production Deployment
env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
  push:
    branches:
      - main
jobs:
  Deploy-Production:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Vercel CLI
        run: npm install --global vercel@canary
      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
</code></pre>
<h2>3、借助marketplace 上的 Github Actions 实现</h2>
<p>也可以借助一些已经实现好的 actions 来实现我们的需求。比如：</p>
<h3>3.1、代码推送或者发起 pull request 时触发</h3>
<pre><code class="language-yaml"># 可以参考我的工作流
# 定义 GitHub Actions 的工作流程名称
name: Deploy to Vercel

# 触发条件
on:
  push:
    branches: [ main ] # 当代码推送到 main 分支时触发
  pull_request:
    branches: [ main ] # 当针对 main 分支发起 Pull Request 时触发

# 定义工作流程中的作业（jobs）
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: '9.8.0'
      - name: Install Dependencies
        run: pnpm install
      - name: Build
        run: pnpm build
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          working-directory: ./
</code></pre>
<h3>3.2、另一例：<a href="https://github.com/BetaHuhn/deploy-to-vercel-action">deploy-to-vercel-action</a></h3>
<p>使用 GitHub Actions 将您的项目/站点部署到<a href="https://vercel.com/">Vercel</a>。在何时部署站点方面，它比 Vercel 的 GitHub 集成提供了更多的自定义功能。使用 GitHub Actions <a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows">Events，</a>您可以选择部署每个提交，仅在新版本上或甚至在 cron 计划上部署。该操作还可以部署每个 PR 并使用自定义预览 URL 对其进行评论。它使用 Vercel CLI，也可以自动在 GitHub 上创建部署。</p>
<pre><code class="language-yaml">name: Deploy CI
on:
  push:
    branches: [master]
  pull_request:
    types: [opened, synchronize, reopened]
jobs:
  deploy:
    runs-on: ubuntu-latest
    if: &quot;!contains(github.event.head_commit.message, '[skip ci]')&quot;
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Deploy to Vercel Action
        uses: BetaHuhn/deploy-to-vercel-action@v1
        with:
          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
</code></pre>
<h2>参考资料</h2>
<p><strong><a href="https://vercel.com/guides/how-can-i-use-github-actions-with-vercel">How can I use GitHub Actions with Vercel?</a></strong></p>
<p><strong><a href="https://github.com/vercel/examples/blob/main/ci-cd/github-actions/README.md">Bring Your Own CI/CD - GitHub Actions</a></strong>** **</p>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>使用 Github Action自动执行Vercel 部署</title>
      <link>https://codexun.com/posts/automating-vercel-deployment-with-github-actions</link>
      <guid isPermaLink="true">https://codexun.com/posts/automating-vercel-deployment-with-github-actions</guid>
      <description>之前本网站部署在 Netlify 上的，有过一篇文章**使用 Github Action自动执行 Netlify 部署****。 **以解决每周自动部署网站的功能，详细可点击查看。 最近想查看下国内访问网站的速度情况，所以使用 itdog ping 工具进行了测试，发现国内的情况十分不理想。 所以萌生了起换到 verc</description>
      <content:encoded><![CDATA[<p>之前本网站部署在 Netlify 上的，有过一篇文章**<a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">使用 Github Action自动执行 Netlify 部署</a>****。 **以解决每周自动部署网站的功能，详细可点击查看。</p>
<p>最近想查看下国内访问网站的速度情况，所以使用<a href="https://www.itdog.cn/"> itdog ping 工具</a>进行了测试，发现国内的情况十分不理想。</p>
<p>所以萌生了起换到 vercel 上的想法，并且 vercel 也有 cdn 功能了，右边是切换到 vercel 上部署的情况，看起来好了很多。</p>
<p><img src="/api/images/image/2026/07/cb7af7f7b7f09472-image.png" alt="image.png"></p>
<p><img src="/api/images/image/2026/07/1612750564712d22-image.png" alt="image.png"></p>
<p>切到 vercel 重新部署，域名重新解析配置好之后，对于之前 github action 定时部署的情况，需要重新写一份 workflow job。</p>
<p>搜索vercel 同样支持 Deploy Hook 的方式。那就简单了，复制一份 Netlify 的文件，简单修改即可。</p>
<h2>1、配置创建 Vercel Build Hooks</h2>
<p>参考 <strong><a href="https://vercel.com/docs/deploy-hooks">Creating &amp; Triggering Deploy Hooks</a></strong>  官方文档。</p>
<p>官方说的清清楚，部署钩子允许您创建接受 HTTP<code>POST</code>请求的 URL，以触发部署并重新运行**<a href="https://vercel.com/docs/deployments/configure-a-build">构建步骤</a>**。这些 URL 与您的项目、存储库和分支唯一链接，因此无需使用任何身份验证机制或向请求提供任何有效负载<code>POST</code>。</p>
<p>此功能允许您将 Vercel 部署与其他系统集成。例如，您可以设置：</p>
<ul>
<li>来自 Headless CMS 的内容更改的自动部署</li>
<li>通过配置第三方 cron 作业服务来触发 Deploy Hook 来进行计划部署</li>
<li>通过命令行强制部署</li>
</ul>
<p>在vercel 项目设置→Git中找到 Deploy Hooks 创建的地方，输入 hooks 名字，和代码分支，即可创建。</p>
<p><img src="/api/images/image/2026/07/badea35baa408a1f-image.png" alt="image.png"></p>
<h2>2、创建 GitHub Actions secrets and variables</h2>
<p>在Github 的项目Setting下设置 github actions 所需要的变量。
例如，创建VECEL_BUILD_HOOK变量，value 为上一步获得的 vecel build hook 地址。</p>
<p><img src="/api/images/image/2026/07/b627f44c0ab20098-image.png" alt="image.png"></p>
<h2>3、实现 GitHub Actions</h2>
<p>触发 hooks 的示例请求为：</p>
<pre><code class="language-bash">curl -X POST https://api.vercel.com/v1/integrations/deploy/prj_98g22o5YUFVHlKOzj9vKPTyN2SDG/tKybBxqhQs
</code></pre>
<p>响应示例：</p>
<pre><code class="language-json">{
  &quot;job&quot;: {
    &quot;id&quot;: &quot;okzCd50AIap1O31g0gne&quot;,
    &quot;state&quot;: &quot;PENDING&quot;,
    &quot;createdAt&quot;: 1662825789999
  }
}
</code></pre>
<p>发送请求后，您可以在项目仪表板上看到它触发了部署。</p>
<p>那么，实现 github actions 代码就比较简单了，参考如下：</p>
<pre><code class="language-yaml">name: Trigger Vercel Build
on:
  schedule:
    - cron: '5 0 * * 2' # At 00:05 on 星期二.
  workflow_dispatch:  # 手动触发
jobs:
  build:
    name: Request Vercel Webhook
    runs-on: ubuntu-latest
    steps:
      - name: Curl request
        env:
          VERCEL_BUILD_HOOK: ${{ secrets.VERCEL_BUILD_HOOK }}
        run: curl -X POST $VERCEL_BUILD_HOOK
</code></pre>
<h2>参考资料</h2>
<p><strong><a href="https://vercel.com/docs/deploy-hooks">Creating &amp; Triggering Deploy Hooks</a></strong>  官方文档</p>
<p><strong><a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">使用 Github Action自动执行 Netlify 部署</a></strong></p>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Sun, 16 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>GitHub Actions：带有输入的调度和计划工作流程</title>
      <link>https://codexun.com/posts/github-actions-workflow-with-inputs</link>
      <guid isPermaLink="true">https://codexun.com/posts/github-actions-workflow-with-inputs</guid>
      <description>最近编写了一个项目，需要每天定时从 producthunt 抓取数据并生成阅读良好的 markdown 文档，项目已经上线，[参考地址](https://indiehack.org/)。 除此之外，还希望能够输入日期，手动触发抓取这一天的数据。 也就是，工作流有输入。当工作流由 cron 触发时，我需要它使用一些默认变</description>
      <content:encoded><![CDATA[<p>最近编写了一个项目，需要每天定时从 producthunt 抓取数据并生成阅读良好的 markdown 文档，项目已经上线，<code>[参考地址](https://indiehack.org/)</code>。</p>
<p>除此之外，还希望能够输入日期，手动触发抓取这一天的数据。</p>
<p>也就是，工作流有输入。当工作流由 cron 触发时，我需要它使用一些默认变量运行。当手动触发时，我想允许用户覆盖一些默认值。</p>
<p><img src="/api/images/image/2026/07/9f68be8ebede8b98-image.png" alt="image.png"></p>
<h2>Github Actions 工作流示例</h2>
<p>比如：有如下 github actions 工作流：</p>
<pre><code class="language-yaml">name: Print some text every 3rd minute
on:
  schedule:
    - cron: '1/3 * * * *'

jobs:
  print_text:
    runs-on: 'ubuntu-20.04'
    steps:
      -name: Print some text
       run: echo &quot;Periodically printing passages&quot;
</code></pre>
<p>每隔一段时间，输出一段文字<code>Periodically printing passages</code></p>
<p>如果手动触发时，允许输入参数覆盖这一段文字，可以在定义工作流中，使用 inputs 来定义可以接收的输入，参考如下：</p>
<pre><code class="language-yaml">name: Print some text every 3rd minute
on:
  schedule:
    - cron: '1/3 * * * *'
  workflow_dispatch: # adding the workflow_dispatch so it can be triggered manually
    inputs:
      text_to_print:
        description: 'What text do you want to print?'
        required: true
        default: 'Periodically printing passages'

jobs:
  print_text:
    runs-on: 'ubuntu-20.04'
    steps:
      - name: Set the variables
        env:
          DEFAULT_MESSAGE: 'Periodically printing passages' # here is the default message
        run: echo &quot;MESSAGE=${{ github.event.inputs.text_to_print || env.DEFAULT_MESSAGE }}&quot; &gt;&gt; $GITHUB_ENV

      - name: Print some text
        run: echo &quot;$MESSAGE&quot; # prints the environmental variable, MESSAGE
</code></pre>
<p>我可以在 GitHub 中的 Actions 标签中触发此操作：</p>
<p><img src="/api/images/image/2026/07/4613aa45df15e7d3-image.png" alt="image.png"></p>
<p>让这个工作发挥作用的关键是：</p>
<p><code>echo &quot;MESSAGE=${{ github.event.inputs.text\_to\_print || env.DEFAULT\_MESSAGE }}&quot; &gt;&gt; $GITHUB\_ENV</code></p>
<p>上面一行选择将定义的变量赋值给变量，<code>MESSAGE</code>然后将其注入到变量中<code>$GITHUB\_ENV</code>。我们可以在<code>Print some text</code>步骤中引用它。</p>
<h2>参考资料</h2>
<p><strong><a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">使用 Github Action自动执行 Netlify 部署</a></strong></p>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Sat, 15 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>remark：转换 Markdown 的神器</title>
      <link>https://codexun.com/posts/remark-transforms-markdown-to-html</link>
      <guid isPermaLink="true">https://codexun.com/posts/remark-transforms-markdown-to-html</guid>
      <description>remark 是一个使用插件转换 Markdown 的工具。它可以检查和修改您的标记，可以在服务器、客户端、CLI、Deno 等上使用 remark。 另外，remark 还是一个插件生态系统，它使用 markdown 作为结构化数据，特别是 AST（抽象语法树）。AST 使程序可以轻松处理 markdown，我们称这</description>
      <content:encoded><![CDATA[<p>remark 是一个使用插件转换 Markdown 的工具。它可以检查和修改您的标记，可以在服务器、客户端、CLI、Deno 等上使用 remark。</p>
<p>另外，remark 还是一个插件生态系统，它使用 markdown 作为结构化数据，特别是 AST（抽象语法树）。AST 使程序可以轻松处理 markdown，我们称这些程序为插件。remark 生态系统包含许多现有插件，当然也可以自己制作插件。</p>
<p><img src="/api/images/image/2026/07/5a4b85724b69da33-image.png" alt="image.png"></p>
<p>guthub：<a href="https://github.com/remarkjs/remark">https://github.com/remarkjs/remark</a></p>
<h2>1、remark主要功能点</h2>
<ul>
<li>100% 符合 CommonMark 和 GFM 或 MDX(通过插件)</li>
<li>**<a href="https://github.com/remarkjs/remark?tab=readme-ov-file#syntax-tree">AST</a>**抽象语法树(AST)使检查和修改内容变得更加容易</li>
<li>**<a href="https://www.npmtrends.com/remark-parse-vs-marked-vs-micromark-vs-markdown-it">popular</a>**世界上最流行的 Markdown 解析器</li>
<li>**<a href="https://github.com/remarkjs/remark?tab=readme-ov-file#plugins">插件</a>**150 多个可选择的插件</li>
</ul>
<h2>2、remark如何使用</h2>
<p>将markdown 转换为html一个示例，比如通过如下代码可以将一段 markdown 转换为</p>
<p>markdown：
<code># Hello, \*Mercury\*!</code></p>
<p>html：
<code>&lt;h1&gt;Hello, &lt;em&gt;Mercury&lt;/em&gt;!&lt;/h1&gt;</code></p>
<p>示例代码：</p>
<pre><code class="language-typescript">import rehypeStringify from 'rehype-stringify'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {unified} from 'unified'

const file = await unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeStringify)
  .process('# Hello, *Mercury*!')

console.log(String(file)) // =&gt; '&lt;h1&gt;Hello, &lt;em&gt;Mercury&lt;/em&gt;!&lt;/h1&gt;'
</code></pre>
<h2>3、remark插件系统</h2>
<ul>
<li><code>[remark-parse](https://github.com/remarkjs/remark/tree/main/packages/remark-parse)</code>— 插件将 markdown 作为输入并将其转换为语法树 (mdast)</li>
<li><code>[remark-stringify](https://github.com/remarkjs/remark/blob/main/packages/remark-stringify)</code>— 插件获取语法树（mdast）并将其转换为 markdown 作为输出</li>
<li><code>[remark](https://github.com/remarkjs/remark/tree/main/packages/remark)</code>—<code>unified、remark-parse、remark-stringify</code>在输入和输出为 markdown 时很有用</li>
<li><code>[remark-cli](https://github.com/remarkjs/remark/tree/main/packages/remark-cli)</code>— CLI 用于<code>remark</code>检查和格式化脚本中的 markdown</li>
<li><code>[remark-gfm](https://github.com/remarkjs/remark-gfm)</code>— 添加对 GFM（GitHub 风格 markdown）的支持</li>
<li><code>[remark-lint](https://github.com/remarkjs/remark-lint)</code>— 检查 markdown 并警告不一致之处</li>
<li><code>**[remark-toc](https://github.com/remarkjs/remark-toc)**</code><strong>— 生成目录</strong></li>
<li><code>**[remark-collapse](https://github.com/Rokt33r/remark-collapse)**</code>** – 使某些部分可折叠**</li>
<li><code>**[remark-breaks](https://github.com/remarkjs/remark-breaks)**</code><strong>— 支持硬中断而不需要空格或转义符。</strong></li>
<li><code>[remark-rehype](https://github.com/remarkjs/remark-rehype)</code>— 将 markdown 转换为 HTML</li>
<li><code>[remark-directive](https://github.com/remarkjs/remark-directive)</code>— 支持指令</li>
<li><code>[remark-frontmatter](https://github.com/remarkjs/remark-frontmatter)</code>— 支持前置内容（YAML、TOML 等）</li>
<li><code>[remark-math](https://github.com/remarkjs/remark-math)</code>— 支持数学</li>
<li><code>**[remark-html](https://github.com/remarkjs/remark-html)**</code><strong>—将 markdown 编译为 HTML 的统一（remark ）插件。当您想将 markdown 转换为 HTML 时，此插件很有用。它是 的快捷方式 <strong><code>**.use(remarkRehype).use(rehypeSanitize).use(rehypeStringify)**</code></strong>。</strong></li>
</ul>
<p>您可以从现有的 150 多个插件中进行选择。以下是查找插件的三种好方法：</p>
<ul>
<li><code>[awesome-remark](https://github.com/remarkjs/awesome-remark)</code>— 精选最棒的项目</li>
<li><code>[官方插件列表](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)</code>— 所有插件的列表</li>
<li><code>[remark-plugin主题](https://github.com/topics/remark-plugin)</code>— GitHub 上的任何带remark-plugin标签的 repo</li>
</ul>
<p>其中<code>[remark-breaks](https://github.com/remarkjs/remark-breaks)</code> 是比较好用的一个插件，经常我们 markdown 如下两个段落，使用了这个插件后，转换的 html 能够保留两个段落。</p>
<pre><code class="language-plain">Mars is
the fourth planet
</code></pre>
<p>转换后</p>
<pre><code class="language-html">&lt;p&gt;Mars is&lt;br&gt;
the fourth planet&lt;/p&gt;
</code></pre>
<p>如果不使用的话，这两行文字转换后得到一行文字。</p>
<h2>4、remark 样式模板</h2>
<p>remark-templates：<a href="https://github.com/3mdeb/remark-templates">https://github.com/3mdeb/remark-templates</a></p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Fri, 14 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>pdftk：命令行工具对 pdf 文档进行合并分割</title>
      <link>https://codexun.com/posts/pdftk-pdf-merge-split-tool</link>
      <guid isPermaLink="true">https://codexun.com/posts/pdftk-pdf-merge-split-tool</guid>
      <description>pdftk 是我们处理 PDF 的命令行工具，可以在客户端脚本或服务器端对 PDF 文档进行处理。 1、PDFtk 功能 PDFtk 可以有如下等功能： 合并 PDF 文档或整理 PDF 页面扫描件 将 PDF 页面拆分为新文档 旋转 PDF 文档或页面 根据需要加密输出 应用背景水印或前景图章 解压 PDF 附件 将</description>
      <content:encoded><![CDATA[<p>pdftk  是我们处理 PDF 的命令行工具，可以在客户端脚本或服务器端对 PDF 文档进行处理。</p>
<p><img src="/api/images/image/2026/07/dbd5e552be5f17b0-image.png" alt="image.png"></p>
<h2>1、PDFtk 功能</h2>
<p>PDFtk 可以有如下等功能：</p>
<ul>
<li>合并 PDF 文档或整理 PDF 页面扫描件</li>
<li>将 PDF 页面拆分为新文档</li>
<li>旋转 PDF 文档或页面</li>
<li>根据需要加密输出</li>
<li>应用背景水印或前景图章</li>
<li>解压 PDF 附件</li>
<li>将 PDF 文档拆分为单页</li>
</ul>
<h2>2、PDFtk 安装</h2>
<p>Linux上安装：</p>
<p><code>$ sudo apt-get install pdftk</code></p>
<p>Mac上安装：因为它对Homebrew支持还不算特别好，需要这样指定文件位置来安装：</p>
<p><code>$ brew install https://raw.githubusercontent.com/turforlag/homebrew-cervezas/master/pdftk.rb</code></p>
<h2>3、PDFtk 使用</h2>
<p>#提取1-15页为一个文件
<code>$ pdftk input.pdf cat 1-15 output new.pdf</code></p>
<p>#提取第1至3，第5，第6至10页，并合并为一个pdf文件
<code>$ pdftk input.pdf cat 1-3 5 6-10 output combined.pdf</code></p>
<p>#合并(concatenate) 前面所有的pdf为output.pdf
<code>$ pdftk file1.pdf file2.pdf ... cat output new.pdf</code></p>
<p>#拆分PDF的每一页为一个新文件 并按照指定格式设定文件名
<code>$ pdftk input.pdf burst output new\_%d.pdf</code></p>
<p>#按照通配符，合并大量PDF文件
<code>$ pdftk \*.pdf cat output combined.pdf</code></p>
<p>#去除第 13 页,其余的保存为新PDF
<code>$ pdftk in.pdf cat 1-12 14-end output out1.pdf</code></p>
<p>#扫描一本书，odd.pdf 为书的全部奇数页，even.pdf 为书的全部偶数页，下面的命令可以将两个 pdf 合并成页码正常的书
<code>$ pdftk A=odd.pdf B=even.pdf shuffle A B output collated.pdf</code></p>
<p>#按180°旋转所有页面
<code>$ pdftk input.pdf cat 1-endsouth output output.pdf</code></p>
<p>#按顺时针90°旋转第三页，其他页不变
<code>$ pdftk input.pdf cat 1-2 3east 4-end output output.pdf</code></p>
<p>#输入密码转换成无密码PDF
<code>pdftk secured.pdf input\_pw foopass output unsecured.pdf</code></p>
<p>更多的功能选项，可以参考<a href="/301e3f3674f981199081f2d71fba6b31?pvs=25">官方文档</a>。</p>
<h2>4、mac m芯片安装使用</h2>
<p>系统：macOS Monterey 12.1</p>
<p>芯片：m1 pro</p>
<p>需要安装m1版本的homebrew，然后在终端使用<code>brew install pdftk-java</code>，安装成功可以使用。</p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>scrcpy：安卓手机投屏到 mac、windows 神器</title>
      <link>https://codexun.com/posts/scrcpy-screen-copy-to-macos</link>
      <guid isPermaLink="true">https://codexun.com/posts/scrcpy-screen-copy-to-macos</guid>
      <description>Scrcpy是“screen copy“的缩写，是Github上的一款开源项目，可以通过有线链接或者无线连接来将安卓屏幕投射到Mac上。并允许使用电脑的键盘和鼠标进行控制。它无需设备具备 root 权限，也无需在设备上安装应用。支持 Linux、Windows 和 macOS 系统。 scrcpy github：***</description>
      <content:encoded><![CDATA[<p>Scrcpy是“<strong>screen copy</strong>“的缩写，是Github上的一款开源项目，可以通过有线链接或者无线连接来将安卓屏幕投射到Mac上。并允许使用电脑的键盘和鼠标进行控制。它无需设备具备 root 权限，也无需在设备上安装应用。支持 Linux、Windows 和 macOS 系统。</p>
<p><strong>scrcpy github：****<a href="https://github.com/Genymobile/scrcpy">https://github.com/Genymobile/scrcpy</a></strong></p>
<p><img src="/api/images/image/2026/07/15bf3f7abfc73967-image.png" alt="image.png"></p>
<h2><strong>1、scrcpy 介绍</strong></h2>
<p>1、开源、免费，支持 macOS、Windows 和 Linux。</p>
<p>2、轻量化，不需要中手机中安装任何软件，只要打开开发者模式和 USB 调试即可。</p>
<p>3、低延迟，不卡顿，无广告，可以不联网。</p>
<p>4、支持声音同步。</p>
<p>5、支持录屏，自定格录制文件格式。</p>
<p>6、支持电脑鼠标键盘操作安卓手机。</p>
<p>7、没有图形化界面，需要命令行操作，不过有很多的自定义命令选项。</p>
<h2><strong>2、scrcpy 安装-以 mac 为例</strong></h2>
<p>1、Mac安装homebrew。</p>
<p><code>$ /bin/bash -c &quot;``**$(**``curl -fsSL https://gitee.com/ineo6/homebrew-install/raw/master/install.sh``**)**``&quot;</code></p>
<p>2、Mac安装scrcpy。</p>
<p><code>$ brew install scrcpy</code></p>
<p>3、Mac安装adb。</p>
<p><code>$ brew install android-platform-tools</code></p>
<h2><strong>3、scrcpy 使用</strong></h2>
<p>1、安卓手机打开开发者选项和 USB 调试</p>
<p>在使用前，有几个基本的操作，第一是打开开发者选项，打开方式一般是在“关于本机”中找到“版本信息”，然后点击“版本号”7次，即可打开开发者选项；（各个系统略有不同）
以小米MIUI12为例：设置--&gt;我的设备--&gt;全部参数--&gt;MIUI版本--&gt;连点7下</p>
<p>第二是在开发者选项中启用 USB 调试，一般默认是启用的，可以确认一下。</p>
<p>2、连接手机投屏</p>
<p>通过 USB 连接，并且只有一台手机的时候：</p>
<p><code>$ scrcpy --select-usb</code></p>
<p>通过 WiFi 连接，并且只有一台手机的时候：</p>
<p><code>$ scrcpy --select-tcpip</code></p>
<p>默认 USB 连接的时候，可以直接输入 scrcpy开始投屏：
<code>$ scrcpy</code></p>
<h2><strong>4、scrcpy 高级使用</strong></h2>
<p>1、设置投屏分辨率分辨率设置稍微小一点，可以提高性能，使投屏更加流畅。</p>
<p><code>scrcpy -m 1024</code>
2、设置比特率</p>
<p>比特率设置高一点，视频会更加流畅一点，默认的 8Mbps。</p>
<p><code>scrcpy -b 20M</code>
3、设置帧率</p>
<p><code>scrcpy --max-fps=15</code>
4、设置编码格式</p>
<p>编码格式有三种，分别是 h264（默认格式）、h265 和 av1，h265 可以提高视频质量，h264 可以提供更低的延迟，av1 格式目前在手机上还不是很普遍。</p>
<p><code>scrcpy --video-codec=h264</code>
5、只投屏声音，不投图像</p>
<p><code>scrcpy --no-video</code>
6、只投图像，不投声音</p>
<p><code>scrcpy --no-audio</code>
7、获取麦克风的声音</p>
<p>默认情况下，投屏的是手机输出的声音，我们要可以投屏获取麦克风的声音。</p>
<p><code>scrcpy --audio-source=mic</code>
8、录制屏幕</p>
<p><code>scrcpy -r record-file.mp4</code>
9、投屏窗口置顶</p>
<p><code>scrcpy --always-on-top</code>
10、投屏时关闭手机屏幕</p>
<p><code>scrcpy --turn-screen-off</code></p>
<h2>5、小米手机上的问题</h2>
<p>在某些设备（尤其是小米设备）上，可能会遇到以下错误，导致鼠标和键盘无法操作手机：</p>
<pre><code class="language-yaml">java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
</code></pre>
<p>在这种情况下，你需要启用 额外选项 USB 调试（安全设置）（此项与普通的 USB 调试 不同）以使用键盘和鼠标控制手机。</p>
<p><strong>注意：启用此选项后需重启手机。</strong></p>
<p><img src="/api/images/image/2026/07/3e90d740764e8b0a-image.png" alt="image.png"></p>]]></content:encoded>
      <category>工具</category>
      <pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Notion：配合使用 Next.js 搭建低成本博客</title>
      <link>https://codexun.com/posts/build-blog-with-nextjs-and-notion-cms</link>
      <guid isPermaLink="true">https://codexun.com/posts/build-blog-with-nextjs-and-notion-cms</guid>
      <description>之前搭建过不少类型的博客，但是总有不少的麻烦之处。 比如 hugo、hexo 搭建的静态博客，需要提交新的 markdown 内容到项目仓库，然后将 build 的内容提交才能更新博客；内容中的图片存储可以使用一些第三方的图片存储服务，不过处理起来也稍显麻烦。 而类似 wordpress 的一些博客方案，需要有自己的服</description>
      <content:encoded><![CDATA[<p>之前搭建过不少类型的博客，但是总有不少的麻烦之处。</p>
<p>比如 hugo、hexo 搭建的静态博客，需要提交新的 markdown 内容到项目仓库，然后将 build 的内容提交才能更新博客；内容中的图片存储可以使用一些第三方的图片存储服务，不过处理起来也稍显麻烦。</p>
<p>而类似 wordpress 的一些博客方案，需要有自己的服务器，要求有一定的运维基础，还需要有配置主题和各种插件进行优化的经验。</p>
<p>总而言之，上面的方案还是稍显麻烦，不想有太多的服务端运维，在博客内容撰写上也越简便越好。</p>
<h2>Notion内容 CMS 博客优点</h2>
<p>偶然发现了 notion 作为内容 cms 的这种博客方案：</p>
<p>1、比如可以选择 nextjs 框架的博客主题，可以使用 Netlify、vercel 这类平台部署项目。</p>
<p>2、notion 上更新内容，提交后博客即可以更新；notion 上图片也有 cdn 的优化，不需要再使用第三方的图片存储服务。</p>
<p>博客代码即可以自己进行源代码上优化，还可以<a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">结合 GitHub actions 进行定时部署</a>，内容管理也比较纯粹和方便，比较符合我当前的诉求。</p>
<h2><strong>博客系统Nobelium介绍</strong></h2>
<p>首先选择了 Notion 作为内容生产后，就需要找一个基于 Notion 的博客系统，将内容渲染成站点，这里选择 <a href="https://github.com/craigary/nobelium">nobelium</a> 这种举例。</p>
<p><img src="/api/images/image/2026/07/9fb119a870e2a266-image.png" alt="image.png"></p>
<p>**Nobelium介绍，**一个基于 Notion 和 Next.js 构建的静态博客，可以部署到 Netlify、vercel 这类平台。</p>
<p><strong>🚀 快速且响应式</strong></p>
<ul>
<li>页面渲染速度快，设计响应式</li>
<li>使用高效编译器实现快速静态生成</li>
</ul>
<p><strong>🤖 即时部署</strong></p>
<ul>
<li>在 Vercel 上几分钟内完成部署</li>
<li>支持增量再生，<strong>更新 Notion 内容后无需重新部署</strong></li>
</ul>
<p><strong>🚙 功能齐全</strong></p>
<ul>
<li>支持评论、全宽页面、快速搜索和标签过滤</li>
<li>提供 RSS、分析、Web 性能指标等更多功能</li>
</ul>
<p><strong>🎨 易于定制</strong></p>
<ul>
<li>丰富的配置选项，支持英文和中文界面</li>
<li>使用 Tailwind CSS 构建，便于个性化定制</li>
</ul>
<p><strong>🕸 美观的 URL 和 SEO 友好</strong></p>
<h2>博客搭建步骤</h2>
<h3>1、Notion 配置</h3>
<p>打开 <a href="https://github.com/craigary/nobelium">Nobelium</a> 仓库，复制 <a href="https://craigary.notion.site/ee99f65a23ab44f8ac80270122ee8138">这个 Notion 模板</a>，并将其设置为公开分享。</p>
<p><img src="/api/images/image/2026/07/4b4471d4c38b447a-image.png" alt="image.png"></p>
<p>获取 Notion 上 NOTION_PAGE_ID 和 NOTION_ACCESS_TOKEN 这两个值，后续部署的时候需要。</p>
<p><strong>NOTION_PAGE_ID：</strong></p>
<p><img src="/api/images/image/2026/07/753faaa212618617-image.png" alt="image.png"></p>
<p>** NOTION_ACCESS_TOKEN：**</p>
<p>在 Notion 设置**“链接”→ “开发或者管理集成”**中，创建新的集成，获取内部集成密钥。</p>
<p><img src="/api/images/image/2026/07/6ceb7a99f31a5302-image.png" alt="image.png"></p>
<h3>2、部署</h3>
<p>在 github 中<a href="https://github.com/craigary/nobelium/fork">Fork</a> 这个项目，可以进行根据自己博客名字等修改相应配置：</p>
<ul>
<li>自定义 <code>blog.config.js</code></li>
<li>将 <code>/public</code> 文件夹中的 <code>favicon.svg</code> 和 <code>favicon.ico</code> 替换为你自己的图标</li>
</ul>
<p>在 Netlify 或者 Vercel 平台上进行部署，设置环境变量</p>
<ul>
<li><code>NOTION\_PAGE\_ID</code>： 你之前公开分享的 Notion 页面 ID，通常在你的工作区地址后有 32 位数字</li>
<li><code>NOTION\_ACCESS\_TOKEN</code> : 之前创建的 notion access token。</li>
</ul>
<h3>3、完成部署</h3>
<p>随后可以在相应配置里，配置你的域名等等。</p>
<p>在项目代码中<code>blog.config.js</code>中，配置博客 TDK 等，以及 google analytics 追踪代码，gitalk, utterances, cusdis 等评论配置等等。。</p>
<p>虽然在 Notion 发布内容之后实时更新，但是为了优化 SEO 和网站速度，内容更新之后最好还是重新 build 部署。然后就有了需要定时触发 Netlify 部署本博客站点这样的需求。可以参见之前这一篇文章，有详细**<a href="/posts/automating-netlify-deployment-every-24-hours-with-github-actions">通过 Github Actions自动执行 Netlify 部署</a>****。**</p>
<h2>参考资料</h2>
<p><a href="https://www.rustc.cloud/blog-dev">✨ 信鑫 Blog_使用 Vercel + Notion 搭建低成本博客</a>[Remix 版本]</p>
<p><a href="https://github.com/craigary/nobelium">✨github：craigary/nobelium</a></p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Mon, 10 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>使用 Github Action自动执行 Netlify 部署</title>
      <link>https://codexun.com/posts/automating-netlify-deployment-every-24-hours-with-github-actions</link>
      <guid isPermaLink="true">https://codexun.com/posts/automating-netlify-deployment-every-24-hours-with-github-actions</guid>
      <description>GitHub Actions 在众多使用场景中都非常实用，例如在特定操作（如推送到某个分支）时触发 CI/CD 构建。还可以扩展为在特定时间运行定时任务（cron job）。 在本文中，我们将探讨如何利用 GitHub Actions 在指定时间触发 Netlify 的构建任务。 本博客是部署在 Netlify 上，技</description>
      <content:encoded><![CDATA[<p>GitHub Actions 在众多使用场景中都非常实用，例如在特定操作（如推送到某个分支）时触发 CI/CD 构建。还可以扩展为在特定时间运行定时任务（cron job）。</p>
<p>在本文中，我们将探讨如何利用 GitHub Actions 在指定时间触发 Netlify 的构建任务。</p>
<p><img src="/api/images/image/2026/07/be5c02e74b44a72f-image.png" alt="image.png"></p>
<p>本博客是部署在 Netlify 上，技术栈是Nextjs + Notion，虽然可以做到在 Notion 发布内容之后实时更新，但是为了优化 SEO 和网站速度，内容更新之后最好还是重新 build 部署。</p>
<p>然后就有了这样的一个需求，需要定时触发 Netlify 部署本博客站点，时间可以根据自己内容的更新频率和实际需要来定，本文以<strong>每天 0 点</strong>触发 Netlify 的编译部署为例。</p>
<p>要实现这种方法，你需要拥有一个 GitHub 账户，并且网站已托管在 Netlify 上，同时启用了持续部署功能。</p>
<h2>1、什么是 Netlify Build Hooks？</h2>
<p>在 Netlify 中，Build Hooks 是一种 Webhook URL，可以用来触发新的构建和部署。你可以点击<a href="https://docs.netlify.com/configure-builds/build-hooks/">这里</a>了解更多详情。</p>
<p>你可以在 Netlify 的项目“设置 &gt; 构建与部署 &gt; 持续部署 &gt; Build Hooks”中找到 Build Hooks。</p>
<p><img src="/api/images/image/2026/07/9e0f057ea6f8d6ab-image.png" alt="image.png"></p>
<h2>2、配置创建 Netlify Build Hooks</h2>
<p>从上一步中找到添加 Build Hook 的设置，点击“添加 Build Hook”按钮，填写名称并选择对应的 GitHub 分支。</p>
<p><img src="/api/images/image/2026/07/a2d8d46ec05e48ad-image.png" alt="image.png"></p>
<p>注意这种方法要求你的 GitHub 仓库已连接到 Netlify，并且启用了持续部署功能。</p>
<p>创建完成后，复制提供的 URL，在后续设置中会用到。</p>
<p><img src="/api/images/image/2026/07/0ef5767a3b95fdd8-image.png" alt="image.png"></p>
<h2>3、创建 GitHub Actions <strong>secrets and variables</strong></h2>
<p>在Github 的项目Setting下设置 github actions 所需要的变量。</p>
<p>例如，创建NETLIFY_BUILD_HOOK变量，value 为上一步获得的 Netlify build hook 地址。</p>
<p><img src="/api/images/image/2026/07/ca303b1efdb6c498-image.png" alt="image.png"></p>
<h2>4、实现 GitHub Actions</h2>
<p>最后创建 Github Actions 相关代码。</p>
<p>在你的Github项目根目录下创建一个名为 .github 的文件夹，在其中再创建一个名为 workflows 的子文件夹，然后在 workflows 文件夹中创建一个 main.yml 文件，并粘贴以下代码。</p>
<pre><code class="language-yaml"># .github/workflows/main.yml

name: Trigger Netlify Build
on:
  schedule:
    - '0 0 * * *' # Run at 0000 daily
  workflow_dispatch:  # 手动触发
jobs:
  build:
    name: Request Netlify Webhook
    runs-on: ubuntu-latest
    steps:
      - name: Curl request
        env:
          NETLIFY_BUILD_HOOK: ${{ secrets.NETLIFY_BUILD_HOOK }}
        run: curl -X POST -d {} $NETLIFY_BUILD_HOOK
</code></pre>
<p><strong>最终的文件路径应为：.github/workflows/main.yml</strong></p>
<p>完成上述操作后，将代码推送到仓库。</p>
<p>最终，Workflow 将在指定时间自动执行。上面示例代码中设置的为 UTC 时间每天的 0 点。</p>
<p>如果需要设置 cron 时间，可以参考 <a href="https://crontab.guru/">https://crontab.guru/</a> 获取帮助。</p>
<h2>参考资料</h2>
<p><a href="https://github.com/Jinksi/netlify-build-github-actions">netlify-build-github-actions</a></p>
<p><strong><a href="https://dev.to/nasrulhazim/automating-netlify-deployments-every-24-hours-with-github-actions-2eb9">使用 Site ID和Personal Access Token</a>****[未验证]</strong></p>
<pre><code class="language-yaml">name: Netlify Scheduled Deployment
on:
  schedule:
    - cron: '0 0 * * *' # This runs every 24 hours at midnight UTC
  workflow_dispatch: # Allows manual triggering of the action from GitHub Actions
jobs:
  deploy:
    if: github.ref == 'refs/heads/main' # Specify the branch 'main' to trigger on
    runs-on: ubuntu-latest

    steps:
      - name: Trigger Netlify Build with Cache Clearing
        run: |
          # Call the Netlify API to trigger a new build and clear the cache
          curl -X POST \
          -H &quot;Content-Type: application/json&quot; \
          -H &quot;Authorization: Bearer ${{ secrets.NETLIFY_ACCESS_TOKEN }}&quot; \
          -d '{&quot;clear_cache&quot;: true}' \
          https://api.netlify.com/api/v1/sites/${{ secrets.NETLIFY_SITE_ID }}/builds
</code></pre>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Sat, 08 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>adb命令行导出已安装应用apk文件</title>
      <link>https://codexun.com/posts/console-adb-pull-android-apk</link>
      <guid isPermaLink="true">https://codexun.com/posts/console-adb-pull-android-apk</guid>
      <description>如果已经在Android手机上安装了App应用程序，那么Android系统会保留应用程序的apk安装副本。如果要导出这些apk文件，可以有如下命令行方案。 一、命令列出当前手机上已经安装的apk 先通过 adb shell pm list packages 二、找出要导出的应用包名，命令行找到 apk 的路径 比如要导</description>
      <content:encoded><![CDATA[<p>如果已经在Android手机上安装了App应用程序，那么Android系统会保留应用程序的apk安装副本。如果要导出这些apk文件，可以有如下命令行方案。</p>
<h2><strong>一、命令列出当前手机上已经安装的apk</strong></h2>
<p>先通过</p>
<pre><code class="language-plain">adb shell pm list packages
</code></pre>
<p><img src="/api/images/image/2026/07/bdc49cd5a235212e-image.png" alt="image.png"></p>
<h2><strong>二、找出要导出的应用包名，命令行找到 apk 的路径</strong></h2>
<p>比如要导出华为的相机apk：com.huawei.camera，那就得知道相机apk的存放路径，通过命令：</p>
<pre><code class="language-plain">adb shell pm path com.huawei.camera
</code></pre>
<p>找出来华为相机的存放路径为：/system/priv-app/HwCamera2/HwCamera2.apk</p>
<h2><strong>三、命令行导出 apk</strong></h2>
<p>然后使用命令: adb pull ，将其导出保存到自己电脑桌面上：</p>
<pre><code class="language-plain">adb pull /system/priv-app/HwCamera2/HwCamera2.apk  C:/Users/fly/Desktop
</code></pre>
<p><img src="/api/images/image/2026/07/deb352e292c5b291-fe11efca080f4a40b949cbd7f5c0528d.png" alt="image"></p>
<p>最终导出成功。</p>]]></content:encoded>
      <category>安卓</category>
      <pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Kokoro 开源TTS引擎上线！多语言支持,无需联网，浏览器内极速运行</title>
      <link>https://codexun.com/posts/tts-open-source-kokoro-sample</link>
      <guid isPermaLink="true">https://codexun.com/posts/tts-open-source-kokoro-sample</guid>
      <description>Kokoro 是一款轻量级的开源文本转语音（TTS）引擎，凭借其高效能和轻量化设计，迅速在技术社区中引起关注。 本文将详细介绍 Kokoro 的主要特点，并提供在浏览器和 Python 环境中的代码示例，帮助您快速上手。 1. Kokoro：可在浏览器中运行的 TTS 引擎 1.1 简介 Kokoro 是一个拥有 82</description>
      <content:encoded><![CDATA[<p>Kokoro 是一款轻量级的开源文本转语音（TTS）引擎，凭借其高效能和轻量化设计，迅速在技术社区中引起关注。</p>
<p>本文将详细介绍 Kokoro 的主要特点，并提供在浏览器和 Python 环境中的代码示例，帮助您快速上手。</p>
<h2><strong>1. Kokoro：可在浏览器中运行的 TTS 引擎</strong></h2>
<h3><strong>1.1 简介</strong></h3>
<p>Kokoro 是一个拥有 8200 万参数的开源 TTS 模型。尽管其架构轻量，但在语音合成质量上可媲美更大型的模型，同时在速度和成本上更具优势。Kokoro 采用 Apache 2.0 许可证，允许在各种环境中自由部署，无论是生产环境还是个人项目。</p>
<p><img src="/api/images/image/2026/07/94919a37d506e720-image.png" alt="image.png"></p>
<h3><strong>1.2 主要特点</strong></h3>
<ul>
<li>多语言支持：Kokoro 支持英语、西班牙语、法语、意大利语、日语和中文普通话，满足不同语言用户的需求。</li>
<li>完全离线运行：Kokoro 可在浏览器中通过 WebGPU 技术实现完全离线、100% 本地化的语音合成，确保数据隐私和安全。</li>
<li>高效性能：得益于轻量级架构，Kokoro 在保持高质量语音输出的同时，实现了快速的推理速度。</li>
<li>开源许可：采用 Apache 2.0 许可证，用户可以自由地在商业和个人项目中使用 Kokoro。</li>
</ul>
<h2><strong>2. 代码示例</strong></h2>
<h3><strong>2.1 浏览器运行的样本代码</strong></h3>
<p>要在浏览器中使用 Kokoro，首先需要安装 <code>kokoro-js</code> 库。</p>
<pre><code class="language-shell">npm install kokoro-js
</code></pre>
<p>然后，可以使用以下代码生成语音：</p>
<pre><code class="language-javascript">import { KokoroTTS } from&quot;kokoro-js&quot;;

const model_id = &quot;onnx-community/Kokoro-82M-v1.0-ONNX&quot;;
const tts = await KokoroTTS.from_pretrained(model_id, {
dtype: &quot;q8&quot;, // 可选值: &quot;fp32&quot;, &quot;fp16&quot;, &quot;q8&quot;, &quot;q4&quot;, &quot;q4f16&quot;
device: &quot;wasm&quot;, // 可选值: &quot;wasm&quot;, &quot;webgpu&quot; (web) 或 &quot;cpu&quot; (node)。如果使用 &quot;webgpu&quot;，建议使用 dtype=&quot;fp32&quot;。
});

const text = &quot;生活就像一盒巧克力，你永远不知道你会得到什么。&quot;;
const audio = await tts.generate(text, {
// 使用 `tts.list_voices()` 列出所有可用的声音
voice: &quot;af_heart&quot;,
});
audio.save(&quot;audio.wav&quot;);
</code></pre>
<p>上述代码在浏览器环境中运行，利用 WebGPU 技术实现高效的语音合成。</p>
<h3><strong>2.2 Python 代码</strong></h3>
<p>在 Python 环境中，可以使用 <code>kokoro</code> 库进行语音合成。</p>
<pre><code class="language-python"># 安装 kokoro 和 soundfile
!pip install kokoro&gt;=0.7.11 soundfile
# 安装 espeak-ng，用于英语 OOD 回退和某些非英语语言
!apt-get -qq -y install espeak-ng &gt; /dev/null 2&gt;&amp;1

from kokoro import KPipeline
from IPython.display import display, Audio
import soundfile as sf

# 初始化管道
pipeline = KPipeline(lang_code='a') # 'a' 表示美式英语

text = '''
天空的颜色像电视机调到没有信号的频道。
&quot;这不像我在使用，&quot;凯斯听到有人说，他挤过 Chat 门口的拥挤人群。&quot;这就像我的身体产生了巨大的药物缺乏症。&quot;
那是一个大都市的声音和笑话。Chatsubo 是一个为专业外籍人士开的酒吧；你可以在那喝上一周，却听不到两句日语。
'''

# 生成并保存音频
generator = pipeline(
    text, voice='af_heart', # 更改声音
    speed=1, split_pattern=r'\n+'
)
for i, (gs, ps, audio) in enumerate(generator):
    print(i)  # 索引
    print(gs) # 字符
    print(ps) # 音素
    display(Audio(data=audio, rate=24000, autoplay=i==0))
    sf.write(f'{i}.wav', audio, 24000) # 保存每个音频文件
</code></pre>
<p>上述代码在 Python 环境中运行，利用 <code>kokoro</code> 库实现文本到语音的转换。</p>
<h3><strong>2.3 浏览器中直接体验</strong></h3>
<p>您可以直接在浏览器中体验 Kokoro TTS，无需任何安装。请访问以下链接：</p>
<p><a href="https://huggingface.co/spaces/webml-community/kokoro-webgpu">https://huggingface.co/spaces/webml-community/kokoro-webgpu</a></p>
<p>请注意，该应用程序约有 300 多兆字节，需要完全加载完成后才能体验。然而，作为一个完全在浏览器中运行的高效 TTS 模型，这样的等待是值得的。</p>
<h2><strong>3. 总结</strong></h2>
<p>Kokoro TTS 以其轻量级架构、多语言支持和高效性能，为开发者和用户提供了一个强大的文本转语音解决方案。无论是在浏览器还是在 Python 环境中，Kokoro 都能实现高质量的语音合成。其开源许可和完全离线运行的特点，使其在数据隐私和安全性方面具有显著优势。</p>
<h2>**4. **参考资料</h2>
<ul>
<li><a href="https://github.com/hexgrad/kokoro/tree/main">https://github.com/hexgrad/kokoro/tree/main</a></li>
<li><a href="https://www.npmjs.com/package/kokoro-js">https://www.npmjs.com/package/kokoro-js</a></li>
<li><a href="https://huggingface.co/spaces/webml-community/kokoro-webgpu">https://huggingface.co/spaces/webml-community/kokoro-webgpu</a></li>
</ul>
<p>通过以上资源，您可以深入了解 Kokoro TTS，并将其应用于您的项目中，体验高效的文本转语音技术。</p>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>添加Google Analytics到Astro 网站</title>
      <link>https://codexun.com/posts/astro-blog-add-google-analytics</link>
      <guid isPermaLink="true">https://codexun.com/posts/astro-blog-add-google-analytics</guid>
      <description>1、设置 Google Analytics 访问**Google Analytics**帐户，为自己的网站创建Google Analytics追踪代码。 最终会得到类似如下的代码 &lt;!-- Google tag (gtag.js) --&gt; &lt;script async src=&quot;https://www.googletag</description>
      <content:encoded><![CDATA[<h2><strong>1、设置 Google Analytics</strong></h2>
<p>访问**<a href="https://analytics.google.com/">Google Analytics</a>**帐户，为自己的网站创建Google Analytics追踪代码。</p>
<p>最终会得到类似如下的代码</p>
<pre><code class="language-typescript">&lt;!-- Google tag (gtag.js) --&gt;
&lt;script async src=&quot;https://www.googletagmanager.com/gtag/js?id=MEASUREMENT_ID&quot;&gt;&lt;/script&gt;
&lt;script&gt;
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'MEASUREMENT_ID');
&lt;/script&gt;
</code></pre>
<blockquote>
<p>注意：如果你复制上述代码，请确保将这两个MEASUREMENT_ID值替换为你的测量 ID 的实际值。</p>
</blockquote>
<h2><strong>2、直接集成 GA 代码到Astro</strong></h2>
<p>一般将上面的代码添加到网站通用的 标签内即可。具体代码位置看下使用的 Astro 模板，比较可能的一个代码文件在 Layout.astro。</p>
<p>此外放入中的这段追踪代码，还需要添加。</p>
<p>最终的实现将如下：</p>
<pre><code class="language-typescript">&lt;!-- Google tag (gtag.js) --&gt;
&lt;script async src=&quot;https://www.googletagmanager.com/gtag/js?id=MEASUREMENT_ID&quot;&gt;&lt;/script&gt;
&lt;script is:inline&gt;
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'MEASUREMENT_ID');
&lt;/script&gt;
</code></pre>
<h2><strong>3、借助 Partytown 第三方库</strong></h2>
<p>直接集成 GA 代码可能会遇到一些性能问题，另一个方法是借助Partytown 的库。</p>
<p>Partytown 允许集成第三方代码实现同样的功能，而不会降低网站的性能。可以参考在**<a href="https://partytown.builder.io/how-does-partytown-work">此处</a>**的文档了解更多相关信息。</p>
<h3>3.1 步骤1 - 安装 Partytown 依赖</h3>
<p>在 Astro 项目终端运行命令来安装 Partytown：</p>
<pre><code class="language-shell">npm install @astrojs/partytown
</code></pre>
<h3>3.2 步骤2 - 配置Partytown</h3>
<p>安装完成后，将Partytown集成添加到你的astro.config.mjs文件中：</p>
<pre><code class="language-typescript">import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import partytown from '@astrojs/partytown'
import sitemap from '@astrojs/sitemap';

// https://astro.build/config
export default defineConfig({
    site: 'https://astro-article.netlify.app/',
    integrations: [
        mdx(), 
        sitemap(),
        partytown({
            config: {
              forward: [&quot;dataLayer.push&quot;],
            },
        }),
    ],
});
</code></pre>
<p>在上面的代码中，我们导入了 Partytown 库：<code>**import partytown from '@astrojs/partytown'**</code>。 然后我们将这段代码添加到integrations对象中。</p>
<h3>3.3 步骤3 - 添加 GA 代码</h3>
<p>将 Google Analytics代码添加到页面</p>
<pre><code class="language-typescript">&lt;!-- Google tag (gtag.js) --&gt;
&lt;script type=&quot;text/partytown&quot; async src=&quot;https://www.googletagmanager.com/gtag/js?id=MEASUREMENT_ID&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/partytown&quot;&gt;
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'MEASUREMENT_ID');
&lt;/script&gt;
</code></pre>
<p>同样上面的代码添加到网站通用的 标签内。具体代码位置看下使用的 Astro 模板，比较可能的一个代码文件在 Layout.astro。</p>
<p>注意：</p>
<blockquote>
<p>我们type=“text/partytown”向两个</p>
</blockquote>
<h3>3.4 步骤4 - 构建部署</h3>
<p>构建并部署你的项目</p>
<p>比如使用npm run build其构建到dist文件夹中，然后将代码推送到 GitHub。这会自动触发 Netlify 部署。</p>
<p>网站部署完成后，需要等待一段时间才能在 Google Analytics 信息中心中查看分析结果。</p>]]></content:encoded>
      <category>前端</category>
      <pubDate>Wed, 23 Oct 2024 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>前端跨域问题梳理</title>
      <link>https://codexun.com/posts/front-end-cross-domain</link>
      <guid isPermaLink="true">https://codexun.com/posts/front-end-cross-domain</guid>
      <description>1、浏览器的同源策略 1.1 同源策略概述**#** 同源策略是浏览器为确保资源安全，而遵循的一种策略，该策略对访问资源进行了一些限制。 W3C 上对同源策略的说明：Same origin policy。 1.2 什么是源（origin）？****# 源的组成部分 下面表格中，只有最后一行的两个源是同源。 源 1 源 </description>
      <content:encoded><![CDATA[<h2><strong>1、浏览器的同源策略</strong></h2>
<h3><strong>1.1 同源策略概述</strong>**<a href="/posts/front-end-cross-domain/#11-%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5%E6%A6%82%E8%BF%B0">#</a>**</h3>
<p>同源策略是浏览器为确保资源安全，而遵循的一种策略，该策略对访问资源进行了一些限制。</p>
<blockquote>
<p>W3C 上对同源策略的说明：Same origin policy。</p>
</blockquote>
<h3><strong>1.2 什么是源（origin）？****<a href="/posts/front-end-cross-domain/#12-%E4%BB%80%E4%B9%88%E6%98%AF%E6%BA%90origin">#</a></strong></h3>
<ol>
<li>源的组成部分</li>
</ol>
<p><img src="/api/images/image/2026/07/f73bed1eeb221dbb-image.png" alt="image.png"></p>
<ol>
<li>下面表格中，只有最后一行的两个源是同源。</li>
</ol>
<table>
<thead>
<tr>
<th><strong>源 1</strong></th>
<th><strong>源 2</strong></th>
<th><strong>是否同源</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><a href="http://www.xyz.com/home">http://www.xyz.com/home</a></strong></td>
<td><strong><a href="https://www.xyz.com/home">https://www.xyz.com/home</a></strong></td>
<td>⛔非同源️</td>
</tr>
<tr>
<td><strong><a href="http://www.xyz.com/home">http://www.xyz.com/home</a></strong></td>
<td><strong><a href="http://mail.xyz.com/home">http://mail.xyz.com/home</a></strong></td>
<td>⛔非同源</td>
</tr>
<tr>
<td><strong><a href="http://www.xyz.com:8080/home">http://www.xyz.com:8080/home</a></strong></td>
<td><strong><a href="http://www.xyz.com:8090/home">http://www.xyz.com:8090/home</a></strong></td>
<td>⛔非同源</td>
</tr>
<tr>
<td><strong><a href="http://www.xyz.com:8080/home">http://www.xyz.com:8080/home</a></strong></td>
<td><strong><a href="http://www.xyz.com:8080/search">http://www.xyz.com:8080/search</a></strong></td>
<td>✅同源︎</td>
</tr>
</tbody>
</table>
<ol>
<li>同源请求和非同源请求</li>
</ol>
<p><img src="/api/images/image/2026/07/6b96865033c96c2e-image.png" alt="image.png"></p>
<ol>
<li>总结:『所处源』与『目标源』不一致，就是『非同源』，又称『异源』或『跨域』。</li>
</ol>
<h2><strong>2、跨域会受到哪些限制</strong></h2>
<p>例如有两个源：『源A』和『源B』，它们是『非同源』的，那么浏览器会有如下限制：</p>
<h3><strong>2.1 限制DOM访问</strong>**<a href="/posts/front-end-cross-domain/#21-%E9%99%90%E5%88%B6dom%E8%AE%BF%E9%97%AE">#</a>**</h3>
<p>『源A』的脚本不能访问『源B』的 DOM。</p>
<pre><code class="language-typescript">&lt;!-- &lt;iframe id=&quot;framePage&quot; src=&quot;./demo.html&quot;&gt;&lt;/iframe&gt; --&gt;
&lt;iframe id=&quot;framePage&quot; src=&quot;https://www.baidu.com&quot;&gt;&lt;/iframe&gt;

&lt;script type=&quot;text/javascript&quot; &gt;
  function showDOM(){
    const framePage = document.getElementById('framePage')
    console.log(framePage.contentWindow.document) //同源的可以获取，非同源的无法获取
  }
&lt;/script&gt;
</code></pre>
<h3><strong>2.2 限制Cookie访问</strong>**<a href="/posts/front-end-cross-domain/#22-%E9%99%90%E5%88%B6cookie%E8%AE%BF%E9%97%AE">#</a>**</h3>
<p>『源A』不能访问『源B』的 cookie。</p>
<pre><code class="language-typescript">&lt;iframe id=&quot;baidu&quot; src=&quot;http://www.baidu.com&quot; width=&quot;500&quot; height=&quot;300&quot;&gt;&lt;/iframe&gt;

&lt;script type=&quot;text/javascript&quot; &gt;
  // 访问的是当前源的cookie，并不是baidu的cookie
  console.log(document.cookie)
&lt;/script&gt;
</code></pre>
<h3><strong>2.3 限制Ajax获取数据</strong>**<a href="/posts/front-end-cross-domain/#23-%E9%99%90%E5%88%B6ajax%E8%8E%B7%E5%8F%96%E6%95%B0%E6%8D%AE">#</a>**</h3>
<p>『源A』可以给『源B』发请求，但是无法获取『源B』响应的数据。</p>
<pre><code class="language-typescript">const url = 'https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc'
let result = await fetch(url)
let data = await result.json();
console.log(data)
</code></pre>
<blockquote>
<p>备注：在上述限制中，浏览器对 Ajax 获取数据的限制是影响最大的一个，且实际开发中经常遇到。</p>
</blockquote>
<h2><strong>3、几个注意点</strong></h2>
<ol>
<li>跨域限制仅存在浏览器端，服务端不存在跨域限制。</li>
<li>即使跨域了，Ajax 请求也可以正常发出，但响应数据不会交给开发者。</li>
</ol>
<p><img src="/api/images/image/2026/07/390cbe7f5c669410-image.png" alt="image.png"></p>
<ol>
<li><code>**&lt;link&gt;**</code>、<code>**&lt;script&gt;**</code>、<code>**&lt;img&gt;**</code>… 这些标签发出的请求也可能跨域，只不过浏览器对标签跨域不做严格限制，对开发几乎无影响。</li>
</ol>
<h2><strong>4、CORS 解决 Ajax 跨域问题</strong></h2>
<h3><strong>4.1 CORS 概述</strong>**<a href="/posts/front-end-cross-domain/#41-cors-%E6%A6%82%E8%BF%B0">#</a>**</h3>
<p>CORS 全称：Cross-Origin Resource Sharing（跨域资源共享），是用于控制浏览器校验跨域请求的一套规范，服务器依照 CORS 规范，添加特定响应头来控制浏览器校验，大致规则如下：</p>
<ul>
<li>服务器明确表示拒绝跨域请求，或没有表示，则浏览器校验不通过。</li>
<li>服务器明确表示允许跨域请求，则浏览器校验通过。</li>
</ul>
<blockquote>
<p>备注说明：使用 CORS 解决跨域是最正统的方式，且要求服务器是“自己人”。</p>
</blockquote>
<h3><strong>4.2 CORS 解决简单请求跨域</strong>**<a href="/posts/front-end-cross-domain/#42-cors-%E8%A7%A3%E5%86%B3%E7%AE%80%E5%8D%95%E8%AF%B7%E6%B1%82%E8%B7%A8%E5%9F%9F">#</a>**</h3>
<p>整体思路：服务器在给出响应时，通过添加Access-Control-Allow-Origin响应头，来明确表达允许某个源发起跨域请求，随后浏览器在校验时，直接通过。</p>
<p><img src="/api/images/image/2026/07/e985a00bba9eb7c1-image.png" alt="image.png"></p>
<p>服务端核心代码（以express框架为例）：</p>
<pre><code class="language-typescript">// 处理跨域中间件
function corsMiddleWare(req,res,next){
   // 允许 http://127.0.0.1:5500 这个源发起跨域请求
   // res.setHeader('Access-Control-Allow-Origin','http://127.0.0.1:5500')
   
   // 允许所有源发起跨域请求
   res.setHeader('Access-Control-Allow-Origin','*')
   next()
}

// 配置路由并使用中间件
app.get('/',corsMiddleWare,(req,res)=&gt;{
   res.send('hello!')
})
</code></pre>
<h3><strong>4.3 简单请求与复杂请求</strong>**<a href="/posts/front-end-cross-domain/#43-%E7%AE%80%E5%8D%95%E8%AF%B7%E6%B1%82%E4%B8%8E%E5%A4%8D%E6%9D%82%E8%AF%B7%E6%B1%82">#</a>**</h3>
<p>CORS 会把请求分为两类，分别是：① 简单请求、② 复杂请求。</p>
<table>
<thead>
<tr>
<th><strong>简单请求</strong></th>
<th><strong>复杂请求</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>✅请求方法（method）为：GET、HEAD、POST</td>
<td></td>
</tr>
<tr>
<td>✅请求头字段要符合《CORS 安全规范》简记：只要不手动修改请求头，一般都能符合该规范。</td>
<td></td>
</tr>
<tr>
<td>✅请求头的Content-Type的值只能是以下三种：● text/plain● multipart/form-data● application/x-www-form-urlencoded</td>
<td>1. 不是简单请求，就是复杂请求。2. 复杂请求会自动发送预检请求。</td>
</tr>
</tbody>
</table>
<p>关于预检请求：</p>
<ol>
<li>发送时机：预检请求在实际跨域请求之前发出，是由浏览器自动发起的。</li>
<li>主要作用：用于向服务器确认是否允许接下来的跨域请求。</li>
<li>基本流程：先发起OPTIONS请求，如果通过预检，继续发起实际的跨域请求。</li>
<li>请求头内容：一个OPTIONS预检请求，通常会包含如下请求头：
| <strong>请求头</strong> | <strong>含义</strong> |
| --- | --- |
| Origin | 发起请求的源 |
| Access-Control-Request-Method | 实际请求的 HTTP 方法 |
| Access-Control-Request-Headers | 实际请求中使用的自定义头（如果有的话） |</li>
</ol>
<h3><strong>4.4 CORS解决复杂请求跨域|****<a href="/posts/front-end-cross-domain/#44-cors%E8%A7%A3%E5%86%B3%E5%A4%8D%E6%9D%82%E8%AF%B7%E6%B1%82%E8%B7%A8%E5%9F%9F">#</a></strong></h3>
<ol>
<li>第一步：服务器先通过浏览器的预检请求，服务器需要返回如下响应头：
| <strong>响应头</strong> | <strong>含义</strong> |
| --- | --- |
| Access-Control-Allow-Origin | 允许的源 |
| Access-Control-Allow-Methods | 允许的方法 |
| Access-Control-Allow-Headers | 允许的自定义头 |
| Access-Control-Max-Age | 预检请求的结果缓存时间（可选） |</li>
</ol>
<p><img src="/api/images/image/2026/07/06f5ae5ed0dbe957-image.png" alt="image.png"></p>
<ol>
<li>第二步：处理实际的跨域请求（与处理简单请求跨域的方式相同）</li>
</ol>
<p><img src="/api/images/image/2026/07/e985a00bba9eb7c1-image.png" alt="image.png"></p>
<pre><code class="language-typescript">// 处理预检请求
app.options('/students', (req, res) =&gt; {
  // 设置允许的跨域请求源
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  // 设置允许的请求方法
  res.setHeader('Access-Control-Allow-Methods', 'GET')
  // 设置允许的请求头
  res.setHeader('Access-Control-Allow-Headers', 'school')
  // 设置预检请求的缓存时间（可选）
  res.setHeader('Access-Control-Max-Age', 7200)
  // 发送响应
  res.send()
})

// 处理实际请求
app.get('/students', (req, res) =&gt; {
  // 设置允许的跨域请求源
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  // 随便设置一个自定义响应头
  res.setHeader('abc',123)
  // 设置允许暴露给客户端的响应头
  res.setHeader('Access-Control-Expose-Headers', 'abc')
  // 打印请求日志
  console.log('有人请求/students了')
  // 发送响应数据
  res.send(students)
})
</code></pre>
<h3><strong>4.5 借助 cors 库快速完成配置</strong>**<a href="/posts/front-end-cross-domain/#45-%E5%80%9F%E5%8A%A9-cors-%E5%BA%93%E5%BF%AB%E9%80%9F%E5%AE%8C%E6%88%90%E9%85%8D%E7%BD%AE">#</a>**</h3>
<blockquote>
<p>上述的配置中需要自己配置响应头，或者需要自己手动封装中间件，借助cors库，可以更方便完成配置</p>
</blockquote>
<p>● 安装cors</p>
<pre><code class="language-typescript">npm i cors
</code></pre>
<p>● 简单配置cors</p>
<pre><code class="language-typescript">app.use(cors())
</code></pre>
<p>● 完整配置cors</p>
<pre><code class="language-typescript">// cors中间件配置
const corsOptions = {
  origin: 'http://127.0.0.1:5500', // 允许的源
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'], // 允许的方法
  allowedHeaders: ['school'], // 允许的自定义头
  exposedHeaders: ['abc'], // 要暴露的响应头
  optionsSuccessStatus: 200 // 预检请求成功的状态码
};

app.use(cors(corsOptions)); // 使用cors中间件
</code></pre>
<h2><strong>5、 JSONP 解决跨域问题</strong></h2>
<ol>
<li>JSONP 概述： JSONP 是利用了</li>
<li>基本流程：
<ul>
<li>第一步：客户端创建一个</li>
<li>第二步：服务端接收到请求后，将数据封装在回调函数中并返回。</li>
<li>第三步：客户端的回调函数被调用，数据以参数的形势传入回调函数。</li>
</ul>
</li>
<li>图示：</li>
</ol>
<p><img src="/api/images/image/2026/07/e10917e7f2b18ab0-image.png" alt="image.png"></p>
<ol>
<li>代码示例：</li>
</ol>
<pre><code class="language-typescript">&lt;button onclick=&quot;getTeachers()&quot;&gt;获取数据&lt;/button&gt;

&lt;script type=&quot;text/javascript&quot; &gt;
  function callback(data){
    console.log(data)
  }

  function getTeachers(url){
    // 创建script元素
    const script = document.createElement('script')
    // 指定script的src属性
    script.src= 'http://127.0.0.1:8081/teachers'
    // 将script元素添加到body中触发脚本加载
    document.body.appendChild(script)
    // script标签加载完毕后移除该标签
    script.onload = ()=&gt;{
      script.remove()
    }
  }
&lt;/script&gt;
</code></pre>
<ol>
<li>jQuery 封装的 jsonp</li>
</ol>
<pre><code class="language-typescript">$.getJSON('http://127.0.0.1:8081/teachers?callback=?',(data)=&gt;{
  console.log(data)
})
</code></pre>
<h2><strong>6、配置代理解决跨域</strong></h2>
<h3><strong>6.1 自己配置代理服务器</strong>**<a href="/posts/front-end-cross-domain/#61-%E8%87%AA%E5%B7%B1%E9%85%8D%E7%BD%AE%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8">#</a>**</h3>
<p>借助http-proxy-middleware配置代理</p>
<pre><code class="language-typescript">const { createProxyMiddleware } = require('http-proxy-middleware');

app.use('/api',createProxyMiddleware({
   target:'https://www.toutiao.com',
   changeOrigin:true,
   pathRewrite:{
      '^/api':''
   }
}))
</code></pre>
<h3><strong>6.2 使用 Nginx 搭建代理服务器</strong>**<a href="/posts/front-end-cross-domain/#62-%E4%BD%BF%E7%94%A8-nginx-%E6%90%AD%E5%BB%BA%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8">#</a>**</h3>
<h3><strong>6.3 借助脚手架搭建服务器</strong>**<a href="/posts/front-end-cross-domain/#63-%E5%80%9F%E5%8A%A9%E8%84%9A%E6%89%8B%E6%9E%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8">#</a>**</h3>]]></content:encoded>
      <category>前端</category>
      <pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>前端项目上线的几种方式</title>
      <link>https://codexun.com/posts/front-end-project-launched</link>
      <guid isPermaLink="true">https://codexun.com/posts/front-end-project-launched</guid>
      <description>前端项目上线可通过打包、部署本地服务器、使用nginx等方式实现，需解决页面刷新404和请求无法发送的问题，建议使用hash模式或配置服务器返回index.html来处理路由问题，并通过http-proxy-middleware设置代理。云服务器部署流程与本地类似。</description>
      <content:encoded><![CDATA[<h2><strong>1、项目打包</strong></h2><ul class="tight" data-tight="true"><li><p>我们开发用的脚手架其实就是一个微型服务器，用于：支撑开发环境、运行代理服务器等。</p></li><li><p>打包完的文件中不存在：.vue、.jsx、.less 等文件，而是：html、css、js等。</p></li><li><p>打包后的文件，不再借助脚手架运行，而是需要部署到服务器上运行。</p></li><li><p>打包前，请务必梳理好前端项目的ajax封装（请求前缀、代理规则等）。</p></li></ul><h2><strong>2、本地服务器部署</strong></h2><h3><strong>2.1、具体操作步骤</strong>**<a target="_blank" rel="noopener noreferrer nofollow" href="/posts/front-end-project-launched/#21%E5%85%B7%E4%BD%93%E6%93%8D%E4%BD%9C%E6%AD%A5%E9%AA%A4">#</a>**</h3><ul class="tight" data-tight="true"><li><p>第一步：准备一个本地的服务器</p></li></ul><pre><code class="language-plain">本地服务器可以用：Java、Php、Go、Node.js 等语言编写，本教程采用是Node.js编写服务器，
端口号为：8088，且已经配置了public文件夹为静态资源。
</code></pre><ul class="tight" data-tight="true"><li><p>第二步：进行前端项目打包 具体的打包命令，可以参考package.json中的scripts字段配置。</p><img src="/api/images/image/2026/07/0e608ad19ee67fcb-image.png" alt="image.png" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"></li><li><p>第三步：将打包结果交给服务器</p></li></ul><pre><code class="language-plain">将打包生成的文件内容，放到服务器的静态资源文件夹中（上文中的public文件夹）
</code></pre><ul class="tight" data-tight="true"><li><p>第四步：测试访问前端项目</p></li></ul><pre><code class="language-plain">浏览器访问：http://localhost:8088即可看到我们的项目，但此时会遇到两个问题：
1、页面刷新 404
2、ajax 请求无法发送
</code></pre><h3><strong>2.2、解决刷新 404 问题</strong>**<a target="_blank" rel="noopener noreferrer nofollow" href="/posts/front-end-project-launched/#22%E8%A7%A3%E5%86%B3%E5%88%B7%E6%96%B0-404-%E9%97%AE%E9%A2%98">#</a>**</h3><p>问题分析：前端项目的路由，通常分为两种工作模式，分别为：</p><ol class="tight" data-tight="true"><li><p>hash模式</p></li></ol><pre><code class="language-plain">hash 值又称锚点，通常用于指定网页中的某个位置，例如下面的网址：
https://www.cctv.com/#SUBD1605080062959435，其中的#SUBD1605080062959435就是 hash 值，
hash 值只在客户端（如浏览器）中使用，是不会带给服务器的，所以使用 hash 模式时，不存在刷新 404 问题。
</code></pre><ol class="tight" data-tight="true"><li><p>history模式</p></li></ol><pre><code class="language-plain">history 去掉了URL中的#号，可以让应用的URL看起来更美观，带来的问题就是刷新时，
会将前端路由携带给后端，而后端没有对应资源的匹配，就出现了 404 问题。
</code></pre><p>解决方案一：将前端路由器工作模式改为 hash 模式 —— 不太推荐。</p><p>解决方案二：让服务器在收到未配置的GET路由时，都返回index.html即可。 方案二最终其实是把 url 中的 path，交给了前端路由去处理，具体配置如下：</p><pre><code class="language-typescript">app.get('*',(req,res)=&gt;{
	res.sendFile(__dirname + '/public/index.html')
})
</code></pre><p>也可以借助connect-history-api-fallback中间件完成配置</p><pre><code class="language-typescript">const history = require('connect-history-api-fallback');

app.use(history());
// 配置静态资源
app.use(express.static(__dirname + '/public'))
</code></pre><p>使用connect-history-api-fallback可以让配置更灵活，比如/login临时不需要作为前端路由处理，就可以按照如下方式配置</p><pre><code class="language-typescript">app.use(history({
	verbose:false,
	rewrites:[
		{ from: /^\/login.*$/, to: (context) =&gt; context.parsedUrl.path },
	]
}))
</code></pre><h3><strong>2.3、请求无法发送问题</strong>**<a target="_blank" rel="noopener noreferrer nofollow" href="/posts/front-end-project-launched/#23%E8%AF%B7%E6%B1%82%E6%97%A0%E6%B3%95%E5%8F%91%E9%80%81%E9%97%AE%E9%A2%98">#</a>**</h3><p>问题分析：脱离脚手架后，就没有了代理服务器，无法转发请求到【提供数据】的服务器。</p><p>如何解决？—— 在 Node 服务器中借助http-proxy-middleware中间件配置代理，具体配置如下：</p><pre><code class="language-typescript">// 引入createProxyMiddleware
const { createProxyMiddleware } = require('http-proxy-middleware')

// 配置代理中间件
app.use('/dev', createProxyMiddleware({
	target: 'http://sph-h5-api.atguigu.cn',
	changeOrigin: true,
	pathRewrite: {
		'^/dev': ''
	}
}))
</code></pre><h2><strong>3、nginx 服务器部署</strong></h2><h3><strong>3.1、nginx 简介</strong>**<a target="_blank" rel="noopener noreferrer nofollow" href="/posts/front-end-project-launched/#31nginx-%E7%AE%80%E4%BB%8B">#</a>**</h3><p>Nginx（发音为“engine-x”）是一款高性能的 HTTP 服务器和反向代理服务器，同时也是一个 IMAP/POP3/SMTP 代理服务器。Nginx 最初由 Igor Sysoev 编写，于 2004 年发布。它以其高性能、高稳定性、丰富的功能集和低系统资源消耗而闻名，主要功能有：</p><ol class="tight" data-tight="true"><li><p>反向代理</p></li><li><p>负载均衡</p></li><li><p>静态内容服务</p></li><li><p>HTTP/2 支持</p></li><li><p>SSL/TLS 支持</p></li><li><p>高速缓存</p></li></ol><h3><strong>3.2、nginx 配置代理练习</strong>**<a target="_blank" rel="noopener noreferrer nofollow" href="/posts/front-end-project-launched/#32nginx-%E9%85%8D%E7%BD%AE%E4%BB%A3%E7%90%86%E7%BB%83%E4%B9%A0">#</a>**</h3><p>今日头条接口地址：<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc"><strong>https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc</strong></a></p><p>直接向其发送 Ajax 请求会有跨域问题，接下来我们借助nginx解决跨域问题</p><ul class="tight" data-tight="true"><li><p>配置方式一：不过滤前缀</p></li></ul><p>以解决今日头条跨域为例，不干/hot-event掉前缀配置如下</p><pre><code class="language-typescript">location /hot-event {
  # 设置代理目标
  proxy_pass https://www.toutiao.com;

  # 允许跨域
  add_header 'Access-Control-Allow-Origin' '*';
  add_header 'Access-Control-Allow-Methods' '*';
  add_header 'Access-Control-Allow-Headers' '*';
  add_header 'Access-Control-Expose-Headers' '*';
}
</code></pre><p>前端写法</p><pre><code class="language-typescript">axios.get('http://localhost:8099/hot-event/hot-board/?origin=toutiao_pc')
</code></pre><ul class="tight" data-tight="true"><li><p>配置方式二：过滤前缀 解决今日头条跨域，不干/hot-event掉前缀，nginx配置（8099 端口）</p></li></ul><pre><code class="language-typescript">location /dev/ {
  # 设置代理目标
  proxy_pass https://www.toutiao.com/;

  # 允许跨域
  add_header 'Access-Control-Allow-Origin' '*';
  add_header 'Access-Control-Allow-Methods' '*';
  add_header 'Access-Control-Allow-Headers' '*';
  add_header 'Access-Control-Expose-Headers' '*';
}
</code></pre><p>前端写法</p><pre><code class="language-typescript">axios.get('http://localhost:8099/dev/hot-event/hot-board/?origin=toutiao_pc')
</code></pre><p>备注：使用下面配置删除上游服务器的指定响应头。</p><pre><code class="language-typescript">proxy_hide_header Access-Control-Allow-Origin;
</code></pre><h3><strong>3.3、nginx 部署前端项目</strong>**<a target="_blank" rel="noopener noreferrer nofollow" href="/posts/front-end-project-launched/#33nginx-%E9%83%A8%E7%BD%B2%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE">#</a>**</h3><p>整体思路：让nginx充当两个角色，既是 静态内容服务器，又是代理服务器。</p><ol class="tight" data-tight="true"><li><p>修改nginx配置如下，注意nginx的根目录最好不是 C 盘</p></li></ol><pre><code class="language-yaml"># 配置nginx根目录
location / {
  root   D:\dist;
  index  index.html index.htm;
}

# 配置代理
location /dev/ {
  # 设置代理目标
  proxy_pass http://sph-h5-api.atguigu.cn/;
}
</code></pre><ol class="tight" data-tight="true"><li><p>修改前端项目，让所有请求都转发给 /dev，随后重新打包</p></li></ol><pre><code class="language-typescript">const request = axios.create({
  baseURL:'/dev',
  timeout:10000
})
</code></pre><ol class="tight" data-tight="true"><li><p>随后直接访问nginx服务器即可，例如 nginx如果运行在8099端口，则访问：&nbsp;<code>**http://localhost:8099**</code></p></li><li><p>随后会遇到刷新404问题，追加nginx配置来解决</p></li></ol><pre><code class="language-typescript"># 配置nginx根目录
location / {
  root   D:\dist;
  index  index.html index.htm;
  try_files $uri $uri/ /index.html; # 解决刷新404
}
# 配置代理
location /dev/ {
  # 设置代理目标
  proxy_pass http://sph-h5-api.atguigu.cn/;
}
</code></pre><h2><strong>4、云服务器部署</strong></h2><p>我们可以在云服务器上借助nginx完成部署，大致流程与本地nginx部署一致</p><ol class="tight" data-tight="true"><li><p>关于购买云服务器，可选择：阿里云、腾讯云等。</p></li><li><p>关于操作系统，看个人习惯，Ubuntu、CentOs、RedHat、都不错。</p></li><li><p>购买完成后记得重置密码</p></li><li><p>linux 远程操作软件：Xshell、Xftp</p></li><li><p>具体配置如下：</p></li></ol><ul class="tight" data-tight="true"><li><p>给服务器安装nginx</p></li></ul><pre><code class="language-shell">yum install nginx
</code></pre><ul class="tight" data-tight="true"><li><p>将打包后的前端资源放在：/var/sph文件夹中。</p></li><li><p>使用Xftp配置服务器的 nginx，修改文件：/etc/nginx/nginx.config。</p></li></ul><pre><code class="language-yaml"># For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
          root   /var/sph;
          index  index.html index.htm;
          try_files $uri $uri/ /index.html; # 解决刷新404
        }
        # 配置代理
        location /dev/ {
          # 设置代理目标
          proxy_pass http://sph-h5-api.atguigu.cn/;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}
</code></pre>]]></content:encoded>
      <category>前端</category>
      <pubDate>Sat, 19 Oct 2024 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Git 分支基本使用</title>
      <link>https://codexun.com/posts/git-basic-use-branch</link>
      <guid isPermaLink="true">https://codexun.com/posts/git-basic-use-branch</guid>
      <description>1、分支的概念**#** 分支是 git 的一个重要特性，它可以让开发人员从主线上分离出来，在一个独立的线路上继续开发，最后可以灵活的选择合并分支，还是丢弃分支。用一个生活中的例子，来理解分支： 比如我们正在写一本小说，这本小说的当前内容就像是 git 中的 master 分支，现在我们有几个新的想法， 比如：新的角色</description>
      <content:encoded><![CDATA[<h2><strong>1、分支的概念</strong>**<a href="/posts/git-basic-use-branch/#1%E5%88%86%E6%94%AF%E7%9A%84%E6%A6%82%E5%BF%B5">#</a>**</h2>
<p>分支是 git 的一个重要特性，它可以让开发人员从主线上分离出来，在一个独立的线路上继续开发，最后可以灵活的选择合并分支，还是丢弃分支。用一个生活中的例子，来理解分支：</p>
<p><img src="/api/images/image/2026/07/65d4b271843ba5a1-image.png" alt="image.png"></p>
<pre><code class="language-plain">比如我们正在写一本小说，这本小说的当前内容就像是 git 中的 master 分支，现在我们有几个新的想法，
比如：新的角色、新的剧情，但不确定这些是否最终一定出现在故事里，所以我们可以这样做：
1. 可以为每个新的想法，创建一个新的笔记本（一个笔记本就相当于 git 中的一个新分支）
2. 每个笔记本（分支）中都可以自由地探索想法，且不会搅乱你的当前内容（master 分支）
3. 当我们觉得一个剧情线非常好，并且想要将其包含在故事中时，就会把那个笔记本的内容（分支）
合并到你的当前内容（master 分支）中。
4. 如果某个想法实验失败，可以选择抛弃那个笔记本（相当于 git 中的删除分支），
且这个过程不会影响当前内容（master分支）
</code></pre>
<h2><strong>2、创建与切换分支</strong>**<a href="/posts/git-basic-use-branch/#2%E5%88%9B%E5%BB%BA%E4%B8%8E%E5%88%87%E6%8D%A2%E5%88%86%E6%94%AF">#</a>**</h2>
<p>● 使用git branch xxx可以创建分支，其中xxx是分支名字</p>
<p><img src="/api/images/image/2026/07/3fdb28f3cf054825-image.png" alt="image.png"></p>
<pre><code class="language-plain">备注：创建完 test 分支后，tets 分支并不是空的，而且保留了 master 分支当前所有的提交记录。
</code></pre>
<p>● 使用git branch可以查看所有分支</p>
<p><img src="/api/images/image/2026/07/533eb46af2439365-image.png" alt="image.png"></p>
<p>● 使用git checkout xxx可以切换分支</p>
<p><img src="/api/images/image/2026/07/ac0acd7ee64e5278-image.png" alt="image.png"></p>
<p>● 注意点如下： ① 切换分支前，最好把当前分支管理好，即最好进行：add、commit操作。 ② 切换分支后，工作区、暂存区、会受到影响，具体为：</p>
<p>● 工作区：会变为切换到的分支的最后一次提交的状态</p>
<p>● 暂存区：同上，且如果暂存区有未提交的更改，那这些更改会被带到新分支的暂存区上。</p>
<p>比如：在 test 分支对 x 文件进行了暂存，没提交，随后切换到 master 分支时，x 文件会自动加在 master 分支的暂存区</p>
<h2><strong>3、合并分支（快速合并）</strong></h2>
<p>例如我们有 master 和 test 两个分支，test 分支是领先于 master 分支的，且 tets 分支包含 master 的所有历史记录，那么这时就可以触发 git 的快速合并，具体演示如下：</p>
<ol>
<li>第一步：创建 a、b 两个文件，随后提交到 master 分支。
<img src="/api/images/image/2026/07/ece51c4d959eb426-image.png" alt="image.png"></li>
<li>第二步：创建 test 分支，随后在 test 分支创建一个 c 文件，随后提交到 test 分支。
<img src="/api/images/image/2026/07/f3dc4c4f02c9cc05-image.png" alt="image.png"></li>
<li>第三步：切回 master 分支 此时发现工作区中 c 文件消失（因为 master 分支中没有 c 文件）。
<img src="/api/images/image/2026/07/d707a070690c9530-image.png" alt="image.png"></li>
<li>第四步：把 test 分支合并到 master 分支 使用git merge test合并分支，合并成功后，会发现工作区中已经有了 c 文件，且可以查看到 c 文件的提交记录。
<img src="/api/images/image/2026/07/ce33ffeee514a876-image.png" alt="image.png"></li>
</ol>
<h2><strong>4、合并分支（提交合并）****<a href="/posts/git-basic-use-branch/#4%E5%90%88%E5%B9%B6%E5%88%86%E6%94%AF%E6%8F%90%E4%BA%A4%E5%90%88%E5%B9%B6">#</a></strong></h2>
<p>场景描述：当进行分支合并时，若两个分支有不一致的版本，则会提示输提示输入提交信息，在当前分支形成一个新的提交记录，具体场景请执行上述15.3的前三步，然后接下来执行下面步骤：</p>
<p>第一步：切到 master 分支后，创建 d 文件，随后提交到 master 分支。</p>
<p><img src="/api/images/image/2026/07/4692570eb79c191d-image.png" alt="image.png"></p>
<p>第二步：在 master 分支中合并 test 分支 输入git merge test命令，进行分支合并</p>
<p><img src="/api/images/image/2026/07/6e8a5512a39de4e7-image.png" alt="image.png"></p>
<p>出现如下提示，含义是本次分支合并，存在不一样的版本，请为生成的新版本编写说明</p>
<p><img src="/api/images/image/2026/07/7d90c410ea2d2d22-image.png" alt="image.png"></p>
<p>输入完说明后，保存并退出随后提示合并完毕，随后提示合并完毕</p>
<p><img src="/api/images/image/2026/07/17b8e2b26efe0170-image.png" alt="image.png"></p>
<h2><strong>5、删除分支</strong>**<a href="/posts/git-basic-use-branch/#5%E5%88%A0%E9%99%A4%E5%88%86%E6%94%AF">#</a>**</h2>
<p>● git branch -d xxx可用于删除一个分支，其中xxx是分支名，例如我们删除 test 分支：</p>
<p><img src="/api/images/image/2026/07/9de5470728b69ce0-image.png" alt="image.png"></p>
<p>● 若删除时提示下图，说明：要删除的分支包含了一些尚未合并到其他分支的内容。</p>
<p><img src="/api/images/image/2026/07/f7e5e9cfeb62df65-image.png" alt="image.png"></p>
<p>解决办法有三种：</p>
<pre><code class="language-plain">○ 第一种方式：将要删除的分支合并到一个其他的分支，再进行删除。
○ 第二种方式：使用git branch -D xxx强制删除。
○ 第三种方式：使用git config advice.forceDeleteBranch false命令关掉该提示。
</code></pre>
<h2><strong>6、分支变基</strong>**<a href="/posts/git-basic-use-branch/#6%E5%88%86%E6%94%AF%E5%8F%98%E5%9F%BA">#</a>**</h2>
<p>在 Git 中，合并（merge）和变基（rebase）都是用于整合来自不同分支的更改的操作，它们区别如下：</p>
<p>● 合并（merge）： 将两个分支的历史合并在一起，gti 会创建一个新的“合并提交”，所有的分支和合并点都会被保留在历史中，有完整的历史记录。</p>
<p><img src="/api/images/image/2026/07/438b0049dfe3f7d7-image.png" alt="image.png"></p>
<p>● 变基（rebase） 将一个分支（test）上的提交重新应用到另一个分支（master）上，变基会重写项目历史，因为它实际上是在创建一系列新的提交，会产生一个更线性的历史记录，看起来更干净、更简单。</p>
<p><img src="/api/images/image/2026/07/1cc495cf55a926cc-image.png" alt="image.png"></p>
<h2><strong>7、tag（标签）****<a href="/posts/git-basic-use-branch/#7tag%E6%A0%87%E7%AD%BE">#</a></strong></h2>
<p>在Git中，标签（Tag）是用来指向特定提交的引用，通常用于标记项目中的重要点，比如版本发布。标签分为两种类型：</p>
<ol>
<li>轻量标签（Lightweight） 轻量标签只是简单地指向一个提交，不包含其他信息，创建轻量标签不会存储任何额外的信息。</li>
<li>附注标签（Annotated） 附注标签存储了额外的信息，例如：标签名、标签信息、创建者名字、电子邮件、创建日期等。因为它们包含了更多的信息，附注标签更适用于公共或正式发布的场合，比如软件版本的发布。 创建标签</li>
</ol>
<table>
<thead>
<tr>
<th><strong>命令</strong></th>
<th><strong>作用</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>git tag 标签名 版本号</td>
<td>给指定提交打 轻量标签</td>
</tr>
<tr>
<td>git tag -a 标签名 版本号 -m “标签描述”</td>
<td>给指定提交打 附注标签</td>
</tr>
<tr>
<td><strong>上述两个命令可以不写版本号，不写就是给最新提交打标签</strong></td>
<td></td>
</tr>
<tr>
<td>git tag</td>
<td>查看标签</td>
</tr>
<tr>
<td>git show 标签名</td>
<td>查看标签信息</td>
</tr>
<tr>
<td>git tag -d 标签名</td>
<td>删除标签</td>
</tr>
</tbody>
</table>
<h2><strong>8、游离分支</strong>**<a href="/posts/git-basic-use-branch/#8%E6%B8%B8%E7%A6%BB%E5%88%86%E6%94%AF">#</a>**</h2>
<p>git checkout也可以将代码签出到指定版本，即可以执行git checkout 具体版本号，当签出到指定版本时，签出的代码出于一个游离分支中，如下图操作：</p>
<p><img src="/api/images/image/2026/07/9f8f38c1141f8444-image.png" alt="image.png"></p>
<p>游离分支上可以对代码进行版本控制，但要注意：一旦从游离分支切走，游离分支的提交不会交给任何一个分支，所以对于游离分支我们的原则是：</p>
<pre><code class="language-plain">1. 尽量不修改游离分支的代码（只是看一看某个版本的代码）。
2. 若确实需要修改游离分支代码，应该从当前游离分支，创建出一个新的分支，随后去修改。
3. 若修在游离分支上发生了提交，随后从游离分支切走了，就要使用 reflog 寻找游离分支的提交。
</code></pre>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Fri, 18 Oct 2024 00:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Git 快速上手</title>
      <link>https://codexun.com/posts/git-quick-start</link>
      <guid isPermaLink="true">https://codexun.com/posts/git-quick-start</guid>
      <description>一、Git 简介 Git 是一款版本控制系统，它可以追踪文件的更改，并能多人协同开发，它最初由 Linus Torvalds 在 2005 年为更好地管理 Linux 内核而设计的； Git 是目前最流行的版本控制系统，主要功能有： ● 代码备份 ● 版本回退 ● 协同开发 ● 权限控制 二、下载安装 下载地址：**h</description>
      <content:encoded><![CDATA[<h2><strong>一、Git 简介</strong></h2>
<p>Git 是一款版本控制系统，它可以追踪文件的更改，并能多人协同开发，它最初由 Linus Torvalds 在 2005 年为更好地管理 Linux 内核而设计的； Git 是目前最流行的版本控制系统，主要功能有： ● 代码备份 ● 版本回退 ● 协同开发 ● 权限控制</p>
<h2><strong>二、下载安装</strong></h2>
<p>下载地址：**<a href="https://git-scm.com/">https://git-scm.com/</a>**** **，随后“傻瓜式”安装（一路下一步即可）</p>
<p><img src="/api/images/image/2026/07/65d6147cb95ae240-image.png" alt="image.png"></p>
<h2><strong>三、Git 基本使用</strong></h2>
<h3><strong>1、起始配置</strong>**<a href="/posts/git-quick-start/#1%E8%B5%B7%E5%A7%8B%E9%85%8D%E7%BD%AE">#</a>**</h3>
<p>第一次使用 Git 的时候，我们需要配置姓名和邮箱，让 Git 知道当前开发者的基本信息 配置姓名：</p>
<pre><code class="language-shell">git config --global user.name &quot;Your Name&quot;
</code></pre>
<p>配置邮箱：</p>
<pre><code class="language-shell">git config --global user.email &quot;email@example.com&quot;
</code></pre>
<p>注意点：</p>
<pre><code class="language-javascript">1. 命令中的各单词中间有空格。
2. 上述两个命令只在第一次使用 Git 时运行, 若输入错误, 重新运行命令即可。
3. 可以使用 git config --list或git config -l命令来查看配置信息 。
4. 上述这两个命令不用自己手敲, 直接复制粘贴，随后修改【姓名】与【邮箱】即可。
</code></pre>
<h3><strong>2、linux 常用命令</strong>**<a href="/posts/git-quick-start/#2linux-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">#</a>**</h3>
<p>Windows 为图形化操作形式，Linux 一般使用命令与系统进行交互，常用的命令如下：</p>
<table>
<thead>
<tr>
<th><strong>命令</strong></th>
<th><strong>含义</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>mkdir</td>
<td>创建文件夹（make directory）</td>
</tr>
<tr>
<td>cd</td>
<td>改变工作目录，（change directory 缩写）</td>
</tr>
<tr>
<td>touch</td>
<td>创建一个文件</td>
</tr>
<tr>
<td>ls</td>
<td>查看文件夹下的文件 （list 单词的缩写）</td>
</tr>
<tr>
<td>cat</td>
<td>查看文件内容</td>
</tr>
<tr>
<td>Tab</td>
<td>路径自动补全</td>
</tr>
<tr>
<td>clear</td>
<td>清屏（也可以使用 ctrl + L 快捷键）</td>
</tr>
<tr>
<td>rm</td>
<td>删除文件或文件夹 （删除不会进入回收站） -r 递归删除（用来删除文件夹）</td>
</tr>
<tr>
<td>ctrl + c</td>
<td>取消命令 c cancel 缩写</td>
</tr>
<tr>
<td>上下方向键</td>
<td>查看命令历史</td>
</tr>
</tbody>
</table>
<h3><strong>3、vim 编辑器</strong>**<a href="/posts/git-quick-start/#3vim-%E7%BC%96%E8%BE%91%E5%99%A8">#</a>**</h3>
<p>Vim 是一款命令行下的文本编辑器，编辑方式跟图形化编辑器不同，基本操作：</p>
<pre><code class="language-typescript">1. 命令行 vim 文件名 打开文件
2. 按i进入编辑模式
3. 开始编辑文件内容
4. ESC + :wq 保存并退出（w =&gt; write   q=&gt;quit）
5. ESC + :q! 不保存并退出 
</code></pre>
<h3><strong>4、Git 的三个区</strong>**<a href="/posts/git-quick-start/#4git-%E7%9A%84%E4%B8%89%E4%B8%AA%E5%8C%BA">#</a>**</h3>
<p>以生活中做菜为例，整个过程中涉及到三个区域：① 货架、② 购物车、③ 厨房</p>
<p><img src="/api/images/image/2026/07/a198b4d7a9880d55-image.png" alt="image.png"></p>
<p>Git 中同样也分为三个区域，分别是：</p>
<ol>
<li>工作区（代码编辑区）</li>
<li>暂存区（暂时存储区）</li>
<li>版本区（版本控制区）</li>
</ol>
<p>工作区、暂存区、版本区，三个区共同组成了一个 Git 仓库</p>
<p><img src="/api/images/image/2026/07/b8c532016491c829-image.png" alt="image.png"></p>
<h3><strong>5、基本操作流程</strong>**<a href="/posts/git-quick-start/#5%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C%E6%B5%81%E7%A8%8B">#</a>**</h3>
<p>第一步：创建一个空文件夹（名称不要包含中文），随后双击打开文件夹。</p>
<p>第二步：在文件夹空白处右键 ，随后点击 “Open Git Bash here” 启动命令行。</p>
<p>第三步：创建一个 Git 仓库，命令为：git init，命令执行成后，提示如下：</p>
<p><img src="/api/images/image/2026/07/337b596a60dad6ad-image.png" alt="image.png"></p>
<pre><code class="language-plain">git init执行后，会出现一个文件夹，这表示当前文件夹已成为一个 Git 仓库。
</code></pre>
<p>第四步：创建一些文件并编写代码，比如创建 【a.txt】，此时工作区已经有了新文件。</p>
<p>第五步：把工作区中【a.txt】添加到暂存区，具体命令为：git add a.txt，此命令无提示。</p>
<p><img src="/api/images/image/2026/07/c567d814a6c2a4d6-image.png" alt="image.png"></p>
<pre><code class="language-plain">补充：git add -A可以把工作区中的所有文件，都添加到暂存区，比较常用。
</code></pre>
<p>第六步：把暂存区【a.txt】添加到版本区，具体命令为：git commit -m ‘xxx’，随后提示如下：</p>
<p><img src="/api/images/image/2026/07/1d6e8ccf63ab8924-image.png" alt="image.png"></p>
<p>注意：此时我们先不用关心上图中的具体提示，后面会详细讲解。关键步骤图示：</p>
<p><img src="/api/images/image/2026/07/faaee20f9ab18077-image.png" alt="image.png"></p>
<h3><strong>6、Git 三区的位置</strong>**<a href="/posts/git-quick-start/#6git-%E4%B8%89%E5%8C%BA%E7%9A%84%E4%BD%8D%E7%BD%AE">#</a>**</h3>
<p>Git 的三区位置大致如下（了解即可）</p>
<p><img src="/api/images/image/2026/07/c5e0439a5eec02e6-image.png" alt="image.png"></p>
<h3><strong>7、查看仓库状态</strong>**<a href="/posts/git-quick-start/#7%E6%9F%A5%E7%9C%8B%E4%BB%93%E5%BA%93%E7%8A%B6%E6%80%81">#</a>**</h3>
<p>git status命令用于显示仓库当前的状态，git status -s也用于查看仓库状态，但提示更为简短。</p>
<ol>
<li>若没有需要提交的内容，提示如下
<img src="/api/images/image/2026/07/da348f29a5f0b493-image.png" alt="image.png"></li>
<li>若工作区的内容没有提交暂存区，提示如下
<img src="/api/images/image/2026/07/ee1d603eeff95a65-image.png" alt="image.png"></li>
<li>若暂存区的内容没有提交版本区，提示如下
<img src="/api/images/image/2026/07/5ff26e4fc6fa7ff7-image.png" alt="image.png"></li>
</ol>
<h3><strong>8、删除暂存</strong>**<a href="/posts/git-quick-start/#8%E5%88%A0%E9%99%A4%E6%9A%82%E5%AD%98">#</a>**</h3>
<p>git rm —cached 可以将文件从暂存区中删除。</p>
<p><img src="/api/images/image/2026/07/4b58b11ffc189255-image.png" alt="image.png"></p>
<h3><strong>9、对比差异</strong>**<a href="/posts/git-quick-start/#9%E5%AF%B9%E6%AF%94%E5%B7%AE%E5%BC%82">#</a>**</h3>
<p>git diff用于对比：工作区 vs 暂存区 git diff —cached 用于对比：暂存区 vs 仓库区 diff 命令的返回格式如下图：</p>
<p><img src="/api/images/image/2026/07/82fed332dfeffa57-image.png" alt="image.png"></p>
<p>注意：实际开发中文件多，且修改内容也多，我们会借助 git 的 GUI 工具进行文件差异对比。</p>
<h3><strong>10、查看版本日志</strong>**<a href="/posts/git-quick-start/#10%E6%9F%A5%E7%9C%8B%E7%89%88%E6%9C%AC%E6%97%A5%E5%BF%97">#</a>**</h3>
<p>借助于下面命令，可以查看整个 git 的提交记录</p>
<ol>
<li>git log：按提交时间降序列出所有提交，最新的提交会首先显示。
<img src="/api/images/image/2026/07/1035b3153cfd541d-image.png" alt="image.png"></li>
<li>git log —oneline：在git log的基础上，以简洁的一行显示每个提交。如果内容偏多， 需要使用方向键上下滚动， 按 q 退出。
<img src="/api/images/image/2026/07/b650625b3bd2b018-image.png" alt="image.png"></li>
</ol>
<h3><strong>11、版本回退</strong>**<a href="/posts/git-quick-start/#11%E7%89%88%E6%9C%AC%E5%9B%9E%E9%80%80">#</a>**</h3>
<p>Git 可以将项目代码内容切换到历史的任何一个版本，使用 git reset —hard 版本号 使用场景如下：</p>
<ol>
<li>创建一个 git 仓库，随后创建：a、b、c、d 四个文件
<img src="/api/images/image/2026/07/f786604096efe4ae-image.png" alt="image.png"></li>
<li>随后对四个文件，分别依次执行：git add、git commit进行版本控制</li>
<li>随后使用git log —online查看版本记录
<img src="/api/images/image/2026/07/ade22314507e58ed-image.png" alt="image.png"></li>
<li>现在我们使用git reset —hard 版本号把代码回滚到：刚提交完 b 文件的时候
<img src="/api/images/image/2026/07/2d23fa114a411cfe-image.png" alt="image.png"></li>
<li>两个注意点：</li>
</ol>
<pre><code class="language-typescript">注意点 1：git reset --hard会有如下动作：
   ① 切换版本区 HEAD （指针）指向
   ② 还原暂存区到对应版本
   ③ 还原工作区到对应版本
   执行git reset --hard之前，最好进行： git add 、git commit操作！
   --soft：重置HEAD到指定提交，所有的更改都会自动添加到暂存区，对工作区无影响。
   --mixed (默认值)：重置HEAD到指定的提交，并且重置暂存区，对工作区无影响。
   --hard：重置HEAD到指定的提交，并且重置暂存区，重置工作区！
注意点 2：使用git reset --hard版本回滚后，使用git log只能看到当前版本之前的记录，
例如：刚才回到了13f46ca这个版本，那使用git log --online就会看到：
</code></pre>
<p><img src="/api/images/image/2026/07/43f9898d46e5df18-image.png" alt="image.png"></p>
<pre><code class="language-plain">此时：看不见 c 文件和 d 文件的提交记录了，因为对于13f46ca 这个版本来说，
c 文件和 d 文件的提交记录属于未来，是看不到的，要想看到最完整的日志记录，
可以使用git reflog命令。  
</code></pre>
<p><img src="/api/images/image/2026/07/2f945d7603884357-image.png" alt="image.png"></p>
<h3><strong>13、修改提交</strong>**<a href="/posts/git-quick-start/#13%E4%BF%AE%E6%94%B9%E6%8F%90%E4%BA%A4">#</a>**</h3>
<p>git commit —amend 命令可以修改最近一次提交，它可以：</p>
<ol>
<li>重新编辑上次的提交信息。</li>
</ol>
<pre><code class="language-plain">如果在一次提交后，意识到提交信息有误或不够详细，
可以使用 git commit --amend 来重新编辑上次提交信息
</code></pre>
<ol>
<li>将新的更改添加到上次提交中。</li>
</ol>
<pre><code class="language-plain">如果在一次提交后，意识到忘记将某文件提交了，
可以先使用 git add 将这些遗漏的更改添加到暂存区，
然后通过 git commit --amend 将它们添加到上一个提交中。
</code></pre>
<p>场景演示（重新编辑上次的提交信息）</p>
<ol>
<li>创建一个 a 文件，其内容如下（包含 4 行内容）
<img src="/api/images/image/2026/07/7bbb381186c18558-image.png" alt="image.png"></li>
<li>随后对 a 文件进行版本控制，但提交信息编写有误
<img src="/api/images/image/2026/07/56a71a93bc9e331d-image.png" alt="image.png"></li>
<li>查看一下提交记录
<img src="/api/images/image/2026/07/fe900ae351501a82-image.png" alt="image.png"></li>
<li>随后使用git commit —amend命令进行修改提交，弹出 vim 编辑框，随后编辑好正确的提交信息，保存并退出
<img src="/api/images/image/2026/07/27dc252aef19e72b-image.png" alt="image.png"></li>
<li>此时提示提交成功
<img src="/api/images/image/2026/07/46af22b1475c581a-image.png" alt="image.png"></li>
<li>随后查看提交日志如下：
<img src="/api/images/image/2026/07/1ee08e676257cd2c-image.png" alt="image.png"></li>
</ol>
<p>场景演示（将新的更改添加到上次提交中）</p>
<ol>
<li>进行了一次提交后，查看提交记录如下：
<img src="/api/images/image/2026/07/ec44cade82d4ce9a-image.png" alt="image.png"></li>
<li>但发现 a 文件中少写了一些代码，随后我们补上了少写的代码，然后想对最近一次提交进行修改，执行git add -A后使用git commit —amend命令进行修改提交。
<img src="/api/images/image/2026/07/51fa00fd4fd197c1-image.png" alt="image.png"></li>
<li>随后弹出 vim 编辑框，我们在编辑框里重新编写提交说明，随后保存并退出。
<img src="/api/images/image/2026/07/bfd5d50a3a8ff53b-image.png" alt="image.png"></li>
<li>随后提交完毕，我们使用git log —online查看提交记录，发现提交已经被修改。
<img src="/api/images/image/2026/07/f6ac0c6939388642-image.png" alt="image.png"></li>
</ol>
<h3><strong>14、忽略文件</strong>**<a href="/posts/git-quick-start/#14%E5%BF%BD%E7%95%A5%E6%96%87%E4%BB%B6">#</a>**</h3>
<p>在项目开发过程中有些文件不应该存储到版本库中，这个时候配置忽略这些文件。 git 中需要创建一个文件 『.gitignore』文件来设置忽略，『.gitignore』文件与.git 目录同级，常用规则如下：</p>
<table>
<thead>
<tr>
<th><strong>内容</strong></th>
<th><strong>含义</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>temp</td>
<td>忽略任何路径下的名为 temp 的文件、文件夹。</td>
</tr>
<tr>
<td>*.log</td>
<td>忽略任何路径下以.log 结尾的文件。</td>
</tr>
<tr>
<td>/dist</td>
<td>忽略根目录下的 dist 文件，不会忽略其他目录下的 dist 文件</td>
</tr>
</tbody>
</table>
<p>备注：.gitkeep文件可以把空文件夹提交</p>]]></content:encoded>
      <category>GitHub</category>
      <pubDate>Thu, 17 Oct 2024 00:00:00 GMT</pubDate>
    </item>
  </channel>
</rss>