Vue 3 的組合 API 如何請求數據?

前言

以前在學習 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

組合 API 的入口

接下來咱們將經過 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

組合 API 中請求數據

在 Vue 2 中,咱們請求數據時,一般須要將發起請求的代碼放到某個生命週期中(createdmounted)。在 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
  }
}

最後效果以下:數據結構

Demo

監聽數據變更

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)
  })
})

Demo

watchEffect() 會監聽傳入函數內全部的響應式數據,一旦其中的某個數據發生變化,函數就會從新執行。若是要取消監聽,能夠調用 watchEffect() 的返回值,它的返回值爲一個函數。下面舉個例子:

const stop = watchEffect(() => {
  if (state.query === 'vue3') {
    // 當 query 爲 vue3 時,中止監聽
    stop()
  }
  fetchData(state.query)
})

當咱們在輸入框輸入 "vue3" 後,就不會再發起請求了。

Demo

返回事件方法

如今有個問題就是 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>

Demo

返回數據修改

做爲強迫症患者,在模板層經過 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,
    }
  }
}

Demo

爲了跟蹤基礎類型的數據(即非對象數據),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 狀態

一般,咱們發起請求的時候,須要爲請求添加 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 狀態:

Demo

將數據請求邏輯抽象

用過 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 還有兩個缺陷:

  1. 傳入的 url 是固定的,query 修改後,不能及時的反應到 url 上;
  2. 不能自動請求,須要手動調用一下 run 方法;
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 方法修改、不支持節流防抖、不支持超時時間等等。最後,但願你們看完文章後能有所收穫。

image

相關文章
相關標籤/搜索