Modules ES6 & HTTP/2

Va-t-on arrêter de packager notre code JavaScript ?

Marc - 32 ans

Lead développeur - Frontend - Acme corporation

ES6

Vincent Ogloblinsky

Architecte logiciel

" Technologies frontend "

@vogloblinsky

Les modules

en JavaScript

Pas nouveau

 

Apports :

 

- séparation

- gestion de dépendances

- gestion du namespace

- implémentation commune

Existe sous différentes approches depuis longtemps :

 

- object literal pattern

- IIFE / revealing module pattern

- CommonJS

- AMD

- UMD

- ES6 modules (ESM ou Native modules)

Object literal pattern

Tire partie de la structure native objet

 

Avantage : simple à comprendre et à implémenter

 

Inconvénient : repose sur une variable globale

//person.js
var person = {
    bipedal: true,
    species: 'homo sapiens'
}

IIFE / Revealing module pattern

Tire partie du pattern façade

 

Avantage :

tire partie de la closure, contrôle plus fin. découplage plus facile

 

Inconvénient :

syntaxe pas élégante, repose encore un peu sur le scope global

//person.js
var person = (function(name) {
    var innerName = (!name) ? 'Anonymous' : name;
    
    return {
        name: function() {
            return innerName;
        },
        bipedal: true,
        species: 'homo sapiens'
    }
})(outerName);

Vient du monde Node.JS

 

Requiert un loader compatible

 

Avantage :

- syntaxe claire et concise, a pas mal influencé celle en ES6

- modules limitent la portée, plus de déclaration globale

 

Inconvénient :

- pas très adapté aux environnements asynchrones

- lazy loading également

module.exports = function() {
    return {
        name: innerName,
        bipedal: true,
        species: 'homo sapiens'
    }
};

Asynchronous
Module
Definition

A émergé pendant les discussions sur CommonJS

 

Requiert un loader (Require.js)

 

Avantage :

- Leader pendant pas mal de temps

 

Inconvénient :

- syntaxe très verbeuse

- caractère synchrone difficile à analyser statiquement

define('person', function(name) {
    var innerName = (!name) ? 'Anonymous' : name;
    return {
        name: function() { return innerName; },
        bipedal: true,
        species: 'homo sapiens'
    }
});

UMD

Universal Module Definition

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    //    methods
    function myFunc(){};

    //    exposed public method
    return myFunc;
}));

Combinaison d'AMD et CommonJS

 

Avantage :

- pas vraiment

 

Inconvénient :

- inconvénients des sous-formats

ES6

MODULES

Le TC39 a appris de ses erreurs

 

Fournit quelque chose de simple et adapté

 

Manque de support dans les browser à la sortie d'ES6

→ recours à Babel ou Traceur

var person = {
    bipedal: true,
    species: 'homo sapiens'
};

export default { person };

import person from './person.js';

Recap

- object literal pattern

- IIFE / revealing module pattern

- CommonJS

- AMD

- UMD

- ES6 modules

Les modules ES6

Chrome 61

Safari 10.1

Edge 15

Firefox 54

On sait clairement qui supporte quoi

nomodule pour les "anciens"

polyfill pour iOS 10.3 et 10.1

http://tinyurl.com/safari-nomodule​

<!-- JS pour navigateurs "modernes" -->
<script type="module" src="main.js"></script>

<!-- JS transpilé en ES5 pour navigateurs "anciens" -->
<script nomodule src="main-legacy.js"></script>

Avantages

syntaxe compacte

structure statique (facilite le tree-shaking, optimisation, analyse statique, ...)

support automatique des dépendances cycliques

fin de la fragmentation des "standards" de modules

fin des APIs dans navigator ?

im dyna

Inconvénients

import dynamique

draft en cours - stage 3

im dyna

Inconvénients

import dynamique

Inconvénients

non supporté dans les web workers (actuellement)

// worker.js
importScripts('subworker.js');

//subworker.js
export const sqrt = Math.sqrt;

// Uncaught SyntaxError: Unexpected token export


//lib.js
export const sqrt = Math.sqrt;

//subworker.js
import sqrt from './lib.js';

// Uncaught SyntaxError: Unexpected token export

conflit avec le format CommonJS

Et dans Node.js

nouvelle extension de fichier : *.mjs

support début 2018

migration douce vers ESM car commune avec le browser

Différences ESM / CommonJS

require import
évaluation dynamique évaluation statique
lève des erreurs au run ​lève des erreurs au parsing

<script> vs "module"

<script> pour le code JS classique avec namespace global

module : pour du code modulaire avec imports/exports explicites

les 2 mixés ne marchent pas bien entendu

<script> vs "module"

scripts modules
Element HTML <script> <script type="module">
Mode par défaut non-strict strict
Variable de haut niveau globale locale au module
Valeur de this window undefined
Exécution synchrone asynchrone
Imports déclaratifs non oui
Extension fichier .js .js

