﻿<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="/atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Ryonnoski0204</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://ryonnoski.com/</id>
  <link href="https://ryonnoski.com/" rel="alternate"/>
  <link href="https://ryonnoski.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Ryonnoski0204</rights>
  <subtitle>记录技术与思考</subtitle>
  <title>Ryon 的博客</title>
  <updated>2026-05-28T12:04:43.831Z</updated>
  <entry>
    <author>
      <name>Ryonnoski0204</name>
    </author>
    <category term="随笔" scheme="https://ryonnoski.com/categories/essay/"/>
    <category term="观察" scheme="https://ryonnoski.com/tags/commentary/"/>
    <category term="互联网" scheme="https://ryonnoski.com/tags/internet/"/>
    <category term="游戏" scheme="https://ryonnoski.com/tags/games/"/>
    <content>
      <![CDATA[<p>2026 年 5 月下旬，一款日活上千万的国民级游戏，因为一个「战斗失败后的坐姿」陷入了上线以来最大的风波。短短几天里，官方账号下涌进二十多万条留言，应用商店评分从 6.5 跌到 2.8，主策划连发三次道歉、写下四千字长信。事情听起来很魔幻：一个坐姿，怎么会演变成这么大的事？</p><p>这篇文章想做的，是把这场风波尽量完整、尽量中立地复盘一遍——它到底改了什么、官方怎么解释、所谓「暗改」这个词准不准确，以及它背后真正值得讨论的问题是什么。不站队，只把事实和不同的解读都摆出来，让你自己判断。涉及一点技术原因，我会用生活里的例子讲清楚，不写代码也能看懂。</p><blockquote><p>说明：本文基于截至 2026 年 5 月底的公开报道与官方公告整理，事件仍在进展中，后续若有新信息，结论可能需要修正。文末附全部参考来源。</p></blockquote><h2 id="一、先把背景交代清楚"><a href="#一、先把背景交代清楚" class="headerlink" title="一、先把背景交代清楚"></a>一、先把背景交代清楚</h2><p>《洛克王国：世界》是腾讯旗下的一款捉宠养成类游戏，2026 年 3 月 26 日正式上线，日活跃用户一度突破 <strong>1300 万</strong>，是当之无愧的现象级产品。</p><p>游戏里，玩家可以创建男号或女号。一个开服就存在的设定是：<strong>在 PvP（玩家对战）失败后，女角色会做出一个「鸭子坐」——跪坐在地、略显沮丧的动作；而男角色只是站着叉腰、垂头丧气。</strong> 这个「鸭子坐」因为造型可爱，被不少玩家当成游戏的标志性细节之一。</p><p>也正是从开服起，就有少数玩家提出过不同意见：有人觉得「女角色跪坐哭、男角色只是站着」的差异，隐含了对女性的刻板印象，建议把男女动作统一。这个声音一直存在，但在很长一段时间里并没有掀起大浪。</p><p>这是理解整件事的前提：<strong>争议的「火种」早就埋下了，只差一个点火的瞬间。</strong></p><h2 id="二、导火索：S2-赛季的「悄悄改动」"><a href="#二、导火索：S2-赛季的「悄悄改动」" class="headerlink" title="二、导火索：S2 赛季的「悄悄改动」"></a>二、导火索：S2 赛季的「悄悄改动」</h2><p>2026 年 5 月 21 日，游戏更新了 S2 赛季。玩家很快发现：<strong>女角色失败时的「鸭子坐」不见了，被改成了和男角色类似的「叉腰站立」——而这个改动，官方长达三万字的更新公告里只字未提。</strong></p><p>更让玩家不安的是，随着大家互相比对，被发现「悄悄变了」的远不止这一处。根据玩家社区的整理，前后大约有 <strong>近 30 处</strong>改动，比如：</p><ul><li>炼金结算时标志性的「甩手」动作，被改成了「拍手」（或被删除）；</li><li>坐骑「奇丽花」的骑乘姿势变了，被吐槽「不够优雅」；</li><li>精灵「暮星辰」的睫毛特效被移除；</li><li>大量交互小动作被微调，或者直接删掉。</li></ul><p>除了这些看得见的外观改动，还有玩家反映<strong>精灵生态、PvP 数值、道具使用、场景交互</strong>等玩法层面的内容也发生了变化。</p><p>这些改动有一个共同点：<strong>实际改了，但更新公告里都没写，全靠玩家自己一点点扒出来。</strong> 这就是后来被称作「暗改门」的由来。</p><h2 id="三、舆论为什么会爆"><a href="#三、舆论为什么会爆" class="headerlink" title="三、舆论为什么会爆"></a>三、舆论为什么会爆</h2><p>如果只是一个坐姿，大概不会闹这么大。真正让事情失控的，是它同时点燃了好几股情绪：</p><p><strong>第一股，是「被冒犯」的对立情绪。</strong> 改动正好踩在那条早已存在的性别争议线上，于是两边瞬间对立：</p><ul><li>一部分玩家（很多是玩女号的）认为「鸭子坐」可爱又没碍着谁，官方凭什么悄悄改掉，喊出「守护鸭子坐自由」，要求改回去；</li><li>另一部分玩家则说，要改就一视同仁，「男号也得有鸭子坐」，凭什么只动女号。</li></ul><p><strong>第二股，是「不被尊重」的信任崩塌。</strong> 比起改了什么，更让玩家愤怒的是「改了却不说」——三万字公告写得密密麻麻，真正的改动一个没提。玩家觉得自己被当傻子。</p><p><strong>第三股，是「优先级错乱」的质疑。</strong> 有相当多的声音指出：官方对影响核心体验的 PvP bug、卡顿长期没动静，却对外部平台（被一些人称为「小红书治游」）的少量投诉「连夜响应」去改动作——这种观感让人觉得「正事不干，净忙着讨好」。</p><p>三股情绪叠加，结果是：游戏官方在 B 站的账号评论区，留言在一天内突破二十万、几天内逼近三十万条；应用商店 TapTap 的评分，从 6.5 分一路暴跌到 2.8 分上下，三天内新增四千多条差评。</p><h2 id="四、官方的三次回应"><a href="#四、官方的三次回应" class="headerlink" title="四、官方的三次回应"></a>四、官方的三次回应</h2><p>面对失控的舆情，官方的应对大致分三步：</p><ol><li><strong>5 月 22 日</strong>：发布致歉声明，承认改动存在问题，并面向全服发放棱镜球等道具作为补偿（价值约合两千元游戏道具）。</li><li><strong>5 月 24 日</strong>：主策划「开水」亲自发布一封约四千字的长信，详细解释成因、承担责任。</li><li><strong>5 月 26 日</strong>：原定 28 日上线的**「自定义动作」功能提前实装**——玩家可以在设置里自由切换失败动作：「动作 1」是叉腰站立，「动作 2」就是经典的鸭子坐。</li></ol><p>这封四千字长信是整件事的关键，因为它第一次给出了官方视角下「为什么会变成这样」的解释。开水在信里说了几个要点（以下为其原话节选）：</p><ul><li>「由于我们在<strong>版本管理、需求准入</strong>等工作失误引发的多数变动」；</li><li>「<strong>公测时我们没能来得及完成的调整与资源遗留在了版本中</strong>」；</li><li>「一些在 <strong>S1 紧急修复了的问题与数据没能合并回 S2 的版本</strong>」；</li><li>「一些『带病上场』的顽疾在解决过程中产生了<strong>意料之外的影响</strong>」；</li><li>「我们对于优化的决策链条非常短，很多时候，一个反馈刚被提出，我们甚至<strong>来不及真正验证，就会立刻进入修改</strong>」；</li><li>「（我们）严重低估了对大家已经获得的内容进行……修改时所面对的风险」。</li></ul><p>同时，他明确否认了当时流传的「团队被换血、有人空降、背后另有团队」等阴谋论：「<strong>没有换血，没有空降，没有幕后团队，搞出这些问题的人，就是我们自己</strong>。」</p><h2 id="五、先搞懂「版本管理」，再看这次怎么出的事"><a href="#五、先搞懂「版本管理」，再看这次怎么出的事" class="headerlink" title="五、先搞懂「版本管理」，再看这次怎么出的事"></a>五、先搞懂「版本管理」，再看这次怎么出的事</h2><p>要理解官方的解释，得先弄明白一个概念：<strong>版本管理</strong>。</p><p>一款像《洛克王国：世界》这样的游戏，背后是几十上百号人在同时开发，内容成千上万。这么多人、这么多东西，怎么保证不互相覆盖、改错了还能退回去？靠的就是一类叫「<strong>版本管理工具</strong>」的软件（业界最常用的是一款叫 <strong>Git</strong> 的工具）。它大致做三件事：</p><ol><li><strong>存档</strong>：每改一次，就给整个项目拍一张「快照」存下来——谁、在什么时候、改了什么，清清楚楚，随时能查、也能退回任意一个历史版本；</li><li><strong>并行</strong>：允许多个人、多条任务线，各自在一份「<strong>副本</strong>」（行话叫「<strong>分支</strong>」）上同时干活，互不打扰；</li><li><strong>合并</strong>：等各自做完，再把这些副本上的改动<strong>汇总回主线</strong>——这一步叫「<strong>合并（merge）</strong>」。</li></ol><p><strong>第 3 步「合并」，正是最容易出事的环节。</strong> 如果合并时没把所有人的改动都正确对齐，就会发生两种典型事故：<strong>有人的改动莫名其妙丢了</strong>，或者<strong>早就改掉的旧东西又冒了回来</strong>。这次的风波，本质上就是这一步出了问题。</p><p>用一个大家都经历过的场景来打比方：</p><blockquote><p>你和同学合写一篇论文，文件存在一个「<strong>主文档</strong>」里。</p><p>为了分头干活，你俩各自<strong>复制了一份</strong>回去改（这就是「副本／分支」）。你负责<strong>修订第三章里的几个错误</strong>（对应游戏里「S1 阶段的紧急修复」）；同学则在他那份副本上<strong>重排全文格式、做一个大改版</strong>（对应「新赛季 S2」）。</p><p>关键问题来了：同学复制走的那一份，是你<strong>修订第三章之前</strong>的旧版本。他闷头改了几周，根本不知道你这边早就把第三章的错给改好了。</p><p>最后交稿，老师收走的是<strong>同学那份大改版</strong>。于是——你辛辛苦苦修订好的第三章，因为没被「合并」进同学的副本，<strong>又退回成了当初那个错误的版本</strong>；与此同时，同学的副本里还<strong>残留着几段他之前试写、没删干净的草稿</strong>。</p><p>没有人「故意」把第三章改回错的，也没人「故意」留下草稿。这一切只是因为：两份副本各改各的，最后<strong>没合并好就交了上去</strong>。</p></blockquote><p>把这个比方翻译回开水信里的原话：</p><ul><li>「你修订好的第三章又退回成错的」，对应他说的「<strong>一些在 S1 紧急修复了的问题与数据没能合并回 S2 的版本</strong>」；</li><li>「同学副本里残留的草稿」，对应「<strong>公测时没能来得及完成的调整与资源遗留在了版本中</strong>」；</li><li>而整件事的总根源，就是他承认的「<strong>版本管理、需求准入等工作失误</strong>」——说白了，就是上面第 3 步「合并」没做好。</li></ul><p>这种「该合的没合好、导致旧改动复活或新草稿乱入」的事故，是所有用 Git 的团队<strong>最常见的翻车之一</strong>。它的典型特征恰恰就是这次的样子：<strong>一大批互不相关的东西同时变了，而且没人记得自己改过它们。</strong></p><p>这也顺带解开了一个最让玩家愤怒的疑问：<strong>为什么这些改动没写进更新公告？</strong> 答案是——既然没人「决定」要改它们，自然也没人会去为它们写说明。<strong>你没法为一个连自己都没意识到发生了的改动，写一行更新日志。</strong> 这就是为什么「三万字公告」里找不到真正的改动，只能靠玩家一处处扒出来。</p><h2 id="六、那么，「暗改」这个词用得准确吗？"><a href="#六、那么，「暗改」这个词用得准确吗？" class="headerlink" title="六、那么，「暗改」这个词用得准确吗？"></a>六、那么，「暗改」这个词用得准确吗？</h2><p>这是整件事里最值得掰扯、也最容易被情绪带跑的一点。</p><p>在玩家的语境里，「暗改」通常同时包含两层意思：<strong>改了 + 故意瞒着</strong>。它暗含了一个「<strong>意图</strong>」——你是存心改的，还存心不让我知道。</p><p>但如果上面的成因属实，更准确的描述其实是：<strong>改了（确实变了）+ 并非故意隐瞒（团队自己都没完全掌握）。</strong> 这两种性质天差地别：前者是「<strong>欺骗</strong>」，后者是「<strong>失职</strong>」。</p><p>到底是哪一种？外部无法百分百断定，但可以把两边的依据都摆出来：</p><p><strong>倾向「这是事故」的理由：</strong></p><ul><li>改动波及面极广、且大多互不相关——炼金动作、坐骑姿势、精灵睫毛特效等一大批和争议毫无关系的内容都跟着变了。一次有目的的改动，不会顺手去动这些八竿子打不着的地方。这种「无差别扩散」是合并事故的典型指纹。</li><li>「PvP bug 重现」和「动作被改」很可能是<strong>同一个原因的两个症状</strong>：S1 修好的东西没合并回 S2，于是「修复丢了（bug 重现）」和「旧状态复活（动作变了）」一起发生。这比「他们故意放着 bug 不管、专心改动作」更省事，也更自洽。</li><li>开水信中给出的技术细节（合并、遗留、紧急修复未回合），与外部能观察到的现象彼此吻合，不是空口辩解。</li></ul><p><strong>让一部分玩家坚持「这是故意」的理由：</strong></p><ul><li>改动方向「<strong>过于精准</strong>」——偏偏改在全游戏最敏感的那个资产上，而且方向正好是早有人要求过的（去掉鸭子坐）。随机的事故，怎么会这么巧正中靶心？</li><li>「无视核心 bug、却快速响应外部投诉」的观感（即「小红书治游」之说），让人怀疑背后有取舍和倾向。</li><li>事后的自定义方案里，<strong>默认动作被设成了「叉腰站立」，而不是把「鸭子坐」恢复成原样</strong>。如果纯粹是误改想回退，按理该恢复原状；把那个「出事的姿势」立为新默认，让一些人怀疑团队本就倾向那个方向。</li></ul><p><strong>以及，外部认知的边界：</strong> 我们看不到内部的代码和提交记录，<strong>无法逐一判定每处改动究竟是「纯粹的回归」，还是「曾被有意做出、又意外上线」</strong>。道歉信是当事方的说法，本身有把责任归于「事故」的动机；但它给出的细节又与现象互相印证，并非毫无根据。</p><p>所以一个相对克制的结论是：<strong>就大多数改动而言，「合并事故」的解释证据更强；但就「鸭子坐」这一处而言，「是否掺杂了某种意图」无法从外部彻底证实或证伪。</strong> 用「暗改」一词一刀切地给整件事定性为「存心欺骗」，至少是不严谨的。</p><h2 id="七、被坐姿盖过去的，才是真问题"><a href="#七、被坐姿盖过去的，才是真问题" class="headerlink" title="七、被坐姿盖过去的，才是真问题"></a>七、被坐姿盖过去的，才是真问题</h2><p>值得提醒的是：<strong>即便完全采信「这是事故」，也绝不等于团队没有责任。</strong> 只是责任的性质变了——从「坏心眼」变成了「能力与流程上的重大缺陷」：</p><ul><li>一支团队，连自己这一版到底改了什么都说不清楚，就敢把它推上线；</li><li>出事后，也拿不出一份完整的「改动前后对照表」让玩家核对；</li><li>发布前缺乏有效的校验环节，沟通又严重滞后。</li></ul><p>换句话说，<strong>这件事里最确凿、也最该被追责的，其实是「版本管理失控 + 缺乏发布校验 + 沟通迟缓」这套工程与流程问题。</strong> 它实打实地损害了玩家利益（包括那些被回退的玩法&#x2F;数值改动，直接影响公平和付费体验），却因为「不够上头」「没法站队」，在舆论里几乎被「坐姿之争」彻底盖了过去。</p><p>这是不少事后复盘都提到的一个观察：<strong>当一个话题足够能挑动对立，它往往会把真正重要、却更「枯燥」的问题挤出公众视野。</strong></p><h2 id="八、一点收尾的思考"><a href="#八、一点收尾的思考" class="headerlink" title="八、一点收尾的思考"></a>八、一点收尾的思考</h2><p>把整件事拉远了看，它其实是一个挺典型的样本：<strong>一个大概率源于技术与流程失误的事故，在传播中被逐渐安上了一个「有意图的动机」。</strong></p><p>原因不难理解——人很难对着一个「版本合并失误」愤怒，那既不具体、也无从发泄、更没法借它站队。于是在传播中，一个**能被归咎、能被对立、能承接情绪的「故事」**会自然生长出来，「暗改」这个自带「故意」二字的词，恰好提供了这样一个着力点。这无关谁对谁错，而是注意力和情绪本身的运作方式。</p><p>也正因如此，这件事最后的解法反而很值得记一笔：官方没有去裁判「鸭子坐到底辱不辱女」、没有替所有人决定哪个动作才「正确」，而是<strong>做了一个自定义功能，把选择权还给每一个玩家</strong>——想要鸭子坐就鸭子坐，不想要就站着，谁也不替谁做主。</p><p>在一个动辄要求「必须有标准答案、必须选边站」的环境里，「<strong>这件事本来就该由当事人自己决定</strong>」这种朴素的解法，往往是真正能让多数人都下台阶的少数出路之一。</p><p>说到底，对屏幕前的我们每个人也是一样的。一场风波刷屏的时候，最不必急着做的就是<strong>选边站</strong>——这游戏到底辱不辱、官方到底坏不坏、该挺哪一方……这些「站队题」多数时候既没有标准答案，也轮不到隔着屏幕的我们替别人拍板。更值得做的，是<strong>先把事情看清楚</strong>：分清哪些是确凿的事实、哪些是别人替你脑补出来的「动机」，分清「确实有错」和「被编出来的故事」。看清了再决定自己怎么想，而不是<strong>跟着风声走、被嗓门最大的那一方裹挟</strong>。</p><p>至于玩不玩，那更是再简单不过的事：<strong>喜欢就接着玩，不喜欢就卸载走人，谁也不欠谁一个交代。</strong> 完全没必要为了证明自己「站对了队」，去骂另一拨人、去给一群素不相识的玩家、或是一个连夜加班的开发团队扣帽子。游戏本来是拿来开心的，犯不着让它变成又一个让人面红耳赤的战场。</p><p><strong>做自己，理性一点，不站队、不听风、不引战</strong>——这大概是「鸭子坐风波」本身之外，最朴素、也最值得带走的一点东西。</p><hr><h3 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h3><ul><li><a href="https://www.gamersky.com/news/202605/2144057.shtml">腾讯又一重磅大作《洛克王国：世界》滑跪！暗改疑似迎合部分玩家引不满 — 游民星空</a></li><li><a href="https://news.17173.com/content/05222026/175701027.shtml">《洛克王国：世界》女洛克只是输了，坐着哭一下都不行？（近 30 处改动）— 17173</a></li><li><a href="https://news.17173.com/content/05222026/140353854.shtml">连夜道歉、全服补偿，《洛克王国》暗改风波玩家会买账吗？— 17173</a></li><li><a href="https://finance.sina.cn/2026-05-22/detail-inhyufhn5273643.d.html">日活 1300 万挡不住 TapTap 跌至 2.8 分，S2「暗改门」炸穿信任基线 — 新浪财经</a></li><li><a href="https://www.sohu.com/a/1027050185_204824">《洛克王国：世界》主策划回应舆情：没有换血，没有空降 — 搜狐</a></li><li><a href="https://36kr.com/p/3824577339101569">《洛克王国：世界》接连致歉，主策否认「团队大换血」传言 — 36 氪</a></li><li><a href="https://www.taptap.cn/moment/807743663592440011">洛克王国 S2 崩盘真相：没有阴谋论，没有空降（开水长信解读）— TapTap</a></li><li><a href="https://www.taptap.cn/moment/808846606337049863">鸭子坐回归！洛克王国「自定义」平息玩家怒火（动作 1 &#x2F; 动作 2）— TapTap</a></li><li><a href="https://www.taptap.cn/moment/807214479921123155">3 万字更新日志写了个寂寞，真改动全自己扒 — TapTap</a></li></ul>]]>
    </content>
    <id>https://ryonnoski.com/2026/05/28/roco-kingdom-s2-recap/</id>
    <link href="https://ryonnoski.com/2026/05/28/roco-kingdom-s2-recap/"/>
    <published>2026-05-28T21:30:00.000Z</published>
    <summary>
      <![CDATA[<p>2026 年 5 月下旬，一款日活上千万的国民级游戏，因为一个「战斗失败后的坐姿」陷入了上线以来最大的风波。短短几天里，官方账号下涌进二十多万条留言，应用商店评分从 6.5 跌到 2.8，主策划连发三次道歉、写下四千字长信。事情听起来很魔幻：一个坐姿，怎么会演变成这么大的事]]>
    </summary>
    <title>《洛克王国：世界》S2 风波复盘：「暗改门」到底改了什么、又为什么会改</title>
    <updated>2026-05-28T12:04:43.831Z</updated>
  </entry>
  <entry>
    <author>
      <name>Ryonnoski0204</name>
    </author>
    <category term="随笔" scheme="https://ryonnoski.com/categories/essay/"/>
    <category term="总结" scheme="https://ryonnoski.com/tags/summary/"/>
    <category term="工具" scheme="https://ryonnoski.com/tags/tools/"/>
    <category term="前端" scheme="https://ryonnoski.com/tags/frontend/"/>
    <category term="工程化" scheme="https://ryonnoski.com/tags/engineering/"/>
    <content>
      <![CDATA[<p>把博客从动态框架搬到 Hexo 静态站，再亲手写一套主题，前后折腾了不少时间。这篇把过程中那些「为什么这么做、又为什么不那么做」的取舍记录下来——既是给自己的复盘，也许也能给同样想折腾博客的人一点参考。</p><p>核心的一条原则贯穿始终：<strong>够用就好，不为了炫技堆功能；但该有的体验细节，一个都不将就。</strong></p><h2 id="一、为什么是静态站"><a href="#一、为什么是静态站" class="headerlink" title="一、为什么是静态站"></a>一、为什么是静态站</h2><p>最初的博客跑在一套动态框架上，评论、点赞、访问统计都依赖后端接口和数据库。它能力强，但代价也实在：要租服务器、要管数据库备份、要担心被刷被攻击，每个月还得交一笔钱。对一个更新频率不高的个人博客来说，这套重型基建有点杀鸡用牛刀。</p><p>静态站的思路完全不同：把所有内容在<strong>构建期</strong>编译成纯 HTML&#x2F;CSS&#x2F;JS，扔到 GitHub Pages 这类静态托管上。好处很直接——</p><ul><li><strong>零成本</strong>：托管免费，没有服务器要养。</li><li><strong>够快</strong>：纯静态文件走 CDN，首字节时间极短。</li><li><strong>够安全</strong>：没有后端、没有数据库，攻击面小到几乎没有。</li><li><strong>可移植</strong>：内容就是一堆 Markdown，换平台、做备份都轻松。</li></ul><p>代价是那些「天生需要后端」的动态功能得换个思路实现。这恰恰是最有意思的部分。</p><h2 id="二、动态功能的静态化方案"><a href="#二、动态功能的静态化方案" class="headerlink" title="二、动态功能的静态化方案"></a>二、动态功能的静态化方案</h2><h3 id="评论：Giscus"><a href="#评论：Giscus" class="headerlink" title="评论：Giscus"></a>评论：Giscus</h3><p>放弃自建评论系统，接入 <a href="https://giscus.app/">Giscus</a>——它把 GitHub Discussions 当作评论数据库，评论数据存在 GitHub 上，前端只是个壳。代价是评论者需要 GitHub 账号，但对一个技术博客来说，读者群体重合度很高，可以接受。额外做的一件事是让 Giscus 的深浅色主题跟随本站切换：监听主题变化，用 <code>postMessage</code> 通知 Giscus 的 iframe 同步。</p><h3 id="访问统计：纯前端计数"><a href="#访问统计：纯前端计数" class="headerlink" title="访问统计：纯前端计数"></a>访问统计：纯前端计数</h3><p>用不蒜子 &#x2F; vercount 这类纯前端脚本统计 PV&#x2F;UV，数据存在第三方，页面只负责展示数字。轻量、无侵入，缺点是依赖外部服务的可用性——但统计数字挂了也不影响阅读，属于可接受的降级。</p><h3 id="站内搜索：索引前置"><a href="#站内搜索：索引前置" class="headerlink" title="站内搜索：索引前置"></a>站内搜索：索引前置</h3><p>动态站的搜索是「输入 → 后端查库 → 返回」。静态站没有后端，于是把索引<strong>提前到构建期</strong>：生成时把所有文章打包成一个 <code>search.json</code>，随站点一起部署，浏览器加载后在内存里做匹配。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 弹出搜索框时才懒加载索引，省首屏流量</span></span><br><span class="line"><span class="keyword">let</span> data = <span class="literal">null</span>;</span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">load</span>(<span class="params"></span>) &#123; data = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/search.json&#x27;</span>).<span class="title function_">then</span>(<span class="function"><span class="params">r</span> =&gt;</span> r.<span class="title function_">json</span>()); &#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">search</span>(<span class="params">q</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> data.<span class="title function_">filter</span>(<span class="function"><span class="params">p</span> =&gt;</span> (p.<span class="property">title</span> + p.<span class="property">content</span>).<span class="title function_">toLowerCase</span>().<span class="title function_">includes</span>(q)).<span class="title function_">slice</span>(<span class="number">0</span>, <span class="number">10</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>几十上百篇文章的规模，这种暴力子串匹配毫秒级完成，体验完全够。真到上万篇再考虑倒排索引或 Pagefind 不迟——<strong>不提前为不存在的规模买单</strong>，也是一种取舍。</p><h2 id="三、无刷新导航：PJAX-的克制"><a href="#三、无刷新导航：PJAX-的克制" class="headerlink" title="三、无刷新导航：PJAX 的克制"></a>三、无刷新导航：PJAX 的克制</h2><p>最影响「手感」的决定，是上 PJAX（pushState + Ajax）。点击站内链接时，不整页刷新，而是 fetch 回目标页面，只替换变化的区域：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fetch</span>(url).<span class="title function_">then</span>(<span class="function"><span class="params">r</span> =&gt;</span> r.<span class="title function_">text</span>()).<span class="title function_">then</span>(<span class="function"><span class="params">html</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> doc = <span class="keyword">new</span> <span class="title class_">DOMParser</span>().<span class="title function_">parseFromString</span>(html, <span class="string">&#x27;text/html&#x27;</span>);</span><br><span class="line">  <span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&#x27;.main&#x27;</span>).<span class="title function_">replaceWith</span>(doc.<span class="title function_">querySelector</span>(<span class="string">&#x27;.main&#x27;</span>));</span><br><span class="line">  <span class="comment">// 顶栏、侧栏、音乐播放器在替换区域之外，纹丝不动</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>因为顶栏、左侧栏和左下角的音乐播放器都在被替换的区域之外，换页时它们保持不动——音乐不断、主题不闪、滚动条不跳，体验接近单页应用。</p><p>但 PJAX 也有一堆坑，每一个都得认真填：</p><ul><li><strong>脚本不会自动执行</strong>：插入的 <code>&lt;script&gt;</code> 不运行，得手动重建节点（否则评论、统计换页后失效）。</li><li><strong>事件要用委托</strong>：监听绑在 <code>document</code> 上，否则换页后节点被替换、监听就丢了。</li><li><strong>滚动管理</strong>：换页瞬时回顶（<code>behavior:&#39;instant&#39;</code> 强制忽略全局平滑滚动），并把 <code>history.scrollRestoration</code> 设为 <code>manual</code> 自己接管，前进&#x2F;后退时再恢复记录的位置。</li></ul><p>最关键的是把它做成<strong>渐进增强</strong>：每个链接仍是真实的 <code>&lt;a href&gt;</code>，每页在服务端都能独立访问、返回完整 HTML。PJAX 只在支持的浏览器里拦截优化，不支持或出错就回退整页跳转。这样既拿到了顺滑手感，又<strong>完整保留了静态站的 SEO 优势</strong>——搜索引擎爬的是真实 URL 和完整 HTML，根本不关心前端有没有拦截。</p><h2 id="四、主题系统：两个正交的维度"><a href="#四、主题系统：两个正交的维度" class="headerlink" title="四、主题系统：两个正交的维度"></a>四、主题系统：两个正交的维度</h2><p>配色是这套主题里我最满意的部分。核心是 CSS 自定义属性：把所有颜色抽成变量，<strong>换主题就是换一组变量的值</strong>。</p><p>我把主题拆成两个<strong>互相独立</strong>的维度：</p><ul><li><code>data-theme</code>：亮 &#x2F; 暗 &#x2F; 跟随系统。</li><li><code>data-palette</code>：主色（青 &#x2F; 蓝 &#x2F; 绿 &#x2F; 粉 &#x2F; 橙 &#x2F; 紫……）。</li></ul><p>两者正交组合——任意主色都能配深色或浅色。更进一步，配色列表做成了<strong>配置式</strong>：在配置文件里列出每套配色的主色，模板用 <code>color-mix()</code> 自动派生出 <code>--accent-soft</code> 等衍生色并生成对应 CSS，增删一套配色只改配置、不碰样式表。</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">--accent-soft</span>: <span class="built_in">color-mix</span>(in srgb, <span class="built_in">var</span>(--accent) <span class="number">13%</span>, transparent);</span><br></pre></td></tr></table></figure><p>几个体验细节：</p><ul><li><strong>防首屏闪烁（FOUC）</strong>：在 <code>&lt;head&gt;</code> 里放一段阻塞执行的内联脚本，在首次绘制前就把 <code>data-theme</code>&#x2F;<code>data-palette</code> 设好，暗色用户不会先看到一道白光。</li><li><strong>圆形扩散切换</strong>：支持 View Transition API 的浏览器，主题切换会从点击位置圆形扩散开，不支持的则瞬切降级。</li><li><strong>记忆与随机</strong>：默认记住上次选择；也提供「每次进入随机一种配色」的开关，每天打开都有新鲜感。</li></ul><h2 id="五、性能与体验的细节"><a href="#五、性能与体验的细节" class="headerlink" title="五、性能与体验的细节"></a>五、性能与体验的细节</h2><p>这些细节单独看都不起眼，叠起来就是「这博客用着真舒服」和「平平无奇」的差别。</p><p><strong>图片懒加载 + 骨架屏</strong>。正文图片用 <code>loading=lazy</code>，加载前显示一条流动微光的骨架占位；文章封面用 <code>aspect-ratio</code> 预留盒子，既能显示骨架，又消除了图片加载时的高度跳动。</p><p><strong>虚拟滚动 + 无限加载</strong>。首页文章列表只挂载可视窗口内的卡片，滚走的回收，上下用占位撑住滚动条高度；滚到底自动按页号拉取下一页。为了让「返回时恢复滚动位置」精准，还把实测的卡片高度持久化下来——这样从文章返回列表，能回到几乎同一像素的位置。</p><p>这里其实有过一次反复：虚拟滚动和「按像素恢复滚动位置」天然冲突（懒加载时页面高度对不上）。最后的解法是<strong>按总数预留全部卡片的估算高度</strong>，让滚动条一开始就是全长的，再用持久化的真实高度校正。这类「想要 A 又想要 B，而 A、B 看似矛盾」的问题，往往才是值得记录的部分。</p><p><strong>全局平滑滚动</strong>也踩过坑：它会把刷新时浏览器的滚动恢复也变成动画（页面”自己往上滚”）。根治办法是先用 <code>scrollRestoration: manual</code> 关掉浏览器的自动恢复，平滑滚动就只作用于真正的滚动动作了。</p><h2 id="六、内容渲染"><a href="#六、内容渲染" class="headerlink" title="六、内容渲染"></a>六、内容渲染</h2><table><thead><tr><th>需求</th><th>方案</th><th>取舍点</th></tr></thead><tbody><tr><td>数学公式</td><td>KaTeX <strong>服务端渲染</strong></td><td>在 marked 之前渲染，规避 <code>$x_i$</code> 下划线被当斜体；无前端闪烁</td></tr><tr><td>流程图</td><td>Mermaid <strong>按需懒加载</strong></td><td>只在含图表的页面加载脚本，跟随主题色重渲染</td></tr><tr><td>代码块</td><td>highlight.js + Mac 三色点</td><td>顶部语言标签、一键复制</td></tr><tr><td>中英排版</td><td>Pangu 自动空格</td><td>只处理文本节点、跳过代码，PJAX 换页也生效</td></tr></tbody></table><p>数学公式特意做成<strong>服务端渲染</strong>而非客户端：构建期就把 <code>$...$</code> 编译成 HTML，既避免了 Markdown 解析器把公式里的下划线、星号当成强调语法的经典问题，也没有「先看到源码再变公式」的闪烁。</p><h2 id="七、一点「无用之用」"><a href="#七、一点「无用之用」" class="headerlink" title="七、一点「无用之用」"></a>七、一点「无用之用」</h2><p>理性上，顶栏中间那只会蹦跶、会追着鼠标跳、点一下还会冒泡说话的小史莱姆，对博客毫无用处。但它让我每次打开页面都会心一笑——而「让自己愿意经常回来写」，对个人博客来说可能比任何功能都重要。</p><p>分寸感在于<strong>克制</strong>：它只占顶栏中间那块本来就空着的地方，移动端自动隐藏，还做成了可配置开关。乐趣和干净，不必二选一。</p><h2 id="八、部署：源码私有，产物公开"><a href="#八、部署：源码私有，产物公开" class="headerlink" title="八、部署：源码私有，产物公开"></a>八、部署：源码私有，产物公开</h2><p>这套主题的视觉设计参考了 <a href="https://www.ihewro.com/archives/489/">ihewro 的 handsome 主题</a>——一款基于 PHP &#x2F; Typecho 框架的付费主题。我用 Hexo 把它的前端独立重写了一遍，并获得作者授权用于个人非开源用途。如果你也喜欢这套设计，欢迎去支持正版、购买原作者的主题。</p><p>正因为有这层授权限制，主题源码不能公开；可 GitHub Pages 偏偏需要一个公开仓库来托管产物——这就成了一对矛盾。解法是拆成<strong>两个仓库</strong>：源码（含主题）放私有仓库，CI 构建后只把 <code>public/</code> 产物推到公开的 Pages 仓库。这样线上能正常访问、源码不外泄，部署也全自动。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>回头看，这套主题没有什么惊天动地的技术，就是一堆小决定的叠加：每个功能都先问一句「真的需要吗」，需要的话再问「最轻的实现是什么」。</p><p>够用就好，不堆功能；但凡留下的，都打磨到顺手。这大概就是我对「个人博客」这四个字的理解。</p>]]>
    </content>
    <id>https://ryonnoski.com/2026/05/25/about-this-theme/</id>
    <link href="https://ryonnoski.com/2026/05/25/about-this-theme/"/>
    <published>2026-05-25T21:30:00.000Z</published>
    <summary>
      <![CDATA[<p>把博客从动态框架搬到 Hexo 静态站，再亲手写一套主题，前后折腾了不少时间。这篇把过程中那些「为什么这么做、又为什么不那么做」的取舍记录下来——既是给自己的复盘，也许也能给同样想折腾博客的人一点参考。</p>
<p>核心的一条原则贯穿始终：<strong>够用就好，不为了]]>
    </summary>
    <title>关于这个博客主题的设计取舍</title>
    <updated>2026-05-28T12:04:43.831Z</updated>
  </entry>
  <entry>
    <author>
      <name>Ryonnoski0204</name>
    </author>
    <category term="技术" scheme="https://ryonnoski.com/categories/tech/"/>
    <category term="前端" scheme="https://ryonnoski.com/tags/frontend/"/>
    <category term="性能" scheme="https://ryonnoski.com/tags/performance/"/>
    <content>
      <![CDATA[<p>页面卡顿、滚动掉帧，很多时候不是 JS 算得慢，而是触发了过多的重排（reflow）。要写出流畅的界面，得先理解浏览器是怎么把代码变成像素的。</p><h2 id="渲染管线"><a href="#渲染管线" class="headerlink" title="渲染管线"></a>渲染管线</h2><p>从 HTML 到屏幕上的像素，浏览器大致走这几步：</p><div class="mermaid">graph LR  A[HTML] --&gt; B[DOM]  C[CSS] --&gt; D[CSSOM]  B --&gt; E[Render Tree]  D --&gt; E  E --&gt; F[Layout 布局]  F --&gt; G[Paint 绘制]  G --&gt; H[Composite 合成]</div><ol><li><strong>解析</strong>：HTML 解析成 DOM 树，CSS 解析成 CSSOM 树。</li><li><strong>构建渲染树</strong>：两者合并，剔除 <code>display:none</code> 的节点。</li><li><strong>Layout（布局&#x2F;重排）</strong>：计算每个节点的几何信息——位置、大小。</li><li><strong>Paint（绘制&#x2F;重绘）</strong>：把节点画成一层层的位图。</li><li><strong>Composite（合成）</strong>：GPU 把这些图层叠在一起输出到屏幕。</li></ol><h2 id="重排-vs-重绘-vs-合成"><a href="#重排-vs-重绘-vs-合成" class="headerlink" title="重排 vs 重绘 vs 合成"></a>重排 vs 重绘 vs 合成</h2><p>这三者的代价依次递减：</p><ul><li><strong>重排（Reflow）</strong>：几何属性变化（width、top、font-size、增删节点）会触发重新布局，<strong>最贵</strong>，因为它可能影响其它元素的位置，往往连带重绘。</li><li><strong>重绘（Repaint）</strong>：只是外观变化（color、background、visibility），位置不变，跳过布局，比重排便宜。</li><li><strong>合成（Composite）</strong>：只用 <code>transform</code> 和 <code>opacity</code> 做动画时，可以只在合成阶段处理，连绘制都跳过，由 GPU 直接搞定，<strong>最便宜</strong>、最流畅。</li></ul><p>这就是为什么动画要优先用 <code>transform: translate()</code> 而不是改 <code>left/top</code>——后者每帧都重排，前者只走合成。</p><h2 id="布局抖动（Layout-Thrashing）"><a href="#布局抖动（Layout-Thrashing）" class="headerlink" title="布局抖动（Layout Thrashing）"></a>布局抖动（Layout Thrashing）</h2><p>最常见的性能陷阱，是在循环里「读一下、写一下」交替操作布局：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 反例：每次读 offsetHeight 都强制同步重排</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> el <span class="keyword">of</span> items) &#123;</span><br><span class="line">  el.<span class="property">style</span>.<span class="property">height</span> = el.<span class="property">offsetHeight</span> + <span class="number">10</span> + <span class="string">&#x27;px&#x27;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>浏览器本来会把多次样式修改<strong>批量</strong>起来、延迟到下一帧统一重排。但 <code>offsetHeight</code>、<code>getBoundingClientRect()</code>、<code>scrollTop</code> 这类属性需要<strong>立即</strong>返回准确值，会强制浏览器同步 flush 布局。读-写交替就把批量优化彻底打碎，每次循环都重排一遍。</p><p>正确做法是<strong>读写分离</strong>：先一次性读完所有需要的值，再统一写：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> heights = items.<span class="title function_">map</span>(<span class="function"><span class="params">el</span> =&gt;</span> el.<span class="property">offsetHeight</span>); <span class="comment">// 集中读</span></span><br><span class="line">items.<span class="title function_">forEach</span>(<span class="function">(<span class="params">el, i</span>) =&gt;</span> &#123;                         <span class="comment">// 集中写</span></span><br><span class="line">  el.<span class="property">style</span>.<span class="property">height</span> = heights[i] + <span class="number">10</span> + <span class="string">&#x27;px&#x27;</span>;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="几条实用优化"><a href="#几条实用优化" class="headerlink" title="几条实用优化"></a>几条实用优化</h2><ul><li><strong>动画用 <code>transform</code> &#x2F; <code>opacity</code></strong>，避开布局和绘制。</li><li><strong><code>will-change: transform</code></strong> 提前提示浏览器把元素提为独立合成层，但别滥用——图层太多反而吃内存。</li><li><strong><code>contain: layout paint</code></strong> 把一个组件的布局&#x2F;绘制影响隔离在自身范围内，避免牵一发动全身。</li><li><strong>批量 DOM 操作</strong>用 <code>DocumentFragment</code>，或先 <code>display:none</code> 改完再显示。</li><li><strong>避免频繁读取布局属性</strong>，必须读时缓存下来。</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>理解「重排 &gt; 重绘 &gt; 合成」的代价梯度，是前端性能优化的基本功。记住两条就能避开大部分坑：动画走 <code>transform/opacity</code>，以及别在循环里读写交替触发布局抖动。剩下的，用 DevTools 的 Performance 面板录一段，看哪里 Layout&#x2F;Paint 占时间长，按图索骥即可。</p>]]>
    </content>
    <id>https://ryonnoski.com/2026/04/28/browser-render-pipeline/</id>
    <link href="https://ryonnoski.com/2026/04/28/browser-render-pipeline/"/>
    <published>2026-04-28T09:40:00.000Z</published>
    <summary>
      <![CDATA[<p>页面卡顿、滚动掉帧，很多时候不是 JS 算得慢，而是触发了过多的重排（reflow）。要写出流畅的界面，得先理解浏览器是怎么把代码变成像素的。</p>
<h2 id="渲染管线"><a href="#渲染管线" class="headerlink" title="渲染管线"]]>
    </summary>
    <title>浏览器渲染管线与重排重绘优化</title>
    <updated>2026-05-28T12:04:43.831Z</updated>
  </entry>
  <entry>
    <author>
      <name>Ryonnoski0204</name>
    </author>
    <category term="教程" scheme="https://ryonnoski.com/categories/tutorial/"/>
    <category term="后端" scheme="https://ryonnoski.com/tags/backend/"/>
    <category term="网络" scheme="https://ryonnoski.com/tags/network/"/>
    <content>
      <![CDATA[<p>「为什么改了 CSS 用户却看到旧样式？」「为什么有的请求返回 304？」这些都绕不开 HTTP 缓存。它分两层：强缓存和协商缓存。搞清楚这两层，就能精准控制资源的更新与复用。</p><h2 id="强缓存：连请求都不发"><a href="#强缓存：连请求都不发" class="headerlink" title="强缓存：连请求都不发"></a>强缓存：连请求都不发</h2><p>强缓存命中时，浏览器<strong>直接用本地副本</strong>，根本不和服务器通信。控制它的是两个响应头：</p><ul><li><strong><code>Cache-Control</code></strong>（HTTP&#x2F;1.1，优先级高）：<ul><li><code>max-age=31536000</code>：缓存有效期（秒）。</li><li><code>no-cache</code>：可以缓存，但每次用前必须走协商缓存验证。</li><li><code>no-store</code>：完全不缓存。</li><li><code>immutable</code>：在有效期内永不重新验证（即使用户刷新）。</li></ul></li><li><strong><code>Expires</code></strong>（HTTP&#x2F;1.0，绝对时间，已基本被 <code>Cache-Control</code> 取代）。</li></ul><p>命中强缓存时，DevTools 的 Network 里 Size 一列会显示 <code>disk cache</code> 或 <code>memory cache</code>，状态码仍是 200（带灰色标注）。</p><h2 id="协商缓存：问一句「变了没」"><a href="#协商缓存：问一句「变了没」" class="headerlink" title="协商缓存：问一句「变了没」"></a>协商缓存：问一句「变了没」</h2><p>当强缓存过期（或设了 <code>no-cache</code>），浏览器会带上「凭证」去问服务器：我手里这份还能用吗？这就是协商缓存。一对头：</p><p><strong>基于内容指纹（ETag）</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">响应：ETag: &quot;a1b2c3&quot;</span><br><span class="line">请求：If-None-Match: &quot;a1b2c3&quot;</span><br></pre></td></tr></table></figure><p>服务器比对 ETag，没变就返回 <code>304 Not Modified</code>（不带 body），浏览器继续用本地副本；变了就返回 200 + 新内容。</p><p><strong>基于修改时间（Last-Modified）</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">响应：Last-Modified: Wed, 16 Apr 2026 08:00:00 GMT</span><br><span class="line">请求：If-Modified-Since: Wed, 16 Apr 2026 08:00:00 GMT</span><br></pre></td></tr></table></figure><p>ETag 比 Last-Modified 更精确（后者只能精确到秒，且文件内容没变只是 mtime 变了也会误判），两者同时存在时 ETag 优先。</p><h2 id="一张决策图"><a href="#一张决策图" class="headerlink" title="一张决策图"></a>一张决策图</h2><div class="mermaid">graph TD  A[请求资源] --&gt; B{强缓存有效?}  B --&gt;|是| C[用本地副本 200 from cache]  B --&gt;|否| D[带 ETag/Last-Modified 询问]  D --&gt; E{服务器: 变了吗}  E --&gt;|没变| F[304 用本地副本]  E --&gt;|变了| G[200 返回新内容]</div><h2 id="实战策略"><a href="#实战策略" class="headerlink" title="实战策略"></a>实战策略</h2><p>现代前端的标准做法是「<strong>文件名带 hash + 长缓存</strong>」：</p><ul><li>带指纹的静态资源（<code>app.3f9a.js</code>）：<code>Cache-Control: max-age=31536000, immutable</code>。内容一变文件名就变，URL 变了自然重新下载，所以可以放心长期强缓存。</li><li>HTML 入口文件：<code>Cache-Control: no-cache</code>。让浏览器每次都验证，这样一旦发布新版，HTML 里引用的新 hash 文件名就能立刻生效。</li></ul><p>这套组合拳既最大化复用、又保证更新及时。开头那个「改了 CSS 看到旧样式」的问题，根因往往就是 CSS 用了固定文件名又设了长 <code>max-age</code>——改成 hash 文件名即可根治。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>强缓存决定「要不要发请求」，协商缓存决定「发了之后要不要传 body」。静态资源用 hash 文件名配长强缓存，HTML 用 no-cache，是兼顾性能与更新及时性的经典方案。</p>]]>
    </content>
    <id>https://ryonnoski.com/2026/04/16/http-cache-explained/</id>
    <link href="https://ryonnoski.com/2026/04/16/http-cache-explained/"/>
    <published>2026-04-16T16:10:00.000Z</published>
    <summary>
      <![CDATA[<p>「为什么改了 CSS 用户却看到旧样式？」「为什么有的请求返回 304？」这些都绕不开 HTTP 缓存。它分两层：强缓存和协商缓存。搞清楚这两层，就能精准控制资源的更新与复用。</p>
<h2 id="强缓存：连请求都不发"><a href="#强缓存：连请求都不发" cl]]>
    </summary>
    <title>HTTP 缓存全解：强缓存与协商缓存</title>
    <updated>2026-05-28T12:04:43.831Z</updated>
  </entry>
  <entry>
    <author>
      <name>Ryonnoski0204</name>
    </author>
    <category term="技术" scheme="https://ryonnoski.com/categories/tech/"/>
    <category term="前端" scheme="https://ryonnoski.com/tags/frontend/"/>
    <category term="工程化" scheme="https://ryonnoski.com/tags/engineering/"/>
    <content>
      <![CDATA[<p>这个博客没有后端，却能站内搜索。静态站既没有数据库也没有服务端接口，搜索这件「天生需要查询」的事，是怎么做到的？答案是：把索引在<strong>构建期</strong>生成好，查询全放到<strong>浏览器端</strong>做。</p><h2 id="思路：索引前置"><a href="#思路：索引前置" class="headerlink" title="思路：索引前置"></a>思路：索引前置</h2><p>动态站的搜索是「用户输入 → 后端查库 → 返回结果」。静态站没有后端，于是换个思路：在生成网站的时候，把所有文章的标题、正文、链接打包成一个 JSON 文件（本站用 <code>hexo-generator-searchdb</code> 生成 <code>search.json</code>），随站点一起部署。浏览器加载这个 JSON，搜索就在前端内存里完成。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span> <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;PJAX 无刷新导航&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/2026/05/12/pjax/&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;这个博客点击菜单...&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span> <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;HTTP 缓存全解&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/2026/04/16/http-cache/&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;为什么改了 CSS...&quot;</span> <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><h2 id="最朴素的实现"><a href="#最朴素的实现" class="headerlink" title="最朴素的实现"></a>最朴素的实现</h2><p>数据量不大时，直接 <code>includes</code> 子串匹配就够用：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> data = <span class="literal">null</span>;</span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">load</span>(<span class="params"></span>) &#123;</span><br><span class="line">  data = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/search.json&#x27;</span>).<span class="title function_">then</span>(<span class="function"><span class="params">r</span> =&gt;</span> r.<span class="title function_">json</span>());</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">search</span>(<span class="params">q</span>) &#123;</span><br><span class="line">  q = q.<span class="title function_">trim</span>().<span class="title function_">toLowerCase</span>();</span><br><span class="line">  <span class="keyword">if</span> (!q) <span class="keyword">return</span> [];</span><br><span class="line">  <span class="keyword">return</span> data</span><br><span class="line">    .<span class="title function_">filter</span>(<span class="function"><span class="params">p</span> =&gt;</span> (p.<span class="property">title</span> + <span class="string">&#x27; &#x27;</span> + p.<span class="property">content</span>).<span class="title function_">toLowerCase</span>().<span class="title function_">includes</span>(q))</span><br><span class="line">    .<span class="title function_">slice</span>(<span class="number">0</span>, <span class="number">10</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本站正是这么做的：弹出搜索框时才懒加载 <code>search.json</code>，输入时实时过滤，取前 10 条。几十上百篇文章的规模，这种暴力匹配在现代浏览器里毫秒级完成，体验完全够。</p><h2 id="几个体验细节"><a href="#几个体验细节" class="headerlink" title="几个体验细节"></a>几个体验细节</h2><ul><li><strong>懒加载索引</strong>：别在首页就下载 <code>search.json</code>，等用户真的打开搜索框再 fetch，省首屏流量。</li><li><strong>防抖</strong>：输入事件触发很频繁，高频场景可以加 <code>debounce</code> 限流；但纯内存子串匹配其实很快，可不加。</li><li><strong>结果高亮</strong>：把命中的关键词用 <code>&lt;mark&gt;</code> 包起来，结果一目了然。</li><li><strong>截断摘要</strong>：正文很长，结果里只展示命中位置附近的一小段。</li></ul><h2 id="什么时候需要更专业的方案"><a href="#什么时候需要更专业的方案" class="headerlink" title="什么时候需要更专业的方案"></a>什么时候需要更专业的方案</h2><p>子串匹配有两个短板：一是不分词（搜「前端性能」不会命中「前端的性能」），二是数据量大时 JSON 体积和遍历成本都上来了。这时可以升级：</p><ul><li><strong>倒排索引</strong>：预先把「词 → 出现在哪些文档」的映射建好，查询时查词表而非遍历全文。<code>Fuse.js</code>、<code>Lunr.js</code>、<code>FlexSearch</code> 都是前端可用的库。</li><li><strong>Pagefind</strong>：专为静态站设计，构建期分片建索引，查询时按需下载分片，万篇文章也不卡。</li><li><strong>Algolia</strong>：托管搜索服务，免费额度够个人博客用，但引入了外部依赖。</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>静态站搜索的精髓是「<strong>把索引提前到构建期，把查询下放到客户端</strong>」。小博客用一个 <code>search.json</code> + 子串匹配就够优雅；规模大了再上倒排索引或 Pagefind。无后端不等于没搜索，只是把计算挪了个位置。</p>]]>
    </content>
    <id>https://ryonnoski.com/2026/04/08/static-site-fulltext-search/</id>
    <link href="https://ryonnoski.com/2026/04/08/static-site-fulltext-search/"/>
    <published>2026-04-08T14:30:00.000Z</published>
    <summary>
      <![CDATA[<p>这个博客没有后端，却能站内搜索。静态站既没有数据库也没有服务端接口，搜索这件「天生需要查询」的事，是怎么做到的？答案是：把索引在<strong>构建期</strong>生成好，查询全放到<strong>浏览器端</strong>做。</p>
<h2 id="思路：索引前置"]]>
    </summary>
    <title>静态博客的全文搜索是怎么做的</title>
    <updated>2026-05-28T12:04:43.831Z</updated>
  </entry>
</feed>
