Nuxt 3 特色功能

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

Nuxt 3 特色功能

Nuxt 为开发者提供了一系列的特色功能,让开发体验更流畅。

自动导入

参考

Nuxt 3 会自动导入 Vue 所提供的 API(如各种组合式 API 和生命周期的钩子函数),以及 Nuxt 额外提供的 API 和内置组件,所以在使用它们前不需要先导入,真正做到开箱即用,让开发体验更流畅。

提示

可以查看官方文档的 API 部分来了解各种自动导入的 Nuxt 3 内置功能:

注意

请特别留意使用这些自动导入的组合时 API 的场景,它们需要依赖一定的上下文环境。

所以尽量只在 <script setup> 代码中、defineNuxtPlugindefineNuxtRouteMiddleware 函数中使用

在以下 📁 目录中所定义并导出的函数/组件,在使用它们时,Nuxt 3 也会支持将它们自动导入:

另外对于 Nuxt 项目中与后端相关的部分,在文件夹 📁 server/utils 中所导出的工具函数,也支持自动导入。

提示

Nuxt 为自动导入的这些函数和组件等,提供了一个别名 #imports,可以从它里面显式地导入所需的内容

vue
<script setup>
import { ref, computed } from '#imports'

const count = ref(1)
const double = computed(() => count.value * 2)
</script>

如果希望 disable 取消自动(隐式)导入的功能,以便统一协作开发的习惯/规范,也可以在配置文件 nuxt.config.ts 中进行相应的设置

nuxt.config.ts
ts
export default defineNuxtConfig({
  imports: {
    // 取消了自动(隐式)导入
    // 但依然支持显式导入,即通过从别名 `#imports` 中导入
    autoImport: false
  }
})

视图

参考

Views

更详细的介绍也可以查看另一篇笔记《Nuxt 3 项目结构》的相关部分:

Nuxt 提供了多种方式设置应用的 UI(user interface),可以在不同的级别/颗粒度上创建用户交互界面

  • app.vue 单文件组件:它是基础组件/根组件,作为项目的入口 entrypoint,(如果项目中不同时存在 pages 目录)就会创建出的是一个 SPA 单页面应用
  • components 目录:在其中所创建的组件,一般只用于构成页面的一个小部分,例如按钮、菜单等
  • pages 目录:在其中所创建的组件表示不同的页面。
  • layouts 目录:在其中所创建的组件会作为页面的骨架,一般是将多个页面的共用 UI 部分(如所有页面的顶部都有的导航栏)抽离出来放到这里

页面导航

参考

Nuxt 3 的一个核心特点是基于文件系统 file-base,例如根据 📁 pages 目录里的文件的层级嵌套结构,可以自动生成相应的页面 URL(嵌套路由)。

自定义路由

由于 Nuxt 3 采用 Vue Router 来创建和管理(多页面模式下的)路由,所以除了基于 📁 pages 目录里的文件创建路由/页面,还可以在项目中创建文件 📁 app/router.options.ts 对 Vue Router 进行配置,以对路由进行设置

app/router.options.ts
ts
// RouterConfig 类型约束 Vue Router 的可配置对象
import type { RouterConfig } from '@nuxt/schema'

// 可查看 Vue Router 的官方文档了解有哪些可配置项
// 参考 https://router.vuejs.org/api/interfaces/routeroptions.html
export default <RouterConfig> {
  // 配置对象具有属性 routes,其值是一个函数
  // 该函数的入参 _route 是 Nuxt 自动扫描 `pages` 目录里的文件所生成的路由对象
  // 它作为默认路由
  // 根据返回值可以覆写或扩展默认路由(Nuxt 自动扫描 `pages` 目录里的文件所生成的路由)
  // * 如果其返回值是一个数组,其中每个元素都对应一个路由,则以这些路由覆盖默认路由
  // * 如果返回 null 或 undefined 则采用默认路由
  routes: (_routes) => [
    {
      name: 'home',
      path: '/',
      component: () => import('~/pages/home.vue')
    }
  ],
}
提示

除了通过创建 app/router.options.ts 配置文件,其实还可以直接在 Nuxt 配置文件 📄 nuxt.config.ts属性 router.options 对 Vue Router 进行配置

nuxt.config.ts
ts
export default defineNuxtConfig({
  router: {
    // https://router.vuejs.org/api/interfaces/routeroptions.html
    options: {}
  }
})

由于在 Nuxt 配置参数需要 JSON 序列化后,才可以传递给 Vue Router,所以在 nuxt.config.ts 中只能以下选项可以设置(它们的属性值采用可序列化的数据类型)

  • linkActiveClass
  • linkExactActiveClass
  • end
  • sensitive
  • strict
  • hashMode

如果希望灵活性更高、有更多的控制选项,推荐使用 app/router.options.ts 文件

说明

使用以上方法(通过 app/router.options.ts 配置文件)所创建的自定义路由,无法通过方法 definePageMeta() 进行扩展

如果希望使用 definePageMeta() 为自定义路由添加元信息,可以用 pages:extend 钩子函数添加自定义路由

