updates
|
@ -1,4 +1,4 @@
|
|||
The `new Date` constructor uses the local time zone by default. So the only important thing to remember is that months start from zero.
|
||||
The `new Date` constructor uses the local time zone. So the only important thing to remember is that months start from zero.
|
||||
|
||||
So February has number 1.
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ Create a function `getDateAgo(date, days)` to return the day of month `days` ago
|
|||
|
||||
For instance, if today is 20th, then `getDateAgo(new Date(), 1)` should be 19th and `getDateAgo(new Date(), 2)` should be 18th.
|
||||
|
||||
Should also work over months/years reliably:
|
||||
Should work reliably for `days=365` or more:
|
||||
|
||||
```js
|
||||
let date = new Date(2015, 0, 2);
|
||||
|
|
|
@ -40,7 +40,7 @@ To create a new `Date` object call `new Date()` with one of the following argume
|
|||
```js run
|
||||
let date = new Date("2017-01-26");
|
||||
alert(date);
|
||||
// The time portion of the date is assumed to be midnight GMT and
|
||||
// The time is not set, so it's assumed to be midnight GMT and
|
||||
// is adjusted according to the timezone the code is run in
|
||||
// So the result could be
|
||||
// Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time)
|
||||
|
@ -51,8 +51,6 @@ To create a new `Date` object call `new Date()` with one of the following argume
|
|||
`new Date(year, month, date, hours, minutes, seconds, ms)`
|
||||
: Create the date with the given components in the local time zone. Only the first two arguments are obligatory.
|
||||
|
||||
Note:
|
||||
|
||||
- The `year` must have 4 digits: `2013` is okay, `98` is not.
|
||||
- The `month` count starts with `0` (Jan), up to `11` (Dec).
|
||||
- The `date` parameter is actually the day of month, if absent then `1` is assumed.
|
||||
|
@ -74,7 +72,7 @@ To create a new `Date` object call `new Date()` with one of the following argume
|
|||
|
||||
## Access date components
|
||||
|
||||
There are many methods to access the year, month and so on from the `Date` object. But they can be easily remembered when categorized.
|
||||
There are methods to access the year, month and so on from the `Date` object:
|
||||
|
||||
[getFullYear()](mdn:js/Date/getFullYear)
|
||||
: Get the year (4 digits)
|
||||
|
@ -217,21 +215,21 @@ The important side effect: dates can be subtracted, the result is their differen
|
|||
That can be used for time measurements:
|
||||
|
||||
```js run
|
||||
let start = new Date(); // start counting
|
||||
let start = new Date(); // start measuring time
|
||||
|
||||
// do the job
|
||||
for (let i = 0; i < 100000; i++) {
|
||||
let doSomething = i * i * i;
|
||||
}
|
||||
|
||||
let end = new Date(); // done
|
||||
let end = new Date(); // end measuring time
|
||||
|
||||
alert( `The loop took ${end - start} ms` );
|
||||
```
|
||||
|
||||
## Date.now()
|
||||
|
||||
If we only want to measure the difference, we don't need the `Date` object.
|
||||
If we only want to measure time, we don't need the `Date` object.
|
||||
|
||||
There's a special method `Date.now()` that returns the current timestamp.
|
||||
|
||||
|
@ -264,6 +262,8 @@ If we want a reliable benchmark of CPU-hungry function, we should be careful.
|
|||
|
||||
For instance, let's measure two functions that calculate the difference between two dates: which one is faster?
|
||||
|
||||
Such performance measurements are often called "benchmarks".
|
||||
|
||||
```js
|
||||
// we have date1 and date2, which function faster returns their difference in ms?
|
||||
function diffSubtract(date1, date2) {
|
||||
|
@ -280,7 +280,7 @@ These two do exactly the same thing, but one of them uses an explicit `date.getT
|
|||
|
||||
So, which one is faster?
|
||||
|
||||
The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it around 100000 times.
|
||||
The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it at least 100000 times.
|
||||
|
||||
Let's measure:
|
||||
|
||||
|
@ -310,7 +310,7 @@ Wow! Using `getTime()` is so much faster! That's because there's no type convers
|
|||
|
||||
Okay, we have something. But that's not a good benchmark yet.
|
||||
|
||||
Imagine that at the time of running `bench(diffSubtract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` the work has finished.
|
||||
Imagine that at the time of running `bench(diffSubtract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` that work has finished.
|
||||
|
||||
A pretty real scenario for a modern multi-process OS.
|
||||
|
||||
|
@ -368,7 +368,7 @@ for (let i = 0; i < 10; i++) {
|
|||
```
|
||||
|
||||
```warn header="Be careful doing microbenchmarking"
|
||||
Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all.
|
||||
Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small, such as how an operator works, or a built-in function. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all.
|
||||
|
||||
The great pack of articles about V8 can be found at <http://mrale.ph>.
|
||||
```
|
||||
|
@ -415,7 +415,7 @@ alert(date);
|
|||
|
||||
Note that unlike many other systems, timestamps in JavaScript are in milliseconds, not in seconds.
|
||||
|
||||
Also, sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point):
|
||||
Sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point):
|
||||
|
||||
```js run
|
||||
alert(`Loading started ${performance.now()}ms ago`);
|
||||
|
|
|
@ -17,10 +17,10 @@ class User {
|
|||
User.staticMethod(); // true
|
||||
```
|
||||
|
||||
That actually does the same as assigning it as a function property:
|
||||
That actually does the same as assigning it as a property:
|
||||
|
||||
```js
|
||||
function User() { }
|
||||
class User() { }
|
||||
|
||||
User.staticMethod = function() {
|
||||
alert(this === User);
|
||||
|
|
|
@ -78,7 +78,7 @@ Please note, that it's possible to copy/paste not just text, but everything. For
|
|||
|
||||
There's a list of methods [in the specification](https://www.w3.org/TR/clipboard-apis/#dfn-datatransfer) that can work with different data types including files, read/write to the clipboard.
|
||||
|
||||
But please note that clipboard is a "global" OS-level thing. Most browsers allow read/write access to the clipboard only in the scope of certain user actions for the safety.
|
||||
But please note that clipboard is a "global" OS-level thing. Most browsers allow read/write access to the clipboard only in the scope of certain user actions for the safety, e.g. in `onclick` event handlers.
|
||||
|
||||
Also it's forbidden to generate "custom" clipboard events with `dispatchEvent` in all browsers except Firefox.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Form submission: event and method submit
|
||||
# Forms: event and method submit
|
||||
|
||||
The `submit` event triggers when the form is submitted, it is usually used to validate the form before sending it to the server or to abort the submission and process it in JavaScript.
|
||||
|
||||
|
|
|
@ -7,12 +7,20 @@ Basically, you just run:
|
|||
window.open('https://javascript.info/')
|
||||
```
|
||||
|
||||
... And it will open a new window with given URL. Most modern browsers are configured to open new tabs instead of separate windows.
|
||||
...And it will open a new window with given URL. Most modern browsers are configured to open new tabs instead of separate windows.
|
||||
|
||||
Popups exist from really ancient times. The initial idea was to show another content without closing the main window. As of now, there are other ways to do that: we can load content dynamically with [fetch](info:fetch) and show it in a dynamically generated `<div>`. So, popups is not something we use everyday.
|
||||
|
||||
Also, popups are tricky on mobile devices.
|
||||
|
||||
Still, there are situations when a popup works good, e.g. for OAuth authorization (login with Google/Facebook/...), because:
|
||||
|
||||
1. A popup is a separate window with its own independent JavaScript environment. So opening a popup with a third-party non-trusted site is safe.
|
||||
2. It's very easy to open a popup, little to no overhead.
|
||||
3. A popup can navigate (change URL) and send messages to the opener window.
|
||||
|
||||
## Popup blocking
|
||||
|
||||
Popups exist from really ancient times. The initial idea was to show another content without closing the main window. As of now, there are other ways to do that: JavaScript is able to send requests for server, so popups are rarely used. But sometimes they are still handy.
|
||||
|
||||
In the past, evil sites abused popups a lot. A bad page could open tons of popup windows with ads. So now most browsers try to block popups and protect the user.
|
||||
|
||||
**Most browsers block popups if they are called outside of user-triggered event handlers like `onclick`.**
|
||||
|
@ -50,14 +58,6 @@ setTimeout(() => window.open('http://google.com'), 1000);
|
|||
|
||||
The difference is that Firefox treats a timeout of 2000ms or less are acceptable, but after it -- removes the "trust", assuming that now it's "outside of the user action". So the first one is blocked, and the second one is not.
|
||||
|
||||
## Modern usage
|
||||
|
||||
As of now, we have many methods to load and show data on-page with JavaScript. But there are still situations when a popup works good, because:
|
||||
|
||||
1. A popup is a separate window with its own independent JavaScript environment. So opening a popup with a third-party non-trusted site is safe.
|
||||
2. It's very easy to open a popup, little to no overhead.
|
||||
3. A popup may persist even if the user left the page. In also can navigate (change URL) in the opener window.
|
||||
|
||||
## window.open
|
||||
|
||||
The syntax to open a popup is: `window.open(url, name, params)`:
|
||||
|
@ -118,16 +118,26 @@ Rules for omitted settings:
|
|||
- If there is no `left/top` in params, then the browser tries to open a new window near the last opened window.
|
||||
- If there is no `width/height`, then the new window will be the same size as the last opened.
|
||||
|
||||
## Accessing a popup
|
||||
## Accessing popup from window
|
||||
|
||||
The `open` call returns a reference to the new window. It can be used to manipulate it's properties, change location and even more.
|
||||
|
||||
In the example below, the contents of the new window is modified after loading.
|
||||
In this example, we generate popup content from JavaScript:
|
||||
|
||||
```js
|
||||
let newWin = window.open("about:blank", "hello", "width=200,height=200");
|
||||
|
||||
newWin.document.write("Hello, world!");
|
||||
```
|
||||
|
||||
And here we modify the contents after loading:
|
||||
|
||||
```js run
|
||||
let newWindow = open('/', 'example', 'width=300,height=300')
|
||||
newWindow.focus();
|
||||
|
||||
alert(newWin.location.href); // (*) about:blank, loading hasn't started yet
|
||||
|
||||
newWindow.onload = function() {
|
||||
let html = `<div style="font-size:30px">Welcome!</div>`;
|
||||
*!*
|
||||
|
@ -136,35 +146,95 @@ newWindow.onload = function() {
|
|||
};
|
||||
```
|
||||
|
||||
Please note that external `document` content is only accessible for windows from the same origin (the same protocol://domain:port).
|
||||
Please note: immediately after `window.open`, the new window isn't loaded yet. That's demonstrated by `alert` in line `(*)`. So we wait for `onload` to modify it. We could also use `DOMContentLoaded` handler for `newWin.document`.
|
||||
|
||||
For windows with URLs from another sites, we are able to change the location by assigning `newWindow.location=...`, but we can't read the location or access the content. That's for user safety, so that an evil page can't open a popup with `http://gmail.com` and read the data. We'll talk more about it later.
|
||||
```warn header="Same origin policy"
|
||||
Windows may only freely modify each other if they come from the same origin (the same protocol://domain:port).
|
||||
|
||||
## Accessing the opener window
|
||||
Otherwise, e.g. if the main window is from `site.com`, and the popup from `gmail.com`, that's impossible for user safety reasons. For the details, see chapter <info:cross-window-communication>.
|
||||
```
|
||||
|
||||
A popup may access the "opener" window as well. A JavaScript in it may use `window.opener` to access the window that opened it. It is `null` for all windows except popups.
|
||||
## Accessing window from popup
|
||||
|
||||
So both the main window and the popup have a reference to each other. They may modify each other freely assuming that they come from the same origin. If that's not so, then there are still means to communicate, to be covered in the next chapter <info:cross-window-communication>.
|
||||
A popup may access the "opener" window as well using `window.opener` reference. It is `null` for all windows except popups.
|
||||
|
||||
If you run the code below, it replaces the opener window content with "Test":
|
||||
|
||||
```js run
|
||||
let newWin = window.open("about:blank", "hello", "width=200,height=200");
|
||||
|
||||
newWin.document.write(
|
||||
"<script>window.opener.document.body.innerHTML = 'Test'<\/script>"
|
||||
);
|
||||
```
|
||||
|
||||
So the connection between the windows is bidirectional: the main window and the popup have a reference to each other.
|
||||
|
||||
## Closing a popup
|
||||
|
||||
If we don't need a popup any more, we can call `newWindow.close()` on it.
|
||||
- To close a window: `win.close()`.
|
||||
- To check if a window is closed: `win.close` property.
|
||||
|
||||
Technically, the `close()` method is available for any `window`, but `window.close()` is ignored by most browsers if `window` is not created with `window.open()`.
|
||||
Technically, the `close()` method is available for any `window`, but `window.close()` is ignored by most browsers if `window` is not created with `window.open()`. So it'll only work on a popup.
|
||||
|
||||
The `newWindow.closed` is `true` if the window is closed. That's useful to check if the popup (or the main window) is still open or not. A user could close it, and our code should take that possibility into account.
|
||||
The `win.closed` property is `true` if the window is closed. That's useful to check if the popup (or the main window) is still open or not. A user can close it anytime, and our code should take that possibility into account.
|
||||
|
||||
This code loads and then closes the window:
|
||||
|
||||
```js run
|
||||
let newWindow = open('/', 'example', 'width=300,height=300')
|
||||
let newWindow = open('/', 'example', 'width=300,height=300');
|
||||
|
||||
newWindow.onload = function() {
|
||||
newWindow.close();
|
||||
alert(newWindow.closed); // true
|
||||
};
|
||||
```
|
||||
|
||||
## Focus/blur on a popup
|
||||
|
||||
## Scrolling and resizing
|
||||
|
||||
There are methods to move/resize a window:
|
||||
|
||||
`win.moveBy(x,y)`
|
||||
: Move the window relative to current position `x` pixels to the right and `y` pixels down. Negative values are allowed (to move left/up).
|
||||
|
||||
`win.moveTo(x,y)`
|
||||
: Move the window to coordinates `(x,y)` on the screen.
|
||||
|
||||
`win.resizeBy(width,height)`
|
||||
: Resize the window by given `width/height` relative to the current size. Negative values are allowed.
|
||||
|
||||
`win.resizeTo(width,height)`
|
||||
: Resize the window to the given size.
|
||||
|
||||
There's also `window.onresize` event.
|
||||
|
||||
```warn header="Only popups"
|
||||
To prevent abuse, the browser usually blocks these methods. They only work reliably on popups that we opened, that have no additional tabs.
|
||||
```
|
||||
|
||||
```warn header="No minification/maximization"
|
||||
JavaScript has no way to minify or maximize a window. These OS-level functions are hidden from Frontend-developers.
|
||||
|
||||
Move/resize methods do not work for maximized/minimized windows.
|
||||
```
|
||||
|
||||
## Scrolling a window
|
||||
|
||||
We already talked about scrolling a window in the chapter <info:size-and-scroll-window>.
|
||||
|
||||
`win.scrollBy(x,y)`
|
||||
: Scroll the window `x` pixels right and `y` down relative the current scroll. Negative values are allowed.
|
||||
|
||||
`win.scrollTo(x,y)`
|
||||
: Scroll the window to the given coordinates `(x,y)`.
|
||||
|
||||
`elem.scrollIntoView(top = true)`
|
||||
: Scroll the window to make `elem` show up at the top (the default) or at the bottom for `elem.scrollIntoView(false)`.
|
||||
|
||||
There's also `window.onscroll` event.
|
||||
|
||||
## Focus/blur on a window
|
||||
|
||||
Theoretically, there are `window.focus()` and `window.blur()` methods to focus/unfocus on a window. Also there are `focus/blur` events that allow to focus a window and catch the moment when the visitor switches elsewhere.
|
||||
|
||||
|
@ -189,12 +259,23 @@ For instance:
|
|||
|
||||
## Summary
|
||||
|
||||
Всплывающие окна используются нечасто. Ведь загрузить новую информацию можно динамически, с помощью технологии AJAX, а показать -- в элементе `<div>`, расположенным над страницей (`z-index`). Ещё одна альтернатива -- тег `<iframe>`.
|
||||
|
||||
Но в некоторых случаях всплывающие окна бывают очень даже полезны. Например, отдельное окно сервиса онлайн-консультаций. Посетитель может ходить по сайту в основном окне, а общаться в чате -- во вспомогательном.
|
||||
|
||||
Если вы хотите использовать всплывающее окно, предупредите посетителя об этом, так же и при использовании `target="_blank"` в ссылках или формах. Иконка открывающегося окошка на ссылке поможет посетителю понять, что происходит и не потерять оба окна из поля зрения.
|
||||
|
||||
- A popup can be opened by the `open(url, name, params)` call. It returns the reference to the newly opened window.
|
||||
- By default, browsers block `open` calls from the code outside of user actions. Usually a notification appears, so that a user may allow them.
|
||||
- The popup may access the opener window using the `window.opener` property, so the two are connected.
|
||||
- If the main window and the popup come from the same origin, they can freely read and modify each other. Otherwise, they can change location of each other and communicate using messages (to be covered).
|
||||
- Browsers block `open` calls from the code outside of user actions. Usually a notification appears, so that a user may allow them.
|
||||
- Browsers open a new tab by default, but if sizes are provided, then it'll be a popup window.
|
||||
- The popup may access the opener window using the `window.opener` property.
|
||||
- The main window and the popup can freely read and modify each other if they havee the same origin. Otherwise, they can change location of each other and [exchange messages](cross-window-communication).
|
||||
|
||||
Methods and properties:
|
||||
|
||||
- To close the popup: use `close()` call. Also the user may close them (just like any other windows). The `window.closed` is `true` after that.
|
||||
- Methods `focus()` and `blur()` allow to focus/unfocus a window. Sometimes.
|
||||
- Events `focus` and `blur` allow to track switching in and out of the window. But please note that a window may still be visible even in the background state, after `blur`.
|
||||
- ...And a few scrolling and resizing methods.
|
||||
|
||||
Also if we open a popup, a good practice is to notify the user about it. An icon with the opening window can help the visitor to survive the focus shift and keep both windows in mind.
|
||||
If we're going to open a popup, a good practice is to inform the user about it. If there's a link that opens a popup, we could place an icon near it, so that visitor can survive the focus shift and keep both windows in mind.
|
||||
|
|
|
@ -26,39 +26,18 @@ The "Same Origin" policy states that:
|
|||
- if we have a reference to another window, e.g. a popup created by `window.open` or a window inside `<iframe>`, and that window comes from the same origin, then we have full access to that window.
|
||||
- otherwise, if it comes from another origin, then we can't access the content of that window: variables, document, anything. The only exception is `location`: we can change it (thus redirecting the user). But we cannot *read* location (so we can't see where the user is now, no information leak).
|
||||
|
||||
Now let's see some examples. First, we'll look at pages that come from the same origin and thus allow direct access, and afterwards we'll cover cross-window messaging that allows to work around the "Same Origin" policy.
|
||||
### In action: iframe
|
||||
|
||||
An `<iframe>` tag hosts embbedded window, with its own separate `document` and `window` objects.
|
||||
|
||||
````warn header="Windows on different subdomains of the same domain"
|
||||
By definition, two URLs with different domains have different origins.
|
||||
We can access them using properties:
|
||||
|
||||
Still, there's a small exclusion here.
|
||||
- `iframe.contentWindow` to get the window inside the `<iframe>`.
|
||||
- `iframe.contentDocument` to get the document inside the `<iframe>`.
|
||||
|
||||
If windows share the same second-level domain, for instance `john.site.com`, `peter.site.com` and `site.com` (so that their common second-level domain is `site.com`), we can make the browser ignore that difference, so that they can be treated as coming from the "same origin" for the purposes of cross-window communication.
|
||||
When we access something inside the embedded window, the browser checks if the iframe has the same origin. If that's not so then the access is denied (writing to `location` is an exception, it's still permitted).
|
||||
|
||||
To make it work, each window (including the one from `site.com`) should run the code:
|
||||
|
||||
```js
|
||||
document.domain = 'site.com';
|
||||
```
|
||||
|
||||
That's all. Now they can interact without limitations. Again, that's only possible for pages with the same second-level domain.
|
||||
````
|
||||
|
||||
## Accessing an iframe contents
|
||||
|
||||
Our first example covers iframes. An `<iframe>` is a two-faced beast. From one side it's a tag, just like `<script>` or `<img>`. From the other side it's a window-in-window.
|
||||
|
||||
The embedded window in the iframe has a separate `document` and `window` objects.
|
||||
|
||||
We can access them like using the properties:
|
||||
|
||||
- `iframe.contentWindow` is a reference to the window inside the `<iframe>`.
|
||||
- `iframe.contentDocument` is a reference to the document inside the `<iframe>`.
|
||||
|
||||
When we access an embedded window, the browser checks if the iframe has the same origin. If that's not so then the access is denied (writing to `location` is an exception, it's still permitted).
|
||||
|
||||
For instance, here's an `<iframe>` from another origin:
|
||||
For instance, let's try reading and writing to `<iframe>` from another origin:
|
||||
|
||||
```html run
|
||||
<iframe src="https://example.com" id="iframe"></iframe>
|
||||
|
@ -66,44 +45,47 @@ For instance, here's an `<iframe>` from another origin:
|
|||
<script>
|
||||
iframe.onload = function() {
|
||||
// we can get the reference to the inner window
|
||||
let iframeWindow = iframe.contentWindow;
|
||||
|
||||
*!*
|
||||
let iframeWindow = iframe.contentWindow; // OK
|
||||
*/!*
|
||||
try {
|
||||
// ...but not to the document inside it
|
||||
let doc = iframe.contentDocument;
|
||||
*!*
|
||||
let doc = iframe.contentDocument; // ERROR
|
||||
*/!*
|
||||
} catch(e) {
|
||||
alert(e); // Security Error (another origin)
|
||||
}
|
||||
|
||||
// also we can't read the URL of the page in it
|
||||
// also we can't READ the URL of the page in iframe
|
||||
try {
|
||||
alert(iframe.contentWindow.location);
|
||||
// Can't read URL from the Location object
|
||||
*!*
|
||||
let href = iframe.contentWindow.location.href; // ERROR
|
||||
*/!*
|
||||
} catch(e) {
|
||||
alert(e); // Security Error
|
||||
}
|
||||
|
||||
// ...but we can change it (and thus load something else into the iframe)!
|
||||
iframe.contentWindow.location = '/'; // works
|
||||
// ...we can WRITE into location (and thus load something else into the iframe)!
|
||||
*!*
|
||||
iframe.contentWindow.location = '/'; // OK
|
||||
*/!*
|
||||
|
||||
iframe.onload = null; // clear the handler, to run this code only once
|
||||
iframe.onload = null; // clear the handler, not to run it after the location change
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
The code above shows errors for any operations except:
|
||||
|
||||
- Getting the reference to the inner window `iframe.contentWindow`
|
||||
- Changing its `location`.
|
||||
- Getting the reference to the inner window `iframe.contentWindow` - that's allowed.
|
||||
- Writing to `location`.
|
||||
|
||||
```smart header="`iframe.onload` vs `iframe.contentWindow.onload`"
|
||||
The `iframe.onload` event is essentially the same as `iframe.contentWindow.onload`. It triggers when the embedded window fully loads with all resources.
|
||||
|
||||
...But `iframe.onload` is always available from outside the iframe, while accessing `iframe.contentWindow.onload` is only permitted from the window with the same origin.
|
||||
```
|
||||
|
||||
And now an example with the same origin. We can do anything with the embedded window:
|
||||
Contrary to that, if the `<iframe>` has the same origin, we can do anything with it:
|
||||
|
||||
```html run
|
||||
<!-- iframe from the same site -->
|
||||
<iframe src="/" id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
|
@ -114,9 +96,33 @@ And now an example with the same origin. We can do anything with the embedded wi
|
|||
</script>
|
||||
```
|
||||
|
||||
### Please wait until the iframe loads
|
||||
```smart header="`iframe.onload` vs `iframe.contentWindow.onload`"
|
||||
The `iframe.onload` event (on the `<iframe>` tag) is essentially the same as `iframe.contentWindow.onload` (on the embedded window object). It triggers when the embedded window fully loads with all resources.
|
||||
|
||||
When an iframe is created, it immediately has a document. But that document is different from the one that finally loads into it!
|
||||
...But we can't access `iframe.contentWindow.onload` for an iframe from another origin, so using `iframe.onload`.
|
||||
```
|
||||
|
||||
## Iframes on subdomains: document.domain
|
||||
|
||||
By definition, two URLs with different domains have different origins.
|
||||
|
||||
But if windows share the same second-level domain, for instance `john.site.com`, `peter.site.com` and `site.com` (so that their common second-level domain is `site.com`), we can make the browser ignore that difference, so that they can be treated as coming from the "same origin" for the purposes of cross-window communication.
|
||||
|
||||
To make it work, each window (including the one from `site.com`) should run the code:
|
||||
|
||||
```js
|
||||
document.domain = 'site.com';
|
||||
```
|
||||
|
||||
That's all. Now they can interact without limitations. Again, that's only possible for pages with the same second-level domain.
|
||||
|
||||
## Iframe: wrong document pitfall
|
||||
|
||||
When an iframe comes from the same origin, and we may access its `document`, there's a pitfall. It's not related to cross-domain things, but important to know.
|
||||
|
||||
Upon its creation an iframe immediately has a document. But that document is different from the one that loads into it!
|
||||
|
||||
So if we do something with the document immediately, that will probably be lost.
|
||||
|
||||
Here, look:
|
||||
|
||||
|
@ -136,13 +142,15 @@ Here, look:
|
|||
</script>
|
||||
```
|
||||
|
||||
That's a well-known pitfall. We shouldn't work with the document of a not-yet-loaded iframe, because that's the *wrong document*. If we set any event handlers on it, they will be ignored.
|
||||
We shouldn't work with the document of a not-yet-loaded iframe, because that's the *wrong document*. If we set any event handlers on it, they will be ignored.
|
||||
|
||||
...We definitely can access the right document when the `onload` event triggers. But it only triggers when the whole iframe with all resources is loaded. What if we want to act sooner, on `DOMContentLoaded` of the embedded document?
|
||||
...The right document is definitely there when `iframe.onload` triggers. But it only triggers when the whole iframe with all resources is loaded.
|
||||
|
||||
If the iframe comes from another origin, we can't access its document, so it's impossible.
|
||||
There's also `DOMContentLoaded` event, that triggers sooner than `onload`. As we assume that the iframe comes from the same origin, we can setup the event handler. But we should set it on the right document, so we need to detect when it's there.
|
||||
|
||||
But for the same origin we can setup the event handler. We just need to set it on the right document. For instance, we can try to catch the moment when a new document appears using checks in `setInterval`, and then setup necessary handlers, like this:
|
||||
Here's a small recipe for this.
|
||||
|
||||
We can try to catch the moment when a new document appears using checks in `setInterval`, and then setup necessary handlers:
|
||||
|
||||
```html run
|
||||
<iframe src="/" id="iframe"></iframe>
|
||||
|
@ -173,7 +181,7 @@ But for the same origin we can setup the event handler. We just need to set it o
|
|||
</script>
|
||||
```
|
||||
|
||||
## window.frames
|
||||
## Collection: window.frames
|
||||
|
||||
An alternative way to get a window object for `<iframe>` -- is to get it from the named collection `window.frames`:
|
||||
|
||||
|
@ -215,7 +223,7 @@ if (window == top) { // current window == window.top?
|
|||
}
|
||||
```
|
||||
|
||||
## The sandbox attribute
|
||||
## The "sandbox" iframe attribute
|
||||
|
||||
The `sandbox` attribute allows for the exclusion of certain actions inside an `<iframe>` in order to prevent it executing untrusted code. It "sandboxes" the iframe by treating it as coming from another origin and/or applying other limitations.
|
||||
|
||||
|
@ -331,6 +339,8 @@ window.addEventListener("message", function(event) {
|
|||
}
|
||||
|
||||
alert( "received: " + event.data );
|
||||
|
||||
// can message back using event.source.postMessage(...)
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -358,11 +368,10 @@ For iframes, we can access parent/children windows using:
|
|||
If windows share the same origin (host, port, protocol), then windows can do whatever they want with each other.
|
||||
|
||||
Otherwise, only possible actions are:
|
||||
- Change the location of another window (write-only access).
|
||||
- Change the `location` of another window (write-only access).
|
||||
- Post a message to it.
|
||||
|
||||
|
||||
Exclusions are:
|
||||
Exceptions are:
|
||||
- Windows that share the same second-level domain: `a.site.com` and `b.site.com`. Then setting `document.domain='site.com'` in both of them puts them into the "same origin" state.
|
||||
- If an iframe has a `sandbox` attribute, it is forcefully put into the "different origin" state, unless the `allow-same-origin` is specified in the attribute value. That can be used to run untrusted code in iframes from the same site.
|
||||
|
||||
|
|
|
@ -13,11 +13,15 @@ new File(fileParts, fileName, [options])
|
|||
- **`fileParts`** -- is an array of Blob/BufferSource/String value, same as `Blob`.
|
||||
- **`fileName`** -- file name string.
|
||||
- **`options`** -- optional object:
|
||||
- **`lastModified`** -- a timestamp (integer date) of last modification.
|
||||
- **`lastModified`** -- the timestamp (integer date) of last modification.
|
||||
|
||||
Second, more often we get a file from `<input type="file">` or drag'n'drop or other browser interfaces. Then the file gets these from OS.
|
||||
|
||||
For instance:
|
||||
As `File` inherits from `Blob`, it has same properties, plus:
|
||||
- `name` -- the file name,
|
||||
- `lastModified` -- the timestamp of last modification.
|
||||
|
||||
That's how we can get a `File` object from `<input type="file">`:
|
||||
|
||||
```html run
|
||||
<input type="file" onchange="showFile(this)">
|
||||
|
@ -50,11 +54,17 @@ let reader = new FileReader(); // no arguments
|
|||
|
||||
The main methods:
|
||||
|
||||
- **`readAsArrayBuffer(blob)`** -- read the data as `ArrayBuffer`
|
||||
- **`readAsText(blob, [encoding])`** -- read the data as a string (encoding is `utf-8` by default)
|
||||
- **`readAsDataURL(blob)`** -- encode the data as base64 data url.
|
||||
- **`readAsArrayBuffer(blob)`** -- read the data in binary format `ArrayBuffer`.
|
||||
- **`readAsText(blob, [encoding])`** -- read the data as a text string with the given encoding (`utf-8` by default).
|
||||
- **`readAsDataURL(blob)`** -- read the binary data and encode it as base64 data url.
|
||||
- **`abort()`** -- cancel the operation.
|
||||
|
||||
The choice of `read*` method depends on which format we prefer, how we're going to use the data.
|
||||
|
||||
- `readAsArrayBuffer` - for binary files, to do low-level binary operations. For high-level operations, like slicing, `File` inherits from `Blob`, so we can calll them directly, without reading.
|
||||
- `readAsText` - for text files, when we'd like to get a string.
|
||||
- `readAsDataURL` -- when we'd like to use this data in `src` for `img` or another tag. There's an alternative to reading a file for that, as discussed in chapter <info:blob>: `URL.createObjectURL(file)`.
|
||||
|
||||
As the reading proceeds, there are events:
|
||||
- `loadstart` -- loading started.
|
||||
- `progress` -- occurs during reading.
|
||||
|
@ -95,16 +105,16 @@ function readFile(input) {
|
|||
```
|
||||
|
||||
```smart header="`FileReader` for blobs"
|
||||
As mentioned in the chapter <info:blob>, `FileReader` works for any blobs, not just files.
|
||||
As mentioned in the chapter <info:blob>, `FileReader` can read not just files, but any blobs.
|
||||
|
||||
So we can use it to convert a blob to another format:
|
||||
We can use it to convert a blob to another format:
|
||||
- `readAsArrayBuffer(blob)` -- to `ArrayBuffer`,
|
||||
- `readAsText(blob, [encoding])` -- to string (an alternative to `TextDecoder`),
|
||||
- `readAsDataURL(blob)` -- to base64 data url.
|
||||
```
|
||||
|
||||
|
||||
```smart header="`FileReaderSync` is available for workers only"
|
||||
```smart header="`FileReaderSync` is available inside Web Workers"
|
||||
For Web Workers, there also exists a synchronous variant of `FileReader`, called [FileReaderSync](https://www.w3.org/TR/FileAPI/#FileReaderSync).
|
||||
|
||||
Its reading methods `read*` do not generate events, but rather return a result, as regular functions do.
|
||||
|
@ -116,7 +126,7 @@ That's only inside a Web Worker though, because delays in synchronous calls, tha
|
|||
|
||||
`File` objects inherit from `Blob`.
|
||||
|
||||
In addition to `Blob` methods and properties, `File` objects also have `fileName` and `lastModified` properties, plus the internal ability to read from filesystem. We usually get `File` objects from user input, like `<input>` or drag'n'drop.
|
||||
In addition to `Blob` methods and properties, `File` objects also have `name` and `lastModified` properties, plus the internal ability to read from filesystem. We usually get `File` objects from user input, like `<input>` or Drag'n'Drop events (`ondragend`).
|
||||
|
||||
`FileReader` objects can read from a file or a blob, in one of three formats:
|
||||
- String (`readAsText`).
|
||||
|
@ -125,4 +135,4 @@ In addition to `Blob` methods and properties, `File` objects also have `fileName
|
|||
|
||||
In many cases though, we don't have to read the file contents. Just as we did with blobs, we can create a short url with `URL.createObjectURL(file)` and assign it to `<a>` or `<img>`. This way the file can be downloaded or shown up as an image, as a part of canvas etc.
|
||||
|
||||
And if we're going to send a `File` over a network, that's also easy, as network API like `XMLHttpRequest` or `fetch` natively accepts `File` objects.
|
||||
And if we're going to send a `File` over a network, that's also easy: network API like `XMLHttpRequest` or `fetch` natively accepts `File` objects.
|
||||
|
|
|
@ -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 |