move generators

This commit is contained in:
Ilya Kantor 2019-03-07 13:51:21 +03:00
parent 890b57b75b
commit 5bca8a5ee2
37 changed files with 2 additions and 0 deletions

View file

@ -0,0 +1,375 @@
# Modules, introduction
As our application grows bigger, we want to split it into multiple files, so called 'modules'.
A module usually contains a class or a library of useful functions.
For a long time, Javascript existed without a language-level module syntax. That wasn't a problem, because initially scripts were small and simple. So there was no need.
But eventually scripts became more and more complex, so the community invented a variety of ways to organize code into modules.
For instance:
- [AMD](https://en.wikipedia.org/wiki/Asynchronous_module_definition) -- one of the most ancient module systems, initially implemented by the library [require.js](http://requirejs.org/).
- [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) -- the module system created for Node.JS server.
- [UMD](https://github.com/umdjs/umd) -- one more module system, suggested as a universal one, compatible with AMD and CommonJS.
Now all these slowly become a part of history, but we still can find them in old scripts. The language-level module system appeared in the standard in 2015, gradually evolved since then, and is now supported by all major browsers and in Node.js.
## What is a module?
A module is just a file, a single script, as simple as that.
Directives `export` and `import` allow to interchange functionality between modules:
- `export` keyword labels variables and functions that should be accessible from outside the file.
- `import` allows to import functionality from other modules.
For instance, if we have a file `sayHi.js` exporting a function:
```js
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
```
...Then another file may import and use it:
```js
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
```
In this tutorial we concentrate on the language itself, but we use browser as the demo environment, so let's see how modules work in the browser.
To use modules, we must set the attribute `<script type="module">`, like this:
[codetabs src="say" height="140" current="index.html"]
The browser automatically fetches and evaluates imports, then runs the script.
## Core module features
What's different in modules, compared to "regular" scripts?
There are core features, valid both for browser and server-side Javascript.
### Always "use strict"
Modules always `use strict`. E.g. assigning to an undeclared variable will give an error.
```html run
<script type="module">
a = 5; // error
</script>
```
Here we can see it in the browser, but the same is true for any module.
### Module-level scope
Each module has its own top-level scope. In other words, top-level variables and functions from a module are not seen in other scripts.
In the example below, two scripts are imported, and `hello.js` tries to use `user` variable declared in `user.js`, and fails:
[codetabs src="scopes" height="140" current="index.html"]
Modules are expected to `export` what they want to be accessible from outside and `import` what they need.
So we should import `user.js` directly into `hello.js` instead of `index.html`.
That's the correct variant:
[codetabs src="scopes-working" height="140" current="hello.js"]
In the browser, independant top-level scope also exists for each `<script type="module">`:
```html run
<script type="module">
// The variable is only visible in this module script
let user = "John";
</script>
<script type="module">
*!*
alert(user); // Error: user is not defined
*/!*
</script>
```
If we really need to make a "global" in-browser variable, we can explicitly assign it to `window` and access as `window.user`. But that's an exception requiring a good reason.
### A module code is evaluated only the first time when imported
If a same module is imported into multiple other places, it's code is executed only the first time, then exports are given to all importers.
That has important consequences. Let's see that on examples.
First, if executing a module code brings side-effects, like showing a message, then importing it multiple times will trigger it only once -- the first time:
```js
// 📁 alert.js
alert("Module is evaluated!");
```
```js
// Import the same module from different files
// 📁 1.js
import `./alert.js`; // Module is evaluated!
// 📁 2.js
import `./alert.js`; // (nothing)
```
In practice, top-level module code is mostly used for initialization. We create data structures, pre-fill them, and if we want something to be reusable -- export it.
Now, a more advanced example.
Let's say, a module exports an object:
```js
// 📁 admin.js
export let admin = {
name: "John"
};
```
If this module is imported from multiple files, the module is only evaluated the first time, `admin` object is created, and then passed to all further importers.
All importers get exactly the one and only `admin` object:
```js
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
*!*
// Both 1.js and 2.js imported the same object
// Changes made in 1.js are visible in 2.js
*/!*
```
So, let's reiterate -- the module is executed only once. Exports are generated, and then they are shared between importers, so if something changes the `admin` object, other modules will see that .
Such behavior is great for modules that require configuration. We can set required properties on the first import, and then in further imports it's ready.
For instance, `admin.js` module may provide certain functionality, but expect the credentials to come into the `admin` object from outside:
```js
// 📁 admin.js
export let admin = { };
export function sayHi() {
alert(`Ready to serve, ${admin.name}!`);
}
```
Now, in `init.js`, the first script of our app, we set `admin.name`. Then everyone will see it, including calls made from inside `admin.js` itself:
```js
// 📁 init.js
import {admin} from './admin.js';
admin.name = "Pete";
```
```js
// 📁 other.js
import {admin, sayHi} from './admin.js';
alert(admin.name); // *!*Pete*/!*
sayHi(); // Ready to serve, *!*Pete*/!*!
```
### import.meta
The object `import.meta` contains the information about the current module.
Its content depends on the environment. In the browser, it contains the url of the script, or a current webpage url if inside HTML:
```html run height=0
<script type="module">
alert(import.meta.url); // script url (url of the html page for an inline script)
</script>
```
### Top-level "this" is undefined
That's kind of a minor feature, but for completeness we should mention it.
In a module, top-level `this` is undefined, as opposed to a global object in non-module scripts:
```html run height=0
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
```
## Browser-specific features
There are also several browser-specific differences of scripts with `type="module"` compared to regular ones.
You may want skip those for now if you're reading for the first time, or if you don't use Javascript in a browser.
### Module scripts are deferred
Module scripts are *always* deferred, same effect as `defer` attribute (described in the chapter [](info:onload-ondomcontentloaded)), for both external and inline scripts.
In other words:
- external module scripts `<script type="module" src="...">` don't block HTML processing.
- module scripts wait until the HTML document is fully ready.
- relative order is maintained: scripts that go first in the document, execute first.
As a side-effect, module scripts always see HTML elements below them.
For instance:
```html run
<script type="module">
*!*
alert(typeof button); // object: the script can 'see' the button below
*/!*
// as modules are deferred, the script runs after the whole page is loaded
</script>
<script>
*!*
alert(typeof button); // Error: button is undefined, the script can't see elements below
*/!*
// regular scripts run immediately, before the rest of the page is processed
</script>
<button id="button">Button</button>
```
Please note: the second script actually works before the first! So we'll see `undefined` first, and then `object`.
That's because modules are deferred, so way wait for the document to be processed. The regular scripts runs immediately, so we saw its output first.
When using modules, we should be aware that HTML-document can show up before the Javascript application is ready. Some functionality may not work yet. We should put transparent overlays or "loading indicators", or otherwise ensure that the visitor won't be confused because of it.
### Async works on inline scripts
Async attribute `<script async type="module">` is allowed on both inline and external scripts. Async scripts run immediately when imported modules are processed, independantly of other scripts or the HTML document.
For example, the script below has `async`, so it doesn't wait for anyone.
It performs the import (fetches `./analytics.js`) and runs when ready, even if HTML document is not finished yet, or if other scripts are still pending.
That's good for functionality that doesn't depend on anything, like counters, ads, document-level event listeners.
```html
<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script *!*async*/!* type="module">
import {counter} from './analytics.js';
counter.count();
</script>
```
### External scripts
There are two notable differences of external module scripts:
1. External scripts with same `src` run only once:
```html
<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
```
2. External scripts that are fetched from another domain require [CORS](mdn:Web/HTTP/CORS) headers. In other words, if a module script is fetched from another domain, the remote server must supply a header `Access-Control-Allow-Origin: *` (may use fetching domain instead of `*`) to indicate that the fetch is allowed.
```html
<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="*!*http://another-site.com/their.js*/!*"></script>
```
That ensures better security by default.
### No bare modules allowed
In the browser, in scripts (not in HTML), `import` must get either a relative or absolute URL. So-called "bare" modules, without a path, are not allowed.
For instance, this `import` is invalid:
```js
import {sayHi} from 'sayHi'; // Error, "bare" module
// must be './sayHi.js' or wherever the module is
```
Certain environments, like Node.js or bundle tools allow bare modules, as they have own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet.
### Compatibility, "nomodule"
Old browsers do not understand `type="module"`. Scripts of the unknown type are just ignored. For them, it's possible to provide a fallback using `nomodule` attribute:
```html run
<script type="module">
alert("Runs in modern browsers");
</script>
<script nomodule>
alert("Modern browsers know both type=module and nomodule, so skip this")
alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>
```
If we use bundle tools, then as modules are bundled together, their `import/export` statements are replaced by special bundler calls, so the resulting build does not require `type="module"`, and we can put it into a regular script:
```html
<!-- Assuming we got bundle.js from a tool like Webpack -->
<script src="bundle.js"></script>
```
## Build tools
In real-life, browser modules are rarely used in their "raw" form. Usually, we bundle them together with a special tool such as [Webpack](https://webpack.js.org/) and deploy to the production server.
One of the benefits of using bundlers -- they give more control over how modules are resolved, allowing bare modules and much more, like CSS/HTML modules.
Build tools do the following:
1. Take a "main" module, the one intended to be put in `<script type="module">` in HTML.
2. Analyze its dependencies: imports and then imports of imports etc.
3. Build a single file with all modules (or multiple files, that's tunable), replacing native `import` calls with bundler functions, so that it works. "Special" module types like HTML/CSS modules are also supported.
4. In the process, other transforms and optimizations may be applied:
- Unreachable code removed.
- Unused exports removed ("tree-shaking").
- Development-specific statements like `console` and `debugger` removed.
- Modern, bleeding-edge Javascript syntax may be transformed to older one with similar functionality using [Babel](https://babeljs.io/).
- The resulting file is minified (spaces removed, variables replaced with shorter named etc).
That said, native modules are also usable. So we won't be using Webpack here: you can configure it later.
## Summary
To summarize, the core concepts are:
1. A module is a file. To make `import/export` work, browsers need `<script type="module">`, that implies several differences:
- Deferred by default.
- Async works on inline scripts.
- External scripts need CORS headers.
- Duplicate external scripts are ignored.
2. Modules have their own, local top-level scope and interchange functionality via `import/export`.
3. Modules always `use strict`.
4. Module code is executed only once. Exports are created once and shared between importers.
So, generally, when we use modules, each module implements the functionality and exports it. Then we use `import` to directly import it where it's needed. Browser loads and evaluates the scripts automatically.
In production, people often use bundlers such as [Webpack](https://webpack.js.org) to bundle modules together for performance and other reasons.
In the next chapter we'll see more examples of modules, and how things can be exported/imported.

View file

@ -0,0 +1,6 @@
<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>

View file

@ -0,0 +1,3 @@
export function sayHi(user) {
return `Hello, ${user}!`;
}

View file

@ -0,0 +1,3 @@
import {user} from './user.js';
document.body.innerHTML = user; // John

View file

@ -0,0 +1,2 @@
<!doctype html>
<script type="module" src="hello.js"></script>

View file

@ -0,0 +1 @@
export let user = "John";

View file

@ -0,0 +1 @@
alert(user); // no such variable (each module has independent variables)

View file

@ -0,0 +1,3 @@
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

View file

@ -0,0 +1 @@
let user = "John";

View file

@ -0,0 +1,438 @@
# Export and Import
Export and import directives are very versatile.
In the previous chapter we saw a simple use, now let's explore more examples.
## Export before declarations
We can label any declaration as exported by placing `export` before it, be it a variable, function or a class.
For instance, here all exports are valid:
```js
// export an array
*!*export*/!* let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// export a constant
*!*export*/!* const MODULES_BECAME_STANDARD_YEAR = 2015;
// export a class
*!*export*/!* class User {
constructor(name) {
this.name = name;
}
}
```
````smart header="No semicolons after export class/function"
Please note that `export` before a class or a function does not make it a [function expression](info:function-expressions-arrows). It's still a function declaration, albeit exported.
Most Javascript style guides recommend semicolons after statements, but not after function and class declarations.
That's why there should be no semicolons at the end of `export class` and `export function`.
```js
export function sayHi(user) {
alert(`Hello, ${user}!`);
} *!* // no ; at the end */!*
```
````
## Export apart from declarations
Also, we can put `export` separately.
Here we first declare, and then export:
```js
// 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
*!*
export {sayHi, sayBye}; // a list of exported variables
*/!*
```
...Or, technically we could put `export` above functions as well.
## Import *
Usually, we put a list of what to import into `import {...}`, like this:
```js
// 📁 main.js
*!*
import {sayHi, sayBye} from './say.js';
*/!*
sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!
```
But if the list is long, we can import everything as an object using `import * as <obj>`, for instance:
```js
// 📁 main.js
*!*
import * as say from './say.js';
*/!*
say.sayHi('John');
say.sayBye('John');
```
At first sight, "import everything" seems such a cool thing, short to write, why should we ever explicitly list what we need to import?
Well, there are few reasons.
1. Modern build tools ([webpack](http://webpack.github.io) and others) bundle modules together and optimize them to speedup loading and remove unused stuff.
Let's say, we added a 3rd-party library `lib.js` to our project with many functions:
```js
// 📁 lib.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
```
Now if we in fact need only one of them in our project:
```js
// 📁 main.js
import {sayHi} from './lib.js';
```
...Then the optimizer will automatically detect it and totally remove the other functions from the bundled code, thus making the build smaller. That is called "tree-shaking".
2. Explicitly listing what to import gives shorter names: `sayHi()` instead of `lib.sayHi()`.
3. Explicit imports give better overview of the code structure: what is used and where. It makes code support and refactoring easier.
## Import "as"
We can also use `as` to import under different names.
For instance, let's import `sayHi` into the local variable `hi` for brevity, and same for `sayBye`:
```js
// 📁 main.js
*!*
import {sayHi as hi, sayBye as bye} from './say.js';
*/!*
hi('John'); // Hello, John!
bye('John'); // Bye, John!
```
## Export "as"
The similar syntax exists for `export`.
Let's export functions as `hi` and `bye`:
```js
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
```
Now `hi` and `bye` are official names for outsiders:
```js
// 📁 main.js
import * as say from './say.js';
say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!
```
## export default
So far, we've seen how to import/export multiple things, optionally "as" other names.
In practice, modules contain either:
- A library, pack of functions, like `lib.js`.
- Or an entity, like `class User` is descirbed in `user.js`, the whole module has only this class.
Mostly, the second approach is preferred, so that every "thing" resides in its own module.
Naturally, that requires a lot of files, as everything wants its own module, but that's not a problem at all. Actually, code navigation becomes easier, if files are well-named and structured into folders.
Modules provide special `export default` syntax to make "one thing per module" way look better.
It requires following `export` and `import` statements:
1. Put `export default` before the "main export" of the module.
2. Call `import` without curly braces.
For instance, here `user.js` exports `class User`:
```js
// 📁 user.js
export *!*default*/!* class User { // just add "default"
constructor(name) {
this.name = name;
}
}
```
...And `main.js` imports it:
```js
// 📁 main.js
import *!*User*/!* from './user.js'; // not {User}, just User
new User('John');
```
Imports without curly braces look nicer. A common mistake when starting to use modules is to forget curly braces at all. So, remember, `import` needs curly braces for named imports and doesn't need them for the default one.
| Named export | Default export |
|--------------|----------------|
| `export class User {...}` | `export default class User {...}` |
| `import {User} from ...` | `import User from ...`|
Naturally, there may be only one "default" export per file.
We may have both default and named exports in a single module, but in practice people usually don't mix them. A module has either named exports or the default one.
**Another thing to note is that named exports must (naturally) have a name, while `export default` may be anonymous.**
For instance, these are all perfecly valid default exports:
```js
export default class { // no class name
constructor() { ... }
}
export default function(user) { // no function name
alert(`Hello, ${user}!`);
}
// export a single value, without making a variable
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
```
That's fine, because `export default` is only one per file, so `import` always knows what to import.
Contrary to that, omitting a name for named imports would be an error:
```js
export class { // Error! (non-default export needs a name)
constructor() {}
}
```
### "Default" alias
The "default" word is a kind of "alias" for the default export, for scenarios when we need to reference it somehow.
For example, if we already have a function declared, that's how to `export default` it:
```js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
export {sayHi as default}; // same as if we added "export default" before the function
```
Or, let's say a module `user.js` exports one main "default" thing and a few named ones (rarely the case, but happens):
```js
// 📁 user.js
export default class User {
constructor(name) {
this.name = name;
}
}
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
```
Here's how to import the default export along with a named one:
```js
// 📁 main.js
import {*!*default as User*/!*, sayHi} from './user.js';
new User('John');
```
Or, if we consider importing `*` as an object, then the `default` property is exactly the default export:
```js
// 📁 main.js
import * as user from './user.js';
let User = user.default;
new User('John');
```
### Should I use default exports?
One should be careful about using default exports, because they are somewhat more different to maintain.
Named exports are explicit. They exactly name what they import, so we have that information from them, that's a good thing.
Also, named exports enforce us to use exactly the right name to import:
```js
import {User} from './user.js';
```
For default exports, we need to create a name on our own:
```js
import MyUser from './user.js'; // could be import Anything..., and it'll work
```
So, there's a little bit more freedom that can be abused, so that team members may use different names for the same thing.
Usually, to avoid that and keep the code consistent, there's a rule that imported variables should correspond to file names, e.g:
```js
import User from './user.js';
import LoginForm from './loginForm.js';
import func from '/path/to/func.js';
...
```
Another solution would be to use named exports everywhere. Even if only a single thing is exported, it's still exported under a name, without `default`.
That also makes re-export (see below) a little bit easier.
## Re-export
"Re-export" syntax `export ... from ...` allows to import things and immediately export them (possibly under another name), like this:
```js
export {sayHi} from './say.js';
export {default as User} from './user.js';
```
What's the point, why that's needed? Let's see a practical use case.
Imagine, we're writing a "package": a folder with a lot of modules, mostly needed internally, with some of the functionality exported outside (tools like NPM allow to publish and distribute packages, but here it doesn't matter).
A directory structure could be like this:
```
auth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
...
```
We'd like to expose the package functionality via a single entry point, the "main file" `auth/index.js`, to be used like this:
```js
import {login, logout} from 'auth/index.js'
```
The idea is that outsiders, developers who use our package, should not meddle with its internal structure. They should not search for files inside our package folder. We export only what's necessary in `auth/index.js` and keep the rest hidden from prying eyes.
Now, as the actual exported functionality is scattered among the package, we can gather and "re-export" it in `auth/index.js`:
```js
// 📁 auth/index.js
import {login, logout} from './helpers.js';
export {login, logout};
import User from './user.js';
export {User};
import Githib from './providers/github.js';
export {Github};
...
```
"Re-exporting" is just a shorter notation for that:
```js
// 📁 auth/index.js
export {login, logout} from './helpers.js';
// or, to re-export all helpers, we could use:
// export * from './helpers.js';
export {default as User} from './user.js';
export {default as Githib} from './providers/github.js';
...
```
````warn header="Re-exporting default is tricky"
Please note: `export User from './user.js'` won't work. It's actually a syntax error. To re-export the default export, we must mention it explicitly `{default as ...}`, like in the example above.
Also, there's another oddity: `export * from './user.js'` re-exports only named exports, exluding the default one. Once again, we need to mention it explicitly.
For instance, to re-export everything, two statements will be necessary:
```js
export * from './module.js'; // to re-export named exports
export {default} from './module.js'; // to re-export default
```
The default should be mentioned explicitly only when re-exporting: `import * as obj` works fine. It imports the default export as `obj.default`. So there's a slight asymmetry between import and export constructs here.
````
## Summary
There are following types of `export`:
- Before declaration:
- `export [default] class/function/variable ...`
- Standalone:
- `export {x [as y], ...}`.
- Re-export:
- `export {x [as y], ...} from "mod"`
- `export * from "mod"` (doesn't re-export default).
- `export {default [as y]} from "mod"` (re-export default).
Import:
- Named exports from module:
- `import {x [as y], ...} from "mod"`
- Default export:
- `import x from "mod"`
- `import {default as x} from "mod"`
- Everything:
- `import * as obj from "mod"`
- Only fetch/evalute the module, don't import:
- `import "mod"`
We can put import/export statements below or after other code, that doesn't matter.
So this is technically fine:
```js
sayHi();
import {sayHi} from './say.js'; // import at the end of the file
```
In practice imports are usually at the start of the file, but that's only for better convenience.
**Please note that import/export statements don't work if inside `{...}`.**
A conditional import, like this, won't work:
```js
if (something) {
import {sayHi} from "./say.js"; // Error: import must be at top level
}
```
...But what if we really need to import something conditionally? Or at the right time? Like, load a module upon request, when it's really needed?
We'll see dynamic imports in the next chapter.

View file

@ -0,0 +1,54 @@
# Dynamic imports
Export and import statements that we covered in previous chaters are called "static".
What's because they are indeed static. The syntax is very strict.
First, we can't dynamicaly generate any parameters of `import`.
The module path must be a primitive string, can't be a function call. This won't work:
```js
import ... from *!*getModuleName()*/!*; // Error, only from "string" is allowed
```
Second, we can't import conditionally or at run-time:
```js
if(...) {
import ...; // Error, not allowed!
}
{
import ...; // Error, we can't put import in any block
}
```
That's because, import/export aim to provide a backbone for the code structure. That's a good thing, as code structure can be analyzed, modules can be gathered and bundled together, unused exports can be removed (tree-shaken). That's possible only because everything is fixed.
But how do we import a module dynamically, on-demand?
## The import() function
The `import(module)` function can be called from anywhere. It returns a promise that resolves into a module object.
The usage pattern looks like this:
```js run
let modulePath = prompt("Module path?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, no such module?>)
```
Or, we could use `let module = await import(modulePath)` if inside an async function.
Like this:
[codetabs src="say" current="index.html"]
So, dynamic imports are very simple to use.
Also, dynamic imports work in regular scripts, they don't require `script type="module"`.

View file

@ -0,0 +1,10 @@
<!doctype html>
<script>
async function load() {
let say = await import('./say.js');
say.hi(); // Hello!
say.bye(); // Bye!
say.default(); // Module loaded (export default)!
}
</script>
<button onclick="load()">Click me</button>

View file

@ -0,0 +1,11 @@
export function hi() {
alert(`Hello`);
}
export function bye() {
alert(`Bye`);
}
export default function() {
alert("Module loaded (export default)!");
}

2
1-js/13-modules/index.md Normal file
View file

@ -0,0 +1,2 @@
# Modules