Merge branch 'refactor'
361
5-network/01-fetch-basics/article.md
Normal file
|
@ -0,0 +1,361 @@
|
|||
|
||||
# Fetch: Basics
|
||||
|
||||
Method `fetch()` is the modern way of sending requests over HTTP.
|
||||
|
||||
Is 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.
|
||||
|
||||
**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);
|
||||
}
|
||||
```
|
||||
|
||||
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),
|
||||
- **`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/iliakan/javascript-tutorial-en/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/iliakan/javascript-tutorial-en/commits')
|
||||
.then(response => response.json())
|
||||
.then(commits => alert(commits[0].author.login));
|
||||
```
|
||||
|
||||
To get the text:
|
||||
```js
|
||||
let text = await response.text();
|
||||
```
|
||||
|
||||
And for the binary example, 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 two seconds
|
||||
img.remove();
|
||||
URL.revokeObjectURL(img.src);
|
||||
}, 2000);
|
||||
```
|
||||
|
||||
````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/iliakan/javascript-tutorial-en/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.
|
||||
|
||||
## Submit JSON
|
||||
|
||||
This code submits a `user` object as JSON:
|
||||
|
||||
```js run async
|
||||
let user = {
|
||||
name: 'John',
|
||||
surname: 'Smith'
|
||||
};
|
||||
|
||||
*!*
|
||||
let response = await fetch('/article/fetch-basics/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.
|
||||
|
||||
## Submit a form
|
||||
|
||||
Let's do the same with an HTML `<form>`.
|
||||
|
||||
|
||||
```html run
|
||||
<form id="formElem">
|
||||
<input type="text" name="name" value="John">
|
||||
<input type="text" name="surname" value="Smith">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
let response = await fetch('/article/fetch-basics/post/user', {
|
||||
method: 'POST',
|
||||
*!*
|
||||
body: new FormData(formElem)
|
||||
*/!*
|
||||
});
|
||||
|
||||
let result = await response.json();
|
||||
|
||||
alert(result.message);
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
Here [FormData](https://xhr.spec.whatwg.org/#formdata) automatically encodes the form, `<input type="file">` fields are handled also, and sends it using `Content-Type: form/multipart`.
|
||||
|
||||
## Submit 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-basics/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-basics/post/image', {
|
||||
method: 'POST',
|
||||
body: blob
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => alert(JSON.stringify(result, null, 2)))
|
||||
}, 'image/png');
|
||||
}
|
||||
```
|
||||
|
||||
## Custom FormData with image
|
||||
|
||||
In practice though, it's often more convenient to send an image as a part of the form, with additional fields, such as "name" and other metadata.
|
||||
|
||||
Also, servers are usually more suited to accept multipart-encoded forms, rather than raw binary data.
|
||||
|
||||
```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 formData = new FormData();
|
||||
formData.append("name", "myImage");
|
||||
formData.append("image", blob);
|
||||
*/!*
|
||||
|
||||
let response = await fetch('/article/fetch-basics/post/image-form', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
let result = await response.json();
|
||||
alert(result.message);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
Now, from the server standpoint, the image is a "file" in the form.
|
||||
|
||||
## 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),
|
||||
- **`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/UrlSearchParams data to submit.
|
||||
|
||||
In the next chapters we'll see more options and use cases.
|
4
5-network/01-fetch-basics/logo-fetch.svg
Normal 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 |
57
5-network/01-fetch-basics/post.view/server.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
const Koa = require('koa');
|
||||
const app = new Koa();
|
||||
const bodyParser = require('koa-bodyparser');
|
||||
const getRawBody = require('raw-body')
|
||||
const busboy = require('async-busboy');
|
||||
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}.`
|
||||
};
|
||||
});
|
||||
|
||||
router.post('/image-form', async (ctx) => {
|
||||
|
||||
let files = [];
|
||||
const { fields } = await busboy(ctx.req, {
|
||||
onFile(fieldname, file, filename, encoding, mimetype) {
|
||||
// read all file stream to continue
|
||||
getRawBody(file, { limit: '1mb'}).then(body => {
|
||||
files.push({
|
||||
fieldname,
|
||||
filename,
|
||||
length: body.length
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
message: `Image saved, name: ${fields.name}, size:${files[0].length}.`
|
||||
};
|
||||
});
|
||||
|
||||
app
|
||||
.use(bodyParser())
|
||||
.use(router.routes())
|
||||
.use(router.allowedMethods());
|
||||
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(app.callback()).listen(8080);
|
||||
} else {
|
||||
exports.accept = app.callback();
|
||||
}
|
104
5-network/02-fetch-progress/article.md
Normal file
|
@ -0,0 +1,104 @@
|
|||
|
||||
# Fetch: Download progress
|
||||
|
||||
Fetch allows to track download progress, but not upload progress.
|
||||
|
||||
Please note: there's currently no way for fetch to track upload progress. For that purpose, please use [XMLHttpRequest](info:xmlhttprequest).
|
||||
|
||||
To track download progress, we can use `response.body` property. It's a "readable stream" -- a special object that provides body chunk-by-chunk, as it comes, so we can see how much is available at the moment.
|
||||
|
||||
Here's the sketch of code that uses it to read response:
|
||||
|
||||
```js
|
||||
// instead of response.json() and other methods
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// infinite loop while the body is downloading
|
||||
while(true) {
|
||||
// done is true for the last chunk
|
||||
// value is Uint8Array of the chunk bytes
|
||||
const {done, value} = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`Received ${value.length} bytes`)
|
||||
}
|
||||
```
|
||||
|
||||
So, we loop, while `await reader.read()` returns response chunks.
|
||||
|
||||
A chunk has two properties:
|
||||
- **`done`** -- true when the reading is complete.
|
||||
- **`value`** -- a typed array of bytes: `Uint8Array`.
|
||||
|
||||
To log the progress, we just need to count chunks.
|
||||
|
||||
Here's the full code to get response and log the progress, more explanations follow:
|
||||
|
||||
```js run async
|
||||
// Step 1: start the fetch and obtain a reader
|
||||
let response = await fetch('https://api.github.com/repos/iliakan/javascript-tutorial-en/commits?per_page=100');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// Step 2: get total length
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
|
||||
// Step 3: read the data
|
||||
let receivedLength = 0; // length at the moment
|
||||
let chunks = []; // array of received binary chunks (comprises the body)
|
||||
while(true) {
|
||||
const {done, value} = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
console.log(`Received ${receivedLength} of ${contentLength}`)
|
||||
}
|
||||
|
||||
// Step 4: concatenate chunks into single Uint8Array
|
||||
let chunksAll = new Uint8Array(receivedLength); // (4.1)
|
||||
let position = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksAll.set(chunk, position); // (4.2)
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
// Step 5: decode into a string
|
||||
let result = new TextDecoder("utf-8").decode(chunksAll);
|
||||
|
||||
// We're done!
|
||||
let commits = JSON.parse(result);
|
||||
alert(commits[0].author.login);
|
||||
```
|
||||
|
||||
Let's explain that step-by-step:
|
||||
|
||||
1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`.
|
||||
|
||||
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.
|
||||
2. Prior to reading, we can figure out the full response length from the `Content-Length` header.
|
||||
|
||||
It may be absent for cross-domain requests (see chapter <info:fetch-crossorigin>) and, well, technically a server doesn't have to set it. But usually it's at place.
|
||||
3. Call `await reader.read()` until it's done.
|
||||
|
||||
We gather response `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).
|
||||
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, so there's some code to do that:
|
||||
1. We create `new Uint8Array(receivedLength)` -- a same-typed array with the combined length.
|
||||
2. Then use `.set(chunk, position)` method to copy each `chunk` one after another in the resulting array.
|
||||
5. We have the result in `chunksAll`. It's a byte array though, not a string.
|
||||
|
||||
To create a string, we need to interpret these bytes. The built-in [TextDecoder](info:text-decoder) does exactly that. Then we can `JSON.parse` it.
|
||||
|
||||
What if we need binary content instead of JSON? That's even simpler. Instead of steps 4 and 5, we could make a blob of all chunks:
|
||||
```js
|
||||
let blob = new Blob(chunks);
|
||||
```
|
||||
|
||||
Once again, please note, that's not for upload progress (no way now), only for download progress.
|
4
5-network/02-fetch-progress/logo-fetch.svg
Normal 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 |
36
5-network/02-fetch-progress/progress.view/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
(async () {
|
||||
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(chunk.value);
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
|
||||
let chunksMerged = new Uint8Array(receivedLength);
|
||||
let length = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksMerged.set(chunk, length);
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
let result = new TextDecoder("utf-8").decode(chunksMerged);
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
</script>
|
20465
5-network/02-fetch-progress/progress.view/long.txt
Normal file
110
5-network/03-fetch-abort/article.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
|
||||
# Fetch: Abort
|
||||
|
||||
Aborting a `fetch` is a little bit tricky. Remember, `fetch` returns a promise. And Javascript generally has no concept of "aborting" a promise. So how can we cancel a fetch?
|
||||
|
||||
There's a special built-in object for such purposes: `AbortController`.
|
||||
|
||||
The usage is pretty simple:
|
||||
|
||||
- Step 1: create a controller:
|
||||
|
||||
```js
|
||||
let controller = new AbortController();
|
||||
```
|
||||
|
||||
The controller is an extremely simple object. It has a single method `abort()`, and a single property `signal`, that generates an event when `abort()` is called:
|
||||
|
||||
We can even use it without `fetch` for our own purposes, like this:
|
||||
|
||||
```js run
|
||||
let controller = new AbortController();
|
||||
let signal = controller.signal;
|
||||
|
||||
// triggers when controller.abort() is called
|
||||
signal.addEventListener('abort', () => alert("abort!"));
|
||||
|
||||
controller.abort(); // abort!
|
||||
|
||||
alert(signal.aborted); // true (after abort)
|
||||
```
|
||||
|
||||
- Step 2: pass the `signal` property to `fetch` option:
|
||||
|
||||
```js
|
||||
let controller = new AbortController();
|
||||
fetch(url, {
|
||||
signal: controller.signal
|
||||
});
|
||||
```
|
||||
|
||||
Now `fetch` listens to the signal.
|
||||
|
||||
- Step 3: to abort, call `controller.abort()`:
|
||||
|
||||
```js
|
||||
controller.abort();
|
||||
```
|
||||
|
||||
We're done: `fetch` gets the event from `signal` and aborts the request.
|
||||
|
||||
When a fetch is aborted, its promise rejects with an error named `AbortError`, so we should handle it:
|
||||
|
||||
```js run async
|
||||
// abort in 1 second
|
||||
let controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 1000);
|
||||
|
||||
try {
|
||||
let response = await fetch('/article/fetch-abort/demo/hang', {
|
||||
signal: controller.signal
|
||||
});
|
||||
} catch(err) {
|
||||
if (err.name == 'AbortError') { // handle abort()
|
||||
alert("Aborted!");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`AbortController` is scalable, it allows to cancel multiple fetches at once.**
|
||||
|
||||
For instance, here we fetch many `urls` in parallel, and the controller aborts them all:
|
||||
|
||||
```js
|
||||
let urls = [...]; // a list of urls to fetch in parallel
|
||||
|
||||
let controller = new AbortController();
|
||||
|
||||
let fetchJobs = urls.map(url => fetch(url, {
|
||||
signal: controller.signal
|
||||
}));
|
||||
|
||||
let results = await Promise.all(fetchJobs);
|
||||
|
||||
// from elsewhere:
|
||||
// controller.abort() stops all fetches
|
||||
```
|
||||
|
||||
If wee have our own jobs, different from `fetch`, we can use a single `AbortController` to stop those, together with fetches.
|
||||
|
||||
|
||||
```js
|
||||
let urls = [...];
|
||||
let controller = new AbortController();
|
||||
|
||||
let ourJob = new Promise((resolve, reject) => {
|
||||
...
|
||||
controller.signal.addEventListener('abort', reject);
|
||||
});
|
||||
|
||||
let fetchJobs = urls.map(url => fetch(url, {
|
||||
signal: controller.signal
|
||||
}));
|
||||
|
||||
let results = await Promise.all([...fetchJobs, ourJob]);
|
||||
|
||||
// from elsewhere:
|
||||
// controller.abort() stops all fetches and ourJob
|
||||
```
|
21
5-network/03-fetch-abort/demo.view/server.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const Koa = require('koa');
|
||||
const app = new Koa();
|
||||
|
||||
const Router = require('koa-router');
|
||||
|
||||
let router = new Router();
|
||||
|
||||
router.get('/hang', async (ctx) => {
|
||||
await new Promise(() => {});
|
||||
});
|
||||
|
||||
app
|
||||
.use(router.routes())
|
||||
.use(router.allowedMethods());
|
||||
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(app.callback()).listen(8080);
|
||||
} else {
|
||||
exports.accept = app.callback();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
We need `Origin`, because sometimes `Referer` is absent. For instance, when we `fetch` HTTP-page from HTTPS (access less secure from more secure), then there's no `Referer`.
|
||||
|
||||
The [Content Security Policy](http://en.wikipedia.org/wiki/Content_Security_Policy) may forbid sending a `Referer`.
|
||||
|
||||
As we'll see, `fetch` also has options that prevent sending the `Referer` and even allow to change it (within the same site).
|
||||
|
||||
By specification, `Referer` is an optional HTTP-header.
|
||||
|
||||
Exactly because `Referer` is unreliable, `Origin` was invented. The browser guarantees correct `Origin` for cross-origin requests.
|
28
5-network/04-fetch-crossorigin/1-do-we-need-origin/task.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Why do we need Origin?
|
||||
|
||||
As you probably know, there's HTTP-header `Referer`, that usually contains an url of the page which initiated a network request.
|
||||
|
||||
For instance, when fetching `http://google.com` from `http://javascript.info/some/url`, the headers look like this:
|
||||
|
||||
```
|
||||
Accept: */*
|
||||
Accept-Charset: utf-8
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Connection: keep-alive
|
||||
Host: google.com
|
||||
*!*
|
||||
Origin: http://javascript.info
|
||||
Referer: http://javascript.info/some/url
|
||||
*/!*
|
||||
```
|
||||
|
||||
As you can see, both `Referer` and `Origin` are present.
|
||||
|
||||
The questions:
|
||||
|
||||
1. Why `Origin` is needed, if `Referer` has even more information?
|
||||
2. If it possible that there's no `Referer` or `Origin`, or it's incorrect?
|
370
5-network/04-fetch-crossorigin/article.md
Normal file
|
@ -0,0 +1,370 @@
|
|||
# Fetch: Cross-Origin Requests
|
||||
|
||||
If we make a `fetch` from an arbitrary web-site, that will probably fail.
|
||||
|
||||
The core concept here is *origin* -- a domain/port/protocol triplet.
|
||||
|
||||
Cross-origin requests -- those sent to another domain (even a subdomain) or protocol or port -- require special headers from the remote side. That policy is called "CORS": Cross-Origin Resource Sharing.
|
||||
|
||||
For instance, let's try fetching `http://example.com`:
|
||||
|
||||
```js run async
|
||||
try {
|
||||
await fetch('http://example.com');
|
||||
} catch(err) {
|
||||
alert(err); // Failed to fetch
|
||||
}
|
||||
```
|
||||
|
||||
Fetch fails, as expected.
|
||||
|
||||
## Why?
|
||||
|
||||
Because cross-origin restrictions protect the internet from evil hackers.
|
||||
|
||||
Seriously. Let's make a very brief historical digression.
|
||||
|
||||
For many years Javascript did not have any special methods to perform network requests.
|
||||
|
||||
**A script from one site could not access the content of another site.**
|
||||
|
||||
That simple, yet powerful rule was a foundation of the internet security. E.g. a script from the page `hacker.com` could not access user's mailbox at `gmail.com`. People felt safe.
|
||||
|
||||
But web developers demanded more power. A variety of tricks were invented to work around it.
|
||||
|
||||
One way to communicate with another server was to submit a `<form>` there. People submitted it into `<iframe>`, just to stay on the current page, like this:
|
||||
|
||||
```html
|
||||
<!-- form target -->
|
||||
<iframe name="iframe"></iframe>
|
||||
|
||||
<!-- a form could be dynamically generated and submited by Javascript -->
|
||||
<form target="iframe" method="POST" action="http://another.com/…">
|
||||
...
|
||||
</form>
|
||||
|
||||
```
|
||||
|
||||
- So, it was possible to make a GET/POST request to another site, even without networking methods.
|
||||
- But as it's forbidden to access the content of an `<iframe>` from another site, it wasn't possible to read the response.
|
||||
|
||||
So, `<form>` allowed to submit the data anywhere, but the response content was unaccessible.
|
||||
|
||||
Another trick was to use a `<script src="http://another.com/…">` tag. A script could have any `src`, from any domain. But again -- it was impossible to access the raw content of such script.
|
||||
|
||||
If `another.com` intended to expose data for this kind of access, then a so-called "JSONP (JSON with padding)" protocol was used.
|
||||
|
||||
Here's the flow:
|
||||
|
||||
1. First, in advance, we declare a global function to accept the data, e.g. `gotWeather`.
|
||||
2. Then we make a `<script>` and pass its name as the `callback` query parameter, e.g. `src="http://another.com/weather.json?callback=gotWeather"`.
|
||||
3. The remote server dynamically generates a response that wraps the data into `gotWeather(...)` call.
|
||||
4. As the script executes, `gotWeather` runs, and, as it's our function, we have the data.
|
||||
|
||||
Here's an example of the code to receive the data in JSONP:
|
||||
|
||||
```js run
|
||||
// 1. Declare the function to process the data
|
||||
function gotWeather({ temperature, humidity }) {
|
||||
alert(`temperature: ${temperature}, humidity: ${humidity}`);
|
||||
}
|
||||
|
||||
// 2. Pass its name as the ?callback parameter for the script
|
||||
let script = document.createElement('script');
|
||||
script.src = `https://cors.javascript.info/article/fetch-crossorigin/demo/script?callback=gotWeather`;
|
||||
document.body.append(script);
|
||||
|
||||
// 3. The expected answer from the server looks like this:
|
||||
/*
|
||||
gotWeather({
|
||||
temperature: 25,
|
||||
humidity: 78
|
||||
});
|
||||
*/
|
||||
```
|
||||
|
||||
|
||||
That works, and doesn't violate security, because both sides agreed to pass the data this way. And, when both sides agree, it's definitely not a hack. There are still services that provide such access, as it works even for very old browsers.
|
||||
|
||||
After a while, modern network methods appeared. At first, cross-origin requests were forbidden. But as a result of long discussions, cross-domain requests were allowed, in a way that does not add any capabilities unless explicitly allowed by the server.
|
||||
|
||||
## Simple requests
|
||||
|
||||
[Simple requests](http://www.w3.org/TR/cors/#terminology) must satisfy the following conditions:
|
||||
|
||||
1. [Simple method](http://www.w3.org/TR/cors/#simple-method): GET, POST or HEAD
|
||||
2. [Simple headers](http://www.w3.org/TR/cors/#simple-header) -- only allowed:
|
||||
- `Accept`,
|
||||
- `Accept-Language`,
|
||||
- `Content-Language`,
|
||||
- `Content-Type` with the value `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`.
|
||||
|
||||
Any other request is considered "non-simple". For instance, a request with `PUT` method or with an `API-Key` HTTP-header does not fit the limitations.
|
||||
|
||||
**The essential difference is that a "simple request" can be made with a `<form>` or a `<script>`, without any special methods.**
|
||||
|
||||
So, even a very old server should be ready to accept a simple request.
|
||||
|
||||
Contrary to that, requests with non-standard headers or e.g. method `DELETE` can't be created this way. For a long time Javascript was unable to do such requests. So an old server may assume that such requests come from a privileged source, "because a webpage is unable to send them".
|
||||
|
||||
When we try to make a non-simple request, the browser sends a special "preflight" request that asks the server -- does it agree to accept such cross-origin requests, or not?
|
||||
|
||||
And, unless the server explicitly confirms that with headers, a non-simple request is not sent.
|
||||
|
||||
Now we'll go into details. All of them serve a single purpose -- to ensure that new cross-origin capabilities are only accessible with an explicit permission from the server.
|
||||
|
||||
## CORS for simple requests
|
||||
|
||||
If a request is cross-origin, the browser always adds `Origin` header to it.
|
||||
|
||||
For instance, if we request `https://anywhere.com/request` from `https://javascript.info/page`, the headers will be like:
|
||||
|
||||
```
|
||||
GET /request
|
||||
Host: anywhere.com
|
||||
*!*
|
||||
Origin: https://javascript.info
|
||||
*/!*
|
||||
...
|
||||
```
|
||||
|
||||
As you can see, `Origin` contains exactly the origin (domain/protocol/port), without a path.
|
||||
|
||||
The server can inspect the `Origin` and, if it agrees to accept such a request, adds a special header `Access-Control-Allow-Origin` to the response. That header should contain the allowed origin (in our case `https://javascript.info`), or a star `*`. Then the response is successful, otherwise an error.
|
||||
|
||||
The browser plays the role of a trusted mediator here:
|
||||
1. It ensures that the corrent `Origin` is sent with a cross-domain request.
|
||||
2. If checks for correct `Access-Control-Allow-Origin` in the response, if it is so, then Javascript access, otherwise forbids with an error.
|
||||
|
||||

|
||||
|
||||
Here's an example of an "accepting" response:
|
||||
```
|
||||
200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
*!*
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
*/!*
|
||||
```
|
||||
|
||||
## Response headers
|
||||
|
||||
For cross-origin request, by default Javascript may only access "simple response headers":
|
||||
|
||||
- `Cache-Control`
|
||||
- `Content-Language`
|
||||
- `Content-Type`
|
||||
- `Expires`
|
||||
- `Last-Modified`
|
||||
- `Pragma`
|
||||
|
||||
Any other response header is forbidden.
|
||||
|
||||
```smart header="Please note: no `Content-Length`"
|
||||
Please note: there's no `Content-Length` header in the list!
|
||||
|
||||
So, if we're downloading something and would like to track the percentage of progress, then an additional permission is required to access that header (see below).
|
||||
```
|
||||
|
||||
To grant Javascript access to any other response header, the server must list it in the `Access-Control-Expose-Headers` header.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
200 OK
|
||||
Content-Type:text/html; charset=UTF-8
|
||||
Content-Length: 12345
|
||||
API-Key: 2c9de507f2c54aa1
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
*!*
|
||||
Access-Control-Expose-Headers: Content-Length,API-Key
|
||||
*/!*
|
||||
```
|
||||
|
||||
With such `Access-Control-Expose-Headers` header, the script is allowed to access `Content-Length` and `API-Key` headers of the response.
|
||||
|
||||
|
||||
## "Non-simple" requests
|
||||
|
||||
We can use any HTTP-method: not just `GET/POST`, but also `PATCH`, `DELETE` and others.
|
||||
|
||||
Some time ago no one could even assume that a webpage is able to do such requests. So there may exist webservices that treat a non-standard method as a signal: "That's not a browser". They can take it into account when checking access rights.
|
||||
|
||||
So, to avoid misunderstandings, any "non-simple" request -- that couldn't be done in the old times, the browser does not make such requests right away. Before it sends a preliminary, so-called "preflight" request, asking for permission.
|
||||
|
||||
A preflight request uses method `OPTIONS` and has no body.
|
||||
- `Access-Control-Request-Method` header has the requested method.
|
||||
- `Access-Control-Request-Headers` header provides a comma-separated list of non-simple HTTP-headers.
|
||||
|
||||
If the server agrees to serve the requests, then it should respond with status 200, without body.
|
||||
|
||||
- The response header `Access-Control-Allow-Method` must have the allowed method.
|
||||
- The response header `Access-Control-Allow-Headers` must have a list of allowed headers.
|
||||
- Additionally, the header `Access-Control-Max-Age` may specify a number of seconds to cache the permissions. So the browser won't have to send a preflight for subsequent requests that satisfy given permissions.
|
||||
|
||||

|
||||
|
||||
Let's see how it works step-by-step on example, for a cross-domain `PATCH` request (this method is often used to update data):
|
||||
|
||||
```js
|
||||
let response = await fetch('https://site.com/service.json', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'API-Key': 'secret'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
There are three reasons why the request is not simple (one is enough):
|
||||
- Method `PATCH`
|
||||
- `Content-Type` is not one of: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`.
|
||||
- Custom `API-Key` header.
|
||||
|
||||
### Step 1 (preflight request)
|
||||
|
||||
The browser, on its own, sends a preflight request that looks like this:
|
||||
|
||||
```
|
||||
OPTIONS /service.json
|
||||
Host: site.com
|
||||
Origin: https://javascript.info
|
||||
Access-Control-Request-Method: PATCH
|
||||
Access-Control-Request-Headers: Content-Type,API-Key
|
||||
```
|
||||
|
||||
- Method: `OPTIONS`.
|
||||
- The path -- exactly the same as the main request: `/service.json`.
|
||||
- Cross-origin special headers:
|
||||
- `Origin` -- the source origin.
|
||||
- `Access-Control-Request-Method` -- requested method.
|
||||
- `Access-Control-Request-Headers` -- a comma-separated list of "non-simple" headers.
|
||||
|
||||
### Step 2 (preflight response)
|
||||
|
||||
The server should respond with status 200 and headers:
|
||||
- `Access-Control-Allow-Method: PATCH`
|
||||
- `Access-Control-Allow-Headers: Content-Type,API-Key`.
|
||||
|
||||
That would allow future communication, otherwise an error is triggered.
|
||||
|
||||
If the server expects other methods and headers, makes sense to list them all at once, e.g:
|
||||
|
||||
```
|
||||
200 OK
|
||||
Access-Control-Allow-Methods: PUT,PATCH,DELETE
|
||||
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
|
||||
Access-Control-Max-Age: 86400
|
||||
```
|
||||
|
||||
Now the browser can see that `PATCH` is in the list of allowed methods, and both headers are in the list too, so it sends out the main request.
|
||||
|
||||
Besides, the preflight response is cached for time, specified by `Access-Control-Max-Age` header (86400 seconds, one day), so subsequent requests will not cause a preflight. Assuming that they fit the allowances, they will be sent directly.
|
||||
|
||||
### Step 3 (actual request)
|
||||
|
||||
When the preflight is successful, the browser now makes the real request. Here the flow is the same as for simple requests.
|
||||
|
||||
The real request has `Origin` header (because it's cross-origin):
|
||||
|
||||
```
|
||||
PATCH /service.json
|
||||
Host: site.com
|
||||
Content-Type: application/json
|
||||
API-Key: secret
|
||||
Origin: https://javascript.info
|
||||
```
|
||||
|
||||
### Step 4 (actual response)
|
||||
|
||||
The server should not forget to add `Accept-Control-Allow-Origin` to the response. A successful preflight does not relieve from that:
|
||||
|
||||
```
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
```
|
||||
|
||||
Now everything's correct. Javascript is able to read the full response.
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
A cross-origin request by default does not bring any credentials (cookies or HTTP authentication).
|
||||
|
||||
That's uncommon for HTTP-requests. Usually, a request to `http://site.com` is accompanied by all cookies from that domain. But cross-domain requests made by Javascript methods are an exception.
|
||||
|
||||
For example, `fetch('http://another.com')` does not send any cookies, even those that belong to `another.com` domain.
|
||||
|
||||
Why?
|
||||
|
||||
That's because a request with credentials is much more powerful than an anonymous one. If allowed, it grants Javascript the full power to act and access sensitive information on behalf of a user.
|
||||
|
||||
Does the server really trust pages from `Origin` that much? A request with credentials needs an additional header to pass through.
|
||||
|
||||
To enable credentials, we need to add the option `credentials: "include"`, like this:
|
||||
|
||||
```js
|
||||
fetch('http://another.com', {
|
||||
credentials: "include"
|
||||
});
|
||||
```
|
||||
|
||||
Now `fetch` sends cookies originating from `another.com` with the request.
|
||||
|
||||
If the server wishes to accept the request with credentials, it should add a header `Access-Control-Allow-Credentials: true` to the response, in addition to `Access-Control-Allow-Origin`.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
200 OK
|
||||
Access-Control-Allow-Origin: https://javascript.info
|
||||
Access-Control-Allow-Credentials: true
|
||||
```
|
||||
|
||||
Please note: `Access-Control-Allow-Origin` is prohibited from using a star `*` for requests with credentials. There must be exactly the origin there, like above. That's an additional safety measure, to ensure that the server really knows who it trusts.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
Networking methods split cross-origin requests into two kinds: "simple" and all the others.
|
||||
|
||||
[Simple requests](http://www.w3.org/TR/cors/#terminology) must satisfy the following conditions:
|
||||
- Method: GET, POST or HEAD.
|
||||
- Headers -- we can set only:
|
||||
- `Accept`
|
||||
- `Accept-Language`
|
||||
- `Content-Language`
|
||||
- `Content-Type` to the value `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`.
|
||||
|
||||
The essential difference is that simple requests were doable since ancient times using `<form>` or `<script>` tags, while non-simple were impossible for browsers for a long time.
|
||||
|
||||
So, practical difference is that simple requests are sent right away, with `Origin` header, but for other ones the browser makes a preliminary "preflight" request, asking for permission.
|
||||
|
||||
**For simple requests:**
|
||||
|
||||
- → The browser sends `Origin` header with the origin.
|
||||
- ← For requests without credentials (default), the server should set:
|
||||
- `Access-Control-Allow-Origin` to `*` or same as `Origin`
|
||||
- ← For requests with credentials, the server should set:
|
||||
- `Access-Control-Allow-Origin` to `Origin`
|
||||
- `Access-Control-Allow-Credentials` to `true`
|
||||
|
||||
Additionally, if Javascript wants no access non-simple response headers:
|
||||
- `Cache-Control`
|
||||
- `Content-Language`
|
||||
- `Content-Type`
|
||||
- `Expires`
|
||||
- `Last-Modified`
|
||||
- `Pragma`
|
||||
|
||||
...Then the server should list the allowed ones in `Access-Control-Expose-Headers` header.
|
||||
|
||||
**For non-simple requests, a preliminary "preflight" request is issued before the requested one:**
|
||||
|
||||
- → The browser sends `OPTIONS` request to the same url, with headers:
|
||||
- `Access-Control-Request-Method` has requested method.
|
||||
- `Access-Control-Request-Headers` lists non-simple requested headers
|
||||
- ← The server should respond with status 200 and headers:
|
||||
- `Access-Control-Allow-Method` with a list of allowed methods,
|
||||
- `Access-Control-Allow-Headers` with a list of allowed headers,
|
||||
- `Access-Control-Max-Age` with a number of seconds to cache permissions.
|
||||
- Then the actual request is sent, the previous "simple" scheme is applied.
|
BIN
5-network/04-fetch-crossorigin/cors-gmail-messages.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
5-network/04-fetch-crossorigin/cors-gmail-messages@2x.png
Normal file
After Width: | Height: | Size: 88 KiB |
31
5-network/04-fetch-crossorigin/demo.view/index.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
|
||||
async function init() {
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
let result = await response.text();
|
||||
console.log(result);
|
||||
//const chunkCount = await read(reader);
|
||||
//console.log(`Finished! Received ${chunkCount} chunks.`);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
</script>
|
31
5-network/04-fetch-crossorigin/demo.view/server.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const Koa = require('koa');
|
||||
const app = new Koa();
|
||||
|
||||
const Router = require('koa-router');
|
||||
|
||||
let router = new Router();
|
||||
|
||||
router.get('/script', async (ctx) => {
|
||||
let callback = ctx.query.callback;
|
||||
|
||||
if (!callback) {
|
||||
ctx.throw(400, 'Callback required!');
|
||||
}
|
||||
|
||||
ctx.type = 'application/javascript';
|
||||
ctx.body = `${callback}({
|
||||
temperature: 25,
|
||||
humidity: 78
|
||||
})`;
|
||||
});
|
||||
|
||||
app
|
||||
.use(router.routes())
|
||||
.use(router.allowedMethods());
|
||||
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(app.callback()).listen(8080);
|
||||
} else {
|
||||
exports.accept = app.callback();
|
||||
}
|
BIN
5-network/04-fetch-crossorigin/xhr-another-domain.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
5-network/04-fetch-crossorigin/xhr-another-domain@2x.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
5-network/04-fetch-crossorigin/xhr-preflight.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
5-network/04-fetch-crossorigin/xhr-preflight@2x.png
Normal file
After Width: | Height: | Size: 136 KiB |
190
5-network/05-fetch-api/article.md
Normal file
|
@ -0,0 +1,190 @@
|
|||
|
||||
# Fetch API
|
||||
|
||||
So far, we know quite a bit about fetch.
|
||||
|
||||
Now let's see the rest of API, to cover all its abilities.
|
||||
|
||||
Here's the full list of all possible fetch options with their default values (alternatives in comments):
|
||||
|
||||
```js
|
||||
let promise = fetch(url, {
|
||||
method: "GET", // POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
"Content-Type": "text/plain;charset=UTF-8" // for a string body, depends on body
|
||||
},
|
||||
body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams
|
||||
referrer: "about:client", // "" for no-referrer, or an url from the current origin
|
||||
referrerPolicy: "no-referrer-when-downgrade", // no-referrer, origin, same-origin...
|
||||
mode: "cors", // same-origin, no-cors
|
||||
credentials: "same-origin", // omit, include
|
||||
cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
|
||||
redirect: "follow", // manual, error
|
||||
integrity: "", // a hash, like "sha256-abcdef1234567890"
|
||||
keepalive: false, // true
|
||||
signal: undefined, // AbortController to abort request
|
||||
window: window // null
|
||||
});
|
||||
```
|
||||
|
||||
An impressive list, right?
|
||||
|
||||
We fully covered `method`, `headers` and `body` in the chapter <info:fetch-basics>.
|
||||
|
||||
The `signal` option is covered in <info:fetch-abort>.
|
||||
|
||||
Now let's explore the rest of options.
|
||||
|
||||
## referrer, referrerPolicy
|
||||
|
||||
These options govern how `fetch` sets HTTP `Referer` header.
|
||||
|
||||
That header contains the url of the page that made the request. In most scenarios, it plays a very minor informational role, but sometimes, for security purposes, it makes sense to remove or modify it.
|
||||
.
|
||||
|
||||
**The `referrer` option allows to set any `Referer` within the current origin) or disable it.**
|
||||
|
||||
To send no referer, set an empty string:
|
||||
```js
|
||||
fetch('/page', {
|
||||
*!*
|
||||
referrer: "" // no Referer header
|
||||
*/!*
|
||||
});
|
||||
```
|
||||
|
||||
To set another url within the current origin:
|
||||
|
||||
```js
|
||||
fetch('/page', {
|
||||
// assuming we're on https://javascript.info
|
||||
// we can set any Referer header, but only within the current origin
|
||||
*!*
|
||||
referrer: "https://javascript.info/anotherpage"
|
||||
*/!*
|
||||
});
|
||||
```
|
||||
|
||||
**The `referrerPolicy` option sets general rules for `Referer`.**
|
||||
|
||||
Possible values are described in the [Referrer Policy specification](https://w3c.github.io/webappsec-referrer-policy/):
|
||||
|
||||
- **`"no-referrer-when-downgrade"`** -- default value: `Referer` is sent always, unless we send a request from HTTPS to HTTP (to less secure protocol).
|
||||
- **`"no-referrer"`** -- never send `Referer`.
|
||||
- **`"origin"`** -- only send the origin in `Referer`, not the full page URL, e.g. `http://site.com` instead of `http://site.com/path`.
|
||||
- **`"origin-when-cross-origin"`** -- send full referrer to the same origin, but only the origin part for cross-origin requests.
|
||||
- **`"same-origin"`** -- send full referrer to the same origin, but no referer for for cross-origin requests.
|
||||
- **`"strict-origin"`** -- send only origin, don't send referrer for HTTPS→HTTP requests.
|
||||
- **`"strict-origin-when-cross-origin"`** -- for same-origin send full referrer, for cross-origin send only origin, unless it's HTTPS→HTTP request, then send nothing.
|
||||
- **`"unsafe-url"`** -- always send full url in `Referer`.
|
||||
|
||||
Let's say we have an admin zone with URL structure that shouldn't be visible from outside.
|
||||
|
||||
If we send a cross-origin `fetch`, then by default it sends the `Referer` header with the full url of our page (except when we request from HTTPS to HTTP, then no `Referer`).
|
||||
|
||||
E.g. `Referer: https://javascript.info/admin/secret/paths`.
|
||||
|
||||
If we'd like to totally hide the referrer:
|
||||
|
||||
```js
|
||||
fetch('https://another.com/page', {
|
||||
referrerPolicy: "no-referrer" // no Referer, same effect as referrer: ""
|
||||
});
|
||||
```
|
||||
|
||||
Otherwise, if we'd like the remote side to see where the request comes from, we can send only the "origin" part of the url:
|
||||
|
||||
```js
|
||||
fetch('https://another.com/page', {
|
||||
referrerPolicy: "strict-origin" // Referer: https://javascript.info
|
||||
});
|
||||
```
|
||||
|
||||
## mode
|
||||
|
||||
The `mode` option serves as a safe-guard that prevents cross-origin requests:
|
||||
|
||||
- **`"cors"`** -- the default, cross-origin requests are allowed, as described in <info:fetch-crossorigin>,
|
||||
- **`"same-origin"`** -- cross-origin requests are forbidden,
|
||||
- **`"no-cors"`** -- only simple cross-origin requests are allowed.
|
||||
|
||||
That may be useful in contexts when the fetch url comes from 3rd-party, and we want a "power off switch" to limit cross-origin capabilities.
|
||||
|
||||
## credentials
|
||||
|
||||
The `credentials` option specifies whether `fetch` should send cookies and HTTP-Authorization headers with the request.
|
||||
|
||||
- **`"same-origin"`** -- the default, don't send for cross-origin requests,
|
||||
- **`"include"`** -- always send, requires `Accept-Control-Allow-Credentials` from cross-origin server,
|
||||
- **`"omit"`** -- never send, even for same-origin requests.
|
||||
|
||||
## cache
|
||||
|
||||
By default, `fetch` requests make use of standard HTTP-caching. That is, it honors `Expires`, `Cache-Control` headers, sends `If-Modified-Since`, and so on. Just like regular HTTP-requests do.
|
||||
|
||||
The `cache` options allows to ignore HTTP-cache or fine-tune its usage:
|
||||
|
||||
- **`"default"`** -- `fetch` uses standard HTTP-cache rules and headers;
|
||||
- **`"no-store"`** -- totally ignore HTTP-cache, this mode becomes the default if we set a header `If-Modified-Since`, `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or `If-Range`;
|
||||
- **`"reload"`** -- don't take the result from HTTP-cache (if any), but populate cache with the response (if response headers allow);
|
||||
- **`"no-cache"`** -- create a conditional request if there is a cached response, and a normal request otherwise. Populate HTTP-cache with the response;
|
||||
- **`"force-cache"`** -- use a response from HTTP-cache, even if it's stale. If there's no response in HTTP-cache, make a regular HTTP-request, behave normally;
|
||||
- **`"only-if-cached"`** -- use a response from HTTP-cache, even if it's stale. If there's no response in HTTP-cache, then error. Only works when `mode` is `"same-origin"`.
|
||||
|
||||
## redirect
|
||||
|
||||
Normally, `fetch` transparently follows HTTP-redirects, like 301, 302 etc.
|
||||
|
||||
The `redirect` option allows to change that:
|
||||
|
||||
- **`"follow"`** -- the default, follow HTTP-redirects,
|
||||
- **`"error"`** -- error in case of HTTP-redirect,
|
||||
- **`"manual"`** -- don't follow HTTP-redirect, but `response.url` will be the new URL, and `response.redirected` will be `true`, so that we can perform the redirect manually to the new URL (if needed).
|
||||
|
||||
## integrity
|
||||
|
||||
The `integrity` option allows to check if the response matches the known-ahead checksum.
|
||||
|
||||
As described in the [specification](https://w3c.github.io/webappsec-subresource-integrity/), supported hash-functions are SHA-256, SHA-384, and SHA-512, there might be others depending on a browser.
|
||||
|
||||
For example, we're downloading a file, and we know that it's SHA-256 checksum is "abc" (a real checksum is longer, of course).
|
||||
|
||||
We can put it in the `integrity` option, like this:
|
||||
|
||||
```js
|
||||
fetch('http://site.com/file', {
|
||||
integrity: 'sha256-abd'
|
||||
});
|
||||
```
|
||||
|
||||
Then `fetch` will calculate SHA-256 on its own and compare it with our string. In case of a mismatch, an error is triggered.
|
||||
|
||||
## keepalive
|
||||
|
||||
The `keepalive` option indicates that the request may outlive the page.
|
||||
|
||||
For example, we gather statistics about how the current visitor uses our page (mouse clicks, page fragments he views), to improve user experience.
|
||||
|
||||
When the visitor leaves our page -- we'd like to save it on our server.
|
||||
|
||||
We can use `window.onunload` for that:
|
||||
|
||||
```js run
|
||||
window.onunload = function() {
|
||||
fetch('/analytics', {
|
||||
method: 'POST',
|
||||
body: "statistics",
|
||||
*!*
|
||||
keepalive: true
|
||||
*/!*
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Normally, when a document is unloaded, all associated network requests are aborted. But `keepalive` option tells the browser to perform the request in background, even after it leaves the page. So it's essential for our request to succeed.
|
||||
|
||||
- We can't send megabytes: the body limit for keepalive requests is 64kb.
|
||||
- If we gather more data, we can send it out regularly, then there won't be a lot for the "onunload" request.
|
||||
- The limit is for all currently ongoing requests. So we cheat it by creating 100 requests, each 64kb.
|
||||
- We don't get the server response if the request is made `onunload`, because the document is already unloaded at that time.
|
||||
- Usually, the server sends empty response to such requests, so it's not a problem.
|
4
5-network/05-fetch-api/logo-fetch.svg
Normal 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 |
36
5-network/05-fetch-api/post.view/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
(async () {
|
||||
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(chunk.value);
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
|
||||
let chunksMerged = new Uint8Array(receivedLength);
|
||||
let length = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksMerged.set(chunk, length);
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
let result = new TextDecoder("utf-8").decode(chunksMerged);
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
</script>
|
55
5-network/05-fetch-api/post.view/server.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
let file = new static.Server('.', {
|
||||
cache: 0
|
||||
});
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.method == 'POST') {
|
||||
let chunks = [];
|
||||
let length = 0;
|
||||
|
||||
req.on('data', function (data) {
|
||||
chunks.push(data);
|
||||
length += data.length;
|
||||
|
||||
// Too much POST data, kill the connection!
|
||||
if (length > 1e6) {
|
||||
request.connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
req.on('end', function() {
|
||||
// let post = JSON.parse(chunks.join(''));
|
||||
|
||||
if (req.url == '/user') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: 'User saved' }));
|
||||
} else if (req.url == '/image') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: "Image saved", imageSize: length }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ------ запустить сервер -------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
96
5-network/06-url/article.md
Normal file
|
@ -0,0 +1,96 @@
|
|||
|
||||
# URL objects
|
||||
|
||||
The built-in [URL](https://url.spec.whatwg.org/#api) class provides a convenient interface for creating and parsing URLs.
|
||||
|
||||
We don't have to use it at all. There are no networking methods that require exactly an `URL` object, strings are good enough. But sometimes it can be really helpful.
|
||||
|
||||
## Creating an URL
|
||||
|
||||
The syntax to create a new URL object:
|
||||
|
||||
```js
|
||||
new URL(url, [base])
|
||||
```
|
||||
|
||||
- **`url`** -- the text url
|
||||
- **`base`** -- an optional base for the `url`
|
||||
|
||||
The `URL` object immediately allows us to access its components, so it's a nice way to parse the url, e.g.:
|
||||
|
||||
```js run
|
||||
let url = new URL('https://javascript.info/url');
|
||||
|
||||
alert(url.protocol); // https:
|
||||
alert(url.host); // javascript.info
|
||||
alert(url.pathname); // /url
|
||||
```
|
||||
|
||||
Here's the cheatsheet:
|
||||
|
||||

|
||||
|
||||
- `href` is the full url, same as `url.toString()`
|
||||
- `protocol` ends with the colon character `:`
|
||||
- `search` starts with the question mark `?`
|
||||
- `hash` ends with the hash character `#`
|
||||
- there are also `user` and `password` properties if HTTP authentication is present.
|
||||
|
||||
We can also use `URL` to create relative urls, using the second argument:
|
||||
|
||||
```js run
|
||||
let url = new URL('profile/admin', 'https://javascript.info');
|
||||
|
||||
alert(url); // https://javascript.info/profile/admin
|
||||
|
||||
url = new URL('tester', url); // go to 'tester' relative to current url path
|
||||
|
||||
alert(url); // https://javascript.info/profile/tester
|
||||
```
|
||||
|
||||
```smart header="We can use `URL` everywhere instead of a string"
|
||||
We can use an `URL` object in `fetch` or `XMLHttpRequest`, almost everywhere where a string url is expected.
|
||||
|
||||
In the vast majority of methods it's automatically converted to a string.
|
||||
```
|
||||
|
||||
## SearchParams
|
||||
|
||||
Let's say we want to create an url with given search params, for instance, `https://google.com/search?query=value`.
|
||||
|
||||
They must be correctly encoded.
|
||||
|
||||
In very old browsers, before `URL` apparead, we'd use built-in functions `encodeURIComponent/decodeURIComponent`.
|
||||
|
||||
Now, there's no need: `url.searchParams` is an object of type [URLSearchParams](https://url.spec.whatwg.org/#urlsearchparams).
|
||||
|
||||
It provides convenient methods for search parameters:
|
||||
|
||||
- **`append(name, value)`** -- add the parameter,
|
||||
- **`delete(name)`** -- remove the parameter,
|
||||
- **`get(name)`** -- get the parameter,
|
||||
- **`getAll(name)`** -- get all parameters with that name,
|
||||
- **`has(name)`** -- check for the existance of the parameter,
|
||||
- **`set(name, value)`** -- set/replace the parameter,
|
||||
- **`sort()`** -- sort parameters by name, rarely needed,
|
||||
- ...and also iterable, similar to `Map`.
|
||||
|
||||
So, `URL` object also provides an easy way to operate on url parameters.
|
||||
|
||||
For example:
|
||||
|
||||
```js run
|
||||
let url = new URL('https://google.com/search');
|
||||
url.searchParams.set('query', 'test me!');
|
||||
|
||||
alert(url); // https://google.com/search?query=test+me%21
|
||||
|
||||
url.searchParams.set('tbs', 'qdr:y'); // add param for date range: past year
|
||||
|
||||
alert(url); // https://google.com/search?query=test+me%21&tbs=qdr%3Ay
|
||||
|
||||
// iterate over search parameters (decoded)
|
||||
for(let [name, value] of url.searchParams) {
|
||||
alert(`${name}=${value}`); // query=test me!, then tbs=qdr:y
|
||||
}
|
||||
```
|
BIN
5-network/06-url/url-object.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
5-network/06-url/url-object@2x.png
Normal file
After Width: | Height: | Size: 35 KiB |
508
5-network/07-xmlhttprequest/article.md
Normal file
|
@ -0,0 +1,508 @@
|
|||
# XMLHttpRequest
|
||||
|
||||
`XMLHttpRequest` is a built-in browser object that allows to make HTTP requests in JavaScript.
|
||||
|
||||
Despite of having the word "XML" in its name, it can operate on any data, not only in XML format. We can upload/download files, track progress and much more.
|
||||
|
||||
Right now, there's another, more modern method `fetch`, that somewhat deprecates `XMLHttpRequest`.
|
||||
|
||||
In modern web-development `XMLHttpRequest` may be used for three reasons:
|
||||
|
||||
1. Historical reasons: we need to support existing scripts with `XMLHttpRequest`.
|
||||
2. We need to support old browsers, and don't want polyfills (e.g. to keep scripts tiny).
|
||||
3. We need something that `fetch` can't do yet, e.g. to track upload progress.
|
||||
|
||||
Does that sound familiar? If yes, then all right, go on with `XMLHttpRequest`. Otherwise, please head on to fetch (coming soon).
|
||||
|
||||
## Basic flow
|
||||
|
||||
XMLHttpRequest has two modes of operation: synchronous and asynchronous.
|
||||
|
||||
Let's see the asynchronous first, as it's used in the majority of cases.
|
||||
|
||||
To do the request, we need 3 steps:
|
||||
|
||||
1. Create `XMLHttpRequest`.
|
||||
```js
|
||||
let xhr = new XMLHttpRequest(); // no arguments
|
||||
```
|
||||
|
||||
2. Initialize it.
|
||||
```js
|
||||
xhr.open(method, URL, [async, user, password])
|
||||
```
|
||||
|
||||
This method is usually called first after `new XMLHttpRequest`. It specifies the main parameters of the request:
|
||||
|
||||
- `method` -- HTTP-method. Usually `"GET"` or `"POST"`.
|
||||
- `URL` -- the URL to request.
|
||||
- `async` -- if explicitly set to `false`, then the request is synchronous, we'll cover that a bit later.
|
||||
- `user`, `password` -- login and password for basic HTTP auth (if required).
|
||||
|
||||
Please note that `open` call, contrary to its name, does not open the connection. It only configures the request, but the network activity only starts with the call of `send`.
|
||||
|
||||
3. Send it out.
|
||||
|
||||
```js
|
||||
xhr.send([body])
|
||||
```
|
||||
|
||||
This method opens the connection and sends the request to server. The optional `body` parameter contains the request body.
|
||||
|
||||
Some request methods like `GET` do not have a body. And some of them like `POST` use `body` to send the data to the server. We'll see examples later.
|
||||
|
||||
4. Listen to events for response.
|
||||
|
||||
These three are the most widely used:
|
||||
- `load` -- when the result is ready, that includes HTTP errors like 404.
|
||||
- `error` -- when the request couldn't be made, e.g. network down or invalid URL.
|
||||
- `progress` -- triggers periodically during the download, reports how much downloaded.
|
||||
|
||||
```js
|
||||
xhr.onload = function() {
|
||||
alert(`Loaded: ${xhr.status} ${xhr.response}`);
|
||||
};
|
||||
|
||||
xhr.onerror = function() { // only triggers if the request couldn't be made at all
|
||||
alert(`Network Error`);
|
||||
};
|
||||
|
||||
xhr.onprogress = function(event) { // triggers periodically
|
||||
// event.loaded - how many bytes downloaded
|
||||
// event.lengthComputable = true if the server sent Content-Length header
|
||||
// event.total - total number of bytes (if lengthComputable)
|
||||
alert(`Received ${event.loaded} of ${event.total}`);
|
||||
};
|
||||
```
|
||||
|
||||
Here's a full example. The code below loads the URL at `/article/xmlhttprequest/example/load` from the server and prints the progress:
|
||||
|
||||
```js run
|
||||
// 1. Create a new XMLHttpRequest object
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
// 2. Configure it: GET-request for the URL /article/.../load
|
||||
xhr.open('GET', '/article/xmlhttprequest/example/load');
|
||||
|
||||
// 3. Send the request over the network
|
||||
xhr.send();
|
||||
|
||||
// 4. This will be called after the response is received
|
||||
xhr.onload = function() {
|
||||
if (xhr.status != 200) { // analyze HTTP status of the response
|
||||
alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
|
||||
} else { // show the result
|
||||
alert(`Done, got ${xhr.response.length} bytes`); // responseText is the server
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onprogress = function(event) {
|
||||
if (event.lengthComputable) {
|
||||
alert(`Received ${event.loaded} of ${event.total} bytes`);
|
||||
} else {
|
||||
alert(`Received ${event.loaded} bytes`); // no Content-Length
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert("Request failed");
|
||||
};
|
||||
```
|
||||
|
||||
Once the server has responded, we can receive the result in the following properties of the request object:
|
||||
|
||||
`status`
|
||||
: HTTP status code (a number): `200`, `404`, `403` and so on, can be `0` in case of a non-HTTP failure.
|
||||
|
||||
`statusText`
|
||||
: HTTP status message (a string): usually `OK` for `200`, `Not Found` for `404`, `Forbidden` for `403` and so on.
|
||||
|
||||
`response` (old scripts may use `responseText`)
|
||||
: The server response.
|
||||
|
||||
If we changed our mind, we can terminate the request at any time. The call to `xhr.abort()` does that:
|
||||
|
||||
```js
|
||||
xhr.abort(); // terminate the request
|
||||
```
|
||||
|
||||
That triggers `abort` event.
|
||||
|
||||
We can also specify a timeout using the corresponding property:
|
||||
|
||||
```js
|
||||
xhr.timeout = 10000; // timeout in ms, 10 seconds
|
||||
```
|
||||
|
||||
If the request does not succeed within the given time, it gets canceled and `timeout` event triggers.
|
||||
|
||||
## Response Type
|
||||
|
||||
We can use `xhr.responseType` property to set the response format:
|
||||
|
||||
- `""` (default) -- get as string,
|
||||
- `"text"` -- get as string,
|
||||
- `"arraybuffer"` -- get as `ArrayBuffer` (for binary data, see chapter <info:arraybuffer-and-views>),
|
||||
- `"blob"` -- get as `Blob` (for binary data, see chapter <info:blob>),
|
||||
- `"document"` -- get as XML document (can use XPath and other XML methods),
|
||||
- `"json"` -- get as JSON (parsed automatically).
|
||||
|
||||
For example, let's get the response as JSON:
|
||||
|
||||
```js run
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', '/article/xmlhttprequest/example/json');
|
||||
|
||||
*!*
|
||||
xhr.responseType = 'json';
|
||||
*/!*
|
||||
|
||||
xhr.send();
|
||||
|
||||
// the response is {"message": "Hello, world!"}
|
||||
xhr.onload = function() {
|
||||
let responseObj = xhr.response;
|
||||
alert(responseObj.message); // Hello, world!
|
||||
};
|
||||
```
|
||||
|
||||
```smart
|
||||
In the old scripts you may also find `xhr.responseText` and even `xhr.responseXML` properties.
|
||||
|
||||
They exist for historical reasons, to get either a string or XML document. Nowadays, we should set the format in `xhr.responseType` and get `xhr.response` as demonstrated above.
|
||||
```
|
||||
|
||||
## Ready states
|
||||
|
||||
`XMLHttpRequest` changes between states as it progresses. The current state is accessible as `xhr.readyState`.
|
||||
|
||||
All states, as in [the specification](https://xhr.spec.whatwg.org/#states):
|
||||
|
||||
```js
|
||||
UNSENT = 0; // initial state
|
||||
OPENED = 1; // open called
|
||||
HEADERS_RECEIVED = 2; // response headers received
|
||||
LOADING = 3; // response is loading (a data packed is received)
|
||||
DONE = 4; // request complete
|
||||
```
|
||||
|
||||
An `XMLHttpRequest` object travels them in the order `0` -> `1` -> `2` -> `3` -> ... -> `3` -> `4`. State `3` repeats every time a data packet is received over the network.
|
||||
|
||||
We can track them using `readystatechange` event:
|
||||
|
||||
```js
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 3) {
|
||||
// loading
|
||||
}
|
||||
if (xhr.readyState == 4) {
|
||||
// request finished
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
You can find `readystatechange` listeners in really old code, for historical reasons.
|
||||
|
||||
Nowadays, `load/error/progress` handlers deprecate it.
|
||||
|
||||
## Synchronous requests
|
||||
|
||||
If in the `open` method the third parameter `async` is set to `false`, the request is made synchronously.
|
||||
|
||||
In other words, Javascript execution pauses at `send()` and resumes when the response is received. Somewhat like `alert` or `prompt` commands.
|
||||
|
||||
Here's the rewritten example, the 3rd parameter of `open` is `false`:
|
||||
|
||||
```js
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', '/article/xmlhttprequest/hello.txt', *!*false*/!*);
|
||||
|
||||
try {
|
||||
xhr.send();
|
||||
if (xhr.status != 200) {
|
||||
alert(`Error ${xhr.status}: ${xhr.statusText}`);
|
||||
} else {
|
||||
alert(xhr.response);
|
||||
}
|
||||
} catch(err) { // instead of onerror
|
||||
alert("Request failed");
|
||||
};
|
||||
```
|
||||
|
||||
It might look good, but synchronous calls are used rarely, because they block in-page Javascript till the loading is complete. In some browsers it becomes impossible to scroll. If a synchronous call takes too much time, the browser may suggest to close the "hanging" webpage.
|
||||
|
||||
Many advanced capabilities of `XMLHttpRequest`, like requesting from another domain or specifying a timeout, are unavailable for synchronous requests. Also, as you can see, no progress indication.
|
||||
|
||||
Because of all that, synchronous requests are used very sparingly, almost never. We won't talk about them any more.
|
||||
|
||||
## HTTP-headers
|
||||
|
||||
`XMLHttpRequest` allows both to send custom headers and read headers from the response.
|
||||
|
||||
There are 3 methods for HTTP-headers:
|
||||
|
||||
`setRequestHeader(name, value)`
|
||||
: Sets the request header with the given `name` and `value`.
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
```
|
||||
|
||||
```warn header="Headers limitations"
|
||||
Several headers are managed exclusively by the browser, e.g. `Referer` and `Host`.
|
||||
The full list is [in the specification](http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method).
|
||||
|
||||
XMLHttpRequest is not allowed to change them, for the sake of user safety and correctness of the request.
|
||||
```
|
||||
|
||||
````warn header="Can't remove a header"
|
||||
Another peciliarity of `XMLHttpRequest` is that one can't undo `setRequestHeader`.
|
||||
|
||||
Once the header is set, it's set. Additional calls add information to the header, don't overwrite it.
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
xhr.setRequestHeader('X-Auth', '123');
|
||||
xhr.setRequestHeader('X-Auth', '456');
|
||||
|
||||
// the header will be:
|
||||
// X-Auth: 123, 456
|
||||
```
|
||||
````
|
||||
|
||||
`getResponseHeader(name)`
|
||||
: Gets the response header with the given `name` (except `Set-Cookie` and `Set-Cookie2`).
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
xhr.getResponseHeader('Content-Type')
|
||||
```
|
||||
|
||||
`getAllResponseHeaders()`
|
||||
: Returns all response headers, except `Set-Cookie` and `Set-Cookie2`.
|
||||
|
||||
Headers are returned as a single line, e.g.:
|
||||
|
||||
```
|
||||
Cache-Control: max-age=31536000
|
||||
Content-Length: 4260
|
||||
Content-Type: image/png
|
||||
Date: Sat, 08 Sep 2012 16:53:16 GMT
|
||||
```
|
||||
|
||||
The line break between headers is always `"\r\n"` (doesn't depend on OS), so we can easily split it into individual headers. The separator between the name and the value is always a colon followed by a space `": "`. That's fixed in the specification.
|
||||
|
||||
So, if we want to get an object with name/value pairs, we need to throw in a bit JS.
|
||||
|
||||
Like this (assuming that if two headers have the same name, then the latter one overwrites the former one):
|
||||
|
||||
```js
|
||||
let headers = xhr
|
||||
.getAllResponseHeaders()
|
||||
.split('\r\n')
|
||||
.reduce((result, current) => {
|
||||
let [name, value] = current.split(': ');
|
||||
result[name] = value;
|
||||
return result;
|
||||
}, {});
|
||||
```
|
||||
|
||||
## POST, FormData
|
||||
|
||||
To make a POST request, we can use the built-in [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.
|
||||
|
||||
The syntax:
|
||||
|
||||
```js
|
||||
let formData = new FormData([form]); // creates an object, optionally fill from <form>
|
||||
formData.append(name, value); // appends a field
|
||||
```
|
||||
|
||||
Create it, optionally from a form, `append` more fields if needed, and then:
|
||||
|
||||
1. `xhr.open('POST', ...)` – use `POST` method.
|
||||
2. `xhr.send(formData)` to submit the form to the server.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run
|
||||
<form name="person">
|
||||
<input name="name" value="John">
|
||||
<input name="surname" value="Smith">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// pre-fill FormData from the form
|
||||
let formData = new FormData(document.forms.person);
|
||||
|
||||
// add one more field
|
||||
formData.append("middle", "Lee");
|
||||
|
||||
// send it out
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/article/xmlhttprequest/post/user");
|
||||
xhr.send(formData);
|
||||
|
||||
xhr.
|
||||
</script>
|
||||
```
|
||||
|
||||
The form is sent with `multipart/form-data` encoding.
|
||||
|
||||
Or, if we like JSON more, then `JSON.stringify` and send as a string.
|
||||
|
||||
Just don't forget to set the header `Content-Type: application/json`, many server-side frameworks automatically decode JSON with it:
|
||||
|
||||
```js
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
let json = JSON.stringify({
|
||||
name: "John",
|
||||
surname: "Smith"
|
||||
});
|
||||
|
||||
xhr.open("POST", '/submit')
|
||||
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
|
||||
|
||||
xhr.send(json);
|
||||
```
|
||||
|
||||
The `.send(body)` method is pretty omnivore. It can send almost everything, including Blob and BufferSource objects.
|
||||
|
||||
## Upload progress
|
||||
|
||||
The `progress` event only works on the downloading stage.
|
||||
|
||||
That is: if we `POST` something, `XMLHttpRequest` first uploads our data, then downloads the response.
|
||||
|
||||
If we're uploading something big, then we're surely more interested in tracking the upload progress. But `progress` event doesn't help here.
|
||||
|
||||
There's another object `xhr.upload`, without methods, exclusively for upload events.
|
||||
|
||||
Here's the list:
|
||||
|
||||
- `loadstart` -- upload started.
|
||||
- `progress` -- triggers periodically during the upload.
|
||||
- `abort` -- upload aborted.
|
||||
- `error` -- non-HTTP error.
|
||||
- `load` -- upload finished successfully.
|
||||
- `timeout` -- upload timed out (if `timeout` property is set).
|
||||
- `loadend` -- upload finished with either success or error.
|
||||
|
||||
Example of handlers:
|
||||
|
||||
```js
|
||||
xhr.upload.onprogress = function(event) {
|
||||
alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
|
||||
};
|
||||
|
||||
xhr.upload.onload = function() {
|
||||
alert(`Upload finished successfully.`);
|
||||
};
|
||||
|
||||
xhr.upload.onerror = function() {
|
||||
alert(`Error during the upload: ${xhr.status}`);
|
||||
};
|
||||
```
|
||||
|
||||
Here's a real-life example: file upload with progress indication:
|
||||
|
||||
```html run
|
||||
<input type="file" onchange="upload(this.files[0])">
|
||||
|
||||
<script>
|
||||
function upload(file) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
// track upload progress
|
||||
*!*
|
||||
xhr.upload.onprogress = function(event) {
|
||||
console.log(`Uploaded ${event.loaded} of ${event.total}`);
|
||||
};
|
||||
*/!*
|
||||
|
||||
// track completion: both successful or not
|
||||
xhr.onloadend = function() {
|
||||
if (xhr.status == 200) {
|
||||
console.log("success");
|
||||
} else {
|
||||
console.log("error " + this.status);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("POST", "/article/xmlhttprequest/post/upload");
|
||||
xhr.send(file);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Cross-origin requests
|
||||
|
||||
`XMLHttpRequest` can make cross-domain requests, using the same CORS policy as [fetch](info:fetch-crossorigin).
|
||||
|
||||
Just like `fetch`, it doesn't send cookies and HTTP-authorization to another origin by default. To enable them, set `xhr.withCredentials` to `true`:
|
||||
|
||||
```js
|
||||
let xhr = new XMLHttpRequest();
|
||||
*!*
|
||||
xhr.withCredentials = true;
|
||||
*/!*
|
||||
|
||||
xhr.open('POST', 'http://anywhere.com/request');
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
Typical code of the GET-request with `XMLHttpRequest`:
|
||||
|
||||
```js
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', '/my/url');
|
||||
|
||||
xhr.send(); // for POST, can send a string or FormData
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status != 200) { // HTTP error?
|
||||
// handle error
|
||||
alert( 'Error: ' + xhr.status);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the response from xhr.response
|
||||
};
|
||||
|
||||
xhr.onprogress = function(event) {
|
||||
// report progress
|
||||
alert(`Loaded ${event.loaded} of ${event.total}`);
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
// handle non-HTTP error (e.g. network down)
|
||||
};
|
||||
```
|
||||
|
||||
There are actually more events, the [modern specification](http://www.w3.org/TR/XMLHttpRequest/#events) lists them (in the lifecycle order):
|
||||
|
||||
- `loadstart` -- the request has started.
|
||||
- `progress` -- a data packet of the response has arrived, the whole response body at the moment is in `responseText`.
|
||||
- `abort` -- the request was canceled by the call `xhr.abort()`.
|
||||
- `error` -- connection error has occured, e.g. wrong domain name. Doesn't happen for HTTP-errors like 404.
|
||||
- `load` -- the request has finished successfully.
|
||||
- `timeout` -- the request was canceled due to timeout (only happens if it was set).
|
||||
- `loadend` -- the request has finished (succeffully or not).
|
||||
|
||||
The most used events are load completion (`load`), load failure (`error`), and also `progress` to track the progress.
|
||||
|
||||
We've already seen another event: `readystatechange`. Historically, it appeared long ago, before the specification settled. Nowadays, there's no need to use it, we can replace it with newer events, but it can often be found in older scripts.
|
||||
|
||||
If we need to track uploading specifically, then we should listen to same events on `xhr.upload` object.
|
30
5-network/07-xmlhttprequest/example.view/index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE HTML>
|
||||
<script>
|
||||
function run() {
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
write(`readyState=${xhr.readyState}`);
|
||||
|
||||
xhr.open('GET', 'digits');
|
||||
write(`readyState=${xhr.readyState}`);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
write(`readyState=${xhr.readyState}, responseText.length=${xhr.responseText.length}`);
|
||||
};
|
||||
|
||||
xhr.onprogress = function() {
|
||||
write(`readyState=${xhr.readyState}, responseText.length=${xhr.responseText.length}`);
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function write(text) {
|
||||
let li = log.appendChild(document.createElement('li'));
|
||||
li.innerHTML = text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick="run()">Load digits</button>
|
||||
|
||||
<ul id="log"></ul>
|
52
5-network/07-xmlhttprequest/example.view/server.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
let file = new static.Server('.');
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.url == '/load') {
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Length': 90000
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
|
||||
let timer = setInterval(write, 1000);
|
||||
write();
|
||||
|
||||
function write() {
|
||||
res.write(String(i).repeat(10000));
|
||||
i++;
|
||||
if (i == 9) {
|
||||
clearInterval(timer);
|
||||
res.end();
|
||||
}
|
||||
|
||||
}
|
||||
} else if (req.url == '/json') {
|
||||
res.writeHead(200, {
|
||||
// 'Content-Type': 'application/json;charset=utf-8',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
|
||||
res.write(JSON.stringify({message: "Hello, world!"}));
|
||||
res.end();
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----- запуск accept как сервера из консоли или как модуля ------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
1
5-network/07-xmlhttprequest/hello.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Hello from the server!
|
42
5-network/07-xmlhttprequest/phones-async.view/index.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button onclick="loadPhones()" id="button">Load phones.json!</button>
|
||||
|
||||
<script>
|
||||
function loadPhones() {
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', 'phones.json');
|
||||
|
||||
|
||||
xhr.send();
|
||||
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState != 4) return;
|
||||
|
||||
button.innerHTML = 'Complete!';
|
||||
|
||||
if (xhr.status != 200) {
|
||||
// handle error
|
||||
alert(xhr.status + ': ' + xhr.statusText);
|
||||
} else {
|
||||
// show result
|
||||
alert(xhr.responseText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
button.innerHTML = 'Loading...';
|
||||
button.disabled = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
155
5-network/07-xmlhttprequest/phones-async.view/phones.json
Normal file
|
@ -0,0 +1,155 @@
|
|||
[
|
||||
{
|
||||
"age": 0,
|
||||
"id": "motorola-xoom-with-wi-fi",
|
||||
"imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg",
|
||||
"name": "Motorola XOOM\u2122 with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)."
|
||||
},
|
||||
{
|
||||
"age": 1,
|
||||
"id": "motorola-xoom",
|
||||
"imageUrl": "img/phones/motorola-xoom.0.jpg",
|
||||
"name": "MOTOROLA XOOM\u2122",
|
||||
"snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)."
|
||||
},
|
||||
{
|
||||
"age": 2,
|
||||
"carrier": "AT&T",
|
||||
"id": "motorola-atrix-4g",
|
||||
"imageUrl": "img/phones/motorola-atrix-4g.0.jpg",
|
||||
"name": "MOTOROLA ATRIX\u2122 4G",
|
||||
"snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone."
|
||||
},
|
||||
{
|
||||
"age": 3,
|
||||
"id": "dell-streak-7",
|
||||
"imageUrl": "img/phones/dell-streak-7.0.jpg",
|
||||
"name": "Dell Streak 7",
|
||||
"snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around."
|
||||
},
|
||||
{
|
||||
"age": 4,
|
||||
"carrier": "Cellular South",
|
||||
"id": "samsung-gem",
|
||||
"imageUrl": "img/phones/samsung-gem.0.jpg",
|
||||
"name": "Samsung Gem\u2122",
|
||||
"snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price."
|
||||
},
|
||||
{
|
||||
"age": 5,
|
||||
"carrier": "Dell",
|
||||
"id": "dell-venue",
|
||||
"imageUrl": "img/phones/dell-venue.0.jpg",
|
||||
"name": "Dell Venue",
|
||||
"snippet": "The Dell Venue; Your Personal Express Lane to Everything"
|
||||
},
|
||||
{
|
||||
"age": 6,
|
||||
"carrier": "Best Buy",
|
||||
"id": "nexus-s",
|
||||
"imageUrl": "img/phones/nexus-s.0.jpg",
|
||||
"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet."
|
||||
},
|
||||
{
|
||||
"age": 7,
|
||||
"carrier": "Cellular South",
|
||||
"id": "lg-axis",
|
||||
"imageUrl": "img/phones/lg-axis.0.jpg",
|
||||
"name": "LG Axis",
|
||||
"snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens"
|
||||
},
|
||||
{
|
||||
"age": 8,
|
||||
"id": "samsung-galaxy-tab",
|
||||
"imageUrl": "img/phones/samsung-galaxy-tab.0.jpg",
|
||||
"name": "Samsung Galaxy Tab\u2122",
|
||||
"snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility."
|
||||
},
|
||||
{
|
||||
"age": 9,
|
||||
"carrier": "Cellular South",
|
||||
"id": "samsung-showcase-a-galaxy-s-phone",
|
||||
"imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg",
|
||||
"name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone",
|
||||
"snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors"
|
||||
},
|
||||
{
|
||||
"age": 10,
|
||||
"carrier": "Verizon",
|
||||
"id": "droid-2-global-by-motorola",
|
||||
"imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg",
|
||||
"name": "DROID\u2122 2 Global by Motorola",
|
||||
"snippet": "The first smartphone with a 1.2 GHz processor and global capabilities."
|
||||
},
|
||||
{
|
||||
"age": 11,
|
||||
"carrier": "Verizon",
|
||||
"id": "droid-pro-by-motorola",
|
||||
"imageUrl": "img/phones/droid-pro-by-motorola.0.jpg",
|
||||
"name": "DROID\u2122 Pro by Motorola",
|
||||
"snippet": "The next generation of DOES."
|
||||
},
|
||||
{
|
||||
"age": 12,
|
||||
"carrier": "AT&T",
|
||||
"id": "motorola-bravo-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg",
|
||||
"name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "An experience to cheer about."
|
||||
},
|
||||
{
|
||||
"age": 13,
|
||||
"carrier": "T-Mobile",
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
},
|
||||
{
|
||||
"age": 14,
|
||||
"carrier": "T-Mobile",
|
||||
"id": "t-mobile-mytouch-4g",
|
||||
"imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg",
|
||||
"name": "T-Mobile myTouch 4G",
|
||||
"snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi."
|
||||
},
|
||||
{
|
||||
"age": 15,
|
||||
"carrier": "US Cellular",
|
||||
"id": "samsung-mesmerize-a-galaxy-s-phone",
|
||||
"imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg",
|
||||
"name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone",
|
||||
"snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors"
|
||||
},
|
||||
{
|
||||
"age": 16,
|
||||
"carrier": "Sprint",
|
||||
"id": "sanyo-zio",
|
||||
"imageUrl": "img/phones/sanyo-zio.0.jpg",
|
||||
"name": "SANYO ZIO",
|
||||
"snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value."
|
||||
},
|
||||
{
|
||||
"age": 17,
|
||||
"id": "samsung-transform",
|
||||
"imageUrl": "img/phones/samsung-transform.0.jpg",
|
||||
"name": "Samsung Transform\u2122",
|
||||
"snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d."
|
||||
},
|
||||
{
|
||||
"age": 18,
|
||||
"id": "t-mobile-g2",
|
||||
"imageUrl": "img/phones/t-mobile-g2.0.jpg",
|
||||
"name": "T-Mobile G2",
|
||||
"snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible."
|
||||
},
|
||||
{
|
||||
"age": 19,
|
||||
"id": "motorola-charm-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg",
|
||||
"name": "Motorola CHARM\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service."
|
||||
}
|
||||
]
|
30
5-network/07-xmlhttprequest/phones-async.view/server.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
var http = require('http');
|
||||
var url = require('url');
|
||||
var querystring = require('querystring');
|
||||
var static = require('node-static');
|
||||
var file = new static.Server('.', {
|
||||
cache: 0
|
||||
});
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.url == '/phones.json') {
|
||||
// искусственная задержка для наглядности
|
||||
setTimeout(function() {
|
||||
file.serve(req, res);
|
||||
}, 2000);
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ------ запустить сервер -------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
155
5-network/07-xmlhttprequest/phones.json
Normal file
|
@ -0,0 +1,155 @@
|
|||
[
|
||||
{
|
||||
"age": 0,
|
||||
"id": "motorola-xoom-with-wi-fi",
|
||||
"imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg",
|
||||
"name": "Motorola XOOM\u2122 with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)."
|
||||
},
|
||||
{
|
||||
"age": 1,
|
||||
"id": "motorola-xoom",
|
||||
"imageUrl": "img/phones/motorola-xoom.0.jpg",
|
||||
"name": "MOTOROLA XOOM\u2122",
|
||||
"snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)."
|
||||
},
|
||||
{
|
||||
"age": 2,
|
||||
"carrier": "AT&T",
|
||||
"id": "motorola-atrix-4g",
|
||||
"imageUrl": "img/phones/motorola-atrix-4g.0.jpg",
|
||||
"name": "MOTOROLA ATRIX\u2122 4G",
|
||||
"snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone."
|
||||
},
|
||||
{
|
||||
"age": 3,
|
||||
"id": "dell-streak-7",
|
||||
"imageUrl": "img/phones/dell-streak-7.0.jpg",
|
||||
"name": "Dell Streak 7",
|
||||
"snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around."
|
||||
},
|
||||
{
|
||||
"age": 4,
|
||||
"carrier": "Cellular South",
|
||||
"id": "samsung-gem",
|
||||
"imageUrl": "img/phones/samsung-gem.0.jpg",
|
||||
"name": "Samsung Gem\u2122",
|
||||
"snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price."
|
||||
},
|
||||
{
|
||||
"age": 5,
|
||||
"carrier": "Dell",
|
||||
"id": "dell-venue",
|
||||
"imageUrl": "img/phones/dell-venue.0.jpg",
|
||||
"name": "Dell Venue",
|
||||
"snippet": "The Dell Venue; Your Personal Express Lane to Everything"
|
||||
},
|
||||
{
|
||||
"age": 6,
|
||||
"carrier": "Best Buy",
|
||||
"id": "nexus-s",
|
||||
"imageUrl": "img/phones/nexus-s.0.jpg",
|
||||
"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet."
|
||||
},
|
||||
{
|
||||
"age": 7,
|
||||
"carrier": "Cellular South",
|
||||
"id": "lg-axis",
|
||||
"imageUrl": "img/phones/lg-axis.0.jpg",
|
||||
"name": "LG Axis",
|
||||
"snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens"
|
||||
},
|
||||
{
|
||||
"age": 8,
|
||||
"id": "samsung-galaxy-tab",
|
||||
"imageUrl": "img/phones/samsung-galaxy-tab.0.jpg",
|
||||
"name": "Samsung Galaxy Tab\u2122",
|
||||
"snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility."
|
||||
},
|
||||
{
|
||||
"age": 9,
|
||||
"carrier": "Cellular South",
|
||||
"id": "samsung-showcase-a-galaxy-s-phone",
|
||||
"imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg",
|
||||
"name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone",
|
||||
"snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors"
|
||||
},
|
||||
{
|
||||
"age": 10,
|
||||
"carrier": "Verizon",
|
||||
"id": "droid-2-global-by-motorola",
|
||||
"imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg",
|
||||
"name": "DROID\u2122 2 Global by Motorola",
|
||||
"snippet": "The first smartphone with a 1.2 GHz processor and global capabilities."
|
||||
},
|
||||
{
|
||||
"age": 11,
|
||||
"carrier": "Verizon",
|
||||
"id": "droid-pro-by-motorola",
|
||||
"imageUrl": "img/phones/droid-pro-by-motorola.0.jpg",
|
||||
"name": "DROID\u2122 Pro by Motorola",
|
||||
"snippet": "The next generation of DOES."
|
||||
},
|
||||
{
|
||||
"age": 12,
|
||||
"carrier": "AT&T",
|
||||
"id": "motorola-bravo-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg",
|
||||
"name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "An experience to cheer about."
|
||||
},
|
||||
{
|
||||
"age": 13,
|
||||
"carrier": "T-Mobile",
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
},
|
||||
{
|
||||
"age": 14,
|
||||
"carrier": "T-Mobile",
|
||||
"id": "t-mobile-mytouch-4g",
|
||||
"imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg",
|
||||
"name": "T-Mobile myTouch 4G",
|
||||
"snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi."
|
||||
},
|
||||
{
|
||||
"age": 15,
|
||||
"carrier": "US Cellular",
|
||||
"id": "samsung-mesmerize-a-galaxy-s-phone",
|
||||
"imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg",
|
||||
"name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone",
|
||||
"snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors"
|
||||
},
|
||||
{
|
||||
"age": 16,
|
||||
"carrier": "Sprint",
|
||||
"id": "sanyo-zio",
|
||||
"imageUrl": "img/phones/sanyo-zio.0.jpg",
|
||||
"name": "SANYO ZIO",
|
||||
"snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value."
|
||||
},
|
||||
{
|
||||
"age": 17,
|
||||
"id": "samsung-transform",
|
||||
"imageUrl": "img/phones/samsung-transform.0.jpg",
|
||||
"name": "Samsung Transform\u2122",
|
||||
"snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d."
|
||||
},
|
||||
{
|
||||
"age": 18,
|
||||
"id": "t-mobile-g2",
|
||||
"imageUrl": "img/phones/t-mobile-g2.0.jpg",
|
||||
"name": "T-Mobile G2",
|
||||
"snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible."
|
||||
},
|
||||
{
|
||||
"age": 19,
|
||||
"id": "motorola-charm-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg",
|
||||
"name": "Motorola CHARM\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service."
|
||||
}
|
||||
]
|
28
5-network/07-xmlhttprequest/phones.view/index.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button onclick="loadPhones()">Load phones.json!</button>
|
||||
|
||||
<script>
|
||||
function loadPhones() {
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', 'phones.json', false);
|
||||
xhr.send();
|
||||
|
||||
if (xhr.status != 200) {
|
||||
// handle error
|
||||
alert('Error ' + xhr.status + ': ' + xhr.statusText);
|
||||
} else {
|
||||
// show result
|
||||
alert(xhr.responseText);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
155
5-network/07-xmlhttprequest/phones.view/phones.json
Normal file
|
@ -0,0 +1,155 @@
|
|||
[
|
||||
{
|
||||
"age": 0,
|
||||
"id": "motorola-xoom-with-wi-fi",
|
||||
"imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg",
|
||||
"name": "Motorola XOOM\u2122 with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)."
|
||||
},
|
||||
{
|
||||
"age": 1,
|
||||
"id": "motorola-xoom",
|
||||
"imageUrl": "img/phones/motorola-xoom.0.jpg",
|
||||
"name": "MOTOROLA XOOM\u2122",
|
||||
"snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)."
|
||||
},
|
||||
{
|
||||
"age": 2,
|
||||
"carrier": "AT&T",
|
||||
"id": "motorola-atrix-4g",
|
||||
"imageUrl": "img/phones/motorola-atrix-4g.0.jpg",
|
||||
"name": "MOTOROLA ATRIX\u2122 4G",
|
||||
"snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone."
|
||||
},
|
||||
{
|
||||
"age": 3,
|
||||
"id": "dell-streak-7",
|
||||
"imageUrl": "img/phones/dell-streak-7.0.jpg",
|
||||
"name": "Dell Streak 7",
|
||||
"snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around."
|
||||
},
|
||||
{
|
||||
"age": 4,
|
||||
"carrier": "Cellular South",
|
||||
"id": "samsung-gem",
|
||||
"imageUrl": "img/phones/samsung-gem.0.jpg",
|
||||
"name": "Samsung Gem\u2122",
|
||||
"snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price."
|
||||
},
|
||||
{
|
||||
"age": 5,
|
||||
"carrier": "Dell",
|
||||
"id": "dell-venue",
|
||||
"imageUrl": "img/phones/dell-venue.0.jpg",
|
||||
"name": "Dell Venue",
|
||||
"snippet": "The Dell Venue; Your Personal Express Lane to Everything"
|
||||
},
|
||||
{
|
||||
"age": 6,
|
||||
"carrier": "Best Buy",
|
||||
"id": "nexus-s",
|
||||
"imageUrl": "img/phones/nexus-s.0.jpg",
|
||||
"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet."
|
||||
},
|
||||
{
|
||||
"age": 7,
|
||||
"carrier": "Cellular South",
|
||||
"id": "lg-axis",
|
||||
"imageUrl": "img/phones/lg-axis.0.jpg",
|
||||
"name": "LG Axis",
|
||||
"snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens"
|
||||
},
|
||||
{
|
||||
"age": 8,
|
||||
"id": "samsung-galaxy-tab",
|
||||
"imageUrl": "img/phones/samsung-galaxy-tab.0.jpg",
|
||||
"name": "Samsung Galaxy Tab\u2122",
|
||||
"snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility."
|
||||
},
|
||||
{
|
||||
"age": 9,
|
||||
"carrier": "Cellular South",
|
||||
"id": "samsung-showcase-a-galaxy-s-phone",
|
||||
"imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg",
|
||||
"name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone",
|
||||
"snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors"
|
||||
},
|
||||
{
|
||||
"age": 10,
|
||||
"carrier": "Verizon",
|
||||
"id": "droid-2-global-by-motorola",
|
||||
"imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg",
|
||||
"name": "DROID\u2122 2 Global by Motorola",
|
||||
"snippet": "The first smartphone with a 1.2 GHz processor and global capabilities."
|
||||
},
|
||||
{
|
||||
"age": 11,
|
||||
"carrier": "Verizon",
|
||||
"id": "droid-pro-by-motorola",
|
||||
"imageUrl": "img/phones/droid-pro-by-motorola.0.jpg",
|
||||
"name": "DROID\u2122 Pro by Motorola",
|
||||
"snippet": "The next generation of DOES."
|
||||
},
|
||||
{
|
||||
"age": 12,
|
||||
"carrier": "AT&T",
|
||||
"id": "motorola-bravo-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg",
|
||||
"name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "An experience to cheer about."
|
||||
},
|
||||
{
|
||||
"age": 13,
|
||||
"carrier": "T-Mobile",
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
},
|
||||
{
|
||||
"age": 14,
|
||||
"carrier": "T-Mobile",
|
||||
"id": "t-mobile-mytouch-4g",
|
||||
"imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg",
|
||||
"name": "T-Mobile myTouch 4G",
|
||||
"snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi."
|
||||
},
|
||||
{
|
||||
"age": 15,
|
||||
"carrier": "US Cellular",
|
||||
"id": "samsung-mesmerize-a-galaxy-s-phone",
|
||||
"imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg",
|
||||
"name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone",
|
||||
"snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors"
|
||||
},
|
||||
{
|
||||
"age": 16,
|
||||
"carrier": "Sprint",
|
||||
"id": "sanyo-zio",
|
||||
"imageUrl": "img/phones/sanyo-zio.0.jpg",
|
||||
"name": "SANYO ZIO",
|
||||
"snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value."
|
||||
},
|
||||
{
|
||||
"age": 17,
|
||||
"id": "samsung-transform",
|
||||
"imageUrl": "img/phones/samsung-transform.0.jpg",
|
||||
"name": "Samsung Transform\u2122",
|
||||
"snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d."
|
||||
},
|
||||
{
|
||||
"age": 18,
|
||||
"id": "t-mobile-g2",
|
||||
"imageUrl": "img/phones/t-mobile-g2.0.jpg",
|
||||
"name": "T-Mobile G2",
|
||||
"snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible."
|
||||
},
|
||||
{
|
||||
"age": 19,
|
||||
"id": "motorola-charm-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg",
|
||||
"name": "Motorola CHARM\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service."
|
||||
}
|
||||
]
|
30
5-network/07-xmlhttprequest/phones.view/server.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
let file = new static.Server('.', {
|
||||
cache: 0
|
||||
});
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.url == '/phones.json') {
|
||||
// stall a bit to let "loading" message show up
|
||||
setTimeout(function() {
|
||||
file.serve(req, res);
|
||||
}, 2000);
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ------ запустить сервер -------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
36
5-network/07-xmlhttprequest/post.view/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<script>
|
||||
(async () {
|
||||
|
||||
const response = await fetch('long.txt');
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const contentLength = +response.headers.get('Content-Length');
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while(true) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("done!");
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(chunk.value);
|
||||
receivedLength += chunk.value.length;
|
||||
console.log(`${receivedLength}/${contentLength} received`)
|
||||
}
|
||||
|
||||
|
||||
let chunksMerged = new Uint8Array(receivedLength);
|
||||
let length = 0;
|
||||
for(let chunk of chunks) {
|
||||
chunksMerged.set(chunk, length);
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
let result = new TextDecoder("utf-8").decode(chunksMerged);
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
</script>
|
58
5-network/07-xmlhttprequest/post.view/server.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
let file = new static.Server('.', {
|
||||
cache: 0
|
||||
});
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.method == 'POST') {
|
||||
let chunks = [];
|
||||
let length = 0;
|
||||
|
||||
req.on('data', function (data) {
|
||||
chunks.push(data);
|
||||
length += data.length;
|
||||
|
||||
// More than 10mb, kill the connection!
|
||||
if (length > 1e8) {
|
||||
req.connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
req.on('end', function() {
|
||||
// let post = JSON.parse(chunks.join(''));
|
||||
|
||||
if (req.url == '/user') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: 'User saved' }));
|
||||
} else if (req.url == '/image') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: "Image saved", imageSize: length }));
|
||||
} else if (req.url == '/upload') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: "Upload complete", size: length }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
file.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ------ запустить сервер -------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
385
5-network/08-websocket/article.md
Normal file
|
@ -0,0 +1,385 @@
|
|||
# WebSocket
|
||||
|
||||
The `WebSocket` protocol, described in the specification [RFC 6455](http://tools.ietf.org/html/rfc6455) provides a way to exchange data between browser and server via a persistent connection.
|
||||
|
||||
Once a websocket connection is established, both client and server may send the data to each other.
|
||||
|
||||
WebSocket is especially great for services that require continuous data exchange, e.g. online games, real-time trading systems and so on.
|
||||
|
||||
## A simple example
|
||||
|
||||
To open a websocket connection, we need to create `new WebSocket` using the special protocol `ws` in the url:
|
||||
|
||||
```js
|
||||
let socket = new WebSocket("*!*ws*/!*://javascript.info");
|
||||
```
|
||||
|
||||
There's also encrypted `wss://` protocol. It's like HTTPS for websockets.
|
||||
|
||||
```smart header="Always prefer `wss://`"
|
||||
The `wss://` protocol not only encrypted, but also more reliable.
|
||||
|
||||
That's because `ws://` data is not encrypted, visible for any intermediary. Old proxy servers do not know about WebSocket, they may see "strange" headers and abort the connection.
|
||||
|
||||
On the other hand, `wss://` is WebSocket over TLS, (same as HTTPS is HTTP over TLS), the transport security layer encrypts the data at sender and decrypts at the receiver, so it passes encrypted through proxies. They can't see what's inside and let it through.
|
||||
```
|
||||
|
||||
Once the socket is created, we should listen to events on it. There are totally 4 events:
|
||||
- **`open`** -- connection established,
|
||||
- **`message`** -- data received,
|
||||
- **`error`** -- websocket error,
|
||||
- **`close`** -- connection closed.
|
||||
|
||||
...And if we'd like to send something, then `socket.send(data)` will do that.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```js run
|
||||
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");
|
||||
|
||||
socket.onopen = function(e) {
|
||||
alert("[open] Connection established, send -> server");
|
||||
socket.send("My name is John");
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
alert(`[message] Data received: ${event.data} <- server`);
|
||||
};
|
||||
|
||||
socket.onclose = function(event) {
|
||||
if (event.wasClean) {
|
||||
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
||||
} else {
|
||||
// e.g. server process killed or network down
|
||||
// event.code is usually 1006 in this case
|
||||
alert('[close] Connection died');
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = function(error) {
|
||||
alert(`[error] ${error.message}`);
|
||||
};
|
||||
```
|
||||
|
||||
For demo purposes, there's a small server [server.js](demo/server.js) written in Node.js, for the example above, running. It responds with "hello", then waits 5 seconds and closes the connection.
|
||||
|
||||
So you'll see events `open` -> `message` -> `close`.
|
||||
|
||||
That's actually it, we can talk WebSocket already. Quite simple, isn't it?
|
||||
|
||||
Now let's talk more in-depth.
|
||||
|
||||
## Opening a websocket
|
||||
|
||||
When `new WebSocket(url)` is created, it starts an HTTP handshake (HTTPS for `wss://`).
|
||||
|
||||
The browser asks the server: "Do you support Websocket?" And if the server says "yes", then the talk continues in WebSocket protocol, which is not HTTP at all.
|
||||
|
||||

|
||||
|
||||
Here's an example of browser request for `new WebSocket("wss://javascript.info/chat")`.
|
||||
|
||||
```
|
||||
GET /chat
|
||||
Host: javascript.info
|
||||
Origin: https://javascript.info
|
||||
Connection: Upgrade
|
||||
Upgrade: websocket
|
||||
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
|
||||
Sec-WebSocket-Version: 13
|
||||
```
|
||||
|
||||
- `Origin` -- the origin of the client page. WebSocket is cross-origin by nature. There are no special headers or other limitations. Old servers are unable to handle WebSocket anyway, so there are no compabitility issues. But `Origin` header is important, as it allows the server to decide whether or not to talk WebSocket with this website.
|
||||
- `Connection: Upgrade` -- signals that the client would like to change the protocol.
|
||||
- `Upgrade: websocket` -- the requested protocol is "websocket".
|
||||
- `Sec-WebSocket-Key` -- a random browser-generated key for security.
|
||||
- `Sec-WebSocket-Version` -- WebSocket protocol version, 13 is the current one.
|
||||
|
||||
```smart header="WebSocket handshake can't be emulated"
|
||||
We can't use `XMLHttpRequest` or `fetch` to make this kind of HTTP-request, because Javascript is not allowed to set these headers.
|
||||
```
|
||||
|
||||
If the server agrees to switch to WebSocket, it should send code 101 response:
|
||||
|
||||
```
|
||||
101 Switching Protocols
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
|
||||
```
|
||||
|
||||
Here `Sec-WebSocket-Accept` is `Sec-WebSocket-Key`, recoded using a special algorithm. The browser uses it to make sure that the response corresponds to the request.
|
||||
|
||||
Afterwards, the data is transfered using WebSocket protocol, we'll see its structure ("frames") soon. And that's not HTTP at all.
|
||||
|
||||
### Extensions and subprotocols
|
||||
|
||||
There may be additional headers `Sec-WebSocket-Extensions` and `Sec-WebSocket-Protocol` that describe extensions and subprotocols.
|
||||
|
||||
For instance:
|
||||
|
||||
- `Sec-WebSocket-Extensions: deflate-frame` means that the browser supports data compression. An extension is something related to transferring the data, not data itself.
|
||||
|
||||
- `Sec-WebSocket-Protocol: soap, wamp` means that we're going to transfer not just any data, but the data in [SOAP](http://en.wikipedia.org/wiki/SOAP) or WAMP ("The WebSocket Application Messaging Protocol") protocols. WebSocket subprotocols are registered in the [IANA catalogue](http://www.iana.org/assignments/websocket/websocket.xml).
|
||||
|
||||
`Sec-WebSocket-Extensions` is sent by the browser automatically, with a list of possible extensions it supports.
|
||||
|
||||
`Sec-WebSocket-Protocol` is depends on us: we decide what kind of data we send. The second optional parameter of `new WebSocket` lists subprotocols:
|
||||
|
||||
```js
|
||||
let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
|
||||
```
|
||||
|
||||
The server should respond with a list of protocls and extensions that it agrees to use.
|
||||
|
||||
For example, the request:
|
||||
|
||||
```
|
||||
GET /chat
|
||||
Host: javascript.info
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Origin: https://javascript.info
|
||||
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
|
||||
Sec-WebSocket-Version: 13
|
||||
*!*
|
||||
Sec-WebSocket-Extensions: deflate-frame
|
||||
Sec-WebSocket-Protocol: soap, wamp
|
||||
*/!*
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
101 Switching Protocols
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
|
||||
*!*
|
||||
Sec-WebSocket-Extensions: deflate-frame
|
||||
Sec-WebSocket-Protocol: soap
|
||||
*/!*
|
||||
```
|
||||
|
||||
Here the server responds that it supports the extension `deflate-frame`, and only SOAP of the requested subprotocols.
|
||||
|
||||
## WebSocket data
|
||||
|
||||
WebSocket communication consists of "frames" that can be sent from either side:
|
||||
|
||||
- "text frames" -- contain text data that parties send to each other.
|
||||
- "binary data frames" -- contain binary data that parties send to each other.
|
||||
- "ping/pong frames" are used to check the connection, sent from the server, the browser responds to these automatically.
|
||||
- "connection close frame" and a few other service frames.
|
||||
|
||||
In the browser, we only care about text or binary frames.
|
||||
|
||||
**WebSocket `.send()` can send either text or binary data, doesn't matter.**
|
||||
|
||||
For sending, `socket.send(body)` allows strings or any binary format, including `Blob`, `ArrayBuffer`, etc. No settings required: just send it out.
|
||||
|
||||
**Textual data always comes as string. For receiving binary data, we can choose between `Blob` and `ArrayBuffer` formats.**
|
||||
|
||||
The `socket.bufferType` is `"blob"` by default, so binary data comes in Blobs.
|
||||
|
||||
[Blob](info:blob) is a high-level binary object, it directly integrates with `<a>`, `<img>` and other tags, so that's a sane default. But for binary processing, to access individual data bytes, we can change it to `"arraybuffer"`:
|
||||
|
||||
```js
|
||||
socket.bufferType = "arraybuffer";
|
||||
socket.onmessage = (event) => {
|
||||
// event.data is either a string (if text) or arraybuffer (if binary)
|
||||
};
|
||||
```
|
||||
|
||||
## Rate limiting
|
||||
|
||||
Imagine, our app is generating a lot of data to send. But network connection is not that fast. The user may be on a mobile, in rural area.
|
||||
|
||||
We can call `socket.send(data)` again and again. But the data will be buffered in memory and sent out only as fast as network speed allows.
|
||||
|
||||
The `socket.bufferedAmount` property stores how many bytes are buffered at this moment, waiting to be sent over the network.
|
||||
|
||||
We can examine it to see whether the socket is actually available for transmission.
|
||||
|
||||
```js
|
||||
// every 100ms examine the socket and send more data only if no data buffered
|
||||
setInterval(() => {
|
||||
if (socket.bufferedAmount == 0) {
|
||||
socket.send(moreData());
|
||||
}
|
||||
}, 100);
|
||||
```
|
||||
|
||||
|
||||
## Connection close
|
||||
|
||||
Normally, when a party wants to close the connection (both browser and server have equal rights), they send a "connection close frame" with a numeric code and a textual reason.
|
||||
|
||||
The method is:
|
||||
```js
|
||||
socket.close([code], [reason]);
|
||||
```
|
||||
|
||||
Then the other party in `close` event handle can get the code and the reason, e.g.:
|
||||
|
||||
```js
|
||||
// one party:
|
||||
socket.close(1000, "Work complete");
|
||||
|
||||
// another party
|
||||
socket.onclose = event => {
|
||||
// event.code === 1000
|
||||
// event.reason === "Work complete"
|
||||
// event.wasClean === true (clean close)
|
||||
};
|
||||
```
|
||||
|
||||
The `code` is not just any number, but a special WebSocket closing code.
|
||||
|
||||
Most common values:
|
||||
|
||||
- `1000` -- the default, normal closure,
|
||||
- `1006` -- can't set such code manually, indicates that the connection was broken (no close frame).
|
||||
|
||||
There are other codes like:
|
||||
|
||||
- `1001` -- the party is going away, e.g. server is shutting down, or a browser leaves the page,
|
||||
- `1009` -- the message is too big to process,
|
||||
- `1011` -- unexpected error on server,
|
||||
- ...and so on.
|
||||
|
||||
Please refer to the [RFC6455, §7.4.1](https://tools.ietf.org/html/rfc6455#section-7.4.1) for the full list.
|
||||
|
||||
WebSocket codes are somewhat like HTTP codes, but different. In particular, an codes less than `1000` are reserved, there'll be an error if we try to set such a code.
|
||||
|
||||
```js
|
||||
// in case connection is broken
|
||||
socket.onclose = event => {
|
||||
// event.code === 1006
|
||||
// event.reason === ""
|
||||
// event.wasClean === false (no closing frame)
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Connection state
|
||||
|
||||
To get connection state, additionally there's `socket.readyState` property with values:
|
||||
|
||||
- **`0`** -- "CONNECTING": the connection has not yet been established,
|
||||
- **`1`** -- "OPEN": communicating,
|
||||
- **`2`** -- "CLOSING": the connection is closing,
|
||||
- **`3`** -- "CLOSED": the connection is closed.
|
||||
|
||||
|
||||
## Chat example
|
||||
|
||||
Let's review a chat example using browser WebSocket API and Node.JS WebSocket module <https://github.com/websockets/ws>.
|
||||
|
||||
HTML: there's a `<form>` to send messages and a `<div>` for incoming messages:
|
||||
|
||||
```html
|
||||
<!-- message form -->
|
||||
<form name="publish">
|
||||
<input type="text" name="message">
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
|
||||
<!-- div with messages -->
|
||||
<div id="messages"></div>
|
||||
```
|
||||
|
||||
Javascript is also simple. We open a socket, then on form submission -- `socket.send(message)`, on incoming message -- append it to `div#messages`:
|
||||
|
||||
```js
|
||||
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");
|
||||
|
||||
// send message from the form
|
||||
document.forms.publish.onsubmit = function() {
|
||||
let outgoingMessage = this.message.value;
|
||||
|
||||
socket.send(outgoingMessage);
|
||||
return false;
|
||||
};
|
||||
|
||||
// show message in div#messages
|
||||
socket.onmessage = function(event) {
|
||||
let message = event.data;
|
||||
|
||||
let messageElem = document.createElement('div');
|
||||
messageElem.textContent = message;
|
||||
document.getElementById('messages').prepend(messageElem);
|
||||
}
|
||||
```
|
||||
|
||||
Server-side code is a little bit beyound our scope here. We're using browser WebSocket API, a server may have another library.
|
||||
|
||||
Still it can also be pretty simple. We'll use Node.JS with <https://github.com/websockets/ws> module for websockets.
|
||||
|
||||
The algorithm will be:
|
||||
1. Create `clients = new Set()` -- a set of sockets.
|
||||
2. For each accepted websocket, `clients.add(socket)` and listen for its messages.
|
||||
3. When a message received: iterate over clients and send it to everyone.
|
||||
4. When a connection is closed: `clients.delete(socket)`.
|
||||
|
||||
```js
|
||||
const ws = new require('ws');
|
||||
const wss = new ws.Server({noServer: true});
|
||||
|
||||
const clients = new Set();
|
||||
|
||||
http.createServer((req, res) => {
|
||||
// in real project we have additional code here to handle non-websocket requests
|
||||
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
|
||||
});
|
||||
|
||||
function onSocketConnect(ws) {
|
||||
clients.add(ws);
|
||||
|
||||
ws.on('message', function(message) {
|
||||
message = message.slice(0, 50); // max message length will be 50
|
||||
|
||||
for(let client of clients) {
|
||||
client.send(message);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', function() {
|
||||
clients.delete(ws);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Here's the working example:
|
||||
|
||||
[iframe src="chat" height="100" zip]
|
||||
|
||||
You can also download it (upper-right button in the iframe) and run locally. Just don't forget to install [Node.js](https://nodejs.org/en/) and `npm install ws` before running.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
WebSocket is a modern way to have persisten browser-server connections.
|
||||
|
||||
- WebSockets don't have cross-origin limitations.
|
||||
- They are well-supported in browsers.
|
||||
- Can send/receive strings and binary data.
|
||||
|
||||
The API is simple.
|
||||
|
||||
Methods:
|
||||
- `socket.send(data)`,
|
||||
- `socket.close([code], [reason])`.
|
||||
|
||||
Events:
|
||||
- `open`,
|
||||
- `message`,
|
||||
- `error`,
|
||||
- `close`.
|
||||
|
||||
WebSocket by itself does not include reconnection, authentication and many other high-level mechanisms. So there are client/server libraries add them. But it's also possible to implement these manually and integrate WebSockets with an existing site.
|
||||
|
||||
For integration purposes, a WebSocket server is usually running in parallel with the main server, and they share a single database. Requests to WebSocket use `wss://ws.site.com`, a subdomain that leads to WebSocket server, while `https://site.com` goes to the main HTTP-server.
|
||||
|
||||
Surely, other ways of integration are also possible. Many servers (such as Node.js) can support both HTTP and WebSocket protocols.
|
39
5-network/08-websocket/chat.view/index.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<form name="publish">
|
||||
<input type="text" name="message" maxlength="50"/>
|
||||
<input type="submit" value="Send"/>
|
||||
</form>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
let url = location.host == 'localhost' ?
|
||||
'ws://localhost:8080/ws' : location.host == 'javascript.local' ?
|
||||
`ws://javascript.local/article/websocket/chat/ws` : // dev integration with local site
|
||||
`wss://javascript.info/article/websocket/chat/ws`; // prod integration with javascript.info
|
||||
|
||||
let socket = new WebSocket(url);
|
||||
|
||||
// send message from the form
|
||||
document.forms.publish.onsubmit = function() {
|
||||
let outgoingMessage = this.message.value;
|
||||
|
||||
socket.send(outgoingMessage);
|
||||
return false;
|
||||
};
|
||||
|
||||
// handle incoming messages
|
||||
socket.onmessage = function(event) {
|
||||
let incomingMessage = event.data;
|
||||
showMessage(incomingMessage);
|
||||
};
|
||||
|
||||
socket.onclose = event => console.log(`Closed ${event.code}`);
|
||||
|
||||
// show message in div#messages
|
||||
function showMessage(message) {
|
||||
let messageElem = document.createElement('div');
|
||||
messageElem.textContent = message;
|
||||
document.getElementById('messages').prepend(messageElem);
|
||||
}
|
||||
</script>
|
61
5-network/08-websocket/chat.view/server.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
Before running:
|
||||
> npm install ws
|
||||
Then:
|
||||
> node server.js
|
||||
> open http://localhost:8080 in the browser
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const ws = new require('ws');
|
||||
|
||||
const wss = new ws.Server({noServer: true});
|
||||
|
||||
const clients = new Set();
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.url == '/ws' && req.headers.upgrade &&
|
||||
req.headers.upgrade.toLowerCase() == 'websocket' &&
|
||||
// can be Connection: keep-alive, Upgrade
|
||||
req.headers.connection.match(/\bupgrade\b/i)) {
|
||||
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
|
||||
} else if (req.url == '/') { // index.html
|
||||
fs.createReadStream('./index.html').pipe(res);
|
||||
} else { // page not found
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
function onSocketConnect(ws) {
|
||||
clients.add(ws);
|
||||
log(`new connection`);
|
||||
|
||||
ws.on('message', function(message) {
|
||||
log(`message received: ${message}`);
|
||||
|
||||
message = message.slice(0, 50); // max message length will be 50
|
||||
|
||||
for(let client of clients) {
|
||||
client.send(message);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', function() {
|
||||
log(`connection closed`);
|
||||
clients.delete(ws);
|
||||
});
|
||||
}
|
||||
|
||||
let log;
|
||||
if (!module.parent) {
|
||||
log = console.log;
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
// to embed into javascript.info
|
||||
log = function() {};
|
||||
// log = console.log;
|
||||
exports.accept = accept;
|
||||
}
|
34
5-network/08-websocket/demo.view/server.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const http = require('http');
|
||||
const ws = require('ws');
|
||||
|
||||
const wss = new ws.Server({noServer: true});
|
||||
|
||||
function accept(req, res) {
|
||||
// all incoming requests must be websockets
|
||||
if (!req.headers.upgrade || req.headers.upgrade.toLowerCase() != 'websocket') {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// can be Connection: keep-alive, Upgrade
|
||||
if (req.headers.connection.match(/\bupgrade\b/i)) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onConnect);
|
||||
}
|
||||
|
||||
function onConnect(ws) {
|
||||
ws.on('message', function (message) {
|
||||
let name = message.match(/\w+$/) || "Guest";
|
||||
ws.send(`Hello, ${name}!`);
|
||||
|
||||
setTimeout(() => ws.close(1000, "Bye!"), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
BIN
5-network/08-websocket/websocket-handshake.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
5-network/08-websocket/websocket-handshake@2x.png
Normal file
After Width: | Height: | Size: 42 KiB |
2
5-network/index.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
# Network requests
|