ENbext.dev
DocumentationPRISM (natif bext)

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Architecture des îles

bext prend en charge le modèle d'architecture des îles : vos pages sont rendues par le serveur au format HTML statique avec zéro JavaScript par défaut. Seuls les composants interactifs spécifiques (« îlots ») sont hydratés sur le client. Cela vous offre le meilleur des deux mondes : des chargements initiaux rapides sans charge utile JS, ainsi qu'une interactivité riche là où vous en avez besoin.

Comment ça marche#

  1. Le serveur affiche la page complète au format HTML, y compris le contenu de secours statique pour chaque île.
  2. Les marqueurs d'îlot ( <bext-island> ) intègrent le nom du composant et les accessoires sérialisés
  3. Un script minimal côté client trouve tous les marqueurs d'îlot et charge paresseusement leurs modules JavaScript.
  4. Chaque île s'hydrate indépendamment – ​​aucun environnement d'exécution de cadre global n'est nécessaire
code
Server HTML (zero JS by default):
┌──────────────────────────────────────┐
│  <nav>Home | Blog | About</nav>     │  ← Static HTML (no JS)
│                                      │
│  <h1>Product Detail</h1>            │  ← Static HTML (no JS)
│  <p>Description text...</p>          │
│                                      │
│  <bext-island data-component="Cart"> │  ← Island marker
│    <button>Add to Cart</button>      │  ← Fallback HTML (works without JS)
│  </bext-island>                      │
│                                      │
│  <footer>© 2026</footer>            │  ← Static HTML (no JS)
└──────────────────────────────────────┘

Client hydration (only for islands):
  Cart.js loaded → mounts over fallback → interactive button

Deux versions : React vs signaux#

bext expédie deux modèles d'hydratation pour les îles. Les deux produisent un Élément hôte <bext-island> sur le serveur ; le script de chargement sélectionne le runtime client à récupérer en fonction de l'attribut data-runtime.

"use client" (React) "use signals" (bext)
Taille du paquet ~ 45 Ko minimum ( react + react -dom) ~ 3 Ko d'exécution + composant
Hydratation re-rendu avec hydrateRoot, différence avec le DOM du serveur marqueurs de marche, attachez des abonnements au DOM existant
Modèle mental les composants renvoient VDOM, le framework se réconcilie les fonctions créent des signaux ; lit, suivez-les
Quand choisir SPA riches en fonctionnalités, bibliothèques react, intégration devtool petits widgets interactifs, rendus sensibles aux performances

Le chemin basé sur React est documenté ci-dessous : il s'agit du modèle « îlot » le plus familier. Pour le chemin des signaux (groupe plus petit, pas de nouveau rendu, stabilité du DOM entre les mises à jour), voir Signaux. Les deux chemins peuvent coexister sur la même page.

API du serveur#

île (nom, accessoires, solution de secours)#

Afficher un marqueur d'île avec du code HTML de secours côté serveur :

typescript
import { island } from "@bext-stack/framework";

function ProductPage(product: Product): string {
  return `
    <h1>${product.name}</h1>
    <p>${product.description}</p>
    <div class="price">$${product.price}</div>

    ${island("AddToCart", { productId: product.id, price: product.price },
      `<button class="btn">Add to Cart — $${product.price}</button>`
    )}
  `;
}

Paramètres :

Paramètre Taper Description
name string Nom du composant. Correspond à /islands/{name}.js sur le client.
props Record<string, any> Accessoires sérialisables transmis au composant client.
fallback string HTML statique affiché avant l'hydratation (et si JS est désactivé).

Sortie HTML :

html
<bext-island data-component="AddToCart" data-props='{"productId":42,"price":29.99}'>
  <button class="btn">Add to Cart — $29.99</button>
</bext-island>

islandScript(basePath?)#

Générez le script d'hydratation côté client. Incluez-le une fois dans votre modèle, généralement avant </body> :

typescript
import { islandScript } from "@bext-stack/framework";

function renderPage(page: Page, ctx: RenderContext): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <title>${page.title}</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <main>${page.html}</main>

  ${islandScript()}
</body>
</html>`;
}

Sortie par défaut :

html
<script type="module">
document.querySelectorAll('bext-island').forEach(async function(el){
  var n=el.dataset.component,p=JSON.parse(el.dataset.props||'{}');
  try{var m=await import('/islands/'+n+'.js');m.mount(el,p);}catch(e){console.warn('Island '+n+':',e);}
});
</script>

Chemin de base personnalisé :

typescript
islandScript("/assets/components")
// Loads from /assets/components/{name}.js instead of /islands/{name}.js

Écriture des composants de l'îlot#

Chaque îlot est un module JavaScript autonome dans votre répertoire public/islands/. Il doit exporter une fonction mount(el, props) :

Île Vanille JS#

javascript
// public/islands/Counter.js
export function mount(el, props) {
  let count = props.initial ?? 0;
  const display = el.querySelector('.count');
  const incBtn = el.querySelector('.inc');
  const decBtn = el.querySelector('.dec');

  function update() {
    display.textContent = count;
  }

  incBtn.addEventListener('click', () => { count++; update(); });
  decBtn.addEventListener('click', () => { count--; update(); });
}

Côté serveur :

typescript
island("Counter", { initial: 0 }, `
  <div class="counter">
    <button class="dec">-</button>
    <span class="count">0</span>
    <button class="inc">+</button>
  </div>
`)

Îlot Preact#

javascript
// public/islands/SearchBox.js
import { h, render } from 'https://esm.sh/preact';
import { useState } from 'https://esm.sh/preact/hooks';

function SearchBox({ placeholder, endpoint }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  async function handleInput(e) {
    const q = e.target.value;
    setQuery(q);
    if (q.length < 2) { setResults([]); return; }
    const res = await fetch(`${endpoint}?q=${encodeURIComponent(q)}`);
    setResults(await res.json());
  }

  return h('div', { class: 'search' },
    h('input', { type: 'text', placeholder, value: query, onInput: handleInput }),
    h('ul', null, results.map(r =>
      h('li', { key: r.id }, h('a', { href: r.url }, r.title))
    ))
  );
}

export function mount(el, props) {
  render(h(SearchBox, props), el);
}

Îlot React#

javascript
// public/islands/DataGrid.js
import React from 'https://esm.sh/react';
import ReactDOM from 'https://esm.sh/react-dom/client';

function DataGrid({ columns, endpoint }) {
  const [rows, setRows] = React.useState([]);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    fetch(endpoint)
      .then(r => r.json())
      .then(data => { setRows(data); setLoading(false); });
  }, [endpoint]);

  if (loading) return React.createElement('div', null, 'Loading...');

  return React.createElement('table', { className: 'data-grid' },
    React.createElement('thead', null,
      React.createElement('tr', null,
        columns.map(col => React.createElement('th', { key: col }, col))
      )
    ),
    React.createElement('tbody', null,
      rows.map((row, i) =>
        React.createElement('tr', { key: i },
          columns.map(col => React.createElement('td', { key: col }, row[col]))
        )
      )
    )
  );
}

export function mount(el, props) {
  const root = ReactDOM.createRoot(el);
  root.render(React.createElement(DataGrid, props));
}

Îlot de signaux bext#

Même composant que le React DataGrid ci-dessus, écrit sous forme d'îlot de signaux. Aucun lot de React ; pas de nouveau rendu lors de la mise à jour. L'hydratation s'exécute une fois et se lie via les commentaires du marqueur.

tsx
// src/components/DataGrid.tsx
"use signals";
/** @jsxImportSource @bext-stack/framework/signals */

import { signal, computed, effect, List } from "@bext-stack/framework/signals";

export default function DataGrid(props: { columns: string[]; endpoint: string }) {
  const rows = signal<any[]>([]);
  const loading = signal(true);

  // Effects run on hydrate (once) — fetch the data and update the
  // signals. The reactive markers in the JSX below pick up the new
  // values automatically; no re-render of the table shell.
  effect(() => {
    fetch(props.endpoint)
      .then((r) => r.json())
      .then((data) => {
        rows.value = data;
        loading.value = false;
      });
  });

  return (
    <div>
      {loading.value ? <p>Loading…</p> : null}
      <table>
        <thead>
          <tr>{props.columns.map((c) => <th>{c}</th>)}</tr>
        </thead>
        <tbody>
          <List
            each={rows}
            key={(_, i) => i}
            render={(row) => (
              <tr>{props.columns.map((c) => <td>{row[c]}</td>)}</tr>
            )}
          />
        </tbody>
      </table>
    </div>
  );
}
Conseil

Pour les petits widgets interactifs (compteurs, bascules, listes simples), préférez le chemin des signaux - il est livré ~ 3 Ko au lieu des ~ 45 Ko de React et met à jour le DOM existant sans nouveau rendu. Voir Signaux pour l'API complète.

Montez via signalsIsland("DataGrid", DataGrid, { columns, endpoint }) sur une page PRISM. Le pipeline de construction découvre automatiquement la directive "use signals" et émet un bundle par île qui importe hydrateSignalsIsland. Consultez le page des signaux pour les primitives, la passe de compilation d'auto-wrap et les détails de réconciliation <List>.

Zéro JS pour les pages non interactives#

Si une page n'a pas d'appels island(), le islandScript() s'affiche toujours mais ne fait rien : querySelectorAll('bext-island') renvoie une NodeList vide et le corps de la boucle ne s'exécute jamais. Le script lui-même est réduit d'environ 200 octets.

Pour les pages garanties sans îlots, vous pouvez inclure le script sous condition :

typescript
function renderPage(page: Page, ctx: RenderContext): string {
  const hasIslands = page.html.includes("bext-island");

  return `<!DOCTYPE html>
