Créer un navigateur personnalisé - 6ème partie

Encapsulation des composants avec Riot

19 février 2016

Introduction

Ce projet est open source et accessible depuis GitHub GL-Browser.

Cet article est le 6ème d'une série d'articles concernant la création d'un navigateur internet incluant par défaut des fonctionnalités que l'on retrouve habituellement sous forme d'extensions dans Chrome ou Firefox.

Exemples de fonctionnalités :

Nous souhaitons :

Nous utilisons Electron qui mixe le navigateur open source Chromium avec la richesse du framework Node.js.

Objectifs

Nous souhaitons améliorer l'architecture du projet pour mieux assurer sa pérénnité.

Dans ce 6ème article, nous allons donc encapsuler les composants visuels à l'aide de Riot.

Encapsulation

Résumé des articles précédents

Article 1 : initialisation du projet avec Electron.
Article 2 : injection du CSS et du JavaScript dans la page de Google.
Article 3 : injection de scripts JavaScript et de styles CSS suivant l'URL.
Article 4 : bloquer les requêtes de suivi/tracking des utilisateurs.
Article 5 : amélioration de l'interface utilisateur avec Bootstrap.

Pour récupérer directement les sources du 5ème article :

git clone https://github.com/emmanuelroecker/GL-Browser
git checkout article5

Riot

Riot permet de structurer l'interface utilisateur d'une application Web monopage sous forme de composants réutilisables.

Chaque élément visuel encapsulé dans un composant/tag possède des états.
À chaque changement d'état, la portion HTML correspondant à l'élément est régénérée automatiquement.

Riot ressemble à React mais sa syntaxe est plus lisible et sa taille est plus petite.

Installer

Ajouter au fichier package.json la nouvelle dépendance Riot :

{
    "devDependencies": {
        "electron-prebuilt": "^0.36.0",
        "js-yaml": "^3.5.0",
        "match-pattern": "^0.0.1",
        "bootstrap": "^3.3.0",
        "jquery": "^1.9.1",
        "riot":"^2.3.0"
    }
}

Pour installer :

npm install

Compilation à la volée

Les tags/composants Riot doivent être compilés en JavaScript avant qu'Electron puisse les exécuter.

Ajouter à la fin du fichier index.html :

<!-- utiliser Riot et son compilateur -->
<script src="./node_modules/riot/riot+compiler.min.js"></script>

<!-- monter/compiler tous les composants -->
<script>
riot.mount('*');
</script>

Les encapsulations

Jusqu'à présent, le code ressemblait à un plat de spaghettis :

Encapsuler le contenu d'un onglet dans un composant page

Le contenu d'un onglet est encapsulé dans un composant Riot indépendant et détachable appelé page.riot.tag, il devient plus facilement maintenable.

page.riot.tag contient le JavaScript, le CSS et le code HTML du composant :

<page>
  <!-- code HTML précédemment dans le fichier page.html -->
  <div class="gl-header input-group">
    <div class="input-group-btn">
      <a class="gl-goback btn btn-default" role="button"><span class="glyphicon glyphicon-arrow-left"></span></a>
    </div>
    <input class="gl-urltext form-control" type="text" placeholder="URL">
    <span class="gl-indicator input-group-addon"></span>
    <div class="input-group-btn">
      <a class="gl-refresh btn btn-default" role="button"><span class="glyphicon glyphicon-repeat"></span></a>
      <a class="gl-dev btn btn-default" role="button"><span class="glyphicon glyphicon-wrench"></span></a>
    </div>
  </div>
  <webview class="gl-webview">
  </webview>

  <!-- le style css ne sera appliqué qu'à ce composant -->
  <style scoped>
    .gl-indicator {
        top: 0px;
        width: 40px;
      }

    .gl-webview {
      display: block;
      border: none;
    }
  </style>

  <!-- code javascript associé au composant -->
  <script>
    'use strict'; //autoriser l'utilisation de ES6
    this.on('mount', function() { //appelé au moment du montage du composant
      let $node = $(this.root); //utiliser ce composant comme racine de jQuery
      let webview = $node.find('.gl-webview');
      let indicator = $node.find('.gl-indicator');

      $node.find('.gl-refresh').click(function () {
        webview.get(0).reload();
      });

      $node.find('.gl-dev').click(function () {
        webview.get(0).openDevTools();
      });

      $node.find('.gl-goback').click(function () {
       webview.get(0).goBack();
      });

      $node.find('.gl-urltext').keypress(function (e) {
        if (e.keyCode !== 13) {
          return true;
        }
        webview.get(0).src = this.value;
        return false;
      });

      webview.on('did-start-loading', () => {
        indicator.toggleClass('glyphicon glyphicon-refresh');
      });
      webview.on('did-stop-loading', () => {
        indicator.toggleClass('glyphicon glyphicon-refresh');
      });
      webview.on('load-commit', function (e) {
        let url = e.originalEvent.url;
        webview.on('did-finish-load', function () {
          let inject = getToInject(url);
          if (inject) {
            webview.get(0).insertCSS(inject.css);
            webview.get(0).executeJavaScript(inject.js);
          }
          $(this).off('did-finish-load');
        });
      });
    });
  </script>
</page>

Ajouter ce nouveau composant page à index.html :

<script src="components/page.riot.tag" type="riot/tag"></script>

Encapsuler la gestion des onglets dans un composant tabs

De la même manière que pour le composant page ci-dessus, encapsuler la gestion des onglets dans un composant Riot appelé tabs.riot.tag.

Ce composant permet d'ajouter ou de supprimer des onglets.
Chaque onglet contient un composant page.

<tabs>
  <ul class="nav nav-tabs">
    <!-- parcourir tous les éléments -->
    <li each={ item in items }>
      <!-- utiliser item.id comme identifiant -->
      <a href="#{item.id}" data-toggle="tab">
        {item.id}
      </a>
      <!-- appeler la méthode remove du parent lors du clic sur le boutton de suppression de l'onglet -->
      <span class="close" onclick={parent.remove}>
        &times;
      </span>
    </li>
    <li>
      <!-- appeler la méthode add lors du clic sur le boutton d'ajout d'un nouvel onglet -->
      <a role="button" class="add-url" data-toggle="tab" onclick={add}>
        +
      </a>
    </li>
  </ul>
  <div class="tab-content">
    <!-- parcourir tous les éléments -->
    <div each={item in items} class="tab-pane fade" id={item.id}>
      <!-- utiliser le composant page -->
      <page name={item.id}></page>
    </div>
  </div>

 <!-- le style css ne sera appliqué qu'à ce composant -->
 <style scoped>
  .nav-tabs > li {
      position:relative;
   }

   .nav-tabs > li > a {
      display:inline-block;
   }

   .nav-tabs > li > span {
     display:none;
     cursor:pointer;
     position:absolute;
     right: 6px;
     top: 11px;
   }

   .nav-tabs > li:hover > span {
     display: inline-block;
   }

   .tab-content > .tab-pane {
     display: block;
     height: 0;
     overflow-y: hidden;
   }

   .tab-content > .active {
     height: auto;
   }
 </style>

 <!-- code javascript associé au composant -->
 <script>
    'use strict';     //autoriser l'utilisation de ES6
    this.items = [];  //tableau des onglets
    this.incid = 0;   //incrémenter les ids

    //méthode appelée lors de l'ajout d'un nouvel onglet
    add(e) {
      this.currentid = 'url' + this.incid;
      this.items.push({id:this.currentid});
      this.incid++;
    }

    //initialiser le composant avec un premier onglet
    this.add();

    //méthode appelée lors de la suppression d'un onglet
    remove (e) {
      if (this.items.length <= 1)
        return;

      //supprimer l'onglet du tableau
      let item = e.item.item;
      let index = this.items.indexOf(item);
      this.items.splice(index,1);

      //activer l'onglet précédent
      index--;
      if (index < 0)
        index = 0;
      this.currentid = this.items[index].id;
    }

    //méthode appelée une fois le composant mis à jour
    this.on('updated', function() {
      let $node = $(this.root); //utiliser ce composant comme racine de jQuery

      //redimensionner le webview à chaque affichage d'un onglet
      $node.find('a[data-toggle="tab"]').on('shown.bs.tab', function () {
        glRefreshWebComponentSize();
      });

      //afficher l'onglet courant
      $node.find(`a[href="#${this.currentid}"]`).tab('show');
    });
 </script>
</tabs>

Ajouter ce nouveau composant tabs à index.html :

<script src="components/tabs.riot.tag" type="riot/tag"></script>

Utiliser le composant tabs

Le corps du fichier index.html se réduit sensiblement :

<body>
  <div>
    <!-- utiliser le composant tabs -->
    <tabs>
    </tabs>
  </div>
</body>

Résultat

En comparant le code source de l'article 5 avec celui-ci, l'architecture devient plus claire :

Avant - Article 5Après - Article 6
index.htmlindex.html
glbrowser.cssglbrowser.css
glbrowser.jsglbrowser.js
inclus dans glbrowser.js et glbrowser.csstabs.riot.tag
inclus dans glbrowser.js et glbrowser.csspage.riot.tag

Envoyer sur GitHub

git add -A
git commit -m "article 6"
git push

Prochain article

Le prochain article traitera de l'intégration d'un gestionnaire de mots de passe pour se connecter de manière transparente à ses comptes.