可以在 Nuxt 针对生命周期 pages:extend 设置钩子函数(在应用构建时 build-time 调用),以增添、改变、删除基于 📁 pages 目录里的文件自动创建的路由/页面

可以在配置文件 📄 nuxt.config.ts 的属性 hooks 中设置钩子函数

nuxt.config.ts
ts
export default defineNuxtConfig({
  hooks: {
    // 为 pages:extend 设置钩子函数
    // 入参是根据 `pages` 目录里的文件自动创建的路由对象(数组)
    'pages:extend' (pages) {
      // 向原有的路由数组中添加 push 一个新的路由
      pages.push({
        name: 'profile',
        path: '/profile',
        component: () => import('~/pages/profile.vue')
      })

      // 删除与规则匹配的路由/页面
      function removePagesMatching (pattern: RegExp, pages: NuxtPage[] = []) {
        const pagesToRemove = [] // 记录需要移除的路由/页面
        for (const page of pages) {
          // 如果该路由对应的就是一个页面
          if (pattern.test(page.file)) {
            pagesToRemove.push(page)
          } else {
            // 因为路由可能是嵌套路由,具有 children 属性
            // 则递归调用该方法
            // 以移除嵌套路里符合条件的页面 ❓
            removePagesMatching(pattern, page.children)
          }
        }
        for (const page of pagesToRemove) {
          // 移除相应的路由
          pages.splice(pages.indexOf(page), 1)
        }
      }
      // 删除通过 .ts 文件所定义/创建的页面(文件名以 `.ts` 结尾)
      removePagesMatching(/\.ts$/, pages)
    }
  }
})
提示

还可以使用 Nuxt Module 模块来配置路由,其中开发模块时常用的 @nuxt/kit 依赖包提供了与路由相关的组合式 API,可以更方便地添加自定义路由

  • extendPages (callback: pages => void)
  • extendRouteRules (route: string, rule: NitroRouteConfig, options: ExtendRouteRulesOptions)

关于如何开发 Nuxt Module 模块可以查看另一篇笔记《Nuxt 3 模块系统

历史模式

如果 Nuxt 应用是一个 SPA 单页面应用(SSR 为 false 时),可以启用 Hash 历史模式来记录路由的改变

