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 !");
},
});
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;
}
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 quelocalhost
; -
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
},
});
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 !");
},
});
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 !");
},
});
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");
},
},
});
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é.
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");
},
},
});
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
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/",
});
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("/");
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
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
Ajoutons les bibliothèques react
et react-dom
:
bun add react react-dom
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" },
});
},
});
Créons un fichier pages/index.js
:
import React from "react";
export default function Index() {
return <p>Hello world!</p>;
}
Lançons notre serveur :
bun index.js
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 }));
}
}
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)
Merci ! Cet article m'a permis de débloquer un soucis que j'avais sur Bun pour le démarrage de mon app !