<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>梦回故里</title>
	<atom:link href="https://www.80shihua.com/feed" rel="self" type="application/rss+xml" />
	<link>https://www.80shihua.com</link>
	<description>梦回故里是一个php程序员的个人博客，这里主要是他分享的php，go，java，python等编程经验</description>
	<lastBuildDate>Sun, 26 Jul 2026 12:44:06 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.4.3</generator>
	<item>
		<title>富文本图片突破窄栏居中：不拉伸、不裁切、自适应的纯 CSS 方案</title>
		<link>https://www.80shihua.com/archives/3442</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Sun, 26 Jul 2026 12:44:00 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3442</guid>

					<description><![CDATA[场景：新闻/博客详情页，正文保持 800px 窄栏阅读，但图片要突破窄栏、在更宽区域（最多 1200px）居中 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading"></h1>



<blockquote class="wp-block-quote">
<p>场景：新闻/博客详情页，正文保持 800px 窄栏阅读，但图片要突破窄栏、在更宽区域（最多 1200px）居中显示。富文本编辑器输出的 <code>&lt;img&gt;</code> 总被包在 <code>&lt;p&gt;</code> 里。本文记录从&#8221;踩坑&#8221;到最终解法的完整过程。</p>
</blockquote>



<h2 class="wp-block-heading">一、问题场景</h2>



<p>内容型网站常希望正文保持<strong>窄栏阅读</strong>（如 800px，行宽舒适），但其中的<strong>图片能突破窄栏、在更宽的区域内居中显示</strong>（如最大 1200px），营造杂志式排版张力。</p>



<p></p>



<figure class="wp-block-image size-large"><a href="https://www.80shihua.com/wp-content/uploads/2026/07/image.png"><img fetchpriority="high" decoding="async" width="1024" height="633" src="https://www.80shihua.com/wp-content/uploads/2026/07/image-1024x633.png" alt="" class="wp-image-3443" srcset="https://www.80shihua.com/wp-content/uploads/2026/07/image-1024x633.png 1024w, https://www.80shihua.com/wp-content/uploads/2026/07/image-300x185.png 300w, https://www.80shihua.com/wp-content/uploads/2026/07/image-768x474.png 768w, https://www.80shihua.com/wp-content/uploads/2026/07/image-1536x949.png 1536w, https://www.80shihua.com/wp-content/uploads/2026/07/image.png 1661w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></figure>



<p>难点在于：后台富文本编辑器输出的 <code>&lt;img&gt;</code>，几乎总是被包在 <code>&lt;p&gt;</code> 标签里：</p>



<p>容器结构：</p>



<p>目标拆解：</p>



<ol>
<li>文字段落保持 800px 窄栏（已有）</li>



<li>图片最多显示 1200px 宽，并<strong>居中于页面</strong></li>



<li><strong>小图（原图 &lt; 1200px）保持原始尺寸，绝不被拉伸</strong></li>



<li><strong>大图（原图 &gt; 1200px）等比缩放到 1200px</strong></li>



<li>不裁切、不产生横向滚动条</li>
</ol>



<h2 class="wp-block-heading">二、第一版：常见的&#8221;负 margin&#8221;写法（有坑）</h2>



<p>最直觉的写法：</p>



<p>意图是用 <code>width</code> 把图片钉死成 1200px，再用负 <code>margin-left</code> 让它突破 800px 父容器、左右对称溢出从而居中。</p>



<p><strong>坑在哪？</strong></p>



<ul>
<li><code>width</code> 被设成 1200px，而原图只有 800px → <code>height:auto</code> 等比放大，<strong>图片被拉伸模糊</strong>。</li>



<li><code>max-width: initial</code> 顺手把浏览器默认的 <code>max-width:100%</code>（保护图片不超过自身固有宽度）关掉了，这是拉伸的直接元凶。</li>



<li>负 <code>margin-left</code> 的计算**依赖&#8221;图片恒为 1200px&#8221;**这个假设。一旦改成 <code>width:auto</code> 让小图变窄，负 margin 按 1200 算就会把小图推偏，不再居中。</li>
</ul>



<h2 class="wp-block-heading">三、尝试 Grid 方案（也失败了）</h2>



<p>既然负 margin 依赖固定宽度，那让图片&#8221;自适应居中&#8221;不就行了？于是有了 Grid 思路：</p>



<p><strong>为什么失败？</strong></p>



<p>Grid 容器（<code>.nd-body</code>）自身只有 800px 宽。Grid 子项（<code>img</code>）所在的网格单元格<strong>也继承了这个 800px 宽度</strong>。无论你怎么 <code>justify-self:center</code>，图片最多只能在 800px 内居中——<strong>根本突破不了 800 栏</strong>。</p>



<p>而且 <code>display:grid</code> 改变了所有子项的默认行为（默认 <code>stretch</code> 撑满单元格），导致<strong>文字段落也溢出 800px 变宽了</strong>，副作用明显。</p>



<p>结论：在&#8221;父容器本身只有 800px&#8221;的前提下，任何把它变成 grid/flex 再试图让子项突破的思路，都会撞上&#8221;容器宽度天花板&#8221;。</p>



<h2 class="wp-block-heading">四、最终方案：宽容器 + 百分比定位</h2>



<p>核心思路转变：<strong>不要去动父容器（.nd-body）的布局，而是让&#8221;含图的那个 <code>&lt;p&gt;</code>&#8220;自己变成宽容器</strong>，然后用经典的水平居中技巧定位。</p>



<h3 class="wp-block-heading">为什么这样就对了？</h3>



<p>逐场景验证：</p>



<figure class="wp-block-table"><table><tbody><tr><td>图片实际宽度</td><td><code>&lt;p&gt;</code> 宽度</td><td>图片渲染</td><td>最终效果</td></tr><tr><td>600px（小图）</td><td>min(视口-144, 1200) = 1200</td><td><code>width:auto</code>→600，<code>max-width:100%</code> 不触发</td><td>600px 原尺寸，在 1200 的 <code>&lt;p&gt;</code> 内经 <code>margin-inline:auto</code> 居中，而 <code>&lt;p&gt;</code> 又经 transform 居中于页面 → <strong>小图原尺寸居中</strong></td></tr><tr><td>1400px（大图）</td><td>1200</td><td><code>max-width:100%</code> 封顶到 1200，等比缩放</td><td>1200px 撑满 <code>&lt;p&gt;</code>，<code>&lt;p&gt;</code> 居中 → <strong>大图等比 1200 居中</strong></td></tr><tr><td>介于之间（如 1000px）</td><td>1200</td><td><code>width:auto</code>→1000</td><td>1000px 原尺寸居中</td></tr></tbody></table></figure>



<p>关键点：</p>



<ul>
<li><strong><code>width:auto</code> + <code>max-width:100%</code></strong> 让浏览器对 <code>&lt;img&gt;</code> 永远不会超过其固有宽度（小图不拉伸），同时受父级 <code>&lt;p&gt;</code> 宽度上限约束（大图封顶）。</li>



<li><strong><code>margin-left:50%</code> + <code>transform:translateX(-50%)</code></strong> 是&#8221;超宽元素在窄父容器内水平居中&#8221;的标准解法，不依赖浏览器对超宽 <code>margin:auto</code> 的计算差异（这点在第三节的 grid 尝试里吃过亏）。</li>



<li><strong><code>margin-inline:auto</code> 加在 <code>img</code> 上</strong>而非 <code>&lt;p&gt;</code> 上——因为 <code>&lt;p&gt;</code> 已经被 <code>transform</code> 居中，图片要在 <code>&lt;p&gt;</code> 内部再居中一次，否则小图会在宽 <code>&lt;p&gt;</code> 里左对齐而偏左。</li>
</ul>



<h2 class="wp-block-heading">五、移动端处理</h2>



<p>桌面端 <code>&lt;p&gt;</code> 被设成固定宽容器 + transform，在手机上不适用（图片应回归正常流式满宽）：</p>



<h2 class="wp-block-heading">六、兼容性</h2>



<ul>
<li><code>:has()</code> 选择器：Chrome/Edge 105+、Safari 15.4+、Firefox 121+（2023 年起全支持）。对 2024 年后的项目无虞。</li>



<li><code>transform</code>、<code>margin:auto</code> 及 <code>:has()</code> 之外的属性均为 CSS 基础特性，无需担忧。</li>
</ul>



<p>若必须兼容更老浏览器（如 Firefox &lt; 121），则需退回到&#8221;改模板、给图片套独立 wrapper&#8221;的方案，或接受小图左对齐的折中——纯 CSS 在不改 DOM 的前提下，<code>:has()</code> + <code>transform</code> 已是当下最优解。</p>



<h2 class="wp-block-heading">七、总结</h2>



<figure class="wp-block-table"><table><tbody><tr><td>需求</td><td>实现要点</td></tr><tr><td>文字窄栏阅读</td><td><code>.nd-body { max-width:800px }</code> 不动</td></tr><tr><td>图片突破窄栏、最大 1200</td><td>含图 <code>&lt;p&gt;</code> 变宽容器 <code>width:min(100vw-144px,1200px)</code></td></tr><tr><td>小图不拉伸、居中</td><td><code>img { width:auto; max-width:100%; margin-inline:auto }</code></td></tr><tr><td>大图等比缩到 1200</td><td>同上，<code>max-width:100%</code> 相对宽 <code>&lt;p&gt;</code> 封顶</td></tr><tr><td>精确居中（不偏）</td><td><code>&lt;p&gt; { margin-left:50%; transform:translateX(-50%) }</code></td></tr><tr><td>不裁切</td><td>不设 <code>overflow:clip/hidden</code></td></tr><tr><td>移动端正常</td><td>媒体查询里重置 <code>&lt;p&gt;</code> 与 <code>img</code></td></tr></tbody></table></figure>



<p><strong>核心认知</strong>：当父容器本身只有 N px 宽时，想让子元素突破 N 并居中——<strong>不要去改父容器的 display（grid/flex 会撞上容器宽度天花板），而是让子元素自己变宽 + 用 <code>margin-left:50%</code> + <code>transform</code> 定位</strong>。这个思路能规避绝大多数&#8221;负 margin 依赖固定宽度&#8221;的坑。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>真正用懂 Trae Solo 之后，我才发现它不是“单人模式”</title>
		<link>https://www.80shihua.com/archives/3438</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Sat, 13 Dec 2025 09:45:21 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[solo]]></category>
		<category><![CDATA[trae]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3438</guid>

					<description><![CDATA[很多人第一次用 Trae 的 Solo 模式，都会产生一个错觉： “这不就是以前 Agent + 文件夹的那套 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>很多人第一次用 Trae 的 <strong>Solo 模式</strong>，都会产生一个错觉：</p>



<blockquote class="wp-block-quote">
<p>“这不就是以前 Agent + 文件夹的那套东西吗？”</p>
</blockquote>



<p>如果只停留在界面层，你会觉得变化不大；<br />但只要你真的跑过几个任务，很快就会意识到一件事：</p>



<blockquote class="wp-block-quote">
<p><strong>Solo 根本不是“简化版 Workspace”，<br />而是一个可以并行执行多个 Agent 的单人任务系统。</strong></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">一、为什么我会有这种感觉？</h2>



<p>因为在真实使用中，Solo 和 IDE / 旧 Agent 模式的差异非常明显：</p>



<ul>
<li>我可以<strong>同时启动多个任务</strong></li>



<li>每个任务都在独立执行</li>



<li>互不阻塞、互不打断</li>



<li>每一个任务的行为，都像“以前完整跑过一次 Agent”</li>
</ul>



<p></p>



<figure class="wp-block-image size-large"><a href="https://www.80shihua.com/wp-content/uploads/2025/12/image.png"><img decoding="async" width="1024" height="782" src="https://www.80shihua.com/wp-content/uploads/2025/12/image-1024x782.png" alt="" class="wp-image-3439" srcset="https://www.80shihua.com/wp-content/uploads/2025/12/image-1024x782.png 1024w, https://www.80shihua.com/wp-content/uploads/2025/12/image-300x229.png 300w, https://www.80shihua.com/wp-content/uploads/2025/12/image-768x587.png 768w, https://www.80shihua.com/wp-content/uploads/2025/12/image-1536x1173.png 1536w, https://www.80shihua.com/wp-content/uploads/2025/12/image.png 1787w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></figure>



<p>这不是心理感受，而是<strong>执行模型的变化</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">二、IDE 模式下，其实只有“一个任务”</h2>



<p>先说清楚以前的情况。</p>



<p>在 IDE 或传统 Agent 使用方式里，流程通常是：</p>



<ol>
<li>进入一个文件夹</li>



<li>打开一个对话</li>



<li>给 Agent 一个目标</li>



<li>Agent 开始执行</li>



<li>执行结束，对话结束</li>
</ol>



<p>本质上是：</p>



<blockquote class="wp-block-quote">
<p><strong>一个会话 = 一个任务 = 一个 Agent 执行实例</strong></p>
</blockquote>



<p>它是<strong>单线程的、串行的</strong>。</p>



<p>你想做第二件事，必须等第一件事跑完，或者手动中断。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">三、Solo 模式真正改变的是什么？</h2>



<p><strong>Solo 改变的不是“Agent 能不能用”，<br />而是“Agent 怎么被执行”。</strong></p>



<p>在 Solo 里，发生了三个关键变化。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1&#xfe0f;&#x20e3; 任务被显式建模了</h3>



<p>以前：</p>



<ul>
<li>任务只是一次对话</li>



<li>执行过程藏在聊天记录里</li>
</ul>



<p>现在：</p>



<ul>
<li><strong>任务是一个独立实体</strong></li>



<li>有自己的生命周期</li>



<li>有明确的执行状态</li>
</ul>



<p>你看到的不再只是聊天，而是<strong>任务列表</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2&#xfe0f;&#x20e3; 每个任务，本质上都是一个 Agent 实例</h3>



<p>这也是你体感最强的一点：</p>



<ul>
<li>每启动一个任务</li>



<li>就相当于启动了一个新的 Agent</li>



<li>有独立上下文、独立推理、独立执行轨迹</li>
</ul>



<p>所以你会自然觉得：</p>



<blockquote class="wp-block-quote">
<p>“每个任务，都像以前完整跑了一次 Agent。”</p>
</blockquote>



<p>这个感觉是完全准确的。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3&#xfe0f;&#x20e3; 执行从“串行”变成了“并行”</h3>



<p>这是 Solo 最核心的变化。</p>



<p>你现在可以：</p>



<ul>
<li>A 任务在查资料</li>



<li>B 任务在写代码</li>



<li>C 任务在生成内容</li>
</ul>



<p>它们<strong>同时进行</strong>，互不影响。</p>



<p>这就是你说的那句话：</p>



<blockquote class="wp-block-quote">
<p><strong>可以并行执行多任务，可以批量执行 Agent。</strong></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">四、为什么 Solo 给人的感觉，不像“聊天工具”了？</h2>



<p>因为使用心态已经变了。</p>



<p>以前你是在：</p>



<blockquote class="wp-block-quote">
<p><strong>和 Agent 聊天</strong></p>
</blockquote>



<p>现在你是在：</p>



<blockquote class="wp-block-quote">
<p><strong>给 Agent 派活、看它们执行、等结果</strong></p>
</blockquote>



<p>这是从「交互式助手」到「任务调度系统」的转变。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">五、用程序员视角来类比，会非常清楚</h2>



<p>如果用工程模型来理解：</p>



<ul>
<li>IDE / 旧 Agent 模式 ≈ <strong>单线程程序</strong></li>



<li>Solo 模式 ≈ <strong>多线程 / 任务队列</strong></li>
</ul>



<p>每一个任务，都是一个独立运行的执行单元。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">六、这和 Workspace / 协作，其实没直接关系</h2>



<p>一个很重要但容易混淆的点是：</p>



<blockquote class="wp-block-quote">
<p><strong>Solo 的核心价值，并不在“单人”，<br />而在“并行任务 + 多 Agent 调度”。</strong></p>
</blockquote>



<p>是否协作，只是外层组织方式；<br />真正让你感觉“好用很多”的，是<strong>执行模型升级</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">七、什么时候 Solo 的优势会被无限放大？</h2>



<p>当你开始做这些事情时：</p>



<ul>
<li>内容批量生成</li>



<li>多方案同时推演</li>



<li>多个 PoC 并行验证</li>



<li>一个大目标拆成多个子任务</li>
</ul>



<p>你会明显感觉到：</p>



<blockquote class="wp-block-quote">
<p><strong>Solo 的效率，不是线性提升，而是成倍提升。</strong></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">八、一句话总结（这是我现在对 Solo 的最终理解）</h2>



<blockquote class="wp-block-quote">
<p><strong>Trae 的 Solo 模式，本质上是一个为个人用户设计的多 Agent 并行执行环境。</strong></p>
</blockquote>



<p>它不是“少了协作的 Workspace”，<br />而是<strong>让一个人，也能像一个小型团队一样同时推进多件事</strong>。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>在 Ghost（v5版本）中通过修改 core 实现模板随机文章推荐</title>
		<link>https://www.80shihua.com/archives/3434</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Wed, 17 Sep 2025 02:43:09 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3434</guid>

					<description><![CDATA[要点：自定义 helper 必须放到 Ghost 的 core helpers 目录（例如 /var/www/ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p></p>



<blockquote class="wp-block-quote">
<p><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-luminous-vivid-amber-color">要点：自定义 helper 必须放到 Ghost 的 core helpers 目录（例如 <code>/var/www/ghost/versions/5.113.0/core/frontend/helpers/</code> 或 <code>current/core/frontend/helpers/</code>），然后重启 Ghost 才会被加载。<strong>托管在 Ghost(Pro) 的站点无法修改 core</strong>，这点请注意。</mark></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">一、为什么要放到 core？</h2>



<p>Ghost 在渲染时只会加载 core 中注册的 Handlebars helper。把 JS 放到主题目录下（<code>content/themes/.../helpers/</code>）<strong>不会</strong>被 Ghost 自动识别为 core helper，因此不会生效。把 helper 放到 core 目录并重启 Ghost，Ghost 才会把它注册为 Handlebars helper 并在模板里可用（我们已在你的环境验证过：把测试 helper 放到 core 后页面能显示 <code>HELLO_HELPER</code>）。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">二、准备工作（备份 + 路径定位）</h2>



<ol>
<li>先备份 core（非常重要）：</li>
</ol>



<pre class="wp-block-code"><code>cd /var/www/ghost
cp -a versions/5.113.0/core versions/5.113.0/core.bak
</code></pre>



<ol start="2">
<li>找到当前运行的 core 目录（两个常见位置）：</li>
</ol>



<ul>
<li>直接版本路径：<code>/var/www/ghost/versions/5.113.0/core/frontend/helpers/</code></li>



<li>或 <code>current</code> 快捷路径：<code>/var/www/ghost/current/core/frontend/helpers/</code></li>
</ul>



<p>用下面命令查看：</p>



<pre class="wp-block-code"><code>ls /var/www/ghost/versions
ls -l /var/www/ghost/current/core/frontend/helpers
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">三、把 helper 放到 core（完整代码）</h2>



<p>创建文件：<br /><code>/var/www/ghost/versions/5.113.0/core/frontend/helpers/random_post.js</code><br />或 <code>/var/www/ghost/current/core/frontend/helpers/random_post.js</code></p>



<p>文件内容（可直接拷贝）：</p>



<pre class="wp-block-code"><code>const Handlebars = require('handlebars');

module.exports = function random_post(options) {
    try {
        // 1) 优先使用传入的 posts（{{#random_post posts=all_posts}}）
        let posts = options.hash &amp;&amp; options.hash.posts ? options.hash.posts : null;

        // 2) 回退：从模板上下文里抓
        if (!posts &amp;&amp; options.data) {
            const root = options.data.root || {};
            const site = options.data.site || {};
            posts = root.posts || site.posts || (root.site &amp;&amp; root.site.posts) || &#91;];
        }

        // 3) 兼容 Bookshelf collection（.models）
        if (posts &amp;&amp; posts.models &amp;&amp; Array.isArray(posts.models)) {
            posts = posts.models.map(m =&gt; (m &amp;&amp; typeof m.toJSON === 'function') ? m.toJSON() : m);
        }

        // 4) 转数组（若不是数组）
        if (posts &amp;&amp; !Array.isArray(posts)) {
            try { posts = Array.from(posts); } catch (e) { posts = Object.values(posts || {}); }
        }

        if (!posts || posts.length === 0) return '';

        // 支持 limit 参数（默认 1）
        const limit = Math.max(1, parseInt(options.hash.limit, 10) || 1);

        // Fisher-Yates 洗牌
        let shuffled = posts.slice();
        for (let i = shuffled.length - 1; i &gt; 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            &#91;shuffled&#91;i], shuffled&#91;j]] = &#91;shuffled&#91;j], shuffled&#91;i]];
        }

        // 取前 N 项
        const selected = shuffled.slice(0, Math.min(limit, shuffled.length));

        // 渲染每项，并手动注入 @index/@first/@last
        let result = '';
        selected.forEach((post, i) =&gt; {
            const ctx = (post &amp;&amp; typeof post.toJSON === 'function') ? post.toJSON() : post;

            let data = Handlebars.createFrame(options.data);
            data.index = i;
            data.first = (i === 0);
            data.last = (i === selected.length - 1);

            result += options.fn(ctx, { data });
        });

        return result;
    } catch (e) {
        try { console.error('random_post helper error:', e); } catch (_) {}
        return '';
    }
};
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">四、重启 Ghost（使 helper 生效）</h2>



<p>在 Ghost 安装根目录执行：</p>



<pre class="wp-block-code"><code>cd /var/www/ghost
# 以 ghost-cli 管理的情况下
ghost restart
</code></pre>



<p>如果需要以特定用户运行（视你服务器配置）：</p>



<pre class="wp-block-code"><code>sudo -u &lt;ghost-user&gt; ghost restart
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">五、测试 helper 是否被加载</h2>



<p>先用最小测试确认加载（先将文件临时改为）：</p>



<pre class="wp-block-code"><code>module.exports = function random_post() {
  return 'HELLO_HELPER';
};
</code></pre>



<ul>
<li>重启 Ghost，刷新页面，查看是否出现 <code>HELLO_HELPER</code>（或在页面源代码中查找）。如果出现，说明 helper 已加载成功。</li>
</ul>



<p>把文件恢复成完整版本后继续下一步。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">六、在模板中的使用（推荐：先用 <code>get</code> 再传 posts）</h2>



<h3 class="wp-block-heading">随机 5 篇（推荐写法）</h3>



<pre class="wp-block-code"><code>{{#get "posts" limit="all" include="authors,tags" as |all_posts|}}
  {{#random_post posts=all_posts limit=5}}
    &lt;div class="related-post {{#if @last}}last{{/if}}"&gt;
      &lt;a href="{{url}}"&gt;{{title}}&lt;/a&gt;
    &lt;/div&gt;
  {{/random_post}}
{{/get}}
</code></pre>



<p>说明：</p>



<ul>
<li><code>{{#get}}</code> 确保我们拿到全站文章数组并把它传入 helper（这保证在任意页面都可用）。</li>



<li>在 helper 里我们已经手动传入 <code>@index</code>/<code>@first</code>/<code>@last</code>，模板中可以使用 <code>{{#if @last}}</code> 等。</li>
</ul>



<h3 class="wp-block-heading">在文章页排除当前文章（示例）</h3>



<pre class="wp-block-code"><code>{{#get "posts" limit="all" filter="id:-{{id}}" as |all_posts|}}
  {{#random_post posts=all_posts limit=3}}
    &lt;a href="{{url}}"&gt;{{title}}&lt;/a&gt;
  {{/random_post}}
{{/get}}
</code></pre>



<p><code>filter="id:-{{id}}"</code> 用来排除当前文章（避免推荐自己）。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">七、常见问题与排查</h2>



<ul>
<li><strong>放在主题下无效</strong>：主题目录下的 <code>helpers/</code> 文件不会被 Ghost core 自动加载 —— 必须放到 core helpers。</li>



<li><strong>权限问题</strong>：确保文件属主/权限正确，例如 <code>sudo chown &lt;ghost-user&gt;:&lt;ghost-user&gt; path</code>、<code>chmod 644</code>。</li>



<li><strong>升级后失效</strong>：Ghost 升级会生成新版本目录，你需要把 helper 复制到新的 <code>versions/X.Y.Z/core/frontend/helpers/</code> 或使用 <code>current</code> 的路径（将 helper 放在 <code>current</code> 的同一路径下会更方便，但升级时仍需确认）。</li>



<li><strong>托管版限制</strong>：若你使用 Ghost(Pro)，你无法修改 core，这种情况请改用“服务端定时脚本写 partial”或其它代理层方案（我可提供脚本）。</li>



<li><strong>gscan / 主题校验</strong>：自定义 core helper 不影响 theme 的 gscan，但如果你在主题里引用了尚未注册的 helper，gscan 可能会报错 ——确保在部署主题前已在 core 注册 helper。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">八、备选方案（当你没法改 core）</h2>



<p>如果你不能修改 core（例如 Ghost(Pro)），使用 <strong>定时脚本 + Content API + 写 partial</strong> 是可行替代：</p>



<ul>
<li>写一个脚本定期（如每 30 分钟）用 Content API 拉取所有文章并随机选若干篇，生成 <code>content/themes/your-theme/partials/random.hbs</code> 内容；</li>



<li>模板中 <code>{{&gt; "random"}}</code> 即可渲染。优点：不改 core、SEO 好；缺点：需要一个定时任务和 Content API key。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>利用模拟表单提交修改 Ghost 设置：绕过 Admin API 的限制</title>
		<link>https://www.80shihua.com/archives/3433</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Mon, 15 Sep 2025 09:45:45 +0000</pubDate>
				<category><![CDATA[nodejs]]></category>
		<category><![CDATA[ghost]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3433</guid>

					<description><![CDATA[Ghost 是一款现代化的开源博客平台，提供了强大的内容管理能力和灵活的 API 接口。然而，在某些场景下，G [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p></p>



<p>Ghost 是一款现代化的开源博客平台，提供了强大的内容管理能力和灵活的 API 接口。然而，在某些场景下，Ghost Admin API 并不开放对站点设置（如标题、描述等）的直接修改接口，这给自动化配置和集成带来了挑战。</p>



<p>本文将介绍一种可行的解决方案：<strong>模拟后台表单登录与提交行为</strong>，实现对 Ghost 设置的修改。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"> Ghost Admin API 的限制</h3>



<p>Ghost Admin API 提供了丰富的内容管理接口，如文章发布、标签管理、用户信息等，但它并未公开用于修改站点设置的 API。例如：</p>



<ul>
<li><code>/ghost/api/admin/settings/</code> 接口存在，但需要后台登录 session，且不接受 API Key 认证。</li>



<li>无法通过 Admin API Key 或 Content API Key 修改站点标题、描述等设置项。</li>
</ul>



<p>这意味着，如果你希望通过代码修改 Ghost 的设置，必须模拟后台登录行为。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">解决方案：模拟后台登录 + 表单提交</h3>



<h4 class="wp-block-heading">步骤一：模拟登录获取 session cookie</h4>



<p>Ghost 后台登录接口为：</p>



<pre class="wp-block-code"><code>POST /ghost/api/admin/session/
</code></pre>



<p>请求体格式：</p>



<pre class="wp-block-code"><code>{
  "username": "admin@example.com",
  "password": "your-password"
}
</code></pre>



<p>登录成功后，服务器会返回 <code>Set-Cookie</code> 响应头，其中包含 <code>ghost-admin-api-session</code>，这是后续认证的关键。</p>



<h4 class="wp-block-heading">步骤二：提取并保存 cookie</h4>



<p>在服务端（如 Node.js）中使用 Axios：</p>



<pre class="wp-block-code"><code>const sessionResponse = await axios.post(`${GHOST_API_URL}/ghost/api/admin/session/`, {
  username,
  password
}, {
  headers: {
    'Content-Type': 'application/json',
    'Accept-Version': 'v5.0'
  }
});

const cookies = sessionResponse.headers&#91;'set-cookie']; // 提取 session cookie
</code></pre>



<h4 class="wp-block-heading">步骤三：使用 cookie 模拟设置修改请求</h4>



<pre class="wp-block-code"><code>await axios.put(`${GHOST_API_URL}/ghost/api/admin/settings/`, {
  settings: {
    title: '新站点标题',
    description: '新描述'
  }
}, {
  headers: {
    'Cookie': cookies.join('; '),
    'Content-Type': 'application/json',
    'Accept-Version': 'v5.0'
  }
});
</code></pre>



<p>注意：此请求必须携带有效的 session cookie，才能被 Ghost 后台接受。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"> 注意事项</h3>



<ul>
<li>Cookie 的 <code>Path</code> 默认是 <code>/ghost</code>，建议在代理时改为 <code>/</code>，确保前端请求能携带。</li>



<li>如果你在浏览器环境中操作，必须设置 <code>withCredentials: true</code>，并确保 CORS 配置允许跨域携带 cookie。</li>



<li>Ghost 的 session cookie 是 <code>HttpOnly</code>，无法通过 <code>document.cookie</code> 设置，只能由服务器通过 <code>Set-Cookie</code> 设置。</li>



<li>res.setHeader(&#8216;Set-Cookie&#8217;, cookies.map(c => c.replace(&#8216;Path=/ghost&#8217;, &#8216;Path=/&#8217;)));</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"> 总结</h3>



<p>Ghost Admin API 虽然功能强大，但在设置管理方面存在限制。通过模拟后台登录和表单提交行为，我们可以实现自动化修改站点设置的目的。这种方式虽然不如标准 API 优雅，但在实际部署和集成中非常实用。</p>



<p>如果你正在构建一个 Ghost 管理工具、自动化部署脚本或自定义后台，这种方法将为你打开一扇通往更深层控制的大门。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>从田埂到聚光灯：一个农村女孩的逆袭之路</title>
		<link>https://www.80shihua.com/archives/3422</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Sun, 31 Aug 2025 01:18:12 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3422</guid>

					<description><![CDATA[1987年，她出生在河北廊坊一个普通农村。家境贫寒，童年没有玩具，也没有新衣。父母是老实巴交的农民，她的成长记 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>1987年，她出生在河北廊坊一个普通农村。家境贫寒，童年没有玩具，也没有新衣。父母是老实巴交的农民，她的成长记忆里，全是干不完的农活和田间的汗水。</p>



<p>六岁那年，她在黑白电视里第一次看到演员的光鲜生活，心里像被点燃了一把火。她渴望站上那个舞台，摆脱命运的束缚。但梦想太贵，家里太穷，只能埋在心底。</p>



<figure class="wp-block-image size-large"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-6.png"><img decoding="async" width="1024" height="600" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-6-1024x600.png" alt="" class="wp-image-3429" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-6-1024x600.png 1024w, https://www.80shihua.com/wp-content/uploads/2025/08/image-6-300x176.png 300w, https://www.80shihua.com/wp-content/uploads/2025/08/image-6-768x450.png 768w, https://www.80shihua.com/wp-content/uploads/2025/08/image-6-1536x900.png 1536w, https://www.80shihua.com/wp-content/uploads/2025/08/image-6-2048x1200.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></figure>



<p>初中毕业后，家里无力供她读艺术学校。她转而选择中专空乘专业，听说毕业能包分配工作。可现实很快打碎了幻想——航空公司根本不招中专生，她连空姐的边都没摸着。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-1.png"><img loading="lazy" decoding="async" width="275" height="447" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-1.png" alt="" class="wp-image-3424" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-1.png 275w, https://www.80shihua.com/wp-content/uploads/2025/08/image-1-185x300.png 185w" sizes="(max-width: 275px) 100vw, 275px" /></a></figure></div>


<p>为了生活，她四处打工：发传单、做服务员、在美容院当小妹。有一次因为不会推销产品，被客人当众骂哭。19岁那年，她在一家小公司当前台，月薪仅800元。老板当着众人面讽刺她是“乡下丫头”，注定没出息。她强忍眼泪，晚上却在出租屋的厕所里，把水龙头开到最大，哭得撕心裂肺。</p>



<figure class="wp-block-image size-large is-resized"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-9.png"><img loading="lazy" decoding="async" width="819" height="1024" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-9-819x1024.png" alt="" class="wp-image-3432" style="width:844px;height:auto" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-9-819x1024.png 819w, https://www.80shihua.com/wp-content/uploads/2025/08/image-9-240x300.png 240w, https://www.80shihua.com/wp-content/uploads/2025/08/image-9-768x961.png 768w, https://www.80shihua.com/wp-content/uploads/2025/08/image-9.png 1124w" sizes="(max-width: 819px) 100vw, 819px" /></a></figure>



<p>2006年，她看到雅虎搜星选秀的广告，毫不犹豫报名。没有专业训练，她就对着镜子练表情，用床单当礼服练走台步。比赛当天，她拿下冯小刚组冠军，以为终于迎来转机。可娱乐圈的现实，像一盆冷水泼了下来。</p>



<div class="wp-block-cover aligncenter" style="min-height:100vh"><span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span><img loading="lazy" decoding="async" width="307" height="451" class="wp-block-cover__image-background wp-image-3425" alt="" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-2.png" data-object-fit="cover" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-2.png 307w, https://www.80shihua.com/wp-content/uploads/2025/08/image-2-204x300.png 204w" sizes="(max-width: 307px) 100vw, 307px" /><div class="wp-block-cover__inner-container is-layout-flow wp-block-cover-is-layout-flow">
<p class="has-text-align-center has-large-font-size"></p>
</div></div>



<p>签约后，她成了公司里的“透明人”。没有戏拍、没人重视，经纪人直言她圆脸不适合演主角，只能演丫鬟。她不服，却只能从最小的角色演起——《金婚》里的小女儿、《锁清秋》里的丫鬟。片场没人记得她的名字，场记喊她“那个谁”，导演甚至直接跳过她的戏份。</p>



<p>那段时间，她连房租都交不起，馒头蘸老干妈成了日常。有人劝她改行，说她不适合娱乐圈。她偏不认输，咬牙坚持。2013年，《陆贞传奇》爆红，她终于有了姓名。但随之而来的不是鲜花和掌声，而是铺天盖地的恶意。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-3.png"><img loading="lazy" decoding="async" width="348" height="549" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-3.png" alt="" class="wp-image-3426" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-3.png 348w, https://www.80shihua.com/wp-content/uploads/2025/08/image-3-190x300.png 190w" sizes="(max-width: 348px) 100vw, 348px" /></a></figure></div>


<p>有人骂她土气、不配演主角；有人造谣她学历造假、背景不干净；甚至有弹幕刷“赵丽颖去死”。合作演员在采访中暗讽她没文化，她在化妆间掐着手心，疼得眼泪打转。她知道，停下来就真的输了。</p>



<p>她憋着一口气，拼命拍戏。《楚乔传》里，40度高温的沙漠戏，她不用替身，被拖行到满身淤青；《知否》里一场落水戏，她反复跳了十几次，高烧39度仍坚持拍摄。她的努力，终于让骂声变成掌声。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-4.png"><img loading="lazy" decoding="async" width="298" height="650" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-4.png" alt="" class="wp-image-3427" style="width:272px;height:auto" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-4.png 298w, https://www.80shihua.com/wp-content/uploads/2025/08/image-4-138x300.png 138w" sizes="(max-width: 298px) 100vw, 298px" /></a></figure></div>


<p>《花千骨》打破收视纪录，《风吹半夏》让她的演技被广泛认可。她用实力撕碎“圆脸演不了主角”的偏见。如今，她被誉为“收视女王”，拿下金鹰奖和白玉兰奖提名。但她从未忘记自己的根。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-5.png"><img loading="lazy" decoding="async" width="282" height="515" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-5.png" alt="" class="wp-image-3428" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-5.png 282w, https://www.80shihua.com/wp-content/uploads/2025/08/image-5-164x300.png 164w" sizes="(max-width: 282px) 100vw, 282px" /></a></figure></div>


<p>每次回老家，她仍会蹲在田埂上啃黄瓜，和村里的孩子聊天。她告诉他们：出身低不可怕，只要敢拼，老天总会给你机会。她用自己的故事证明——努力真的能改变命运。</p>



<p>她的经历，像极了我们身边那些为生活打拼的人。谁没遇过挫折？谁没被质疑过？但她没低头，硬是靠自己闯出一条路。数据显示，《花千骨》最高收视率达3.89%，网络播放量突破200亿。这不是运气，是她一步步拼出来的。</p>



<p>娱乐圈不缺光鲜亮丽的故事，但她的故事格外动人。因为她不只是明星，更像我们每一个普通人——有过低谷、有过不被看好。但她告诉我们：只要不放弃，时间终会看到你的努力。</p>



<figure class="wp-block-image size-large"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-7.png"><img loading="lazy" decoding="async" width="819" height="1024" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-7-819x1024.png" alt="" class="wp-image-3430" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-7-819x1024.png 819w, https://www.80shihua.com/wp-content/uploads/2025/08/image-7-240x300.png 240w, https://www.80shihua.com/wp-content/uploads/2025/08/image-7-768x960.png 768w, https://www.80shihua.com/wp-content/uploads/2025/08/image-7-1229x1536.png 1229w, https://www.80shihua.com/wp-content/uploads/2025/08/image-7.png 1440w" sizes="(max-width: 819px) 100vw, 819px" /></a></figure>



<p>如今，她站在聚光灯下，成了无数人心中的榜样。但她依然保持初心，回到村里，还是那个朴实的农村姑娘。她的故事没有教条，却让人忍不住鼓掌。</p>



<figure class="wp-block-image size-large"><a href="https://www.80shihua.com/wp-content/uploads/2025/08/image-8.png"><img loading="lazy" decoding="async" width="819" height="1024" src="https://www.80shihua.com/wp-content/uploads/2025/08/image-8-819x1024.png" alt="" class="wp-image-3431" srcset="https://www.80shihua.com/wp-content/uploads/2025/08/image-8-819x1024.png 819w, https://www.80shihua.com/wp-content/uploads/2025/08/image-8-240x300.png 240w, https://www.80shihua.com/wp-content/uploads/2025/08/image-8-768x960.png 768w, https://www.80shihua.com/wp-content/uploads/2025/08/image-8-1229x1536.png 1229w, https://www.80shihua.com/wp-content/uploads/2025/08/image-8.png 1440w" sizes="(max-width: 819px) 100vw, 819px" /></a></figure>



<p>你是否也曾觉得自己起点低、机会少？她的故事就像一面镜子，照见我们心里的那团火。只要你敢往前走，生活终会给你一个答案。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Jest 测试框架实战指南：深入理解 jest.spyOn() 和 console.error mock 技巧</title>
		<link>https://www.80shihua.com/archives/3420</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Fri, 27 Jun 2025 03:30:57 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3420</guid>

					<description><![CDATA[在 Node.js 或前端项目中编写高质量的单元测试时，Jest 几乎成了默认选择。其开箱即用的特性、强大的  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>在 Node.js 或前端项目中编写高质量的单元测试时，Jest 几乎成了默认选择。其开箱即用的特性、强大的 Mock 能力以及直观的语法，使其成为开发者的最爱。本文将围绕两个常见但容易被忽视的技巧展开：如何使用 <code>jest.spyOn()</code> 精准捕捉函数行为，以及如何 mock <code>console.error()</code>，避免测试被“误判”。</p>



<h2 class="wp-block-heading">为什么选择 Jest？</h2>



<p>Jest 是一个由 Meta（原 Facebook）开发的测试框架，支持：</p>



<ul>
<li>零配置快速上手</li>



<li>内建 Mock 和断言工具</li>



<li>支持 Promise / async/await 的异步测试</li>



<li>提供详细的测试覆盖率报告</li>
</ul>



<h2 class="wp-block-heading"> 聚焦：<code>jest.spyOn()</code> 的强大用法</h2>



<p><code>jest.spyOn()</code> 是测试中<strong>监听对象方法调用行为</strong>的利器，尤其适用于那些带副作用或不便直接验证的方法，比如日志、事件触发、外部依赖调用等。</p>



<h3 class="wp-block-heading">基础使用</h3>



<p></p>



<pre class="wp-block-code"><code>const utils = {
  greet(name) {
    console.log(`Hello, ${name}`);
  }
};

test('should call greet with correct name', () =&gt; {
  const spy = jest.spyOn(utils, 'greet');

  utils.greet('梦回');

  expect(spy).toHaveBeenCalled();                 // 是否被调用
  expect(spy).toHaveBeenCalledWith('梦回');       // 调用参数是否正确

  spy.mockRestore(); // 恢复原始实现
});
</code></pre>



<h3 class="wp-block-heading">替换实现（Mock Implementation）</h3>



<p></p>



<pre class="wp-block-code"><code>jest.spyOn(utils, 'greet').mockImplementation(name =&gt; {
  return `Hi, ${name}`;
});
</code></pre>



<p>这让我们可以脱离真实逻辑，在测试中定制行为，避免副作用影响测试结果。</p>



<h2 class="wp-block-heading"> 防止测试“假失败”：Mock <code>console.error</code></h2>



<p>在一些测试场景中，某些第三方库或内部逻辑可能会抛出警告（如 React 的 prop 类型错误），即使测试本身是成功的，却会因为 <code>console.error</code> 输出而<strong>误判为测试失败</strong>或污染输出结果。</p>



<h3 class="wp-block-heading"> 解决方案：临时 Mock <code>console.error</code></h3>



<pre class="wp-block-code"><code>beforeEach(() =&gt; {
  jest.spyOn(console, 'error').mockImplementation(() =&gt; {});
});

afterEach(() =&gt; {
  console.error.mockRestore();
});
</code></pre>



<p>这段代码做了两件事：</p>



<ul>
<li>使用 <code>spyOn</code> 监听 <code>console.error</code>，并替换为一个空函数，避免实际输出。</li>



<li>在每个测试后恢复默认行为，确保不会影响其他逻辑。</li>
</ul>



<p>这种方法特别适用于：</p>



<ul>
<li>你在测试某个组件或方法时，预期它会触发警告，但不影响功能正确性。</li>



<li>使用了旧版依赖导致的 log 输出较多，但暂无法移除。</li>
</ul>



<p>&#x1f4a1; <strong>Tip</strong>：切记只在测试中使用，不建议污染生产环境。</p>



<h2 class="wp-block-heading">小结</h2>



<figure class="wp-block-table"><table><thead><tr><th>工具/技巧</th><th>用途</th><th>好处</th></tr></thead><tbody><tr><td><code>jest.spyOn()</code></td><td>监听对象方法的调用情况</td><td>精准验证函数调用次数和参数</td></tr><tr><td><code>mockImplementation</code></td><td>替代方法实现</td><td>便于隔离副作用、构造特定测试场景</td></tr><tr><td><code>mockRestore()</code></td><td>恢复原始方法</td><td>保持测试间的独立性</td></tr><tr><td>Mock <code>console.error()</code></td><td>避免测试过程中的误报</td><td>让测试报告更干净，识别真实失败</td></tr></tbody></table></figure>



<p>如果你在构建自己的自动化测试体系，<code>spyOn</code> 和 <code>console.error</code> mock 是必须掌握的两项技巧。写得好的测试不仅能验证功能，也让你对系统的行为了然于心。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>rust配置国内源</title>
		<link>https://www.80shihua.com/archives/3418</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Mon, 09 Jun 2025 14:33:52 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3418</guid>

					<description><![CDATA[在用户家目录下面，进入.cargo文件夹，然后创建config.toml文件 文件内容如下：]]></description>
										<content:encoded><![CDATA[
<p>在用户家目录下面，进入.cargo文件夹，然后创建config.toml文件</p>



<p>文件内容如下：</p>



<pre class="wp-block-code"><code>&#91;source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = '镜像源名' # 如：tuna、sjtu、ustc，或者 rustcc

# 注：以下源配置一个即可，无需全部

# 中国科学技术大学
&#91;source.ustc]
registry = "https://mirrors.ustc.edu.cn/crates.io-index"
# >>> 或者 &lt;&lt;&lt;
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大学
&#91;source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index/"

# 清华大学
&#91;source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# rustcc社区
&#91;source.rustcc]
registry = "https://code.aliyun.com/rustcc/crates.io-index.git"</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>深入解析：.user.ini 文件如何导致 PHP 权限访问问题</title>
		<link>https://www.80shihua.com/archives/3417</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Fri, 06 Jun 2025 10:11:15 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3417</guid>

					<description><![CDATA[在 PHP 应用的部署和开发过程中，我们经常会遇到各种权限问题，其中一个不常被提及但又可能导致棘手问题的元凶就 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>在 PHP 应用的部署和开发过程中，我们经常会遇到各种权限问题，其中一个不常被提及但又可能导致棘手问题的元凶就是 <code>.user.ini</code> 文件。尽管它旨在提供灵活的 PHP 配置管理，但其不当使用或在特定环境下的行为，却可能成为权限问题的根源。</p>



<h3 class="wp-block-heading">什么是 <code>.user.ini</code> 文件？</h3>



<p>首先，我们来回顾一下 <code>.user.ini</code> 的作用。</p>



<p>从 PHP 5.3 开始，PHP 引入了对每个目录进行 <code>.ini</code> 配置的支持。这个功能通过 <code>user_ini.filename</code>（默认是 <code>.user.ini</code>）和 <code>user_ini.cache_ttl</code> 这两个 <code>php.ini</code> 指令来控制。</p>



<p>它的主要目的是允许非特权用户（例如共享主机环境下的用户）在不修改主 <code>php.ini</code> 文件的情况下，覆盖或设置某些 PHP 配置指令。这些指令通常包括 <code>display_errors</code>、<code>upload_max_filesize</code>、<code>memory_limit</code> 等，使得开发者可以对特定目录下的 PHP 行为进行细粒度控制。</p>



<p>当 PHP-FPM 或 Apache/Nginx 等 Web 服务器处理一个请求时，它会从当前执行脚本的目录开始向上查找 <code>.user.ini</code> 文件，直到达到文档根目录或更高层级。找到的 <code>.user.ini</code> 文件中的指令会覆盖之前设置的指令。</p>



<h3 class="wp-block-heading"><code>.user.ini</code> 导致权限问题的原理</h3>



<p>那么，这样一个看似方便的功能，为什么会成为权限问题的诱因呢？这主要发生在以下几种情况：</p>



<ol>
<li><strong>文件所有者与 PHP 进程用户不匹配：</strong>
<ul>
<li><strong>常见场景：</strong> 在 Docker 环境中，尤其是在 Windows 或 macOS 上通过 <code>bind mount</code> 挂载本地代码目录时，或者在 Linux 服务器上手动上传代码后。</li>



<li><strong>问题所在：</strong> <code>.user.ini</code> 文件本身是应用程序代码的一部分。如果你的应用程序文件（包括 <code>.user.ini</code>）在宿主机上由用户 <code>youruser</code> 创建，然后通过绑定挂载进入 Docker 容器，或者直接上传到 Linux 服务器上。而容器内部运行 PHP-FPM（或 Apache/Nginx 的 worker 进程）的用户是 <code>www-data</code> (UID 33) 或其他非 <code>root</code> 用户。</li>



<li><strong>结果：</strong> 当 PHP 尝试读取 <code>.user.ini</code> 文件时，如果 <code>www-data</code> 用户对这个 <code>.user.ini</code> 文件没有 <strong>读取权限</strong>，PHP 将无法解析其中的配置。虽然 PHP 可能不会直接报错 &#8220;Permission Denied for .user.ini&#8221;，但它会<strong>回退到主 <code>php.ini</code> 或默认配置</strong>，并且可能因为缺少关键配置（例如 <code>open_basedir</code>、错误报告设置等）而导致后续的应用程序逻辑执行失败，或者表现出奇怪的行为。更糟的是，如果 PHP <strong>无法访问该文件</strong>，它可能会直接中断执行，因为无法确定是否允许当前目录执行 PHP 脚本。</li>
</ul>
</li>



<li><strong><code>open_basedir</code> 指令的冲突：</strong>
<ul>
<li><strong><code>open_basedir</code> 的作用：</strong> 这是一个重要的安全指令，用于限制 PHP 脚本可以访问的文件系统路径。它会创建一个“沙盒”，规定 PHP 脚本只能在指定的目录及其子目录中操作。</li>



<li><strong>问题所在：</strong> 如果主 <code>php.ini</code> 或更上层的 <code>.user.ini</code> 设置了一个 <code>open_basedir</code> 路径，而你当前目录的 <code>.user.ini</code> 又想访问其他不在 <code>open_basedir</code> 允许范围内的文件，就会触发 <code>open_basedir</code> 限制，导致 &#8220;Permission Denied&#8221; 或 &#8220;Operation not permitted&#8221; 错误。</li>



<li><strong>隐蔽性：</strong> 最棘手的情况是，你可能在某个调试过程中，为了测试某个功能在某个子目录创建了一个临时的 <code>.user.ini</code> 并设置了 <code>open_basedir</code>，然后忘记删除或修改。当其他 PHP 脚本在同一个子目录被执行时，就会受到这个限制的影响。</li>
</ul>
</li>



<li><strong>错误的 PHP 配置指令：</strong>
<ul>
<li><strong>问题所在：</strong> 如果 <code>.user.ini</code> 中包含了语法错误、无效的指令，或者指令值不符合预期（例如，将 <code>memory_limit</code> 设置为负数），PHP 在解析这个文件时可能会失败。</li>



<li><strong>结果：</strong> PHP 可能会报错并停止执行，或者干脆忽略该文件中的所有配置，导致应用程序行为异常。虽然这不直接是权限问题，但错误的配置可能导致一些资源访问失败，从而间接表现为“权限”问题。</li>
</ul>
</li>



<li><strong><code>.user.ini</code> 文件本身的可执行权限问题 (不常见，但可能)：</strong>
<ul>
<li>虽然 <code>.user.ini</code> 是一个配置文件，通常不需要可执行权限。但在某些不寻常的配置或极端的权限设置下，如果它意外地获得了可执行权限，或者其父目录的权限设置阻止了 PHP 进程对其进行“读取”，也可能导致问题。不过，这通常是 <code>chmod</code> 设置错误而非 <code>.user.ini</code> 本身的问题。</li>
</ul>
</li>
</ol>



<h3 class="wp-block-heading">如何诊断和解决 <code>.user.ini</code> 导致的权限问题</h3>



<p>诊断 <code>.user.ini</code> 导致的权限问题需要一定的细心和系统性。</p>



<ol>
<li><strong>检查文件权限和所有者：</strong>
<ul>
<li><strong>在容器内部（如果使用 Docker）：</strong> 进入你的 PHP 容器 (<code>docker-compose exec php bash</code>)，然后导航到你的项目根目录。</li>



<li>执行 <code>ls -l .user.ini</code>。</li>



<li>确认 <code>.user.ini</code> 的所有者和组是否与 PHP 进程运行的用户（通常是 <code>www-data</code>）匹配，或者确保该用户对 <code>.user.ini</code> 拥有至少 <strong>读（r）权限</strong>。</li>



<li>如果权限不正确，使用 <code>chmod</code> 命令修复。例如，如果 <code>.user.ini</code> 是 <code>root</code> 拥有，而 PHP 进程是 <code>www-data</code>，你可以尝试： Bash<code>chmod 644 .user.ini # 保证 www-data 可读 chown www-data:www-data .user.ini # 如果想更严格，改变所有者</code> 对于 Windows/macOS 上的 <code>bind mount</code>，你可能需要对整个项目目录执行 <code>chmod -R 777</code> 来解决权限问题，或者如前所述，在 <code>Dockerfile</code> 中将 PHP-FPM 运行用户设置为 <code>root</code>（仅限开发环境）。</li>
</ul>
</li>



<li><strong>检查 <code>open_basedir</code> 配置：</strong>
<ul>
<li><strong>运行时检查：</strong> 在你的 PHP 脚本中，添加 <code>phpinfo();</code> 并访问该页面。查找 <code>open_basedir</code> 指令。</li>



<li><strong>手动检查：</strong> 检查你的主 <code>php.ini</code> 文件、各个 <code>php-fpm</code> pool 的配置文件，以及你应用目录层级中的所有 <code>.user.ini</code> 文件，看是否有 <code>open_basedir</code> 的设置。</li>



<li><strong>解决：</strong> 确保所有 <code>open_basedir</code> 的设置都包含了你的应用程序需要访问的所有目录。如果发现冲突，移除不必要的 <code>open_basedir</code> 限制，或调整其路径以包含所需目录。</li>
</ul>
</li>



<li><strong>检查 <code>.user.ini</code> 语法和内容：</strong>
<ul>
<li><strong>语法检查：</strong> 仔细检查 <code>.user.ini</code> 文件中的所有指令，确保没有拼写错误、非法字符或不正确的格式。</li>



<li><strong>指令有效性：</strong> 确认文件中使用的指令都是 PHP 允许在 <code>.user.ini</code> 中设置的（可以通过查阅 PHP 官方文档）。某些核心指令是不能在 <code>.user.ini</code> 中覆盖的。</li>
</ul>
</li>



<li><strong>临时删除 <code>.user.ini</code> 进行测试：</strong>
<ul>
<li>最直接的验证方法是，<strong>暂时将你的 <code>.user.ini</code> 文件移出项目目录或重命名</strong>（例如 <code>mv .user.ini .user.ini.bak</code>）。</li>



<li>然后清除 PHP-FPM 缓存（如果适用，重启 PHP-FPM 服务），并重新访问你的应用。</li>



<li>如果问题消失，那么问题就出在 <code>.user.ini</code> 上。你可以逐步恢复其中的指令，找出是哪个指令导致的问题。</li>
</ul>
</li>



<li><strong>查看 PHP 错误日志：</strong>
<ul>
<li>即使没有直接的权限错误，PHP 日志（通常在 Nginx/Apache 的 <code>error.log</code> 或 PHP-FPM 的 <code>error.log</code> 中）也可能包含有关 <code>.user.ini</code> 解析失败的警告或错误信息。</li>
</ul>
</li>
</ol>



<h3 class="wp-block-heading">总结</h3>



<p><code>.user.ini</code> 文件是一个强大的工具，可以为 PHP 应用程序提供灵活的配置管理。然而，在文件所有权、权限设置、<code>open_basedir</code> 限制以及不正确的指令使用方面，它也可能成为导致 PHP 权限访问问题的隐蔽源头。</p>



<p>理解 <code>.user.ini</code> 的工作原理，并在遇到权限问题时将其作为一个潜在的检查点，将大大提高你诊断和解决 PHP 应用部署难题的效率。在生产环境中，要格外谨慎地使用 <code>.user.ini</code>，并确保其权限和内容都经过严格审查。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>进入 PHP 容器后提示 ps: command not found，该怎么办？</title>
		<link>https://www.80shihua.com/archives/3416</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Fri, 06 Jun 2025 04:10:52 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3416</guid>

					<description><![CDATA[在使用 Docker 容器运行 PHP 服务时，开发者常常会进入容器内部进行调试或查看运行状态。例如，使用命令 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>在使用 Docker 容器运行 PHP 服务时，开发者常常会进入容器内部进行调试或查看运行状态。例如，使用命令：</p>



<p></p>



<p>docker exec -it &lt;container_name> bash</p>



<p>进入容器后，习惯性地执行 <code>ps</code> 命令来查看进程信息：</p>



<p></p>



<p>ps aux</p>



<p>但是有时会遇到提示：</p>



<p></p>



<p>bash: ps: command not found</p>



<p>这是什么原因呢？</p>



<h2 class="wp-block-heading">原因分析</h2>



<p><code>ps</code> 是 Linux 系统中用于查看进程状态的一个基础命令，它属于 <code>procps</code> 工具包（或 <code>procps-ng</code> 包）。这个工具包包含了一组用于查看和管理系统进程的命令，如 <code>ps</code>, <code>top</code>, <code>free</code> 等。</p>



<p>然而，Docker 镜像往往会选择尽可能轻量化，减少不必要的软件包体积。例如，官方的 <code>php:8.4-fpm</code> 镜像，基于 Debian 或 Alpine 这样的精简版系统，默认并不包含完整的系统工具包，尤其是不常用于生产环境的命令行工具。</p>



<p>因此，在轻量级 PHP 容器中默认没有安装 <code>procps</code>，执行 <code>ps</code> 就会出现“command not found”的错误。</p>



<h2 class="wp-block-heading">如何解决？</h2>



<h3 class="wp-block-heading">方案一：安装 <code>procps</code> 工具包</h3>



<p>进入容器后，通过包管理工具安装 <code>procps</code>，即可恢复 <code>ps</code> 命令的使用。</p>



<ul>
<li>对于基于 Debian/Ubuntu 的镜像：</li>
</ul>



<p></p>



<p>apt-get update &amp;&amp; apt-get install -y procps</p>



<ul>
<li>对于基于 Alpine 的镜像：</li>
</ul>



<p></p>



<p>apk add &#8211;no-cache procps</p>



<p>安装完成后，即可正常使用 <code>ps</code> 命令。</p>



<h3 class="wp-block-heading">方案二：构建时自定义镜像，提前安装</h3>



<p>如果你频繁需要使用 <code>ps</code> 命令，可以在 Dockerfile 里添加安装步骤：</p>



<p></p>



<p><code>FROM php:8.4-fpm RUN apt-get update &amp;&amp; apt-get install -y procps</code></p>



<p>然后重新构建镜像。</p>



<h2 class="wp-block-heading">总结</h2>



<ul>
<li><code>ps</code> 命令属于 <code>procps</code> 工具包，不是 PHP 镜像的必备组件。</li>



<li>轻量化镜像（如 <code>php:8.4-fpm</code>）通常不预装 <code>procps</code>，以减小镜像体积。</li>



<li>遇到 <code>ps: command not found</code>，通过包管理器安装 <code>procps</code> 即可解决。</li>



<li>推荐在自定义镜像中提前安装，避免每次进入容器都要临时安装。</li>
</ul>



<p>这样，你就能顺利使用 <code>ps</code> 命令查看容器内的进程状态，方便调试和管理。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>nginx 代理缓存</title>
		<link>https://www.80shihua.com/archives/3414</link>
		
		<dc:creator><![CDATA[dreamfly]]></dc:creator>
		<pubDate>Fri, 09 May 2025 07:29:49 +0000</pubDate>
				<category><![CDATA[个人博客]]></category>
		<guid isPermaLink="false">https://www.80shihua.com/?p=3414</guid>

					<description><![CDATA[最近在开发一个ghost项目，使用nginx作为反向代理，开发的时候遇到了一个诡异问题，在此记录一下，那就是当 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>最近在开发一个ghost项目，使用nginx作为反向代理，开发的时候遇到了一个诡异问题，在此记录一下，那就是当错误配置了ghost routes.yml导致301之后，nginx就会缓存下来，一开始以为是浏览器缓存问题，然而使用curl -I 请求之后，301还是存在，因此可以排除是浏览器缓存问题。</p>



<p>在修改了ghost routes.yml之后，301问题还是存在，期间重启了nginx也是不起作用，更奇怪的是当我把ghost服务停止了，应该理论上所有的页面访问都是502，但是301页面依然纹丝不动，还是返回301，到这里，基本上可以确定问题就是出现在nginx上面了，并且因该是nginx缓存了301，接下来就是去nginx的目录看看哪里配置了缓存，或者有缓存文件夹，发现了proxy_cache_dir，看起来应该就是缓存，于是大胆删除了里面的内容，但是没起作用，于是重启了nginx，然后301不见了。</p>



<p>缓存的存在，对于性能的提升有巨大的帮助，但是在开发的时候也给我们带来了很多困扰，小道浏览器缓存，大道服务器缓存，dns缓存，cdn缓存，可能一个缓存问题，就会导致我们对自己的程序产生无尽的怀疑，其实本质上，还是我们对于底层的不深刻理解，如果理解的话，就可以快速定位到产生问题的地方，接下来就是解决问题了。</p>



<p>在计算机界有个定理，我们觉得问题很难解决的时候，往往多添加一个层就解决了，而我们排查问题就是反过来，去掉一个层或者多个层，也就解决了。</p>



<p>努力，奋斗，301nginx缓存！！！</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
