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

相关文章
相关标签/搜索