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

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

[[347283]]

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

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

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

  1. npm init vite-app vue3-app 
  1. # 打开生成的项目文件夹 
  2. cd vue3-app 
  3. # 安装依赖 
  4. npm install 
  5. # 启动项目 
  6. npm run dev 

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

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

  1.   "hits": [ 
  2.     { 
  3.       "objectID""24518295"
  4.       "title""Vue.js 3"
  5.       "url""https://github.com/vuejs/vue-next/releases/tag/v3.0.0"
  6.     }, 
  7.     {...}, 
  8.     {...}, 
  9.   ] 

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

  1. <template> 
  2.   <ul> 
  3.     <li 
  4.       v-for="item of hits" 
  5.       :key="item.objectID" 
  6.     > 
  7.       <a :href="item.url">{{item.title}}</a> 
  8.     </li> 
  9.   </ul> 
  10. </template> 
  11.  
  12. <script> 
  13. import { reactive } from 'vue' 
  14.  
  15. export default { 
  16.   setup() { 
  17.     const state = reactive({ 
  18.       hits: [] 
  19.     }) 
  20.     return state 
  21.   } 
  22. </script> 

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

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

生命周期

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

  1. import { reactive, onMounted } from 'vue' 
  2.  
  3. export default { 
  4.   setup() { 
  5.     const state = reactive({ 
  6.       hits: [] 
  7.     }) 
  8.     onMounted(async () => { 
  9.       const data = await fetch
  10.         'https://hn.algolia.com/api/v1/search?query=vue' 
  11.       ).then(rsp => rsp.json()) 
  12.       state.hits = data.hits 
  13.     }) 
  14.     return state 
  15.   } 

最后效果如下:

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

  1. <template> 
  2.   <input type="text" v-model="query" /> 
  3.   <ul> 
  4.     <li 
  5.       v-for="item of hits" 
  6.       :key="item.objectID" 
  7.     > 
  8.       <a :href="item.url">{{item.title}}</a> 
  9.     </li> 
  10.   </ul> 
  11. </template> 
  12.  
  13. <script> 
  14. import { reactive, onMounted } from 'vue' 
  15.  
  16. export default { 
  17.   setup() { 
  18.     const state = reactive({ 
  19.       query: 'vue'
  20.       hits: [] 
  21.     }) 
  22.     onMounted((async () => { 
  23.       const data = await fetch
  24.         `https://hn.algolia.com/api/v1/search?query=${state.query}` 
  25.       ).then(rsp => rsp.json()) 
  26.       state.hits = data.hits 
  27.     }) 
  28.     return state 
  29.   } 
  30. </script> 

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

  1. import { reactive, onMounted, watchEffect } from 'vue' 
  2.  
  3. export default { 
  4.   setup() { 
  5.     const state = reactive({ 
  6.       query: 'vue'
  7.       hits: [] 
  8.     }) 
  9.     const fetchData = async (query) => { 
  10.       const data = await fetch
  11.         `https://hn.algolia.com/api/v1/search?query=${query}` 
  12.       ).then(rsp => rsp.json()) 
  13.       state.hits = data.hits 
  14.     } 
  15.     onMounted(() => { 
  16.       fetchData(state.query) 
  17.       watchEffect(() => { 
  18.         fetchData(state.query) 
  19.       }) 
  20.     }) 
  21.     return state 
  22.   } 

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

  1. onMounted(() => { 
  2. - fetchData(state.query) 
  3.   watchEffect(() => { 
  4.     fetchData(state.query) 
  5.   }) 
  6. }) 

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

  1. const stop = watchEffect(() => { 
  2.   if (state.query === 'vue3') { 
  3.     // 当 query 为 vue3 时,停止监听 
  4.     stop() 
  5.   } 
  6.   fetchData(state.query) 
  7. }) 

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

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

  1. <template> 
  2.   <input type="text" v-model="input" /> 
  3.   <button @click="setQuery">搜索</button> 
  4.   <ul> 
  5.     <li 
  6.       v-for="item of hits" 
  7.       :key="item.objectID" 
  8.     > 
  9.       <a :href="item.url">{{item.title}}</a> 
  10.     </li> 
  11.   </ul> 
  12. </template> 
  13.  
  14. <script> 
  15. import { reactive, onMounted, watchEffect } from 'vue' 
  16.  
  17. export default { 
  18.   setup() { 
  19.     const state = reactive({ 
  20.       input: 'vue'
  21.       query: 'vue'
  22.       hits: [] 
  23.     }) 
  24.     const fetchData = async (query) => { 
  25.       const data = await fetch
  26.         `https://hn.algolia.com/api/v1/search?query=${query}` 
  27.       ).then(rsp => rsp.json()) 
  28.       state.hits = data.hits 
  29.     } 
  30.     onMounted(() => { 
  31.       watchEffect(() => { 
  32.         fetchData(state.query) 
  33.       }) 
  34.     }) 
  35.      
  36.     const setQuery = () => { 
  37.       state.query = state.input 
  38.     } 
  39.     return { setQuery, state } 
  40.   } 
  41. </script> 

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

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

  1. <template> 
  2.   <input type="text" v-model="state.input" /> 
  3.   <button @click="setQuery">搜索</button> 
  4.   <ul> 
  5.     <li 
  6.       v-for="item of state.hits" 
  7.       :key="item.objectID" 
  8.     > 
  9.       <a :href="item.url">{{item.title}}</a> 
  10.     </li> 
  11.   </ul> 
  12. </template> 

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

  1. <template> 
  2.   <input type="text" v-model="input" /> 
  3.   <button class="search-btn" @click="setQuery">搜索</button> 
  4.   <ul class="results"
  5.     <li 
  6.       v-for="item of hits" 
  7.       :key="item.objectID" 
  8.     > 
  9.       <a :href="item.url">{{item.title}}</a> 
  10.     </li> 
  11.   </ul> 
  12. </template> 
  13.  
  14. <script> 
  15. import { reactive, onMounted, watchEffect } from 'vue' 
  16.  
  17. export default { 
  18.   setup(props, ctx) { 
  19.     const state = reactive({ 
  20.       input: 'vue'
  21.       query: 'vue'
  22.       hits: [] 
  23.     }) 
  24.     // 省略部分代码... 
  25.     return { 
  26.       ...state, 
  27.       setQuery, 
  28.     } 
  29.   } 
  30. </script> 

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

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

  1. export default { 
  2.   setup(props, ctx) { 
  3.     // 省略部分代码... 
  4.     return { 
  5.       input: 'vue'
  6.       query: 'vue'
  7.       hits: [], 
  8.       setQuery, 
  9.     } 
  10.   } 

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

  1. import { ref } from 'vue' 
  2.  
  3. const count = ref(0) 
  4. console.log(count.value) // 0 
  5.  
  6. count.value++ 
  7. console.log(count.value) // 1 

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

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

  1. import { ref, onMounted, watchEffect } from 'vue' 
  2. export default { 
  3.   setup() { 
  4.     const state = { 
  5.       input: ref('vue'), 
  6.       query: ref('vue'), 
  7.       hits: ref([]) 
  8.     } 
  9.     const fetchData = async (query) => { 
  10.       const data = await fetch
  11.         `https://hn.algolia.com/api/v1/search?query=${query}` 
  12.       ).then(rsp => rsp.json()) 
  13.       state.hits.value = data.hits 
  14.     } 
  15.     onMounted(() => { 
  16.       watchEffect(() => { 
  17.         fetchData(state.query.value) 
  18.       }) 
  19.     }) 
  20.     const setQuery = () => { 
  21.       state.query.value = state.input.value 
  22.     } 
  23.     return { 
  24.       ...state, 
  25.       setQuery, 
  26.     } 
  27.   } 

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

  1. import { toRefs, reactive, onMounted, watchEffect } from 'vue' 
  2.  
  3. export default { 
  4.   setup() { 
  5.     const state = reactive({ 
  6.       input: 'vue'
  7.       query: 'vue'
  8.       hits: [] 
  9.     }) 
  10.     const fetchData = async (query) => { 
  11.       const data = await fetch
  12.         `https://hn.algolia.com/api/v1/search?query=${query}` 
  13.       ).then(rsp => rsp.json()) 
  14.       state.hits = data.hits 
  15.     } 
  16.     onMounted(() => { 
  17.       watchEffect(() => { 
  18.         fetchData(state.query) 
  19.       }) 
  20.     }) 
  21.     const setQuery = () => { 
  22.       state.query = state.input 
  23.     } 
  24.     return { 
  25.       ...toRefs(state), 
  26.       setQuery, 
  27.     } 
  28.   } 

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

  1. export default { 
  2.   setup() { 
  3.     const state = reactive({ 
  4.       input: 'vue'
  5.       query: 'vue'
  6.       hits: [], 
  7.       error: false
  8.       loading: false
  9.     }) 
  10.     const fetchData = async (query) => { 
  11.       state.error = false 
  12.       state.loading = true 
  13.       try { 
  14.         const data = await fetch
  15.           `https://hn.algolia.com/api/v1/search?query=${query}` 
  16.         ).then(rsp => rsp.json()) 
  17.         state.hits = data.hits 
  18.       } catch { 
  19.         state.error = true 
  20.       } 
  21.       state.loading = false 
  22.     } 
  23.     onMounted(() => { 
  24.       watchEffect(() => { 
  25.         fetchData(state.query) 
  26.       }) 
  27.     }) 
  28.     const setQuery = () => { 
  29.       state.query = state.input 
  30.     } 
  31.     return { 
  32.       ...toRefs(state), 
  33.       setQuery, 
  34.     } 
  35.   } 

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

  1. <template> 
  2.   <input type="text" v-model="input" /> 
  3.   <button @click="setQuery">搜索</button> 
  4.   <div v-if="loading">Loading ...</div> 
  5.   <div v-else-if="error">Something went wrong ...</div> 
  6.   <ul v-else
  7.     <li 
  8.       v-for="item of hits" 
  9.       :key="item.objectID" 
  10.     > 
  11.       <a :href="item.url">{{item.title}}</a> 
  12.     </li> 
  13.   </ul> 
  14. </template> 

展示 Loading、Error 状态:

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

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

  1. import { 
  2.   toRefs, 
  3.   reactive, 
  4. from 'vue' 
  5.  
  6. export default (options) => { 
  7.   const { url } = options 
  8.   const state = reactive({ 
  9.     data: {}, 
  10.     error: false
  11.     loading: false
  12.   }) 
  13.  
  14.   const run = async () => { 
  15.     state.error = false 
  16.     state.loading = true 
  17.     try { 
  18.       const result = await fetch(url).then(res => res.json()) 
  19.       state.data = result 
  20.     } catch(e) { 
  21.       state.error = true 
  22.     } 
  23.     state.loading = false 
  24.   } 
  25.  
  26.   return { 
  27.     run, 
  28.     ...toRefs(state) 
  29.   } 

然后在 App.vue 中引入:

  1. <template> 
  2.   <input type="text" v-model="query" /> 
  3.   <button @click="search">搜索</button> 
  4.   <div v-if="loading">Loading ...</div> 
  5.   <div v-else-if="error">Something went wrong ...</div> 
  6.   <ul v-else
  7.     <li 
  8.       v-for="item of data.hits" 
  9.       :key="item.objectID" 
  10.     > 
  11.       <a :href="item.url">{{item.title}}</a> 
  12.     </li> 
  13.   </ul> 
  14. </template> 
  15.  
  16. <script> 
  17. import { ref, onMounted } from 'vue' 
  18. import useRequest from './useRequest' 
  19.  
  20. export default { 
  21.   setup() { 
  22.     const query = ref('vue'
  23.     const { data, loading, error, run } = useRequest({ 
  24.       url: 'https://hn.algolia.com/api/v1/search' 
  25.     }) 
  26.     onMounted(() => { 
  27.       run() 
  28.     }) 
  29.     return { 
  30.       data, 
  31.       query, 
  32.       error, 
  33.       loading, 
  34.       search: run, 
  35.     } 
  36.   } 
  37. </script> 

当前的 useRequest 还有两个缺陷:

传入的 url 是固定的,query 修改后,不能及时的反应到 url 上;
不能自动请求,需要手动调用一下 run 方法;

  1. import { 
  2.   isRef, 
  3.   toRefs, 
  4.   reactive, 
  5.   onMounted, 
  6. from 'vue' 
  7.  
  8. export default (options) => { 
  9.   const { url, manual = false, params = {} } = options 
  10.  
  11.   const state = reactive({ 
  12.     data: {}, 
  13.     error: false
  14.     loading: false
  15.   }) 
  16.  
  17.   const run = async () => { 
  18.     // 拼接查询参数 
  19.     let query = '' 
  20.     Object.keys(params).forEach(key => { 
  21.       const val = params[key
  22.       // 如果去 ref 对象,需要取 .value 属性 
  23.       const value = isRef(val) ? val.value : val 
  24.       query += `${key}=${value}&` 
  25.     }) 
  26.     state.error = false 
  27.     state.loading = true 
  28.     try { 
  29.       const result = await fetch(`${url}?${query}`) 
  30.        .then(res => res.json()) 
  31.       state.data = result 
  32.     } catch(e) { 
  33.       state.error = true 
  34.     } 
  35.     state.loading = false 
  36.   } 
  37.  
  38.   onMounted(() => { 
  39.     // 第一次是否需要手动调用 
  40.     !manual && run() 
  41.   }) 
  42.  
  43.   return { 
  44.     run, 
  45.     ...toRefs(state) 
  46.   } 

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

  1. import useRequest from './useRequest' 
  2.  
  3. export default { 
  4.   setup() { 
  5.     const query = ref('vue'
  6.     const { data, loading, error, run } = useRequest( 
  7.       { 
  8.         url: 'https://hn.algolia.com/api/v1/search'
  9.         params: { 
  10.           query 
  11.         } 
  12.       } 
  13.     ) 
  14.     return { 
  15.       data, 
  16.       query, 
  17.       error, 
  18.       loading, 
  19.       search: run, 
  20.     } 
  21.   } 

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

 

责任编辑:姜华 来源: 更了不起的前端
相关推荐

2020-10-20 09:30:13

Vue 3 API 数据

2021-04-12 05:55:29

缓存数据Axios

2022-06-13 08:39:21

Vue3API

2024-06-20 13:50:53

Vue 3API开发

2022-07-13 10:07:31

vue3组件监听器

2024-01-12 07:22:49

Vue组合式props

2021-03-27 22:21:48

HTTPPython数据

2020-09-19 21:15:26

Composition

2021-05-19 09:29:52

VueAxios异步请求

2011-04-21 09:59:48

WEBjavascript

2022-03-24 15:28:43

Vue开发框架

2020-07-22 18:04:00

VueAPI共享

2022-07-08 08:52:25

Vue3组合动态返回

2013-08-20 13:22:35

PythonGo编程语言

2021-11-30 11:04:52

API网络技术应用程序

2021-03-30 08:05:39

Vue 3 生命周期Vue2

2019-07-30 11:17:18

系统数据安全

2021-07-29 12:05:18

Vue3Api前端

2023-04-19 08:12:00

VueAPI组合式

2021-12-16 08:27:54

Vue3 插件Vue应用
点赞
收藏

51CTO技术栈公众号