écrits/tutorial/2026/06
Tutorial5 juin 2026·25 min

Créer des Applications Bureau Ultra-Rapides avec Electrobun et TypeScript (2026)

Apprenez à créer des applications bureau rapides, légères et multiplateformes avec Electrobun — le framework natif TypeScript qui produit des applications jusqu'à 5× plus petites qu'Electron. Ce tutoriel couvre la configuration, l'IPC typé, l'accès au système de fichiers, les menus natifs et le packaging pour la production.

Prérequis

Avant de commencer ce tutoriel, assurez-vous d'avoir :

  • Bun 1.2+ installé (curl -fsSL https://bun.sh/install | bash)
  • Une bonne maîtrise de TypeScript (interfaces, génériques, async/await)
  • Une familiarité avec React (hooks, état, effets)
  • Un éditeur de code — VS Code avec l'extension Bun est recommandé
  • macOS 13+, Windows 10+, ou Ubuntu 22.04+

Ce que vous allez construire

Vous créerez NoteFlow — une application de prise de notes Markdown minimaliste pour bureau qui :

  • Crée, édite et sauvegarde des notes sur le système de fichiers local
  • Liste les notes dans une barre latérale avec changement en un clic
  • Utilise un RPC typé pour que l'interface et le backend partagent une seule définition de type
  • Se compile en moins de 20 Mo — contre plus de 200 Mo pour une application Electron équivalente

Pourquoi Electrobun ? La justification technique

Electron a révolutionné le développement d'applications bureau, mais il embarque un moteur Chromium complet avec chaque application. Une application "Hello World" Electron fait environ 140 Mo. Tauri a résolu le problème de taille en écrivant le backend en Rust — mais vous avez maintenant besoin de deux langages.

Electrobun adopte une approche différente :

FonctionnalitéElectronTauriElectrobun
RuntimeNode.js + ChromiumRust + WebView systèmeBun + WebView système
Taille minimale140 Mo3 Mo12 Mo
Langage backendJavaScript / TypeScriptRustTypeScript
Style IPCChaînes d'événementsCommandes RustRPC async typé
Mises à jour différentiellesManuellesIntégréesIntégrées (patches de 4 Ko)
Courbe d'apprentissageFaibleÉlevéeFaible

Étape 1 : Installer Bun

Electrobun nécessite Bun comme runtime. Si vous ne l'avez pas encore installé :

# macOS / Linux
curl -fsSL https://bun.sh/install | bash
 
# Windows (PowerShell)
powershell -c "irm bun.sh/install.ps1|iex"
 
# Vérification
bun --version   # doit afficher 1.2.x ou supérieur

Étape 2 : Créer la structure du projet

Electrobun est livré avec un générateur de projet qui configure la bonne arborescence de dossiers :

bunx electrobun init

Répondez aux questions :

  • Nom du projet : noteflow
  • Template : react
  • Gestionnaire de paquets : bun

Entrez ensuite dans le répertoire et installez les dépendances :

cd noteflow
bun install

La structure du projet ressemble à ceci :

noteflow/
├── electrobun.config.ts   # Identité de l'app + cibles de build + canal de mise à jour
├── package.json
├── bun.lockb
├── src/
│   ├── main/              # Processus principal — tourne dans Bun, accès complet au système
│   │   └── index.ts
│   ├── shared/            # Types partagés entre le processus principal et la WebView
│   └── views/             # Interface WebView — un dossier par vue
│       └── mainview/
│           ├── index.html
│           ├── index.tsx
│           └── style.css
└── public/                # Assets statiques copiés dans le bundle

Étape 3 : Comprendre l'architecture à deux processus

Electrobun divise votre application en deux processus isolés :

Processus principal (src/main/) tourne dans Bun :

  • Crée et gère les fenêtres natives (BrowserWindow)
  • Lit et écrit dans le système de fichiers via les API natives de Bun
  • Construit les menus natifs, la barre système, les boîtes de dialogue
  • Exécute toutes les opérations privilégiées que la WebView ne peut pas faire

Processus WebView (src/views/) affiche votre interface en utilisant le moteur de navigateur natif du système :

  • macOS : WebKit (le même moteur que Safari)
  • Windows : Edge WebView2 (basé sur Chromium, fourni avec Windows 10+)
  • Linux : WebKitGTK

Les deux processus communiquent via RPC typé — la fonctionnalité principale d'Electrobun.

Étape 4 : Définir le contrat RPC partagé

Créez src/shared/rpc.ts :

import { defineRpc } from "electrobun/bun";
 
// Appels de la WebView vers le processus principal
export const mainRpc = defineRpc({
  listNotes: async (): Promise<string[]> => [],
  readNote: async (filename: string): Promise<string> => "",
  writeNote: async (filename: string, content: string): Promise<void> => {},
  deleteNote: async (filename: string): Promise<void> => {},
  openFilePicker: async (): Promise<string | null> => null,
});
 
// Appels du processus principal vers la WebView (événements push)
export const webviewRpc = defineRpc({
  onNoteChanged: async (filename: string): Promise<void> => {},
});

defineRpc inspecte les signatures de fonctions au moment du build. Quand la WebView appelle mainRpc.call.readNote("ma-note.md"), TypeScript applique exactement les types des paramètres et de la valeur de retour.

Étape 5 : Implémenter le processus principal

Remplacez src/main/index.ts par l'implémentation complète du backend :

import { BrowserWindow, app, dialog } from "electrobun/bun";
import { mainRpc } from "../shared/rpc";
import { join } from "path";
import { homedir } from "os";
import { mkdirSync } from "fs";
 
const NOTES_DIR = join(homedir(), ".noteflow", "notes");
mkdirSync(NOTES_DIR, { recursive: true });
 
// Enregistrer les handlers RPC avant l'ouverture de la fenêtre
mainRpc.handle({
  async listNotes() {
    const glob = new Bun.Glob("*.md");
    const files: string[] = [];
    for await (const file of glob.scan(NOTES_DIR)) {
      files.push(file);
    }
    return files.sort();
  },
 
  async readNote(filename) {
    const path = join(NOTES_DIR, filename);
    const file = Bun.file(path);
    if (!(await file.exists())) return "";
    return file.text();
  },
 
  async writeNote(filename, content) {
    await Bun.write(join(NOTES_DIR, filename), content);
  },
 
  async deleteNote(filename) {
    await Bun.file(join(NOTES_DIR, filename)).delete();
  },
 
  async openFilePicker() {
    const result = await dialog.showOpenDialog({
      filters: [{ name: "Markdown", extensions: ["md", "txt"] }],
      properties: ["openFile"],
    });
    return result.canceled ? null : result.filePaths[0];
  },
});
 
app.on("ready", () => {
  const win = new BrowserWindow({
    title: "NoteFlow",
    url: "views://mainview/index.html",
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    styleMask: ["titled", "closable", "miniaturizable", "resizable"],
  });
 
  win.show();
});
 
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

Deux points importants :

  1. Les handlers RPC sont enregistrés avant app.on("ready"). La WebView peut les appeler dès son chargement.
  2. dialog.showOpenDialog est un sélecteur de fichiers natif du système d'exploitation.

Étape 6 : Construire l'interface React

Remplacez src/views/mainview/index.tsx :

import React, { useState, useEffect, useCallback, useRef } from "react";
import { createRoot } from "react-dom/client";
import { mainRpc } from "../../shared/rpc";
 
type Note = { filename: string; title: string };
 
function noteTitle(filename: string) {
  return filename.replace(/\.md$/, "").replace(/-/g, " ");
}
 
function App() {
  const [notes, setNotes] = useState<Note[]>([]);
  const [active, setActive] = useState<string | null>(null);
  const [content, setContent] = useState("");
  const [dirty, setDirty] = useState(false);
  const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
 
  useEffect(() => {
    refresh();
  }, []);
 
  async function refresh() {
    const files = await mainRpc.call.listNotes();
    const noteList = files.map((f) => ({ filename: f, title: noteTitle(f) }));
    setNotes(noteList);
    if (noteList.length > 0 && active === null) {
      await openNote(noteList[0].filename);
    }
  }
 
  async function openNote(filename: string) {
    if (dirty && active) await save(active, content);
    const text = await mainRpc.call.readNote(filename);
    setActive(filename);
    setContent(text);
    setDirty(false);
  }
 
  function handleChange(value: string) {
    setContent(value);
    setDirty(true);
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      if (active) save(active, value);
    }, 1500);
  }
 
  const save = useCallback(async (filename: string, text: string) => {
    await mainRpc.call.writeNote(filename, text);
    setDirty(false);
  }, []);
 
  async function newNote() {
    const timestamp = new Date().toISOString().split("T")[0];
    const filename = `note-${timestamp}-${Date.now()}.md`;
    await mainRpc.call.writeNote(filename, `# Nouvelle note\n\n`);
    await refresh();
    await openNote(filename);
  }
 
  async function deleteActive() {
    if (!active || !confirm(`Supprimer "${noteTitle(active)}" ?`)) return;
    await mainRpc.call.deleteNote(active);
    setActive(null);
    setContent("");
    setDirty(false);
    await refresh();
  }
 
  return (
    <div className="app">
      <aside className="sidebar">
        <div className="sidebar-header">
          <span className="brand">NoteFlow</span>
          <button className="icon-btn" onClick={newNote} title="Nouvelle note">+</button>
        </div>
        <ul className="note-list">
          {notes.map((n) => (
            <li
              key={n.filename}
              className={n.filename === active ? "active" : ""}
              onClick={() => openNote(n.filename)}
            >
              {n.title}
            </li>
          ))}
        </ul>
      </aside>
 
      <main className="editor-area">
        {active ? (
          <>
            <div className="toolbar">
              <span className="status">{dirty ? "Non sauvegardé…" : "Sauvegardé"}</span>
              <button onClick={() => active && save(active, content)}>Sauvegarder</button>
              <button className="danger" onClick={deleteActive}>Supprimer</button>
            </div>
            <textarea
              className="editor"
              value={content}
              onChange={(e) => handleChange(e.target.value)}
              onKeyDown={(e) => {
                if ((e.ctrlKey || e.metaKey) && e.key === "s") {
                  e.preventDefault();
                  if (active) save(active, content);
                }
              }}
              spellCheck={false}
            />
          </>
        ) : (
          <div className="empty-state">
            <p>Cliquez sur + pour créer votre première note.</p>
          </div>
        )}
      </main>
    </div>
  );
}
 
