Как API композиции Vue 3 запрашивает данные?

внешний интерфейс Vue.js
Как API композиции Vue 3 запрашивает данные?

предисловие

В процессе изучения React Hooks я увидел в интернете статью, запрашивающую данные через Hooks, и абстрагирующую эту логику в новый Hooks для повторного использования другими компонентами, я также перевел ее в своем блоге:Как запросить данные в React 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 есть параметр запроса, в предыдущем случае мы зафиксировали этот параметр, а теперь определяем эту переменную через Response data.

<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Синхронное обновление, но не вызывает выборку, поэтому нам нужно пройти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

метод возврата события

Теперь есть проблема, что каждый раз, когда значение на входе изменяется, будет запускаться запрос.Мы можем добавить кнопку и запускать ее после нажатия кнопки.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>

Вы можете заметить, что метод события щелчка, привязанного к кнопке, также проходит черезsetup()метод возвращает, мы можем использоватьsetup()Возвращаемое значение метода понимается как в Vue2data()Методы иmethodsслияние объектов.

Исходное состояние возвращаемого значения стало атрибутом текущего возвращаемого значения, поэтому, когда мы извлекаем данные на уровне шаблона, нам нужно внести некоторые изменения, добавив перед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Измените объект ответа на простой объект, затем все свойства используютrefpackage, чтобы после модификации могла вступить в силу последующая деконструкция. Недостатком этого является то, что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,
    }
  }
}

Состояние загрузки и ошибки

Обычно, когда мы инициируем запрос, нам нужно добавить к запросу состояния загрузки и ошибки, нам нужно только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>

Отображение загрузки, статус ошибки:

Demo

Абстрактная логика запроса данных

Студенты, которые использовали umi, должны знать, что umi предоставляет хуки, называемые useRequest, которые очень удобны для запроса данных, поэтому мы также можем абстрагировать общедоступный метод, аналогичный useRequest, через комбинированный API Vue.

Далее создаем новый файл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 зафиксирован, после модифицированного запроса он не может быть отражено на URL-адрес во времени;
  2. Его нельзя запросить автоматически, вам нужно вручную вызвать метод запуска;
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-методов, не поддерживает троттлинг и анти-шейк, не поддерживает таймаут и так далее. Наконец, я надеюсь, что вы сможете что-то получить после прочтения статьи.