Vue 3 的組合 API 如何請求數據?
前言
之前在學習 React Hooks 的過程中,看到一篇外網文章,通過 Hooks 來請求數據,并將這段邏輯抽象成一個新的 Hooks 給其他組件復用,我也在我的博客里翻譯了一下:《在 React Hooks 中如何請求數據?》,感興趣可以看看。雖然是去年的文章,在閱讀之后一下子就掌握了 Hooks 的使用方式,而且數據請求是在業務代碼中很常用的邏輯。
Vue 3 已經發布一段時間了,其組合 API 多少有點 React Hooks 的影子在里面,今天我也打算通過這種方式來學習下組合 API。
項目初始化
為了快速啟動一個 Vue 3 項目,我們直接使用當下最熱門的工具 Vite 來初始化項目。整個過程一氣呵成,行云流水。
- npm init vite-app vue3-app
- # 打開生成的項目文件夾
- cd vue3-app
- # 安裝依賴
- npm install
- # 啟動項目
- npm run dev
我們打開 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",
- },
- {...},
- {...},
- ]
- }
我們通過 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 } from 'vue'
- export default {
- setup() {
- const state = reactive({
- hits: []
- })
- return state
- }
- }
- </script>
在講解數據請求前,我看先看看 setup() 方法,組合 API 需要通過 setup() 方法來啟動,setup() 返回的數據可以在模板內使用,可以簡單理解為 Vue 2 里面 data() 方法返回的數據,不同的是,返回的數據需要先經過 reactive() 方法進行包裹,將數據變成響應式。
組合 API 中請求數據
在 Vue 2 中,我們請求數據時,通常需要將發起請求的代碼放到某個生命周期中(created或 mounted)。在 setup() 方法內,我們可以使用 Vue 3 提供的生命周期鉤子將請求放到特定生命周期內,關于生命周期鉤子方法與之前生命周期的對比如下:
生命周期
可以看到,基本上就是在之前的方法名前加上了一個 on,且并沒有提供 onCreated 的鉤子,因為在 setup() 內執行就相當于在 created 階段執行。下面我們在 mounted 階段來請求數據:
- 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 方法修改、不支持節流防抖、不支持超時時間等等。最后,希望大家看完文章后能有所收獲。
本文轉載自微信公眾號「更了不起的前端」,可以通過以下二維碼關注。轉載本文請聯系更了不起的前端公眾號。