[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site":3,"post-jian-li-de-xie-gang-hui-bian-cheng":59,"related-jian-li-de-xie-gang-hui-bian-cheng":93},{"name":4,"url":5,"description":6,"bio":7,"avatar":8,"skills":9,"tools":16,"social":34,"hero":47,"heatmap":52,"icp":55,"police":56,"policeIcon":57,"shortName":58},"时过境迁技术笔记","https:\u002F\u002Fhsgjq.top","一个记录前端与 Node 实践的个人博客，内容围绕 Vue、Nuxt、NestJS 与工程化展开，附源码级的原理笔记。","一名热爱技术的全栈开发者，专注 TypeScript 与 Web 工程化，习惯把踩过的坑和思考记录在这里。","\u002Fuploads\u002Fc485c101-179e-48db-96d1-238693784c38.jpg",[10,11,12,13,14,15],"TypeScript","Vue 3","Nuxt","NestJS","Node.js","工程化",[17,23,28],{"items":18,"title":22},[19,20,21],"Cursor","Git","Chrome DevTools","开发工具",{"items":24,"title":27},[25,26],"Vite","npm","构建工具",{"items":29,"title":33},[30,31,32],"Docker","CI\u002FCD(GitHub Actions)","Vercel","部署运维",[35,39,43],{"href":36,"icon":37,"label":38},"https:\u002F\u002Fgithub.com\u002Fhsgjq","github","GitHub",{"href":40,"icon":41,"label":42},"2971861643@qq.com","mail","邮箱",{"href":44,"icon":45,"label":46},"\u002Frss.xml","rss","RSS",{"lines":48,"subtitle":51},[49,50],"人生若只如初见，","何事秋风悲画扇。","《木兰花·拟古决绝词柬友》",{"enabled":53,"platform":37,"username":54},true,"hsgjq","桂ICP备2024046985号","桂公网安备45011002000103号","\u002Fuploads\u002F72c67ef3-bc86-4b64-b0df-0aba71065050.png","时过境迁",{"slug":60,"title":61,"excerpt":62,"date":63,"updatedAt":64,"tags":65,"content":66,"series":67,"seriesIndex":68,"seriesNav":69,"prev":87,"next":90},"jian-li-de-xie-gang-hui-bian-cheng","键里的斜杠会变成冒号：清缓存清了个空","按源码里的分组名去匹配存储键，结果一条都没删掉，接口还回 200。返回一个可被证伪的数字，这类问题当场就能发现。","2026-09-30","2026-10-19",[12,15],"文章页有 SSR 缓存，后端保存成功之后要通知前端把这条路径的缓存删掉。前端那个接口我写成这样：\n\n```ts\nconst keys = await storage.getKeys()\nconst stale = keys.filter((key) => key.includes('nitro\u002Froutes'))\n```\n\nnitro 的 routeRules 缓存分组在源码里就是这么声明的：\n\n```ts\nhandler = cachedEventHandler(handler, {\n  group: 'nitro\u002Froutes',\n  ...routeRules.cache,\n})\n```\n\n所以这行看起来完全正确。它返回 removed: 0，一条都没删。而且接口回 200，日志里也没有异常——静默成功。\n\n我把键打出来看：\n\n```\ncache:nitro:routes:_:postsceshi.8f2c1a...json\n```\n\n分组声明里的 `nitro\u002Froutes` 那个斜杠，落到存储层变成了冒号。原因是 unstorage 用冒号做键的层级分隔，写入时会把键里的 `\u002F` 规范化成 `:`。我按源码里的写法去匹配，匹配的是运行时根本不存在的形式。\n\n修法是比较之前把两边都归一：\n\n```ts\nconst stale = keys.filter((key) => {\n  const normalized = key.replace(\u002F\\\u002F\u002Fg, ':')\n  return normalized.includes('nitro:routes:')\n    && fragments.some((f) => normalized.includes(f))\n})\n```\n\n这样无论将来存储层怎么规范化，两边都在同一个形态下比较。\n\n第二个坑是路径片段。缓存键里那部分是框架自己算的，我如果用自己的规则去猜，就会不一致。去安装目录里读了那段实现：\n\n```js\nfunction escapeKey(key) {\n  return String(key).replace(\u002F\\W\u002Fg, '')\n}\n\u002F\u002F ...\n_pathname = escapeKey(decodeURI(parseURL(_path).pathname)).slice(0, 16) || 'index'\n_hashedPath = `${_pathname}.${hash(_path)}`\n```\n\n于是我的片段生成完全照抄它：\n\n```ts\nfunction pathFragment(path: string) {\n  return path.replace(\u002F\\W\u002Fg, '').slice(0, 16)\n}\n```\n\n这里有个细节值得记：`\\W` 会把连字符也去掉，所以 \u002Fposts\u002Fce-shi-2 变成 postsceshi2。如果我按\"路径分段\"去猜，会算成 posts:ce-shi-2 之类的形式，永远匹配不上。带连字符的 slug 恰好是最常见的情况。\n\n但这次最重要的收获不是\"要读源码\"，而是那两个字：requested 和 removed。\n\n```ts\nreturn { requested: paths.length, removed: stale.length }\n```\n\n后端把这个回执打进日志。有了它，\"我没生效\"和\"本来就没有缓存可清\"是两件能一眼分开的事。线上第一次验证时我看到的是 removed: 0，因为那篇文章刚建、前台还没有人访问过，压根没有缓存条目——这是正确结果，不是故障。如果接口只返回 { ok: true }，我会认为链路坏了，然后开始改一些其实没坏的东西。\n\n同一类问题我在别处也遇到过：批量删除接口返回成功，实际一条都没删，因为筛选条件写错了。从那以后我写这类\"改动一批东西\"的接口都会带一个数量返回，并且让调用方把它打进日志。\n\n还有一条：这个接口的路径不能对公网开。它虽有密钥保护，但多一个入口就多一分风险，所以在 nginx 里显式返回 404，只有 compose 内网能访问到前端容器。","线上事故记录",3,[70,74,78,79,83],{"slug":71,"title":72,"seriesIndex":73},"1-9g-nei-cun-de-fu-wu-qi","1.9G 内存的服务器上，nuxt build 在第 36 秒被杀",1,{"slug":75,"title":76,"seriesIndex":77},"wo-ba-compose-de-override-xie-cuo-zhan","我把 compose 的 override 写错，站点停了二十分钟",2,{"slug":60,"title":61,"seriesIndex":68},{"slug":80,"title":81,"seriesIndex":82},"dian-xiang-mu-ye-mian-jiu-si-yi","点「项目」页面就死：一次客户端无限重定向",4,{"slug":84,"title":85,"seriesIndex":86},"gang-deng-lu-jiu-guo-qi-yi-ci","一个 401，查出四个前端机制：缓存回放、隐藏页的 rAF、百分比圆角、缓动过冲",5,{"slug":88,"title":89},"hou-tai-bao-cun-wen-zhang-qian-tai","后台保存文章，前台一小时没变",{"slug":91,"title":92},"shang-xian-na-tian-yu-ming-zheng-shu","上线那天：域名、证书，和一条我打错的 IP",[94,99,104,108,114],{"slug":80,"title":81,"excerpt":95,"date":96,"tags":97},"为了保留旧地址大小写加的一条跳转规则，被注册成了客户端路由；路由匹配不区分大小写，于是它自己跳自己。","2026-10-02",[12,98],"浏览器",{"slug":100,"title":101,"excerpt":102,"date":96,"tags":103},"cong-da-bao-shang-chuan-dao-push-ji","从打包上传到 push 即上线：把 CD 接通","私有仓库的镜像包默认也是私有的，健康检查的探测地址在宿主机有 nginx 时会打错地方。两处都是配好之后才验出来的。",[30,15],{"slug":88,"title":89,"excerpt":105,"date":106,"tags":107},"SSR 缓存要有主动失效这条通道。密钥两侧必须同值、这个接口不能对公网开、回执要能被打脸（计数）。","2026-09-27",[12],{"slug":109,"title":110,"excerpt":111,"date":112,"tags":113},"301-302-307-wo-yi-kai-shi-fen","301、302、307 我一开始分不清","永久和临时不只是名字区别，搜索端权重转移不一样。框架给的默认值不一定是你要的那个，得显式写。","2026-09-18",[12],{"slug":115,"title":116,"excerpt":117,"date":118,"tags":119},"tu-pian-dao-di-cun-na-cong-dui","图片到底存哪：从对象存储的念头回到自建目录","算了一下流量和成本，先放在自己的卷里，把上传路径和访问路径分开设计，将来要换存储只改一处。","2026-07-14",[15]]