Nuxt 3 项目结构

nuxt3
Created 9/16/2022
Updated 3/22/2023

Nuxt 3 项目结构

一般 Nuxt 项目会具有以下文件系统

md
📂 root
 |-- 📁 .nuxt
 |-- 📁 .output
 |-- 📁 assets
 |-- 📁 components
 |-- 📁 composables
 |-- 📁 layouts
 |-- 📁 middleware
 |-- 📁 node_modules
 |-- 📁 pages
 |-- 📁 plugins
 |-- 📁 public
 |-- 📁 server
 |-- 📄 .gitignore
 |-- 📄 .nuxtignore
 |-- 📄 app.vue
 |-- 📄 nuxt.config.ts
 |-- 📄 package.json
 |-- 📄 tsconfig.json

Nuxt 3 自动地为以上的文件和目录赋予不同功能,从而极大地优化开发体验,例如在 📁 /components 目录中定义的组件,会自动导入可以直接使用。不过 Nuxt 3 依然可以允许开发者通过配置 📄 nuxt.config.ts 文件,以修改并覆盖这些默认的行为。

配置相关

该小节介绍 Nuxt 项目结构中与配置相关的文件

nuxt.config.ts 文件

参考

位于根目录下的 ⚙ nuxt.config.ts 文件(也可以采用 .js.mjs 扩展名)是 Nuxt 应用的配置文件

该文件导出一个由 defineNuxtConfig() 方法(这是 Nuxt 内置函数,它是自动导入的,所以在文件的开头不需要导入 import 该方法)封装的配置对象

nuxt.config.ts
ts
// defineNuxtConfig() 方法是 Nuxt 自动导入的,所以可以直接使用
// 也可以通过以下代码,显式地手动导入
// import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
  // Nuxt 应用的配置对象
})
说明

具体有哪些可配置项可以参考官方文档

如果在项目中使用了模块 modules,也是在 ⚙ nuxt.config.ts 文件中对于这些模块进行配置,具体方法应该查看模块的相应文档

为了确保 Nuxt 应用的更新及时,如果检测到 nuxt.config.ts.env.nuxtignore.nuxtrc 文件有更新,Nuxt 就会将整个应用重启

由于 Nuxt 是一个可灵活拓展的框架,所以除了 ⚙ nuxt.config.ts 文件,还有一些额外的配置文件 External Configuration Files,以便对项目所整合的依赖包进行配置,例如针对 TypeScript 的配置文件 ⚙ tsconfig.json

但是对于一些特定的工具,可以在 ⚙ nuxt.config.ts 文件中设置相应的字段,忽略掉它们原生的配置文件(将 Nuxt 的配置文件作为单一可信的配置源,以便提高项目的稳定性 ❓)

工具名称(被覆盖)原生的配置文件(替换为)nuxt.config 中相应的字段
Nitronitro.config.tsUse nitro key
PostCSSpostcss.config.jsUse postcss key
Vitevite.config.tsUse vite key
webpackwebpack.config.tsUse webpack

runtimeConfig

其中配置对象的属性 runtimeConfig 可以为 Nuxt 项目设置一些全局可及的变量 global accessible,该属性的值是一个对象,以键值对的形式定义了变量名和它相应的值。

nuxt.config.ts
ts
export default defineNuxtConfig({
  runtimeConfig: {
    // 以下属性只能在服务端访问
    // 所以也称为 private key
    apiSecret: '123',
    public: {
      // 以下属性在服务端或客户端都可以访问
      apiBase: '/api'
    }
  }
})
说明

runtimeConfig 会先被序列化 serialized 再传递给 Nuxt 的后端引擎 Nitro,所以该对象里的属性的值只能是基础数据类型(可以被序列化的值,不能采用函数 function、集合 Set、映射 Map 等数据类型)

它们就相当于环境变量 environment variable,可以在 .env 文件中设置相应的环境变量(全大写,以 NUXT 为前缀,以下划线 _ 拼接单词),覆盖相应的变量值。

所以可以先在 runtimeConfig 对象中先「预留」需要使用的字段,但是属性值以空字符串等作为默认值,再在部署项目的平台上设置环境变量,设置「真实」的变量值

.env
env
# 环境变量 NUXT_API_SECRET
# 它所覆盖的是 runtimeConfig 对象的属性 apiSecret
# 所以构建 Nuxt 项目时,该属性最终的值是 345
NUXT_API_SECRET=345
注意

默认情况下,这些变量只能在服务端(例如在 server routes 服务端的路由中使用)才能访问。

而在 public 属性之下所定义的键值对,则在服务端和客户端(此时应用处于运行时)均可以访问的。

Nuxt 会将 runtimeConfig.public 添加到每个页面的 payload 数据负载中。

在项目中通过方法 useRuntimeConfig() 获取该对象,该组合式 API 只在 setup() 函数和生命周期相关的钩子函数中可用。

