以前在學習 React Hooks 的過程當中,看到一篇外網文章,經過 Hooks 來請求數據,並將這段邏輯抽象成一個新的 Hooks 給其餘組件複用,我也在個人博客裏翻譯了一下:《在 React Hooks 中如何請求數據?》,感興趣能夠看看。雖然是去年的文章,在閱讀以後一會兒就掌握了 Hooks 的使用方式,並且數據請求是在業務代碼中很經常使用的邏輯。html
Vue 3 已經發布一段時間了,其組合 API 多少有點 React Hooks 的影子在裏面,今天我也打算經過這種方式來學習下組合 API。vue
爲了快速啓動一個 Vue 3 項目,咱們直接使用當下最熱門的工具 Vite 來初始化項目。整個過程一鼓作氣,行雲流水。react
npm init vite-app vue3-app
# 打開生成的項目文件夾 cd vue3-app # 安裝依賴 npm install # 啓動項目 npm run dev
咱們打開 App.vue
將生成的代碼先刪掉。git
接下來咱們將經過 Hacker News API 來獲取一些熱門文章,Hacker News API返回的數據結構以下:github
{ "hits": [ { "objectID": "24518295", "title": "Vue.js 3", "url": "https://github.com/vuejs/vue-next/releases/tag/v3.0.0", }, {...}, {...}, ] }
咱們經過 ui > li
將新聞列表展現到界面上,新聞數據從 hits
遍歷中獲取。npm
<template> <ul> <li v-for="item of hits" :key="item.objectID" > <a :href="item.url">{{item.title}}</a> </li> </ul> </template> <script> import { reactive } from 'vue' export default { setup() { const state = reactive({ hits: [] }) return state } } </script>
在講解數據請求前,我看先看看 setup()
方法,組合 API 須要經過 setup()
方法來啓動,setup()
返回的數據能夠在模板內使用,能夠簡單理解爲 Vue 2 裏面 data()
方法返回的數據,不一樣的是,返回的數據須要先通過 reactive()
方法進行包裹,將數據變成響應式。json
在 Vue 2 中,咱們請求數據時,一般須要將發起請求的代碼放到某個生命週期中(created
或 mounted
)。在 setup()
方法內,咱們可使用 Vue 3 提供的生命週期鉤子將請求放到特定生命週期內,關於生命週期鉤子方法與以前生命週期的對好比下:api
能夠看到,基本上就是在以前的方法名前加上了一個 on
,且並無提供 onCreated
的鉤子,由於在 setup()
內執行就至關於在 created
階段執行。下面咱們在 mounted
階段來請求數據:bash
import { reactive, onMounted } from 'vue' 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 } }
最後效果以下:數據結構
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 } from 'vue' 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>
如今咱們在輸入框修改,就能觸發 state.query
同步更新,可是並不會觸發 fetch 從新調用,因此咱們須要經過 watchEffect()
來監聽響應數據的變化。
import { reactive, onMounted, watchEffect } from 'vue' 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 } }
因爲 watchEffect()
首次調用的時候,其回調就會執行一次,形成初始化時會請求兩次接口,因此咱們須要把 onMounted
中的 fetchData
刪掉。
onMounted(() => { - fetchData(state.query) watchEffect(() => { fetchData(state.query) }) })
watchEffect()
會監聽傳入函數內全部的響應式數據,一旦其中的某個數據發生變化,函數就會從新執行。若是要取消監聽,能夠調用 watchEffect()
的返回值,它的返回值爲一個函數。下面舉個例子:
const stop = watchEffect(() => { if (state.query === 'vue3') { // 當 query 爲 vue3 時,中止監聽 stop() } fetchData(state.query) })
當咱們在輸入框輸入 "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 } from 'vue' 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>
能夠注意到 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>
做爲強迫症患者,在模板層經過 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 } from 'vue' export default { setup(props, ctx) { const state = reactive({ input: 'vue', query: 'vue', hits: [] }) // 省略部分代碼... return { ...state, setQuery, } } } </script>
答案是『不能夠』。修改代碼後,能夠看到頁面雖然發起了請求,可是頁面並無展現數據。
state
在解構後,數據就變成了靜態數據,不能再被跟蹤,返回值相似於:
export default { setup(props, ctx) { // 省略部分代碼... return { input: 'vue', query: 'vue', hits: [], setQuery, } } }
爲了跟蹤基礎類型的數據(即非對象數據),Vue3 也提出瞭解決方案:ref()
。
import { ref } from 'vue' const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1
上面爲 Vue 3 的官方案例,ref()
方法返回的是一個對象,不管是修改仍是獲取,都須要取返回對象的 value
屬性。
咱們將 state
從響應對象改成一個普通對象,而後全部屬性都使用 ref
包裹,這樣修改後,後續的解構才作才能生效。這樣的弊端就是,state
的每一個屬性在修改時,都必須取其 value
屬性。可是在模板中不須要追加 .value
,Vue 3 內部有對其進行處理。
import { ref, onMounted, watchEffect } from 'vue' 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, } } }
有沒有辦法保持 state
爲響應對象,同時又支持其對象解構的呢?固然是有的,Vue 3 也提供瞭解決方案:toRefs()
。toRefs()
方法能夠將一個響應對象變爲普通對象,而且給每一個屬性加上 ref()
。
import { toRefs, reactive, onMounted, watchEffect } from 'vue' 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, } } }
一般,咱們發起請求的時候,須要爲請求添加 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, } } }
同時在模板使用這兩個變量:
<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>
展現 Loading、Error 狀態:
用過 umi 的同窗確定知道 umi 提供了一個叫作 useRequest 的 Hooks,用於請求數據很是的方便,那麼咱們經過 Vue 的組合 API 也能夠抽象出一個相似於 useRequest 的公共方法。
接下來咱們新建一個文件 useRequest.js
:
import { toRefs, reactive, } from 'vue' 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) } }
而後在 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 } from 'vue' 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>
當前的 useRequest
還有兩個缺陷:
import { isRef, toRefs, reactive, onMounted, } from 'vue' 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) } }
通過修改後,咱們的邏輯就變得異常簡單了。
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, } } }
固然,這個 useRequest
還有不少能夠完善的地方,例如:不支持 http 方法修改、不支持節流防抖、不支持超時時間等等。最後,但願你們看完文章後能有所收穫。