Вашему приложению нужен «Отменяемый модуль асинхронного HTTP-запроса».

JavaScript
Вашему приложению нужен «Отменяемый модуль асинхронного HTTP-запроса».

Связь

Вашему приложению нужен «Отменяемый модуль асинхронного HTTP-запроса».Видео версия

github

Как отменить асинхронный HTTP-запрос?

Асинхронные HTTP-запросы, возможно, широко распространены в современных веб-приложениях. Для лучшего взаимодействия с пользователем в 2005 году появился Ajax, который поддерживает частичные обновления без обновления страницы.

Ajax поддерживает как синхронные, так и асинхронные методы, но в основном все используют только асинхронные методы, потому что отправка синхронных запросов приведет к переходу браузера в состояние временной приостановки, особенно для интерфейсов, требующих обработки больших объемов данных и длительного ожидания. случае использование синхронного запроса приведет к очень плохому взаимодействию с пользователем. Таким образом, все обычно принимают метод асинхронного запроса, поэтому сегодняшняя тема, вам может понадобиться отменяемый асинхронный HTTP-запрос. Вы можете подумать над следующими вопросами:

  • Зачем вам нужен отменяемый асинхронный HTTP-запрос?

    Мы часто сталкиваемся с ситуацией, когда отправляется HTTP-запрос, а результат вдруг не нужен в ожидании ответа от интерфейса.

  • В каких сценариях он будет использоваться?

    Например, на странице есть несколько вкладок, щелкните каждую вкладку, чтобы отправить соответствующий HTTP-запрос, а затем отобразите результат запроса в области содержимого. Теперь, когда пользователь работает, щелкните метку Tab1 и получите результат запроса интерфейса 1, но после нажатия Tab2, поскольку интерфейс должен ждать возврата 3 с, пользователь не хочет ждать и напрямую нажимает Tab3. В настоящее время интерфейс Tab2 больше не нужен.

  • Для чего это?

    В это время, если вы не отмените запрос Tab2, в области содержимого появится следующее явление: сначала отобразите результаты интерфейса Tab3, а затем подождите некоторое время ( 0

Следующие две анимации являются демонстрацией вышеописанного процесса, которые могут помочь вам лучше понять сцену. Первая анимация — это нормальная работа. При нажатии кнопки «Вкладка 2» дождитесь возврата интерфейса, прежде чем продолжать нажимать вкладку 3. Но вторая анимация демонстрирует это ненормальное явление: после нажатия на вкладку 2 я нажимаю на вкладку 3, не дожидаясь возврата интерфейса, и обнаруживаю, что содержимое сначала отображает содержимое интерфейса вкладки 3, а затем становится результатом вкладки 2. интерфейс.

正常结果 异常结果

Можно сказать, что такие требования и сценарии очень распространены в современной разработке веб-приложений. Просто вы можете не осознавать, что здесь будут проблемы, потому что эта ошибка существует только в интерфейсе (например, во вкладке 2), которому нужно долго ждать, чтобы получить результат ответа, поэтому, если ваше приложение имеет интерфейс, который необходимо обработать и передать Эта проблема может возникнуть, когда большой объем данных или приложений используется в слабой сетевой среде. Поэтому зрелое и стабильное веб-приложение должно поддерживать отменяемые асинхронные HTTP-запросы.

Пример

В современной разработке веб-приложений обычной практикой является инкапсуляция общедоступного модуля HTTP-запроса, который обычно основан на сторонних библиотеках с открытым исходным кодом (таких как Axios) или нативных методах (таких как Fetch API, XMLHttpRequest).

Далее мы будем использовать пример для моделирования реальной сцены проекта.На самом деле, приведенная выше анимация исходит из реальной разработки проекта, и фактический случай неудобен для предоставления, поэтому мы используем пример для моделирования. Модуль HTTP-запроса (request) в данном случае реализован через Axios, Fetch API и XMLHttpRequest соответственно.

Сервер

Здесь сервер реализован через экспресс-фреймворк с использованиемnode server.jsилиnodemon server.jsначать

const app = require('express')()

const cors = require('cors')
app.use(cors())

app.get('/tab1', (req, res) => {
  res.json('Tab 1 的结果')
})

app.get('/tab2', (req, res) => {
  // 这里通过延时代码来模拟处理大数据量的场景
  setTimeout(() => {
    res.json('Tab 2 的结果')
  }, 3000)
})

app.get('/tab3', (req, res) => {
  res.json('Tab 3 的结果')
})

app.listen(3000, () => {
  console.info('app start at 3000 port')
})

внешний интерфейс

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #content {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 200px;
      height: 100px;
      border: 1px solid #eee;
    }
  </style>