如果在浏览器访问 runtimeConfig,正如其名此时它是运行时的配置项,可修改更新(但是在服务端访问 runtimeConfig 则为只读对象

vue
<script setup>
const runtimeConfig  = useRuntimeConfig()
console.log(runtimeConfig.public.apiBase) // '/api'
if (process.server) {
  // 只有在服务端才可以访问 apiSecret 属性
  console.log('API secret:', runtimeConfig .apiSecret)
}
</script>

app.config.ts 文件

参考

Nuxt 提供了一种方式为应用提供全局变量 public variables,称为 App Configuration

这些变量可以在网页运行时 runtime within lifecycle 访问,而且支持响应性更新 reactive configuration,所以可以在前端页面(包括 Nuxt Plugin 插件)或服务端渲染页面 server-rendering 时使用

注意

由于该对象会暴露给客户端(打包 bundle 进前端页面的代码中),所以不应该在该对象中设置 secret 私密信息

在项目的根目录里创建一个名为 ⚙ app.config.ts 文件(也可以采用 .js.mjs 扩展名),它导出一个由 defineAppConfig() 方法(这是 Nuxt 内置函数,它是自动导入的,所以在文件的开头不需要导入 import 该方法)封装的配置对象

app.config.ts
ts
export default defineAppConfig({
  title: 'Hello Nuxt',
  theme: {
    dark: true,
    colors: {
      primary: '#ff0000'
    }
  }
})

在应用中可以通过 useAppConfig() 方法获取该对象

ts
<script setup>
const appConfig = useAppConfig()
console.log(appConfig.theme);
</script>
对比

App Configuration 和 runtimeConfig 很相似,以下表格总结了两者的异同

功能runtimeConfigapp.config.ts
Client Side 前端可使用场景Hydrated 前端完成混合渲染时Bundled 打包完成后
Environment Variables 可否使用环境变量覆写✅ Yes❌ No
Reactive (前端)响应性✅ Yes✅ Yes
Types support 类型约束✅ Partial 部分支持✅ Yes
Configuration per Request ❓❌ No✅ Yes
Hot Module Replacement 开发时支持热重载❌ No✅ Yes
None primitive JS types 非基础数据类型作为变量值❌ No✅ Yes

.nuxtignore 文件

参考

📄 .nuxtignore 文件是用于配置 Nuxt 在构建应用阶段应该忽略哪些目录下的文件,可以包含 📁 layout、📁 pages、📁 components、📁 composables、📁 middleware 这些目录下的文件。

Tip

该文件的作用类似于 .gitignore,也可以采用类似的语法来指定忽略哪些文件,具体语法可以参考 git 的官方文档

.nuxtignore
nuxtignore
# ignore layout foo.vue
layouts/foo.vue
# ignore layout files whose name ends with -ignore.vue
layouts/*-ignore.vue

# ignore page bar.vue
pages/bar.vue
# ignore page inside ignore folder
pages/ignore/*.vue

# ignore route middleware files under foo folder except foo/bar.js
middleware/foo/*.js
!middleware/foo/bar.js

.env 文件

参考

📄 .env 文件用于保存环境变量。这些环境变量会在应用运行时自动载入,可以在配置文件 nuxt.config.ts 或 modules 模块中访问到

Nuxt CLI 内置了 dotenv,所以支持从 📄 .env 文件中加载环境变量,以便一些变量可以在开发环境和生产环境中使用不同值

提示

如果希望使用其他文件来保存环境变量,可以在使用命令启动应用时,通过参数 --dotenv 进行设置

bash
# 在开发环境下使用 .env.local 文件来存放环境变量
npx nuxi dev --dotenv .env.local

当更改 📄 .env 文件里的环境变量后,Nuxt 应用实例会自动重启,以便将新的环境变量应用于 process.env

注意

但是删除环境变量,或删除 📄 .env 文件,并不会导致已经载入到应用中的值重设

.gitignore 文件

参考

📄 .gitignore 文件指定 git 应该不跟踪(有意忽略)哪些目录和文件

推荐设置

推荐该文件中至少包含以下字段

.gitignore
gitignore
# Nuxt 在生产环境时所产生的目录,可直接用于部署
.output

# Nuxt 在开发环境自动生成的目录
.nuxt

# 存放项目依赖包的目录
node_modules

# 系统生成的一系列日志文件
*.log

.nuxtignore

参考

📄 .nuxtignore 文件的作用和 .gitignore.eslintignore 等类型文件类似,但是它是针对 Nuxt 程序的,用以指定 Nuxt 在构建应用时应该忽略哪些文件

它采用与 .gitignore 文档相同的语法,例如可以使用正则表达式来匹配并忽略一系列符合条件的文件,具体可以参考 gitignore 官方文档

.nuxtignore
nuxtignore
# 忽略某个布局:被忽略的布局文件将不会被 Nuxt.js 识别,因此你不能在 nuxt.config.js 或页面文件中使用这些布局。
layouts/foo.vue
# 忽略文件名以 -ignore.vue 结尾的布局组件
layouts/*-ignore.vue

# 忽略某个页面
pages/bar.vue

# 忽略在 middleware/foo 目录中的所有路由中间件
# 但是并不忽略 middleware/foo/bar.js 这个文件
middleware/foo/*.js
!middleware/foo/bar.js

tsconfig.json 文件

参考

在根目录下的 📄 tsconfig.json 文件是 TypeScript 的配置文件。

但在里面其实并不需要进行详细的设置,只需要指向/引用 Nuxt 在开发环境中自动生成的 .nuxt/tsconfig.json 文件

tsconfig.json
json
{
  "extends": "./.nuxt/tsconfig.json"
}
不推荐

虽然可以在 tsconfig.json 文件里进行配置,但是并不推荐对 targetmodulemoduleResolution 字段进行设置,因为这会覆写了 Nuxt 自动生成的配置而导致错误

另外并不推荐设置 paths 字段,因为这会覆写 Nuxt 自动对路径别名的解析。更推荐通过配置文件 📄 nuxt.config.ts 的属性 alias 对路径别名进行扩展(而不是覆写)

资源管理

该小节介绍 Nuxt 项目结构中与资源管理相关的目录

参考

public 目录

📁 public 目录用于存放静态资源文件,例如 favicon.ico 图片。

在编译项目时,该目录下的文件和子文件夹都会被**直接「搬运」**到部署文件夹的根目录下

该目录下的文件不会进行压缩转换处理。

在开发时,如果需要使用该目录下的文件,其路径是以根目录 / 开始的。例如在该目录中的一张图片,其路径是 public/img/nuxt.png,则在一个组件中可以通过相应的路径进行引用

vue
<template>
  <img src="/img/nuxt.png" alt="Discover Nuxt 3" />
</template>
Tip

在部署项目后,这些文件可以直接通过 URL 访问,例如以上示例 nuxt.jpg 文件存放在 public/img/ 目录中,项目部署后网址是 www.nuxt-demo.com,则该图片文件可以通过 URL www.nuxt-demo.com/img/nuxt.jpg 直接查看

assets 目录

📁 assets 目录是用于存放项目的资产文件,例如样式表、字体、图片(较小的图片)等,在编译项目时,该目录下的文件会被打包工具(如 Vite 或 Webpack)进行处理(如压缩、缓存),整合(内嵌)到前端页面中

该目录适用存储一些可被打包工具(及其插件)处理的文件,例如样式文件 stylesheet、字体文件、SVG 文件等

在开发时,如果需要引用该目录下的文件,其路径是以 ~/assets/ 开始的(其中 ~ 是根目录的别名)。例如该目录中的一张图片,其路径是 assets/img/nuxt.png,则可以使用如下方法在组件中进行引用

vue
<template>
  <img src="~/assets/img/nuxt.png" alt="Discover Nuxt 3" />
</template>
提示

在开发时一般都是以名为 assets 的目录作为静态资源的存储目录,但也可以采用其他名称的目录,可以在配置文件 nuxt.config.ts 的属性 dir.assets 覆写默认值

构建和部署项目时,该目录下的文件会被打包器处理(整合到 bundle 源码中,而不是直接在服务器中进行托管),因此该目录里的文件无法public 目录一样通过 URL 直接查看

Tip

如果希望将文件托管到线上服务器中,可以放置在 📁 public 目录中

前端相关

该小节介绍 Nuxt 项目结构中与前端相关的文件和目录

app.vue 文件

参考

在根目录下可能存在一个 📄 app.vue 文件(在 Nuxt 3 所提供的默认模板中就提供了该文件)

它会作为项目的入口 entrypoint(实际上 Nuxt 在后台会自动创建 main.js 文件,并创建 Vue app 实例),即作为应用的根组件,Nuxt 就会创建出的是一个 SPA 单页面应用(如果项目中没有 pages 目录等额外的设置)

对于简单的项目(不需要多个页面),一般以 app.vue 为主要的组件,将页面的主要 UI 写在这个单文件组件中就足够了

注意

如果项目中有 📄 app.vue 文件,但同时存在 📁 pages 目录(则该项目采用多页面模式),⚠️ 记得需要在 app.vue 组件的模板中添加内置组件 <NuxtPage/>

可以理解为 app.vue 是每个路由的(共用)父组件,而不同路由所对应的页面的不同内容则由 pages 目录中的相应文件构成,它们作为 app.vue 的子组件,替换 <NuxtPage/> 所指示的位置

app.vue
vue
<template>
  <div>
    <NuxtPage />
  </div>
</template>

由于 Nuxt 3 会在 <NuxtPage> 里使用 <Suspense> 内置组件,所以 <NuxtPage> 组件不能作为 app.vue 模板的根元素

components 目录

参考

在 📁 components 目录创建 Vue 组件,使用它们时支持自动导入 auto-import

提示

也可以手动显式导入组件,Nuxt 提供了一个 #components 别名(包含所有内置组件和自动扫描注册的组件)

vue
<template>
  <div>
    <h1>Mountains</h1>
    <LazyMountainsList v-if="show" />
    <button v-if="!show" @click="show = true">Show List</button>
    <NuxtLink to="/">Home</NuxtLink>
  </div>
</template>

<script setup>
  import { NuxtLink, LazyMountainsList } from '#components'
  const show = ref(false)
</script>

配置目录

Nuxt 默认只扫描 components 目录,将里面的文件注册为组件

可以在配置文件 📄 nuxt.config.ts 的属性 components 添加其他目录,或设置扫描的方式

nuxt.config.ts
ts
export default defineNuxtConfig({
  // 设置存放组件的目录
  // 属性值是数组,每一个元素就是一个需要自动扫描的目录
  components: [
    // 以对象的方式,添加一个子目录
    {
      // 需要添加的目录的路径
      path: '~/anotherComponents',
      // 该目录里的组件的名称都添加特定的前缀
      prefix: 'Special'
    },
    // 或以字符串的方式,添加一个子目录
    // 直接给出目录的路径
    '~/components' // 这是默认目录
  ]
})
提示

在配置文件 nuxt.config.ts 的属性 extensions 指定哪一些类型的文件看作是组件,其默认值如下

json
[
  ".js",
  ".jsx",
  ".mjs",
  ".ts",
  ".tsx",
  ".vue"
]

可以通过更改这个属性值,来决定 Nuxt 会自动扫描在 components 目录里的哪些文件,并将它们注册为组件

也可以通过属性 extensions 来指定要扫描的文件类型

nuxt.config.ts
ts
export default defineNuxtConfig({
  components: [
    {
      path: '~/components',
      // 只扫描该目录下的 .vue 文件
      extensions: ['.vue'],
    }
  ]
})

组件名称

组件的名称一般和文件名称相同,所以推荐 components 目录里的文件命名方式采用 Pascal Case 大驼峰命名法,如 ImageView.vue

但是如果文件是内嵌在子目录里,则所组件的名称会有各层级的目录名称和文件名称拼接而成

md
📂 components
 |-- 📁 base
      |-- 📁 foo
           |-- 📄 Button.vue

以上示例文件 Button.vue 会注册为 <BaseFooButton /> 组件

推荐

Nuxt 在自动扫描并注册组件时,会将组件名中重复的部分自动去除

所以推荐将文件名设置为与最终组件的名称一致,这样更符合日常的使用习惯,可以根据组件名快速准确地定位到相应的文件,例如对于以上示例文件 Button.vue 应该命名为 BaseFooButton.vue,两种文件命名方式最终所对应的组件名称都是 <BaseFooButton />

如果不想基于文件所在的目录的层级结构来命名组件,可以通过属性 pathPrefix 来取消这个默认行为

nuxt.config.ts
ts
export default defineNuxtConfig({
  components: [
    {
      path: '~/components',
      // 目录路径不会成为组件名的一部分
      pathPrefix: false,
    },
  ],
});

例如 ~/components/Some/MyComponent.vue 文件最终所对应的组件名称是 <MyComponent>(而不是 <SomeMyComponent>

提示

还可以在配置文件 nuxt.config.ts 的属性 components 中添加某个组件容器/目录的子目录,那么该子目录就会作为一个单独的组件容器 ❓,组件名称就不会有一长串的目录路径名

由于 Nuxt 会依序对数组中所列出的目录进行扫描,所以需要将子目录写在前面

ts
export default defineNuxtConfig({
  components: [
    {
      path: '~/components/special-components', // 子目录
      prefix: 'Special'
    },
    '~/components'
  ]
})

动态组件

对于自动导入的组件,只有在模板中以 <componentName> 形式引用才可以被 Nuxt 识别 ❓ 由于并没有显式导入组件实例,如果需要使用动态组件 <component :is="componentA" />(其中 componentA 是组件实例)则还需要借助一个工具函数 resolveComponent()

vue
<template>
  <component :is="clickable ? MyButton : 'div'" />
</template>

<script setup>
// 使用方法 `resolveComponent(componentName)` 获取一个实例
// 参数 componentName 是字符串,组件的名称
const MyButton = resolveComponent('MyButton')
</script>
提示

另一种解决方法是将组件进行全局注册,这样不需要以上的工具函数也可以直接使用

但并不推荐 :thumbsdown: 采用这种方式,因为在全局注册组件可能会引起名字的冲突

nuxt.config.ts
ts
export default defineNuxtConfig({
  components: {
    global: true, // 将该目录里的组件进行全局注册
    dirs: ['~/components']
  },
})

动态导入

只需要在组件名称之前添加 Lazy 即可实现组件的动态导入/按需加载/懒加载 lazy-loading

如果组件并不是页面必须的,则可以让组件采用懒加载的方式,在满足特定的条件时才加载它,可以优化页面的 JavaScript bundle size 代码打包的大小,加快首次打开页面的速度

pages/index.vue
vue
<template>
  <div>
    <h1>Mountains</h1>
    <!-- 该组件的原名是 MountainsList -->
    <LazyMountainsList v-if="show" />
    <button v-if="!show" @click="show = true">Show List</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: false
    }
  }
}
</script>

ClientOnly 内置组件

Nuxt 提供了一个内置组件 <ClientOnly> 以指定仅在客户端渲染的代码块

vue
<template>
  <div>
    <Sidebar />
    <ClientOnly>
      <!-- this component will only be rendered on client-side -->
      <Comments />
    </ClientOnly>
  </div>
</template>

该内置组件提供了一个 Prop fallbackTag 用于设置在服务端渲染时,该内置组件会渲染为哪一个 HTML 标签。

也提供了一个名为 fallback 的插槽,用于设置该组件在服务端渲染哪些内容(作为一种回退策略),等到页面在浏览器渲染出来时,会用「真实」的内容替换该插槽。

vue
<template>
  <div>
    <Sidebar />
    <!-- This renders the "span" element on the server side -->
    <ClientOnly fallbackTag="span">
      <!-- this component will only be rendered on client side -->
      <Comments />
      <template #fallback>
        <!-- this will be rendered on server side -->
        <p>Loading comments...</p>
      </template>
    </ClientOnly>
  </div>
</template>

另外也可以为文件的名称添加 .client (其中 . 点是与前面的文件名分隔,该后缀并不作为组件名称的一部分),则 Nuxt 会自动将组件转换为 client-only,表示该组件仅在前端渲染

说明

以上特性只适用于 Nuxt 自动导入或通过 #components 导入的组件,如果组件是手动显式地从真实路径导入的,则无法将其转为 client-only 组件

client-only 组件要等到 mounted 到页面后才进行渲染,所以如果要访问该组件的模板的渲染结果 access the template,逻辑代码需要编写在 onMounted 钩子函数中

而且为了保证组件渲染后才进行访问,还要在钩子函数的回调函数中 💡 使用 await nextTick() 对逻辑代码进行包裹

md
📂 components
 |-- 📄 Comments.client.vue
pages/example.vue
vue
<template>
  <div>
    <!-- this component will only be rendered on client side -->
    <Comments />
  </div>
</template>

Server Only 组件

.client 相对应,也可在添加 .server 后缀,但是目前默认情况下,添加了该后缀的组件是可以在前后端通用

其实可以将添加了 .server 后缀的组件设置为「标准的」仅在后端渲染,需要在配置文件 nuxt.config.ts 中设置属性 experimental.componentIslands

nuxt.config.ts
ts
export default defineNuxtConfig({
  experimental: {
    componentIslands: true
  }
})

server-only 组件只会在后端渲染,当组件的 Props 更新时,会触发一个网络请求,让后端根据新的数据重新渲染组件,最后采用 in-place 原位更新的方式更新页面上相应的 HTML 元素

md
📂 components
 |-- 📄 HighlightedMarkdown.server.vue
pages/example.vue
vue
<template>
  <div>
    <!--
      this will automatically be rendered on the server,
      meaning your markdown parsing + highlighting libraries
      are not included in your client bundle.
     -->
    <HighlightedMarkdown markdown="# Headline" />
  </div>
</template>
注意

目前 server-only 组件属于实验功能,可能存在一些 Bugs,例如在该组件中不能使用异步相关的代码

如果在项目中分别针对客户端和后端创建了同名的两个组件,则它们需要遵顼一定的规则

md
📂 components
 |-- 📄 Comments.client.vue <!-- 在前端采用该组件 -->
 |-- 📄 Comments.server.vue <!-- 在后端采用该组件 -->

其中 client-only 组件需要在初次加载时和 server-only 组件渲染的结果相同,这样页面才可以在前端 hydrate 渲染成功,否则会发生 hydration mismatch 错误

DevOnly 内置组件

Nuxt 提供了一个内置组件 <DevOnly> 以指定仅在开发环境中渲染的代码块,而在生成环境中会被 tree-shaken 抛弃掉,不会在包含在最后的打包结果中

vue
<template>
  <div>
    <Sidebar />
    <DevOnly>
      <!-- this component will only be rendered during development -->
      <LazyDebugBar />
    </DevOnly>
  </div>
</template>

NuxtClientFallback 内置组件

Nuxt 提供了一个内置组件 <NuxtClientFallback> 作为一个容器,以捕获它(默认插槽中)所包含的内容在 SSR 服务端渲染时抛出错误(该组件的内容无法渲染) ❓ 并通过一个 Prop fallback-tag 设置该组件渲染为哪一个 HTML 标签作为「候补内容」

vue
<template>
  <div>
    <Sidebar />
    <!-- this component will be rendered on client-side -->
    <NuxtClientFallback fallback-tag="span">
      <Comments />
      <BrokeInSSR />
    </NuxtClientFallback>
  </div>
</template>

pages 目录

参考

在 📁 pages 目录里所创建的文件/组件表示不同的页面。

Nuxt 会根据这个目录下各文件之间的层级嵌套结构,自动创建出对应的路由/页面

提示

也可以通过 definePageMeta() 方法设置 path 属性,为页面设置所匹配的路由路径,而不采用由文件和目录名称所默认构成的路由路径。

通过这种方式可以采用正则表达式构建出更复杂的页面-路由路径的匹配模式,具体可以参考 Vue Router 的官方文档

文件类型一般是 *.vue,每一个文件对应构建一个网页页面

pages/index.vue
vue
<template>
  <h1>Index page</h1>
</template>

也可以是 *.js*.jsx.mjs*.ts*.tsx 格式

pages/index.ts
ts
// 使用渲染函数来生成一个页面
// https://vuejs.org/guide/extras/render-function.html
export default defineComponent({
  render () {
    return h('h1', 'Index page')
  }
})
pages/index.tsx
tsx
// 使用 tsx 语法和渲染函数来生成一个页面
// https://vuejs.org/guide/extras/render-function.html
export default defineComponent({
  render () {
    return h('h1', 'Index page')
  }
})
提示

该目录是可选的,对于简单的应用一般仅使用 app.vue 作为唯一页面即可。

如果不使用该目录时,仅用 app.vue 构建单页面应用,则 Nuxt 并不会引入 Vue Router,让应用的打包大小更优化。

app.vue 单文件组件和 pages 目录一般对应于两种应用模式,前者是单页面应用,后者是对页面应用,一般并不会在项目中同时使用两者。

如果项目中同时存在 📄 app.vue 文件和 📁 pages 目录,⚠️ 则需要app.vue 文件中添加 <NuxtPage/> 内置组件。此时 app.vue 作为根组件,而 pages 目录中的文件则渲染成为一个组件插入到根组件中

可以理解为 app.vue 是每个路由的(共用)父组件,而不同路由所对应的页面的不同内容则由 pages 目录中的相应文件构成,它们作为 app.vue 的子组件,替换 <NuxtPage/> 所指示的位置

app.vue
vue
<template>
  <div>
    <!-- 应用中各页面的共用部分,例如导航栏 -->
    <div>Header</div>
    <!-- 这个内置组件相当于一个「占位符」 -->
    <!-- 在不同路由下,会被 pages 目录中的相应组件替换 -->
    <NuxtPage />
  </div>
</template>
注意

pages 目录下的 vue 单文件组件都必须只有一个根元素 a single root element

而且由于在 Vue 的模板中 HTML 注释也会算作是一个元素,所以不能在根元素外还有同级的注释

这个约束是为了在前端进行页面切换时,可以控制/实现过渡的效果

在创建页面时,Nuxt 会使用 Vue Router 根据文件 file-base 之间的层级嵌套关系来创建相应的嵌套路由,例如以上的示例中文件 pages/index.vue 所创建的页面的路由是 /

注意

为了优化性能,在路由导航时,可能会重用页面中一些结构类似的组件(而不是替换整体 DOM,仅仅更新其中内容)

但是这可能会造成切换页面时,切换动效 transition 不起作用

为了避免这个问题,可以为 <NuxtPage :page-key="someKey" /> 设置 pageKey 属性,将其值与变动的子组件关联(如使用嵌套路由的路径)

pages/parent.vue
vue
<script setup>
const route = useRoute()
const key = route.fullPath // 以当前页面的完整路由作为 key
</script>
<template>
  <div>
    <h1>I am the parent view</h1>
    <NuxtPage :page-key="key" />
  </div>
</template>

也可以调用方法 definePageMeta() 为不同的路由页面设置相应的元信息

pages/parent/child.vue
vue
<script setup>
definePageMeta({
  key: route => route.fullPath
})
</script>

嵌套路由

嵌套路由所对应的页面就像积木一样,通过每一层路由所对应的组件嵌套搭建而成的,即子组件会内嵌到其父组件的特定位置,这个位置就是通过在父组件中使用内置组件 <NuxtPage/> 来指定的

md
📂 pages
 |-- 📂 parent
 |    |-- 📄 child.vue
 |-- 📄parent.vue

Nuxt 根据以上的文件系统生成以下的嵌套路由

js
[
  {
    path: '/parent', // 对应的路由是 /parent
    component: '~/pages/parent.vue',
    name: 'parent',
    children: [
      {
        path: 'child', // 对应的路由是 /parent/child
        component: '~/pages/parent/child.vue',
        name: 'parent-child'
      }
    ]
  }
]

为了展示出 child.vue 需要在 pages/parent.vue 的模板中使用内置组件 <NuxtPage /> 指明位置

pages/parent.vue
vue
<template>
  <div>
    <h1>I am the parent view</h1>
    <NuxtPage :foobar="123" />
  </div>
</template>

动态路由

Nuxt 除了可以基于该目录的文件结构来构建页面路由,还可以基于文件名来创建特别的路由

在目录或文件名称中使用方括号 [paraName] 来创建动态路由,即页面可以匹配一系列 URL(动态路由)。其中方括号中就是动态路由的参数变量,可以通过页面的路由对象 route 的相应属性来获取参数值

md
📂 pages
 |-- 📄 index.vue
 |-- 📂 users-[group]
      |-- 📄 [id].vue
pages/users-[group
vue
<!-- 该页面可以匹配一系列 URL(动态路由) -->
<template>
  <!-- 使用选项式 Option API 显示当前路由的参数 group 和 id 的值 -->
  <p>{{ $route.params.group }} - {{ $route.params.id }}</p>
</template>

<script setup>
// 使用组合式 API 来访问该页面的路由对象
const route = useRoute()

if (route.params.group === 'admins' && !route.params.id) {
  console.log('Warning! Make sure user is authenticated!')
}
</script>

例如当用户访问的 URL 为 /users-admins/123 时,则生成的页面为

html
<p>admins - 123</p>
提示

如果希望参数是可选的 optional,则可以使用双方括号 [[paraName]] 来创建动态路由,例如文件 pages/[[slug]]/index.vue(或 pages/[[slug]].vue)可以匹配 //test 等两类不同的路由

还可以设置一个文件名称的形式为 [...slug].vue 格式的页面(...slug 就类似于 JS 解构语法),它会匹配所有的路由,参数 slug 是将嵌套路由的各部分解构为一系列的参数所构成一个数组

pages/[...slug
slug
<template>
  <p>{{ $route.params.slug }}</p>
</template>

如果用户访问 URL 为 /hello/world 则渲染出的页面如下

html
<p>["hello", "world"]</p>
提示

另外 Nuxt 支持设置一个 pages/404.vue 页面,以处理当用户访问的 URL 未匹配到任何路由的情况,展示该页面并返回 404 响应状态码。

页面元信息

以上说到的页面元信息 Page Metadata 是为了给页面/路由对象提供一些额外信息的,通过 definePageMeta() 方法进行设置。这些信息可以在页面的路由对象的属性 route.meta 访问得到。

说明

如果页面对应的是嵌套路由,且在不同的组件中设置了页面元信息,则 Nuxt 会将它们合并到一个对象

即在页面中通过路由对象 route 可以访问到在嵌套路由所对应的所有组件中分别设置的页面元信息。

vue
<script setup>
definePageMeta({
  title: 'My home page'
})

const route = useRoute()

console.log(route.meta.title) // My home page
</script>
注意

definePageMeta() 方法是一个编译器函数 compiler macro,所以它是先于 Vue 执行的,因此不能访问当前页面/组件以及里面定义的变量,但可以访问导入到页面的其他值

vue
<script setup>
import { someData } from '~/utils/example'

const title = ref('')

definePageMeta({
  title, // ❌ This will create an error
  someData // ✅ can access this data
})
</script>

可以为页面添加任意的元信息,其中有一些元信息属性是具有特定的含义

  • keepalive 属性:如果将其属性值设置为 true,则 Nuxt 会为页面组件外包裹一个 Vue 内置组件 <keepAlive>,在路由切换时缓存该页面的状态,切换回来时可以看到页面的状态数据是被保留的。
    该属性值还可以是一个对象,会作为 Props 传递给 <KeepAlive> 以便对该组件进行更详细的配置。
    可以在配置文件 nuxt.config.ts 的属性 app.keepalive 为该内置组件设置默认的 Props 参数
    提示

    如果在嵌套路由中想保留子组件的状态,也可以直接在内置组件设置相应的属性 <NuxtPage keepalive />

  • key 属性:该页面组件的唯一标识符
  • layout 属性:设置该页面使用哪个布局模板,也可以设置为 false 表示不使用任何布局模板(但依然可以在页面中通过 <NuxtLayout> 组件来应用模板)。具体参考本文的 layouts 目录部分
  • middleware 属性:为该页面设置专属的路由中间件。具体参考本文的 middleware 目录
  • layoutTransition 属性:为该页面设置布局更改的过渡动效
  • pageTransition 属性:为该页面设置页面切换的过渡动效
Tip

以上两个属性都会影响页面的过渡效果,如果在切换页面的同时,发生了布局的改变,且设置了 layoutTransition 属性,则优先采用布局过渡动效

可以将属性值设置为 false 表示页面切换时不采用过渡动效

可以在配置文件 nuxt.config.ts 的属性 app.layoutTransitionapp.pageTransition 设置默认的过渡参数

更详细的介绍可以查看另一篇笔记《Nuxt 3 特色功能》关于过渡动效的部分

  • alias 属性:为该页面设置路由别名,这样就可以通过不同的 URL 来访问该页面。该属性值可以是字符串或数组,具体可以参考 Vue Router 的官方文档
  • name 属性:为该页面所对应的路由设置名称(需要是唯一值,作为该路由的标识符)。在(编程式)导航时,用名称来指代操作路径较长的路由,可以便于操作。具体可以参考 Vue Router 的官方文档
  • path 属性:为页面设置所匹配的路由路径,而不采用由文件和目录名称所默认构成的路由路径。支持采用正则表达式构建更复杂的匹配模式,具体可以参考 Vue Router 的官方文档

页面导航

除了在地址栏手动输入 URL 来访问相应的页面,Nuxt 还提供一个内置组件 <NuxtLink to="urlPath" /> 来生成链接以供用户点击触发路由导航

vue
<template>
  <!-- 链接到主页 -->
  <NuxtLink to="/">Home page</NuxtLink>
</template>

此外 Nuxt 还提供 navigateTo() 方法来实现编程式导航 programmatic navigation

这是一个异步函数,如果希望在完成导航后执行其他操作,需要使用 async-await 语法或链式调用

vue
<script setup>
const router = useRouter();
const name = ref('');
const type = ref(1);

function navigate(){
  return navigateTo({
    path: '/search',
    query: {
      name: name.value,
      type: type.value
    }
  })
}
</script>

layouts 目录

参考

在 📁 layouts 目录所创建的组件会作为页面的布局骨架,一般是将多个页面的共用 UI 部分(如所有页面的顶部都有的导航栏)抽离出来放到这里

创建布局

这个目录下的组件只是提供了特定的页面布局,所以模板中还需要包含插槽 <slot/>(至少一个),以便将具体的页面内容插入到其中

注意

布局组件必须只有一个根元素 a single root element,而且这个根元素不能<slot/>,以便 Nuxt 在页面进行布局切换时应用过渡效果 transition

需要在该目录下应该创建一个名为 default.vue 的文件,以便让页面具有默认布局可以使用

layouts/default.vue
vue
<template>
  <div>
    <AppHeader />
    <slot />
    <AppFooter />
  </div>
</template>

也可以设置多个具名插槽,实现更复杂的布局

layouts/default.vue
vue
<template>
  <div>
    Some shared layout content:
    <!-- 具名插槽 header -->
    <slot name="header">
      header slot content for title
    </slot>
    <!-- 默认插槽 -->
    <slot />
  </div>
</template>

采用默认布局

在使用布局时,如果项目采用单页面模式,即项目中只有 app.vue 文件,则需要在其中添加 Nuxt 的内置组件 <NuxtLayout> 将布局应用到页面上

app.vue
vue
<template>
  <NuxtLayout>
    some page content
  </NuxtLayout>
</template>
推荐

若整个应用都只采用一个布局,则推荐使用 app.vue 作为项目的入口,不需要再创建 layout/default.vue 布局组件,直接在 app.vue 中设置页面的骨架布局即可。如果同时需要采用多页面模式(在目录 pages 目录下创建页面),则需要在 app.vue 组件的模板中添加内置组件 <NuxtPage/>

app.vue
vue
<template>
  <div>
    <NuxtPage />
  </div>
</template>

如果项目采用多页面模式,即在目录 pages 下创建页面,则这些页面会采用默认布局

采用其他布局

一般情况下采用默认布局 layouts/default.vue,如果需要采用其他布局,可以进行如下设置

如果需要为 app.vue 设置特定的布局模板,可以通过 <NuxtLayout name="layoutName"> 来配置,则相应的布局模板就会按需自动异步导入 asynchronous import

说明

layout 的名称采用 kebab-case 短横线分隔命名法,例如文件名为 someLayout 对应的布局名称是 some-layout

app.vue
vue
<template>
  <NuxtLayout name="custom">
    <!-- 将指定的内容插入到布局模板的默认插槽中 -->
    Hello world!
  </NuxtLayout>
</template>

而对于 📁 pages 目录中的文件,则可以通过页面元信息 page metadata 设置使用哪个布局模板

pages/index.vue
vue
<script setup>
// 通过设置页面元信息来使用相应的布局模板
definePageMeta({
  layout: "custom",
});
</script>

如果希望 📁 pages 目录下的页面也采用 <NuxtLayout name="layoutName"> 的方式来设置布局模板,则需要先将页面元信息的 layout 属性设置为 false

注意

需要注意一点是,在目录 pages 的文件里使用内置组件 <NuxtLayout> 来设置当前页面的布局时,该内置组件不能作为根元素

pages/index.vue
vue
<script setup>
// 将页面元信息设置为 false
definePageMeta({
  layout: false,
});
</script>

<template>
  <NuxtLayout name="custom">
    <template #header> Some header template content. </template>
    The rest of the page
  </NuxtLayout>
</template>
提示

页面的元信息 page meta 可以通过页面的路由对象 route 来获取。

它是一个响应式变量,可以通过修改它的相应属性 meta实现页面布局的切换

vue
<script setup>
// 路由对象
const route = useRoute()
function enableCustomLayout () {
  // 通过改变该页面路由对象的相应属性来切换页面的布局
  route.meta.layout = "custom"
}

// 通过 `definePageMeta()` 设置该页面元信息
// 将属性 layout 设置为 false 表示不采用默认的布局模板
definePageMeta({
  layout: false,
});
</script>
<template>
  <div>
    <!-- 点击该按钮,调用相应的方法,更改页面所采用的布局 -->
    <button @click="enableCustomLayout">Update layout</button>
  </div>
</template>

middleware 目录

参考

在 📁 middleware 目录种存放着一些路由中间件 Route Middleware,以控制应用在不同页面之间的导航行为

说明

Nuxt 3 的路由中间件 Route Middleware 是指在运行在 Vue 内的一系列函数(即 Nuxt 应用的前端部分),会在应用进入特定路由执行 run before navigating to a particular route,即路由守卫钩子函数

这里所说的路由中间件 Route Middleware 和常见于后端的服务器中间件 server middleware 不同。

Nuxt 的服务器中间件是定义在 server 目录里的,它们是在 Nitro 引擎中运行的一系列函数,其作用一般是对 HTTP 请求进行检验、修改等。

创建路由中间件

通过函数 defineNuxtRouteMiddleware(handler) 创建一个路由中间件(具名路由中间件或全局路由中间件)

该函数 handler(to, from) 接收两个参数

  • 第一个参数 to:表示需要导航到的目标页面的路由对象
  • 第二个参数 from:表示当前页面的路由对象

Nuxt 根据处理函数 handler() 的返回值来实现不同的导航行为,包括放行导航、重定向、阻止导航

该函数最后可以返回以下类型的值

  • 无返回值:守卫放行。进入下一个路由中间件,或执行路由导航
  • 返回 navigateTo(route) 函数:它是 Nuxt 提供的 helper 工具函数,用于跳转到给定的路由 route
    提示

    该方法也可以在前端直接调用以触发路由导航,具体用法可以参考另一篇笔记《Nuxt 3 API》关于 navigateTo 的部分

  • 返回 abortNavigation(err) 函数:它是 Nuxt 提供的 helper 工具函数,用于阻止路由导航,可以传入一个(可选)参数 err 以便同时抛出错误信息
middleware/exampleMiddleware.ts
ts
// 定义一个路由中间件
export default defineNuxtRouteMiddleware((to, from) => {
  // 根据目标路由的参数 id 的值
  // 决定页面导航的行为
  if (to.params.id === '1') {
    // 如果该参数值为 1
    // 则阻止页面导航
    return abortNavigation()
  }
  // 如果该参数值不为 1
  // 则导航到首页
  return navigateTo('/')
})
提示

Nuxt 的路由中间件的处理函数 handler() 和 Vue Router 的路由守卫钩子函数类似,可以尝试返回 Vue Router 的路由守卫钩子函数所支持的返回值(大部分都是可行的),以控制导航行为

但是推荐采用上文所描述的 3 种类型的值作为处理函数 handler() 的返回值,因为未来 Nuxt 版本升级后,可能就不兼容 Vue Router 路由守卫钩子函数的某些了返回值了

其中值得注意的不同点是,与 Vue Router 的路由守卫钩子函数不同,Nuxt 路由中间件的处理函数没有传递 next 作为第三个参数,所以在处理函数中 ❌ 无法使用 next() 方法

还可以通过的 helper 函数 addRouteMiddleware(middle-name, handler(to, from), configObj) 为应用动态添加 dynamically一个具名路由中间件(默认)或全局路由中间件(在该函数的第二个参数对象中设置 { global: true})。

这种方法一般在插件中使用

ts
// 一般用于插件中
export default defineNuxtPlugin(() => {
  // 添加一个全局路由中间件
  addRouteMiddleware('global-test', () => {
    console.log('this global middleware was added in a plugin and will be run on every route change')
  }, {
    global: true
  })

  // 添加一个具名路由中间件
  addRouteMiddleware('named-test', () => {
    console.log('this named middleware was added in a plugin and would override any existing middleware of the same name')
  })
})
注意

由于 Nuxt 支持 SSR 服务端渲染,所以路由中间件也可能会在后端环境中执行(渲染生成页面时)

对于依赖浏览器 API 的路由中间件,需要在其代码逻辑中进行前后端的兼容性考量,例如通过 Nuxt 自定义在 process 对象上定义的属性 process.serverprocess.client 来判断当前运行环境

提示

Nuxt 在 process 对象上创建了属性 serverclient,由于 process 是 Node.js 所提供的变量,所提设置在该对象上,可以很方便地获取到。

然后在打包工具中根据运行环境,对它们的值(布尔值)进行设置,例如打包工具 vite 是在 client.ts 文件server.ts 文件针对不同的运行环境对它们进行设置。

ts
export default defineNuxtRouteMiddleware(to => {
  // 如果页面是在服务端进行渲染的
  // 则直接返回/放行,不执行该路由中间件的余下代码
  if (process.server) return
  // 如果页面是在前端被访问时
  // 则直接返回/放行,不执行该路由中间件的余下代码
  if (process.client) return
  // or only skip middleware on initial client load
  const nuxtApp = useNuxtApp()
  // 如果页面是在客户端首次加载
  // 则直接返回/放行,不执行该路由中间件的余下代码
  if (process.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return
})

不同类型的路由中间件

Nuxt 提供了 3 种类型的路由中间件,以便设置不同等级(守卫范围不同)的路由守卫 route guard:

  • 匿名/内联路由中间件 Anonymous/Inline Route Middleware:直接在特定页面的元信息中进行创建
    pages/example.vue
    vue
    <script setup>
    definePageMeta({
      // This is an example of inline middleware
      middleware: () => {
        console.log('Strictly forbidden.')
        // 返回 false 表示阻止路由导航
        // 所以无法进入该页面
        return false
      }
    })
    </script>
    
  • 具名路由中间件 Named Route Middleware:在 📁 middleware/ 目录中创建,然后在(需应用该路由守卫的)页面的元信息中进行设置。
    在导航到该页面时,该路由守卫代码就会异步加载
    注明

    具名路由中间件的名称会根据文件名而定,而且会通过 kebab-case 短横线分隔命名法进行标准化,例如在名为 someMiddleware 文件中所定义的路由中间件,在使用/引用它时,对应的名称是 some-middle


    先定义一个路由中间件
    middleware/auth.ts
    ts
    // 该路由中间件的作用是鉴别访问页面的用户是否已经拥有相应的权限
    export default defineNuxtRouteMiddleware((to, from) => {
      // isAuthenticated() 方法表示在其他文件中定义的工具函数,用于鉴权
      // 它返回一个布尔值,以表示用户是否已经获取了相应的权限
      // isAuthenticated() is an example method verifying if a user is authenticated
      if (isAuthenticated() === false) {
        // 如果用户没有获取相应的权限,则跳转到登录页面
        return navigateTo('/login')
      }
    })
    

    再在页面中应用
    pages/dashboard.vue
    vue
    <script setup>
    definePageMeta({
      middleware: 'auth'
    })
    </script>
    <template>
      <h1>Welcome to your dashboard</h1>
    </template>
    
  • 全局路由中间件 Global Route Middleware:在 📁 middleware/ 目录中创建具有 .global 后缀的文件,则在其中所定义的路由中间件会自动应用到所有页面
    middleware/alwaysRun.global.ts
    ts
    // 全局路由中间件,文件名具有 .global 后缀
    export default defineNuxtRouteMiddleware(() => {
      console.log('running global middleware')
    })
    

composables 目录

参考

📁 composables 目录是用于存放自定义的 Vue 组合式函数,这些组合式 API 支持自动导入 auto-import

注意

在运行命令 nuxi preparenuxi devnuxi build 时 Nuxt 会自动为这些组合式 API 生成 .nuxt/imports.d.ts 类型文件,以便在开发环境中使用这些组合式 API 时有良好的提示和类型约束

因此在首次创建项目后,如果没有执行命令运行/构建应用,则可能会在代码编辑器中看到 Cannot find name 'componentName' 的错误提示

这是因为还没有生成 .nuxt/imports.d.ts 类型文件,需要先执行前面提到的命令,以便生成相应的类型文件

Nuxt 只会扫描 📁 composables 目录下的作为直接子元素的文件(即位于顶层的文件)

md
📂 composables
 |-- 📄 index.ts ✅
 |-- 📄 useFoo.ts ✅
 |-- 📂 nested
      |-- 📄 utils.ts <!-- ❌ not scanned -->

以上示例中 Nuxt 只会扫描搜索到 📄 useFoo.ts 和 📄 index.ts 文件

为了让嵌套在子目录里的组合式 API 也可以被「扫描」到(以支持自动导入 auto-import),Nuxt 官方推荐 :thumbsup: 创建一个文件 composables/index.ts 作为**「汇总」文件**,将子目录里的文件导入后再重新导出

composables/index.ts
ts
// Enables auto import for this export
export { utils } from './nested/utils.ts'

也可以在配置文件 nuxt.config.ts 的属性 import.dirs 添加其他目录,以扫描其中的文件

nuxt.config.ts
ts
export default defineNuxtConfig({
  imports: {
    dirs: [
      'composables', // 默认目录
      // 扫描直接子目录(只嵌套一层的目录)里的特定文件
      'composables/*/index.{ts,js,mjs,mts}',
      // 扫描该目录下(包括其后代目录里的)所有文件
      'composables/**'
    ]
  }
})

