Nuxt
Vue 生态的 SSR 框架,本文基于 Nuxt ^3.13.0 版本,主要描述 Nuxt 的基本概念及用法。
Github:https://github.com/nuxt/nuxt
中文文档-翻译质量更好:https://nuxt.com.cn
中文文档-翻译相对更全:https://nuxt.zhcndoc.com
英文文档:https://nuxt.com/
模块组件库文档:https://nuxt.com.cn/modules
VsCode 插件:https://marketplace.visualstudio.com/items?itemName=Nuxtr.nuxtr-vscode
安装
环境要求
Node.js > 18.0.0
pnpm dlx nuxi@latest init <project-name>
code <project-name>
pnpm install
pnpm dev -o
安装报错见 Nuxt安装报错问题。
升级
使用该命令升级 Nuxt,而不是单独升级 Nuxt,以确保 Nuxt 相关依赖同步更新。
npx nuxi@latest upgrade --force
Nuxt nitro
Nuxt 的服务端。
Nuxt nitro
是没有 BOM(Browser Object Model)api
和引入的库的 api 的(基于 BOM 封装),故调用会导致项目报错,如 alert。
但 Nuxt 开发环境提供 hmr,这导致 hmr 时开发客户端浏览器还是可以正常使用(会误以为可执行于服务端)。
故需注意,不要让会导致报错的客户端方法于服务端执行,要牢记代码会在服务端及客户端分别渲染(以解决渲染过程的一些 bug ),代码的执行顺序,也会影响最终的水合过程。
Nuxt 开发环境基于 vite,会自动 loader。
Nuxt 的 vue script 最外层会在服务端和客户端都执行一次,故服务端不存在的 api,需要在onMounted钩子执行
(DOM 已于客户端挂载完毕)。
.vue script 最外层代码,log 等会在服务端客户端分别执行,一共两次。
这涉及到水合的概念,详见hydration[水合]。
区分服务端及客户端
使用环境变量区分服务端及客户端。
import.meta.server
-> 服务端
import.meta.client
-> 客户端
渲染模式
Nuxt 支持不同的渲染模式,包括通用渲染、客户端渲染、混合渲染、边缘渲染等。
官方文档:https://nuxt.com.cn/docs/guide/concepts/rendering
客户端渲染
浏览器下载一个<body>
大部分内容为空的 HTML 文档,通过加载运行 JS,动态插入 DOM、CSS 及 JS,使网页渲染和支持互动。
通用渲染
1.服务端将完整渲染的 HTML 文档返回给浏览器。
2.浏览器下载和运行 JS。
3.hydration(水合)
步骤完成,Vue 接管 HTML 的再次渲染,使静态页面样式化与具备交互性(Ajax交互、SPA单页面应用等,不需要再次请求 HTML 文档)。
在 1-2 步骤中,用户无需像客户端渲染一样,等待 JS 渲染 HTML 文档,可以直接查看服务端渲染完成的部分 HTML 文档。
页面同时具备服务端渲染,兼顾 SEO 的同时,又不失去客户端渲染的好处,使静态页面具备交互性,如 JS 渲染动态导航界面等 SPA
应用的优点边缘渲染
边缘渲染(ESR),允许通过 CDN 的边缘服务器,调用离客户端最近的服务器,渲染并返回 HTML 文档,减少延迟,并更快地加载页面。
hydration[水合]
在浏览器中使静态页面具备交互性即称为 hydration[水合]。
一个例子:
好兄弟准备租的房子没装修,房东说你需要用到什么家具我就给你添置什么家具。
好兄弟准备租的房子就可以理解为一个空的 HTML 文档,而房东按需添置的过程,我们就可以理解为 hydration[水合]
,使我们的空房子(静态页面)具备入住条件(样式化及交互性)。
案例
因为 hydration[水合]
过程的存在,服务端变量自增1,渲染到HTML中的值为自增结果,渲染为 <p>1</p>
,HTML文档加载完成时用户看到的是1,但在客户端水合时值会初始化为0,num.value++
重新执行再自增为1,这样用户得到的HTML文档渲染为自增结果,又在水合过程中初始化为0并自增。
最终服务端生成的HTML与客户端加载的JS完成水合,确保了代码的正确执行与服务端客户端的一致性
。
注意:
开发 Nuxt 时,要牢记代码会在服务端及客户端分别渲染(以解决渲染过程的一些 bug ),代码的执行顺序,也会影响最终的水合过程。
<template>
<p>{{ num }}</p>
<!-- num为1 -->
</template>
<script setup>
const num = ref(0)
num.value++
console.log(num.value) // 初始为0,自增至1,此时客户端最终渲染为1
</script>
混合渲染
混合渲染,Nuxt 允许为每个路由分别定义不同的路由规则,使用不同的渲染模式。
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// 主页在构建时预渲染
'/': { prerender: true },
// 产品页面按需生成,后台自动重新验证
'/products/**': { swr: 3600 },
// 博客文章按需生成,直到下一次部署前持续有效
'/blog/**': { isr: true },
// 管理仪表板仅在客户端渲染
'/admin/**': { ssr: false },
// 在API路由上添加cors头
'/api/**': { cors: true },
// 跳转旧的URL
'/old-page': { redirect: '/new-page' },
},
})
客户端渲染的优缺点引用自官方文档
客户端渲染的优点:
- 开发速度:在完全在客户端进行工作时,我们不必担心代码的服务器兼容性,例如,使用仅在浏览器中可用的 API,如
window
对象。 - 成本较低:运行服务器会增加基础设施成本,因为需要在支持 JavaScript 的平台上运行。我们可以将仅客户端的应用程序托管在任何具有 HTML、CSS 和 JavaScript 文件的静态服务器上。
- 离线工作:因为代码完全在浏览器中运行,所以在网络不可用的情况下,它可以继续正常工作。
客户端渲染的缺点:
- 性能:用户必须等待浏览器下载、解析和运行 JavaScript 文件。根据下载部分的网络和解析和执行的用户设备的性能,这可能需要一些时间,并影响用户的体验。
- 搜索引擎优化:通过客户端渲染提供的内容进行索引和更新比使用服务器渲染的 HTML 文档需要更长的时间。这与我们讨论的性能缺点有关,因为搜索引擎爬虫不会等待界面在第一次尝试索引页面时完全渲染。纯客户端渲染将导致内容在搜索结果页面中显示和更新所需的时间更长。
服务端渲染的优缺点引用自官方文档
服务器端渲染的好处:
- 性能:用户可以立即访问页面的内容,提高了首屏的加载速度,因为浏览器可以比 JavaScript 生成的内容更快地显示静态内容。同时,当水合过程发生时,Nuxt 保留了 Web 应用程序的交互性。
- 搜索引擎优化:通用渲染将整个 HTML 内容作为经典服务器应用程序直接传递给浏览器。Web 爬虫可以直接索引页面的内容,这使得通用渲染成为任何希望快速索引的内容的绝佳选择。
服务器端渲染的缺点:
- 开发约束:服务器和浏览器环境提供的 API 不同,编写可以在两个环境中无缝运行的代码可能会有些棘手。幸运的是,Nuxt 提供了指导方针和特定的变量,帮助你确定代码在哪个环境中执行。
- 成本:为了即时渲染页面,服务器需要运行。这和任何传统服务器一样增加了每月的成本。然而,由于浏览器在客户端导航时接管了服务器调用,调用次数大大减少。通过利用边缘端渲染,可以实现成本的降低。
Nuxt Router
官方文档:https://nuxt.com.cn/docs/getting-started/routing
Nuxt 已对 vue-router 进行封装,无需创建路由即可使用。
Nuxt 文件系统路由为 pages/
目录中的每个文件创建一个路由。
index.vue 是特殊的,直接对应 / 根路由。
// 页面结构
-| pages/
---| users-[uid].vue
---| users-[uid]/
-----| [id].vue
// 生成路由
{
path: '/users-:uid',
component: '~/pages/users-[uid].vue',
name: 'users-[uid]',
children: [
{
path: ':id',
component: '~/pages/users-[uid]/[id].vue',
name: 'users-[uid]-id'
}
]
}
动态路由
与文件夹或文件的 [] 中的内容,都会生成为 Nuxt 的动态路由参数。
如:/user-[uid]/[name].vue
对应 /user-1/zhiyu
。
params: {
uid: 1,
name: 'zhiyu'
}
在 /user-[uid]/
文件夹下的 index.vue
,则对应该 /user-[uid]
根路由。
全匹配路由
用于匹配该路由下未定义
的其他路由,可用于做 404
或无权限页面
。
[...xxx].vue
嵌套路由
/pages
下新建user.vue
,再创建/pages/user
文件夹,此时/pages/user
下的.vue
即对应user.vue
的子路由。
注意:
如果/pages/user
文件夹下存在index.vue
,路由 name 会于根目录的user.vue
相同。
在/user 没有子路由<nuxtPage/>
时,则会展示/pages/user/index.vue
的内容,为了避免不必要的理解误差,还是不要再次定义index.vue
。
如要渲染嵌套路由,<NuxtPage/>
也需要放在/pages/user.vue
中。
嵌套路由中,每一层.vue
的log
都会被执行。
路由组nuxt@3.13.0+
在需要文件夹分组而不想让路由多一层嵌套时,可以使用路由组,将 vue 文件放入使用()
包裹命名的文件夹中。
-| pages/
---| index.vue
---| (marketing)/
-----| about.vue
-----| contact.vue
此时 Nuxt 会生成 /、/about 和 /contact 页面,而 marketing 组的嵌套路由将会被忽略。
definePageMeta
自定义路由信息 可用于定义覆盖自动生成的 path
、其他路由信息、中间件。
definePageMeta({
path: '/about',
})
navigateTo
更加强大的编程路由导航,可以在服务端重定向
跳转。
场景:想要让访问 /wel
重定向至 /user
。
使用 router.push
,客户端会看到跳转过程,
而 navigateTo
,则在服务端进行了 302重定向
跳转过程,
navigateTo
并不会阻塞组件后续代码在服务端的运行。
middleware
路由中间件,类似 vue 中的路由导航守卫。
官方文档:https://nuxt.com.cn/docs/guide/directory-structure/middleware
中间件的定义
在 /middleware
文件夹下定义中间件,x.ts
为普通中间件,x.global.ts
为全局中间件,
并于 definePageMeta
中的 middleware
配置中使用,该配置是一个数组
,可直接传递一个中间件函数。
默认暴露一个 defineNuxtRouteMiddlew
函数,函数参数接收一个回调(参数 to, from
)。
Nuxt
提供了两个全局可用的辅助函数
,可以直接从中间件中返回。
navigateTo - 重定向到给定的路由
abortNavigation - 中止导航,并可选择提供错误消息
中间件必须 return 返回,否则中间件无效。
定义多个中间件的情况下,前一个中间件不能直接 navigateTo
,这会导致下一个中间件无法执行
,第一个中间件可 return true
。
中间件的使用
/middleware/user.ts => middleware: ['user']
中间件执行顺序
1.全局中间件
(两个全局则按首字符字母或数字 ASCII码
顺序决定)01.global.ts -> 02.global.ts。
2.definePageMeta middleware
数组配置中的索引
顺序。
Nuxt Request
Nuxt 提供的请求函数。
官方文档:https://nuxt.com.cn/docs/getting-started/data-fetching
$fetch
Nuxt 提供的基于 ofetch
的全局函数,用于 Http 请求。
示例
<script setup>
// 直接写在最外层,在SSR中数据将被获取两次,一次在服务器端,一次在客户端
const dataTwice = await $fetch('/api/item')
// 在SSR中,使用useAsyncData,数据仅在服务器端获取并传递到客户端
const { data } = await useAsyncData('item', () => $fetch('/api/item'))
// 你也可以使用useFetch作为useAsyncData + $fetch的快捷方式
const { data } = await useFetch('/api/item')
</script>
参数
url
:请求地址options
:配置项,如method:'POST'
,具体看官方文档 ofetch
返回值
request
:响应体
useAsyncData
官方文档:https://nuxt.com.cn/docs/api/composables/use-async-data
Nuxt 内置的用于处理异步数据获取的组合式函数,是一个 异步函数
,不会阻塞后续函数,加上 await 以同步执行。
可用于 SSR
预渲染数据,在页面渲染时获取数据,并渲染于 HTML 文档中返回给客户端。
示例
<script setup>
const num = ref(1)
let { data, status } = await useAsyncData(
'getData', // key
() =>
$fetch(`${runtimeConfig.public.baseUrl}/getData`, {
method: 'POST',
}),
{
watch: [num], // 监听num重新执行useAsyncData
}
)
console.log(status.value) // useAsyncData加上await同步执行,那此处返回的status始终是success
</script>
参数
key
:提供的 key 在服务端与客户端执行时必须要唯一
;- 用于在渲染时告诉客户端已经在服务端执行了一次,再通过这个 key 去拿 useAsyncData 返回对应数据(而不需要重新执行),如果失去了唯一性,将会执行两次。
- 如果不提供 key,则会
自动生成
。
handler
:一个异步函数,确保函数返回一个 Promise,一般返回一个 $fetch。options
:提供多个配置,如watch
监听变量更新自动执行,具体看官方文档。lazy
:默认为 false,阻塞客户端导航(等待关联的响应数据返回后才渲染路由页面),为 true 则不阻塞。default
:一个工厂函数,用于设置返回值data
的默认值。
返回值
包装处理过的响应式数据,包含错误处理。
data
: 异步函数返回值。pending
:表示异步函数的结果是否仍在等待中状态。error
:错误对象。status
:请求状态。refresh
:用于刷新函数返回数据的函数。- 只在服务端执行一次。
useFetch
和useAsyncData
都支持请求缓存。- 如果上一次请求仍在缓存有效期内,
refresh 直接返回缓存数据
,而不是重新发起请求。
- 如果上一次请求仍在缓存有效期内,
- 默认情况下,Nuxt 会等待
refresh
完成后才能再次执行。
useFetch
官方文档:https://nuxt.com.cn/docs/api/composables/use-fetch
包装了 useAsyncData + $fetch
的语法糖,自动生成 key,不需要传入 handler 函数。
示例
<script setup>
let {
data: fetchData,
status: fetchStatus,
refresh,
} = await useLazyFetch(`${runtimeConfig.public.baseUrl}/getData`, {
method: 'POST',
})
</script>
参数
url
options
:继承 $fetch 及 useAsyncData 的 options,并扩展了钩子函数、body、headers、baseURL 等配置。lazy
:默认为 false,阻塞客户端导航(等待关联的响应数据返回后才渲染路由页面),为 true 则不阻塞。query/params
:请求参数。请求拦截器
:{ onRequest({ request, options }) { // 设置请求头 options.headers = options.headers || {} options.headers.authorization = '...' }, onRequestError({ request, options, error }) { // 处理请求错误 }, onResponse({ request, response, options }) { // 处理响应数据 localStorage.setItem('token', response._data.token) }, onResponseError({ request, response, options }) { // 处理响应错误 } }
返回值
包装处理过的响应式数据,包含错误处理。
data
: 异步函数返回值。pending
:表示异步函数的结果是否仍在等待中状态。error
:错误对象。status
:请求状态。refresh
:用于刷新函数返回数据的函数。- 只在服务端执行一次。
useFetch
和useAsyncData
都支持请求缓存。- 如果上一次请求仍在缓存有效期内,
refresh 直接返回缓存数据
,而不是重新发起请求。
- 如果上一次请求仍在缓存有效期内,
- 默认情况下,Nuxt 会等待
refresh
完成后才能再次执行。
useLazyAsyncData
useAsyncData 参数 options 传入 lazy:true
的包装版本,会立即触发导航
。
useLazyFetch
useLazyFetch 参数 options 传入 lazy:true
的包装版本,会立即触发导航
。
Composables
Nuxt 提供的组合式函数。
官方文档:https://nuxt.com.cn/docs/api/composables/use-app-config
useAppConfig
用于获取 app.config defineAppConfig
暴露的变量。
useNuxtApp
用于获取使用 defineNuxtPlugin
绑定到上下文并在 nuxt.config 注册 plugins 的插件
。
runWithContext
runWithContext
用于提供一个明确的 Nuxt 上下文。
通常,Nuxt 上下文是隐式传递的,不需要担心这个问题。
然而,当与复杂的 async/await
场景在中间件/插件中工作时,可能会遇到在异步调用之后上下文丢失的问题,导致无法调用如 navigateTo
之类的上下文函数。
const nuxtApp = useNuxtApp() // 在可以获取上下文的代码块useNuxtApp
// ...
// navigateTo() // 假设navigateTo方法在复杂的异步环境中,无法调用该Nuxt的上下文函数
// 调用runWithContext,此时该函数体内即可正常调用Nuxt的上下文函数
nuxtApp.runWithContext(() => {
navigateTo()
})
// ...
这个问题更详细的解释可以查看官方文档对这部分的详细说明。
useRuntimeConfig
用于获取在 nuxt.config runtimeConfig
中定义的运行时环境的全局变量,可用外层变量判断是否服务端。
外层变量只能在服务端获取
public
对象,客户端服务端都可以获取的变量
useRequestHeaders
用于获取访问页面、组件和插件中传入请求头信息。
可用于获取 cookie、authorization、三级域名
等。
示例
<script setup>
// 获取所有请求头信息
const headers = useRequestHeaders()
// 仅获取 cookie 请求头信息
const { cookie } = useRequestHeaders(['cookie'])
// 获取三级域名
const { host } = useRequestHeaders(['host'])
console.log('host', host.split('.')[0])
// 获取请求的authorization头部信息,用于SSR服务端的内部授权
const { data } = await useFetch('/api/confidential', {
headers: useRequestHeaders(['authorization']),
})
</script>
useState
服务端和客户端共享
的变量。
在水合案例中,提到了服务端与客户端变量会通过水合过程后达成一致,而该组合函数可以使服务端的状态共享至客户端。
该组合函数如果只在客户端定义,其表现与客户端状态管理库行为类似(如 vuex,pinia),可以在SPA中共享状态,但刷新页面会丢失。
示例
<template>
<p>{{ num }}</p>
<!-- HTML文档num为1 -->
<!-- 客户端num最终为2 -->
</template>
<script setup>
const num = useState('count', () => 0)
num.value++ // 客户端初始值继承至服务端
console.log(num.value) // 初始为0,自增至1,此时客户端最终渲染为2
// 获取该共享状态,useState('count').value
</script>
参数
key
: 一个唯一的键,确保数据获取在请求中被正确地去重。如果你不提供键,则会为useState
的实例生成一个在文件和行号上唯一的键。init
: 当未初始化时,提供状态的初始值的函数。这个函数也可以返回一个Ref
。
Nuxt Seo
Nuxt 的 Seo
处理,详细可参考官方文档,下列是部分 Seo 相关的组合函数和组件的简易示例。
config
nuxt.config.ts 中定义的属性,将应用至整个网站。
export default defineNuxtConfig({
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
}
}
})
useHead
该组合函数允许以响应式编辑 head 标签。
app.vue 的 useHead 定义将应用至整个网站,而每个页面的 useHead 则会覆盖
app.vue 的 useHead 定义。
示例
<script setup>
const title = ref('Nuxt Demo')
useHead({
title,
meta: [
{
name: 'description',
content: 'About description',
},
],
bodyAttrs: {
class: title,
},
titleTemplate(titleChunk) {
// return '%s - About' // %s占位字符将替换为全局标题
return titleChunk ? `${titleChunk} - About` : title
},
script: [
{
src: 'https://baidu.com',
// 有效选项为: 'head' | 'bodyClose' | 'bodyOpen'
tagPosition: 'bodyClose', // 追加至body末尾
},
],
})
</script>
参数
meta
:数组中的每个对象及属性对应单个 meta 标签及其属性。titleTemplate
:配置动态模板以自定义每个页面的标题。- 更多参数访问官方文档。
useHeadSafe
useHead 的包装版,只允许安全的输入值
,参数与 useHead 基本一致。
<script setup>
useHeadSafe({
script: [
{ id: 'xss-script', innerHTML: 'alert("xss")' }
],
meta: [
{ 'http-equiv': 'refresh', content: '0;javascript:alert(1)' }
]
})
// 将安全地生成
// <script id="xss-script"></script>
// <meta content="0;javascript:alert(1)">
</script>
useSeoMeta
该组合函数可通过对象参数形式定义 meta 标签。
每个属性 key: value
对应 meta 标签的 property 和 content,ogTitle
等驼峰属性会转换为 og:title
。
<script setup lang="ts">
useSeoMeta({
title: '我的神奇网站',
ogTitle: '我的神奇网站',
description: '这是我的神奇网站,让我给你介绍一切。',
ogDescription: '这是我的神奇网站,让我给你介绍一切。',
ogImage: 'https://example.com/image.png',
})
</script>
components
Nuxt 提供了 <Title>
、<Base>
、<NoScript>
、<Style>
、<Meta>
、<Link>
、<Body>
、<Html>
和 <Head>
组件。
你可以在组件的模板中直接与元数据进行交互。
适合定义于 <Layout>
组件中,实现统一配置。
<template>
<div>
<Head>
<Title>{{ title }}</Title>
<Meta name="keywords" :content="title" />
<Style type="text/css" children="body { background-color: green; }">
/* slot内容覆盖children属性 */
p { color: red; }
</Style>
</Head>
</div>
</template>
<script setup>
const title = ref('Hello Nuxt')
</script>
Nuxt Components
Nuxt 提供的自带组件。
官方文档:https://nuxt.com.cn/docs/api/components/
NuxtPage
<routerView/>
的封装,接受相同的name
和route
属性。
支持命名路由、可选路由。
NuxtLink
<routerLink/>
的封装。
切换路由时,这一部分是客户端渲染,并未刷新页面,如单页面应用逻辑。
也可通过 import.meta.server
判断是否为服务端。
NuxtLayout
布局组件,自动创建对应 /layouts
下的布局,文件名对应 name
属性。
可用于展示通用头部。
官方文档:https://nuxt.com.cn/docs/api/components/nuxt-layout
ClientOnly
渲染组件,该组件插槽内容只在客户端渲染。
Props
placeholderTag
|fallbackTag
: 指定在服务器端渲染的标签。placeholder
|fallback
: 指定在服务器端渲染的内容。
Slots
#fallback
: 指定在服务器端显示的内容。
<template>
<div>
<ClientOnly>
<!-- ... -->
<template #fallback>
<!-- 这将在服务器端渲染 -->
<p>加载评论中...</p>
</template>
</ClientOnly>
</div>
</template>
Nuxt Error
全屏错误捕获页面
在 Nuxt 服务端遇到致命错误(语法报错,变量属性不存在等)时,Nuxt 会传入 error props 重定向至该页面。
尽管该页面被称为错误页面,但它不是一个路由,不应该放在 ~/pages
目录中,而是与 app.vue
同级,会替代 Nuxt 默认的全屏错误页面。
---app.vue
---error.vue // 全局错误捕获页面
---nuxt.config.ts
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<hr />
<h3>{{ error.message }}</h3>
<h3>{{ error.url }}</h3>
<h3>{{ error.data }}</h3>
</div>
</template>
<script setup>
// 全屏错误捕获,获取nuxt传入页面的props-error
defineProps({
error: {
type: Object,
},
})
</script>
相关函数
createError
:用于创建错误对象并抛出,触发跳转至全屏错误捕获页面,可以使用clearError
清除该错误。 在服务端触发一个全屏错误页面,在客户端会触发一个浏览器报错,如果在客户端需要一个全屏错误,传入fatal
参数。throw createError({ statusCode: 404, statusMessage: 'Page Not Found', data: '❌❌❌❌❌',// 自定义数据 // fatal: true // 用于在客户端触发错误并跳转至全屏错误捕获页面 })
showError
:客户端服务端都能用的触发全屏报错函数,建议使用throw createError
。clearError
:将清除当前正在处理的 Nuxt 错误,接受一个可选的路径进行重定向(例如重新导航到根路径)。
Nuxt安装报错问题
Error: Failed to download template from registry: Failed to download
https://raw.githubusercontent.com/nuxt/starter/templates/templates/v3.json: TypeError: fetch failed
从 Nuxt 仓库 issues 找到的解决方法。
设置Host
185.199.108.133 raw.githubusercontent.com
185.199.109.133 raw.githubusercontent.com
185.199.110.133 raw.githubusercontent.com
185.199.111.133 raw.githubusercontent.com
服务端log无法转换为字符串
WARN [nuxt] Failed to stringify dev server logs. Received DevalueError: Cannot stringify a function.
You can define your own reducer/reviver for rich types following the instructions in
https://nuxt.com/docs/api/composables/use-nuxt-app#payload
对于 json 格式
的服务端输出,应使用 JSON.stringify
返回为字符串。