<html lang="en">
<head><title>${page.title}</title></head>
<body>
  <main>${page.html}</main>
  ${hasIslands ? islandScript() : ""}
</body>
</html>`;
}

Amélioration progressive#

Les îles sont conçues pour une amélioration progressive. Le HTML de secours rendu par le serveur est entièrement fonctionnel sans JavaScript :

typescript
// Server: render a working form as fallback
island("ContactForm", { endpoint: "/api/contact" }, `
  <form action="/api/contact" method="POST">
    <label>Name <input type="text" name="name" required></label>
    <label>Email <input type="email" name="email" required></label>
    <label>Message <textarea name="message" required></textarea></label>
    <button type="submit">Send</button>
  </form>
`)
javascript
// Client: enhance with AJAX submission and validation
// public/islands/ContactForm.js
export function mount(el, props) {
  const form = el.querySelector('form');
  const button = el.querySelector('button');

  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    button.disabled = true;
    button.textContent = 'Sending...';

    const data = Object.fromEntries(new FormData(form));
    const res = await fetch(props.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });

    if (res.ok) {
      el.innerHTML = '<p class="success">Message sent! We\'ll be in touch.</p>';
    } else {
      button.disabled = false;
      button.textContent = 'Send';
      alert('Failed to send. Please try again.');
    }
  });
}

Sans JavaScript : Le formulaire est soumis normalement via POST, le serveur le traite et renvoie une page de réponse.

Avec JavaScript : Le formulaire est soumis via récupération, affiche un état de chargement et affiche un retour de réussite/erreur en ligne.

Exemple complet#

Une page de liste de produits avec un îlot de recherche et des îlots d'ajout au panier :

typescript
// server/pages/products.ts
import { island, islandScript } from "@bext-stack/framework";
import type { Page } from "@bext-stack/framework";
import { dbQuery } from "@bext-stack/framework/bridge";

export function getProductsPage(): Page {
  const { rows: products } = dbQuery("./shop.db", "SELECT * FROM products WHERE active = 1");

  const productCards = products!.map((p: any) => `
    <div class="product-card">
      <img src="/images/${p.slug}.jpg" alt="${p.name}" loading="lazy">
      <h3>${p.name}</h3>
      <p class="price">$${p.price}</p>
      ${island("AddToCart", { id: p.id, name: p.name, price: p.price },
        `<button class="btn-add">Add to Cart</button>`
      )}
    </div>
  `).join("");

  return {
    title: "Products",
    description: "Browse our product catalog",
    html: `
      <h1>Products</h1>

      ${island("ProductSearch", { endpoint: "/api/products/search" },
        `<input type="search" placeholder="Search products..." class="search-input">`
      )}

      <div class="product-grid">
        ${productCards}
      </div>
    `,
  };
}
typescript
// server/template.ts
import { islandScript, stylesheet } from "@bext-stack/framework";
import type { Page, RenderContext } from "@bext-stack/framework";

export function renderPage(page: Page, ctx: RenderContext): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${page.title} — My Shop</title>
  ${stylesheet("/styles.css")}
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/products">Products</a>
    ${ctx.authHtml}
  </nav>
  <main>${page.html}</main>
  ${islandScript()}
</body>
</html>`;
}

Structure du projet#

code
my-shop/
├── server/
│   ├── entry.ts          # createSite()
│   ├── pages/
│   │   └── products.ts   # Server-rendered product page
│   └── template.ts       # HTML shell with islandScript()
├── public/
│   ├── islands/
│   │   ├── AddToCart.js   # Client-side cart interactions
│   │   └── ProductSearch.js  # Client-side search with autocomplete
│   ├── styles.css
│   └── images/
└── bext.config.toml

Profil de charge utile illustratif#

La taille de la charge utile dépend du runtime et du composant de l’îlot choisi. Les valeurs ci-dessous décrivent cet exemple, et non la fusillade canonique SSR ; pour les numéros actuels de rendu uniquement, voir Performances PRISM.

Métrique Valeur
Temps de rendu du serveur Dépend de la page ; 15,7 µs compilés / 75,3 µs automatique sur le luminaire canonique de 38,3 Ko
Charge utile HTML initiale ~8 Ko (pas de framework JS)
Île JS chargée ~3 Ko par île ( Preact ) ou ~0,5 Ko (vanille)
Il est temps de passer à l'interactivité < 100 ms (les îles s'hydratent indépendamment)
Pages sans îles 0 Ko JavaScript

En rapport#

  • Cadre PRISM — le framework de serveur qui restitue les espaces réservés des îlots
  • Signaux — modèle d'hydratation alternatif léger (~ 3 Ko, pas de React)
  • Exécution client — Navigation de type SPA et rechargement en direct pour la même page
  • Actions du serveur — Mutations "use server" qui complètent les îles
  • React, Preact et solide — utilisation des bibliothèques de framework dans l'îlot mount()
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.