组合式函数可以有两种不同的方式导出,这会导致组合式 API 的名称略有不同

  • 默认导出:使用文件名作为组合式 API 的名称(采用 Camel Case 小驼峰命名法)
    composables/use-foo.ts 或 composables/useFoo.ts
    ts
    // It will be available as useFoo() (camelCase of file name without extension)
    export default function () {
      return useState('foo', () => 'bar')
    }
    
  • 命名导出:使用变量名作为组合式 API 的名称
    composables/useFoo.ts
    ts
    export const useFoo = () => {
      return useState('foo', () => 'bar')
    }
    

然后在组件中可以直接使用(因为这些组合式 API 支持自动导入 auto-import)

vue
<template>
  <div>
    {{ foo }}
  </div>
</template>

<script setup>
// 使用组合式 API
const foo = useFoo()
</script>

后端相关

该小节介绍 Nuxt 项目结构中与后端相关的文件和目录

server 目录

参考

Nuxt 会自动扫描 📁 server 目录下的 3 个文件夹 server/apiserver/routeserver/middleware,基于 server/apiserver/route 目录里面的文件(层级嵌套结构)注册服务端路由/API,而根据 server/middleware 目录里的文件注册服务端路由守卫

api 子目录

Nuxt 会基于 📁 server/api 目录里的文件的层级嵌套结构,创建出相应的服务端路由(拥有 /api 前缀)