<script> vs "module"

<script> bloquants par défaut

modules defer par défaut

<script> vs "module"

Attributs communs

 

type : "module" ou "text/javascript"

src

defer (par défault pour les modules)

async

crossorigin

Nuances de syntaxe

chemin dans les imports

// Supporté
import {foo} from 'https://www.website.com/utils/bar.js';
import {foo} from '/utils/bar.js';
import {foo} from './bar.js';
import {foo} from '../bar.js';

// Non supporté
import {foo} from 'bar.js';
import {foo} from 'utils/bar.js';

Nuances de syntaxe

module inline aussi defer

<!-- This script will execute after… -->
<script type="module">
  addTextToBody("Inline module executed");
</script>

<!-- …this script… -->
<script src="1.js"></script>

<!-- …and this script… -->
<script defer>
  addTextToBody("Inline script executed");
</script>

<!-- …but before this script. -->
<script defer src="2.js"></script>

Nuances de syntaxe

async aussi pour les modules inline

<!-- This executes as soon as its imports have fetched -->
<script async type="module">
  import {addTextToBody} from './utils.js';

  addTextToBody('Inline module executed.');
</script>

<!-- This executes as soon as it & its imports have fetched -->
<script async type="module" src="1.js"></script>

Nuances de syntaxe

exécution unique

<!-- 1.js only executes once -->
<script type="module" src="1.js"></script>
<script type="module" src="1.js"></script>
<script type="module">
  import "./1.js";
</script>

<!-- Whereas normal scripts execute multiple times -->
<script src="2.js"></script>
<script src="2.js"></script>

Nuances de syntaxe

toujours récupéré en mode CORS

<!-- This will not execute, as it fails a CORS check -->
<script type="module" src="https://….now.sh/no-cors"></script>

<!-- This will not execute, as one of its imports fails a CORS check -->
<script type="module">
  import 'https://….now.sh/no-cors';

  addTextToBody("This will not execute.");
</script>

<!-- This will execute as it passes CORS checks -->
<script type="module" src="https://….now.sh/cors"></script>

Nuances de syntaxe

mode strict par défaut

// Plus de 'use strict'

mistypeVariable = 17; // Uncaught ReferenceError: mistypeVariable is not defined

delete Object.prototype; // Uncaught TypeError: Cannot delete property 'prototype'

function sum(a, a, c) { // Uncaught SyntaxError: Duplicate parameter name not allowed
    return a + b + c;
}

Nuances de syntaxe

imports hoisted

alert(`main-bundled.js - counter: ${counter}`);

import {counter} from './increment.js';

comme les fonction en JS

 

exécuté dans l'ordre du graphe de dépendances

Nuances de syntaxe

import / export de haut niveau

if (Math.random()>0.5) {
      import './module1.js'; // SyntaxError: Unexpected token 'import'
}

const import2 = (import './main2.js'); // SyntaxError: Unexpected token 'import'

try {
   import './module3.js'; // SyntaxError: Unexpected token 'import'
} catch(err) {
   console.error(err);
}

const moduleNumber = 4;

import module4 from `module${moduleNumber}`; // SyntaxError: Unexpected token

Nuances de syntaxe

imports en lecture seule d'un export

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3

incCounter();

console.log(counter); // 4

counter += 1; // Uncaught TypeError: Assignment to constant variable.

im dyna

Nuances de syntaxe

this === undefined

// module.js

console.log(this); // undefined

// script.js

console.log(this); // window

Nuances de syntaxe

Dépendances circulaires autorisées

// vehicle.js
import { Car } from './car.js';

let id = 0;

export class Vehicle {
    constructor() {
        this.id = ++id;
    }

    static build() {
        return new Car();
    }
}


// car.js
import { Vehicle } from './vehicle.js';

export class Car extends Vehicle {
    constructor() {
        super();
        this.model = 'Generic';
    }
}
// main.js
import { Car } from './car.js';

let myCar = new Car()

console.log(myCar);

// Car {id: 1, model: "Generic"}

Utilitaire

Détecter si le script courant est exécuté comme un module

const isAModule = (this === undefined);

const isNotAModule = (this === window);

// WARNING , this peut aussi bien être bindé

bref pas de moyen clair

Utilitaire

Détecter si le type="module" est supporté

function checkJsModulesSupport() {
  // create an empty ES module
  const scriptAsBlob = new Blob([''], {
    type: 'application/javascript'
  });
  const srcObjectURL = URL.createObjectURL(scriptAsBlob);

  // insert the ES module and listen events on it
  const script = document.createElement('script');
  script.type = 'module';
  document.head.appendChild(script);

  // return the loading script Promise
  return new Promise((resolve, reject) => {
    // HELPERS
    let isFulfilled = false;

    function triggerResolve() {
      if (isFulfilled) return;
      isFulfilled = true;
      
      resolve();
      onFulfill();
    }

    function triggerReject() {
      if (isFulfilled) return;
      isFulfilled = true;

      reject();
      onFulfill();
    }

    function onFulfill() {
      // cleaning
      URL.revokeObjectURL(srcObjectURL);
      script.parentNode.removeChild(script)
    }

    // EVENTS
    script.onload = triggerResolve;
    script.onerror = triggerReject;
    setTimeout(triggerReject, 100); // reject on timeout

    // start loading the script
    script.src = srcObjectURL;
  });
};

