nuxt 框架请求迷思:nuxt的反直觉的请求到底解决了什么问题?

723 阅读4分钟

1. 前言

最近公司又上马了一个新项目,由于是全新的项目,所以也准备使用全新的支持 SSR 的框架 nuxt 来开发。

image.png

学习一个新框架还是比较耗时间的,毕竟要了解别人的想法、规范、API等一系列的东西。

不过在看了一段时间文档后,最令我感到迷惑的点就是 nuxt 的请求了。

2. nuxt 的特殊请求

nuxt 请求特殊在哪里呢?在 nuxt 官网上 nuxt 给出了三种请求方式:

  1. $fetch
  2. useAsyncData
  3. useFetch

$fetch 类似我们日常开发中使用的 axios

const users = await $fetch('/api/users').catch((error) => error.data)

image.png

最主要的是相比于 axios 更好的兼容不同环境下的请求处理。

令我疑惑不解的是 useFetchuseAsyncData 这两个可以完成数据水合功能的请求函数


<script setup>
const { data, pending, error, refresh } = await useAsyncData(
  'mountains',
  () => $fetch('https://api.nuxtjs.dev/mountains')
)
</script>

可以看到官方例子中,它不像 $fetch 直接返回数据给我们,而是附赠了一堆大礼包

  1. data ref 数据对象
  2. pending 一个布尔值,指示数据是否仍在获取中
  3. error 如果数据获取失败,则为一个错误对象
  4. 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>

这样不就能实现直接获取数据的想法了吗?

重新刷新下页面,发现页面正常加载了。

image.png

  1. 所有的请求都不能直接在 steup 中 await 进行调用
  2. 请求之后的数据处理需要是同步的,不能异步处理数据
  3. 一次只能发送一个请求,不能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()

image.png

可以看到这里实际是服务端返回渲染的是 测试一下 而不是 关于页面。这是因为 doOther 函数的执行周期已经不在 onServerPreFetch 中了,所以当 doOther 执行时其实页面已经返回了。

nuxt 官方他是怎么做的呢?

image.png

nuxt 选择直接传入 transform 函数来解决这个问题。这样就保证了 transform 函数会在 onServerPreFetch 中执行。

5. 结语

到这里 nuxt 请求已经拆解完毕了,nuxt 主要还是为了将 onServerPreFetch 这个生命周期给隐藏起来,让开发者可以用一种更一致的方式来写代码。

当然这种方式到底是更易用还是更加复杂,我不好评价

每一个看起来很难受的点,后面可能都有开发者不得不这么做的理由,只不过都藏在冰山之下 (当然有可能就是菜),等待着我们去挖掘出来

另外再推一本 藤本树 的 《再见绘梨》吧 (其实是懒得找封面)

1730358985072.jpg