DEV Community

Sylvain Gougouzian
Sylvain Gougouzian

Posted on

Bun : Un serveur Web ???

Dans l'article précédent, nous avons vu comment manipuler les fichiers et répertoires avec Bun.

Dans ce nouvel article, nous allons voir comment créer un serveur web avec Bun.

Un serveur web, finalement c'est quoi ? C'est un passe-plat de fichiers qui peuvent être créés à la volée ou disponible dans des répertoires.

Nous allons aussi voir comment associer à ce serveur web un serveur de WebSockets qui permettra d'avoir des interactions simplifiées avec les clients.

Dans un troisième temps, nous verrons comment gérer les routes avec Bun.

Et pour terminer nous verrons comment SliDesk utilise le serveur pour afficher la présentation, et comment l'application utilise le serveur de WebSockets pour la synchronisation avec les notes et le contrôle de la présentation.

Coder un server en quelques lignes

En ces quelques lignes,

const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Bonjour le monde !");
  },
});
Enter fullscreen mode Exit fullscreen mode

Bun crée un serveur web qui écoute sur le port 3000.

Le paramètre request est de type Request comme défini dans la Fetch API.

https://developer.mozilla.org/en-US/docs/Web/API/Request permet d'avoir un aperçu détaillé des propriétés de cet objet.

Donc grace à l'objet request, on peut détecter quelle url est demandée et donc faire le traitement associé.

La propriété fetch du paramètre de la fonction Bun.serve doit retourner un objet Response détaillé ici également https://developer.mozilla.org/en-US/docs/Web/API/Response.

La définition de la fonction serve est la suivante :

interface Bun {
  serve(options: {
    fetch: (req: Request, server: Server) => Response | Promise<Response>,
    hostname?: string,
    port?: number,
    development?: boolean,
    error?: (error: Error) => Response | Promise<Response>,
    tls?: {
      key?:
        | string
        | TypedArray
        | BunFile
        | Array<string | TypedArray | BunFile>,
      cert?:
        | string
        | TypedArray
        | BunFile
        | Array<string | TypedArray | BunFile>,
      ca?: string | TypedArray | BunFile | Array<string | TypedArray | BunFile>,
      passphrase?: string,
      dhParamsFile?: string,
    },
    maxRequestBodySize?: number,
    lowMemoryMode?: boolean,
  }): Server;
}

interface Server {
  development: boolean;
  hostname: string;
  port: number;
  pendingRequests: number;
  stop(): void;
}
Enter fullscreen mode Exit fullscreen mode

Nous pouvons constater que seule la méthode fetch est obligatoire. Il est également possible de spécifier :

  • hostname : si l'on souhaite autre chose que localhost;
  • port : par défaut à 80, il est possible de donner une autre valeur;
  • development : si cette valeur est à true, un affichage détaillé est disponible lors d'une erreur;
  • error : dans le cas d'une erreur (page 404 par exemple), il est possible de préciser un comportement
  • tls : afin de naviguer de manière sécurisée, nous devons spécifier les informations de certificats.

Par exemple :

Bun.serve({
  fetch(req) {
    return new Response("Hello!!!");
  },
  tls: {
    key: Bun.file("./key.pem"), // path to TLS key
    cert: Bun.file("./cert.pem"), // path to TLS cert
    ca: Bun.file("./ca.pem"), // path to root CA certificate
  },
});
Enter fullscreen mode Exit fullscreen mode

Finalement, faire un serveur avec Bun est simple.

"Chaussette, j'ai dit chaussette" - Le nain - 2001

J'ai connu un temps que les moins de 20 ans ne peuvent pas connaître, le monde sans WebSockets, un monde où l'on utilisait les XHR pour communiquer avec le serveur sans rafraichir la page.

Heureusement ce monde est révolu et les WebSockets sont arrivées, simplifiant grandement la communication entre les clients et le serveur.

Reprenons notre exemple si dessus :

const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Bonjour le monde !");
  },
});
Enter fullscreen mode Exit fullscreen mode

Nous souhaitons utiliser l'url ws://localhost:3000 pour établir le canal de communication entre les clients et le serveur.

Pour cela, il faut modifier notre fonction fetch pour qu'elle puisse changer de protocole en fonction du canal utilisé (http(s) ou ws(s)).

const server = Bun.serve({
  port: 3000,
  fetch(request, server) {
    if (server.upgrade(request)) {
      // Bun va retourner une "101 - Switching Protocols"
      // si la mise à jour se passe bien
      return undefined;
    }
    return new Response("Bonjour le monde !");
  },
});
Enter fullscreen mode Exit fullscreen mode

Aussi nous allons ajouter une propriété websocket à l'objet passé en paramètre de la fonction qui nous permettra de gérer les connexions, les disconnexions, les messages.

const server = Bun.serve({
  port: 3000,
  fetch(request, server) {
    if (server.upgrade(request)) {
      // Bun va renvoyer une "101 - Switching Protocols"
      // si la mise à jour se passe bien
      return undefined;
    }
    return new Response("Bonjour le monde !");
  },
  websocket: {
    open(ws) {
      ws.subscribe("mongroupe");
    },
    message(ws, message) {
      // on transmet le message à tout le groupe
      ws.publish("mongroupe", message);
    },
    close(ws) {
      ws.unsubscribe("mongroupe");
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Juste avec les quelques lignes présentes ci-dessus, nous obtenons un serveur web et un serveur websocket performant.

A titre comparatif, sur son site, Bun montre un comparatif de ses performances avec les deux autres runtimes principaux du marché.

Server-side rendering React benchmark

WebSocket chat server

WebSocket chat server

Comment gérer les routes ?

Comme nous pouvons le constater, le paramètre à fournir à la fonction serve n'a qu'une seule propriété fetch qui traite l'ensemble des requêtes web (http(s) et ws(s)).

Une des premières solutions seraient un traitement de la donnée request. Comme vu précédemment dans l'article, request est de type Request et dispose donc d'une propriété url qui nous permet de savoir quelle route nous souhaitons afficher.

Ainsi le code suivant peut être envisagé :

const server = Bun.serve({
  port: 3000,
  fetch(request, server) {
    if (server.upgrade(request)) {
      // Bun va renvoyer une "101 - Switching Protocols"
      // si la mise à jour se passe bien
      return undefined;
    }
    switch (request.url) {
      case "/":
        return new Response("index");
      case "/search":
        return new Response("page de recherche");
      // ...
      default:
        return new Response("Bonjour le monde !");
    }
  },
  websocket: {
    open(ws) {
      ws.subscribe("mongroupe");
    },
    message(ws, message) {
      // on transmet le message à tout le groupe
      ws.publish("mongroupe", message);
    },
    close(ws) {
      ws.unsubscribe("mongroupe");
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Quand on a quelques routes, cette solution peut convenir, mais elle n'est pas dynamique.

Pour cela, Bun a implémenté un système de routes "à la Next.js".

Prenons la structure de fichiers suivante :

pages
├── index.tsx
├── settings.tsx
├── blog
│   ├── [slug].tsx
│   └── index.tsx
└── [[...catchall]].tsx
Enter fullscreen mode Exit fullscreen mode

Bun dispose d'une classe FileSystemRouter qui permet ce traitement.

Ainsi nous pouvons configurer le système de la manière suivante :

const router = new Bun.FileSystemRouter({
  style: "nextjs",
  dir: "./pages",
  origin: "http://localhost",
  assetPrefix: "_next/static/",
});
Enter fullscreen mode Exit fullscreen mode

Nous constatons qu'un paramètre style existe, ce qui sous-entend que Bun souhaite faire évoluer cette classe pour gérer d'autres modèles de gestion automatisée de routes.

Une fonction match permet de diriger correctement le traitement.

router.match("/");
Enter fullscreen mode Exit fullscreen mode

Nous pouvons l'intégrer dans notre exemple de la partie précédente de la manière suivante.

Créons un répertoire de travail et entrons dans ce répertoire.

mkdir myserver
cd myserver
Enter fullscreen mode Exit fullscreen mode

Utilisons la commande bun init pour instancier le projet.

bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (myserver):
entry point (index.ts): index.js

Done! A package.json file was saved in the current directory.
 + index.js
 + .gitignore
 + jsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.js
Enter fullscreen mode Exit fullscreen mode

Ajoutons les bibliothèques react et react-dom :

bun add react react-dom
Enter fullscreen mode Exit fullscreen mode

Modifions le fichier index.js par le contenu suivant :

#!/usr/bin/env bun
import { renderToReadableStream } from "react-dom/server";

const router = new Bun.FileSystemRouter({
  style: "nextjs",
  dir: "./pages",
});

Bun.serve({
  port: 3000,
  async fetch(request) {
    const match = router.match(request.url);
    const Component = await import(match.filePath);
    const stream = await renderToReadableStream(<Component.default />);

    return new Response(stream, {
      headers: { "Content-Type": "text/html; charset=utf-8" },
    });
  },
});
Enter fullscreen mode Exit fullscreen mode

Créons un fichier pages/index.js :

import React from "react";
export default function Index() {
  return <p>Hello world!</p>;
}
Enter fullscreen mode Exit fullscreen mode

Lançons notre serveur :

bun index.js
Enter fullscreen mode Exit fullscreen mode

Allons sur notre navigateur préféré sur l'url http://localhost:3000 et là, la magie opère, nous obtenons une page avec écrit "Hello world!".

Cette solution est bien plus évoluée et utilise toute la puissance de React pour faire une application SSR (Server Side Rendering).

Et SliDesk dans tout ça ?

SliDesk a besoin d'un serveur web et d'un serveur websocket pour afficher la présentation et la vue du présentateur, et également les faire interagir entre elles.

Le code du serveur est proche de la première méthode proposée. En effet SliDesk n'est pas une SPA (Single Page Application), elle ne dispose pas de contenu qui s'affiche en fonction d'action, de paramètre. Il n'y a pas de base de données derrière. Il suffit "juste" de servir une page .html qui contient le css et le js nécessaire au bon fonctionnement. Seules les images sont les chemins inconnus.

Ainsi avec le code suivant, il est possible de gérer le multilingue (une page par langue), la page de notes et le serveur WebSockets qui permet de naviguer de manière synchronisée entre les "viewers".

import {
  langPage,
  defaultPage,
  favicon,
  notePage,
  webSockets,
  defaultAction,
} from "../utils/server";

const { log } = console;

export default class Server {
  static create(html, options, path) {
    globalThis.html = html;
    globalThis.path = path;
    // eslint-disable-next-line no-undef
    globalThis.server = Bun.serve({
      port: options.port,
      fetch(req) {
        const url = new URL(req.url);
        if (url.pathname.match(/^\/--(\w+)--\/$/g))
          return langPage(url.pathname);
        switch (url.pathname) {
          case "/":
            return defaultPage();
          case "/favicon.svg":
            return favicon();
          case "/notes":
            return notePage(options);
          case "/ws":
            return webSockets(req);
          default:
            return defaultAction(req, options);
        }
      },
      websocket: {
        open(ws) {
          ws.subscribe("slidesk");
        },
        message(ws, message) {
          ws.publish("slidesk", message);
        },
        close(ws) {
          ws.unsubscribe("slidesk");
        },
      },
    });
    if (options.notes)
      log(
        `Your speaker view is available on: \x1b[1m\x1b[36;49mhttp://${options.domain}:${options.port}/notes\x1b[0m`
      );
    log(
      `Your presentation is available on: \x1b[1m\x1b[36;49mhttp://${options.domain}:${options.port}\x1b[0m`
    );
    log();
  }

  static setHTML(html) {
    globalThis.html = html;
    globalThis.server.publish("slidesk", JSON.stringify({ action: "reload" }));
  }

  static send(action) {
    globalThis.server.publish("slidesk", JSON.stringify({ action }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Cet article nous a montré comment créer un serveur web et un serveur websocket en gérer des routes.

Le prochain article traitera de la partie build de Bun.

Top comments (1)

Collapse
 
shiipou profile image
Flavien from Lenra

Merci ! Cet article m'a permis de débloquer un soucis que j'avais sur Bun pour le démarrage de mon app !