Luciano Mammino PRO
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com
21/11/2018
❤️
💔💔
Solution Architect at
with @mariocasciaro
with @andreaman87
with @Podgeypoos79
Dynamic DOM manipulation
React, Angular, Vue
are so... overrated! 😆
Dynamic Favicon
Custom animated tooltips
🎉 Confetti rainfall 😱
7 Requests only for the JS code!
zepto@1.2.0/dist/zepto.min.js
uuidjs@4.0.3/dist/uuid.core.js
store2@2.7.0/dist/store2.min.js
tippy.js@2.2.3/dist/tippy.all.min.js
confetti@0.0.10/lib/main.js
favico.js@0.3.10/favico-0.3.10.min.js
zepto@1.2.0/dist/zepto.min.js
uuidjs@4.0.3/dist/uuid.core.js
store2@2.7.0/dist/store2.min.js
tippy.js@2.2.3/dist/tippy.all.min.js
confetti@0.0.10/lib/main.js
favico.js@0.3.10/favico-0.3.10.min.js
vendors.js
+
+
+
+
+
=
./buildVendors.sh > vendors.js
buildVendors.sh
$
npx lumpy build
$
# lumpy.txt https://unpkg.com/zepto@1.2.0/dist/zepto.min.js https://unpkg.com/uuidjs@4.0.3/dist/uuid.core.js https://unpkg.com/store2@2.7.0/dist/store2.min.js https://unpkg.com/tippy.js@2.2.3/dist/tippy.all.min.js https://unpkg.com/confetti-js@0.0.11/dist/index.min.js https://unpkg.com/dom-confetti@0.0.10/lib/main.js https://unpkg.com/favico.js@0.3.10/favico-0.3.10.min.js
Lumpy allows you to define
all the vendors in a text file
lumpy build
$
7 requests
2 requests
Even better if you "minify" these!
The bricks for structuring non-trivial applications, but also the main mechanism to enforce information hiding by keeping private all the functions and variables that are not explicitly marked to be exported
* yeah, I quite like quoting my stuff... 😅
(Immediately Invoked Function Expression)
const sum = (a, b) => a + b
function sum(a, b) {
return a + b
}
const four = sum(2, 2)
(a, b) => {
const secretString = "Hello"
return a + b
}
console.log(secretString) // undefined
(arg1, arg2) => {
// do stuff here
const iAmNotVisibleOutside = true
}
(
A function with its own scope
This wrapper executes the function immediately and passes arguments from the outer scope
)(someArg1, someArg2)
const myModule = (() => {
const privateFoo = () => { /* ... */ }
const privateBar = [ /* ... */ ]
const exported = {
publicFoo: () => { /* ... */ },
publicBar: [ /* ... */ ]
};
return exported
})()
A module
myModule.publicFoo()
myModule.publicBar[0]
myModule.privateFoo // undefined
myModule.privateBar // undefined
privateFoo // undefined
privateBar // undefined
IIFE
Creates an isolated scope
and executes it
information hiding
non-exposed functionality
defines exported functionalities
propagates the exports
to the outer scope (assigning it to myModule)
Can access exported functionalities
No visibility for the
non-exported ones
Must have
Nice to have
globals
CommonJS (Node.js)
AMD (Require.js / Dojo)
UMD
ES2015 Modules (ESM)
Many others (SystemJS, ...)
var $, jQuery
$ = jQuery = (() => {
return { /* ... */ }
})()
// ... use $ or jQuery in the global scope
$.find('.button').remove()
👎 Might generate naming collisions
(e.g. $ overrides browser global variable)
👎 Modules needs to be "fully loaded" in the right order
👎 Cannot import parts of modules
// loDash.js
const loDash = {
/* ... */
}
module.exports = loDash
module
// app.js
// import full module
const _ = require('./loDash')
_.concat([1], [2], [3])
// or import single functionality
const { concat } = require('./loDash')
concat([1], [2], [3])
app using module
👍 No naming collisions
(imported modules can be renamed)
👍 Huge repository of modules through NPM
👎 Synchronous import only
👎 Works natively on the server side only (Node.js)
// jquery-1.9.0.js
define(
'jquery',
['sizzle', 'jqueryUI'],
function (sizzle, jqueryUI) {
// Returns the exported value
return function () {
// ...
}
}
)
module
module name
dependencies
factory function used to construct the module,
receives the dependencies as arguments
exported value
// app.js
// define paths
requirejs.config({
baseUrl: 'js/lib',
paths: {
jquery: 'jquery-1.9.0'
}
})
define(['jquery'], function ($) {
// this is executed only when jquery
// and its deps are loaded
});
app
Require.js config
jquery will be loaded from
://<currentDomain>/js/lib/jquery-1.9.0.js
app main function
Has jquery as dependency
👍 Asynchronous modules
👍 Works on Browsers and Server side
👎 Very verbose and convoluted syntax (my opinion™)
(function (root, factory) {
if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('dep'))
} else if (typeof define === 'function' && define.amd) {
// AMD
define(['dep'], function (dep) {
return (root.returnExportsGlobal = factory(dep))
})
} else {
// Global Variables
root.myModule = factory(root.dep)
}
}(this, function (dep) {
// Your actual module
return {}
}))
IIFE with arguments:
- Current scope (this) and the module factory function.
- "dep" is a sample dependency of the module.
👍 Allows you to define modules that
can be used by almost any module loader
👎 Complex, the wrapper code
is almost impossible to write manually
Cool & broad subject, it would deserve it's own talk
Wanna know more?
🔗 import / export syntax reference
🔗 ECMAScript modules in browsers
// calculator.js
const add = (num1, num2) => num1 + num2
const sub = (num1, num2) => num1 - num2
const div = (num1, num2) => num1 / num2
const mul = (num1, num2) => num1 * num2
export { add, sub, div, mul }
// app.js
import { add } from './calculator'
console.log(add(2,2)) // 4
module
exported functionalities
app
import specific functionality
// index.html
<html>
<body>
<!-- ... -->
<script type="module">
import { add } from 'calculator.js'
console.log(add(2,2)) // 4
</script>
</body>
</html>
"works" in some modern browsers
🙄 Syntactically very similar to CommonJS...
BUT
👍 import & export are static
(allow static analysis of dependencies)
👍 It is a (still work in progress) standard format
👍 Works (almost) seamlessly in browsers & servers
Current most used practice:
Use CommonJS or ES2015 & create "compiled bundles"
const $ = require('zepto')
const tippy = require('tippy.js')
const UUID = require('uuidjs')
const { confetti } = require('dom-confetti/src/main')
const store = require('store2')
const Favico = require('favico.js')
!(function () {
const colors = ['#a864fd', '#29cdff', '#78ff44', '#ff718d', '#fdff6a']
const todoApp = (rootEl, opt = {}) => {
const todos = opt.todos || []
let completedTasks = opt.completedTasks || 0
const onChange = opt.onChange || (() => {})
const list = rootEl.find('.todo-list')
const footer = rootEl.find('.footer')
const todoCount = footer.find('.todo-count')
const insertInput = rootEl.find('.add-todo-box input')
const insertBtn = rootEl.find('.add-todo-box button')
const render = () => {
let tips
list.html('')
if (todos.length === 0) {
list.html('<div class="nothing"><p>Nothing to do</p><p><small>It\'s good to be lazy 🎉</small></p></div>')
} else {
const listContainer = $('<ul></ul>')
todos.forEach(item => {
const el = $(`<li id="${item.id}"><a title="Click to mark as done" href="#">${item.text}</a></li>`)
el.on('click', (e) => {
e.preventDefault()
const elId = $(e.target).parent().attr('id')
if (tips) tips.destroyAll()
remove(elId)
})
el.on('keyup', (e) => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
const elId = $(e.target).parent().attr('id')
if (tips) tips.destroyAll()
remove(elId)
}
})
const link = el.find('a')
link.on('focus', (e) => {
$(e.target).attr('title', 'Press [space] or [enter] to mark as done')
})
link.on('blur', (e) => {
$(e.target).attr('title', 'Click to mark as done')
})
el.appendTo(listContainer)
})
listContainer.appendTo(list)
tips = tippy(list.find('li').toArray(), {
dynamicTitle: true,
placement: 'top',
animation: 'shift-away',
arrow: 'up',
target: 'a',
trigger: 'focus mouseenter',
theme: 'poo'
})
}
let footerText = todos.length ? `<strong>${todos.length}</strong> task${todos.length !== 1 ? 's' : ''} to get done!` : ''
footerText += completedTasks ? ` <strong>${completedTasks}</strong> completed task${completedTasks !== 1 ? 's' : ''}</strong>` : ''
todoCount.html(footerText)
}
const add = (text) => {
const item = { id: UUID.generate(), text }
todos.push(item)
render()
onChange(todos, completedTasks)
return item
}
const remove = (id) => {
const elIndex = todos.findIndex((item) => item.id === id)
const removedItem = todos.splice(elIndex, 1)[0]
completedTasks++
// every 10 todos completed spread some confetti!
if (completedTasks % 10 === 0) {
confetti(rootEl.get(0), {colors, angle: 45})
}
render()
onChange(todos, completedTasks)
return removedItem
}
const getAll = () => todos
const getRootEl = () => rootEl
insertInput.on('keyup', (e) => {
if (insertInput.val() && e.key === 'Enter') {
e.preventDefault()
add(insertInput.val())
insertInput.val('')
}
})
insertBtn.on('click', (e) => {
e.preventDefault()
if (insertInput.val()) {
add(insertInput.val())
insertInput.val('')
}
})
render()
return ({ add, remove, getAll, getRootEl })
}
const returning = store.get('init')
const storedTodos = store.get('todos', [])
const storedCompletedTasks = store.get('completedTasks', 0)
const favicon = new Favico({
animation: 'popFade', position: 'up'
})
favicon.badge(storedTodos.length)
const app = todoApp($('#app'), {
todos: storedTodos,
completedTasks: storedCompletedTasks,
onChange: (todos, completedTasks) => {
favicon.badge(todos.length)
store('todos', todos)
store('completedTasks', completedTasks)
}
})
store.set('init', true)
if (!returning) {
// for the first init
app.add('Buy soy milk')
app.add('Do homeworks')
app.add('Prepare material for the talk')
app.add('Read SPAM emails')
app.add('Write a module bundler')
app.add('Tick some todos')
app.add('Find a better name for this example')
}
})()
The browser doesn't know how to process
It doesn't support CommonJS!
A tool that takes modules with dependencies and emits static assets representing those modules
Those static assets can be processed by browsers!
A graph built by connecting every module with its direct dependencies.
app
dependency A
dependency B
dependency A2
shared
dependency
// app.js
const calculator = require('./calculator')
const log = require('./log')
log(calculator('2 + 2 / 4'))
// log.js
module.exports = console.log
// calculator.js
const parser = require('./parser')
const resolver = require('./resolver')
module.exports = (expr) => resolver(parser(expr))
// parser.js
module.exports = (expr) => { /* ... */ }
// resolver.js
module.exports = (tokens) => { /* ... */ }
app
calculator
log
parser
resolver
(entrypoint)
(1)
(2)
(3)
(4)
(5)
{ }
'./app': (module, require) => { … },
'./calculator': (module, require) => { … },
'./log': (module, require) => { … },
'./parser': (module, require) => { … },
'./resolver': (module, require) => { … },
const parser = require('./parser');const resolver = require('./resolver');module.exports = (expr) => resolver(parser(expr))
require path
module factory function
.js
{ --- : --- --- : ---- --- : -- }
((modulesMap) => {
const require = (name) => {
const module = { exports: {} }
modulesMap[name](module, require)
return module.exports
}
require('./app')
})(
{
'./app': (module, require) => { … },
'./calculator': (module, require) => { … },
'./log': (module, require) => { … },
'./parser': (module, require) => { … },
'./resolver': (module, require) => { … }
}
)
IIFE passing the modules map as argument
Custom require function:
it will load the modules by evaluating the code from the modules map
A reference to a module with an empty module.exports.
This will be filled at evaluation time
Invoking the factory function for the given module name.
(Service locator pattern)
The current reference module is passed, the factory function will modify this object by adding the proper exported values.
The custom require function is passed so, modules can recursively require other modules
The resulting module.exports is returned
The entrypoint module is required, triggering the actual execution of the business logic
If you do, let me know... I'll arrange a prize for you!
TIP: you can use acorn or babel-parser to parse JavaScript files (look for require and module.exports) and resolve to map relative module paths to actual files in the filesystem.
Need an inspiration?
Check the awesome minipack and @adamisnotdead's w_bp_ck!
npm install webpack webpack-cli
webpack app.js
yep, recent versions of Webpack work without config! 😎
webpack --mode=development app.js
Do not compress the code and add annotations!
const { resolve, join } = require('path')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
entry: './app.js',
output: {
path: resolve(join(__dirname, 'build')),
filename: 'app.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
]
},
plugins: [
new CompressionPlugin()
]
}
Webpack.config.js
Entrypoint
Build the dependency graph starting from ./app.js
Output
Save the resulting bundled file in ./build/app.js
Loaders
All the files matching "*.js" are processed with babel and converted to ES5 Javascript
Plugins
Uses a plugin that generates a gzipped copy of every emitted file.
import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
</div>
);
}
}
export default App
This is how Webpack allows you to use Babel, TypeScript, Clojure, Elm, Imba but also to load
{
test: /\.css$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9',
],
flexbox: 'no-2009',
}),
],
},
}
]
}
// Defines how to load .css files (uses a pipeline of loaders)
// parses the file with post-css
// process @import and url()
// statements
// inject the resulting code with a <style> tag
Device
CDN
Origin
Server
example.com
bundle.js
bundle.js
bundle.js
(original)
bundle.js
(cache copy)
bundle.js
(cache copy)
Device
CDN
Origin
Server
example.com
bundle.js
bundle.js
bundle.js
(original)
bundle.js
(cache copy)
bundle.js
(cache copy)
NEW VERSION
STALE!
STALE!
bundle.js?v=1
bundle.js?v=2
bundle.js?v=3
Doesn't play nice with some CDNs and Proxies
(they won't consider different query parameters to be different resources)
bundle-v1.js
bundle-v2.js
bundle-v3.js
...
Better, but still a lot of diligence and manual effort needed...
bundle.9f61f58dd1cc3bb82182.js bundle.aacdf58ef1aa12382199.js bundle.ed61f68defef3bb82221.js
...
output: { filename: '[name].[contenthash].js' }
contenthash + webpack-html-plugin
Every new asset version will generate a new file
cache is automatically cleaned up on every release
(if content actually changed)
html-plugin will update the reference to the new file
npm i -g create-react-app
create-react-app new-project
cd new-project
npm run eject
cat config/webpack*.js
npx twaffle --keywords '#oredev #IWANTTOWINAPRIZE'
Checkout
Special thanks:
Tadeas from Øredev (for reviewing my stuff thoroughly)
@Podgeypoos79, @andreaman87, @mariocasciaro, @eugenserbanescu (reviewers) and @MarijnJH (inspirations: his amazing book and his workshop on JS modules)
Images by Streamline Emoji pack.
By Luciano Mammino
The landscape of module bundlers has evolved significantly since the days you would manually copy-paste your libraries to create a package for your frontend app. Like many parts of the JS world, the evolution has happened somewhat haphazardly, and the pace of change can feel overwhelming. Has Webpack ever felt like magic to you? How well do you understand what’s really going on under the hood? In this talk, I will uncover the history of JS module bundlers and illustrate how they actually work. Once we have the basics down, I will dive deeper into some of the more advanced topics, such as bundle cache boost and resolving cycling dependencies. At the end of this session, you will have a much more profound understanding of what’s going on behind the scenes.
Cloud developer, entrepreneur, fighter, butterfly maker! #nodejs #javascript - Author of https://www.nodejsdesignpatterns.com , Founder of https://fullstackbulletin.com