checkJsModulesSupport().then(
  () => {
    console.log('ES modules ARE supported');
  },
  () => {
    console.log('ES modules are NOT supported');
  }
);

Intégration de code externe

Code externe

support.js

main.js

module1

module2

support-es6.js

Code externe - CommonJS

// support.js
var leftpad = require('left-pad');

module.exports = leftpad;

rollup

+ plugins node et commonjs

// support-cjs.js
var leftpad = function (a, b, c) { ... };

module.exports = leftpad;

cjs-to-es6

// support-cjs-to-es6.js
var leftpad = function (a, b, c) { ... };

export default leftpad;
// main.js
import leftpad from './support-cjs.js';

console.log(leftpad('foo', 5));

//     foo

Code externe ES6

// AVANT

import * as lodash from 'lodash';

// MAINTENANT

import add from '../node_modules/lodash-es/add.js';

Support aisé du tree-shaking

rollup-node-resolve par exemple injectera le code dans le package full ES6

Webpack & co ont-ils encore un intérêt ?

Etude

Webpagetest.org

Moto G4 - Chrome beta - Emerging market 3G

Livrer 3 versions différentes sur 3 urls différentes

Code ES5

Code ES6 non bundlé

Code ES6 bundlé

Version Poids
(non-minifié)
Parse/eval time
(ms)
Load time
(ms)
ES2015 - unbunded 23 60 4.42
ES2015 - bundled 23 65 3.14
ES5 240 190 3.06

Webpagetest.org

Moto G4 - Chrome beta - Emerging market 3G

HTTP/2 à la rescousse

Features

multiplexing

push

- compression des entêtes

- etc...

Et pour nous les "fronteux"

Légende urbaine :

 

" livrer des petits fichiers plutôt qu'un gros "

Problèmes de performances sur la compression des entêtes

Ne pas dépasser un certain nombre de bundles pour tirer partie du multiplexing

Un peu de push ?

Push

Le serveur a besoin de savoir quoi pushé ?

 

N'est pas la recette magique : pas très cache friendly

Service worker

Script tournant en tâche de fond

Middleware entre le navigateur et l'extérieur

Caractéristiques : synchronisation, push notification et aussi du CACHE

prpl pattern

Service worker

Recap

La combinaison

Push + service worker est une solution mais :

- requiert du setup serveur et client

- n'est pas encore cross-browsers

Build : au final on fait quoi ?

Avec Webpack ou Rollup.js

<script nomodule src="main.legacy.js"></script>

Concaténer vos modules ES6 dans un seul fichier transpilé en ES5

avec Babel + babel-preset-env

<script type="module" src="main.js"></script>

Avec Webpack ou Rollup.js

Réaliser la même concaténation sans transpilage

Vision à moyen terme

Communauté web et open-source très dynamique

Support full ES6 des browsers + outils de debugs

En mode développement, cela peut aider

Plus de polyfills, moins de transpilage

→code plus léger

“ Ecrire du code ES2015 est un gain pour les développeurs, et livrer en prod du code ES2015 est aussi un gain pour les utilisateurs ”

ES6

Sam Thorogood - @samthor

Ressources

ES6 modules

Native ECMAScript modules - the first overview

https://hospodarets.com/native-ecmascript-modules-the-first-overview

 

Native ECMAScript modules: the new features and differences from Webpack modules

https://hospodarets.com/native-ecmascript-modules-new-features

 

ES6 modules support lands in browsers: is it time to rethink bundling ?

https://www.contentful.com/blog/2017/04/04/es6-modules-support-lands-in-browsers-is-it-time-to-rethink-bundling/

Ressources

ES6 modules

Ressources

ES6 modules

Ressources

ES6 modules

HTTP/2 push is tougher than I thought

https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/

 

Performance Best Practices in the HTTP/2 Era

https://deliciousbrains.com/performance-best-practices-http2/

 

HTTP/2 Server Push and Service Workers : The Perfect Partnership

https://24ways.org/2016/http2-server-push-and-service-workers/

Ressources

HTTP/2

Merci

pour votre écoute, une question ?

bit.ly/es6-modules-in-browser

Modules ES6 & HTTP/2 - Va-t-on arrêter de packager notre code JavaScript ?

By Vincent Ogloblinsky

Modules ES6 & HTTP/2 - Va-t-on arrêter de packager notre code JavaScript ?

  • 358
Loading comments...

More from Vincent Ogloblinsky