采用这种模式,在页面切换时路由整体其实并没有更改(只是改变了 URL 的 hash 即 # 井字号后面的部分),不会向后端发送请求

将配置文件 📄 nuxt.config.ts 的属性 router.options.hasMode 设置为 true,以启用 Hash 历史模式

nuxt.config.ts
ts
export default defineNuxtConfig({
  ssr: false,
  router: {
    options: {
      hashMode: true
    }
  }
})
自定义历史模式

Vue Router 支持自定义路由的历史模式

app/router.options.ts
ts
import type { RouterConfig } from '@nuxt/schema'
import { createMemoryHistory } from 'vue-router'

// https://router.vuejs.org/api/interfaces/routeroptions.html
export default <RouterConfig> {
  history: base => process.client ? createMemoryHistory(base) : null /* default */
}

Nuxt 3 提供了一个内置组件 <NuxtLink> 可以在模板中使用,它最终会渲染为 HTML 的 <a> 标签,以供用户点击实现页面的跳转。该组件与原生的 HTML 的 <a> 标签相比有很多优点,例如可以预加载目标页面的相关数据、设置页面之间的过渡动效。

路由中间件

Nuxt 支持 3 种不同类型的路由中间件 Route Middleware,它们运行在 Nuxt 应用的前端部分,以设置不同等级的路由守卫 route guard来控制页面导航行为

提示

Nuxt 提供了一个组合式 API useRoute() 以便获取当前页面的路由相关信息

vue
<script setup>
// 该文件路径是 pages/posts/[id].vue
const route = useRoute()

// When accessing /posts/1, route.params.id will be 1
console.log(route.params.id)
</script>
说明

这里所介绍的 Route Middleware 是运行在应用的前端部分的

如果希望了解服务器的中间件 server middleware(应用在服务端的 Nitro 引擎中),请查看另一篇笔记《Nuxt 3 项目结构》的server 目录这一部分

也可以通过页面元信息的属性 validate 对页面进行验证,实现一个简单的路由守卫。

该属性值一般是一个异步函数,它接受当前页面的路由信息对象 route 作为入参,根据函数的返回值,判断页面是否通过了验证(是否可以进行访问,该页面是否可以进行渲染)

如果返回 true 表示当前页面可以进行访问;如果该函数返回 false 而且没有其他匹配的路由,则抛出 404 错误提示(跳转到 404 页面 ❓);也可以返回一个对象,其中包含 statusCode/statusMessage 和所抛出的错误对象

vue
<script setup>
definePageMeta({
  validate: async (route) => {
    // Check if the id is made up of digits
    return /^\d+$/.test(route.params.id)
  }
})
</script>

过渡动效

Nuxt 利用 Vue 的内置组件 <Transition> 实现布局和页面的过渡动效 apply transitions between pages and layouts

注意

其中一个前提是项目使用 app.vue 作为应用的根元素 ❓ 这样组件 <Transition> 才可以控制整个应用,实现页面或布局的切换动效

页面切换动效

可以在配置文件 nuxt.config.ts 的属性 app.pageTransition 为所有页面的切换(切入和离开页面时)开启过渡动效(如果希望取消所有页面的过渡动效,可以将该属性值设置为 false

说明

该属性的值是一个对象,其中各字段会作为组件 <Transition> 的 Props,具体支持哪些字段可以参考 Vue 的官方文档

例如设置的属性 name 是添加到页面根元素的类名 class,属性 mode 是设置动效切换模式

nuxt.config.ts
ts
export default defineNuxtConfig({
  app: {
    pageTransition: {
      name: 'page', // CSS 前缀
      mode: 'out-in' // 切换模式,旧元素先退出,新元素再进入
    }
  },
})
提示

也可以直接在 app.vue 文件中的组件 <NuxtPage> 上通过 transition Prop 进行设置

app.vue
vue
<template>
  <div>
    <NuxtLayout>
      <NuxtPage :transition="{
        name: 'bounce',
        mode: 'out-in'
      }" />
    </NuxtLayout>
  </div>
</template>

但是通过这种方式所设置的过渡动效,无法在特定页面使用 definePageMeta 进行覆写。为了后期可以在特定页面采用不同的过渡动效,推荐在配置文件 nuxt.config.ts 中进行设置

然后在 app.vue 根组件里设置过渡相关的 CSS 代码

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

<style>
/* 使用的前缀 page 是在 nuxt.config.ts 中所设置的 */
.page-enter-active,
.page-leave-active {
  transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
  opacity: 0;
  filter: blur(1rem);
}
</style>

也可以为特定的页面单独设置不同的(切入和离开该页面时)过渡动效,在其元信息的属性 pageTransition 设置不同的前缀。还要记得在 app.vue 中设置过渡相关的 CSS 代码

pages/about.vue
vue
<script setup lang="ts">
// 切入和离开该页面时
// 使用与过渡相关的 CSS 代码的前缀是 rotate
definePageMeta({
  pageTransition: {
    name: 'rotate'
  }
})
</script>
app.vue
vue
<template>
  <NuxtPage />
</template>

<style>
/* ... */
.rotate-enter-active,
.rotate-leave-active {
  transition: all 0.4s;
}
.rotate-enter-from,
.rotate-leave-to {
  opacity: 0;
  transform: rotate3d(1, 1, 1, 15deg);
}
</style>

也可以将属性 pageTransition 设置为 false 取消该页面的切换过渡动效

pages/some-page.vue
vue
<script setup lang="ts">
definePageMeta({
  pageTransition: false
})
</script>
提示

如果在切换页面的同时发生了布局的改变,则页面切换的动效就无法启用,应该设置布局更改的动效

布局更改动效

可以在配置文件 nuxt.config.ts 的属性 app.layoutTransition 为所有布局的更改开启过渡动效(如果希望取消所有布局的过渡动效,可以将该属性值设置为 false

说明

该属性的值是一个对象,其中各字段会作为组件 <Transition> 的 Props,具体支持哪些字段可以参考 Vue 的官方文档

app.config.ts
ts
export default defineNuxtConfig({
  app: {
    layoutTransition: {
      name: 'layout', // CSS 前缀
      mode: 'out-in' // 切换模式,旧元素先退出,新元素再进入
    }
  },
})

然后在 app.vue 根组件里设置过渡相关的 CSS 代码

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

<style>
/* 使用的前缀 layout 是在 nuxt.config.ts 中所设置的 */
.layout-enter-active,
.layout-leave-active {
  transition: all 0.4s;
}
.layout-enter-from,
.layout-leave-to {
  filter: grayscale(1);
}
</style>

类似地,也可以为特定页面的布局单独设置不同的(切入和离开该页面时)过渡动效,在其元信息的属性 layoutTransition 设置不同的前缀。还要记得在 app.vue 中设置过渡相关的 CSS 代码

pages/about.vue
vue
<script setup lang="ts">
definePageMeta({
  layout: 'orange',
  layoutTransition: {
    name: 'slide-in'
  }
})
</script>

也可以将属性 layoutTransition 设置为 false 取消该页面的切换过渡动效

pages/some-page.vue
vue
<script setup lang="ts">
definePageMeta({
  layoutTransition: false
})
</script>

JavaScript Hooks

除了使用 CSS,还可以通过 Vue 为 <Transition> 组件所提供的钩子函数,使用 JavaScript 设置更复杂的过渡动效,例如使用 GSAPTween.js 动画库

page/some-page.vue
vue
<script setup lang="ts">
definePageMeta({
  pageTransition: {
    name: 'custom-flip',
    mode: 'out-in',
    // 使用钩子函数在过渡的不同时期执行特定操作
    onBeforeEnter: (el) => {
      console.log('Before enter...')
    },
    onEnter: (el, done) => {},
    onAfterEnter: (el) => {}
  }
})
</script>

动态过渡

还可以通过内联路由中间件 Inline Route Middleware,(它是一个函数,接受 tofrom 两个参数)通过设置路由对象的属性 to.meta.pageTransition为特定页面设置切换动效

因为路由中间件是一个函数,所以可以根据其他数据动态地为该页面设置切换动效

vue
<script setup lang="ts">
// 文件路径是 pages/[id].vue
definePageMeta({
  pageTransition: {
    name: 'slide-right',
    mode: 'out-in'
  },
  middleware (to, from) {
    // 根据目标路由对象 to 的参数 params.id,为目标页面动态设置过渡动效
    to.meta.pageTransition.name =
    +to.params.id > +from.params.id ? 'slide-left' : 'slide-right'
    // 如果从 id 数值较小的页面切换到较大的页面,则采用 slide-left 向左滑动的切换动效
    // 否则就采用 slide-right 向右滑动的切换动效
  }
})
</script>

<template>
  <h1>#{{ $route.params.id }}</h1>
</template>

<style>
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
  transition: all 0.2s;
}
/* 向左滑动 */
.slide-left-enter-from {
  opacity: 0;
  transform: translate(50px, 0);
}
.slide-left-leave-to {
  opacity: 0;
  transform: translate(-50px, 0);
}
/* 向右滑动 */
.slide-right-enter-from {
  opacity: 0;
  transform: translate(-50px, 0);
}
.slide-right-leave-to {
  opacity: 0;
  transform: translate(50px, 0);
}
</style>

资源管理

参考

Assets

更详细的介绍也可以查看另一篇笔记《Nuxt 3 项目结构》的相关部分:

Nuxt 提供了两个目录 publicassets 用于存储和处理项目的资源文件

  • public 目录:在编译项目时,该目录下的文件都会直接「搬运」到部署服务器根目录下,并不会进行压缩转换处理
  • assets 目录:在编译项目时,该目录下的文件会被打包工具(如 Vite 或 Webpack)进行处理,整合(内嵌)到前端页面中

SEO

Nuxt 提供了多种方法为网页设置头部 <head> 标签的信息,以提升应用的 SEO(search engine optimization)

Nuxt 定义了一个名为 MetaObject 的接口,该类型约束是用于设置头部信息的两个场景,配置 nuxt.config.ts 的属性 head 和组合式 API useHead(obj: MetaObject)

提示

该接口类型由 @unhead/schema 依赖包提供

ts
interface MetaObject {
  // 设置网页的标题
  title?: string;
  // 设置网页的标题模板 ❓
  // 以便为不同的网页动态生成不同的标题
  titleTemplate?: string | ((title?: string) => string);
  templateParams?: Record<string, string | Record<string, string>>;
  // 设置文档根 URL 元素,即 `<base>` 标签
  // 参考 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/base
  base?: Base;
  // 设置样式表
  link?: Link[];
  // 设置元信息
  meta?: Meta[];
  // 设置网页样式
  style?: Style[];
  // 设置脚本,属性值是一个数组
  // 每个元素都是一个对象
  // 其中属性 src 是脚本的来源/链接
  // 还可以通过属性 tagPosition 设置该脚本插入到页面的位置
  // 属性值可以是 'head' | 'bodyClose' | 'bodyOpen' 三者之一
  // tagPosition: bodyClose 将脚本置于 <body> 标签的最后,待面加载完成后才执行该脚本
  //
  script?: Script[];
  // 设置 <noscript> 元素
  // 该元素定义了一些内容
  // 当网页无法执行脚本时,这些内容会插入到网页中以提示用户
  // 参考 https://developer.mozilla.org/en-US/docs/web/html/element/noscript
  noscript?: Noscript[];
  // 为 `<html>` 元素添加属性
  htmlAttrs?: HtmlAttributes;
  // 为 `<body>` 元素添加属性
  bodyAttrs?: BodyAttributes;
}

配置文件

可以通过配置文件 📁 nuxt.config.ts属性 head 为应用的所有页面设置头部信息

属性 head 的值是一个对象,在其中支持设置一系列的属性(以在 <head> 标签内生成相应的标签),可以设置的字段参考前文提及的 MetaObject 接口

nuxt.config.ts
ts
// 为应用的所有网页设置头部信息的示例
export default defineNuxtConfig({
  app: {
    head: {
      meta: [
        { name: 'viewport', content: 'width=device-width, initial-scale=1' }
        // 生成标签 <meta name="viewport" content="width=device-width, initial-scale=1">
      ],
      script: [
        { src: 'https://awesome-lib.js' }
        // 生成标签 <script src="https://myawesome-lib.js"></script>
      ],
      link: [
        { rel: 'stylesheet', href: 'https://awesome-lib.css' }
        // 生成标签 <link rel="stylesheet" href="https://myawesome-lib.css">
      ],
      // please note that this is an area that is likely to change
      style: [
        { children: ':root { color: red }', type: 'text/css' }
        // 生成标签 <style type="text/css">:root { color: red }</style>
      ]
    }
  }
})
说明

Nuxt 默认为每个网页设置以下两个 <meta> 标签

  • <meta charset="UTF-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1.0">

Nuxt 在配置文件 📄 nuxt.config.ts 中特地提供了两个属性 app.head.charsetapp.head.viewport 快速对这两个默认的 <meta> 标签进行修改

nuxt.config.ts
ts
export default defineNuxtConfig({
  app: {
    head: {
      charset: "utf-16",
      viewport: "width=500, initial-scale=1",
    }
  }
})

组合式 API

可以使用组合式 API useHead() 来设置特定的页面的头部信息

入参是一个对象(),该对象可以设置的字段参考前文提及的 MetaObject 接口

响应性

组合式 API useHead() 的入参可以是一个函数,以便动态地(基于不同的页面条件)生成头部信息,该函数最终返回一个符合 MetaObject 接口的对象

此外组合式 API useHead() 的入参也可以是响应式变量,这样就可以让页面的头部信息实现响应式的更改。使用 ref()computed()reactive() 方法对符合 MetaObject 类型的对象进行封装,构造出响应性对象

vue
<script setup>
useHead({
  // 设置网页的标题模板,基于原有的网页标题进行更改
  // 其中 `%s` 是占位符,最终会用页面的原始标题 title 取代
  titleTemplate: 'My App - %s', // or, (title) => `My App - ${title}`
  // 设置 <meta name="viewport" ...> 标签
  viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
  // 设置 <meta charset="UTF-8"> 标签
  charset: 'utf-8',
  // 添加 <meta> 标签
  meta: [
    { name: 'description', content: 'My amazing site.' }
  ],
  // 为 <body> 添加类名
  bodyAttrs: {
    class: 'test'
  }
})
</script>

此外 Nuxt 还提供了两个组合式 API useSeoMeta()useServerSeoMeta() 专门用于为页面设置与 SEO 相关的头部信息。

参考

这两个组合式 API 其实是由 @unjs/Unhead 依赖包提供的,具体可以参考它的官方文档

这两个方法的入参都是一个对象,该对象有完整的 Typescript 支持,以便设置正确合理的 SEO 信息

说明

一般情况下,搜索引擎的爬虫机器人只会对页面首次加载的状态进行扫描,无法捕抓页面(在前端)的响应性更改,所以推荐使用 useServerSeoMeta() 方法设置 SEO 信息,它与看,而获得无关且性能更佳

vue
<script setup lang="ts">
useServerSeoMeta({
  title: 'My Amazing Site',
  ogTitle: 'My Amazing Site',
  description: 'This is my amazing site, let me tell you all about it.',
  ogDescription: 'This is my amazing site, let me tell you all about it.',
  ogImage: 'https://example.com/image.png',
  twitterCard: 'summary_large_image',
})
</script>

和类似 useHead(),这两个组合式 API 的入参也可以是函数,以便动态地(根据其他数据)设置 SEO 信息

vue
<script setup lang="ts">
// 先异步获取数据
const data = useFetch(() => $fetch('/api/example'))

// 再基于数据设置 SEO 信息
useServerSeoMeta({
  ogTitle: () => `${data.value?.title} - My Site`,
  description: () => data.value?.description,
  ogDescription: () => data.value?.description,
})
</script>

内置组件

Nuxt 还提供了一些内置的组件(它们的名称都是首字母大写)来设置网页的头部信息

  • <Head>
  • <Title>
  • <Base>
  • <Link>
  • <Style>
  • <NoScript>
  • <Meta>
  • <Body>
  • <Html>
提示

允许在 <Head><Body> 内部嵌套使用其他的组件,让这些组件的使用习惯和原生标签一样,但是最后这些组件会被渲染为相应的 <meta> 元素,并且位于网页的头部标签内的

vue
<script setup>
const title = ref('Hello World')
</script>

<template>
  <div>
    <Head>
      <Title>{{ title }}</Title>
      <Meta name="description" :content="title" />
    </Head>
  </div>
</template>
Warning

虽然这些组件与 HTML 的标签类似,但它们不是原生的标签,记得使用这些组件时,它们是以大写开头

示例

一个常见的需求是为不同的页面设置不同的标题

  • 基于路由设置页面标题
    可以在特定页面使用 definePageMeta() 方法为该页面(使用默认布局 layout/default.vue)所对应的路由添加元信息
    pages/some-page.vue
    vue
    <script setup>
    // 设置页面元信息(标题)
    definePageMeta({
      title: 'Some Page'
    })
    </script>
    

    在布局组件中通过 useHead() 基于路由元信息为网页设置头部信息
    layouts/default.vue
    vue
    <script setup lang="ts">
    const route = useRoute()
    
    useHead({
      meta: [
        {
          property: 'og:title',
          // 根据路由信息设置 og:title
          content: `App Name - ${route.meta.title}`
        }
      ]
    })
    </script>
    
  • 如果项目存在 app.vue 文件,可以在其中设置好标题模板 titleTemplate,然后在不同页面就可以设置略有不同的标题(如果项目不存在 app.vue 文件,也可以在配置文件 nuxt.config.ts 的属性 app.head.titleTemplate 进行设置)
    app.vue
    vue
    <script setup>
    useHead({
      // titleTemplate 可以是字符串,其中包含 '%s' 占位符
      // 最终会用页面的原始标题 title 取代
      // as a string,
      // where `%s` is replaced with the title
      titleTemplate: '%s - Site Title',
      // titleTemplate 也可以是函数,以它的返回值作为标题
      // ... or as a function
      titleTemplate: (productCategory) => {
        return productCategory
          ? `${productCategory} - Site Title`
          : 'Site Title'
      }
    })
    </script>
    

数据获取

Nuxt 提供了 4 个不同的组合式 API 用于异步获取数据,它们的作用和 JavaScript 原生的 fetch API 类似,以异步的方式获取数据,此外还提供了很多使用性的功能,例如使用这些方法向后端发送请求,则它们会自动反序列化 parse 响应返回的 JSON 数据

注意

这些组合式 API 都只能在 Vue 组件的 setup 或 Lifecycle Hooks 生命周期钩子函数中使用

  • useAsyncData() 方法:以异步的方式获取数据,该方法默认会阻碍进行路由导航直到异步请求的结果返回
  • useLazyAsyncData() 方法:和 useAsyncData() 方法类似,也是用于异步获取数据,但是采用「懒加载」的方式,即不阻碍页面/路由的加载
  • useFetch() 方法:将 useAsyncData()$fetch() 进行封装,专门针对向后端 API 发送请求获取数据的场景,所以只需要传入 URL 即可(不需要繁琐地去设置 handler 处理函数,使用 $fetch() 作为 handler),该方法会阻碍进行路由导航
  • useLazyFetch() 方法:和 useFetch() 方法类似,但是采用「懒加载」的方式(实际是将配置对象的属性 lazy 设置为 true),即不阻碍进行路由导航。所以在设计页面时需要考虑处理未获取到数据的情况
提示

具体介绍可参考另一篇笔记《Nuxt 3 API》。

状态管理

Nuxt 提供了一个组合式 API useState(key, initFn?) 来创建一个全局可及的响应式状态(即整个应用可共享的值)。它用于替代 Vue 的组合式 API ref(),除了可以用于前端,同时还适用于 SSR 服务端渲染,初始化后可以传递给各组件使用。

该方法可以接收两个参数

  • 第一个参数 key:字符串,作为该状态的唯一标识符。(使用唯一标识符来区分状态,如果应用中已经存在了同名/标识符相同的状态,就不会再创建,而是进行引用,这样就可以避免在同一个应用中创建两个相同的状态 ❓)
  • 第二个(可选)参数 initFn:是一个用于设置状态的初始值的函数,以它的返回值作为该状态的初始值
注意

由于 useState() 所封装的值需要序列化 serialized 为 JSON 格式(以便在整个应用中共享 ❓),所以传入的值不能是无法序列化的数据类型,例如 class 类、function 函数、symbol 等

useState() 只可用于 setupVue 的生命周期钩子函数 Lifecycle Hooks

vue
<script setup>
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>

<template>
  <div>
    Counter: {{ counter }}
    <button @click="counter++">
      +
    </button>
    <button @click="counter--">
      -
    </button>
  </div>
</template>

以上例子在一个组件内使用 useState() 方法创建了一个 key 为 counter 的响应式状态,除了可以在该组件内通过变量 counter 使用该状态/值,也可以在其他组件通过 useState('counter')来获取该状态/值

更推荐的做法是将这些全局共享的响应式状态,统一定义到一个文件 📄 composables/state.ts 便于管理

composables/states.ts
ts
// 以组合式 API 的形式导出这些状态
export const useCounter = () => useState<number>('counter', () => 0)
export const useColor = () => useState<string>('color', () => 'pink')

因为在目录 📁 composables 里的文件会自动导入 auto-import,所以在其他组件中可以直接调用相应的组合式 API 来使用这些状态

vue
<script setup>
const color = useColor()
// 也可使用 useState('color') 来获取相应的状态
// 因为该 key 所对应的状态已经创建过
// 所以这里并不会创建一个新的同名的状态,而是会引用在 composables/states.ts 里的状态值 ❓
</script>

<template>
  <p>Current color: {{ color }}</p>
</template>

错误处理

Nuxt 是一个全栈 full-stack 框架,所以在处理错误时需要考虑它可能发生在不同的上下文/运行时中

  • 在 Vue 应用生成的阶段 Vue Rendering Lifecycle(SSR 或 SPA)
    可以使用 Vue 所提供的生命周期钩子函数 onErrorCaptured() 来捕获该阶段所抛出的错误
    也可以使用 Nuxt 所提供的一个类似的生命周期钩子 vue:error,来捕获在生成 Vue 应用时抛出的错误
    提示

    如果在项目中使用了错误汇报框架,需要收集 Vue 抛出的错误,可以在运行时上下文 nuxtApp 的方法 vueApp.config.errorHandler 中设置相关的逻辑,该方法会接受所有由 Vue 抛出的错误,即使它们已经被处理了

    ts
    // 通过创建一个插件来设置/扩展运行时上下文 `nuxtApp`
    export default defineNuxtPlugin((nuxtApp) => {
      nuxtApp.vueApp.config.errorHandler = (error, context) => {
        // ...
      }
    })
    
  • 在执行服务器 API 的处理函数时(后端引擎 Nitro 的运行时)
    目前仍无法通过一个生命周期钩子来捕获该阶段抛出的错误,但可以采用生成「错误页面」(查看下文 👇)的方式来展现该阶段的错误
  • 服务端或客户端启动时 Server and Client Startup(SSR 或 SPA)
    Nuxt 提供的一个生命周期钩子 app:error 来捕获应用启动时抛出的错误
    说明

    前面所说的 Nuxt 启动过程,具体包括以下阶段

    • Nuxt 插件运行的过程
    • app:createdapp:beforeMount 生命周期
    • 客户端生成 app 的过程(虽然该过程也可以通过 Vue 的生命周期钩子函数 onErrorCaptured 或 Nuxt 所提供的 Hook vue:error 来捕获)
    • app:mounted 生命周期
  • 在页面加载 JS chunks 代码分块时
    由于网络连接故障或新部署(使旧的 hashed 所标记的 JS chunks 无效)导致加载 JS chunks 时抛出错误,针对这一场景 Nuxt 提供了内置的支持
    在路由导航期间由于 JS 块加载失败时,会自动执行强制刷新来处理块加载错误
    提示

    可以通过将配置文件 📁 nuxt.config.ts属性 experimental.emitRouteChunkError 设置为 false 来取消这个默认行为;或设置为 manual 以允许手动处理该阶段的错误

工具函数

Nuxt 提供了一系列处理错误的工具函数

  • useError() 获取当前抛出待处理的错误
  • createError() 生成并抛出一个错误,可以设置关于错误的元信息
    vue
    <script setup>
    // 文件路径 pages/movies/[slug].vue
    const route = useRoute()
    const { data } = await useFetch(`/api/movies/${route.params.slug}`)
    if (!data.value) {
      // 如果无法获取数据
      // 就手动生成一个错误,并包含相关信息
      throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
    }
    </script>
    
  • showError() 抛出一个触发全页面展示的错误 full-screen error page,可以调用 clearError() 清除错误信息
    推荐

    推荐使用 throw createError() 来替代

  • clearError() 清除错误信息,可以传入(可选)参数 {redirect: 'path-to-redirect'} 同时将页面从错误信息提示页面重定向到其他指定的页面

错误信息提示页面

在应用的不同阶段抛出错误时,Nuxt 都会会生成一个 JSON 格式的响应对象(如果是在请求后端 API 时发生错误)或 HTML 页面以报告错误信息。

可以在根目录创建一个 error.vue 文件来定制错误信息的展示页面,该页面接收一个 Prop error 它包含了抛出的错误信息

error.vue
vue
<template>
  <button @click="handleError">Clear errors</button>
</template>

<script setup>
const props = defineProps({
  error: Object
})

// 在开发时,根据错误信息排除了 bug 后
// 可以调用方法 clearError() 清除错误信息,并重定向到指定的页面,离开错误信息提示页面
const handleError = () => clearError({
  redirect: '/'
})
</script>
提示

Nuxt 提供了一个内置组件 <NuxtErrorBoundary> 捕抓前端抛出的错误 client-side error,并阻止错误冒泡 bubbling up to top level 可避免跳转到全页面展示错误信息

该组件会捕抓默认插槽 default slot 中抛出的错误,并将错误信息渲染到 #error 插槽中,所以可以将该组件作为页面的根组件(包裹其他组件),这样就可以捕抓到该前端页面所抛出的错误

pages/index.vue
vue
<template>
  <!-- some content -->
  <NuxtErrorBoundary @error="someErrorLogger">
    <!-- You use the default slot to render your content -->
    <template #error="{error}">
      You can display the error locally here.
      <!-- 排除 bug 后,可以将 error 错误对象设置为 null 以取消显示该插槽的内容 -->
      <button @click="error=null">
        This will clear the error.
      </button>
    </template>
  </NuxtErrorBoundary>
</template>

可扩展层

Nuxt 提供了一种称为 layer(在该笔记中会翻译为「可扩展层」)的方式来实现配置项和代码的复用

在终端输入以下命令就可以新建一个 Nuxt Layer

bash
# 使用了一个名为 layer 的模板作为新项目的起手式
# 将 <my-nuxt-layer-name> 替换为项目的名称
# 参考 https://github.com/nuxt/starter/tree/layer
npx nuxi init --template layer <my-nuxt-layer-name>
提示

另外有一种特别的 Nuxt Layer 称为 Nuxt Theme 主题,它是专门针对 nuxt-content 模块而言的,Nuxt Theme 是围绕该模块编写一些可复用的代码

这个模块是可以基于 Markdown、JSON、YAML 等文件生成静态页面,可以将其看作是一个静态博客生成器,相应地 Nuxt Theme 可以看作为博客主题。

如果希望开发一个 Nuxt Theme 主题,则可以采用以下命令新建一个项目

bash
# 使用存储在 github 仓库的模板作为新项目的起手式
# 将 <my-theme-name> 替换为项目的名称
# 参考 https://github.com/nuxt-themes/starter
npx nuxi init --template gh:nuxt-themes/starter <my-theme-name>

和开发 layer 所采用的模板相比,开发主题所采用的模板 templatepackage.json 中预设了 @nuxt/content 依赖包,并在配置文件 📄 nuxt.config.ts 中进行了相应的设置

可以参考学习官方所开发的主题

Nuxt Layer 和普通的 Nuxt 项目结构基本相同,除了 package.json 中进行了不一样的配置,表明该项目是一个 JavaScript 模块(以便让该 layer 可以通过 npm package 的方式进行分发和使用)

package.json
json
{
  "name": "my-nuxt-layer", // 设置 layer 的名称,也是 npm package 依赖包的名称
  "type": "module", // 表示该项目是 JavaScript 模块
  "version": "0.0.1",
  "main": "./nuxt.config.ts", // 该项目的入口点
  // ...
}

另外在模板中有一个 .playground 目录,它是一个极简的 Nuxt 项目,用于调试当前的 layer 项目(在它的配置文件中预设采用 extend 当前的 layer)

说明

在项目中指定使用哪些 layers 后,Nuxt 会自动扫描这些 layers 中的相应文件和目录,将它们整合到项目中,以扩展项目的配置和功能

  • components 目录:扫描 layer 中的 components 目录,在其中定义的组件会整合到当前项目中,而且支持 auto-import 可直接使用
  • composables 目录:扫描 layer 中的 composables 目录,在其中定义的组合式 API 会整合到当前项目中,而且支持 auto-import 可直接使用
  • pages 目录:扫描 layer 中的 pages 目录,在其中定义的页面/组件会整合到当前项目中,会在应用中生成相应的路由/页面
  • server 目录:扫描 layer 中的 server 目录,在其中定义的服务端 API endpoint 和中间件 middleware 会整合到当前项目中,创建相应的服务端路由
  • nuxt.config.ts 文件:将 layer 中配置项与当前项目的配置项整合
  • app.config.ts 文件:将 layer 在该文件中定义的全局变量整合到当前项目中,可以在网页运行时访问到相应的变量

然后在其他的 Nuxt 项目中,通过配置文件 📄 nuxt.config.ts 的属性 extend 指定要采用哪些 layer 以扩展项目的功能

nuxt.config.ts
ts
// 其他项目
export default defineNuxtConfig({
  // 可以同时指定多个 layer 扩展项目
  extends: [
    // 使用位于本地的 local layer,例如在 monorepo 中
    '../base',
    // 使用的 layer 是 npm package 依赖包的
    // 📢 需要先在 package.json 中先 install 相应的依赖包
    '@my-themes/awesome',
    // 使用位于 Github 等远程代码库的 layer
    'github:my-themes/awesome#v1',
  ]
})
提示

Nuxt 项目中所采用的 layer 可以有多种来源,它们分别由相应的依赖包处理、加载、整合,可以查看相应的官方文档或源码了解更多信息

更多关于 layer 的支持和优化可以追踪查看这个 issue#13367

相对路径和别名

🔒 如果在 layer 的组件和组合式 API 中使用了别名 ~/@/,那么当这些代码扩展 extend 到其他 Nuxt 项目时,但是别名的解析会以 Nuxt 项目为参照基础的,这就会导致代码引用的错误

🔑 目前官方给出的解决方法是使用相对路径来替代,而避免使用别名,虽然可能让代码编写起来比较麻烦(特别是文件存在深度嵌套的层级结构)

🔒 而在 layer 的配置文件 nuxt.config.ts 中使用相对路径则会遇到另一个问题,当配置整合到其他 Nuxt 项目时,(除了 extends 属性以外)这些相对路径的解析会以 Nuxt 项目为参照基础的,这就会导致代码引用的错误

🔑 官方给出的解决方法是在 layer 的配置文件中使用路径解析器,将相对路径转换为完整的路径

nuxt.config.ts
ts
// layer 的配置文件
import { createResolver } from '@nuxt/kit'

// 基于当前模块的路径 import.meta.url 创建一个路径解析器
const { resolve } = createResolver(import.meta.url)

export default defineNuxtConfig({
  css: [
    // 通过路径解析器将相对路径转换为完整的路径
    resolve('./assets/main.css')
  ]
})

模块

Nuxt Module 模块可以对构造时 Build Time 进行设置,所以可以使用模块在构建应用时读取并定制 Nuxt 项目中所采用的 layer ❓

modules/my-local-module.ts
ts
export default defineNuxtModule({
  setup(_options, nuxt) {
    for (const layer of nuxt.options._layers) {
      // You can check for a custom directory existence to extend for each layer
      console.log('Custom extension for', layer.cwd, layer.config)
    }
  }
})

通过 nuxt.options._layers 可以读取到在项目中使用的所有 layers 信息,该属性返回一个数组,每一个元素表示项目中所使用的一个 layer,第一个元素是当前项目中编写的文件(优先级最高),随后元素的优先级依次降低,即前面的 layer 的设置会覆盖后面的 layer


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes