Nuxt 3 特色功能
Nuxt 为开发者提供了一系列的特色功能,让开发体验更流畅。
自动导入
Nuxt 3 会自动导入 Vue 所提供的 API(如各种组合式 API 和生命周期的钩子函数),以及 Nuxt 额外提供的 API 和内置组件,所以在使用它们前不需要先导入,真正做到开箱即用,让开发体验更流畅。
提示
可以查看官方文档的 API 部分来了解各种自动导入的 Nuxt 3 内置功能:
注意
请特别留意使用这些自动导入的组合时 API 的场景,它们需要依赖一定的上下文环境。
所以尽量只在 <script setup>
代码中、defineNuxtPlugin
或 defineNuxtRouteMiddleware
函数中使用
在以下 📁 目录中所定义并导出的函数/组件,在使用它们时,Nuxt 3 也会支持将它们自动导入:
另外对于 Nuxt 项目中与后端相关的部分,在文件夹 📁 server/utils
中所导出的工具函数,也支持自动导入。
提示
Nuxt 为自动导入的这些函数和组件等,提供了一个别名 #imports
,可以从它里面显式地导入所需的内容
<script setup>
import { ref, computed } from '#imports'
const count = ref(1)
const double = computed(() => count.value * 2)
</script>
如果希望 disable 取消自动(隐式)导入的功能,以便统一协作开发的习惯/规范,也可以在配置文件 nuxt.config.ts
中进行相应的设置
export default defineNuxtConfig({
imports: {
// 取消了自动(隐式)导入
// 但依然支持显式导入,即通过从别名 `#imports` 中导入
autoImport: false
}
})
视图
参考
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 进行配置,以对路由进行设置
// 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 进行配置
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
中设置钩子函数
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 历史模式
export default defineNuxtConfig({
ssr: false,
router: {
options: {
hashMode: true
}
}
})
自定义历史模式
Vue Router 支持自定义路由的历史模式
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 */
}
NuxtLink 组件
Nuxt 3 提供了一个内置组件 <NuxtLink>
可以在模板中使用,它最终会渲染为 HTML 的 <a>
标签,以供用户点击实现页面的跳转。该组件与原生的 HTML 的 <a>
标签相比有很多优点,例如可以预加载目标页面的相关数据、设置页面之间的过渡动效。
路由中间件
Nuxt 支持 3 种不同类型的路由中间件 Route Middleware,它们运行在 Nuxt 应用的前端部分,以设置不同等级的路由守卫 route guard来控制页面导航行为
提示
Nuxt 提供了一个组合式 API useRoute()
以便获取当前页面的路由相关信息
<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
和所抛出的错误对象
<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
是设置动效切换模式
export default defineNuxtConfig({
app: {
pageTransition: {
name: 'page', // CSS 前缀
mode: 'out-in' // 切换模式,旧元素先退出,新元素再进入
}
},
})
提示
也可以直接在 app.vue
文件中的组件 <NuxtPage>
上通过 transition
Prop 进行设置
<template>
<div>
<NuxtLayout>
<NuxtPage :transition="{
name: 'bounce',
mode: 'out-in'
}" />
</NuxtLayout>
</div>
</template>
但是通过这种方式所设置的过渡动效,无法在特定页面使用 definePageMeta
进行覆写。为了后期可以在特定页面采用不同的过渡动效,推荐在配置文件 nuxt.config.ts
中进行设置
然后在 app.vue
根组件里设置过渡相关的 CSS 代码
<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 代码
<script setup lang="ts">
// 切入和离开该页面时
// 使用与过渡相关的 CSS 代码的前缀是 rotate
definePageMeta({
pageTransition: {
name: 'rotate'
}
})
</script>
也可以将属性 pageTransition
设置为 false
取消该页面的切换过渡动效
<script setup lang="ts">
definePageMeta({
pageTransition: false
})
</script>
提示
如果在切换页面的同时发生了布局的改变,则页面切换的动效就无法启用,应该设置布局更改的动效
布局更改动效
可以在配置文件 nuxt.config.ts
的属性 app.layoutTransition
为所有布局的更改开启过渡动效(如果希望取消所有布局的过渡动效,可以将该属性值设置为 false
)
说明
该属性的值是一个对象,其中各字段会作为组件 <Transition>
的 Props,具体支持哪些字段可以参考 Vue 的官方文档。
export default defineNuxtConfig({
app: {
layoutTransition: {
name: 'layout', // CSS 前缀
mode: 'out-in' // 切换模式,旧元素先退出,新元素再进入
}
},
})
然后在 app.vue
根组件里设置过渡相关的 CSS 代码
<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 代码
<script setup lang="ts">
definePageMeta({
layout: 'orange',
layoutTransition: {
name: 'slide-in'
}
})
</script>
也可以将属性 layoutTransition
设置为 false
取消该页面的切换过渡动效
<script setup lang="ts">
definePageMeta({
layoutTransition: false
})
</script>
JavaScript Hooks
除了使用 CSS,还可以通过 Vue 为 <Transition>
组件所提供的钩子函数,使用 JavaScript 设置更复杂的过渡动效,例如使用 GSAP 或 Tween.js 动画库
<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,(它是一个函数,接受 to
和 from
两个参数)通过设置路由对象的属性 to.meta.pageTransition
为特定页面设置切换动效
因为路由中间件是一个函数,所以可以根据其他数据动态地为该页面设置切换动效
<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>
资源管理
参考
Nuxt 提供了两个目录 public
和 assets
用于存储和处理项目的资源文件
public
目录:在编译项目时,该目录下的文件都会直接「搬运」到部署服务器根目录下,并不会进行压缩转换处理assets
目录:在编译项目时,该目录下的文件会被打包工具(如 Vite 或 Webpack)进行处理,整合(内嵌)到前端页面中
SEO
Nuxt 提供了多种方法为网页设置头部 <head>
标签的信息,以提升应用的 SEO(search engine optimization)
Nuxt 定义了一个名为 MetaObject
的接口,该类型约束是用于设置头部信息的两个场景,配置 nuxt.config.ts
的属性 head
和组合式 API useHead(obj: MetaObject)
提示
该接口类型由 @unhead/schema
依赖包提供
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
接口
// 为应用的所有网页设置头部信息的示例
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.charset
和 app.head.viewport
快速对这两个默认的 <meta>
标签进行修改
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
类型的对象进行封装,构造出响应性对象
<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 信息,它与看,而获得无关且性能更佳
<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 信息
<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>
元素,并且位于网页的头部标签内的
<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.vuevue<script setup> // 设置页面元信息(标题) definePageMeta({ title: 'Some Page' }) </script>
在布局组件中通过useHead()
基于路由元信息为网页设置头部信息layouts/default.vuevue<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.vuevue<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()
只可用于 setup
和 Vue 的生命周期钩子函数 Lifecycle Hooks 中
<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
便于管理
// 以组合式 API 的形式导出这些状态
export const useCounter = () => useState<number>('counter', () => 0)
export const useColor = () => useState<string>('color', () => 'pink')
因为在目录 📁 composables
里的文件会自动导入 auto-import,所以在其他组件中可以直接调用相应的组合式 API 来使用这些状态
<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:created
和app:beforeMount
生命周期 - 客户端生成 app 的过程(虽然该过程也可以通过 Vue 的生命周期钩子函数
onErrorCaptured
或 Nuxt 所提供的 Hookvue:error
来捕获) - 在
app:mounted
生命周期
- 在页面加载 JS chunks 代码分块时
由于网络连接故障或新部署(使旧的 hashed 所标记的 JS chunks 无效)导致加载 JS chunks 时抛出错误,针对这一场景 Nuxt 提供了内置的支持
在路由导航期间由于 JS 块加载失败时,会自动执行强制刷新来处理块加载错误
工具函数
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
它包含了抛出的错误信息
<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
插槽中,所以可以将该组件作为页面的根组件(包裹其他组件),这样就可以捕抓到该前端页面所抛出的错误
<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
# 使用了一个名为 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 主题,则可以采用以下命令新建一个项目
# 使用存储在 github 仓库的模板作为新项目的起手式
# 将 <my-theme-name> 替换为项目的名称
# 参考 https://github.com/nuxt-themes/starter
npx nuxi init --template gh:nuxt-themes/starter <my-theme-name>
和开发 layer 所采用的模板相比,开发主题所采用的模板 template 的 package.json
中预设了 @nuxt/content
依赖包,并在配置文件 📄 nuxt.config.ts
中进行了相应的设置
可以参考学习官方所开发的主题
Nuxt Layer 和普通的 Nuxt 项目结构基本相同,除了 package.json
中进行了不一样的配置,表明该项目是一个 JavaScript 模块(以便让该 layer 可以通过 npm package 的方式进行分发和使用)
{
"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 以扩展项目的功能
// 其他项目
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 的配置文件中使用路径解析器,将相对路径转换为完整的路径
// 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 ❓
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