</head>

<body>
  <!-- 内容显示区 -->
  <h3>内容区</h3>
  <p id="content">no content</p>

  <!-- 通过三个按钮来模拟三个 Tab 标签 -->
  <button id="tab1">Tab 1</button>
  <button id="tab2">Tab 2</button>
  <button id="tab3">Tab 3</button>

  <button id="reset">reset</button>

  <!-- axios -->
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <!-- 基于 axios 封装的 request 接口 -->
  <script src="./axiosRequest.js"></script>
  <!-- 基于 fetch 封装的 request 接口 -->
  <!-- <script src="./fetchRequest.js"></script> -->
  <!-- 基于 XMLHttpRequest 封装的 request 接口 -->
  <!-- <script src="./xhrRequest.js"></script> -->

  <script>
    // 分别给三个按钮添加点击事件,当发生点击时执行相应的回调函数请求对应的接口,请求成功后将结果显示到内容区
    const tab1 = document.querySelector('#tab1')
    const tab2 = document.querySelector('#tab2')
    const tab3 = document.querySelector('#tab3')

    // 内容显示区
    const content = document.querySelector('#content')

    // reset
    const reset = document.querySelector('#reset')

    tab1.addEventListener('click', async function () {
      const { data } = await request({ url: '/tab1' })
      content.textContent = data
    })

    tab2.addEventListener('click', async function () {
      const { data } = await request({ url: '/tab2' }, true)
      content.textContent = data
    })

    tab3.addEventListener('click', async function () {
      const { data } = await request({ url: '/tab3' })
      content.textContent = data
    })

    reset.addEventListener('click', function () {
      content.textContent = 'no content'
    })

  </script>
</body>

</html>

axiosRequest.js

На основе интерфейса запроса, инкапсулированного Axios, он может быть расширен в соответствии с потребностями вашего бизнеса.Как правило, некоторые расширения выполняются в двух перехваченных местах.

const baseURL = 'http://localhost:3000'

const ins = axios.create({
  baseURL,
  timeout: 10000
})

ins.interceptors.request.use(config => {
  // 拦截请求,可以在这里自定义一些配置,比如 token
  return config
})

ins.interceptors.response.use(response => {
  // 拦截响应,可以根据服务端返回的状态码做一些自定义的响应和信息提示
  return response
})

function request(reqArgs) {
  return ins.request(reqArgs)
}

fetchRequest.js

Интерфейс запроса инкапсулирован на основе fetch API, инкапсуляция проста, просто для иллюстрации проблемы; формат возвращаемых данных совместим с примером кода на основе axiosRequest

const baseURL = 'http://localhost:3000'

function request(reqArgs) {
  // 接口返回的数据格式是为了兼容 axios 的示例代码
  return fetch(baseURL + reqArgs.url).then(async response => ({ data: await response.json() }))
}

xhrRequest.js

Интерфейс запроса инкапсулирован на основе API XMLHttpRequest, инкапсуляция проста, просто для иллюстрации проблемы, формат возвращаемых данных совместим с примером кода на основе axiosRequest

const baseURL = 'http://localhost:3000'

const xhr = new XMLHttpRequest()
function request(reqArgs) {
  return new Promise((resolve, reject) => {
    xhr.onload = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        // 接口返回的数据格式是为了兼容 axios 的示例代码
        resolve({ data: xhr.responseText })
      } else {
        // 出错了
        reject(xhr.status)
      }
    }
    xhr.open(reqArgs.method || 'get', baseURL + reqArgs.url, true)
    xhr.send(reqArgs.data || null)
  })
}

решение

Ниже описано, как модифицировать существующее приложение, чтобы обеспечить безопасное и плавное обновление с минимальными изменениями. Для нового проекта вам нужно только интегрировать следующие решения при создании архитектуры.

Решения можно разделить на две категории:

  • собственный метод

    Axios, Fetch API и XMLHttpRequest изначально предоставляют возможность отмены асинхронных HTTP-запросов, но некоторые из них могут быть не столь полезными, например Fetch API.

  • Основной подход

    Я предпочитаю общий метод, который прост и понятен, и мне не нужно запоминать все виды нативных методов.

Отменяемые обещания

Прежде чем вступить в формальную трансформацию, пусть каждый популяризирует знание,Как отменить обещание?

