[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site":3,"post-dian-xiang-mu-ye-mian-jiu-si-yi":59,"related-dian-xiang-mu-ye-mian-jiu-si-yi":92},{"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":67,"series":68,"seriesIndex":69,"seriesNav":70,"prev":88,"next":91},"dian-xiang-mu-ye-mian-jiu-si-yi","点「项目」页面就死：一次客户端无限重定向","为了保留旧地址大小写加的一条跳转规则，被注册成了客户端路由；路由匹配不区分大小写，于是它自己跳自己。","2026-10-02","2026-10-19",[12,66],"浏览器","站点上线当天遇到的第一个线上问题：点导航栏的「项目」，页面能打开、内容也渲染出来了，然后就整页卡死，Chrome 弹「1 个页面无响应」，F12 都打不开。只有这一个页面这样，其它页面正常。\n\n这个症状本身就是线索：地址栏直接输入能打开、内容也出来了，说明服务端没问题；点导航才卡，说明问题在客户端路由；\"内容渲染出来了才卡死\"说明不是死在渲染，是死在渲染之后的某个循环里。\n\n查下去发现是我自己加的一条\"旧地址跳转\"造成的。\n\n之前把目录 pages\u002FProject 改成小写 pages\u002Fproject。改完担心已经进搜索引擎的旧地址 \u002FProject 变 404，就在 nuxt.config 的 routeRules 里加了一条：\n\n```ts\nrouteRules: {\n  '\u002FProject': { redirect: '\u002Fproject' },\n}\n```\n\n这条规则不只是服务端的重定向 —— Nuxt 还会把它注册成一条客户端路由。而路由表匹配默认不区分大小写，于是访问 \u002Fproject 时命中的是 \u002FProject 这条记录，触发它的 redirect，跳到 \u002Fproject，再命中自己。\n\n开发环境里 Vue Router 检测到环会中止，只留一条警告，所以本地怎么点都正常：\n\n```\n[Vue Router warn]: Detected a possibly infinite redirection\nin a navigation guard when going from \"\u002Fseries\" to \"\u002Fproject\"\n```\n\n生产构建没有这层保护，主线程就死在那儿了。这条警告我是回到本地跑了一遍才看到的，比线上晚了两天。\n\n中间有四次判断是错的，写出来免得下次再犯。\n\n一次是拿截图工具超时当\"绘制卡死\"的证据。它只能说明页面没到完成态，说明不了为什么。这个区别听起来显然，但当时我拿着它当结论用了半天。\n\n一次是用 display:none 把可疑组件藏起来做排除。display 只影响布局，不阻止 JS 执行，所以这个实验根本没把组件代码排除掉，等于没做。\n\n一次是断言\"关掉脚本还能复现\"。我用的参数是 --blink-settings=scriptsEnabled=false，键名是错的（应该是 scriptEnabled），那次实验完全没生效，结论也是错的。\n\n还有一次怀疑是某个 CSS 属性触发了渲染死循环。被自己的对照实验否掉了：把出问题页面和正常页面的内联样式抓下来做差集，一条差异都没有。\n\n把范围收窄的是两件事。第一件是把 HTML 里的 script 标签全删掉，用静态服务器喂给浏览器，一秒出图——问题从\"渲染\"挪到了\"JS\"。第二件是在构建产物里搜那条路由：\n\n```bash\ngrep -l '\"\u002FProject\"' .output\u002Fpublic\u002F_nuxt\u002F*.js\n```\n\n搜得到，说明客户端包里确实多了这条路由记录。方向当场就定了。\n\n修法是不让框架把这条跳转变成客户端路由，放到服务端做：\n\n```ts\n\u002F\u002F apps\u002Fweb\u002Fserver\u002Fmiddleware\u002Fproject-redirect.ts\nexport default defineEventHandler((event) => {\n  const [path, query] = (event.path || '\u002F').split('?')\n  if (path !== '\u002FProject') return\n  return sendRedirect(event, query ? `\u002Fproject?${query}` : '\u002Fproject', 301)\n})\n```\n\n中间件只看实际进来的请求路径，不参与客户端路由表，没有自跳的机会。查询参数要带过去，我第一版漏了，从带参数的旧地址跳过去参数就没了。\n\n状态码也得说一下：之前那条规则名义上是永久重定向，实际返回的是 307 —— 框架给 redirect 的默认值就是 307，要 301 必须显式传对象。临时重定向不转移权重，搜索端会长期留着两个地址。这个问题不会当天暴露，是几个月后看收录数据才会发现。\n\n改完之后 \u002FProject 返回 301 指向 \u002Fproject，页面正常，那条警告也没了。验证方式我留了两条：curl 一次看状态码和 redirect_url，以及用无头浏览器跑一次真实绘制，看它能不能出图（改之前固定超时）。\n\n给 URL 改名的时候，\"旧地址怎么办\"确实是任务的一部分，但跳转要放在服务端做，尤其涉及大小写。另一件我以后再不会省的事：本地跑一遍不等于验完，dev 里那条保护性警告，恰恰就是生产环境打死页面的东西。\n\n还有个跟这个 bug 无关、但同一天发生的失误。我在那条 routeRules 的注释里同时写了两个断言 —— \"不会自循环\"和\"保留 301\"。第一个我确实验过，第二个没验就发上去了。一句话里有几个可验证的点，就得分别验完，验过一个不算整句成立。","线上事故记录",4,[71,75,79,83,84],{"slug":72,"title":73,"seriesIndex":74},"1-9g-nei-cun-de-fu-wu-qi","1.9G 内存的服务器上，nuxt build 在第 36 秒被杀",1,{"slug":76,"title":77,"seriesIndex":78},"wo-ba-compose-de-override-xie-cuo-zhan","我把 compose 的 override 写错，站点停了二十分钟",2,{"slug":80,"title":81,"seriesIndex":82},"jian-li-de-xie-gang-hui-bian-cheng","键里的斜杠会变成冒号：清缓存清了个空",3,{"slug":60,"title":61,"seriesIndex":69},{"slug":85,"title":86,"seriesIndex":87},"gang-deng-lu-jiu-guo-qi-yi-ci","一个 401，查出四个前端机制：缓存回放、隐藏页的 rAF、百分比圆角、缓动过冲",5,{"slug":89,"title":90},"cong-da-bao-shang-chuan-dao-push-ji","从打包上传到 push 即上线：把 CD 接通",{"slug":85,"title":86},[93,100,104,110,116],{"slug":85,"title":86,"excerpt":94,"date":95,"tags":96},"响应头里的 token 被 304 原样回放，把刚签发的新 token 顶掉，表现成刚登录就过期。顺带记三个前端细节：隐藏标签页里 rAF 不执行、扁盒子的 50% 圆角是胶囊、缓动曲线的过冲只有 0.0008。","2026-10-13",[97,98,99,66],"CSS","JavaScript","Vue",{"slug":80,"title":81,"excerpt":101,"date":102,"tags":103},"按源码里的分组名去匹配存储键，结果一条都没删掉，接口还回 200。返回一个可被证伪的数字，这类问题当场就能发现。","2026-09-30",[12,15],{"slug":105,"title":106,"excerpt":107,"date":108,"tags":109},"hou-tai-bao-cun-wen-zhang-qian-tai","后台保存文章，前台一小时没变","SSR 缓存要有主动失效这条通道。密钥两侧必须同值、这个接口不能对公网开、回执要能被打脸（计数）。","2026-09-27",[12],{"slug":111,"title":112,"excerpt":113,"date":114,"tags":115},"301-302-307-wo-yi-kai-shi-fen","301、302、307 我一开始分不清","永久和临时不只是名字区别，搜索端权重转移不一样。框架给的默认值不一定是你要的那个，得显式写。","2026-09-18",[12],{"slug":117,"title":118,"excerpt":119,"date":120,"tags":121},"rss-he-sitemap-shi-zen-me-bei-pa","RSS 和 sitemap 是怎么被爬虫看到的","生成时机、地址、robots 里要不要写。sitemap 带 lastmod 之后，改动被抓取的速度明显不一样。","2026-04-28",[12]]