Le JavaScript invité

Techniques pour le frontend distribué

Presenter Notes

Le but du jeu

Inclure facilement du contenu venant de notre serveur en copiant un fragment de code.

Presenter Notes

Cas d'utilisation réels

  • ajouter un formulaire de réservation à des sites d'hotels
  • inclure des rapports financiers issus d'un système de gestion
  • ajouter des infos en rapport avec un lieu
  • inclure des critiques issues d'un système de revue
  • inclure des cartes électorales sur des sites de presse

Presenter Notes

Stratégie d'inclusion

  • iframe
  • injection HTML dans la page
  • ...ou une combinaison des deux !

Presenter Notes

iframe

<!doctype html><meta charset="utf-8">
<style>
  body { font-family: "Comic Sans MS"; color: purple; }
  iframe { width: 100%; }
</style>
<p>Contenu de la page hôte</p>
<iframe src="iframe_content.html"></iframe>
<p>Suite du contenu de la page hôte</p>

Presenter Notes

iframe

Problème : la hauteur de l'iframe ne s'adapte pas au contenu.

Presenter Notes

iframe

Problème : le contenu ne peut pas sortir du cadre.

Pose problème pour :

  • fenêtres popup
  • tooltips

Presenter Notes

iframe avec Pym.js

Dans la page hôte :

<p>Contenu de la page hôte</p>
<div id="iframe-wrapper"></div>
<p>Suite du contenu de la page hôte</p>
<script src="pym.js"></script>
<script>
    new pym.Parent('iframe-wrapper', 'iframe_content', {});
</script>

Dans la page invitée :

<script src="pym.js"></script>
<script>
    new pym.Child();
</script>

Presenter Notes

iframe avec Pym.js

Le problème de hauteur de l'iframe est résolu.

Presenter Notes

iframe avec Pym.js

Mais on ne peut toujours pas sortir de l'iframe.

Presenter Notes

iframe

Avantage

  • isolation parfaite

Inconvénients

  • isolation parfaite
  • problèmes d'adaptation à la taille contenu, voir Pym.js (merci @mab_)
  • enfermé dans un cadre, problème par ex. avec tooltips
  • performance

Presenter Notes

Injection HTML

Avantage

  • s'intègre dans la page
  • performance

Inconvénients

  • risques d'interactions indésirables avec la page (JavaScript et CSS)

Presenter Notes

Injection HTML

Script de chargement

  • 2 éléments
  • 1 élément
  • SDK

Chargement de données

  • Same Origin Policy
  • JSONP
  • CORS

Presenter Notes

Chargement en deux éléments

Page hôte :

<div class="toulousejs-widget-container"></div>
<!-- ... bla bla ... -->
<script src="http://example.com/widget.js"></script>

Notre script widget.js :

(function() {
    var container = document.getElementById("toulousejs-widget-container");
    container.innerHTML = "Le contenu provenant de notre serveur";
})();

Avantages :

  • permet de placer l'élément <script> en fin de page
  • code d'injection simple

Inconvénients :

  • pas forcément évident pour tous les auteurs de sites

Presenter Notes

Chargement en un seul élément

Page hôte :

<p>Contenu de la page hôte</p>
<script async src="http://example.com/widget.js"
        data-toulouse-js-id="42">
</script>
<p>Suite du contenu de la page hôte</p>

Notre script :

(function() {
  var script = document.querySelector("script[data-toulouse-js-id]");
  var content = 
    "Contenu issu de notre script. " +
    "Paramètre : " + script.getAttribute("data-toulouse-js-id");
  script.insertAdjacentHTML("beforebegin", content);
})();

Presenter Notes

SDK

<script>
    window.ToulouseJSWidget_ready = function() {
      ToulouseJSWidget.init({
        param1: "foo", param2: "bar"
      });
      ToulouseJSWidget.doSomething();
    };
    (function() {
      var script = document.createElement('script');
      script.async = true;
      script.src = 'http://example.com/widget.js';
      var entry = document.getElementsByTagName('script')[0];
      entry.parentNode.insertBefore(script, entry);
    })();
</script>

Note : la création dynamique d'élément <script> permet le chargement asynchrone avec les vieux navigateurs

Presenter Notes

Chargement de scripts

