1. 前言
最近公司又上马了一个新项目,由于是全新的项目,所以也准备使用全新的支持 SSR 的框架 nuxt 来开发。
学习一个新框架还是比较耗时间的,毕竟要了解别人的想法、规范、API等一系列的东西。
不过在看了一段时间文档后,最令我感到迷惑的点就是 nuxt 的请求了。
2. nuxt 的特殊请求
nuxt 请求特殊在哪里呢?在 nuxt 官网上 nuxt 给出了三种请求方式:
- $fetch
- useAsyncData
- useFetch
$fetch 类似我们日常开发中使用的 axios
const users = await $fetch('/api/users').catch((error) => error.data)
最主要的是相比于 axios 更好的兼容不同环境下的请求处理。
令我疑惑不解的是 useFetch 和 useAsyncData 这两个可以完成数据水合功能的请求函数
<script setup>
const { data, pending, error, refresh } = await useAsyncData(
'mountains',
() => $fetch('https://api.nuxtjs.dev/mountains')
)
</script>
可以看到官方例子中,它不像 $fetch 直接返回数据给我们,而是附赠了一堆大礼包
- data ref 数据对象
- pending 一个布尔值,指示数据是否仍在获取中
- error 如果数据获取失败,则为一个错误对象
- refresh 一个重新发起请求的函数
这就很令人疑惑了, nuxt 为啥不和上面 $fetch 一样调用之后直接返回数据就好了。还需要绕一个弯返回这么多东西让我处理。
这不是化简为繁吗?将好好的同步请求,相当于重新转换为了异步(只不过是数据驱动方式的异步)。
对于写习惯了直接获取数据的我来说,这种请求方式还是稍稍令我有点难受,那么 nuxt 这么做到底是为啥呢?总不可能是为了冲业绩增加点代码量吧?
3. 浅析 nuxt 特殊请求
想要知道为啥 nuxt 这样做,那么首先得知道 useAsyncData 到底对 $fetch 做了怎么样得封装。
这里我简化和截取 useAsyncData 的一部分代码做一个简单的分析
export function useAsyncData(...args) {
// key 就是 url
// _handler 就是传入的 $fetch 方法
// options 是配置
let [key, _handler, options = {}] = args;
// 对请求进行包装
const promise = new Promise(
(resolve, reject) => {
try {
resolve(_handler(nuxtApp));
} catch (err) {
reject(err);
}
}
).then(async (_result) => {
asyncData.data.value = result;
asyncData.error.value = asyncDataDefaults.errorValue;
asyncData.status.value = "success";
}).catch((error) => {
asyncData.error.value = createError(error);
asyncData.data.value = unref(options.default());
asyncData.status.value = "error";
})
// 如果是服务端,则把请求放到 onServerPrefetch 中执行
if (import.meta.server && fetchOnServer && options.immediate) {
if (getCurrentInstance()) {
onServerPrefetch(() => {promise()});
}
} else {
promise()
}
}
其中最核心的点就在于在服务端,需要把请求放到 onServerPrefetch 中去请求。那也就造成了如果想统一服务端和客户端的话,useAsyncData 就不能直接返回数据了,而是要等待 onServerPreFetch 生命周期执行完毕之后,才能拿到数据。
那可能有人就要问了,为啥不能把 onServerPrefetch 变成 Promise 返回不就行了?
这样做能不能行呢? 还真行 (不过这样会有别的问题,猜一猜是啥问题)
4. nuxt 直接请求改造问题
既然可以直接返回请求数据,我还搞这么麻烦干什么,直接开始改造一波
async function doPlatformFetch (url, options) {
if (process.client) {
return await $fetch(url, options)
}
// 这里如果是服务端 则 返回一个 Promise
return new Promise((resolve, reject) => {
onServerPrefetch(async () => {
try {
let data= await $fetch(url, options)
resolve(data)
} catch (err) {
reject(err)
}
})
})
}
export default async function doFetch (url, options) {
const nuxtApp = useNuxtApp();
let key = url
if (process.client) {
// 如果缓存有值
if (nuxtApp.payload.data[key] != null && nuxtApp.payload.data[key].isConsumed == false) {
nuxtApp.payload.data[key].isConsumed = true
return nuxtApp.payload.data[key].data
}
}
let data = await doPlatformFetch(url, options)
if (!process.client) {
nuxtApp.payload.data[key] = {
data: data,
isConsumed: false
}
}
return data
}
这样就能愉快的使用 doFetch 在客户端和服务端进行请求了。
<script setup lang="ts">
await doFetch('/topic/allTopic', {
method: "POST",
query: packagePageParams(params)
})
</script>
测试一下,发现页面怎么白屏了,一直在加载中的状态
仔细分析一下生命周期我们就能发现, vue 需要走完 steup 生命周期才能调用之后 onMounted onServerPreFetch 等等一系列的生命周期。
那我们上面的代码不就死锁了嘛? steup 一直在等待 onServerPreFetch 的执行,而 onServerPreFetch 又需要 steup 执行完毕之后。
欸,那我只需要将 doFetch 异步,不就没这个问题了吗?
<script setup lang="ts">
async aFetch () {
await doFetch('/topic/allTopic', {
method: "POST",
query: packagePageParams(params)
})
}
// 直接将请求异步
aFetch();
</script>
这样不就能实现直接获取数据的想法了吗?
重新刷新下页面,发现页面正常加载了。
- 所有的请求都不能直接在 steup 中 await 进行调用
- 请求之后的数据处理需要是同步的,不能异步处理数据
- 一次只能发送一个请求,不能A请求完毕后再发起B请求
这里有个小栗子来说明这个问题
let other = ref('测试一下')
async function test () {
const data = await doFetch()
console.log(data)
await doOther()
}
async function doOther () {
// 延迟更新
await new Promise(() => {
setTimeout(() => {
other.value = '关于页面'
}, 300)
})
}
test()
可以看到这里实际是服务端返回渲染的是 测试一下 而不是 关于页面。这是因为 doOther 函数的执行周期已经不在 onServerPreFetch 中了,所以当 doOther 执行时其实页面已经返回了。
那 nuxt 官方他是怎么做的呢?
nuxt 选择直接传入 transform 函数来解决这个问题。这样就保证了 transform 函数会在 onServerPreFetch 中执行。
5. 结语
到这里 nuxt 请求已经拆解完毕了,nuxt 主要还是为了将 onServerPreFetch 这个生命周期给隐藏起来,让开发者可以用一种更一致的方式来写代码。
当然这种方式到底是更易用还是更加复杂,我不好评价
每一个看起来很难受的点,后面可能都有开发者不得不这么做的理由,只不过都藏在冰山之下 (当然有可能就是菜),等待着我们去挖掘出来
另外再推一本 藤本树 的 《再见绘梨》吧 (其实是懒得找封面)