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 16

Firefox 60

ES6 Modules in the browser

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

Safari 10.1

Chrome 63

Safari Technology Preview 24

im dyna

Inconvénients

import dynamique

Safari 10.1

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 ( v8.5.0 )

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 ?

  • 5,957