Как мы все знаем, как только логика обещания начинает выполняться, его нельзя остановить, пока оно не завершит выполнение. Поэтому мы часто сталкиваемся с ситуациями, когда программа уже не нуждается в своих результатах при обычной обработке асинхронной логики, что очень похоже на наш случай. На данный момент было бы неплохо отменить Promise, некоторые сторонние библиотеки, такие как Axios, предоставляют эту возможность. На самом деле, комитет TC39 также планировал добавить эту функцию, но в конечном итоге соответствующее предложение было отозвано. В результате обещания ES6 считаются «радикальными».

На самом деле, мы можем предоставить временный пакет через характеристики промиса, чтобы добиться аналогичной отмены промиса (но аналогично). Все мы знаем, что состояние промисов больше не изменится после того, как оно будет установлено (с Pending на Fulfilled или Rejected).

const p = new Promise((resolve, reject) => {
  resolve('result message')
  // 这个 resolve 会被忽略
  resolve('我被忽略了。。。')
  console.log('I am running !!')
})

// I am running!!

// Promise {<fulfilled>: "result message"}
console.log(p)

Мы можем воспользоваться этой функцией для реализации отменяемого промиса. Функция отмены может быть открыта извне, и эта функция вызывается, когда обещание необходимо отменить.При вызове функции будет выполняться метод resovle или reject обещания, так что разрешение или отклонение будут игнорироваться, когда интерфейс получает ответ. Таким образом реализованы такие функции, как отмена Promise.

<!-- 可取消的 Promise -->

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #result {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 200px;
      height: 100px;
      border: 1px solid #eee;
    }
  </style>
</head>

<body>
  <!-- 显示请求结果 -->
  <h3>请求结果</h3>
  <p id="result">no result</p>

  <!-- 三个按钮:请求按钮、取消请求的按钮、复位按钮 -->
  <button id="req1">req 1</button>
  <button id="cancel">cancel</button>
  <button id="reset">reset</button>

  <script>
    // 暴露取消 Promise 的接口
    let cancelReq = null
    // 暴露 request 接口
    function request(reqArgs) {
      return new Promise((resolve, reject) => {
        // 通过延时代码来模拟异步 http 请求
        setTimeout(() => {
          resolve('result message')
        }, 2000);

        // 给用户提供一个取消请求的函数
        cancelReq = function () {
          resolve('请求被取消了')
          cancelReq = null
        }
      })
    }
  </script>

  <script>
    // 三个按钮
    const req1 = document.querySelector('#req1')
    const cancel = document.querySelector('#cancel')
    const reset = document.querySelector('#reset')
    // 结果显示区
    const result = document.querySelector('#result')

    // 给三个按钮添加 click 事件
    req1.addEventListener('click', async function () {
      const ret = await request('/req')
      result.textContent = ret
    })
    cancel.addEventListener('click', function () {
      cancelReq()
    })
    reset.addEventListener('click', function() {
      result.textContent = 'no result'
    })
  </script>
</body>

</html>

Имея вышеуказанную основу, мы можем начать преобразовывать наш код случая.

Эффект

Во-первых, вы можете видеть, что после обновления предыдущей проблемы не существует.

index.html

// 在需要取消 http 请求的地方添加取消函数调用
tab3.addEventListener('click', async function () {
  // 取消上一个请求
  cancelFn('Tab 2 的接口请求被取消了')

  const { data } = await request({ url: '/tab3' })
  content.textContent = data
})

axiosRequest.js

Фактически, собственное решение axios такое же, как и общее решение, которое реализуется с использованием неизменного состояния после выполнения промиса.

Родная программаофициальный сайт Аксиос

const baseURL = 'http://localhost:3000'
const CancelToken = axios.CancelToken

const ins = axios.create({
  baseURL,
  timeout: 10000
})

ins.interceptors.request.use(config => {
  // 拦截请求,可以在这里自定义一些配置,比如 token
  return config
})

ins.interceptors.response.use(response => {
  // 拦截响应,可以根据服务端返回的状态码做一些自定义的响应和信息提示
  return response
})

// 初始化为一个函数,防止报错
let cancelFn = function () {}

function request(reqArgs) {
  // 在传递的参数中设置一个 cancelToken 实例
  reqArgs.cancelToken = new CancelToken(function (cancel) {
    // 向外暴露取消函数
    cancelFn = cancel
  })
  return ins.request(reqArgs)
}

Общее решение

