Vue 3 的组合 API 如何请求数据?

网友投稿 212 2023-05-11

[[347283]]

前言 之前在学习 React Hooks 的过程中,看到一篇外网文章,通过 Hooks 来请求数据,并将这段逻辑抽象成一个新的 Hooks 给其他组件复用,我也在我的博客里翻译了一下:《在 React Hooks 中如何请求数据?》,感兴趣可以看看。虽然是去年的文章,在阅读之后一下子就掌握了 Hooks 的使用方式,而且数据请求是在业务代码中很常用的逻辑。

Vue 3 已经发布一段时间了,其组合 API 多少有点 React Hooks 的影子在里面,今天我也打算通过这种方式来学习下组合 API。

项目初始化 为了快速启动一个 Vue 3 项目,我们直接使用当下最热门的工具 Vite 来初始化项目。整个过程一气呵成,行云流水。

复制npm init vite-app vue3-app  1.
复制# 打开生成的项目文件夹  cd vue3-app  # 安装依赖  npm install  # 启动项目  npm run dev  1.2.3.4.5.6.

我们打开 App.vue 将生成的代码先删掉。

组合 API 的入口 接下来我们将通过 Hacker News API 来获取一些热门文章,Hacker News API返回的数据结构如下:

复制   "hits": [      {        "objectID""24518295"       "title""Vue.js 3"       "url""https://github.com/vuejs/vue-next/releases/tag/v3.0.0"     },      {...},      {...},    ]  1.2.3.4.5.6.7.8.9.10.11.

我们通过 ui > li 将新闻列表展示到界面上,新闻数据从 hits 遍历中获取

复制<template>    <ul>      <li        v-for="item of hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  <script>  import { reactive } fromvue export default {    setup() {      const state = reactive({        hits: []      })      return state    }  </script>  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.

在讲解数据请求前,我看先看看 setup() 方法,组合 API 需要通过 setup() 方法来启动,setup() 返回的数据可以在模板内使用,可以简单理解为 Vue 2 里面 data() 方法返回的数据,不同的是,返回的数据需要先经过 reactive() 方法进行包裹,将数据变成响应式。

组合 API 中请求数据 在 Vue 2 中,我们请求数据时,通常需要将发起请求的代码放到某个生命周期中(created 或 mounted)。在 setup() 方法内,我们可以使用 Vue 3 提供的生命周期钩子将请求放到特定生命周期内,关于生命周期钩子方法与之前生命周期的对比如下:

生命周期

可以看到,基本上就是在之前的方法名前加上了一个 on,且并没有提供 onCreated 的钩子,因为在 setup() 内执行就相当于在 created 阶段执行。下面我们在 mounted 阶段来请求数据:

复制import { reactive, onMounted } fromvue export default {    setup() {      const state = reactive({        hits: []      })      onMounted(async () => {        const data = await fetch         https://hn.algolia.com/api/v1/search?query=vue       ).then(rsp => rsp.json())        state.hits = data.hits      })      return state    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.

最后效果如下:

监听数据变动 Hacker News 的查询接口有一个 query 参数,前面的案例中,我们将这个参数固定了,现在我们通过响应式的数据来定义这个变量。

复制<template>    <input type="text" v-model="query" />    <ul>      <li        v-for="item of hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  <script>  import { reactive, onMounted } fromvue export default {    setup() {      const state = reactive({        query: vue       hits: []      })      onMounted((async () => {        const data = await fetch         `https://hn.algolia.com/api/v1/search?query=${state.query}`        ).then(rsp => rsp.json())        state.hits = data.hits      })      return state    }  </script>  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.

现在我们在输入框修改,就能触发 state.query 同步更新,但是并不会触发 fetch 重新调用,所以我们需要通过 watchEffect() 来监听响应数据的变化。

复制import { reactive, onMounted, watchEffect } fromvue export default {    setup() {      const state = reactive({        query: vue       hits: []      })      const fetchData = async (query) => {        const data = await fetch         `https://hn.algolia.com/api/v1/search?query=${query}`        ).then(rsp => rsp.json())        state.hits = data.hits      }      onMounted(() => {        fetchData(state.query)        watchEffect(() => {          fetchData(state.query)        })      })      return state    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.

由于 watchEffect() 首次调用的时候,其回调就会执行一次,造成初始化时会请求两次接口,所以我们需要把 onMounted 中的 fetchData 删掉。

复制onMounted(() => {  - fetchData(state.query)    watchEffect(() => {      fetchData(state.query)    })  })  1.2.3.4.5.6.

watchEffect() 会监听传入函数内所有的响应式数据,一旦其中的某个数据发生变化,函数就会重新执行。如果要取消监听,可以调用 watchEffect() 的返回值,它的返回值为一个函数。下面举个例子:

复制const stop = watchEffect(() => {    if (state.query === vue3) {      // 当 query 为 vue3 时,停止监听      stop()    }    fetchData(state.query)  })  1.2.3.4.5.6.7.

当我们在输入框输入 "vue3" 后,就不会再发起请求了。

返回事件方法 现在有个问题就是 input 内的值每次修改都会触发一次请求,我们可以增加一个按钮,点击按钮后再触发 state.query 的更新。

复制<template>    <input type="text" v-model="input" />    <button @click="setQuery">搜索</button>    <ul>      <li        v-for="item of hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  <script>  import { reactive, onMounted, watchEffect } fromvue export default {    setup() {      const state = reactive({        input: vue       query: vue       hits: []      })      const fetchData = async (query) => {        const data = await fetch         `https://hn.algolia.com/api/v1/search?query=${query}`        ).then(rsp => rsp.json())        state.hits = data.hits      }      onMounted(() => {        watchEffect(() => {          fetchData(state.query)        })      })      const setQuery = () => {        state.query = state.input      }      return { setQuery, state }    }  </script>  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.

可以注意到 button 绑定的 click 事件的方法,也是通过 setup() 方法返回的,我们可以将 setup() 方法返回值理解为 Vue2 中 data() 方法和 methods 对象的合并。

原先的返回值 state 变成了现在返回值的一个属性,所以我们在模板层取数据的时候,需要进行一些修改,在前面加上 state.。

复制<template>    <input type="text" v-model="state.input" />    <button @click="setQuery">搜索</button>    <ul>      <li        v-for="item of state.hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  1.2.3.4.5.6.7.8.9.10.11.12.

返回数据修改 作为强迫症患者,在模板层通过 state.xxx 的方式获取数据实在是难受,那我们是不是可以通过对象解构的方式将 state 的数据返回呢?

复制<template>    <input type="text" v-model="input" />    <button class="search-btn" @click="setQuery">搜索</button>    <ul class="results"     <li        v-for="item of hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  <script>  import { reactive, onMounted, watchEffect } fromvue export default {    setup(props, ctx) {      const state = reactive({        input: vue       query: vue       hits: []      })      // 省略部分代码...      return {        ...state,        setQuery,      }    }  </script>  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.

答案是『不可以』。修改代码后,可以看到页面虽然发起了请求,但是页面并没有展示数据。

state 在解构后,数据就变成了静态数据,不能再被跟踪,返回值类似于:

复制export default {    setup(props, ctx) {      // 省略部分代码...      return {        input: vue       query: vue       hits: [],        setQuery,      }    }  1.2.3.4.5.6.7.8.9.10.11.

为了跟踪基础类型的数据(即非对象数据),Vue3 也提出了解决方案:ref() 。

复制import { ref } fromvue const count = ref(0)  console.log(count.value) // 0  count.value++  console.log(count.value) // 1  1.2.3.4.5.6.7.

上面为 Vue 3 的官方案例,ref() 方法返回的是一个对象,无论是修改还是获取,都需要取返回对象的 value 属性。

我们将 state 从响应对象改为一个普通对象,然后所有属性都使用 ref 包裹,这样修改后,后续的解构才做才能生效。这样的弊端就是,state 的每个属性在修改时,都必须取其 value 属性。但是在模板中不需要追加 .value,Vue 3 内部有对其进行处理。

复制import { ref, onMounted, watchEffect } fromvue export default {    setup() {      const state = {        input: ref(vue),        query: ref(vue),        hits: ref([])      }      const fetchData = async (query) => {        const data = await fetch         `https://hn.algolia.com/api/v1/search?query=${query}`        ).then(rsp => rsp.json())        state.hits.value = data.hits      }      onMounted(() => {        watchEffect(() => {          fetchData(state.query.value)        })      })      const setQuery = () => {        state.query.value = state.input.value      }      return {        ...state,        setQuery,      }    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

有没有办法保持 state 为响应对象,同时又支持其对象解构的呢?当然是有的,Vue 3 也提供了解决方案:toRefs() 。toRefs() 方法可以将一个响应对象变为普通对象,并且给每个属性加上 ref()。

复制import { toRefs, reactive, onMounted, watchEffect } fromvue export default {    setup() {      const state = reactive({        input: vue       query: vue       hits: []      })      const fetchData = async (query) => {        const data = await fetch         `https://hn.algolia.com/api/v1/search?query=${query}`        ).then(rsp => rsp.json())        state.hits = data.hits      }      onMounted(() => {        watchEffect(() => {          fetchData(state.query)        })      })      const setQuery = () => {        state.query = state.input      }      return {        ...toRefs(state),        setQuery,      }    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.

Loading 与 Error 状态 通常,我们发起请求的时候,需要为请求添加 Loading 和 Error 状态,我们只需要在 state 中添加两个变量来控制这两种状态即可。

复制export default {    setup() {      const state = reactive({        input: vue       query: vue       hits: [],        error: false       loading: false     })      const fetchData = async (query) => {        state.error = false       state.loading = true       try {          const data = await fetch           `https://hn.algolia.com/api/v1/search?query=${query}`          ).then(rsp => rsp.json())          state.hits = data.hits        } catch {          state.error = true       }        state.loading = false     }      onMounted(() => {        watchEffect(() => {          fetchData(state.query)        })      })      const setQuery = () => {        state.query = state.input      }      return {        ...toRefs(state),        setQuery,      }    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.

同时在模板使用这两个变量:

复制<template>    <input type="text" v-model="input" />    <button @click="setQuery">搜索</button>    <div v-if="loading">Loading ...</div>    <div v-else-if="error">Something went wrong ...</div>    <ul v-else     <li        v-for="item of hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  1.2.3.4.5.6.7.8.9.10.11.12.13.14.

展示 Loading、Error 状态:

将数据请求逻辑抽象 用过 umi 的同学肯定知道 umi 提供了一个叫做 useRequest 的 Hooks,用于请求数据非常的方便,那么我们通过 Vue 的组合 API 也可以抽象出一个类似于 useRequest 的公共方法。

接下来我们新建一个文件 useRequest.js :

复制import {    toRefs,    reactive,  fromvue export default (options) => {    const { url } = options    const state = reactive({      data: {},      error: false     loading: false   })    const run = async () => {      state.error = false     state.loading = true     try {        const result = await fetch(url).then(res => res.json())        state.data = result      } catch(e) {        state.error = true     }      state.loading = false   }    return {      run,      ...toRefs(state)    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.

然后在 App.vue 中引入:

复制<template>    <input type="text" v-model="query" />    <button @click="search">搜索</button>    <div v-if="loading">Loading ...</div>    <div v-else-if="error">Something went wrong ...</div>    <ul v-else     <li        v-for="item of data.hits"       :key="item.objectID"     >        <a :href="item.url">{{item.title}}</a>      </li>    </ul>  </template>  <script>  import { ref, onMounted } fromvue import useRequest from./useRequest export default {    setup() {      const query = ref(vue     const { data, loading, error, run } = useRequest({        url: https://hn.algolia.com/api/v1/search     })      onMounted(() => {        run()      })      return {        data,        query,        error,        loading,        search: run,      }    }  </script>  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.

当前的 useRequest 还有两个缺陷:

传入的 url 是固定的,query 修改后,不能及时的反应到 url 上;

不能自动请求,需要手动调用一下 run 方法;
复制import {    isRef,    toRefs,    reactive,    onMounted,  fromvue export default (options) => {    const { url, manual = false, params = {} } = options    const state = reactive({      data: {},      error: false     loading: false   })    const run = async () => {      // 拼接查询参数      let query =      Object.keys(params).forEach(key => {        const val = params[key       // 如果去 ref 对象,需要取 .value 属性        const value = isRef(val) ? val.value : val        query += `${key}=${value}&`      })      state.error = false     state.loading = true     try {        const result = await fetch(`${url}?${query}`)         .then(res => res.json())        state.data = result      } catch(e) {        state.error = true     }      state.loading = false   }    onMounted(() => {      // 第一次是否需要手动调用      !manual && run()    })    return {      run,      ...toRefs(state)    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.

经过修改后,我们的逻辑就变得异常简单了。

复制import useRequest from./useRequest export default {    setup() {      const query = ref(vue     const { data, loading, error, run } = useRequest(        {          url: https://hn.algolia.com/api/v1/search         params: {            query          }        }      )      return {        data,        query,        error,        loading,        search: run,      }    }  1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.

当然,这个 useRequest 还有很多可以完善的地方,例如:不支持 http 方法修改、不支持节流防抖、不支持超时时间等等。最后,希望大家看完文章后能有所收获。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Facebook推出用于android数据加密的开源API
下一篇:使用Odata API连接云计算数据源
相关文章

 发表评论

暂时没有评论,来抢沙发吧~