Tan Li Hau
One level deeper
- What is a module bundler
- Live Coding (write a module bundler)
<html>
...
<script src="/src/foo.js" />
<script src="/src/bar.js" />
<script src="/src/baz.js" />
<script src="/src/qux.js" />
<script src="/src/quux.js" />
<script src="/src/corge.js" />
<script src="/src/grault.js" />
<script src="/src/garply.js" />
<script src="/src/waldo.js" />
<script src="/src/fred.js" />
...
</html>
<html>
...
<script src="/src/foo.js" />
<script src="/src/bar.js" />
<script src="/src/baz.js" />
<script src="/src/qux.js" />
<script src="/src/quux.js" />
<script src="/src/corge.js" />
<script src="/src/grault.js" />
<script src="/src/garply.js" />
<script src="/src/waldo.js" />
<script src="/src/fred.js" />
...
</html>
10 requests for JS code!
<html>
...
<script src="/dist/bundle.js" />
</html>
1 request for JS code!
<html>
...
<script src="/dist/bundle.js" />
</html>
const foo = require('./foo');
module.exports = bar;
import foo from './foo';
export default bar;
// circle.js
const PI = 3.141;
export default function area(radius) {
return PI * radius * radius;
}
// square.js
export default function area(side) {
return side * side;
}
// app.js
import squareArea from './square';
import circleArea from './circle';
console.log('Area of square: ', squareArea(5))
console.log('Area of circle', circleArea(5))
// webpack-bundle.js
const modules = {
'circle.js': function(exports, require) {
const PI = 3.141;
exports.default = function area(radius) {
return PI * radius * radius;
}
},
'square.js': function(exports, require) {
export.default = function area(side) {
return side * side;
}
},
'app.js': function(exports, require) {
const squareArea = require('square.js').default;
const circleArea = require('circle.js').default;
console.log('Area of square: ', squareArea(5))
console.log('Area of circle', circleArea(5))
}
}
webpackStart({
modules,
entry: 'app.js'
});
// ... continue webpack-bundle.js
function webpackStart({ modules, entry }) {
const moduleCache = {};
const require = (moduleName) => {
// if in cache, return the cached version
if (moduleCache[moduleName]) {
return moduleCache[moduleName];
}
const exports = {};
// NOTE: you will thank me later
moduleCache[moduleName] = exports;
// "require"-ing the module,
// exported stuff will assigned to "exports"
modules[moduleName](exports, require);
return moduleCache[moduleName];
}
// start the program
require(entry);
}
// rollup-bundle.js
const PI = 3.141;
function circle$area(radius) {
return PI * radius * radius;
}
function square$area(side) {
return side * side;
}
console.log('Area of square: ', square$area(5));
console.log('Area of circle', circle$area(5));
// shape.js
const circle = require('./circle');
module.exports.PI = 3.141;
// circle.js
const PI = require('./shape');
const _PI = PI * 1
module.exports = function(radius) {
return _PI * radius * radius;
}
// rollup-bundle.js
const _PI = PI * 1;
function circle$Area(radius) {
return PI * radius * radius;
}
const PI = 3.141;
- Larger bundle
- Wrapped each module with 1 function
- Has a "runtime" that glues static import and dynamic import
- Smaller bundle
- Flat bundle
- Everything is defined into global scope. Uses browser's module system for dynamic import
(The way)
// <root>/a.js
import './b.js'
// <root>/foo/a.js
import './b.js'
// <root>/a.js
import './b.js'
// <root>/foo/a.js
import './b.js'
request path is the same,
but the requested file is different
// <root>/a.js
import './b.js'
// <root>/foo/a.js
import './b.js'
// /foo/bar/baz/a.js
import './b'
// /foo/bar/baz/a.js
import './b'
Load as File
/foo/bar/baz/b
/foo/bar/baz/b.js
/foo/bar/baz/b.json
/foo/bar/baz/b.node
// /foo/bar/baz/a.js
import './b'
Load as Directory
if /foo/bar/baz/b/package.json :
find "main" in package.json and load the file
else:
/foo/bar/baz/b/index.js
/foo/bar/baz/b/index.json
/foo/bar/baz/b/index.node
// /foo/bar/baz/a.js
import 'b'
// /foo/bar/baz/a.js
import 'b'
Load as node_module
/foo/bar/baz/node_modules/b
/foo/bar/node_modules/b
/foo/node_modules/b
/node_modules/b
// webpack-bundle.js
const modules = {
'circle.js': function(exports, require) {
const PI = 3.141;
exports.default = function area(radius) {
return PI * radius * radius;
}
},
'square.js': function(exports, require) {
export.default = function area(side) {
return side * side;
}
},
'app.js': function(exports, require) {
const squareArea = require('square.js').default;
const circleArea = require('circle.js').default;
console.log('Area of square: ', squareArea(5))
console.log('Area of circle', circleArea(5))
}
}
webpackStart({
modules,
entry: 'app.js'
});
// ... continue webpack-bundle.js
function webpackStart({ modules, entry }) {
const moduleCache = {};
const require = (moduleName) => {
// if in cache, return the cached version
if (moduleCache[moduleName]) {
return moduleCache[moduleName];
}
const exports = {};
// NOTE: you will thank me later
moduleCache[moduleName] = exports;
// "require"-ing the module,
// exported stuff will assigned to "exports"
modules[moduleName](exports, require);
return moduleCache[moduleName];
}
// start the program
require(entry);
}
- import CSS, SVG, PNG, TS, ...
- commonjs, amd, dynamic import...
- optimisation: minification, code splitting, ...
- webpack dev server, watching, hot reloading
- ...
- import CSS, SVG, PNG, TS, ...
- commonjs, amd, dynamic import...
- optimisation: minification, code splitting, ...
- webpack dev server, watching, hot reloading
- ...
Prior art: https://git.garena.com/tanlh/cepture
Article: writing your own bundler
- import CSS, SVG, PNG, TS, ...
- commonjs, amd, dynamic import...
- optimisation: minification, code splitting, ...
- webpack dev server, watching, hot reloading
- ...
article: writing your own bundler
WHY NOT <SCRIPT TYPE="MODULE" /> ?
<html>
...
<script type="module" src="/src/bar.mjs" />
</html>
<html>
...
<script type="module" src="/src/bar.mjs" />
</html>
// bar.mjs
import foo from './foo.mjs';
export default bar;