const baseURL = 'http://localhost:3000'
const CancelToken = axios.CancelToken

const ins = axios.create({
  baseURL,
  timeout: 10000
})

ins.interceptors.request.use(config => {
  // 拦截请求,可以在这里自定义一些配置,比如 token
  return config
})

ins.interceptors.response.use(response => {
  // 拦截响应,可以根据服务端返回的状态码做一些自定义的响应和信息提示
  return response
})

// 初始化为一个函数,防止报错
let cancelFn = function () {}

function request(reqArgs) {
  return new Promise((resolve, reject) => {
    // 请求接口
    ins.request(reqArgs).then(res => resolve(res))

    // 向外暴露取消函数
    cancelFn = function (msg) {
      reject({ message: msg })
    }
  })
}

fetchRequest.js

Fetch API поддерживает запросы на прерывание через AbortController/AbortSignal, также доступно универсальное решение. На самом деле общее решение лучше, потому что прерванная выборка будет отмечена флажком и станет недоступной, пока страница не будет обновлена. На самом деле родное решение fetch не может решить проблему в нашем случае. Хотя он прерывает запрос, он также предотвращает отправку последующих запросов.

Собственное решение

После выполнения вы увидите в консоли следующую информацию:

Первый указывает на то, что пользователь завершил запрос на выборку, то есть сообщение-подсказку, сгенерированное вызовом функции cancelFn. Второе сообщение об ошибке связано с тем, что мы повторно отправляем другой запрос на выборку после прерывания запроса на выборку (нажав кнопку вкладки 3), который сообщает вам, что выборка текущего объекта окна была прекращена пользователем, поэтому вам необходимо обновить страницу, повторно инициализировать эти глобальные объекты (window.fetch)

// 通过 AbortController/AbortSignal 中断请求
const abortController = new AbortController()

// 向外暴露取消函数
function cancelFn() {
  // 中断所有网络传输,特别适合希望停止传输大型负载的情况
  abortController.abort()
}

const baseURL = 'http://localhost:3000'

function request(reqArgs) {
  // 接口返回的数据格式是为了兼容 axios 的示例代码
  return fetch(baseURL + reqArgs.url, { signal: abortController.signal }).then(async response => ({ data: await response.json() }))
}

Общее решение

// 初始化为一个函数,防止报错
let cancelFn = function () {}

const baseURL = 'http://localhost:3000'

function request(reqArgs) {
  return new Promise((resolve, reject) => {
    // 接口返回的数据格式是为了兼容 axios 的示例代码
    fetch(baseURL + reqArgs.url).then(async response => resolve({ data: await response.json() }))

    // 向外暴露取消函数
    cancelFn = function(msg) {
      reject({ message: msg })
    }
  })
}

xhrRequest.js

Собственное решение

const baseURL = 'http://localhost:3000'

const xhr = new XMLHttpRequest()
function request(reqArgs) {
  return new Promise((resolve, reject) => {
    xhr.onload = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        // 接口返回的数据格式是为了兼容 axios 的示例代码
        resolve({ data: xhr.responseText })
      } else {
        // 出错了
        reject(xhr.status)
      }
    }
    xhr.open(reqArgs.method || 'get', baseURL + reqArgs.url, true)
    xhr.send(reqArgs.data || null)
  })
}

// 向外暴露取消函数
function cancelFn() {
  // xhr 原生提供了 abort 方法
  xhr.abort()
}

Общее решение

const baseURL = 'http://localhost:3000'

const xhr = new XMLHttpRequest()

// 初始化取消函数,防止调用报错
let cancelFn = function() {}

function request(reqArgs) {
  return new Promise((resolve, reject) => {
    xhr.onload = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        // 接口返回的数据格式是为了兼容 axios 的示例代码
        resolve({ data: xhr.responseText })
      } else {
        // 出错了
        reject(xhr.status)
      }
    }
    xhr.open(reqArgs.method || 'get', baseURL + reqArgs.url, true)
    xhr.send(reqArgs.data || null)

    // 向外暴露取消函数
    cancelFn = function (msg) {
      reject({ message: msg })
    }
  })
}

Суммировать

Вот и все сценарии терминации асинхронных HTTP-запросов, которые можно объединить в две категории:

  • Собственное решение

  • Общее решение для вторичной инкапсуляции на основе Promise

Каждый может выбрать в соответствии со своими потребностями.

Связь

Вашему приложению нужен «Отменяемый модуль асинхронного HTTP-запроса».Видео версия

github