Skip to content

Commit 66fa857

Browse files
committed
fetch draft
1 parent 440f408 commit 66fa857

File tree

18 files changed

+54282
-0
lines changed

18 files changed

+54282
-0
lines changed

7-network/1-fetch/article.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
2+
# Fetch
3+
4+
Method `fetch()` is the modern way of sending requests over HTTP.
5+
6+
Is evolved for several years and continues to improve, right now its support is pretty solid among browsers.
7+
8+
The basic syntax is:
9+
10+
```js
11+
let promise = fetch(url, [params])
12+
```
13+
14+
- **`url`** -- the URL to access.
15+
- **`params`** -- optional parameters: method, headers etc.
16+
17+
The browser starts the request right away and returns a `promise`.
18+
19+
Accepting a response is usually a two-step procedure.
20+
21+
**The `promise` resolves as soon as the server responded with headers.**
22+
23+
So we can access the headers, we know HTTP status, whether the response is successful, but don't have the body yet.
24+
25+
We need to wait for the response body additionally, like this:
26+
27+
```js run async
28+
let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits');
29+
*!*
30+
let commits = await response.json();
31+
*/!*
32+
alert(commits[0].author.login);
33+
```
34+
35+
Or, using pure promises:
36+
37+
```js run
38+
fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits')
39+
.then(response => response.json())
40+
.then(commits => alert(commits[0].author.login));
41+
```
42+
43+
A `fetch` resolves with `response` -- an object of the built-in [Response](https://fetch.spec.whatwg.org/#response-class) class.
44+
45+
The main response properties are:
46+
- **`ok`** -- boolean, `true` if the HTTP status code is 200-299.
47+
- **`status`** -- HTTP status code.
48+
- **`headers`** -- HTTP headers, a Map-like object.
49+
50+
## How to get headers?
51+
52+
We can iterate over headers the same way as over a `Map`:
53+
54+
```js run async
55+
let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits');
56+
57+
// have headers already
58+
for (let [key, value] of response.headers) {
59+
alert(`${key} = ${value}`);
60+
}
61+
62+
if (response.ok) {
63+
// wait for the body
64+
let json = await response.json();
65+
} else {
66+
// handle error
67+
}
68+
```
69+
70+
## How to get response?
71+
72+
`Response` allows to access the body in multiple formats, using following promises:
73+
74+
- **`json()`** -- parse as JSON object,
75+
- **`text()`** -- as text,
76+
- **`formData()`** -- as formData (form/multipart encoding),
77+
- **`blob()`** -- as Blob (for binary data),
78+
- **`arrayBuffer()`** -- as ArrayBuffer (for binary data),
79+
- `response.body` is a [ReadableStream](https://streams.spec.whatwg.org/#rs-class) object, it allows to read the body chunk-by-chunk.
80+
81+
We already saw how to get the response as json.
82+
83+
As text:
84+
```js
85+
let text = await response.text();
86+
```
87+
88+
For the binary example, let's download an image and show it:
89+
90+
```js async run
91+
let response = await fetch('/article/fetch/logo-fetch.svg');
92+
93+
*!*
94+
let blob = await response.blob(); // download as Blob object
95+
*/!*
96+
97+
// create <img> with it
98+
let img = document.createElement('img');
99+
img.src = URL.createObjectURL(blob);
100+
101+
// show it for 2 seconds
102+
document.body.append(img);
103+
img.style = 'position:fixed;top:10px;left:10px;width:100px';
104+
setTimeout(() => img.remove(), 2000);
105+
```
106+
107+
## The full syntax
108+
109+
The second argument provides a lot of flexibility to `fetch` syntax.
110+
111+
Here's the full list of possible options with default values (alternatives commented out):
112+
113+
```js
114+
let promise = fetch(url, {
115+
method: "GET", // POST, PUT, DELETE, etc.
116+
headers: {
117+
"Content-Type": "text/plain;charset=UTF-8"
118+
},
119+
destination: "", // audio, audioworklet, document, embed, font...
120+
referrer: "about:client", // "" for no-referrer, or an url from the current origin
121+
referrerPolicy: "", // no-referrer, no-referrer-when-downgrade, same-origin...
122+
mode: "cors", // same-origin, no-cors, navigate, or websocket
123+
credentials: "same-origin", // omit, include
124+
cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
125+
redirect: "follow", // manual, error
126+
integrity: "" // a hash, like "sha256-abcdef1234567890"
127+
keepalive: false // true
128+
body: "string" // FormData, Blob, BufferSource, or URLSearchParams
129+
})
130+
131+
132+
133+
## How to track progress?
134+
135+
To track download progress, we need to use `response.body`.
136+
137+
It's a "readable stream" - a special object that provides access chunk-by-chunk.
138+
139+
Here's the code to do this:
140+
141+
```js
142+
const reader = response.body.getReader();
143+
144+
while(true) {
145+
// done is true for the last chunk
146+
// value is Uint8Array of bytes
147+
const chunk = await reader.read();
148+
149+
if (chunk.done) {
150+
break;
151+
}
152+
153+
console.log(`Received ${chunk.value.length} bytes`)
154+
}
155+
```
156+
157+
We do the infinite loop, while `await reader.read()` returns response chunks.
158+
159+
A chunk has two properties:
160+
- **`done`** -- true when the reading is complete.
161+
- **`value`** -- a typed array of bytes: `Uint8Array`.
162+
163+
The full code to get response and log the progress:
164+
165+
```js run async
166+
// Step 1: start the request and obtain a reader
167+
let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits?per_page=100');
168+
169+
const reader = response.body.getReader();
170+
171+
// Step 2: get total length
172+
const contentLength = +response.headers.get('Content-Length');
173+
174+
// Step 3: read the data
175+
let receivedLength = 0;
176+
let chunks = [];
177+
while(true) {
178+
const {done, value} = await reader.read();
179+
180+
if (done) {
181+
break;
182+
}
183+
184+
chunks.push(value);
185+
receivedLength += value.length;
186+
187+
console.log(`Received ${receivedLength} of ${contentLength}`)
188+
}
189+
190+
// Step 4: join chunks into result
191+
let chunksAll = new Uint8Array(receivedLength); // (4.1)
192+
let position = 0;
193+
for(let chunk of chunks) {
194+
chunksAll.set(chunk, position); // (4.2)
195+
position += chunk.length;
196+
}
197+
198+
// Step 5: decode
199+
let result = new TextDecoder("utf-8").decode(chunksMerged);
200+
let commits = JSON.parse(result);
201+
202+
// We're done!
203+
alert(commits[0].author.login);
204+
```
205+
206+
Let's explain that step-by-step:
207+
208+
1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`.
209+
210+
Please note, we can't use both these methods to read the same response. Either use a reader or a response method to get the result.
211+
2. Prior to reading, we can figure out the full response length by its `Content-Length` header.
212+
213+
It may be absent for cross-domain requests (as in the example) and, well, technically a server doesn't have to set it. But usually it's at place.
214+
3. Now `await reader.read()` until it's done.
215+
216+
We gather the `chunks` in the array. That's important, because after the response is consumed, we won't be able to "re-read" it using `response.json()` or another way (you can try, there'll be an error).
217+
4. At the end, we have `chunks` -- an array of `Uint8Array` byte chunks. We need to join them into a single result. Unfortunately, there's no single method that concatenates those.
218+
1. We create `new Uint8Array(receivedLength)` -- a same-type array with the combined length.
219+
2. Then use `.set(chunk, position)` method that copies each `chunk` at the given `position` (one by one) in the resulting array.
220+
5. We have the result in `chunksAll`. It's a byte array though, not a string.
221+
222+
To create a string, we need to interpret these bytes. The built-in `TextEncoder` does exactly that. Then we can `JSON.parse` it.
223+
224+
What if it were a binary file? We could make a blob of it:
225+
```js
226+
let blob = new Blob([chunksAll.buffer]);
227+
```
228+
229+
```js run async
230+
let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits?per_page=100');
231+
232+
const contentLength = +response.headers.get('Content-Length');
233+
234+
const reader = response.body.getReader();
235+
236+
let receivedLength = 0;
237+
let chunks = [];
238+
while(true) {
239+
const chunk = await reader.read();
240+
241+
if (chunk.done) {
242+
console.log("done!");
243+
break;
244+
}
245+
246+
chunks.push(chunk.value);
247+
248+
receivedLength += chunk.value.length;
249+
console.log(`${receivedLength}/${contentLength} received`)
250+
}
251+
252+
let chunksMerged = new Uint8Array(receivedLength);
253+
let length = 0;
254+
for(let chunk of chunks) {
255+
chunksMerged.set(chunk, length);
256+
length += chunk.length;
257+
}
258+
259+
let result = new TextDecoder("utf-8").decode(chunksMerged);
260+
console.log(JSON.parse(result));
261+
```

7-network/1-fetch/logo-fetch.svg

Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!doctype html>
2+
<script>
3+
(async () {
4+
5+
const response = await fetch('long.txt');
6+
const reader = response.body.getReader();
7+
8+
const contentLength = +response.headers.get('Content-Length');
9+
let receivedLength = 0;
10+
let chunks = [];
11+
while(true) {
12+
const chunk = await reader.read();
13+
14+
if (chunk.done) {
15+
console.log("done!");
16+
break;
17+
}
18+
19+
chunks.push(chunk.value);
20+
receivedLength += chunk.value.length;
21+
console.log(`${receivedLength}/${contentLength} received`)
22+
}
23+
24+
25+
let chunksMerged = new Uint8Array(receivedLength);
26+
let length = 0;
27+
for(let chunk of chunks) {
28+
chunksMerged.set(chunk, length);
29+
length += chunk.length;
30+
}
31+
32+
let result = new TextDecoder("utf-8").decode(chunksMerged);
33+
console.log(result);
34+
})();
35+
36+
</script>

0 commit comments

Comments
 (0)