Serg Hospodarets
Engineering and technology leader, speaker, Web and Cloud infrastructure enthusiast.
Pros:
- Code separation
- Separate modules
β
Cons:
- Code reuse
- Global scope
Simple solutions:
- Global vars
- IIFE (immediately-invoked function expressions)
<script src="./vendor/polyfill.js"></script>
<script src="./lib/core.js"></script>
<script src="./components/dropdown.js"></script>
<script src="./components/modal.js"></script>
<script src="./application.js"></script>
// dropdown.js
var delay = 2000; // in ms
// modal.js
var delay = 4000; // in ms
Variable overwrite
// dropdown.js
(function(){
var delay = 2000; // in ms
APP.dropdown.delay = delay;
}());
// modal.js
(function(){
var delay = 4000; // in ms
APP.modal.delay = delay;
}());
Pros:
- Module scope
- Module reuse
- Dependency injection
(testing etc.)
- Tooling: Require.js etc.
Cons:
- Confusing syntax
- Dependencies order because of async nature
- Multiple requests in HTTP 1 era
- Usage with Angular, required additional effort to avoid minification problems
// dropdown.js
define(
// Module definition
'components/dropdown',
// dependencies
['vendor/polyfill'
'lib/core'],
// module code
function(core) {
var dropdown = {};
dropdown.delay = 2000; // in ms
return dropdown;
});
// application.js
define([
'components/dropdown',
'components/modal',
], function(dropdown, modal) {
// APP LOGIC
});
Cons:
- No async module load (either load and use as a callback, or implement something with file.read and extracting module.exports)
- Not a standard, not supported in browsers
// dropdown.js
require('../vendor/polyfill');
require('../lib/core');
var dropdown = {};
dropdown.delay = 2000; // in ms
module.exports= dropdown;
// application.js
const dropdown = require('./components/dropdown');
const modal = require('./components/modal')
// APP LOGIC
Pros:
- Supported in Node.js
- Scoping and context
- Modules reuse
- Tooling: Browserify etc.
Cons:
- Not supported in browsers?
- Not supported in Node.js?
// dropdown.js
import '../vendor/polyfill.js';
import '../lib/core.js';
export const dropdown = {};
dropdown.delay = 2000; // in ms
// application.js
// STATIC imports
import {dropdown} from './components/dropdown.js';
// Dynamic imports
import('./components/modal.js')
.then(({modal})=> {
modal.open();
});
// OTHER APP LOGIC
Pros:
- Module scopes
- Modules reuse
- Multiple, named exports
- Deferred, buy executed in order
- Both static and dynamic
- ES 2015 standard
- Tooling: Webpack/Babel/Rollup
ECMAScript 2015 (aka ES6) standard
June 2015
~middle 2016
Major browsers and Node.js support ~100% of ES6 features (NONE included ESM)
Modules ES Spec describes how to parse, instantiate and evaluate modules, but not how to get the files.
ECMAScript 2015 (aka ES6) standard
June 2015
~middle 2016
Major browsers and Node.js support ~100% of ES6 features (NONE included ESM)
~fall 2016
HTML spec, which describes how to include, find and load modules
ECMAScript 2015 (aka ES6) standard
June 2015
~middle 2016
Major browsers and Node.js support ~100% of ES6 features (NONE included ESM)
~autumn 2016
HTML spec, which describes how to include, find and download modules
First native ES modules implementation in Safari
~end 2016
Support landed in
MS EDGE, Chrome and Node.js
2017-2018
May 2018
Support landed in Firefox
Module Scopes |
Tools |
Static modules |
Dynamic loading |
Clear syntax |
Handy multiple exports |
|
---|---|---|---|---|---|---|
Pure JS |
β |
OOB |
- |
- |
β |
β |
AMD |
β |
RequireJS |
β |
β |
β |
ββ |
CommonJS |
β |
Browserify |
β |
β |
ββ |
β |
ESM |
β |
Webpack, Babel, Rollup |
β |
β |
β |
β |
Great, so Webpack/Babel/Rollup do the thing, right? π§
Node.js OOB support |
Browsers OOB support |
Flag for ES6+ features availability |
Config NOT required |
Build NOT required |
|
---|---|---|---|---|---|
Pure JS |
β |
β |
β |
β |
β |
AMD |
β |
β |
β |
β |
β |
CommonJS |
β |
β |
β |
β |
ββ |
Native ESM |
β |
β |
β |
β |
β |
As result- immediate dev cycle, saves time, benefits in debugging and build configs/browser tools
Project structure
Dependencies graph
<script src="./js/classic.js"></script>
/* type="module" is a flag for the browser,
so the script is executed as a module */
<script type="module" src="./js/module.js"></script>
<script type="module"> console.log('inline module'); </script>
1. How to include an ES module script
<script type="module" src="./js/untranspiled-es6+-features.js"></script>
3. Can be used to deliver untranspiled ES6+ features, as old browsers won't execute it (e.g. Babel flag)
(is just a specific "named" export)
export default 60*60;
// or
const secondsInHour=60*60
export { secondsInHour as default };
export.js
import.js
import secondsInHour from './export.js';
// or
import { default as secondsInHour }
from './export.js';
export const secondsInHour=60*60;
import {secondsInHour} from './export.js';
// Just execute the module
import './export.js';
// Importing the module as an object
import * as lib from './export.js';
lib.secondsInHour; // 3600
lib.secondsInDay=60*60*24; // 86400
// Multiple exports
export const secondsInHour=60*60;
export const secondsInDay=60*60*24;
// Module is not required
// to export something
Named
Default
Just
execute
Multiple
exports
Modules have own scopes and contexts, the same time having access to the global scope
imports are immutable references and cannot be reassigned
'import' hoisting, top level only
Real Execution Order
always CORS (cross-origin mechanism)
Fix
must have a βvalid JavaScript MIME type
deferred by default, non-blocking but executed in the order
strict by default
Construction β find, download, and parse all of the files into module records.
Instantiation βfind boxes in memory to place all of the exported values in (but donβt fill them in with values yet). Then make both exports and imports point to those boxes in memory. This is called linking.
Evaluation βrun the code to fill in the boxes with the variablesβ actual values.
import {nextEven, isEven} from './evens.js';
import {nextOdd, isOdd} from './odds.js';
// "true" output is expected in all of these expressions
console.log(nextEven(1) === 2);
console.log(nextOdd(1) === 3);
console.log(isOdd(1));
console.log(!isOdd(0));
console.log(isEven(0));
console.log(!isEven(1));
// ./evens.js
import {isOdd} from './odds.mjs';
export function nextEven (n) {
return isOdd(n) ? n + 1 : n + 2;
}
export function isEven(n) {
return n % 2 === 0;
}
// ./odds.js
import {isEven} from './evens.mjs';
export function nextOdd(n) {
return isEven(n) ? n + 1 : n + 2;
}
export function isOdd(n) {
return !isEven(n);
}
Works because of the "instantiation" phase, which handles circular dependencies between the modules.
cannot be dynamic for static imports
no extensions resolutions
no Node.js node_modules etc. resolutions
Possible solutions: Service Worker URLs proxy- quite complex
β’ must be a full URL (include extension)
β’ must be an absolute URL or:
β’ must start with β/β, β./β, or β../β
<script type="packagemap" src="package-map.json"></script>
<script type="packagemap">
{
"path_prefix": "/node_modules",
"packages": {
"moment": { "main": "src/moment.js" },
"lodash": { "path": "lodash-es", "main": "lodash.js" }
}
}
</script>
<script type="module">
import moment from 'moment';
import _ from 'lodash';
</script>
Main questions/alternatives:
- should it replicate Node.js module resolution algorithm
- should it follow NPM "main" field resolution
- how to apply for modules in browsers / Node.js and Workers
Other examples: Code Splitting, polyfills, additional pages, 3rd party scripts, and services etc.
β’ Under the flag since Node.js 8.5
β’ Complete polyfill for Node.jsΒ (not fully compliant)Β is available
β’ Requires .mjs extensions for separation
main |
browser |
module |
|
---|---|---|---|
Format |
CommonJS |
CommonJS |
ESM |
Webpack |
β |
β |
β |
Rollup |
β |
β |
β |
β’ CSS Houdiny (API to expose browser internals for developers)
Use Worklets- ES modules by default
Online test: https://hospodarets.com/es-modules-test/
β’Β aka Wasm (the new type of high-performant code that can be run in modern web browsers)
in progress of applying ESM
Web/Service Workers
β’Β TypeScript: some compiler options are available. Open issue for full proper support
HTTP/2 is required
Chrome team analysis ESM performance work on Β optimizations
DEMO: Loading ~640 lodash-es native modulesΒ comparing with the bundled version
- allows preloading module script graphs
<link rel="modulepreload" href="dependency.js">
<link rel="modulepreload" href="sub-dependency.js">
...
<script type="module" src="app.js"></script>
<script type="module" src="app.js"></script>
// other ability- for dynamic imports (preloaded but not evaluated)
<link rel="modulepreload" href="modal.js">
<button onclick="import('./modal.js')
.then(modal => modal.open())">
Script
onload/onerror
events work for ESM
Β
Tests should be deferred
Add the nomodule attribute for the bundled script
Add type="module" script pointing to the entry file
Resolve import URLs (aliases, '.js' extensions etc.)
Resolve non-JS modules (load dynamically, extract CSS etc.)
Solve node_modules dependencies and their publishing to prod
β’ Node.js:
β’ Web components:
Polymer
Great support
How to include: type="module" (browser), .mjs (Node.js)
nomodule solution for migration
HTTP2 and <link module-preload /> for performance
Simplified configuration, debugging and publishing
Not fully ready for enterprise projects, perfect for small and middle-size apps
By Serg Hospodarets
All the modern browsers support native JavaScript modules, and itβs a perfect time to start using them, which will change the way we are bundling the JavaScript using Webpack, Rollup, and other bundlers, and how the code is executed. We will take a look how they work, what is the level of support in the browsers and Node.js, plus main findings and gotchas on the way of publishing and using them in production. Looking into examples, we will understand the native modules features, performance details and lazy loading JS modules techniques.
Engineering and technology leader, speaker, Web and Cloud infrastructure enthusiast.