1. 拆掉“官方文档”这层滤镜:它其实是一份可运行的 Vue.js 通用应用教科书
很多同学第一次打开 Nuxt 2 官方文档网站,目的很单纯:查个 API、抄个配置、看看asyncData到底怎么写。我最初也是这样,页面右上角搜索框里敲几个关键词,找到代码片段,复制到项目里,跑通就关掉。直到我手上一个内容型站点因为 SEO 问题被反复投诉,我才回头把这份文档从头到尾当一本书读了一遍,读完的第一感受是:它根本不像一份 API 手册,更像一份把工程决策、目录约束、渲染策略都摊开给你看的实战笔记。
Nuxt 2 是建立在 Vue.js 之上的通用应用框架,它把路由、状态管理、元信息管理、构建打包这些原本要你手写的事情全部收进约定里。而这份官方文档网站,本身就是用 Nuxt 2 构建的,也就是说,你读的每一页排版、每一次跳转、每一条侧边栏展开逻辑,背后都是它自己那套机制在跑。这个特性很值钱——你可以把它当成一个持续在线的、可被浏览器调试工具解剖的样本工程。
这篇文章不打算复述 API 列表,那没意义。我想把这份文档背后真正值得学的东西拆成四块:目录约定为什么这么设计、渲染模式怎么选、首屏数据从哪来、出问题时往哪查。适合已经会写 Vue.js 单文件组件、但一碰到服务端渲染就发怵的开发者,也适合那些项目里 Nuxt 2 版本已经跑了两三年、想系统性补课的人。你要是纯新手,也能看懂,只是中间几节需要跟着动手敲一遍才有感觉。
1.1 从一次后台改版说起:我为什么要啃这份文档
我接手过一个内容站,最早是纯 Vue.js 的单页应用。首页打开是一段空白,等 JS 加载完再渲染内容,抓取工具拿到的是一份骨架 HTML。改版目标很朴素:让内容出现在首屏源码里,顺便把首屏时间压下去。当时的候选方案有两个,一是自己搭一套服务端渲染的壳子,二是直接上 Nuxt。
自己搭壳子这条路我试了三天,卡在三个地方:路由和组件映射要手写、打包配置要区分服务端和客户端两份、样式在服务端渲染时无法正确提取导致页面闪烁。这三件事在 Nuxt 2 里都有默认解,而且默认解的合理性很高。比如打包这件事,框架内部已经把客户端和服务端拆成了两份 webpack 配置,你不需要理解全部细节,只要知道build里的扩展点在哪就行。
这就是我后来读文档时的基本心态:不要把它当成“功能介绍”,当成“别人踩完坑之后定下来的规范”。你看到的每一个目录名、每一个默认值,背后都对应着一个具体的问题。layouts目录不是凭空冒出来的,它是为了让不同页面共享外壳又不互相污染;middleware不是多余的一层,它是为了解决“进入页面前先判断一次”这种横切需求。
1.2 文档站的组织逻辑:为什么它读起来不像 API 手册
把目录摊开你会发现它的排序很讲究:先讲概念和目录结构,再讲配置项,最后才是命令和部署。这个顺序和大多数人的学习顺序是反的——大部分人想直接看配置,但它偏要先让你接受约定。
我后来理解了这样排的用意。Nuxt 2 的核心竞争力不是某个 API 强大,而是“你按我的目录放文件,我就帮你把剩下的都接上”。如果你跳过约定直接看配置,很容易把框架用成一个需要大量补丁的半成品:路由手写、元信息手写、数据请求手写,最后得到的是一个“装了 Nuxt 的普通 Vue 项目”,收益几乎为零。
所以读这份文档有个技巧:第一遍只读概念和目录两章,把pages、layouts、components、plugins、middleware、store、static、assets这八个目录的分工记牢;第二遍再回头查配置。这个顺序能让你在写代码时少走很多弯路,因为你会在动手前就知道“这段逻辑应该放哪”,而不是写到一半发现放错地方再搬。
1.3 哪些人真的该读它,哪些人可以绕开
说句实话,不是所有人都需要精读。如果你只是做内部管理系统,用户固定、不需要被搜索引擎抓取、首屏慢两秒也没人在意,那用纯 Vue.js 单页应用就够了,上 Nuxt 属于给自己增加构建复杂度。这类项目里 Nuxt 的收益主要来自约定式路由和自动引入,收益有限。
但下面这几类场景,值得认真读:内容型站点、电商商品页、需要分享卡片的营销页、需要按路由做服务端缓存的场景。共同点是“首屏可被外部读取”这件事很重要。一旦这个前提成立,服务端渲染带来的收益会覆盖掉你为它付出的构建成本。
还有一类人我也建议读:手上项目已经在用 Nuxt 2、但代码写得很别扭的团队。常见症状是所有页面都在mounted里发请求、layouts里塞了一堆业务逻辑、plugins里挂了十几个全局方法。这些问题不是框架的锅,是没吃透约定导致的。读完目录那一章,很多重构方向会自然浮现。
提醒:Nuxt 2 已经进入停止维护阶段,新项目不建议从它起步。但存量项目极多,读它的文档对理解“约定式框架”这套思路仍然很有价值,迁移到新一代方案时这套认知可以直接复用。
2. 目录结构与约定:Nuxt 2 最值钱的设计都藏在这里
很多人觉得“约定优于配置”是句空话,直到他们第一次在 Nuxt 2 里新建一个文件就自动有了路由。这个体验很直观,但真正值得学的是约定背后的边界划分:哪些东西该全局共享、哪些东西该按页面隔离、哪些东西该在构建期处理。这一章我想把这份文档里关于目录的部分拆细,并且给出可以直接抄的骨架。
我见过太多项目把components目录当垃圾桶,什么组件都往里扔,最后几百个文件平铺在一起;也见过把业务逻辑全塞plugins的,全局注入了一堆方法,谁也说不清哪个方法被谁用。这些问题的根源都相同:没搞清楚每个目录的职责边界。
2.1 一次初始化,一份能直接抄的目录骨架
先看初始化。文档里给出的命令是创建项目的最短路径,但真正关键的是它生成出来的目录层级。我按自己的使用习惯整理了一份带注释的版本,新项目可以直接照着建。
# 创建项目,交互式选择包管理器和 UI 框架 npx create-nuxt-app my-docs-site # 或者最简依赖手装 npm init -y npm i nuxt@^2.15.8 vue@^2.6.14 vue-server-renderer@^2.6.14 vue-template-compiler@^2.6.14vue、vue-server-renderer、vue-template-compiler三者的版本必须严格对齐,这是最容易翻车的地方。vue-server-renderer的版本和vue不一致时,构建阶段报的错往往莫名其妙,比如提示某个渲染方法不存在,实际原因只是版本错位。我习惯在package.json里把这三个都写成固定版本,不加^。
目录骨架大致是这样:
my-docs-site/ ├── assets/ # 需要被 webpack 处理的资源:样式、字体、图片 ├── components/ # 可复用组件,按业务域分子目录 ├── layouts/ # 页面外壳,default.vue 是默认外壳 ├── middleware/ # 路由中间件,进入页面前执行 ├── pages/ # 路由的唯一来源,文件即路由 ├── plugins/ # 需要全局注册的第三方库或方法 ├── static/ # 原样拷贝到根目录的静态文件,不进 webpack ├── store/ # Vuex 状态,按文件自动拆模块 ├── nuxt.config.js # 全局配置 └── package.jsonassets和static的区别是新手最容易混淆的。简单记:assets里的文件会被 webpack 当成模块处理,支持路径别名、会被哈希命名、可以按需打包;static里的文件原封不动拷到输出目录根路径,文件名不变。文档站里那种需要固定 URL 的favicon.ico、robots.txt、第三方校验文件,放static;项目里引用的图片资源,放assets。
2.2 pages 目录与自动路由:文件名就是 URL
这一块是 Nuxt 2 最直观的设计。pages/index.vue对应/,pages/about.vue对应/about,pages/docs/guide.vue对应/docs/guide。目录嵌套就是 URL 嵌套,不需要写一行路由配置。文档站那么大一棵侧边栏树,本质上就是pages目录的镜像。
动态路由用下划线前缀表示。pages/_slug.vue匹配一级任意路径,参数通过this.$route.params.slug拿到;pages/docs/_category/_slug.vue匹配/docs/a/b这种两级结构。这个设计在文档站里非常实用,因为文档天然是“分类 + 页面”的两级结构。
我遇到过一个问题值得说:动态路由和静态路由并存时,优先级怎么定?答案是静态路由优先。你同时有pages/docs/index.vue和pages/docs/_slug.vue,访问/docs/会命中前者,访问/docs/anything命中后者。这个规则很符合直觉,但要注意pages/docs/index.vue和pages/docs.vue是两个不同的东西——前者匹配/docs/,后者匹配/docs,做面包屑和侧边栏高亮时这点差异会咬人。
还有一层是路由校验。动态路由默认什么都能匹配,意味着任意乱码路径都会命中你的页面组件,然后你在组件内部发现拿不到数据,只能手写一个 404 分支。文档里提供了validate方法,在页面组件里定义它,返回false时会直接走错误页,比在组件内部判断干净得多。
export default { validate({ params }) { // 只允许字母、数字、短横线,长度 1 到 80 return /^[a-z0-9-]{1,80}$/.test(params.slug) } }这个正则在文档站场景里很必要。不加校验时,爬虫或者用户拼错路径都会命中同一个页面组件,服务端仍然返回 200,搜索引擎会认为这个页面存在,久而久之索引里全是无效地址。
2.3 layouts、components、store 的边界怎么划
layouts是页面外壳,pages是内容主体,这个关系要建立起来。默认情况下所有页面套用layouts/default.vue,需要换外壳的页面在组件里写layout: 'docs'就能切换。文档站通常有两种外壳:首页用营销型布局,内页用“顶栏 + 左侧导航 + 正文”的布局。
我在实际项目里定过一条规矩,后来一直沿用:layouts里只放跨页面稳定的结构,任何会随路由变化而重新请求的逻辑都不放。原因是layouts在路由切换时不一定重新渲染,放进去的数据请求可能只在首次进入时执行一次,后续切换页面拿到的是旧数据。这个坑很隐蔽,表现为“侧边栏的目录树在切换分类后不更新”。
components目录按业务域分二级目录比平铺强太多。我的习惯是按layout、content、common三类分:
| 子目录 | 放什么 | 典型例子 |
|---|---|---|
layout/ | 外壳内的结构件 | 顶栏、侧边栏、页脚、目录树 |
content/ | 正文渲染相关 | 代码块、提示框、表格包装、锚点标题 |
common/ | 与业务无关的通用件 | 按钮、图标、加载骨架 |
store目录在 Nuxt 2 里有个自动拆模块的机制:目录下的每个.js文件会变成一个命名空间模块,文件名就是模块名。这个机制省掉了手写modules的功夫,但也带来一个约束——你不能在store/index.js里写全局 mutation 之后又期望各模块共享,跨模块调用要走dispatch('模块名/action')。文档站场景里通常只需要一个store/ui.js管侧边栏展开状态,没必要上全套。
3. 渲染模式与数据流:文档站“打开就能看”的底层原因
这一章是整份文档里技术密度最高的部分,也是最容易被跳过的部分。大多数人对渲染模式的理解停留在“服务端渲染 SEO 好”这一句话上,但真到了要选型、要算成本、要处理首屏数据注入的时候,一句话解释不了任何问题。
我把这块拆成三层:先选模式,再谈数据从哪进,最后看配置项怎么调。三层是递进关系,跳过任何一层,后面都会出问题。
3.1 server、static、spa 三种模式,文档站该选哪个
Nuxt 2 的模式由两个配置项共同决定:mode和target。mode控制运行时行为(universal表示服务端渲染,spa表示纯客户端渲染),target控制构建产物(server产出需要 Node 进程托管的服务,static产出纯静态文件)。
这四种组合里,三种有意义:
mode: 'universal'+target: 'server':每个请求进来由 Node 实时渲染,内容可以按请求变化,适合登录态相关或实时性要求高的场景。mode: 'universal'+target: 'static':构建期把所有路由预渲染成 HTML 文件,部署到任意静态托管。首屏最快,服务器成本最低。mode: 'spa'+target: 'static':产出单页应用,服务端不渲染,SEO 无收益。
文档站这类内容站,最优解几乎总是第二种。原因是内容更新频率低、页面数量有限、没有个性化内容,构建期预渲染完全够用,而且省掉了 Node 服务的运维成本和实时渲染的 CPU 开销。
选型的判断依据我给一个简单的标准:页面内容是否随请求中的用户身份变化。变,就走服务端实时渲染;不变,就走构建期静态生成。绝大多数文档站属于后者。这个判断能省掉一大半架构讨论。
注意:
target: 'static'不代表完全没有服务端逻辑,构建期仍然会跑一遍服务端渲染来生成 HTML。所以异步数据请求里的服务端代码依然要保证在 Node 环境下能跑,不能碰window。
3.2 asyncData 与 fetch:首屏数据到底在哪一步塞进去
这是所有人都会卡住的地方。页面上的数据从哪来、什么时候来、怎么和模板绑定,这三个问题答不清楚,写出来的代码就会在“有时正常、有时空白”之间反复横跳。
asyncData是页面级方法,只在页面组件里可用,第一个参数是上下文对象(含params、query、store、error),返回值会被合并进组件的data。关键点是它在组件实例创建之前执行,所以拿不到this,这也是很多人第一次写就报错的原因。
export default { async asyncData({ params, $axios, error }) { try { const { data } = await $axios.get(`/api/docs/${params.slug}`) return { doc: data, toc: data.toc } } catch (e) { // 接口返回 404 时主动抛错,让框架走错误页 error({ statusCode: 404, message: '页面不存在' }) } } }fetch在 2.12 之后换了实现,现在它可以访问this,可以修改组件状态,但返回值不会被合并进data,需要自己赋值。两者的选择标准我的建议是:需要把数据当组件初始状态用的,用asyncData;需要在数据回来之后做副作用(比如设置元信息、调用 store action)的,用fetch。
两者共同的痛点是没有内置超时。上游接口卡住,服务端渲染就会一直挂着,直到进程级的超时兜底。我在生产环境吃过这个亏,某次上游抖动导致构建卡死四十分钟。解法是自己在请求层加超时,或者用Promise.race包一层:
function withTimeout(promise, ms = 5000) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error('asyncData timeout')), ms) ) ]) }超时时间给多少合适,这个可以算。假设你的静态生成阶段需要预渲染 800 个页面,单页平均请求耗时 200ms,并发 10,总耗时约 16 秒。如果某一页的请求卡到 30 秒,整体时间会被拉长到接近一分钟。设 5 秒超时,最坏情况整体也就多出几秒,且失败页面会被标记出来重跑。这个取舍在构建期比在运行时更重要。
3.3 nuxt.config.js 逐项拆解与参数计算
配置文件是这份文档里查阅频率最高的一页,但很多人只抄片段不问为什么。我挑几个真正影响文档站表现的项说。
head负责全局元信息,页面组件里可以覆盖。文档站最容易被忽略的是titleTemplate,它让每个页面标题自动带上站名后缀,省掉逐页拼接。
generate在静态生成时最关键。dir决定输出目录,fallback决定要不要生成一个兜底的 SPA 页面,routes决定额外的动态路由列表,concurrency控制并发抓取数。并发数默认是 500,这个值在页面少的时候很爽,页面多了会把构建机内存吃满。我一般按内存反推:单页渲染峰值按 40MB 估算,一台 2GB 可用内存的构建机,安全并发大概在 20 到 30 之间,留出余量取 10 更稳。
export default { target: 'static', head: { titleTemplate: '%s · 文档站', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' } ] }, generate: { dir: 'dist', fallback: '200.html', concurrency: 10, routes() { // 从接口或本地文件读出所有文档路径 return fetch('https://example.invalid/api/routes') .then(res => res.json()) .then(list => list.map(item => `/docs/${item.category}/${item.slug}`)) } }, build: { analyze: process.env.ANALYZE === 'true', optimization: { splitChunks: { chunks: 'all' } } } }build.analyze打开后构建结束会自动弹出一份依赖体积报告页。这个功能我建议每个项目在优化首屏前先跑一次,因为很多体积问题一眼就能看出来——最常见的是一个只需要几个工具函数的库,把整包都打进了主 chunk。判断标准很直白:主 chunk 超过 300KB(未压缩)就该拆。
再补一个render配置。如果你走服务端实时渲染,render.compressor和render.etag会显著影响首字节时间。开启 gzip 之后 HTML 体积通常能压缩到原来的三成左右,一份 80KB 的文档页面压到 25KB 上下。这个收益在弱网环境下是决定性的。静态托管场景下这一层交给托管平台处理即可,不用在框架层重复配置。
4. 从零跑起来:完整实操链路与关键环节
前面讲的都是“为什么”,这一章全部落到“怎么做”。我按自己起一个新文档站的真实顺序走一遍,中间带上版本选择、构建分析、部署三个环节里容易出错的细节。你照着做一遍,基本能跑出一个可用的骨架。
4.1 依赖版本锁定与环境准备
Node 版本对 Nuxt 2 影响很大。太新的 Node 版本在部分依赖上会报兼容问题,尤其是老版本的node-sass或fibers。我习惯用 Node 14 或 16 的 LTS 版本,配合.nvmrc固定下来,避免 CI 和本地环境不一致。
包管理器我选 yarn 或 pnpm 都行,但要注意 lock 文件必须提交。Nuxt 2 的依赖树很深,间接依赖的小版本变化就可能引发构建失败。提交 lock 文件之后,CI 上装出来的依赖和本地完全一致,这是最省心的做法。
SCSS 支持要在build里声明。如果用 sass,记得同时锁sass-loader的版本,8.x 和 10.x 的配置写法不同。我踩过一次:本地是 8.x,CI 拉到 10.x,构建直接报 loader 选项不认识,排查了半小时。
{ "scripts": { "dev": "nuxt dev", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start", "analyze": "ANALYZE=true nuxt generate" }, "dependencies": { "nuxt": "2.15.8", "vue": "2.6.14", "vue-server-renderer": "2.6.14", "vue-template-compiler": "2.6.14" } }开发阶段的第一个检查点是热更新是否正常。不正常时先看nuxt.config.js里的build.watch,再看是不是把pages目录挂到了虚拟文件系统之外(比如放在 Docker 挂载卷里,文件事件监听会失效,需要开轮询)。
4.2 构建产物分析与性能调优的量化过程
跑一次nuxt generate之后,输出目录里会有每个路由对应的 HTML 文件。我做的第一件事是量一下体积:随便挑三个典型页面,看 HTML 文件的原始大小和 gzip 后的大小。
一次实测数据大致是这样的:某文档页 HTML 原始 96KB,gzip 后 24KB,其中正文内容只占 18KB,其余全是内联的样式和预取脚本。这个比例说明问题不在内容,在框架注入。这时可以做的优化有几项:
- 把
build.extractCSS打开(生产环境默认开启),把样式抽成独立文件,利用浏览器缓存。 - 检查
build.vendor是否需要拆分第三方依赖,避免每次业务代码改动导致 vendor chunk 的 hash 变化。 - 关掉不用的预取。
render.prefetch打开会在页面里注入所有后续路由的预取链接,页面多的时候 HTML 会膨胀。
首屏时间的量化我用两个指标:服务端返回首字节时间,和内容可见时间。静态托管下首字节时间基本等于网络往返,这个没得优化,只能靠 CDN。内容可见时间主要受样式阻塞影响,把关键样式内联、非关键样式异步加载,能明显摁下去。
我做过一次对比:同一份文档页,样式全量同步加载时内容可见时间约 1.2 秒(弱网模拟),改成关键样式内联之后降到 0.5 秒左右。这个提升在移动端非常值。实现方式是在head里手写内联样式,或者在构建阶段把首屏用到的 CSS 提取出来注入模板。
4.3 静态生成与线上部署
生成完之后是部署。静态托管的坑集中在一个地方:刷新子路径返回 404。原因是托管服务收到/docs/a/b请求时去找对应文件,如果没开回退配置就会直接 404,而这个文件其实是存在的,只是路径映射需要服务端配合。
用fallback: '200.html'会在输出目录生成一个兜底文件,服务端配置把未命中的请求回退到这个文件,前端路由接管后渲染正确页面。
server { listen 80; root /var/www/docs/dist; index index.html; location / { try_files $uri $uri/ $uri.html /200.html; } location ~* \.(js|css|woff2|png|svg)$ { expires 30d; add_header Cache-Control "public, immutable"; } location = /200.html { add_header Cache-Control "no-cache"; } }这段配置里有三处是必须的。try_files里的$uri.html让/docs/a能命中docs/a.html;/200.html兜底防止刷新 404;静态资源加长缓存但要给200.html单独设no-cache,否则用户拿到的永远是旧版本外壳,新内容出不来。
如果走服务端实时渲染,部署方式换成 Node 进程托管。nuxt build之后nuxt start起来,前面挂反向代理,进程管理用 pm2 之类的工具保证崩溃重启。这时要注意nuxt.config.js里的server.port和反向代理的目标端口一致,以及内存上限的调整——默认的 Node 堆内存对中型站点够用,页面组件庞大时可能需要调高。
5. 常见问题与排查技巧实录
这一章全部来自实际项目里的报错和排查。我按“现象、原因、解法”整理成速查表,后面挑三个最典型的展开讲,最后补几条文档里不写但很省时间的经验。
5.1 高频报错速查表
| 现象 | 根本原因 | 处理方式 |
|---|---|---|
window is not defined | 服务端渲染阶段执行了浏览器专属代码 | 用process.client判断,或把库放plugins并设ssr: false |
| 刷新子路径 404 | 静态托管未配置回退 | 生成200.html并配置try_files |
| 页面内容在,但源码里是空壳 | 数据在mounted里请求 | 挪到asyncData或fetch |
| 构建卡住不动 | 异步请求无超时 | 请求层加超时 + 并发调低 |
| 动态路由全被索引成同一页 | 未做validate校验 | 加参数格式校验,非法时error |
| 样式与内容短暂错位 | 水合前后数据不一致 | 首屏数据全部走服务端注入,避免客户端二次生成 |
| 元信息不更新 | head写成了静态对象 | 改成函数返回,依赖响应式数据 |
| 本地正常、CI 构建失败 | 依赖版本漂移 | 提交 lock 文件,锁死核心三个包的版本 |
| 内存溢出 | 并发过高或单页渲染太重 | 降concurrency,拆分超大组件 |
5.2 动态路由、404 与 SEO 元信息的三个坑
第一个坑是动态路由的“假 200”。详情页拿不到数据时,很多人习惯在模板里写一句“内容不存在”然后正常渲染。用户看到是对的,但服务端返回的是 200,搜索引擎会把这个地址当成有效页面收进索引。正确做法是在asyncData里主动抛错:
async asyncData({ params, $axios, error }) { const { data } = await $axios.get(`/api/docs/${params.slug}`) if (!data || !data.title) { return error({ statusCode: 404, message: '页面不存在' }) } return { doc: data } }第二个坑是元信息不随页面变化。文档里head可以写成对象,也可以写成函数。写成对象时它只在初始化时求值一次,页面切换后标题不会跟着变。写成函数后每次都能拿到最新的组件状态:
head() { return { title: this.doc.title, meta: [ { hid: 'description', name: 'description', content: this.doc.summary } ] } }这里hid必须写,且同一类标签之间不能重复。没有hid时框架无法判断哪个标签该被替换,结果是页面里堆积多个 description,抓取工具取第一个,内容全是旧的。
第三个坑是水合不一致。表现是页面加载完成后内容闪一下再变。根因是服务端渲染出的 HTML 和客户端接管后重新计算的结果不同。常见诱因有三个:用了随机数或时间戳、用了只有客户端才有的值(比如本地存储里的主题偏好)、以及在created里改了会在模板中体现的状态。解法一律是:首屏渲染依赖的数据全部由服务端注入,客户端只做增强。
5.3 我踩过的几个隐性坑
plugins目录的注册顺序是有讲究的。同一个页面依赖两个插件,后注册的会被先注册的覆盖同名方法。我遇到过一次,两个插件都往原型上挂了$format,结果行为不对。现在我的做法是每个插件挂载的方法都加业务前缀,避免撞名,注册顺序在配置里按依赖关系排,不靠猜。
static目录里的文件名不要带中文或空格。静态托管和 CDN 对这类文件名的处理不一致,有的会自动编码,有的直接 404。文档站的图片、附件一律用英文短横线命名,这个习惯能省掉一堆线上问题。
构建缓存要主动清理。nuxt generate有时会复用工上一次的构建产物,导致某些页面更新后内容没变。表现很迷惑:改了三处,两处生效,一处还是旧的。排查思路是删掉.nuxt和输出目录重跑一次,如果问题消失就是缓存问题。CI 上我直接在构建脚本开头加清理步骤,虽然多花几十秒,但比线上内容不同步划算得多。
最后说一个心态上的经验。我一开始读这份文档,总想着把每个配置项都搞明白再动手,结果读了两周还没跑起来。后来换了方式:先照着目录建一个最小可跑的骨架,遇到具体问题再回文档查对应那页。带着问题读,比通读效率高得多,而且记得住。文档站里的配置页其实是一份“字典”,不适合当“教材”从头读,目录和概念那两章才是教材,剩下的当查询手册用。这个使用方式的转变,是我从这份文档里受益最大的一点。