[{"data":1,"prerenderedAt":671},["ShallowReactive",2],{"page-/post/nuxt/nuxt4-migration-from-nuxt3":3,"surrounding-page":662},{"id":4,"title":5,"author":6,"body":7,"date":649,"description":13,"extension":650,"group":651,"lastmod":649,"meta":652,"navigation":584,"path":653,"rawbody":654,"seo":655,"showTitle":5,"stem":656,"tags":657,"versions":659,"__hash__":661},"content/post/nuxt/nuxt4-migration-from-nuxt3.md","Nuxt 4 迁移清单：从 Nuxt 3 升级到 Nuxt 4（最少踩坑版）","阿Z",{"type":8,"value":9,"toc":631},"minimark",[10,14,23,31,34,46,51,54,89,92,100,104,107,112,137,142,158,161,169,173,178,181,235,239,245,251,304,310,338,341,346,350,353,356,366,369,385,388,392,395,406,409,412,416,419,489,492,495,505,509,519,522,526,529,532,546,549,553,569,573,616,619,627],[11,12,13],"p",{},"我写这篇的出发点很简单：",[15,16,17,20],"blockquote",{},[11,18,19],{},"本以为升级 Nuxt 4 就是把版本号改一下。",[11,21,22],{},"结果最容易翻车的，反而不是代码。",[11,24,25,26,30],{},"所以这篇不讲“新特性”，就给你一张",[27,28,29],"strong",{},"能照着做","的迁移清单。",[11,32,33],{},"官方升级文档在这（先收藏）：",[35,36,37],"ul",{},[38,39,40],"li",{},[41,42,43],"a",{"href":43,"rel":44},"https://nuxt.com/docs/4.x/getting-started/upgrade",[45],"nofollow",[47,48,50],"h2",{"id":49},"_0-先选迁移策略别一上来就乱改","0. 先选迁移策略（别一上来就乱改）",[11,52,53],{},"我一般会把迁移分两种打法：",[35,55,56,79],{},[38,57,58,61,62],{},[27,59,60],{},"策略 A：先升级版本，让项目先跑起来","（推荐）",[35,63,64,72],{},[38,65,66,67,71],{},"先把 ",[68,69,70],"code",{},"nuxt"," 升到 4.x，保证 CI / 本地能启动。",[38,73,74,75,78],{},"然后再慢慢处理目录结构（",[68,76,77],{},"app/","）这些“大工程”。",[38,80,81,84],{},[27,82,83],{},"策略 B：升级版本 + 立刻迁移到新目录结构",[35,85,86],{},[38,87,88],{},"适合：项目刚起步、结构很干净，改动成本小。",[11,90,91],{},"一句话总结：",[15,93,94,97],{},[11,95,96],{},"先跑起来。",[11,98,99],{},"再优雅。",[47,101,103],{"id":102},"_1-升级到-nuxt-4","1) 升级到 Nuxt 4",[11,105,106],{},"两种方式，选一种就行：",[35,108,109],{},[38,110,111],{},"升级到最新稳定：",[113,114,119],"pre",{"className":115,"code":116,"language":117,"meta":118,"style":118},"language-bash shiki shiki-themes github-light","npx nuxt upgrade\n","bash","",[68,120,121],{"__ignoreMap":118},[122,123,126,130,134],"span",{"class":124,"line":125},"line",1,[122,127,129],{"class":128},"s7eDp","npx",[122,131,133],{"class":132},"sYBdl"," nuxt",[122,135,136],{"class":132}," upgrade\n",[35,138,139],{},[38,140,141],{},"或者手动指定 Nuxt 4：",[113,143,145],{"className":115,"code":144,"language":117,"meta":118,"style":118},"pnpm add nuxt@^4.0.0\n",[68,146,147],{"__ignoreMap":118},[122,148,149,152,155],{"class":124,"line":125},[122,150,151],{"class":128},"pnpm",[122,153,154],{"class":132}," add",[122,156,157],{"class":132}," nuxt@^4.0.0\n",[11,159,160],{},"我会建议你先做一件很“程序员”的事：",[15,162,163,166],{},[11,164,165],{},"先别急着改一堆配置。",[11,167,168],{},"第一目标是能启动。",[47,170,172],{"id":171},"_2-迁移重点-1新目录结构app","2) 迁移重点 1：新目录结构（app/）",[174,175,177],"h3",{"id":176},"_21-nuxt-4-默认变了什么重点看路径解析","2.1 Nuxt 4 默认变了什么（重点看路径解析）",[11,179,180],{},"Nuxt 4 默认启用新的目录结构（但它有向后兼容：如果检测到你在用旧结构，会继续按旧结构跑）。官方要点我按“会影响你找不到文件”的方式翻译一下：",[35,182,183,196,209,226],{},[38,184,185,186,189,190,192,193,195],{},"默认 ",[68,187,188],{},"srcDir"," 变成 ",[68,191,77],{},"，大部分东西从 ",[68,194,77],{}," 解析",[38,197,198,201,202,205,206,208],{},[68,199,200],{},"serverDir"," 默认变成 ",[68,203,204],{},"\u003CrootDir>/server","（不再跟着 ",[68,207,188],{},"）",[38,210,211,214,215,214,218,221,222,225],{},[68,212,213],{},"layers/","、",[68,216,217],{},"modules/",[68,219,220],{},"public/"," 默认按 ",[68,223,224],{},"\u003CrootDir>"," 去找",[38,227,228,229,232,233,225],{},"如果你用了 Nuxt Content v2.13+，",[68,230,231],{},"content/"," 也是按 ",[68,234,224],{},[174,236,238],{"id":237},"_22-你要迁移的话照着这个搬清单版","2.2 你要迁移的话，照着这个搬（清单版）",[11,240,241,242,244],{},"1）创建 ",[68,243,77],{}," 目录",[11,246,247,248,250],{},"2）把这些目录/文件移动到 ",[68,249,77],{}," 下：",[35,252,253,258,263,268,273,278,283,288,293],{},[38,254,255],{},[68,256,257],{},"assets/",[38,259,260],{},[68,261,262],{},"components/",[38,264,265],{},[68,266,267],{},"composables/",[38,269,270],{},[68,271,272],{},"layouts/",[38,274,275],{},[68,276,277],{},"middleware/",[38,279,280],{},[68,281,282],{},"pages/",[38,284,285],{},[68,286,287],{},"plugins/",[38,289,290],{},[68,291,292],{},"utils/",[38,294,295,214,298,214,301],{},[68,296,297],{},"app.vue",[68,299,300],{},"error.vue",[68,302,303],{},"app.config.ts",[11,305,306,307,309],{},"3）这些保持在根目录（不要放进 ",[68,308,77],{},"）：",[35,311,312,317,321,325,329,333],{},[38,313,314],{},[68,315,316],{},"nuxt.config.ts",[38,318,319],{},[68,320,231],{},[38,322,323],{},[68,324,213],{},[38,326,327],{},[68,328,217],{},[38,330,331],{},[68,332,220],{},[38,334,335],{},[68,336,337],{},"server/",[11,339,340],{},"4）顺手检查一下第三方配置",[35,342,343],{},[38,344,345],{},"tailwind / eslint / typescript 有没有写死旧目录路径（很容易被忽略）",[174,347,349],{"id":348},"_23-我不想迁移结构行不行","2.3 我不想迁移结构行不行？",[11,351,352],{},"可以。",[11,354,355],{},"Nuxt 4 通常能自动识别旧结构。",[11,357,358,359,361,362,365],{},"但我想提醒你一个“看起来很无辜，其实很坑”的点：如果你自定义过 ",[68,360,188],{},"，那 Nuxt 4 的解析基准会让 ",[68,363,364],{},"modules/public/server"," 的解析行为跟 Nuxt 3 不一样。",[11,367,368],{},"这时候别硬扛，直接显式配置：",[35,370,371,376,381],{},[38,372,373],{},[68,374,375],{},"dir.modules",[38,377,378],{},[68,379,380],{},"dir.public",[38,382,383],{},[68,384,200],{},[11,386,387],{},"（官方文档里也有“强制 v3 结构”的配置思路。）",[47,389,391],{"id":390},"_3-迁移重点-2usefetch-useasyncdata-的同-key-共享","3) 迁移重点 2：useFetch / useAsyncData 的“同 key 共享”",[11,393,394],{},"Nuxt 4 把数据获取层整理了一下（官方叫：Singleton Data Fetching Layer）。核心变化一句话：",[15,396,397],{},[11,398,399],{},[27,400,401,402,405],{},"同一个 key 的 ",[68,403,404],{},"useAsyncData/useFetch"," 会共享 data/error/status refs。",[11,407,408],{},"听起来挺美好对吧？",[11,410,411],{},"但是它会带来两个实际迁移点。",[174,413,415],{"id":414},"_31-同-key-但-options-不一致-会冲突","3.1 同 key 但 options 不一致 → 会冲突",[11,417,418],{},"以前你可能在不同组件里这么写：",[113,420,424],{"className":421,"code":422,"language":423,"meta":118,"style":118},"language-ts shiki shiki-themes github-light","useAsyncData('users', () => $fetch('/api/users'), { deep: false })\nuseAsyncData('users', () => $fetch('/api/users'), { deep: true })\n","ts",[68,425,426,463],{"__ignoreMap":118},[122,427,428,431,435,438,441,445,448,450,453,456,460],{"class":124,"line":125},[122,429,430],{"class":128},"useAsyncData",[122,432,434],{"class":433},"sgsFI","(",[122,436,437],{"class":132},"'users'",[122,439,440],{"class":433},", () ",[122,442,444],{"class":443},"sD7c4","=>",[122,446,447],{"class":128}," $fetch",[122,449,434],{"class":433},[122,451,452],{"class":132},"'/api/users'",[122,454,455],{"class":433},"), { deep: ",[122,457,459],{"class":458},"sYu0t","false",[122,461,462],{"class":433}," })\n",[122,464,466,468,470,472,474,476,478,480,482,484,487],{"class":124,"line":465},2,[122,467,430],{"class":128},[122,469,434],{"class":433},[122,471,437],{"class":132},[122,473,440],{"class":433},[122,475,444],{"class":443},[122,477,447],{"class":128},[122,479,434],{"class":433},[122,481,452],{"class":132},[122,483,455],{"class":433},[122,485,486],{"class":458},"true",[122,488,462],{"class":433},[11,490,491],{},"Nuxt 4 下会警告/不一致，因为它们共享同一份 refs。",[11,493,494],{},"我的建议很直接：",[35,496,497,502],{},[38,498,499],{},[27,500,501],{},"把这类“同 key + 自定义 options”的调用抽成一个 composable",[38,503,504],{},"保证所有地方用同一份配置（不要各写各的）",[174,506,508],{"id":507},"_32-getcacheddata-行为变化","3.2 getCachedData 行为变化",[11,510,511,514,515,518],{},[68,512,513],{},"getCachedData"," 现在触发更频繁（包括 watcher / refresh），并且多了 ",[68,516,517],{},"ctx"," 参数让你判断触发原因（initial / refresh / watch…）。",[11,520,521],{},"如果你之前写过缓存逻辑，升级后记得按官方提示把签名改掉。",[47,523,525],{"id":524},"_4-迁移工具codemods能省很多手工活","4) 迁移工具：Codemods（能省很多手工活）",[11,527,528],{},"Nuxt 官方提到可以用 Codemod 自动化一部分迁移步骤。",[11,530,531],{},"我自己的推荐姿势是：",[35,533,534,537,540,543],{},[38,535,536],{},"开一个干净分支",[38,538,539],{},"跑 codemod",[38,541,542],{},"review diff",[38,544,545],{},"再合到主分支",[11,547,548],{},"不然你会得到一个“我也不知道它改了啥，但反正能跑”的神秘提交。（我不想你这样。）",[47,550,552],{"id":551},"_5-常见症状你以为是-bug其实是迁移遗漏","5) 常见症状：你以为是 bug，其实是迁移遗漏",[35,554,555,558,561],{},[38,556,557],{},"Dev 启动变慢 / watch 报错：很多时候与目录结构（根目录文件太多）有关",[38,559,560],{},"同 key 的请求状态互相影响：本质是 singleton 行为",[38,562,563,564,566,567],{},"server 目录解析位置变化导致路径不对：重点检查 ",[68,565,200],{}," 与 ",[68,568,188],{},[47,570,572],{"id":571},"_6-最短验收清单按这个过一遍就差不多了","6) 最短验收清单（按这个过一遍就差不多了）",[35,574,577,591,600,610],{"className":575},[576],"contains-task-list",[38,578,581,586,587,590],{"className":579},[580],"task-list-item",[582,583],"input",{"disabled":584,"type":585},true,"checkbox"," ",[68,588,589],{},"pnpm dev"," 可以启动并打开首页",[38,592,594,586,596,599],{"className":593},[580],[582,595],{"disabled":584,"type":585},[68,597,598],{},"pnpm build"," 可以通过",[38,601,603,605,606,609],{"className":602},[580],[582,604],{"disabled":584,"type":585}," 核心页面的 ",[68,607,608],{},"useFetch/useAsyncData"," 没有“同 key 不一致 options”警告",[38,611,613,615],{"className":612},[580],[582,614],{"disabled":584,"type":585}," 如果用了 content：文章都能被正确解析（frontmatter YAML 无报错）",[47,617,618],{"id":618},"参考",[35,620,621],{},[38,622,623,624],{},"Nuxt Upgrade Guide（Nuxt 4）：",[41,625,43],{"href":43,"rel":626},[45],[628,629,630],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}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 pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}",{"title":118,"searchDepth":465,"depth":465,"links":632},[633,634,635,641,645,646,647,648],{"id":49,"depth":465,"text":50},{"id":102,"depth":465,"text":103},{"id":171,"depth":465,"text":172,"children":636},[637,639,640],{"id":176,"depth":638,"text":177},3,{"id":237,"depth":638,"text":238},{"id":348,"depth":638,"text":349},{"id":390,"depth":465,"text":391,"children":642},[643,644],{"id":414,"depth":638,"text":415},{"id":507,"depth":638,"text":508},{"id":524,"depth":465,"text":525},{"id":551,"depth":465,"text":552},{"id":571,"depth":465,"text":572},{"id":618,"depth":465,"text":618},"2026-02-13T00:00:00.000Z","md",null,{},"/post/nuxt/nuxt4-migration-from-nuxt3","---\ntitle: Nuxt 4 迁移清单：从 Nuxt 3 升级到 Nuxt 4（最少踩坑版）\ndate: 2026-02-13\nlastmod: 2026-02-13\ntags: [\"Nuxt\"]\nversions: [\"nuxt@4.x\"]\nauthor: \"阿Z\"\nshowTitle: Nuxt 4 迁移清单：从 Nuxt 3 升级到 Nuxt 4（最少踩坑版）\n---\n\n我写这篇的出发点很简单：\n\n> 本以为升级 Nuxt 4 就是把版本号改一下。\n>\n> 结果最容易翻车的，反而不是代码。\n\n所以这篇不讲“新特性”，就给你一张**能照着做**的迁移清单。\n\n官方升级文档在这（先收藏）：\n- https://nuxt.com/docs/4.x/getting-started/upgrade\n\n## 0. 先选迁移策略（别一上来就乱改）\n\n我一般会把迁移分两种打法：\n\n- **策略 A：先升级版本，让项目先跑起来**（推荐）\n  - 先把 `nuxt` 升到 4.x，保证 CI / 本地能启动。\n  - 然后再慢慢处理目录结构（`app/`）这些“大工程”。\n\n- **策略 B：升级版本 + 立刻迁移到新目录结构**\n  - 适合：项目刚起步、结构很干净，改动成本小。\n\n一句话总结：\n\n> 先跑起来。\n>\n> 再优雅。\n\n## 1) 升级到 Nuxt 4\n\n两种方式，选一种就行：\n\n- 升级到最新稳定：\n\n```bash\nnpx nuxt upgrade\n```\n\n- 或者手动指定 Nuxt 4：\n\n```bash\npnpm add nuxt@^4.0.0\n```\n\n我会建议你先做一件很“程序员”的事：\n\n> 先别急着改一堆配置。\n>\n> 第一目标是能启动。\n\n## 2) 迁移重点 1：新目录结构（app/）\n\n### 2.1 Nuxt 4 默认变了什么（重点看路径解析）\n\nNuxt 4 默认启用新的目录结构（但它有向后兼容：如果检测到你在用旧结构，会继续按旧结构跑）。官方要点我按“会影响你找不到文件”的方式翻译一下：\n\n- 默认 `srcDir` 变成 `app/`，大部分东西从 `app/` 解析\n- `serverDir` 默认变成 `\u003CrootDir>/server`（不再跟着 `srcDir`）\n- `layers/`、`modules/`、`public/` 默认按 `\u003CrootDir>` 去找\n- 如果你用了 Nuxt Content v2.13+，`content/` 也是按 `\u003CrootDir>` 去找\n\n### 2.2 你要迁移的话，照着这个搬（清单版）\n\n1）创建 `app/` 目录\n\n2）把这些目录/文件移动到 `app/` 下：\n- `assets/`\n- `components/`\n- `composables/`\n- `layouts/`\n- `middleware/`\n- `pages/`\n- `plugins/`\n- `utils/`\n- `app.vue`、`error.vue`、`app.config.ts`\n\n3）这些保持在根目录（不要放进 `app/`）：\n- `nuxt.config.ts`\n- `content/`\n- `layers/`\n- `modules/`\n- `public/`\n- `server/`\n\n4）顺手检查一下第三方配置\n- tailwind / eslint / typescript 有没有写死旧目录路径（很容易被忽略）\n\n### 2.3 我不想迁移结构行不行？\n\n可以。\n\nNuxt 4 通常能自动识别旧结构。\n\n但我想提醒你一个“看起来很无辜，其实很坑”的点：如果你自定义过 `srcDir`，那 Nuxt 4 的解析基准会让 `modules/public/server` 的解析行为跟 Nuxt 3 不一样。\n\n这时候别硬扛，直接显式配置：\n- `dir.modules`\n- `dir.public`\n- `serverDir`\n\n（官方文档里也有“强制 v3 结构”的配置思路。）\n\n## 3) 迁移重点 2：useFetch / useAsyncData 的“同 key 共享”\n\nNuxt 4 把数据获取层整理了一下（官方叫：Singleton Data Fetching Layer）。核心变化一句话：\n\n> **同一个 key 的 `useAsyncData/useFetch` 会共享 data/error/status refs。**\n\n听起来挺美好对吧？\n\n但是它会带来两个实际迁移点。\n\n### 3.1 同 key 但 options 不一致 → 会冲突\n\n以前你可能在不同组件里这么写：\n\n```ts\nuseAsyncData('users', () => $fetch('/api/users'), { deep: false })\nuseAsyncData('users', () => $fetch('/api/users'), { deep: true })\n```\n\nNuxt 4 下会警告/不一致，因为它们共享同一份 refs。\n\n我的建议很直接：\n- **把这类“同 key + 自定义 options”的调用抽成一个 composable**\n- 保证所有地方用同一份配置（不要各写各的）\n\n### 3.2 getCachedData 行为变化\n\n`getCachedData` 现在触发更频繁（包括 watcher / refresh），并且多了 `ctx` 参数让你判断触发原因（initial / refresh / watch…）。\n\n如果你之前写过缓存逻辑，升级后记得按官方提示把签名改掉。\n\n## 4) 迁移工具：Codemods（能省很多手工活）\n\nNuxt 官方提到可以用 Codemod 自动化一部分迁移步骤。\n\n我自己的推荐姿势是：\n- 开一个干净分支\n- 跑 codemod\n- review diff\n- 再合到主分支\n\n不然你会得到一个“我也不知道它改了啥，但反正能跑”的神秘提交。（我不想你这样。）\n\n## 5) 常见症状：你以为是 bug，其实是迁移遗漏\n\n- Dev 启动变慢 / watch 报错：很多时候与目录结构（根目录文件太多）有关\n- 同 key 的请求状态互相影响：本质是 singleton 行为\n- server 目录解析位置变化导致路径不对：重点检查 `serverDir` 与 `srcDir`\n\n## 6) 最短验收清单（按这个过一遍就差不多了）\n\n- [ ] `pnpm dev` 可以启动并打开首页\n- [ ] `pnpm build` 可以通过\n- [ ] 核心页面的 `useFetch/useAsyncData` 没有“同 key 不一致 options”警告\n- [ ] 如果用了 content：文章都能被正确解析（frontmatter YAML 无报错）\n\n## 参考\n- Nuxt Upgrade Guide（Nuxt 4）：https://nuxt.com/docs/4.x/getting-started/upgrade\n",{"title":5,"description":13},"post/nuxt/nuxt4-migration-from-nuxt3",[658],"Nuxt",[660],"nuxt@4.x","SxLlEVsYo4-P_5XEoTIVhOfGqpZetTW_e7cdhvIUPzw",[663,667],{"title":664,"path":665,"stem":666,"children":-1},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":668,"path":669,"stem":670,"children":-1},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005084793]