[{"data":1,"prerenderedAt":483},["ShallowReactive",2],{"post:\u002Fblog\u002Fnuxt-4-architecture-subtraction":3,"surroundings:\u002Fblog\u002Fnuxt-4-architecture-subtraction":472},{"id":4,"title":5,"accent":6,"body":7,"description":451,"draft":452,"experience":453,"extension":454,"featured":452,"image":455,"meta":461,"navigation":462,"path":463,"publishedAt":464,"readingTime":100,"seo":465,"stem":466,"tags":467,"updatedAt":453,"__hash__":471},"posts\u002Fblog\u002Fnuxt-4-architecture-subtraction.md","Nuxt 4 的架构减法","mint",{"type":8,"value":9,"toc":443},"minimark",[10,14,22,27,30,104,107,114,117,120,261,264,284,287,290,293,296,299,305,309,316,324,327,330,333,336,410,413,416,419,436,439],[11,12,13],"p",{},"很多 Nuxt 项目变复杂，并不是因为业务真的复杂，而是因为我们太早把所有可能性都放进了架构。",[11,15,16,17,21],{},"个人博客是一个很好的校准器：页面数量有限、内容结构稳定、交互集中在少数区域。它提醒我们，框架最有价值的部分往往不是增加能力，而是",[18,19,20],"strong",{},"删除不必要的选择","。",[23,24,26],"h2",{"id":25},"从默认-ssr-开始","从默认 SSR 开始",[11,28,29],{},"技术文章需要被搜索引擎理解，也需要在弱网下尽早出现正文。Nuxt 默认的服务端渲染刚好符合这个目标。",[31,32,38],"pre",{"className":33,"code":34,"filename":35,"language":36,"meta":37,"style":37},"language-ts shiki shiki-themes github-dark github-dark","export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent'],\n  nitro: {\n    preset: 'cloudflare',\n  },\n})\n","nuxt.config.ts","ts","",[39,40,41,61,74,80,92,98],"code",{"__ignoreMap":37},[42,43,46,50,53,57],"span",{"class":44,"line":45},"line",1,[42,47,49],{"class":48},"sOPea","export",[42,51,52],{"class":48}," default",[42,54,56],{"class":55},"sFR8T"," defineNuxtConfig",[42,58,60],{"class":59},"suv1-","({\n",[42,62,64,67,71],{"class":44,"line":63},2,[42,65,66],{"class":59},"  modules: [",[42,68,70],{"class":69},"s4wv1","'@nuxt\u002Fcontent'",[42,72,73],{"class":59},"],\n",[42,75,77],{"class":44,"line":76},3,[42,78,79],{"class":59},"  nitro: {\n",[42,81,83,86,89],{"class":44,"line":82},4,[42,84,85],{"class":59},"    preset: ",[42,87,88],{"class":69},"'cloudflare'",[42,90,91],{"class":59},",\n",[42,93,95],{"class":44,"line":94},5,[42,96,97],{"class":59},"  },\n",[42,99,101],{"class":44,"line":100},6,[42,102,103],{"class":59},"})\n",[11,105,106],{},"这里没有把整个站改成 SPA，也没有为每个页面单独发明数据加载方式。页面在服务端完成第一次查询和渲染，客户端接管后继续使用同一套路由。",[11,108,109,110,113],{},"如果某一小块必须依赖浏览器 API，再局部使用 ",[39,111,112],{},"ClientOnly","。不要为了一个客户端组件牺牲整页的语义和首屏。",[23,115,116],{"id":116},"内容查询留在页面边界",[11,118,119],{},"首页、文章列表和文章详情需要的数据粒度不同。查询应该贴近消费它的页面：",[31,121,123],{"className":33,"code":122,"language":36,"meta":37,"style":37},"const { data: posts } = await useAsyncData('latest-posts', () => {\n  return queryCollection('posts')\n    .where('draft', '=', false)\n    .order('publishedAt', 'DESC')\n    .limit(6)\n    .all()\n})\n",[39,124,125,171,187,213,232,246,256],{"__ignoreMap":37},[42,126,127,130,133,137,140,144,147,150,153,156,159,162,165,168],{"class":44,"line":45},[42,128,129],{"class":48},"const",[42,131,132],{"class":59}," { ",[42,134,136],{"class":135},"s-3mD","data",[42,138,139],{"class":59},": ",[42,141,143],{"class":142},"s8ozJ","posts",[42,145,146],{"class":59}," } ",[42,148,149],{"class":48},"=",[42,151,152],{"class":48}," await",[42,154,155],{"class":55}," useAsyncData",[42,157,158],{"class":59},"(",[42,160,161],{"class":69},"'latest-posts'",[42,163,164],{"class":59},", () ",[42,166,167],{"class":48},"=>",[42,169,170],{"class":59}," {\n",[42,172,173,176,179,181,184],{"class":44,"line":63},[42,174,175],{"class":48},"  return",[42,177,178],{"class":55}," queryCollection",[42,180,158],{"class":59},[42,182,183],{"class":69},"'posts'",[42,185,186],{"class":59},")\n",[42,188,189,192,195,197,200,203,206,208,211],{"class":44,"line":76},[42,190,191],{"class":59},"    .",[42,193,194],{"class":55},"where",[42,196,158],{"class":59},[42,198,199],{"class":69},"'draft'",[42,201,202],{"class":59},", ",[42,204,205],{"class":69},"'='",[42,207,202],{"class":59},[42,209,210],{"class":142},"false",[42,212,186],{"class":59},[42,214,215,217,220,222,225,227,230],{"class":44,"line":82},[42,216,191],{"class":59},[42,218,219],{"class":55},"order",[42,221,158],{"class":59},[42,223,224],{"class":69},"'publishedAt'",[42,226,202],{"class":59},[42,228,229],{"class":69},"'DESC'",[42,231,186],{"class":59},[42,233,234,236,239,241,244],{"class":44,"line":94},[42,235,191],{"class":59},[42,237,238],{"class":55},"limit",[42,240,158],{"class":59},[42,242,243],{"class":142},"6",[42,245,186],{"class":59},[42,247,248,250,253],{"class":44,"line":100},[42,249,191],{"class":59},[42,251,252],{"class":55},"all",[42,254,255],{"class":59},"()\n",[42,257,259],{"class":44,"line":258},7,[42,260,103],{"class":59},[11,262,263],{},"这样做有几个直接好处：",[265,266,267,275,278,281],"ul",{},[268,269,270,271,274],"li",{},"数据参与 SSR，不需要 ",[39,272,273],{},"onMounted"," 后再闪烁加载；",[268,276,277],{},"查询键明确，Nuxt 可以在 hydration 时复用 payload；",[268,279,280],{},"页面知道自己需要什么，不必经过一层通用 store；",[268,282,283],{},"草稿过滤与排序规则出现在真正相关的位置。",[11,285,286],{},"只有当多个页面共享了同一段复杂业务状态时，才值得抽成 composable 或 store。",[23,288,289],{"id":289},"组件只抽取稳定重复",[11,291,292],{},"“看起来可能复用”不是组件边界。真正稳定的重复通常同时包含结构、语义和行为。",[11,294,295],{},"这个博客里，文章卡片是合适的组件：它在首页和索引页重复出现，拥有固定的元数据、标题、摘要、标签和跳转行为。",[11,297,298],{},"相反，首页 Hero 即使很大，也未必需要拆成五个小组件。它只在一个页面出现，内部元素共同完成一段视觉叙事。过早拆分只会让样式关系跨文件跳转。",[300,301,302],"blockquote",{},[11,303,304],{},"组件不是文件整理工具，而是可独立理解和复用的 UI 契约。",[23,306,308],{"id":307},"服务端-api-保持窄接口","服务端 API 保持窄接口",[11,310,311,312,315],{},"登录与留言属于运行时数据，放在 ",[39,313,314],{},"server\u002Fapi","。每个 endpoint 只做一件事：",[31,317,322],{"className":318,"code":320,"language":321,"meta":37},[319],"language-text","POST \u002Fapi\u002Fauth\u002Fregister\nPOST \u002Fapi\u002Fauth\u002Flogin\nPOST \u002Fapi\u002Fauth\u002Flogout\nGET  \u002Fapi\u002Fauth\u002Fsession\nGET  \u002Fapi\u002Fcomments\u002F:post\nPOST \u002Fapi\u002Fcomments\u002F:post\n","text",[39,323,320],{"__ignoreMap":37},[11,325,326],{},"窄接口使授权规则更容易检查。读取留言是公开的，创建留言需要会话；退出只销毁当前令牌，不需要触碰用户资料。",[11,328,329],{},"如果一个 endpoint 同时处理读取、写入、登录和格式转换，它就已经失去了清晰边界。",[23,331,332],{"id":332},"把状态放回正确的位置",[11,334,335],{},"Vue 的响应式状态很方便，但不是所有状态都应该进入 JavaScript：",[337,338,339,352],"table",{},[340,341,342],"thead",{},[343,344,345,349],"tr",{},[346,347,348],"th",{},"状态",[346,350,351],{},"最合适的位置",[353,354,355,364,375,383,394,402],"tbody",{},[343,356,357,361],{},[358,359,360],"td",{},"当前文章",[358,362,363],{},"路由",[343,365,366,369],{},[358,367,368],{},"搜索词",[358,370,371,372],{},"页面 ",[39,373,374],{},"ref",[343,376,377,380],{},[358,378,379],{},"标签筛选",[358,381,382],{},"URL query + 页面状态",[343,384,385,388],{},[358,386,387],{},"登录用户",[358,389,390,391],{},"SSR 安全的 ",[39,392,393],{},"useState",[343,395,396,399],{},[358,397,398],{},"文章正文",[358,400,401],{},"Nuxt Content",[343,403,404,407],{},[358,405,406],{},"会话真实性",[358,408,409],{},"服务端 D1",[11,411,412],{},"当状态位置正确时，大量“同步逻辑”会自然消失。我们不再需要监听路由去修复 store，也不需要用 localStorage 假装登录状态可靠。",[23,414,415],{"id":415},"减法不是功能更少",[11,417,418],{},"好的架构减法不会让用户少得到什么。相反，它把复杂度预算留给真正有感知的部分：",[265,420,421,424,427,430,433],{},[268,422,423],{},"更清晰的排版；",[268,425,426],{},"更快的首屏；",[268,428,429],{},"更可靠的登录；",[268,431,432],{},"更舒服的移动端阅读；",[268,434,435],{},"更容易坚持的 Markdown 写作流程。",[11,437,438],{},"Nuxt 4 提供了足够多的能力。项目成熟的标志，不是把它们全部用上，而是知道哪些能力可以暂时不用。",[440,441,442],"style",{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":37,"searchDepth":76,"depth":76,"links":444},[445,446,447,448,449,450],{"id":25,"depth":63,"text":26},{"id":116,"depth":63,"text":116},{"id":289,"depth":63,"text":289},{"id":307,"depth":63,"text":308},{"id":332,"depth":63,"text":332},{"id":415,"depth":63,"text":415},"一个内容型站点不需要复杂前端状态。让路由、服务端渲染和内容查询各自回到最擅长的位置。",false,null,"md",{"src":456,"alt":457,"caption":458,"width":459,"height":460},"\u002Fmedia\u002Fposts\u002Fnuxt-4-architecture-subtraction\u002Fcover-v1.webp","复杂模块逐步收敛为清晰精简的 Nuxt 应用核心","用架构减法换取清晰边界",1600,900,{},true,"\u002Fblog\u002Fnuxt-4-architecture-subtraction","2026-07-16",{"title":5,"description":451},"blog\u002Fnuxt-4-architecture-subtraction",[468,469,470],"Nuxt","Vue","架构","St4BQLFtVYREpXSjTqcxEzk7igp5zuh24rE67bUhxyc",[473,478],{"title":474,"path":475,"stem":476,"description":477,"children":-1},"从一封 GPT‑4 邀请邮件，到智能体时代","\u002Fblog\u002Ffrom-gpt-4-invite-to-agents","blog\u002Ffrom-gpt-4-invite-to-agents","2023 年，我等来的是一次 8K context 的 API 访问资格；三年后，我们开始学习如何向智能体交代目标、权限、边界与验收标准。",{"title":479,"path":480,"stem":481,"description":482,"children":-1},"AI 系统的可观测性，不只是 Token 账单","\u002Fblog\u002Fobservable-ai-systems","blog\u002Fobservable-ai-systems","从提示词到工具调用，再到用户可见结果，AI 应用需要一条能解释“为什么”的观测链路。",1785034337222]