This commit is contained in:
Ilya Kantor 2019-07-03 17:19:00 +03:00
parent 94c83e9e50
commit cc5213b09e
79 changed files with 1341 additions and 357 deletions

View file

@ -0,0 +1,24 @@
async function getUsers(names) {
let jobs = [];
for(let name of names) {
let job = fetch(`https://api.github.com/users/${name}`).then(
successResponse => {
if (successResponse.status != 200) {
return null;
} else {
return successResponse.json();
}
},
failResponse => {
return null;
}
);
jobs.push(job);
}
let results = await Promise.all(jobs);
return results;
}

View file

@ -0,0 +1,4 @@
async function getUsers(names) {
/* your code */
}

View file

@ -0,0 +1,10 @@
describe("getUsers", function() {
it("gets users from GitHub", async function() {
let users = await getUsers(['iliakan', 'remy', 'no.such.users']);
assert.equal(users[0].login, 'iliakan');
assert.equal(users[1].login, 'remy');
assert.equal(users[2], null);
});
});

View file

@ -0,0 +1,41 @@
To fetch a user we need:
1. `fetch('https://api.github.com/users/USERNAME')`.
2. If the response has status `200`, call `.json()` to read the JS object.
If a `fetch` fails, or the response has non-200 status, we just return `null` in the resulting arrray.
So here's the code:
```js demo
async function getUsers(names) {
let jobs = [];
for(let name of names) {
let job = fetch(`https://api.github.com/users/${name}`).then(
successResponse => {
if (successResponse.status != 200) {
return null;
} else {
return successResponse.json();
}
},
failResponse => {
return null;
}
);
jobs.push(job);
}
let results = await Promise.all(jobs);
return results;
}
```
Please note: `.then` call is attached directly to `fetch`, so that when we have the response, it doesn't wait for other fetches, but starts to read `.json()` immediately.
If we used `await Promise.all(names.map(name => fetch(...)))`, and call `.json()` on the results, then it would wait for all fetches to respond. By adding `.json()` directly to each `fetch`, we ensure that individual fetches start reading data as JSON without waiting for each other.
That's an example of how low-level `Promise` API can still be useful even if we mainly use `async/await`.

View file

@ -0,0 +1,12 @@
# Fetch users from GitHub
Create an async function `getUsers(names)`, that gets an array of GitHub user names, fetches them from GitHub and returns an array of GitHub users instead.
The GitHub url with user informaiton is: `https://api.github.com/users/USERNAME`.
There's a test example in the sandbox.
Important details:
1. There should be one `fetch` request per user. And requests shouldn't wait for each other. So that the data arrives as soon as possible.
2. If a request fails, or if there's no such user, the function should return `null` in the resulting array.

View file

