updates
|
@ -1,9 +1,22 @@
|
|||
|
||||
# Fetch: Basics
|
||||
# Fetch
|
||||
|
||||
Method `fetch()` is the modern way of sending requests over HTTP.
|
||||
JavaScript can send network requests to the server and load new information whenever is needed.
|
||||
|
||||
It evolved for several years and continues to improve, right now the support is pretty solid among browsers.
|
||||
For example, we can:
|
||||
|
||||
- Submit an order,
|
||||
- Load user information,
|
||||
- Receive latest updates from the server,
|
||||
- ...etc.
|
||||
|
||||
...And all of that without reloading the page!
|
||||
|
||||
There's an umbrella term "AJAX" (abbreviated <b>A</b>synchronous <b>J</b>avascript <b>A</b>nd <b>X</b>ml) for that. We don't have to use XML though: the term comes from old times, that's why it's here.
|
||||
|
||||
There are multiple ways to send a network request and get information from the server.
|
||||
|
||||
The `fetch()` method is modern and versatile, so we'll start with it. It evolved for several years and continues to improve, right now the support is pretty solid among browsers.
|
||||
|
||||
The basic syntax is:
|
||||
|
||||
|
@ -18,7 +31,7 @@ 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.**
|
||||
**First, the `promise` resolves with an object of the built-in [Response](https://fetch.spec.whatwg.org/#response-class) class as soon as the server responds with headers.**
|
||||
|
||||
|
||||
So we can check HTTP status, to see whether it is successful or not, check headers, but don't have the body yet.
|
||||
|
@ -43,13 +56,13 @@ if (response.ok) { // if HTTP-status is 200-299
|
|||
}
|
||||
```
|
||||
|
||||
To get the response body, we need to use an additional method call.
|
||||
**Second, to get the response body, we need to use an additional method call.**
|
||||
|
||||
`Response` provides multiple promise-based methods to access the body in various formats:
|
||||
|
||||
- **`response.json()`** -- parse the response as JSON object,
|
||||
- **`response.text()`** -- return the response as text,
|
||||
- **`response.formData()`** -- return the response as FormData object (form/multipart encoding),
|
||||
- **`response.formData()`** -- return the response as `FormData` object (form/multipart encoding, explained in the [next chapter](info:formdata)),
|
||||
- **`response.blob()`** -- return the response as [Blob](info:blob) (binary data with type),
|
||||
- **`response.arrayBuffer()`** -- return the response as [ArrayBuffer](info:arraybuffer-binary-arrays) (pure binary data),
|
||||
- additionally, `response.body` is a [ReadableStream](https://streams.spec.whatwg.org/#rs-class) object, it allows to read the body chunk-by-chunk, we'll see an example later.
|
||||
|
@ -74,12 +87,16 @@ fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commi
|
|||
.then(commits => alert(commits[0].author.login));
|
||||
```
|
||||
|
||||
To get the text:
|
||||
```js
|
||||
let text = await response.text();
|
||||
To get the text, `await response.text()` instead of `.json()`:
|
||||
```js run async
|
||||
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
|
||||
|
||||
let text = await response.text(); // read response body as text
|
||||
|
||||
alert(text.slice(0, 80) + '...');
|
||||
```
|
||||
|
||||
And for the binary example, let's fetch and show an image (see chapter [Blob](info:blob) for details about operations on blobs):
|
||||
As a show-case for reading in binary format, let's fetch and show an image (see chapter [Blob](info:blob) for details about operations on blobs):
|
||||
|
||||
```js async run
|
||||
let response = await fetch('/article/fetch/logo-fetch.svg');
|
||||
|
@ -96,10 +113,10 @@ document.body.append(img);
|
|||
// show it
|
||||
img.src = URL.createObjectURL(blob);
|
||||
|
||||
setTimeout(() => { // hide after two seconds
|
||||
setTimeout(() => { // hide after three seconds
|
||||
img.remove();
|
||||
URL.revokeObjectURL(img.src);
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
````warn
|
||||
|
@ -178,9 +195,7 @@ To make a `POST` request, or a request with another method, we need to use `fetc
|
|||
|
||||
Let's see examples.
|
||||
|
||||
## Submit JSON
|
||||
|
||||
This code submits a `user` object as JSON:
|
||||
For example, this code submits `user` object as JSON:
|
||||
|
||||
```js run async
|
||||
let user = {
|
||||
|
@ -189,7 +204,7 @@ let user = {
|
|||
};
|
||||
|
||||
*!*
|
||||
let response = await fetch('/article/fetch-basics/post/user', {
|
||||
let response = await fetch('/article/fetch/post/user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
|
@ -202,38 +217,9 @@ 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.
|
||||
Please note, if the body is a string, then `Content-Type` is set to `text/plain;charset=UTF-8` by default. So we use `headers` option to send `application/json` instead, that's the correct content type for JSON-encoded data.
|
||||
|
||||
## 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
|
||||
## Sending an image
|
||||
|
||||
We can also submit binary data directly using `Blob` or `BufferSource`.
|
||||
|
||||
|
@ -254,7 +240,7 @@ For example, here's a `<canvas>` where we can draw by moving a mouse. A click on
|
|||
|
||||
async function submit() {
|
||||
let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
|
||||
let response = await fetch('/article/fetch-basics/post/image', {
|
||||
let response = await fetch('/article/fetch/post/image', {
|
||||
method: 'POST',
|
||||
body: blob
|
||||
});
|
||||
|
@ -273,7 +259,7 @@ The `submit()` function can be rewritten without `async/await` like this:
|
|||
```js
|
||||
function submit() {
|
||||
canvasElem.toBlob(function(blob) {
|
||||
fetch('/article/fetch-basics/post/image', {
|
||||
fetch('/article/fetch/post/image', {
|
||||
method: 'POST',
|
||||
body: blob
|
||||
})
|
||||
|
@ -283,48 +269,6 @@ function submit() {
|
|||
}
|
||||
```
|
||||
|
||||
## 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`:
|
||||
|
@ -349,13 +293,13 @@ Response properties:
|
|||
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.formData()`** -- return the response as `FormData` object (form/multipart encoding, see the next chapter),
|
||||
- **`response.blob()`** -- return the response as [Blob](info:blob) (binary data with type),
|
||||
- **`response.arrayBuffer()`** -- return the response as [ArrayBuffer](info:arraybuffer-binary-arrays) (pure binary data),
|
||||
|
||||
Fetch options so far:
|
||||
- `method` -- HTTP-method,
|
||||
- `headers` -- an object with request headers (not any header is allowed),
|
||||
- `body` -- string/FormData/BufferSource/Blob/UrlSearchParams data to submit.
|
||||
- `body` -- `string`, `FormData`, `BufferSource`, `Blob` or `UrlSearchParams` object to send.
|
||||
|
||||
In the next chapters we'll see more options and use cases.
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
@ -2,7 +2,6 @@ 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();
|
||||
|
@ -22,28 +21,6 @@ router.post('/image', async (ctx) => {
|
|||
};
|
||||
});
|
||||
|
||||
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())
|
183
5-network/02-formdata/article.md
Normal file
|
@ -0,0 +1,183 @@
|
|||
|
||||
# FormData
|
||||
|
||||
This chapter is about sending HTML forms: with or without files, with additional fields and so on. [FormData](https://xhr.spec.whatwg.org/#interface-formdata) is the object to handle that.
|
||||
|
||||
The constructor is:
|
||||
```js
|
||||
let formData = new FormData([form]);
|
||||
```
|
||||
|
||||
If HTML `form` element is provided, it automatically captures its fields.
|
||||
|
||||
Network methods, such as `fetch` can use `FormData` objects as a body, they are automatically encoded and sent with `Content-Type: form/multipart`.
|
||||
|
||||
So from the server point of view, that's a usual form submission.
|
||||
|
||||
## Sending a simple form
|
||||
|
||||
Let's send a simple form first.
|
||||
|
||||
As you can see, that's almost one-liner:
|
||||
|
||||
```html run autorun
|
||||
<form id="formElem">
|
||||
<input type="text" name="name" value="John">
|
||||
<input type="text" name="surname" value="Smith">
|
||||
<input type="submit">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
formElem.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let response = await fetch('/article/formdata/post/user', {
|
||||
method: 'POST',
|
||||
*!*
|
||||
body: new FormData(formElem)
|
||||
*/!*
|
||||
});
|
||||
|
||||
let result = await response.json();
|
||||
|
||||
alert(result.message);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## FormData Methods
|
||||
|
||||
We can modify fields in `FormData` with methods:
|
||||
|
||||
- `formData.append(name, value)` - add a form field with the given `name` and `value`,
|
||||
- `formData.append(name, blob, fileName)` - add a field as if it were `<input type="file">`, the third argument `fileName` sets file name (not form field name), as it it were a name of the file in user's filesystem,
|
||||
- `formData.delete(name)` - remove the field with the given `name`,
|
||||
- `formData.get(name)` - get the value of the field with the given `name`,
|
||||
- `formData.has(name)` - if there exists a field with the given `name`, returns `true`, otherwise `false`
|
||||
|
||||
A form is technically allowed to have many fields with the same `name`, so multiple calls to `append` add more same-named fields.
|
||||
|
||||
There's also method `set`, with the same syntax as `append`. The difference is that `.set` removes all fields with the given `name`, and then appends a new field. So it makes sure there's only field with such `name`:
|
||||
|
||||
- `formData.set(name, value)`,
|
||||
- `formData.set(name, blob, fileName)`.
|
||||
|
||||
|
||||
Also we can iterate over formData fields using `for..of` loop:
|
||||
|
||||
```js run
|
||||
let formData = new FormData();
|
||||
formData.append('key1', 'value1');
|
||||
formData.append('key2', 'value2');
|
||||
|
||||
// List key/value pairs
|
||||
for(let [name, value] of formData) {
|
||||
alert(`${name} = ${value}`); // key1=value1, then key2=value2
|
||||
}
|
||||
```
|
||||
|
||||
## Sending a form with a file
|
||||
|
||||
The form is always sent as `Content-Type: form/multipart`. So, `<input type="file">` fields are sent also, similar to a usual form submission.
|
||||
|
||||
Here's an example with such form:
|
||||
|
||||
```html run autorun
|
||||
<form id="formElem">
|
||||
<input type="text" name="firstName" value="John">
|
||||
Picture: <input type="file" name="picture" accept="image/*">
|
||||
<input type="submit">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
formElem.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let response = await fetch('/article/formdata/post/user-avatar', {
|
||||
method: 'POST',
|
||||
*!*
|
||||
body: new FormData(formElem)
|
||||
*/!*
|
||||
});
|
||||
|
||||
let result = await response.json();
|
||||
|
||||
alert(result.message);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Sending a form with blob
|
||||
|
||||
As we've seen in the chapter <info:fetch>, sending a dynamically generated `Blob`, e.g. an image, is easy. We can supply it directly as `fetch` body.
|
||||
|
||||
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.
|
||||
|
||||
This example submits an image from `<canvas>`, along with some other fields, using `FormData`:
|
||||
|
||||
```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 imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
|
||||
|
||||
*!*
|
||||
let formData = new FormData();
|
||||
formData.append("firstName", "John");
|
||||
formData.append("image", imageBlob, "image.png");
|
||||
*/!*
|
||||
|
||||
let response = await fetch('/article/formdata/post/image-form', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
let result = await response.json();
|
||||
alert(result.message);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
Please note how the image `Blob` is added:
|
||||
|
||||
```js
|
||||
formData.append("image", imageBlob, "image.png");
|
||||
```
|
||||
|
||||
That's same as if there were `<input type="file" name="image">` in the form, and the visitor submitted a file `image.png` from their filesystem.
|
||||
|
||||
## Summary
|
||||
|
||||
[FormData](https://xhr.spec.whatwg.org/#interface-formdata) objects are used to read HTML form and submit it using `fetch` or another network method.
|
||||
|
||||
We can either create `new FormData(form)` from an HTML form, or create an empty object, and then append fields with methods:
|
||||
|
||||
- `formData.append(name, value)`
|
||||
- `formData.append(name, blob, fileName)`
|
||||
- `formData.set(name, value)`
|
||||
- `formData.set(name, blob, fileName)`
|
||||
|
||||
Two peculiarities here:
|
||||
1. The `set` method removes fields with the same name, `append` doesn't.
|
||||
2. To send a file, 3-argument syntax is needed, the last argument is a file name, that normally is taken from user filesystem for `<input type="file">`.
|
||||
|
||||
Other methods are:
|
||||
|
||||
- `formData.delete(name)`
|
||||
- `formData.get(name)`
|
||||
- `formData.has(name)`
|
||||
|
||||
That's it!
|
78
5-network/02-formdata/post.view/server.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
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-form', async (ctx) => {
|
||||
|
||||
let files = [];
|
||||
const { fields } = await busboy(ctx.req, {
|
||||
onFile(fieldname, file, filename, encoding, mimetype) {
|
||||
// read all file stream to continue
|
||||
let length = 0;
|
||||
file.on('data', function(data) {
|
||||
length += data.length;
|
||||
});
|
||||
file.on('end', () => {
|
||||
files.push({
|
||||
fieldname,
|
||||
filename,
|
||||
length
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
message: `Image saved, firstName: ${fields.firstName}, size:${files[0].length}, fileName: ${files[0].filename}.`
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
router.post('/user-avatar', async (ctx) => {
|
||||
|
||||
let files = [];
|
||||
const { fields } = await busboy(ctx.req, {
|
||||
onFile(fieldname, file, filename, encoding, mimetype) {
|
||||
// read all file stream to continue
|
||||
let length = 0;
|
||||
file.on('data', function(data) {
|
||||
length += data.length;
|
||||
});
|
||||
file.on('end', () => {
|
||||
files.push({
|
||||
fieldname,
|
||||
filename,
|
||||
length
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
message: `User with picture, firstName: ${fields.firstName}, picture 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();
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
|
||||
# Fetch: Download progress
|
||||
|
||||
The `fetch` method allows to track download progress.
|
||||
The `fetch` method allows to track *download* progress.
|
||||
|
||||
Please note: there's currently no way for `fetch` to track upload progress. For that purpose, please use [XMLHttpRequest](info:xmlhttprequest).
|
||||
Please note: there's currently no way for `fetch` to track *upload* progress. For that purpose, please use [XMLHttpRequest](info:xmlhttprequest), we'll cover it later.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Here's the sketch of code that uses it to read response:
|
||||
Unlike `response.text()`, `response.json()` and other methods, `response.body` gives full control over the reading process, and we can see how much is consumed at the moment.
|
||||
|
||||
Here's the sketch of code that reads the reponse from `response.body`:
|
||||
|
||||
```js
|
||||
// instead of response.json() and other methods
|
||||
|
@ -27,13 +29,13 @@ while(true) {
|
|||
}
|
||||
```
|
||||
|
||||
So, we loop, while `await reader.read()` returns response chunks.
|
||||
So, we read response chunks in the loop, while `await reader.read()` returns them. When it's done, no more data, so we're done.
|
||||
|
||||
A chunk has two properties:
|
||||
The result of `await reader.read()` call is an object with 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.
|
||||
To log progress, we just need for every `value` add its length to the counter.
|
||||
|
||||
Here's the full code to get response and log the progress, more explanations follow:
|
||||
|
||||
|
@ -96,9 +98,11 @@ Let's explain that step-by-step:
|
|||
|
||||
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);
|
||||
```
|
||||
What if we need binary content instead of JSON? That's even simpler. Replace steps 4 and 5 with a single call to a blob from 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.
|
||||
At we end we have the result (as a string or a blob, whatever is convenient), and progress-tracking in the process.
|
||||
|
||||
Once again, please note, that's not for *upload* progress (no way now with `fetch`), only for *download* progress.
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
@ -30,7 +30,7 @@ let promise = fetch(url, {
|
|||
|
||||
An impressive list, right?
|
||||
|
||||
We fully covered `method`, `headers` and `body` in the chapter <info:fetch-basics>.
|
||||
We fully covered `method`, `headers` and `body` in the chapter <info:fetch>.
|
||||
|
||||
The `signal` option is covered in <info:fetch-abort>.
|
||||
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
@ -1,105 +0,0 @@
|
|||
|
||||
# URL objects
|
||||
|
||||
The built-in [URL](https://url.spec.whatwg.org/#api) class provides a convenient interface for creating and parsing URLs.
|
||||
|
||||
There are no networking methods that require exactly an `URL` object, strings are good enough. So technically we don't have to use `URL`. 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 URL string or path (if base is set, see below).
|
||||
- **`base`** -- an optional base, if set and `url` has only path, then the URL is generated relative to `base`.
|
||||
|
||||
For example, these two URLs are same:
|
||||
|
||||
```js run
|
||||
let url1 = new URL('https://javascript.info/profile/admin');
|
||||
let url2 = new URL('/profile/admin', 'https://javascript.info');
|
||||
|
||||
alert(url1); // https://javascript.info/profile/admin
|
||||
alert(url2); // https://javascript.info/profile/admin
|
||||
```
|
||||
|
||||
Go to the path relative to the current URL:
|
||||
|
||||
```js run
|
||||
let url = new URL('https://javascript.info/profile/admin');
|
||||
let testerUrl = new URL('tester', url);
|
||||
|
||||
alert(testerUrl); // https://javascript.info/profile/tester
|
||||
```
|
||||
|
||||
|
||||
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` - a string of parameters, starts with the question mark `?`
|
||||
- `hash` starts with the hash character `#`
|
||||
- there are also `user` and `password` properties if HTTP authentication is present.
|
||||
|
||||
|
||||
```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=JavaScript`.
|
||||
|
||||
They must be correctly encoded to include non-latin charcters, spaces etc.
|
||||
|
||||
Some time ago, before `URL` objects appeared, we'd use built-in functions `encodeURIComponent/decodeURIComponent`. They have some problems, but now that doesn't matter.
|
||||
|
||||
There's URL property for that: `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 (if many, e.g. `?user=John&user=Pete`),
|
||||
- **`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!'); // added parameter with a space and !
|
||||
|
||||
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
|
||||
}
|
||||
```
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 35 KiB |
206
5-network/07-url/article.md
Normal file
|
@ -0,0 +1,206 @@
|
|||
|
||||
# URL objects
|
||||
|
||||
The built-in [URL](https://url.spec.whatwg.org/#api) class provides a convenient interface for creating and parsing URLs.
|
||||
|
||||
There are no networking methods that require exactly an `URL` object, strings are good enough. So technically we don't have to use `URL`. 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 URL string or path (if base is set, see below).
|
||||
- **`base`** -- an optional base, if set and `url` has only path, then the URL is generated relative to `base`.
|
||||
|
||||
For example, these two URLs are same:
|
||||
|
||||
```js run
|
||||
let url1 = new URL('https://javascript.info/profile/admin');
|
||||
let url2 = new URL('/profile/admin', 'https://javascript.info');
|
||||
|
||||
alert(url1); // https://javascript.info/profile/admin
|
||||
alert(url2); // https://javascript.info/profile/admin
|
||||
```
|
||||
|
||||
Go to the path relative to the current URL:
|
||||
|
||||
```js run
|
||||
let url = new URL('https://javascript.info/profile/admin');
|
||||
let testerUrl = new URL('tester', url);
|
||||
|
||||
alert(testerUrl); // https://javascript.info/profile/tester
|
||||
```
|
||||
|
||||
|
||||
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` - a string of parameters, starts with the question mark `?`
|
||||
- `hash` starts with the hash character `#`
|
||||
- there may be also `user` and `password` properties if HTTP authentication is present: `http://login:password@site.com` (not painted above, rarely used).
|
||||
|
||||
|
||||
```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=JavaScript`.
|
||||
|
||||
We can provide them in the URL string:
|
||||
|
||||
```js
|
||||
new URL('https://google.com/search?query=JavaScript')
|
||||
```
|
||||
|
||||
...But that's not good due to encoding issues. Parameters need to be encoded if they contain spaces, non-latin letterrs, etc (more about that below).
|
||||
|
||||
So there's URL property for that: `url.searchParams`, 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 the same `name` (that's possible, e.g. `?user=John&user=Pete`),
|
||||
- **`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`.
|
||||
|
||||
For example:
|
||||
|
||||
```js run
|
||||
let url = new URL('https://google.com/search');
|
||||
url.searchParams.set('q', 'test me!'); // added parameter with a space and !
|
||||
|
||||
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?q=test+me%21&tbs=qdr%3Ay
|
||||
|
||||
// iterate over search parameters (decoded)
|
||||
for(let [name, value] of url.searchParams) {
|
||||
alert(`${name}=${value}`); // q=test me!, then tbs=qdr:y
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Encoding
|
||||
|
||||
There's a standard [RFC3986](https://tools.ietf.org/html/rfc3986) that defines which characters are allowed and which are not.
|
||||
|
||||
Those that are not allowed, must be encoded, for instance non-latin letters and spaces - replaced with their UTF-8 codes, prefixed by `%`, such as `%20` (a space can be encoded by `+`, for historical reasons that's allowed in URL too).
|
||||
|
||||
The good news is that `URL` objects handle all that automatically. We just supply all parameters unencoded, and then convert the URL to the string:
|
||||
|
||||
```js run
|
||||
// using some cyrillic characters for this example
|
||||
|
||||
let url = new URL('https://ru.wikipedia.org/wiki/Тест');
|
||||
|
||||
url.searchParams.set('key', 'ъ');
|
||||
alert(url); //https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82?key=%D1%8A
|
||||
```
|
||||
As you can see, both `Тест` in the url path and `ъ` in the parameter are encoded.
|
||||
|
||||
### Encoding strings
|
||||
|
||||
If we're using strings instead of URL objects, then we can encode manually using built-in functions:
|
||||
|
||||
- [encodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) - encode URL as a whole.
|
||||
- [encodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) - decode it back.
|
||||
- [encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) - encode URL components, such as search parameters, or a hash, or a pathname.
|
||||
- [decodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) - decodes it back.
|
||||
|
||||
What's the difference between `encodeURIComponent` and `encodeURI`?
|
||||
|
||||
That's easy to understand if we look at the URL, that's split into components in the picture above:
|
||||
|
||||
```
|
||||
http://site.com:8080/path/page?p1=v1&p2=v2#hash
|
||||
```
|
||||
|
||||
As we can see, characters such as `:`, `?`, `=`, `&`, `#` are allowed in URL. Some others, including non-latin letters and spaces, must be encoded.
|
||||
|
||||
That's what `encodeURI` does:
|
||||
|
||||
```js run
|
||||
// using cyrcillic characters in url path
|
||||
let url = encodeURI('http://site.com/привет');
|
||||
|
||||
// each cyrillic character is encoded with two %xx
|
||||
// together they form UTF-8 code for the character
|
||||
alert(url); // http://site.com/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82
|
||||
```
|
||||
|
||||
...On the other hand, if we look at a single URL component, such as a search parameter, we should encode more characters, e.g. `?`, `=` and `&` are used for formatting.
|
||||
|
||||
That's what `encodeURIComponent` does. It encodes same characters as `encodeURI`, plus a lot of others, to make the resulting value safe to use in any URL component.
|
||||
|
||||
For example:
|
||||
|
||||
```js run
|
||||
let music = encodeURIComponent('Rock&Roll');
|
||||
|
||||
let url = `https://google.com/search?q=${music}`;
|
||||
alert(url); // https://google.com/search?q=Rock%26Roll
|
||||
```
|
||||
|
||||
Compare with `encodeURI`:
|
||||
|
||||
```js run
|
||||
let music = encodeURI('Rock&Roll');
|
||||
|
||||
let url = `https://google.com/search?q=${music}`;
|
||||
alert(url); // https://google.com/search?q=Rock&Roll
|
||||
```
|
||||
|
||||
As we can see, `encodeURI` does not encode `&`, as this is a legit character in URL as a whole.
|
||||
|
||||
But we should encode `&` inside a search parameter, otherwise, we get `q=Rock&Roll` - that is actually `q=Rock` plus some obscure parameter `Roll`. Not as intended.
|
||||
|
||||
So we should use only `encodeURIComponent` for each search parameter, to correctly insert it in the URL string. The safest is to encode both name and value, unless we're absolutely sure that either has only allowed characters.
|
||||
|
||||
### Why URL?
|
||||
|
||||
Lots of old code uses these functions, these are sometimes convenient, and by noo means not dead.
|
||||
|
||||
But in modern code, it's recommended to use classes [URL](https://url.spec.whatwg.org/#url-class) and [URLSearchParams](https://url.spec.whatwg.org/#interface-urlsearchparams).
|
||||
|
||||
One of the reason is: they are based on the recent URI spec: [RFC3986](https://tools.ietf.org/html/rfc3986), while `encode*` functions are based on the obsolete version [RFC2396](https://www.ietf.org/rfc/rfc2396.txt).
|
||||
|
||||
For example, IPv6 addresses are treated differently:
|
||||
|
||||
```js run
|
||||
// valid url with IPv6 address
|
||||
let url = 'http://[2607:f8b0:4005:802::1007]/';
|
||||
|
||||
alert(encodeURI(url)); // http://%5B2607:f8b0:4005:802::1007%5D/
|
||||
alert(new URL(url)); // http://[2607:f8b0:4005:802::1007]/
|
||||
```
|
||||
|
||||
As we can see, `encodeURI` replaced square brackets `[...]`, that's not correct, the reason is: IPv6 urls did not exist at the time of RFC2396 (August 1998).
|
||||
|
||||
Such cases are rare, `encode*` functions work well most of the time, it's just one of the reason to prefer new APIs.
|
BIN
5-network/07-url/url-object.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
5-network/07-url/url-object@2x.png
Normal file
After Width: | Height: | Size: 36 KiB |
|
@ -12,7 +12,7 @@ In modern web-development `XMLHttpRequest` may be used for three reasons:
|
|||
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 <info:fetch-basics>.
|
||||
Does that sound familiar? If yes, then all right, go on with `XMLHttpRequest`. Otherwise, please head on to <info:fetch>.
|
||||
|
||||
## The basics
|
||||
|
||||
|
@ -27,7 +27,7 @@ To do the request, we need 3 steps:
|
|||
let xhr = new XMLHttpRequest(); // the constructor has no arguments
|
||||
```
|
||||
|
||||
2. Initialize it:s
|
||||
2. Initialize it:
|
||||
```js
|
||||
xhr.open(method, URL, [async, user, password])
|
||||
```
|
||||
|
@ -35,7 +35,7 @@ To do the request, we need 3 steps:
|
|||
This method is usually called right after `new XMLHttpRequest`. It specifies the main parameters of the request:
|
||||
|
||||
- `method` -- HTTP-method. Usually `"GET"` or `"POST"`.
|
||||
- `URL` -- the URL to request.
|
||||
- `URL` -- the URL to request, a string, can be [URL](info:url) object.
|
||||
- `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).
|
||||
|
||||
|
@ -121,14 +121,6 @@ Once the server has responded, we can receive the result in the following proper
|
|||
`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
|
||||
|
@ -137,6 +129,19 @@ 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.
|
||||
|
||||
````smart header="URL search parameters"
|
||||
To pass URL parameters, like `?name=value`, and ensure the proper encoding, we can use [URL](info:url) object:
|
||||
|
||||
```js
|
||||
let url = new URL('https://google.com/search');
|
||||
url.searchParams.set('q', 'test me!');
|
||||
|
||||
// the parameter 'q' is encoded
|
||||
xhr.open('GET', url); // https://google.com/search?q=test+me%21
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
## Response Type
|
||||
|
||||
We can use `xhr.responseType` property to set the response format:
|
||||
|
@ -207,6 +212,20 @@ You can find `readystatechange` listeners in really old code, it's there for his
|
|||
|
||||
Nowadays, `load/error/progress` handlers deprecate it.
|
||||
|
||||
## Aborting request
|
||||
|
||||
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.
|
||||
|
||||
That
|
||||
Also, `x and `xhr.status` become `0` in that case.
|
||||
|
||||
|
||||
## Synchronous requests
|
||||
|
||||
If in the `open` method the third parameter `async` is set to `false`, the request is made synchronously.
|
||||
|
@ -500,9 +519,11 @@ There are actually more events, the [modern specification](http://www.w3.org/TR/
|
|||
- `error` -- connection error has occurred, 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).
|
||||
- `loadend` -- triggers after `load`, `error`, `timeout` or `abort`.
|
||||
|
||||
The most used events are load completion (`load`), load failure (`error`), and also `progress` to track the progress.
|
||||
The `error`, `abort`, `timeout`, and `load` events are mutually exclusive.
|
||||
|
||||
The most used events are load completion (`load`), load failure (`error`), or we can use a single `loadend` handler and check event and response to see what happened.
|
||||
|
||||
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.
|
||||
|
75
5-network/09-resume-upload/article.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Resumable file upload
|
||||
|
||||
With `fetch` method it's fairly easy to upload a file.
|
||||
|
||||
How to resume the upload after lost connection? There's no built-in option for that, but we have the pieces to implement it.
|
||||
|
||||
Resumable uploads should come with upload progress indication, as we expect big files (if we may need to resume). So, as `fetch` doesn't allow to track upload progress, we'll use [XMLHttpRequest](info:xmlhttprequest).
|
||||
|
||||
## Not-so-useful progress event
|
||||
|
||||
To resume upload, we need to know how much was uploaded till the connection was lost.
|
||||
|
||||
There's `xhr.upload.onprogress` to track upload progress.
|
||||
|
||||
Unfortunately, it's useless here, as it triggers when the data is *sent*, but was it received by the server? The browser doesn't know.
|
||||
|
||||
Maybe it was buffered by a local network proxy, or maybe the remote server process just died and couldn't process them, or it was just lost in the middle when the connection broke, and didn't reach the receiver.
|
||||
|
||||
So, this event is only useful to show a nice progress bar.
|
||||
|
||||
To resume upload, we need to know exactly the number of bytes received by the server. And only the server can tell that.
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. First, we create a file id, to uniquely identify the file we're uploading, e.g.
|
||||
```js
|
||||
let fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
|
||||
```
|
||||
That's needed for resume upload, to tell the server what we're resuming.
|
||||
|
||||
2. Send a request to the server, asking how many bytes it already has, like this:
|
||||
```js
|
||||
let response = await fetch('status', {
|
||||
headers: {
|
||||
'X-File-Id': fileId
|
||||
}
|
||||
});
|
||||
|
||||
// The server has that many bytes
|
||||
let startByte = +await response.text();
|
||||
```
|
||||
|
||||
This assumes that the server tracks file uploads by `X-File-Id` header. Should be implemented at server-side.
|
||||
|
||||
3. Then, we can use `Blob` method `slice` to send the file from `startByte`:
|
||||
```js
|
||||
xhr.open("POST", "upload", true);
|
||||
|
||||
// send file id, so that the server knows which file to resume
|
||||
xhr.setRequestHeader('X-File-Id', fileId);
|
||||
// send the byte we're resuming from, so the server knows we're resuming
|
||||
xhr.setRequestHeader('X-Start-Byte', startByte);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
|
||||
};
|
||||
|
||||
// file can be from input.files[0] or another source
|
||||
xhr.send(file.slice(startByte));
|
||||
```
|
||||
|
||||
Here we send the server both file id as `X-File-Id`, so it knows which file we're uploading, and the starting byte as `X-Start-Byte`, so it knows we're not uploading it initially, but resuming.
|
||||
|
||||
The server should check its records, and if there was an upload of that file, and the current uploaded size is exactly `X-Start-Byte`, then append the data to it.
|
||||
|
||||
|
||||
Here's the demo with both client and server code, written on Node.js.
|
||||
|
||||
It works only partially on this site, as Node.js is behind another server named Nginx, that buffers uploads, passing them to Node.js when fully complete.
|
||||
|
||||
But you can download it and run locally for the full demonstration:
|
||||
|
||||
[codetabs src="upload-resume" height=200]
|
||||
|
||||
As you can see, modern networking methods are close to file managers in their capabilities -- control over headers, progress indicator, sending file parts, etc.
|
50
5-network/09-resume-upload/upload-resume.view/index.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE HTML>
|
||||
|
||||
<script src="uploader.js"></script>
|
||||
|
||||
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
|
||||
<input type="file" name="myfile">
|
||||
<input type="submit" name="submit" value="Upload (Resumes automatically)">
|
||||
</form>
|
||||
|
||||
<button onclick="uploader.stop()">Stop upload</button>
|
||||
|
||||
|
||||
<div id="log">Progress indication</div>
|
||||
|
||||
<script>
|
||||
function log(html) {
|
||||
document.getElementById('log').innerHTML = html;
|
||||
console.log(html);
|
||||
}
|
||||
|
||||
function onProgress(loaded, total) {
|
||||
log("progress " + loaded + ' / ' + total);
|
||||
}
|
||||
|
||||
let uploader;
|
||||
|
||||
document.forms.upload.onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let file = this.elements.myfile.files[0];
|
||||
if (!file) return;
|
||||
|
||||
uploader = new Uploader({file, onProgress});
|
||||
|
||||
try {
|
||||
let uploaded = await uploader.upload();
|
||||
|
||||
if (uploaded) {
|
||||
log('success');
|
||||
} else {
|
||||
log('stopped');
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
log('error');
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
122
5-network/09-resume-upload/upload-resume.view/server.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
let http = require('http');
|
||||
let static = require('node-static');
|
||||
let fileServer = new static.Server('.');
|
||||
let path = require('path');
|
||||
let fs = require('fs');
|
||||
let debug = require('debug')('example:resume-upload');
|
||||
|
||||
let uploads = Object.create(null);
|
||||
|
||||
function onUpload(req, res) {
|
||||
|
||||
let fileId = req.headers['x-file-id'];
|
||||
let startByte = req.headers['x-start-byte'];
|
||||
|
||||
if (!fileId) {
|
||||
res.writeHead(400, "No file id");
|
||||
res.end();
|
||||
}
|
||||
|
||||
// we'll files "nowhere"
|
||||
let filePath = '/dev/null';
|
||||
// could use a real path instead, e.g.
|
||||
// let filePath = path.join('/tmp', fileId);
|
||||
|
||||
debug("onUpload fileId: ", fileId);
|
||||
|
||||
// initialize a new upload
|
||||
if (!uploads[fileId]) uploads[fileId] = {};
|
||||
let upload = uploads[fileId];
|
||||
|
||||
debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte)
|
||||
|
||||
let fileStream;
|
||||
|
||||
// for byte 0, create a new file, otherwise check the size and append to existing one
|
||||
if (startByte == 0) {
|
||||
upload.bytesReceived = 0;
|
||||
fileStream = fs.createWriteStream(filePath, {
|
||||
flags: 'w'
|
||||
});
|
||||
debug("New file created: " + filePath);
|
||||
} else {
|
||||
if (upload.bytesReceived != startByte) {
|
||||
res.writeHead(400, "Wrong start byte");
|
||||
res.end(upload.bytesReceived);
|
||||
return;
|
||||
}
|
||||
// append to existing file
|
||||
fileStream = fs.createWriteStream(filePath, {
|
||||
flags: 'a'
|
||||
});
|
||||
debug("File reopened: " + filePath);
|
||||
}
|
||||
|
||||
|
||||
req.on('data', function(data) {
|
||||
debug("bytes received", upload.bytesReceived);
|
||||
upload.bytesReceived += data.length;
|
||||
});
|
||||
|
||||
// send request body to file
|
||||
req.pipe(fileStream);
|
||||
|
||||
// when the request is finished, and all its data is written
|
||||
fileStream.on('close', function() {
|
||||
if (upload.bytesReceived == req.headers['x-file-size']) {
|
||||
debug("Upload finished");
|
||||
delete uploads[fileId];
|
||||
|
||||
// can do something else with the uploaded file here
|
||||
|
||||
res.end("Success " + upload.bytesReceived);
|
||||
} else {
|
||||
// connection lost, we leave the unfinished file around
|
||||
debug("File unfinished, stopped at " + upload.bytesReceived);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// in case of I/O error - finish the request
|
||||
fileStream.on('error', function(err) {
|
||||
debug("fileStream error");
|
||||
res.writeHead(500, "File error");
|
||||
res.end();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function onStatus(req, res) {
|
||||
let fileId = req.headers['x-file-id'];
|
||||
let upload = uploads[fileId];
|
||||
debug("onStatus fileId:", fileId, " upload:", upload);
|
||||
if (!upload) {
|
||||
res.end("0")
|
||||
} else {
|
||||
res.end(String(upload.bytesReceived));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function accept(req, res) {
|
||||
if (req.url == '/status') {
|
||||
onStatus(req, res);
|
||||
} else if (req.url == '/upload' && req.method == 'POST') {
|
||||
onUpload(req, res);
|
||||
} else {
|
||||
fileServer.serve(req, res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------------------------------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
console.log('Server listening at port 8080');
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
75
5-network/09-resume-upload/upload-resume.view/uploader.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
class Uploader {
|
||||
|
||||
constructor({file, onProgress}) {
|
||||
this.file = file;
|
||||
this.onProgress = onProgress;
|
||||
|
||||
// create fileId that uniquely identifies the file
|
||||
// we can also add user session identifier, to make it even more unique
|
||||
this.fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
|
||||
}
|
||||
|
||||
async getUploadedBytes() {
|
||||
let response = await fetch('status', {
|
||||
headers: {
|
||||
'X-File-Id': this.fileId
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error("Can't get uploaded bytes: " + response.statusText);
|
||||
}
|
||||
|
||||
let text = await response.text();
|
||||
|
||||
return +text;
|
||||
}
|
||||
|
||||
async upload() {
|
||||
this.startByte = await this.getUploadedBytes();
|
||||
|
||||
let xhr = this.xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "upload", true);
|
||||
|
||||
// send file id, so that the server knows which file to resume
|
||||
xhr.setRequestHeader('X-File-Id', this.fileId);
|
||||
// send the byte we're resuming from, so the server knows we're resuming
|
||||
xhr.setRequestHeader('X-Start-Byte', this.startByte);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
this.onProgress(this.startByte + e.loaded, this.startByte + e.total);
|
||||
};
|
||||
|
||||
console.log("send the file, starting from", this.startByte);
|
||||
xhr.send(this.file.slice(this.startByte));
|
||||
|
||||
// return
|
||||
// true if upload was successful,
|
||||
// false if aborted
|
||||
// throw in case of an error
|
||||
return await new Promise((resolve, reject) => {
|
||||
|
||||
xhr.onload = xhr.onerror = () => {
|
||||
console.log("upload end status:" + xhr.status + " text:" + xhr.statusText);
|
||||
|
||||
if (xhr.status == 200) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error("Upload failed: " + xhr.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
// onabort triggers only when xhr.abort() is called
|
||||
xhr.onabort = () => resolve(false);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
95
5-network/10-long-polling/article.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
# Long polling
|
||||
|
||||
Long polling is the simplest way of having persistent connection with server, that doesn't use any specific protocol like WebSocket or Server Side Events.
|
||||
|
||||
Being very easy to implement, it's also good enough in a lot of cases.
|
||||
|
||||
## Regular Polling
|
||||
|
||||
The simplest way to get new information from the server is polling.
|
||||
|
||||
That is, periodical requests to the server: "Hello, I'm here, do you have any information for me?". For example, once in 10 seconds.
|
||||
|
||||
In response, the server first takes a notice to itself that the client is online, and second - sends a packet of messages it got till that moment.
|
||||
|
||||
That works, but there are downsides:
|
||||
1. Messages are passed with a delay up to 10 seconds.
|
||||
2. Even if there are no messages, the server is bombed with requests every 10 seconds. That's quite a load to handle for backend, speaking performance-wise.
|
||||
|
||||
So, if we're talking about a very small service, the approach may be viable.
|
||||
|
||||
But generally, it needs an improvement.
|
||||
|
||||
## Long polling
|
||||
|
||||
Long polling -- is a better way to poll the server.
|
||||
|
||||
It's also very easy to implement, and delivers messages without delays.
|
||||
|
||||
The flow:
|
||||
|
||||
1. A request is sent to the server.
|
||||
2. The server doesn't close the connection until it has a message.
|
||||
3. When a message appears - the server responds to the request with the data.
|
||||
4. The browser makes a new request immediately.
|
||||
|
||||
The situation when the browser sent a request and has a pending connection with the server, is standard for this method. Only when a message is delivered, the connection is reestablished.
|
||||
|
||||

|
||||
|
||||
Even if the connection is lost, because of, say, a network error, the browser immediately sends a new request.
|
||||
|
||||
A sketch of client-side code:
|
||||
|
||||
```js
|
||||
async function subscribe() {
|
||||
let response = await fetch("/subscribe");
|
||||
|
||||
if (response.status == 502) {
|
||||
// Connection timeout, happens when the connection was pending for too long
|
||||
// let's reconnect
|
||||
await subscribe();
|
||||
} else if (response.status != 200) {
|
||||
// Show Error
|
||||
showMessage(response.statusText);
|
||||
// Reconnect in one second
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await subscribe();
|
||||
} else {
|
||||
// Got message
|
||||
let message = await response.text();
|
||||
showMessage(message);
|
||||
await subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
subscribe();
|
||||
```
|
||||
|
||||
The `subscribe()` function makes a fetch, then waits for the response, handles it and calls itself again.
|
||||
|
||||
```warn header="Server should be ok with many pending connections"
|
||||
The server architecture must be able to work with many pending connections.
|
||||
|
||||
Certain server architectures run a process per connect. For many connections there will be as many processes, and each process takes a lot of memory. So many connections just consume it all.
|
||||
|
||||
That's often the case for backends written in PHP, Ruby languages, but technically isn't a language, but rather implementation issue.
|
||||
|
||||
Backends written using Node.js usually don't have such problems.
|
||||
```
|
||||
|
||||
## Demo: a chat
|
||||
|
||||
Here's a demo:
|
||||
|
||||
[codetabs src="longpoll" height=500]
|
||||
|
||||
## Area of usage
|
||||
|
||||
Long polling works great in situations when messages are rare.
|
||||
|
||||
If messages come very often, then the chart of requesting-receiving messages, painted above, becomes saw-like.
|
||||
|
||||
Every message is a separate request, supplied with headers, authentication overhead, and so on.
|
||||
|
||||
So, in this case, another method is preferred, such as [Websocket](info:websocket) or [Server Sent Events](info:server-sent-events).
|
BIN
5-network/10-long-polling/long-polling.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
5-network/10-long-polling/long-polling@2x.png
Normal file
After Width: | Height: | Size: 48 KiB |
54
5-network/10-long-polling/longpoll.view/browser.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Sending messages, a simple POST
|
||||
function PublishForm(form, url) {
|
||||
|
||||
function sendMessage(message) {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: message
|
||||
});
|
||||
}
|
||||
|
||||
form.onsubmit = function() {
|
||||
let message = form.message.value;
|
||||
if (message) {
|
||||
form.message.value = '';
|
||||
sendMessage(message);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Receiving messages with long polling
|
||||
function SubscribePane(elem, url) {
|
||||
|
||||
function showMessage(message) {
|
||||
let messageElem = document.createElement('div');
|
||||
messageElem.append(message);
|
||||
elem.append(messageElem);
|
||||
}
|
||||
|
||||
async function subscribe() {
|
||||
let response = await fetch(url);
|
||||
|
||||
if (response.status == 502) {
|
||||
// Connection timeout
|
||||
// happens when the connection was pending for too long
|
||||
// let's reconnect
|
||||
await subscribe();
|
||||
} else if (response.status != 200) {
|
||||
// Show Error
|
||||
showMessage(response.statusText);
|
||||
// Reconnect in one second
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await subscribe();
|
||||
} else {
|
||||
// Got message
|
||||
let message = await response.text();
|
||||
showMessage(message);
|
||||
await subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
subscribe();
|
||||
|
||||
}
|
18
5-network/10-long-polling/longpoll.view/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="browser.js"></script>
|
||||
|
||||
All visitors of this page will see messages of each other.
|
||||
|
||||
<form name="publish">
|
||||
<input type="text" name="message" />
|
||||
<input type="submit" value="Send" />
|
||||
</form>
|
||||
|
||||
<div id="subscribe">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new PublishForm(document.forms.publish, 'publish');
|
||||
// random url to avoid any caching issues
|
||||
new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
|
||||
</script>
|
87
5-network/10-long-polling/longpoll.view/server.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
let http = require('http');
|
||||
let url = require('url');
|
||||
let querystring = require('querystring');
|
||||
let static = require('node-static');
|
||||
|
||||
let fileServer = new static.Server('.');
|
||||
|
||||
let subscribers = Object.create(null);
|
||||
|
||||
function onSubscribe(req, res) {
|
||||
let id = Math.random();
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
|
||||
res.setHeader("Cache-Control", "no-cache, must-revalidate");
|
||||
|
||||
subscribers[id] = res;
|
||||
|
||||
req.on('close', function() {
|
||||
delete subscribers[id];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function publish(message) {
|
||||
|
||||
for (let id in subscribers) {
|
||||
let res = subscribers[id];
|
||||
res.end(message);
|
||||
}
|
||||
|
||||
subscribers = Object.create(null);
|
||||
}
|
||||
|
||||
function accept(req, res) {
|
||||
let urlParsed = url.parse(req.url, true);
|
||||
|
||||
// new client wants messages
|
||||
if (urlParsed.pathname == '/subscribe') {
|
||||
onSubscribe(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// sending a message
|
||||
if (urlParsed.pathname == '/publish' && req.method == 'POST') {
|
||||
// accept POST
|
||||
req.setEncoding('utf8');
|
||||
let message = '';
|
||||
req.on('data', function(chunk) {
|
||||
message += chunk;
|
||||
}).on('end', function() {
|
||||
publish(message); // publish it to everyone
|
||||
res.end("ok");
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// the rest is static
|
||||
fileServer.serve(req, res);
|
||||
|
||||
}
|
||||
|
||||
function close() {
|
||||
for (let id in subscribers) {
|
||||
let res = subscribers[id];
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
console.log('Server running on port 8080');
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
|
||||
if (process.send) { // if run by pm2 have this defined
|
||||
process.on('message', (msg) => {
|
||||
if (msg === 'shutdown') {
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', close);
|
||||
}
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |