Svelte, encore un framework front ?

Non, Svelte est une nouvelle librairie qui permet de gérer les composants front-end d’une application. Mais il existe son grand frère, Sapper, qui lui est un framework basé sur Svelte et qui permet de créer une application de type SPA.

A la grande différence des dernières librairies et frameworks Front-End, où la plupart du travail est directement exécuté au niveau du navigateur Web via le VirtualDOM, Svelte a décidé d’effectuer le gros du travail lors de la compilation afin de générer la logique de manipulation du DOM. Cette particularité offre deux avantages :

  • Des performances accrues car il y a moins de code à exécuter lors d’un changement d’état au sein d’un composant.
  • Le poids du code final est largement inférieur et dépend du nombre de fonctionnalités utilisées dans le code.

Générer un premier composant Svelte

Alors là, pas de magie comme avec les différents CLI des autres librairies ou framework. Svelte ne propose pas encore de CLI digne de ce nom. Il faut générer le projet en clonant un template présent sur Github / en utilisant leur RPL (un fichier ZIP) ou en utilisant degit. C’est cette méthode que nous allons utiliser ici.

npx degit sveltejs/template counter-button

Une fois le projet généré, il faut installer les dépendances via votre gestionnaire de package préféré.

yarn install
// ou
npm install

Toutes les dépendances sont installées, nous sommes prêts pour le développement de notre composant.

Contrairement aux autres librairies / frameworks, Svelte se démarque encore une fois dans l’outil de “build” qu’il utilise. Là où les autres ont l’habitude d’utiliser Webpack, Svelte utilise par défaut Rollup.

Je dois avouer que la configuration de Rollup est un jeu d’enfant comparé à la configuration de Webpack.

Donc pas de surprise, on démarre le serveur de développement avec un script présent dans le fichier package.json.

yarn dev
// ou
npm run dev

Voilà. Notre composant Svelte est servi en localhost sur le port 5000.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fa037bd7-7a1a-4a71-9609-5463a2c0d2a6/Capture_decran_2020-04-07_a_15.54.00.png

Création de notre counter-button

Anatomie d’un composant Svelte

Ok, nous avons un composant avec des fichiers .svelte, mais que contient-il ?

<script>
	export let name;
</script>

<main>
	<h1>Hello {name}!</h1>
	<p>Visit the <a href="<https://svelte.dev/tutorial>">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>

<style>
	main {
		text-align: center;
		padding: 1em;
		max-width: 240px;
		margin: 0 auto;
	}

	h1 {
		color: #ff3e00;
		text-transform: uppercase;
		font-size: 4em;
		font-weight: 100;
	}

	@media (min-width: 640px) {
		main {
			max-width: none;
		}
	}
</style>

Un peu comme un fichier React ou VueJS, le fichier est divisé en 3 blocs:

  • un bloc contenant le code JavaScript
  • un bloc contenant le template HTML
  • et un dernier bloc contenant le style CSS

Modifions un peu le template de base pour obtenir notre composant counter-bouton qui va pouvoir prendre en propriété d’entrée un nombre qui par défaut vaut 0.

// main.js
import App from "./App.svelte";

const app = new App({
  target: document.body,
  props: {
    count: 0,
  },
});

export default app;

<script>
  export let count;

  function handleClick() {
    count = +count + 1;
  }
</script>

<style>
  .button {
    display: flex;
    flex-direction: row;
    margin: 0.75rem;
    padding: 0.75rem 1.5rem;

    border: none;
    border-radius: 0.2rem;
    outline: none;

    background-color: tomato;

    color: white;
    font-family: inherit;
    font-size: 1.25rem;
    font-weight: 400;
    line-height: 1.5rem;
    text-decoration: none;
    text-align: center;

    cursor: pointer;
    transition: all 150ms ease-out;
  }
  button:hover {
    background-color: tint(tomato, 10%);
    box-shadow: 0 0 0 (1.5rem / 8) white, 0 0 0 (1.5rem / 4) tint(tomato, 10%);
  }

  button:active {
    background-color: shade(tomato, 5%);
    box-shadow: 0 0 0 (1.5rem / 8) shade(tomato, 5%),
      0 0 0 (1.5rem / 4) shade(tomato, 5%);
    transition-duration: 150ms / 2;
  }

  .button span {
    display: block;
    min-width: 24px;
    height: 24px;
    line-height: 24px;
    text-align: center;
    margin: 0 0 0 0.5rem;
    background: white;
    font-size: 0.825rem;
    color: tomato;
    border-radius: 50%;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

<svelte:options tag="counter-button" />
<main>
  <button class="button" on:click={handleClick}>
    Click Me !!
    <span>{count}</span>
  </button>
</main>

On obtient le résultat suivant :

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/38315d9e-fa09-41e7-acec-aa7d8ebe1cb6/Capture_decran_2020-04-08_a_12.10.37.png

Builder son composant Svelte en WebComponent

Pour builder notre application Svelte en WebComponent, il faut modifier légèrement la configuration de Rollup et placer une option <svelte:options tag:"NOM_DU_WEBCOMPONENT"/> dans notre fichier App.svelte afin que celui-ci soit construit en tant que WebComponent avec un nom bien spécifique (ici “counter-button“).

// App.svelte
<svelte:options tag="counter-button" />

// Rollup.config.js
...
	svelte({
      // enable run-time checks when not in production
      dev: !production,
      **customElement: true,**
      // we'll extract any component CSS out into
      // a separate file - better for performance
      css: (css) => {
        css.write("public/build/bundle.css");
      },
    }),
...

Maintenant que la configuration est terminée, le build d’un composant est relativement simple, il suffit d’exécuter la commande build :

yarn build
// ou
npm run build

Voilà le composant est buildé sous forme d’un fichier Javascript dans le dossier public/build

Intégrer le WebComponent dans une page HTML

Pour cela rien de plus simple :

  • créez une page HTML classique dans un nouveau dossier, voire même dans un nouveau projet.
// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My page with Svelte WebComponent</title>
    <script src="./js/bundle.js"></script>
  </head>
  <body>
    <h1>My WebComponent</h1>
    <div class="content">
      <counter-button count="1"></counter-button>
    </div>
  </body>
</html>
  • Copiez le fichier bundle.js qui est le résultat du build précédent et collez-le dans le dossier js.

Il faut maintenant servir la page HTML avec votre serveur web préféré. (Pour ma part j’utilise http-server, installé en global sur ma machine).

On obtient le résultat suivant :

On peut maintenant ajouter plusieurs composants <counter-button> dans une même page. Ils sont tous totalement indépendants.

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My page with Svelte WebComponent</title>
    <script src="./js/bundle.js"></script>
  </head>
  <body>
    <h1>My WebComponent</h1>
    <div class="content">
      <div class="component">
        <span>My Counter Button 1</span>
        <counter-button count="1"></counter-button>
      </div>
      <div class="component">
        <span>My Counter Button 2 start at 10</span>
        <counter-button count="10"></counter-button>
      </div>
    </div>
  </body>
</html>

Communication entre WebComponents

Créer un WebComponent avec Svelte s’avère très simple et surtout assez pratique.

Mais comment les composants peuvent communiquer avec la page qui les utilise ou avec d’autres WebComponents ?

Deux possibilités :

  • la première via les évènements dit custom
  • la seconde via un bus d’évènements hébergés au niveau de la page HTML.

Nous allons tester la première solution.

Pour cela, créons un second WebComponent sur le même principe que le premier et qui affichera la somme des différents boutons présents dans la page.

Composant Sum

<script>
  export let sum;
</script>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  span {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

<svelte:options tag="counter-sum" />
<main>
  <p>
    The Sum is :
    <span>{sum}</span>
  </p>
</main>

On intègre le nouveau composant dans notre page HTML :

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My page with Svelte WebComponent</title>
    <script src="./js/bundle.js"></script>
    <script src="./js/counter-sum.js"></script>
  </head>
  <body>
    <h1>My WebComponent</h1>
    <div class="content">
      <div class="component">
        <span>My Counter Button 1</span>
        <counter-button count="1" id="buttonOne"></counter-button>
      </div>
      <div class="component">
        <span>My Counter Button 2 start at 10</span>
        <counter-button count="10" id="buttonTwo"></counter-button>
      </div>
      <div class="sum">
        <counter-sum id="sumComponent" sum="0"></counter-sum>
      </div>
    </div>
    <script src="./js/main.js"></script>
  </body>
</html>

Exportation des évènements depuis les composants <counter-button>

Pour exporter la donnée d’un WebComponent créé via Svelte, il faut créer un émetteur d’évènement custom via la méthode dispatch.

// CounterButton
// App.svelte
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
...
  function handleClick() {
    count = +count + 1;
    exportData();
  }
  function exportData() {
    dispatch("data", {
      count: count
    });
  }
...
</script>
...

Remarquez le +count; qui nous permet de transformer un string en number car on passe un string en valeur à la propriété count du webComponent.

Il faut construire à nouveau le composant et l’intégrer dans la page HTML.

On récupère ensuite l’évènement custom créé par notre composant au niveau d’un fichier main.js

// main.js
// HTML Elements
const buttonOne = document.getElementById("buttonOne");
const buttonTwo = document.getElementById("buttonTwo");
const sumElem = document.getElementById("sumComponent");

// Listeners
buttonOne.$on("data", (event) => {
  updateCountOne(event);
});
buttonTwo.$on("data", (event) => {
  updateCountTwo(event);
});

// Variables
let countOne = 1;
let countTwo = 10;
let buttonSum = 0;

// Init all attribute
buttonOne.setAttribute("count", countOne);
buttonTwo.setAttribute("count", countTwo);

// Methods
function updateCountOne(event) {
  countOne = event.detail.count;
  computeSum();
}

function updateCountTwo(event) {
  countTwo = event.detail.count;
  computeSum();
}

function computeSum() {
  buttonSum = countOne + countTwo;
  sumElem.setAttribute("sum", buttonSum);
}

computeSum();

Détaillons un peu le fichier main.js :

// HTML Elements
const buttonOne = document.getElementById("buttonOne");
const buttonTwo = document.getElementById("buttonTwo");
const sumElem = document.getElementById("sumComponent");

On commence par récupérer les différents WebComponent de notre page et on les assigne à des constantes.

// Listeners
buttonOne.$on("data", (event) => {
  updateCountOne(event);
});
buttonTwo.$on("data", (event) => {
  updateCountTwo(event);
});

Puis on ajoute des écouteurs d’évènements qui vont récupérer la donnée émise par chacun de nos composants <counter-button> et qui vont faire appel à des méthodes à chaque fois qu’un évènement est émis.

// Variables
let countOne = 1;
let countTwo = 10;
let buttonSum = 0;

On définit les variables qui nous servent à initialiser les propriétés count de nos deux composants <counter-button> mais également qui vont servir à calculer la somme de nos deux composants.

// Init all attribute
buttonOne.setAttribute("count", countOne);
buttonTwo.setAttribute("count", countTwo);

On initialise les props de chacun de composant <counter-button>

// Methods
function updateCountOne(event) {
  countOne = event.detail.count;
  computeSum();
}

function updateCountTwo(event) {
  countTwo = event.detail.count;
  computeSum();
}

function computeSum() {
  buttonSum = countOne + countTwo;
  sumElem.setAttribute("sum", buttonSum);
}

computeSum();

Enfin nous avons trois méthodes qui vont nous permettre de mettre à jour chacune des variables et de calculer la somme des valeurs de nos différents composants <counter-button>.

Voici le résultat attendu:

En conclusion, Svelte est une librairie intéressante et s’avère être une bonne alternative aux autres librairies front. Son principe de fonctionnement est vraiment bien pensé et performant. C’est une librairie à surveiller de près.

Nous verrons dans un prochain article comment utiliser son grand frère (Sapper) pour créer un site complet.