Integrar un framework de vistas
Qué hace un controlador de widget por un framework de vistas, mostrado con un widget escrito sin ninguno, en HTML simple, con un controlador de cliente y uno de servidor; luego cómo hacen lo mismo los adaptadores React 19, Vue y Svelte suministrados, y qué debe asumir un adaptador nuevo.
- Disponibilidad: Experimental
- Evidencia: Ejecución registrada
- Guía práctica
Compilar y renderizar son dos extensiones
Un procesador de Beyond Packages le enseña al compilador un formato de fuente: el bundler ts suministrado compila fuentes .ts/.tsx, componentes de archivo único .vue y .svelte, y hojas de estilos .css/.scss. Un adaptador de controlador le enseña a Widgets cómo montar, hidratar, refrescar y desmontar una vista de un framework. Un framework escrito en JavaScript simple necesita solo el adaptador; una sintaxis de componentes necesita también un procesador. Lee Crear un bundler o un procesador para el lado del compilador; esta página es el lado del controlador.
Un widget sin framework de vistas
El módulo declara un punto de entrada por plataforma, y ambos puntos de entrada exportan un Controller:
{
"platforms": ["web", "node"],
"widget": {
"element": { "name": "hello-html", "attrs": ["label"] }
},
"conditionals": {
"web": { "entry": "client.ts", "excludes": ["server.ts"] },
"node": { "entry": "server.ts", "excludes": ["client.ts"] }
}
}El marcado es una sola función usada en ambos lados, para que el navegador pueda conservar lo que el servidor renderizó:
import { label } from '@testbed/boundaries/shared';
/**
* The markup of the widget, written the same way by the server and by the browser, so that the browser
* can keep what the server rendered and only attach its behavior to it
*/
export const markup = (title: string, count: number): string =>
`<div class="html"><h2 class="title">${title}</h2><p class="shared-label">${label('HTML count', count)}</p><button class="counter">Add</button></div>`;El controlador de cliente extiende WidgetClientController. Todo lo que hace un adaptador de framework se ve aquí: mount escribe el marcado en el holder del elemento salvo que ya haya marcado del servidor, enlaza dentro de la raíz las hojas de estilos que reunió el administrador de estilos e informa cada una al cargarse, y conecta su comportamiento; unmount libera el listener, la suscripción y el marcado.
import { WidgetClientController } from '@beyond-js/widgets/controller';
import { markup } from './markup';
/**
* A widget without a view framework: the controller writes its markup into the holder of the element and
* attaches its behavior with plain DOM APIs. It shows what a framework adapter does for a widget: mount the
* view, adopt the stylesheets the styles manager collects, keep server markup on hydration, refresh after
* an update of the module and release everything on unmount.
*/
export class Controller extends WidgetClientController {
#count = 0;
#listener: () => void;
#mounted = false;
get mounted() {
return this.#mounted;
}
get #holder(): HTMLElement {
return (<any>this.widget).holder;
}
get #title(): string {
return this.attributes.get('label') ?? '';
}
/**
* A link for each stylesheet of the widget, inside its root, reported to the manager as it loads
*/
#stylesheets = () => {
const root = this.#holder.parentNode as ShadowRoot;
for (const href of this.styles.resources.keys()) {
if (root.querySelector(`link[href="${href}"]`)) continue;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = () => this.styles.onloaded(href);
link.onerror = () => this.styles.onerror(href);
root.insertBefore(link, this.#holder);
}
};
mount() {
if (this.#mounted) return;
this.#mounted = true;
const holder = this.#holder;
this.#stylesheets();
this.styles.on('change', this.#stylesheets);
// Server markup is kept as it is; a fresh mount writes the markup itself
holder.children.length || (holder.innerHTML = markup(this.#title, this.#count));
holder.style.display = '';
this.#listener = () => {
this.#count++;
holder.querySelector('.shared-label').textContent = markup(this.#title, this.#count).match(/<p class="shared-label">([^<]*)<\/p>/)[1];
};
holder.querySelector('button.counter').addEventListener('click', this.#listener);
}
unmount() {
if (!this.#mounted) return;
this.#mounted = false;
this.styles.off('change', this.#stylesheets);
this.#holder.querySelector('button.counter')?.removeEventListener('click', this.#listener);
this.#holder.innerHTML = '';
}
}El controlador de servidor extiende WidgetServerController y devuelve el mismo marcado como cadena, con un enlace por cada hoja de estilos de las dependencias del módulo:
import { WidgetServerController } from '@beyond-js/widgets/controller';
import type { IWidgetRendered } from '@beyond-js/widgets/controller';
import { markup } from './markup';
/**
* The server side of the widget: the same markup as a string, with a link for each stylesheet the
* dependencies of the module register, which the browser hydrates
*/
export class Controller extends WidgetServerController {
render(props: { attributes?: Map<string, string> }): IWidgetRendered {
const title = props.attributes?.get('label') ?? '';
const links = this.styles.map(href => `<link rel="stylesheet" href="${href}">`).join('');
return { html: `${links}${markup(title, 0)}` };
}
}.title {
color: rgb(124, 45, 18);
margin: 0;
}Lo que el elemento hace alrededor del controlador es igual para todo adaptador:
- La construcción abre el shadow root y comienza a importar el módulo del widget.
- En la primera conexión, las rutas de renderizado en servidor y estático se completan antes de construir el controlador, así que lo que dejan en el holder es lo que
mounthidrata. initialisecrea el store, lo hidrata con datos del servidor, renderiza y se suscribe a las actualizaciones del módulo y a las hojas de estilos de sus dependencias.- Una actualización del módulo, o de un módulo que importa, llama a
refresh, que de forma predeterminada desmonta y vuelve a montar; un adaptador puede hacerlo mejor, como hace React. - La desconexión llama a
disconnect, que desmonta y libera las suscripciones; conectar el elemento de nuevo monta el mismo controlador, con su store y sus atributos, en el mismo holder.
De qué se encargan los adaptadores suministrados
| Adaptador | Montar e hidratar | Refrescar tras una actualización | Desmontar |
|---|---|---|---|
React 19 (@beyond-js/react-19-widgets/base) |
createRoot en un holder vacío, hydrateRoot sobre marcado del servidor |
El componente raíz renderiza de nuevo con la vista actual; el estado sobrevive cuando el módulo de la vista no cambió | root.unmount(), de forma síncrona |
Vue (@beyond-js/vue-widgets/base) |
createApp o createSSRApp, montado en el holder |
El componente raíz identifica la vista por una versión, así que la vista se crea de nuevo | app.unmount() |
Svelte 5 (@beyond-js/svelte-widgets/base) |
mount o hydrate de svelte |
El componente raíz renderiza la vista dentro de {#key version} |
unmount |
Cada adaptador tiene un componente raíz que renderiza los enlaces a las hojas de estilos del widget junto a la vista y muestra la vista cuando las hojas están cargadas, y un controlador de servidor que renderiza la vista a una cadena con react-dom/server, vue/server-renderer o svelte/server.
Escribir un adaptador nuevo
- Encárgate del framework: un controlador de cliente con
mount,unmountyrefresh, y un controlador de servidor conrender(props)que devuelve{ html, errors? }. Mantén una sola identidad pública para ambos, con una entrada por plataforma en el manifiesto, como hacen los adaptadores suministrados. - Monta en
widget.holder, nunca directamente en el shadow root, y conserva el marcado del servidor al hidratar. - Renderiza los enlaces de
styles.resourcesdentro de la raíz, informastyles.onloaded(url)ystyles.onerror(url), y suscríbete astyles.on('change')para los reemplazos. - Libera todo en
unmount: la raíz del framework, los listeners y las suscripciones. El elemento monta el mismo controlador de nuevo al reconectarse. - No hagas del compilador parte del adaptador: una sintaxis de componentes es un procesador del bundler, y su salida debe conservar la identidad pública, las referencias bare, los mapas de código y la hoja de estilos separada del módulo.
Siguiente
Incrusta el widget en una página construida sin Beyond: Incrustar un widget en una página existente. Ejecútalo en un servidor: Crear un entorno de ejecución modular.