function loadScript(url, callback) {
  if(typeof callback === "undefined") { callback = function() {}; }
  var scriptTag = document.createElement('script');
  scriptTag.setAttribute("type", "text/javascript");
  scriptTag.setAttribute("src", url);
  if (scriptTag.readyState) {
    // Pour les vieux IE
    scriptTag.onreadystatechange = function () {
      if (this.readyState === 'complete' || this.readyState === 'loaded') {
        callback();
      }
    };
  } else {
    scriptTag.onload = callback;
  }
  document.documentElement.appendChild(scriptTag);
}

Presenter Notes

Example : chargement de jQuery

loadScript("http://example.com/jquery.js", function() {
    var $;
    // On rétabli window.$ et window.jQuery tels qu'ils étaient
    // auparavant et on garde des références à notre propre version
    window.$ToulouseJSWidget_jQuery = $ = window.jQuery.noConflict(true);
    continueWithTheRestOfTheCode($);
});

Presenter Notes

Précautions de codage JavaScript

  • réduire au maximum les variables globales
  • nommer les variables globales de manière à réduire les conflits de nommage, eg. window.nomDeMonOrganisationNomDuProjetFoo.
  • utiliser un linter (ex: JSHint) et "use strict"; pour ne pas introduire de variables globales par erreur (vérification de la présence du mot clé var)

Presenter Notes

Communication avec le serveur

La Same Origin Policy

<div id="container"></div>
<script src="jquery-1.11.2.js"></script>
<script>
  $("#container").load("http://localhost:8080/content.txt");
</script>

Firefox :

Chrome :

Presenter Notes

JSONP

Le chargement de script n'est pas soumis à la Same Origin Policy.

Le client définit une fonction :

function foo(data) {
  // faire quelque chose avec data
}

et charge un script depuis le serveur tiers :

<script src="http://example.com/script.js"></script>

Le script fourni par le serveur tiers appelle la fonction définie par le client :

foo({toto: 42, baz: "quux"});

Presenter Notes

JSONP

Avantages

  • supporte les vieux navigateurs
  • simple
  • supporté par jQuery

Inconvénients

  • bidouille
  • lecture seule
  • gestion d'erreur (voir jquery-jsonp )

Step by step tutorial

How to build a web widget (using jQuery)

Presenter Notes

CORS

Serveur

var fs = require('fs'), http = require('http');

http.createServer(function (req, res) {
  fs.readFile(__dirname + req.url, function (err,data) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.writeHead(200);
    res.end(data);
  });
}).listen(8080);

Client

<div id="container"></div>
<script src="jquery-1.11.2.js"></script>
<script>
  $("#container").load("http://localhost:8080/content.txt");
</script>

Résultat

Presenter Notes

CORS

Avantages

Inconvénients

  • pas supporté par les vieux navigateurs
  • pas forcément trivial à mettre en place

Presenter Notes

CORS

Presenter Notes

Chargement de CSS

function loadCss(url) {
  var link = $("<link>", {rel: "stylesheet", href: url});
  link.appendTo($(document));  
}

Inconvénients

  • risque de FOUC (Flash Of Unstyled Content)
  • pas de manière fiable de détecter quand la CSS a fini d'être chargée et appliquée.

Contournement possibles

  • appliquer un style "rare" (une couleur) à un élément et faire du polling sur le document pour détecter quand ce style a été appliqué
  • inclure les styles dans un élément <style> qui précède le HTML

Presenter Notes

Précautions de codage CSS

  • faire des règles spécifiques
  • préfixer ses classes
  • attention si on utilise des z-index

Presenter Notes

Import de CSS tiers

Exemple avec Bootstrap

.toulousejs-widget-container {
  // Import de bootstrap.css avec prefix
  @import (less) "src/lib/bootstrap/bootstrap.css";

  // Idem pour select2
  @import (less) "src/lib/select2/select2.css";
  @import (less) "src/lib/select2-bootstrap.css";
  @import "src/lib/bootstrap/variables.less";

  // Reste de nos styles
  .foo-bar {
    text-align: left;
  }
  // ...
}

Plus d'info :

Presenter Notes

Merci

Les slides : http://alexmarandon.com/slides/javascript-invite/

Des questions ?

Presenter Notes