developit / developit/unfetch

The second alternative to AbortController support (#54, #68, #137)

Đang mở
#148 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
JavaScript
Star
5.7k
Fork
204
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Mô tả

Resolves #54
Alternative to #68, and #137

### This solution
- Supports single `abort()` for the several parallel fetch requests
- Actually do http request abort over the network (not wiaitng for the result and ignores it like with `Promise.race` surrogates)
- Based on standard `AbortController` implementation (can run with polyfills on IE, see below)
- Do not run request if it was aborted already
- Do not abort request if it was complete (`readyState === DONE_STATE`)
- Add credentials not only for `include` but for `same-origin` also (usefull for some CORS use cases)
- Accumulates #54, #68, #137 discussions, [this](https://github.com/stereobooster/react-ideal-image/blob/master/src/components/unfetch.js) `unfetch` enchancement and [this](https://codesandbox.io/s/92vnjok08r?file=/src/xhr/xhr.js:316-330) implementation alternative

Not sure about **500 bytes** of the bundle size, but should be very close to it :wink:

### The code (`abortable-unfetch.mjs`)
```js
export default function (url, { method, headers, credentials, body, signal } = {}) {
return new Promise((resolve, reject) => {
const abortError = () => {
try {
return new DOMException('Aborted', 'AbortError')
} catch (error) { /* the DOMException constructor is not supported */
const abortError = new Error('Aborted')
abortError.name = 'AbortError'
return abortError
}
}

if (signal && signal.aborted) {
reject(abortError())
return
}

const request = new XMLHttpRequest()
const keys = []
const all = []
const respHeaders = {}

const response = () => ({
ok: (request.status / 100 | 0) === 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
text: () => Promise.resolve(request.responseText),
json: () => Promise.resolve(request.responseText).then(JSON.parse),
blob: () => Promise.resolve(new Blob([request.response])),
clone: response,
headers: {
keys: () => keys,
entries: () => all,
get: n => respHeaders[n.toLowerCase()],
has: n => n.toLowerCase() in respHeaders,
},
})

request.open(method || 'get', url, true)

request.onload = () => {
request.getAllResponseHeaders().
replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,
(m, key, value) => {
keys.push(key = key.toLowerCase())
all.push([key, value])
respHeaders[key] = respHeaders[key]
? `${respHeaders[key]},${value}`
: value
})
resolve(response())
}

if (signal) {
const abortListener = () => request.abort()
signal.addEventListener('abort', abortListener)
request.onreadystatechange = () => {
if (request.readyState === 4) { /* DONE_STATE = 4 */
signal.removeEventListener('abort', abortListener)
}
}
}
request.onabort = () => reject(abortError())
request.onerror = reject

request.withCredentials = credentials === 'same-origin' || credentials === 'include'

for (const i in headers) {
request.setRequestHeader(i, headers[i])
}

request.send(body || null)
})
}
```

### The polyfill (`abortable-unfetch-polyfill.mjs`)
```js
import abortableUnfetch from './abortable-unfetch'

const g =
typeof self !== 'undefined' ? self :
typeof window !== 'undefined' ? window :
typeof global !== 'undefined' ? global :
undefined

if (g) {
if (typeof g.fetch === 'undefined') {
g.fetch = abortableUnfetch
}
}
```

### `AbortController` polyfills

There are two alternatives to polyfill the standard `AbortController` behaviour on the old browsers (like IE11):
- [`abortcontroller-polyfill`](https://github.com/mo/abortcontroller-polyfill)
- [`abort-controller`](https://github.com/mysticatea/abort-controller#readme)

For the minimum bundle size I prefer the first one and because we based on the standard AbortController API we do not need to polyfill fetch additionally:
```sh
npm i abortcontroller-polyfill
yarn add abortcontroller-polyfill
pnpm add abortcontroller-polyfill
```
Then somewhere in your `index.mjs`:
```js
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
import './abortable-unfetch-polyfill'
```
Do not forget to exclude transpiled polyfill code from the Babel transpile:
`exclude: [ "node_modules/**" ],` or `exclude: [ "node_modules/abortcontroller-polyfill/**" ],`

### Usage example
```js
let abortController

// abort can be called as many times as you want (it run only ones)
const abort = () => abortController && abortController.abort()

const doFetch = () => {

abort() // abort the previous / ongoing call
abortController = new AbortController()

fetch('http://api.plos.org/search?q=title:DNA',
{ credentials: 'same-origin', signal: abortController.signal }).
then(response => {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`)
}
return response.json()
}).
then(data => {
console.log('REQUEST FINISHED')
console.dir(data)
}).
catch(error => {
if (error.name === 'AbortError') {
console.log('REQUEST ABORTED')
} else {
console.error(`REQUEST FAILED: ${error ? error.message : ''}`)
}

/* The alternative way to distinct the AbortError */
if (abortController.signal.aborted) {
console.log('REQUEST ABORTED')
} else {
console.error(`REQUEST FAILED: ${error ? error.message : ''}`)
}
})
}

doFetch()
setTimeout(abort, 5000)
```

P.S. @developit, @prk3, @simonbuerger, @prabirshrestha PR, tests and discussion are welcome :smile:

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.