一文彻底解决你对 Nuxt3 中 http 请求的困惑
背景
最近在做自己的项目时,挺手贱的,把一个半成品项目从 vue3 的单页应用用 nuxt3 重写了,虽然项目第一版还没发布就进行重构听起来不像话,但还是想要更好地呈现它最终的状态,如果真写完了在迁移,就更崩溃了。
本着轻松迁移的心态,结果没写多少发现自己好像没写过前端一样,怎么在 nuxt 中想做点什么都跟平时写 CSR 的项目不一样!比如上来兴高采烈,发现不会写路由守卫;然后迁移个登录吧,在什么都没了解的情况下,直接把 CSR 中的 axios 封装复制了过来,虽然写着没发现问题,但在文档中没有发现纯客户端 api 相关的部分,全都是服务端请求,网上一搜,结果大家都是问题,或者都是迎合官方文档说 axios 被淘汰了(一脸问号,这怎么可能),甚至怀疑 axios 还能不能在 nuxt 中用,不会有啥问题吧,搞得我也不自信了......
然后冷静思考了许久,我悟了,是我没转换过来,所以会被网上的说法扰乱心智,Nuxt 作为 SSR 方案,其实是一个全栈框架,虽然它仍然能写纯客户端代码,但是如果只有纯客户端,干嘛不用单页应用而是选择 nuxt 呢?所以,是我的问题,我看网上内容的时候把自己绕进去了。
好好好,没人给我讲清楚是吧,那我就自己给自己讲。
首先认清 CSR 与 SSR
大家在使用 Nuxt 之前需要先思考一下,你的项目有必要用 SSR 吗?如果真的需要,那是哪个页面,哪个功能需要呢?如果真没有的话,出门左/右转,去使用 Vue 创建普通的 Vue 项目吧。
以我自己的项目为例,我的项目定位是个 ToC 的应用,项目中涉及到操作的大多数都是客户端功能,因为 SSR 的意义是让爬虫爬到自己的内容,任何操作的部分,都应该是客户端功能。然后项目中纯展示部分,比如一些文章,用户评论,需要在网络上能被搜到的,这些需要做 SSR,其他的可以都 CSR。就像掘金,它的文章可以做 SSR,这样用户通过搜索引擎可以找到对应的内容,但它的创作者中心,完全不需要在搜索引擎被搜出来,那就是纯粹的 CSR 功能。我的项目有点像掘金,有一部分内容想要被搜索引擎搜到,但大部分还是 CSR 功能要靠用户操作。
到这里,认识到 SSR 跟 CSR 的定位后,就可以说说在 Nuxt 中的请求相关的问题了。
CSR 纯客户端功能请求
对于 CSR 纯客户端功能的请求,那就随便发挥就好了,之前的 axios 该怎么用就怎么用,像上面截图中 v2ex 社区老哥提到的,是不是需要把后端所有的接口在 server/api 再写一遍呀?当然不需要了,Nuxt 官方不说,它是想让你用 Nuxt 做全栈应用,服务端请求是可以直接请求数据库的,对于一些轻量级的应用,前端自己就把接口实现了,但是你的项目如果功能复杂,那还是该后端写接口就让后端去写,你仍然是前端,所以除了 SSR 相关的功能,其他部分,如果你用 axios 用的炉火纯青了,那就别折腾重新封装什么 fetch 了,听话,直接 axios 就行了。
怎么二次封装 axios,就不需要我多说了,老司机们肯定都再熟悉不过了。
SSR 服务端请求
好,到了 SSR 的部分,这里才是需要去学习 Nuxt 文档的地方。一定要考虑清楚,你当前实现的功能是否需要 SSR,不需要的话直接上面的 axios 拿来用。
Nuxt 中的数据请求可以区分为 内部请求 与外部请求
内部请求是什么
内部请求就是你在 nuxt 的 /server/api 中定义过的请求,/server/api 这里的代码相当于后端,你可以在这里面写一些访问数据库的代码,如果是一些轻量的全栈应用,直接用 nuxt 的服务端 api 就可以搞定,不需要再单独写个后端项目了。
当然,除了直接访问数据库,也可以做接口的转发,例如你拥有一个完整后端项目,前端只需要在这里对接口进行整合,将最终前端需要展示的数据返回出去,但是要注意,不是所有的请求都应该经过这一层,例如一些 CSR 的功能,例如用户注册登录,添加评论,上传文件等,如果你拥有一个后端服务了,可以直接用 axios 请求,像平时一样用就行了,经过这里的转发你的请求需要经过两个后端服务,肯定会变慢,所以一定要考虑当前这个功能,是否必须 SSR。但如果是如果你真的需要用 Nuxt 做 BFF,就不多说啥了。
外部请求是什么
外部请求就是不用经过 /server/api的部分,就算是你们的后端项目提供的接口,如果你要直接访问,不通过 server/api 进行转发,也属于外部请求,并不是只有访问第三方接口才算外部请求。
SSR 请求
SSR 请求就是上面提到的内部请求,除了上面的内部请求,其他的地方你可以完全使用 axios,不需要多考虑别的问题。
Nuxt3 提供的请求方案
请求方案的基础 $fetch
Nuxt 使用了 ofetch 在全局暴露了 $fetch 辅助函数,这是一句很官方的话,换句话来说,你要先了解 ofetch 是什么,它是一个对更好用的 fetch api,好用的点在于,使用它我们不需要关注所在的环境,它可以在 node、浏览器以及 worker 线程中直接使用,因此 Nuxt 将它挂在到了全局,起了个别名为 $fetch,然后内部实现了几个请求数据的钩子函数(composables)都是依赖这个 $fetch 的,$fetch 是 Nuxt 请求方案的基础。
nuxt 中的几种请求方案
- 直接在组件中使用
$fetch函数
<script setup lang="ts">
const { data } = await $fetch('/api/hello')
</script>
这样是不推荐的,因为直接在 setup 中调用,会导致 $fetch 函数执行两次,一次是在这个服务端渲染生成 html 的时候,一次是在客户端。原因是 setup 其实就是个函数,在服务端渲染的时候执行一遍,在客户端渲染的时候执行一遍。除非你自己手动判断一遍当前是否是处于服务端,或者在 onMounted 钩子中调用,因为 onMounted 只会在客户端调用,服务端就只负责生成最初始的模板, 不会深度调用等待组件生命周期。
- 使用
useAsyncData+$fetch配合
<script setup lang="ts">
const { data } = await useAsyncData('hello', () => $fetch('/api/hello'))
</script>
<script setup lang="ts">
const { data, pending, error, refresh } = await useAsyncData(
'mountains',
() => $fetch('https://api.nuxtjs.dev/mountains')
)
</script>
使用 useAsyncData 获取数据,它只会在服务端执行,执行之后,会将 data 装配为响应式数据,在客户端激活后,不会再次调用 api,它比较适合在 SSR 的场景中使用。
- 使用
useFetch钩子
<script setup lang="ts">
const { data } = await useFetch('/api/hello')
</script>
useFetch 对上面 useAsyncData + $fetch 的进一步封装,只是使用起来更加简单方便了,他们效果一致,也是适合在 SSR 场景使用。
useLazyAsyncData+$fetch配合
<script setup lang="ts">
const { data } = await useLazyAsyncData('hello', () => $fetch('/api/hello'));
</script>
useLazyAsyncData 只是对 useAsyncData 加了个 lazy 参数,与 useAsyncData 的区别是, useAsyncData 会阻塞路由导航,需要等待请求执行完之后,才会继续进行导航;但是加了 lazy 就不会了。可以看看官方的 demo 说明
<script setup lang="ts">
/* 在获取完成之前,导航将会发生。
在组件的模板中直接处理挂起和错误状态
*/
const { pending, data: count } = await useLazyAsyncData('count', () => $fetch('/api/count'))
watch(count, (newCount) => {
// 因为 count 可能最初为 null,你不会立即访问到它的内容,但你可以监视它。
})
</script>
<template>
<div>
{{ pending ? '加载中' : count }}
</div>
</template>
useLazyFetch钩子
这个钩子也是对上述方式的一个封装,使用起来风方便,但效果一致。
<script setup lang="ts">
const { data } = await useLazyFetch('/api/hello');
</script>
总结
可以看出,如果不说 $fetch,则 nuxt 本质上只提供了一种请求方案,即 SSR请求。但提供了两组方案,一种会阻塞路由,一种不会,两组方案均有两种写法。
使用 $fetch 做纯客户端请求
上面说了 $fetch 是 Nuxt 使用 ofetch 在全局挂在的一个函数,它可以在 node 浏览器 worker 线程 中直接调用,因此可以在一些 handler 处理函数中使用 $fetch 进行 api 请求的封装,并且 ofetch 也是支持拦截器的,对于 token refreshToken 的刷新等功能都能很简单的实现,不过还是要做二次业务封装。
<script setup lang="ts">
const login = () => {
const result = await $fetch('/auth/login', {
method: 'POST',
body: {
username: 'username',
password: 'password'
}
});
}
</script>
<template>
<div>
<button @click="login">
登录
</button>
</div>
</template>
像这样绑定在 dom 上的处理函数,只会在客户端执行。如果需要进入页面就请求数据,但不需要在服务端运行,可以判断一下环境,或者最简单的方案就是在 onMounted 中调用。
使用 axios 做纯客户端请求
最开始不是说了嘛,axios 可以做纯客户端请求,不过它的使用也要考虑在服务端代码,比如直接写在 setup 的 script 中,服务端也会调用一次,不是很合理,所以一般用的场景与上面 【使用 $fetch 做纯客户端请求】 中的注意项一样,一般就用在一些 handler 处理函数以及 onMonted 中。
所以,其实到这里我们也能明白目前网络上那些人的文章,都没有推荐 axios,可能也是有这层心智负担的原因,但你只要认清了我们最开始说的 SCR 与 SSR,那你用 axios 也是没有任何问题的,特别是在我们很熟悉 axios 的时候,复用它的业务封装可以减少不少我们的学习成本以及时间精力。
特别是很多页面,我们可以直接在路由配置中将其设置为 ssr: false,这样更没有心智负担了。所以该用啥就用啥吧,axios 并没有被淘汰,别被别人影响了。
文章源于个人项目【极客角色】开发过程中的记录,点个关注持续观察这个叼毛(小可爱)的记录状态吧。