Tip

该目录下所创建的服务器 API 会拥有 /api 前缀

如果想创建具有 /api 前缀的服务端 API,可以将文件放置在 📁 server/routes 目录下

server/api/hello.ts
ts
// 创建了一个 API '/api/hello'
// 为该服务端 API 设置处理函数
export default defineEventHandler((event) => {
  return {
    api: 'works'
  }
})
// 然后可以在客户端向 API `await $fetch('/api/hello')` 发送请求

此外还可以采用特定的目录或文件名称创建具有特定功能的路由

  • 可以在文件名中使用方括号 [] 来创建动态路由
    例如通过文件 server/api/hello/[name].ts 创建了一个动态路由
    server/api/hello/[name
    ts
    // 创建了一个动态路由
    // 这里在服务端为该 API 设置处理函数
    // 通过 event.context.params 获取该动态路由的参数值
    export default defineEventHandler(event => `Hello, ${event.context.params.name}!`)
    // 如果前端端向 API 发送请求 `await $fetch('/api/hello/ben')`
    // 则可以获得的响应值是 `Hello, ben!`
    
  • 可以通过 [...] 创建一个匹配所有的 URL 的路由,即通用路由
    例如创建一个文件 server/api/foo/[...].ts 可以响应一系列的请求,譬如(前端)向 /api/foo/bar/baz 所发送的请求
  • 如果将请求的方法作为文件名的后缀(用 . 点分隔,例如 .get.post.put.delete 等),可以针对同一个服务端的路由的不同请求方式,设置不同的处理函数
    server/api/test.get.ts
    ts
    // 该处理函数会响应使用 GET 方法向该路由发送过来的请求
    export default defineEventHandler(() => 'Test get handler')
    
    server/api/test.post.ts
    ts
    // 而该处理函数则会响应使用 POST 方法向该路由发送过来的请求
    export default defineEventHandler(() => 'Test post handler')
    

    以上例子为 /test 路由设置了两个处理函数,而且分别针对采用 GETPOST 方法向该 API 发送请求的情况。而如果使用其他方法方式请求,则会返回 404 错误提示。
    说明

    因为不同的请求方法传递的信息和格式不相同,一般需要设置不同的处理方法。所以 Nuxt 为此场景提出基于特定的文件命名规则,可以十分方便地针对不同的请求方法,设置不同的响应处理方法。

    例如使用 POST 方法发送请求时,一般会发送请求体 request body,可以在路由处理函数中获取到它

    server/api/submit.post.ts
    ts
    // 该服务器路由的处理函数只会响应通过 `POST` 发送的请求
    // 这样在处理函数内就可以安全地使用组合式 API `readBody()` 获取请求体
    // 如果处理函数不对请求方法加以区分
    // 对通过 `GET` 方法发送过来的请求也调用了 `readBody(event)`
    // 则会引起 `405 Method Not Allowed` 的错误。
    export default defineEventHandler(async (event) => {
        // 使用 readBody() 方法获取请求体
        const body = await readBody(event)
        return { body }
    })
    

routes 子目录

Nuxt 会基于 📁 server/routes 目录里的文件的层级嵌套结构,创建出相应的服务端路由(不包含 /api 前缀)

server/routes/hello.ts
ts
// 创建一个 /hello 路由
export default defineEventHandler(() => 'Hello World!')
// 那么在开发环境中,就可以在客户端通过 http://localhost:3000/hello 访问该 API

middleware 子目录

Nuxt 会将 📁 server/middleware 目录下的文件作为服务器的中间件,它们会在每一个请求进入服务器相应路由执行,例如对请求进行检查、修改请求内容、记录日志等

注意

中间件的处理函数不应该阻止请求或返回任何值作为响应,它们应该只检查请求,或扩展请求内容,或抛出错误

server/middleware/log.ts
ts
// 设置一个中间件
// 其作用是输出每一个请求的目标 url
export default defineEventHandler((event) => {
  console.log('New request: ' + event.req.url)
})
server/middleware/auth.ts
ts
// 设置一个中间件
// 其作用是为每一个请求的内容中添加一个属性 auth
export default defineEventHandler((event) => {
  event.context.auth = { user: 123 }
})

以上 3 个子目录中的文件最后都应该导出一个 defineEventHandle(handler) 函数,以处理来自客户端的相应请求

其中处理函数 handler(event) 的入参是 event 可以是请求对象(如果请求是在前端发出的),也可以表示事件对象(因为在 SSR 服务端渲染时,会由服务器自身发出模拟请求,这时实际上分发的是相应的事件,以触发相应的路由处理函数执行)

处理函数 handler 最后应该返回一个 JSON 对象,或一个 Promise 作为响应,或使用 event.res.end()(作出响应,但没有返回具体的内容)以发送响应

plugins 子目录

在 📁 server 目录下还可以创建一个名为 📁 plugin 的子目录

Nuxt 的后端引擎是 Nitro,可以在 📁 /server/plugins 目录里为 Nitro 创建插件,以便对 Nitro 的运行时 Runtime 进行扩展

server/plugins/nitroPlugin.ts
ts
export default defineNitroPlugin((nitroApp) => {
  console.log('Nitro plugin', nitroApp)
})

关于如何开发 Nitro 插件,可以查看 Nitro 官方指南的相关部分

示例

以下是服务器路由的配置示例,以及一些更高级的处理请求的场景

  • 获取请求体
    sever/api/submit.post.ts
    ts
    // 创建一个服务端路由的处理函数
    // 针对使用 POST 方法发过来的请求
    export default defineEventHandler(async (event) => {
        // 因为这个处理函数针对的是 POST 请求
        // 所以该请求里具有请求体 request body
        // 可以使用 h3 库所提供的组合式 API readBody() 来获取请求体
        // 参考 https://github.com/unjs/h3#utilities
        const body = await readBody(event)
        // 返回请求体,作为响应
        return { body }
    })
    // 例如前端对 '/api/submit` 所发送的请求如下(包括请求体)
    // $fetch('/api/submit', { method: 'post', body: { test: 123 } })
    // 则所获得的响应是 { method: 'post', body: { test: 123 } }
    
  • 获取请求参数
    server/api/query.get.ts
    ts
    // 创建一个服务端路由的处理函数
    // 针对使用 GEt 方法发过来的请求
    export default defineEventHandler((event) => {
      // 使用 h3 库所提供的组合式 API getQuery() 来获取请求时所带的参数
      // 参考 https://github.com/unjs/h3#utilities
      const query = getQuery(event)
      // 基于请求时所带的参数,构建响应数据
      return { a: query.param1, b: query.param2 }
    })
    // 例如前端使用 GET 方法向 `/api/query?param1=123&param2=456` 发送请求
    // 则所获得的响应是 { a: 123, b: 456 }
    
提示

Nuxt 后端引擎 Nitro 使用 h3来创建服务端路由,它提供了一系列的 helper 函数,以更方便地获取请求的数据或构建响应数据等

  • readBody(event) 方法:获取请求体
  • getQuery(event) 方法:获取请求参数
  • useRuntimeConfig() 方法:获取应用运行时的配置
  • parseCookies(event) 方法:获取请求所附带的 Cookies

也支持在 📁 server/utils 目录中自己创建一些自定义 helper 函数,以用于服务器路由处理函数中

  • 返回流数据(实验性支持)
    server/api/foo.get.ts
    ts
    import fs from 'node:fs'
    import { sendStream } from 'h3'
    
    export default defineEventHandler((event) => {
      return sendStream(event, fs.createReadStream('/path/to/file'))
    })
    
  • 重定向
    server/api/foo.get.ts
    ts
    export default defineEventHandler((event) => {
      // 使用组合式 API sendRedirect() 实现重定向
      return sendRedirect(event, '/path/redirect/to', 302)
    })
    
  • 存储层:后端引擎 Nitro 提供了一个存储层 storage layer,它可以对接文件存储系统、数据库等数据源,并将不同的数据存储平台抽象为统一的 API,方便在项目中使用
    nuxt.config.ts
    ts
    export default defineNuxtConfig({
      // 在配置文件设置存储层
      nitro: {
        storage: {
          'redis': {
            driver: 'redis',
            /* redis connector options */
            port: 6379, // Redis port
            host: "127.0.0.1", // Redis host
            username: "", // needs Redis >= 6
            password: "",
            db: 0, // Defaults to 0
            tls: {} // tls/ssl
          }
        }
      }
    })
    
    server/api/test.post.ts
    ts
    // 设置一个服务端路由处理函数
    // 用于将前端发过来的数据设置存储到 storage layer 中
    export default defineEventHandler(async (event) => {
      const body = await readBody(event)
      await useStorage().setItem('redis:test', body)
      return 'Data is set'
    })
    
    server/api/test.get.ts
    ts
    // 设置一个服务端路由处理函数
    // 从 storage layer 获取数据
    export default defineEventHandler(async (event) => {
      const data = await useStorage().getItem('redis:test')
      return data
    })
    
  • 错误处理
    默认情况下,如果服务端路由正常响应,则返回的状态码 status code 是 200 OK;如果有任何错误被触发,则返回的状态码是 500 Internal Server Error
    如果希望路由处理成功时,返回其他状态码,可以使用工具函数 setResponseStatus() 进行修改
    ts
    // 该路由文件的路径是 server/api/validation/[id].ts
    export default defineEventHandler((event) => {
      setResponseStatus(event, 202)
    })
    

    如果希望在发生错误时,返回其他的状态码,以便让前端了解到更详细具体的错误信息,可以使用 createError 来创建错误并抛出
    ts
    // 该服务端路由文件的路径是 server/api/validation/[id].ts
    // 创建一个服务端路由的处理函数
    export default defineEventHandler((event) => {
      const id = parseInt(event.context.params.id) as number
      if (!Number.isInteger(id)) {
        // 如果 id 不是整数就抛出一个自定义的错误
        throw createError({
          statusCode: 400, // 状态码
          statusMessage: 'ID should be an integer', // 错误信息
        })
      }
      return 'All good'
    })
    
旧版本

服务端处理函数可以采用传统的方式,但是应该尽量避免采用这种旧模式

server/api/legacy.ts
ts
export default (req, res) => {
  res.end('Legacy handler')
}

扩展相关

该小节介绍 Nuxt 项目结构中与扩展性相关的文件和目录

plugins 目录

参考

Plugins Directory

更详细的介绍也可以查看另一篇笔记《Nuxt 3 插件》的相关部分

📁 plugins 目录用于放置 Nuxt Plugins 插件,可以对应用的运行时 Runtime nuxtApp 进行扩展

Nuxt 会自动注册该目录下的插件,所以不需要在配置文件 nuxt.config.ts 进行手动注册,即可在创建 Vue 前端应用时加载到其中

Nuxt 只会扫描该目录下的作为作为直接子元素的文件(即位于顶层的文件)

md
📂 plugins
 |-- 📄 myPlugin.ts ✅
 |-- 📂 myOtherPlugin
      |-- 📄 supportingFile.ts <!-- ❌ not scanned -->
      |-- 📄 componentToRegister.vue <!-- ❌ not scanned -->
      |-- 📄 index.ts <!-- ❗ currently scanned but deprecated -->

以上示例中 Nuxt 只会扫描搜索到 📄 myPlugin.ts 和 📄 myOtherPlugin/index.ts 文件并自动导入

为了让嵌套在子目录里的文件也可以被「扫描」到,Nuxt 官方推荐 :thumbsup: 创建一个文件 plugins/index.ts 作为**「汇总」文件**,将子目录里的文件导入后再重新导出

可以为文件的名称添加数字前缀(用 . 点分隔),以调整相应的创建的载入顺序。例如一个插件 myOtherPlugin.ts 需要依赖另一个插件 myPlugin.ts

则可以为它们添加相应的数字前缀,则会先载入 myPlugin.ts 插件,再载入 myOtherPlugin.ts

md
📂 plugins
 |-- 📄 1.myPlugin.ts
 |-- 📄 2.myOtherPlugin.ts

可以为文件的名称添加 .client后缀,则该插件只在前端页面 client-only 载入;或添加 .server 后缀,则该插件只在后代渲染页面 SSR 时载入

modules 目录

参考

更详细的介绍也可以查看另一篇笔记《Nuxt 3 模块系统

Nuxt 3 提供了一个模块系统,它其实是一些异步函数,它们会在 nuxt devnuxi build 命令运行时依次被调用,可以方便地对 Nuxt 3 的各种核心功能进行修改和扩展。

📁 modules 目录用于存放本地定义的 Nuxt Modules 模块

Nuxt 会扫描该目录,并自动注册以下文件里所定义的模块

  • modules/*/index.ts 该目录里的(包含内嵌到后代目录的)index.ts 文件
  • modules/*.ts 该目录下的(作为直接子元素的)*.ts 文件

所以在以上文件里所定义的 Nuxt 模块,并不需要在配置文件 nuxt.config.ts 里手动注册就可以自动应用于项目中

说明

本地模块会按照字母的先后顺序 alphabetical order 进行自动注册,而模块的注册顺序会决定它们的执行顺序

可以在命名文件时,添加相应的数字前缀(并以 . 点分隔)来调整相应的模块的注册顺序

md
📁 modules
 |-- 📁 1.first-module/
      |-- 📄 index.ts
 |-- 📄 2.second-module.ts

其他

.nuxt 目录

参考

📁 .nuxt 目录是在开发环境下(执行 nuxt dev 命令时)Nuxt 自动生成的目录,在该目录中存放的是生成的应用,一般不需要对该目录下的文件进行修改。

该目录在执行 nuxt dev 命令时会自动重新生成整个 .nuxt 目录,所以一般不需要手动清理

说明

Nuxt 为模块 module 提供了一个 VFS(Virtual File System)虚拟文件系统,在开发时可以将模块的内容/模板添加到项目中,但并不需要将文件真的写入到系统的硬盘里,可以通过 http://localhos:3000/_vfs 查看

.output 目录

参考

📁 .output 目录是在生产环境下(执行 nuxt build 命令时)Nuxt 自动生成的目录,存放适用于部署到生产环境的 Nuxt 应用文件。

一般不需要再对该目录下文件进行修改,该文件可以直接用于部署

该目录在执行 nuxt build 命令时会自动重新生成整个 .output 目录,所以一般不需要手动清理

utils 目录

参考

在 📁 utils 目录下存放一些辅助/工具函数,在使用它们时支持自动导入 auto-import

说明

该目录和 composables 目录类似,但是从语义上而言,该目录存放的工具函数更具通用性,而 composables 目录存放的组合式 API 则更偏向于针对特定的应用场景 ❓

该目录下的工具函数只能用在 Vue 应用(前端)部分。而用于后端引擎 Nitro 的工具函数则是存放在 📁 server/utils 目录中。


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes