Les frameworks Javascript évanescents : Stencil

Le développement côté client est bien plus agité que celui côté serveur, au point de parfois s’y perdre si on ne procède pas à une veille vigilante. J’ai évoqué plusieurs fois les web components dans mon blog généraliste, mais aussi dans celui-ci lorsque j’ai présenté les API. Aujourd’hui il est devenu tout à fait réaliste de se passer de framework Javascript malgré l’intérêt grandissant suscité par React et Vue.

A mi-chemin entre l’utilisation brute des web components et les frameworks Javascripts évolués émerge un autre mouvement très intéressant que l’on nomme disappearing frameworks ou même invisibles frameworks. L’idée générale est de pouvoir écrire du code dans le genre de celui que l’on utilise avec Vue ou React mais au lieu d’avoir un traitement dans le navigateur à l’exécution (run-time) on a une compilation (compile-time) pour obtenir des web components standards. c’est pour cette raison qu’on dit que ces frameworks disparaissent parce qu’après la compilation on ne sait même plus qu’on les a utilisés. Un autre avantage à générer des composants standards c’est qu’ils peuvent s’intégrer facilement dans les frameworks comme Vue et React.

Les deux stars de cette approche sont Svelte et Stencil. Même si le premier connait plus de succès j’ai une petite préférence pour le second même si je dois avouer que la version 3 de Svelte est vraiment séduisante. Il a été créé par l’équipe d’Ionic. Dans cet article je vais présenter ce framework fantôme.

On se lance !

Pour utiliser Stencil il faut un Node.js récent accompagné de npm :

npm init stencil

On se retrouve à devoir choisir :

? Pick a starter » - Use arrow-keys. Return to submit.

>  ionic-pwa     Everything you need to build fast, production ready PWAs
   app           Minimal starter for building a Stencil app or website
   component     Collection of web components that can be used anywhere

On va partir avec le starter (deuxième choix) :

√ Pick a starter » app
√ Project name » essai

✔ All setup in 11 ms

  > npm start
    Starts the development server.

  > npm run build
    Builds your components/app in production mode.

  > npm test
    Starts the test runner.


  We suggest that you begin by typing:

   > cd essai
   > npm start

  Further reading:

   - https://github.com/ionic-team/stencil-app-starter

  Happy coding!

On se retrouve avec cette architecture :

On lance Stencil :

cd essai
npm start

Et une page s’ouvre en localhost:3333 :

Un composant

On va maintenant créer un composant. La création de composants avec stencil se fonde sur deux technologies : TypeScript et JSX. TypeScript est une couche supplémentaire de Javascript qui a de plus en plus de succès parce qu’il apporte plus de rigueur à Javascript. JSX est aussi une extension de Javascript qui a été initiée avec React. Avec JSX on peut écrire ce genre de chose :

const element = <h1>Coucou !</h1>

C’est un peu un mélange des genres !

Pour créer un composant Stencil on crée un fichier avec le suffixe .tsx dans le dossier src/components :

Avec ce code :

import { Component, h } from '@stencil/core';

@Component({
  tag: 'mon-composant',
})

export class MonComposant {

  render() {
    return (
      <p>Mon premier composant !</p>
    );
  }
}

On commence par importer du code de Stencil.

Ensuite on a le décorateur @Component qui permet de préciser des informations pour le composant, ici son tag (le nom utilisé comme balise dans le HTML).

Ensuite on déclare la classe MonComposant. La fonction render est celle chargée de générer le code HTML, donc l’apparence du composant avec une syntaxe JSK.

Pour tester notre composant on pourrait le générer avec le script prévu pour ça :

npm run generate

Mais comme on utilise le starter on va mettre notre composant à la place de celui prévu par défaut dans le fichier index.html. A la base on a ça :

<body>
  <app-root></app-root>
</body>

On le remplace par :

<body>
  <mon-composant></mon-composant>
</body>

La compilation se fait automatiquement parce qu’on a utilisé au départ la commande npm run start. Avec cette commande on est en mode développement, on recompile à chaque changement, et on lance le serveur, il suffit de regarder dans package.json pour vérifier ça :

"scripts": {
  ...
  "start": "stencil build --dev --watch --serve",

Si tout s’est bien passé on se retrouve avec notre texte sur la page :

Mon premier composant !

Pour le moment ce n’est pas très glorieux alors on va un peu approfondir tout ça…

Les propriétés

On a toujours besoin de transmettre des informations à un composant, c’est là qu’interviennent les propriétés. On va enrichir notre composant :

import { Component, Prop, h } from '@stencil/core';

@Component({
  tag: 'mon-composant',
})

export class MonComposant {

  @Prop() age: number;

  render() {
    return (
      <p>Mon âge est {this.age} ans.</p>
    );
  }
}

On utilise le décorateur @Prop pour définir une propriété age de type number. On peut ensuite accéder à la valeur de cette propriété avec l’opérateur this.

Quand on déclare le composant on peut ainsi transmettre une valeur à la propriété :

<mon-composant age="20"></mon-composant>
Mon âge est 20 ans.

Par défaut les propriétés ne sont pas modifiables par le composant, ce qui est logique, mais on peut modifier ce comportement parce qu’on peut préciser des options pour les propriétés et par exemple prévoir la mutabilité :

@Prop({ mutable: true }) age: number;

On peut aussi prévoir une valeur par défaut :

@Prop() age: number = 20;

Les événements

Un composant peut écouter et émettre des événements, on a donc deux décorateurs : @Listen et @Event. Logiquement on écoute les événements émis par les composants enfants et on émet des événements aux composants parents.

Prenons un exemple d’émission d’événement :

import { Component, Event, EventEmitter, h } from '@stencil/core';

@Component({
  tag: 'mon-composant',
})

export class MonComposant {

  @Event() active: EventEmitter;

  activeClick(e) {
    this.active.emit(true);
  }

  render() {
    return (
      <button onClick={(e) => this.activeClick(e)}>Mon bouton</button>
    );
  }
}

On a un bouton sur lequel on écoute l’événement click, et lorsqu’on clique on émet un événement nommé active avec un paramètre transmis.

Dans le composant parent on utilise le décorateur @Listen pour écouter cet événement :

@Listen('active')
activedHandler(event: CustomEvent) {
  console.log('On a reçu l\'événement "active" : ', event.detail);
}

Etat

On a souvent besoin d’avoir des données internes au composant. On peut définir ça avec le décorateur @State :

@State() active = false

toggleActive() {
 this.active = !this.active
}

Cette variable n’est pas accessible de l’extérieur du composant mais peut être modifiée en interne. Lorsque la valeur change le composant est recalculé pour son affichage. L’utilisation de ce décorateur n’est donc pas à prévoir pour des changements de valeur qui n’affectent pas l’affichage, dans ce cas on utilise des variables classiques.

Les méthodes

Lorsqu’on veut exposer publiquement des méthodes pour un composant on utilise le décorateur @Method. Comme toute ml’architecture de Stencil est asynchrone les méthodes publiques doivent aussi l’être :

import { Method } from '@stencil/core';

@Method()
async getActive() {
 return this.active;
}

Sans préciser que la méthode est asynchrone on peut aussi renvoyer une promesse :

@Method()
getActive() {
  return Promise.resolve(this.active);
}

Pour appeler ces méthodes de l’extérieur c’est classique :

const component = document.querySelector('mon-composant')
const active = component.getActive()

Surveiller les modifications des propriétés

Si on veut faire des actualisations lorsque la valeur d’une propriété change on doit utiliser le décorateur @Watch :

import { Component, Prop, Watch, h } from '@stencil/core';

@Component({
  tag: 'mon-composant',
})

export class MonComposant {

  @Prop() active: boolean;

  @Watch('active')
  watchHandler(newValue: boolean, oldValue: boolean) {
    console.log('La nouvelle valeur est : ', newValue);
  }
...

Processus du composant

Il est souvent intéressant de savoir comment les choses se passent, surtout dans quel ordre. Voici les événements principaux du cycle pour le composant :

  • connectedCallback : connexion au DOM
  • disconnectedCallback : déconnexion du DOM
  • componentDidLoad : après génération
  • componentWillUpdate : avant actualisation
  • componentDidUpdate : après actualisation
  • componentDidUnload : après suppression

Vous pouvez faire un essai :

connectedCallback() {
  console.log('Composant correctement chargé')
}

Le routeur

Quand on regarde le package.json du starter on ne trouve pas que le code de Stencil mais aussi un routeur (la documentation est ici) :

"devDependencies": {
  "@stencil/core": "^1.8.8",
  "@stencil/router": "^1.0.1"
},

Quand on regarde dans le composant de base app-root on trouve ce code :

<stencil-router>
  <stencil-route-switch scrollTopOffset={0}>
    <stencil-route url='/' component='app-home' exact={true} />
    <stencil-route url='/profile/:name' component='app-profile' />
  </stencil-route-switch>
</stencil-router>

On a donc plusieurs composants pour le routeur :

  • stencil-router : c’est le wrapper qui communique avec le navigateur
  • stencil-route-switch : pour grouper des routes
  • stencil-route : fait correspondre une url avec un composant à utiliser

Pour voir comment les routes sont activées il faut aller voir dans le composant app-home :

<stencil-route-link url='/profile/stencil'>
  <button>
    Profile page
  </button>
</stencil-route-link>

Là on utilise le composant stencil-route-link pour appeler une url.

On voit que le routeur a été pensé pour être simple d’utilisation.

La production

Stencil peut se compiler de différentes manières. Si on regarde le fichier de configuration (stencil.config.js) :

import { Config } from '@stencil/core';

// https://stenciljs.com/docs/config

export const config: Config = {
  globalStyle: 'src/global/app.css',
  globalScript: 'src/global/app.ts',
  outputTargets: [
    {
      type: 'www',
      // comment the following line to disable service workers in production
      serviceWorker: null,
      baseUrl: 'https://myapp.local/'
    }
  ]
};

On voit que par défaut on vise un compilation www, donc pour un site Internet. On pourrait aussi choisir dist pour un composant distribuable avec npm, ou des format documentaires. Si on lance la génération du starter :

npm run build

On obtient alors le code compilé dans le dossier www. Dans le sous-dossier build on se retrouve avec une grande quantité de fichiers. C’est là une des faiblesses de Stencil, rien n’est prévu pour regrouper tout le code dans un seul fichier.

Pour compléter je présenterai Svelte dans un prochain article.

 

Print Friendly, PDF & Email

Laisser un commentaire