const el = document.getElementById("app");
if (el) createRoot(el).render(<App />);

Patterns clés :

  • Sauvegarde automatique après 1,5 seconde d'inactivité
  • Cmd+S / Ctrl+S déclenche une sauvegarde immédiate
  • Changer de note sauvegarde automatiquement la précédente avant le chargement

Étape 7 : Ajouter les styles CSS

Remplacez src/views/mainview/style.css par les styles de la version anglaise (les styles CSS sont identiques pour toutes les langues).

Étape 8 : Lancer en mode développement

Démarrez le serveur de développement :

bun start

Electrobun lance le processus principal dans Bun et ouvre une fenêtre native affichant votre WebView. Les modifications des sources WebView rechargent automatiquement l'interface. Les changements du processus principal nécessitent un redémarrage.

Étape 9 : Ajouter un menu natif

Ajoutez un menu d'application macOS/Windows approprié dans src/main/index.ts :

import { Menu, MenuItem } from "electrobun/bun";
 
function buildMenu(win: BrowserWindow) {
  const menu = new Menu();
 
  const fileMenu = new MenuItem({
    label: "Fichier",
    submenu: [
      new MenuItem({
        label: "Nouvelle note",
        accelerator: "CmdOrCtrl+N",
        click: () => win.webContents.executeScript("window.__newNote?.()"),
      }),
      new MenuItem({ type: "separator" }),
      new MenuItem({ role: "quit" }),
    ],
  });
 
  const editMenu = new MenuItem({
    label: "Édition",
    submenu: [
      new MenuItem({ role: "undo" }),
      new MenuItem({ role: "redo" }),
      new MenuItem({ type: "separator" }),
      new MenuItem({ role: "cut" }),
      new MenuItem({ role: "copy" }),
      new MenuItem({ role: "paste" }),
      new MenuItem({ role: "selectAll" }),
    ],
  });
 
  menu.append(fileMenu);
  menu.append(editMenu);
  Menu.setApplicationMenu(menu);
}

Étape 10 : Compiler pour la production

Quand votre application est prête à être distribuée :

bunx electrobun build --env=stable

Emplacements de sortie :

  • macOS : dist/NoteFlow.app
  • Windows : dist/NoteFlow-win-x64.exe
  • Linux : dist/NoteFlow-linux-x64.AppImage

Le bundle macOS fera environ 14–64 Mo selon les dépendances — sans Chromium, sans Node.js, sans runtime Electron.

Étape 11 : Activer les mises à jour différentielles

Le système de mise à jour d'Electrobun génère des diffs binaires entre les versions. Une mise à jour typique ne dépasse pas 4–14 Ko au lieu d'une réinstallation complète.

Configurez electrobun.config.ts :

import { defineConfig } from "electrobun/config";
 
export default defineConfig({
  app: {
    name: "NoteFlow",
    version: "1.0.0",
    identifier: "tn.noqta.noteflow",
  },
  updates: {
    provider: "github",
    owner: "your-org",
    repo: "noteflow",
    channel: "stable",
  },
  build: {
    targets: ["mac-arm64", "mac-x64", "win-x64", "linux-x64"],
  },
});

Vérifiez les mises à jour depuis le processus principal :

import { autoUpdater } from "electrobun/bun";
 
autoUpdater.on("update-downloaded", () => {
  autoUpdater.quitAndInstall();
});
 
await autoUpdater.checkForUpdates();

Tester votre implémentation

Après bun start, vérifiez ces points :

  1. La fenêtre s'ouvre avec le titre correct NoteFlow
  2. Cliquer sur + crée un nouveau fichier .md dans ~/.noteflow/notes/
  3. Taper dans l'éditeur sauvegarde automatiquement après 1,5 seconde
  4. Cmd+S / Ctrl+S déclenche une sauvegarde immédiate
  5. Changer de note sauvegarde automatiquement la précédente
  6. Après bunx electrobun build, le bundle fait moins de 100 Mo

Résolution des problèmes

Fenêtre blanche au lancement Vérifiez que src/views/mainview/index.html contient <div id="app"></div> et que index.tsx importe style.css. Lancez bun run build:webview séparément pour détecter les erreurs de bundle.

L'appel RPC reste en attente indéfiniment Assurez-vous que mainRpc.handle(...) est appelé avant app.on("ready"). Si le handler est enregistré après l'ouverture de la fenêtre, le premier appel RPC de la WebView peut arriver avant que le handler existe.

Linux : WebView vide sur Ubuntu 22.04 Installez la dépendance WebKit : sudo apt install webkit2gtk-4.1. Sur Ubuntu 24.04+, cela est installé par défaut.

Le bundle de production est plus grand que prévu Utilisez le flag --external dans electrobun.config.ts pour marquer les gros packages comme externes et ne bundler que ce dont vous avez besoin. Analysez avec bunx electrobun analyze.

Prochaines étapes

Une fois NoteFlow fonctionnel, voici des extensions naturelles à explorer :

  • Aperçu Markdown — ajoutez un volet divisé avec la bibliothèque marked pour le rendu HTML en temps réel
  • Barre système — utilisez l'API tray d'Electrobun pour garder NoteFlow accessible sans fenêtre
  • Fenêtres multiples — créez un second BrowserWindow pour un panneau de préférences
  • Synchronisation iCloud — pointez NOTES_DIR vers un dossier iCloud Drive sur macOS pour une synchronisation transparente
  • Pipeline de release GitHub Actions — automatisez le build et la notarisation à chaque push sur main

Conclusion

Vous avez construit une application bureau entièrement fonctionnelle en TypeScript pur — sans Rust, sans moteur de navigateur embarqué, et avec un bundle inférieur à 20 Mo. Le RPC typé d'Electrobun relie proprement les processus principal et WebView, et le système de mises à jour différentielles signifie que vos utilisateurs ne téléchargent jamais une réinstallation complète.

Electrobun est encore en train de mûrir, mais la v1 est prête pour la production pour les outils internes, les utilitaires de développement et les applications métier. Si vous êtes à l'aise avec TypeScript et souhaitez distribuer des logiciels de bureau natifs sans apprendre Rust, Electrobun est le chemin le plus ergonomique disponible en 2026.