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

View file

@ -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.

View file

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

Before After
Before After

View file

@ -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())

View 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!

View 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();
}

View file

@ -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.

View file

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Before After
Before After

View file

@ -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>.

View file

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

Before After
Before After

View file

@ -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:
![](url-object.png)
- `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
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

206
5-network/07-url/article.md Normal file
View 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:
![](url-object.png)
- `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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -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.

View 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.

View 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>

View 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;
}

View 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();
}
}
}

View 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.
![](long-polling.png)
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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View 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();
}

View 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>

View 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);
}

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Before After
Before After