@ -0,0 +1,305 @@
# Fetch
JavaScript can send network requests to the server and load new information whenever is needed.
For example, we can:
- Submit an order,
- Load user information,
- Receive latest updates from the server,
- ...etc.
...And all of that without reloading the page!
There's an umbrella term "AJAX" (abbreviated <b>A</b>synchronous <b>J</b>avascript <b>A</b>nd <b>X</b>ml) for that. We don't have to use XML though: the term comes from old times, that's why it's here.
There are multiple ways to send a network request and get information from the server.
The `fetch()` method is modern and versatile, so we'll start with it. It evolved for several years and continues to improve, right now the support is pretty solid among browsers.
The basic syntax is:
```js
let promise = fetch(url, [options])
```
- **`url`** -- the URL to access.
- **`options`** -- optional parameters: method, headers etc.
The browser starts the request right away and returns a `promise`.
Getting a response is usually a two-stage process.
**First, the `promise` resolves with an object of the built-in [Response](https://fetch.spec.whatwg.org/#response-class) class as soon as the server responds with headers.**
So we can check HTTP status, to see whether it is successful or not, check headers, but don't have the body yet.
The promise rejects if the `fetch` was unable to make HTTP-request, e.g. network problems, or there's no such site. HTTP-errors, even such as 404 or 500, are considered a normal flow.
We can see them in response properties:
- **`ok`** -- boolean, `true` if the HTTP status code is 200-299.
- **`status`** -- HTTP status code.
For example:
```js
let response = await fetch(url);
if (response.ok) { // if HTTP-status is 200-299
// get the response body (see below)
let json = await response.json();
} else {
alert("HTTP-Error: " + response.status);
}
```
**Second, to get the response body, we need to use an additional method call.**
`Response` provides multiple promise-based methods to access the body in various formats:
- **`response.json()`** -- parse the response as JSON object,
- **`response.text()`** -- return the response as text,
- **`response.formData()`** -- return the response as `FormData` object (form/multipart encoding, explained in the [next chapter](info:formdata)),
- **`response.blob()`** -- return the response as [Blob](info:blob) (binary data with type),
- **`response.arrayBuffer()`** -- return the response as [ArrayBuffer](info:arraybuffer-binary-arrays) (pure binary data),
- additionally, `response.body` is a [ReadableStream](https://streams.spec.whatwg.org/#rs-class) object, it allows to read the body chunk-by-chunk, we'll see an example later.
For instance, here we get a JSON-object with latest commits from GitHub:
```js run async
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
*!*
let commits = await response.json(); // read response body and parse as JSON
*/!*
alert(commits[0].author.login);
```
Or, the same using pure promises syntax:
```js run
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
```
To get the text, `await response.text()` instead of `.json()`:
```js run async
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
let text = await response.text(); // read response body as text
alert(text.slice(0, 80) + '...');
```
As a show-case for reading in binary format, let's fetch and show an image (see chapter [Blob](info:blob) for details about operations on blobs):
```js async run
let response = await fetch('/article/fetch/logo-fetch.svg');
*!*
let blob = await response.blob(); // download as Blob object
*/!*
// create <img> for it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);
// show it
img.src = URL.createObjectURL(blob);
setTimeout(() => { // hide after three seconds
img.remove();
URL.revokeObjectURL(img.src);
}, 3000);
```
````warn
We can choose only one body-parsing method.
If we got the response with `response.text()`, then `response.json()` won't work, as the body content has already been processed.
```js
let text = await response.text(); // response body consumed
let parsed = await response.json(); // fails (already consumed)
````
## Headers
There's a Map-like headers object in `response.headers`.
We can get individual headers or iterate over them:
```js run async
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
// get one header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
// iterate over all headers
for (let [key, value] of response.headers) {
alert(`${key} = ${value}`);
}
```
To set a header, we can use the `headers` option, like this:
```js
let response = fetch(protectedUrl, {
headers: {
Authentication: 'abcdef'
}
});
```
...But there's a list of [forbidden HTTP headers](https://fetch.spec.whatwg.org/#forbidden-header-name) that we can't set:
- `Accept-Charset`, `Accept-Encoding`
- `Access-Control-Request-Headers`
- `Access-Control-Request-Method`
- `Connection`
- `Content-Length`
- `Cookie`, `Cookie2`
- `Date`
- `DNT`
- `Expect`
- `Host`
- `Keep-Alive`
- `Origin`
- `Referer`
- `TE`
- `Trailer`
- `Transfer-Encoding`
- `Upgrade`
- `Via`
- `Proxy-*`
- `Sec-*`
These headers ensure proper and safe HTTP, so they are controlled exclusively by the browser.
## POST requests
To make a `POST` request, or a request with another method, we need to use `fetch` options:
- **`method`** -- HTTP-method, e.g. `POST`,
- **`body`** -- one of:
- a string (e.g. JSON),
- `FormData` object, to submit the data as `form/multipart`,
- `Blob`/`BufferSource` to send binary data,
- [URLSearchParams](info:url), to submit the data as `x-www-form-urlencoded`, rarely used.
Let's see examples.
For example, this code submits `user` object as JSON:
```js run async
let user = {
name: 'John',
surname: 'Smith'
};
*!*
let response = await fetch('/article/fetch/post/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});
*/!*
let result = await response.json();
alert(result.message);
```
Please note, if the body is a string, then `Content-Type` is set to `text/plain;charset=UTF-8` by default. So we use `headers` option to send `application/json` instead, that's the correct content type for JSON-encoded data.
## Sending an image
We can also submit binary data directly using `Blob` or `BufferSource`.
For example, here's a `<canvas>` where we can draw by moving a mouse. A click on the "submit" button sends the image to server:
```html run autorun height="90"
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<input type="button" value="Submit" onclick="submit()">
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
async function submit() {
let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
let response = await fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
});
let result = await response.json();
alert(result.message);
}
</script>
</body>
```
Here we also didn't need to set `Content-Type` manually, because a `Blob` object has a built-in type (here `image/png`, as generated by `toBlob`).
The `submit()` function can be rewritten without `async/await` like this:
```js
function submit() {
canvasElem.toBlob(function(blob) {
fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
})
.then(response => response.json())
.then(result => alert(JSON.stringify(result, null, 2)))
}, 'image/png');
}
```
## Summary
A typical fetch request consists of two `awaits`:
```js
let response = await fetch(url, options); // resolves with response headers
let result = await response.json(); // read body as json
```
Or, promise-style:
```js
fetch(url, options)
.then(response => response.json())
.then(result => /* process result */)
```
Response properties:
- `response.status` -- HTTP code of the response,
- `response.ok` -- `true` is the status is 200-299.
- `response.headers` -- Map-like object with HTTP headers.
Methods to get response body:
- **`response.json()`** -- parse the response as JSON object,
- **`response.text()`** -- return the response as text,
- **`response.formData()`** -- return the response as `FormData` object (form/multipart encoding, see the next chapter),
- **`response.blob()`** -- return the response as [Blob](info:blob) (binary data with type),
- **`response.arrayBuffer()`** -- return the response as [ArrayBuffer](info:arraybuffer-binary-arrays) (pure binary data),
Fetch options so far:
- `method` -- HTTP-method,
- `headers` -- an object with request headers (not any header is allowed),
- `body` -- `string`, `FormData`, `BufferSource`, `Blob` or `UrlSearchParams` object to send.
In the next chapters we'll see more options and use cases.

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="#fff" stroke="#3c790a" stroke-width="10"/>
<path d="m34,55a60,60,0,0,0,20,-20a6,10,0,0,1,13,-1a10,6,0,0,1,-1,13a60,60,0,0,0,-20,20a6,10,0,0,1,-13,1a10,6,0,0,1,1,-13" fill="#3c790a"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View file

@ -0,0 +1,34 @@
const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
const getRawBody = require('raw-body')
const Router = require('koa-router');
let router = new Router();
router.post('/user', async (ctx) => {
ctx.body = {
message: "User saved."
};
});
router.post('/image', async (ctx) => {
let body = await getRawBody(ctx.req, {
limit: '1mb'
});
ctx.body = {
message: `Image saved, size:${body.length}.`
};
});
app
.use(bodyParser())
.use(router.routes())
.use(router.allowedMethods());
if (!module.parent) {
http.createServer(app.callback()).listen(8080);
} else {
exports.accept = app.callback();
}