El proyecto que utilizaremos para el siguiente artículo es Palaver Sample
Palaver utiliza varios frameworks:
- Componentes web nativos sin marco
- Stencil.js (1.3.0)
- Angular Elements (9.1.7)
- LitElement (2.2.1)
- React (16.9)
- Vue.js (2.6.10)
Ejemplo de proyecto: Estructura del proyecto
Antes de sumergirnos en cómo crear componentes web con cada uno de los frameworks antes mencionados, echemos un vistazo a la estructura general del proyecto. El siguiente árbol de carpetas muestra una visión general de alto nivel de Palaver.
palaver
|-- docker-build-all.sh
|-- docker-push-all.sh
|-- npm-i.sh
|-- backend
|-- frontend
|-- package.json
|-- apps
| |-- ng-chat-app
|-- dist
|-- web-components
|-- angular-chat-window
|-- lit-element-chat-link
|-- native-web-components
|-- react-contact-list
|-- stencil-components
|-- vue-login
Puedes pensar en este proyecto como un mono repositorio para varios proyectos individuales. Para ello, necesitas algunos scripts para automatizar las tareas. Puedes hacerlo directamente con scripts de shell, como hicimos nosotros, o puedes utilizar herramientas especializadas como lerna o lage.
El npm-i.sh
instalará todos los paquetes npm para cada proyecto de una sola vez. Los scripts docker-build-all.sh
y docker-push-all.sh
construyen los artefactos públicos del proyecto usando Docker y un Dockerfile
.
Esto es el backend (Node.js) y el frontend Angular ubicado en frontend /apps/ng-chat-app
. Ninguno de los proyectos de Web Components está publicado y alojado en algún lugar. Eso también sería un escenario si tiene sentido para tu caso de uso. Sin embargo, en la demostración, decidimos desplegar sólo los proyectos de cara al público como se mencionó.
Backend
La carpeta backend contiene el backend de todo el proyecto. Está escrito en JavaScript puro y utiliza socket.io para la parte de comunicación en tiempo real para el chat. También contiene un Dockerfile necesario para construir y ejecutar el backend, que se despliega en una Azure Web App for Containers.
Frontend
La carpeta frontend contiene todos los proyectos de Web Component, así como la aplicación anfitriona, que reúne todos los Web Components y proporciona los servicios necesarios para acceder al backend.
El archivo frontend/package.json
contiene las tareas para construir cada proyecto de Web Component individualmente y envía sus artefactos de construcción a la carpeta dist. Elimina la carpeta dist
antes de copiar los artefactos de construcción, por lo que no se conservarán los archivos antiguos y no utilizados.
La carpeta web-components
contiene todos los proyectos de Web Component. Cada carpeta es un proyecto en sí mismo, como usted, lo esperaría en un único repositorio de proyectos. Contienen su propio package.json
, .gitignore
, README.md
, y más. De hecho, usted sería capaz de tomar cada carpeta y ponerlos en su propio repositorio, si lo desea.
Comandos comunes para proyectos de componentes web
Cada proyecto de Web Component tiene un comando npm start
y npm run
build-wc
. El primero iniciará el entorno de desarrollo de cada Web Component.
La idea es desarrollar cada Web Component en un entorno sandbox, y posteriormente construirlo para su uso en otras aplicaciones. Eso hace que sea fácil y rápido iterar un solo Web Component sin tener que ejecutar todo un proyecto que los utilice.
Por ejemplo, si ejecutas npm start
en la carpeta react-contact-list
, se inicia el entorno de desarrollo de React por defecto. Si ejecutas npm start
en la carpeta angular-chat-window
, se inicia el entorno de desarrollo predeterminado de Angular CLI.
El segundo comando, npm run build-wc
, crea el componente web real.
Por ejemplo, en el angular-chat-window
, envolverá el componente Angular con Angular Elements
. El componente Vue.js se envuelve con @vue/web-component-wrapper
.
Es crucial saber, que el comando de construcción actual construye el componente web para la producción. Esto significa que no hay ningún mapa de fuentes, lo que podría dificultar la depuración del componente web cuando se utilice en la aplicación final. Sin embargo, esto es sólo una parte de esta aplicación de demostración. Si hay necesidad de construir una versión de depuración de los Web Components, el package.json
necesita ser extendido con esas tareas. Los pull requests son bienvenidos 🙂 .
Además, en frontend/package.json
también hay un comando npm run build-wc
. Este comando construirá todos los proyectos individuales de Web Component y copiará los artefactos de construcción en frontend/dist
.
Comandos comunes para proyectos de aplicaciones
La carpeta frontend/apps
contiene todas las aplicaciones de cara al público. En el momento de escribir este artículo, sólo contiene un proyecto típico de Angular (CLI).
El package.json
contiene un comando adicional npm run wc
. Ese comando copiará todos los artefactos de construcción de frontend/dist
en frontend/apps/ng-chat-app/assets/web-components
. Igualmente, es una simulación de la carpeta node_modules
.
En un proyecto típico, donde varios equipos desarrollan varias apps y proyectos de Web Components, es probable, que los artefactos de compilación se publiquen como un paquete npm.
Flujo de trabajo
Para el flujo de trabajo, por lo general npm iniciar en las aplicaciones (por ejemplo, frontend/apps/ng-chat-app
) y luego npm iniciar en los componentes web que desea trabajar. Ten en cuenta que todos los proyectos de Web Components en Palaver están alojados en su propio puerto, así que puedes iniciarlos todos a la vez si quieres.
Cuando hayas hecho los cambios en tu Web Component, haz cd frontend y ejecuta npm run build-wc
&& (cd apps/ng-chat-app && npm run wc). El comando primero construirá todos los componentes web (de hecho, podrías construir cada uno individualmente, sólo tienes que echar un vistazo a frontend/package.json), y luego copiar todos los componentes en la aplicación.
Actualmente, no hay ningún trabajo de vigilancia o algo que lo haga automáticamente. Dependiendo de cómo se trabaje, no sería necesario, ya que los componentes web se desarrollan dentro del propio entorno sandbox y definiendo una interfaz (por ejemplo, atributos HTML).
Si se hace, entonces los componentes web se construyen y se copian. Sin embargo, tener un trabajo de vigilancia desplegaría automáticamente los Web Components modificados en las aplicaciones, por lo que su mecanismo de recarga se encargará entonces. Lo mismo aquí: Pull requests son bienvenidos 🙂 .
Creando Componentes Web con los 'Tres Grandes'
Después de entender la estructura, los comandos y el flujo de trabajo de Palaver, vamos a ver cómo crear Web Components cuando se utiliza un framework SPA moderno. En esta sección estamos cubriendo la creación de Web Components con Angular, React, y Vue.
Descargo de responsabilidad: Soy un desarrollador de Angular, ni de React ni de Vue. Por ello, el código mostrado para React o Vue tendrá margen de mejora. Si tienes buenas sugerencias sobre cómo mejorarlo, házmelo saber. ¡Estaré encantada de actualizar el código así como este artículo!
Elementos de Angular
Para crear un Web Component con Angular, debes utilizar Angular Elements. Te permite envolver cualquier componente que hayas creado en un Web Component. Para ello, sólo tienes que ajustar tu AppModule
(o como se llame tu módulo raíz de Angular) o crear una biblioteca para tus Web Components. En Palaver, estamos utilizando el AppModule
.
Aquí está el código relevante para crear un Web Component:
// other imports ...
import { createCustomElement } from '@angular/elements';
@NgModule({
declarations: [
AppComponent, // Debug
ChatWindowComponent,
TimestampFormatPipe,
ChatMessageComponent,
MessageOrderByTimestampPipe,
],
imports: [ BrowserModule ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA], // Debug
bootstrap: environment.production ? [] : [AppComponent],
entryComponents: [ChatWindowComponent],
})
export class AppModule implements DoBootstrap {
constructor(private readonly injector: Injector) {
}
ngDoBootstrap() {
if (environment.production) {
const chatWindowCustomElement = createCustomElement(ChatWindowComponent, { injector: this.injector });
window.customElements.define('angular-chat-window', chatWindowCustomElement);
}
}
}
Las líneas marcadas con // Debug
sólo son necesarias ya que utilizamos el mismo módulo para la creación del Web Component así como para el entorno de demostración. Si creas una biblioteca que sólo contenga los componentes para los Web Components puedes eliminar con seguridad las líneas marcadas.
Como puedes ver en el ejemplo de código, declaras un módulo típico de Angular con sus declaraciones, importaciones y posibles proveedores. Luego, tienes que adaptar la propiedad bootstrap, si utilizas un módulo para la generación de demos y Web Component.
En la propiedad bootstrap, se arranca el AppComponent
si el entorno no es de producción. El AppComponent
se utiliza para desarrollar el ChatWindowComponent
. En un entorno de producción, el módulo no arrancará un componente, de ahí la matriz vacía.
El siguiente paso es definir un componente de entrada que Angular pueda cargar imperativamente, debido a que este componente no es referenciado en otra parte del código. Recuerda que en producción el AppComponent
no es parte de la salida final, por eso el ChatWindowComponent
no es referenciado también. Eso llevaría a eliminar su código en el proceso de agitación del árbol. Sin embargo, queremos mantener ese componente, por lo que tenemos que definirlo como un componente de entrada.
Además, tenemos que implementar la interfaz DoBootstrap
. En la implementación de la interfaz llamamos a createCustomElement
, que se importa de @angular/elements
. El primer parámetro define el componente de Angular que debe ser envuelto como un Web Component.
El segundo parámetro define un objeto de opción, que quiere que el inyector se establezca. Sin él, puedes usar cualquier cosa que requiera la inyección de un token, como los servicios. El resultado de createCustomElement
es una clase, que puede ser usada para crear el Componente Web real usando el customElements.define-API del navegador
.
Hemos creado un Web Component utilizando Angular Elements. También es posible definir más de un Web Component por módulo. Sólo hay que repetir el código para cada componente de Angular, que debe ser envuelto en un Componente Web.
Proceso de construcción
Por defecto, si construyes la aplicación Angular a través del CLI, obtienes varios archivos de salida, como main.js
, vendor.js
, etc. Para una distribución más fácil, un solo archivo sería mejor. Podríamos utilizar ngx-build-plus
o ir a un enfoque manual simple. Para un proyecto real, sugerimos echar un vistazo a ngx-build-plus
. Para Palaver, y para ensuciarse las manos, hemos decidido optar por un enfoque manual.
Echando un vistazo al frontend/web-components/angular-chat-window/package.json
, hay dos tareas, que crean un único archivo para su distribución:
{
"scripts": {
"build-wc": "ng build --prod && npm run build-wc:bundle",
"build-wc:bundle": "cat dist/angular-chat-window/runtime.*.js > dist/angular-chat-window.js && cat dist/angular-chat-window/polyfills.*.js >> dist/angular-chat-window.js && cat dist/angular-chat-window/main.*.js >> dist/angular-chat-window.js"
}
}
El script build-wc
construye una versión de producción del proyecto. Luego llama a build-wc:bundle
, que simplemente fusiona el archivo runtime.js
, polyfills.js
y main.js
en angular-chat-window.js
.
Ten en cuenta que ni este proceso de compilación ni ngx-build-plus
procesan archivos de activos adicionales que no forman parte de la compilación (por ejemplo, si los pone en la carpeta de activos). si necesitas los activos también, debes copiarlos manualmente o comprobar si puedes incluirlos en la salida compilada a través de un cargador de webpack.
Ahora, nuestro componente web de Angular Elements está listo para ser utilizado.
Vue
¿Cómo crear un Web Component con Vue? Vue proporciona un @vue/web-component-wrapper
para la creación de Web Components. Es realmente sencillo de usar.
Todo lo que tienes que hacer es adaptar tu proceso de construcción. Para un componente típico de Vue, no hay nada más que hacer. Sin embargo, deberías echar un vistazo a los detalles de proxy de la interfaz, que enumera algunas cosas especiales que hace Vue cuando envuelve un componente (por ejemplo, el casting de valores booleanos y numéricos a su tipo).
Para el proceso de construcción en sí, simplemente necesitas una tarea como esta (ver frontend/web-components/vue-login/package.json)
:
Con la instalación de @vue/web-component-wrapper
tu servicio vue-cli
obtiene un nuevo objetivo llamado wc. Así que simplemente llama al servicio de compilación, con --target wc
, especifica el --name vue-login
(que es la etiqueta HTML de Web Component más adelante para su uso) y apunta al archivo de componente *.vue
que quieres envolver. En nuestro caso es el archivo ./src/components/LoginForm.vue
. Por defecto, la compilación de vue-cli-service
no incluye el propio framework Vue en la salida de la compilación, por lo que el host necesita cargar Vue. Si quieres cambiar esto para tener una versión de despliegue de un solo archivo de tu componente, necesitas especificar --include-vue
.
Después de ejecutar el comando, se obtiene un único archivo JavaScript llamado vue-login.js
, que registra un Web Component .
React
El uso de Web Components en React funciona bien.
Así que, si quieres crear un Web Component desde React, hay varios proyectos de la comunidad que intentan proporcionar un wrapper. En el momento de la creación de Palaver, ninguno de ellos funcionaba bien. Por eso hemos decidido crear nuestro propio wrapper, que se puede encontrar en frontend/web-components/react-contact-list/src/custom-element.js
.
Konstantin Denerz y Manuel Rauber han trabajado en ese wrapper. Está lejos de estar listo para la producción y tiene sus defectos, pero funcionó lo suficientemente bien para las demostraciones. Recordar que ambos no son desarrolladores de React. Estoy seguro de que alguien con un conocimiento más profundo de React sería capaz de resolver nuestros defectos actuales.
Envoltura de elementos personalizados
Echemos un vistazo al archivo custom-element.js
:
import React from 'react';
import ReactDOM from 'react-dom';
export default function defineElement(Component, elementName, observedAttributes = [], events = []) {
class CustomElement extends HTMLElement {
constructor() {
super();
observedAttributes.forEach(property => Object.defineProperty(this, property, { set: value => this.setterProxy(property, value) }));
this.events = events;
}
setterProxy(name, value) {
this.attributeChangedCallback(name, value, value); // Careful, this is a bug, since the oldVal always equals the new val
}
connectedCallback() {
const props = [...this.attributes].reduce((props, attribute) => ({ ...props, [attribute.name]: attribute.value }),
{ root: this });
const instance = (<Component {...(props)} />);
this.assignEvents(instance);
ReactDOM.render(instance, this);
this.instance = instance;
this.props = props;
}
attributeChangedCallback(name, oldValue, newValue) {
const { instance } = this;
if (!instance) return;
const newProps = { ...(this.props), ...({ [name]: newValue }) };
const newInstance = (<Component {...(newProps)} />);
this.assignEvents(newInstance);
ReactDOM.render(newInstance, this);
this.instance = newInstance;
this.props = newProps;
}
assignEvents(instance) {
this.events.forEach(event => instance.props[event] = eventArgs => this.dispatchEvent(new CustomEvent(event, { detail: eventArgs })));
}
}
CustomElement.observedAttributes = observedAttributes;
window.customElements.define(elementName, CustomElement);
}
Ese monstruo de archivo es básicamente lo que hay detrás de Angular Elements y @vue/web-component-wrapper
. Vamos a recorrer este archivo de arriba a abajo.
Al principio, creamos una nueva función defineElements
. Necesita varios parámetros. El primero es el componente React que debe ser envuelto en un Web Component. El segundo es el nombre del Web Component. Los dos últimos parámetros opcionales definen los atributos que deben ser observados, y los eventos que deben ser vinculados al componente React.
La función defineElementos
entonces crea una nueva clase que extiende de HTMLElement
, como cualquier Componente Web necesita hacer. En el constructor de la clase, iteramos a través de los observedAttributes
y los definimos mediante Object.defineProperty
en la propia clase. Sin embargo, hacemos un proxy de la función set, que se llama cuando se establece una propiedad en la clase. Dentro del proxy, llamamos a this.setterProxy
que luego invoca la API attributeChangedCallback
del componente web. Aquí está también la primera desventaja: el attributeChangedCallback
es normalmente invocado con el nombre de la propiedad cambiada, su valor antiguo y su nuevo valor. Sin embargo, en este wrapper, no se rastrea el valor antiguo, por lo que llamamos al callback sólo con el nuevo valor.
Después, se define el connectedCallback
que es una API del navegador, que se ejecuta cuando el Web Component se coloca en el DOM. En la función, creamos un objeto con todos los atributos definidos en el elemento, por lo que estarán vinculados al componente React. Ten en cuenta que esto vincula cualquier atributo definido en el Web Component al componente React.
A continuación, creamos el componente React real con la sintaxis JSX const instance = (<Componente {...(props)} />)
; y colocamos todos los atributos en el componente. Luego, asignamos todos los eventos definidos en el componente usando el método assignEvents
. En este método, simplemente iteramos a través de los eventos y creamos un manejador de eventos en los props de React. El manejador simplemente invocará la API dispatchEvent
y creará un CustomEvent
. Con ello, podemos usar eventos como estamos acostumbrados en React y el wrapper lo transforma en un evento personalizado. Después de asignar los eventos, usamos ReactDOM.render
para renderizar el componente real en el DOM.
Ahora, tenemos que ocuparnos de si los atributos cambian en el componente web. Para ello, necesitamos implementar attributeChangedCallback
. Aquí, y este es el siguiente fallo, simplemente recreamos el componente React. Sería mucho mejor si se pudiera reutilizar el componente en lugar de volver a crearlo cuando cambia una sola propiedad. Esta es la parte en la que alguien con más conocimiento de React podría mejorar este wrapper significativamente.
Por último, pero no menos importante, tenemos que establecer el static observedAttributes
en la clase, que es la API del navegador para saber, qué atributo debe observar e invocar el attributeChangedCallback
. Entonces, finalmente podemos usar customElements.define
para definir el elemento personalizado.
Bien hecho. ¡Hablemos del proceso de construcción!
Proceso de construcción
Como en todos los proyectos, el frontend/web-components/react-contact-list/package.json
define varios scripts:
{
"scripts": {
"build-wc": "npm run generate-wc && npm run package-wc",
"generate-wc": "cp src/WebComponents.js src/index.js && GENERATE_SOURCEMAP=false react-scripts build",
"package-wc": "rm -rf dist && mkdir dist && cat build/static/js/runtime-main.*.js > dist/react-contact-list.js && cat build/static/js/*.chunk.js >> dist/react-contact-list.js && cp build/static/css/main.*.css dist/react-contact-list.css"
}
}
El primer script build-wc
sólo llama a otros dos scripts de forma secuencial. El script generate-wc
compila los Web Components. El proyecto utiliza react-scripts
para la construcción. Desgraciadamente, react-scripts
tiene un punto de entrada predefinido src/index.js
que no se puede cambiar fácilmente (una posibilidad sería expulsar todo el react-script
o utilizar un paquete de la comunidad que haga cosas extrañas). Así que, para mantenernos sencillos, copiamos un src/WebComponents.js
y anulamos el src/index.js
existente, para que nuestro script pueda ser utilizado como punto de entrada. Nos ocuparemos del src/WebComponents.js
en un momento. Después de copiar, llamamos a react-scripts build para ejecutar el proceso de construcción.
El siguiente script package-wc
es básicamente el mismo que hemos visto en la sección de Angular Elements. Fusiona los archivos generados en un único archivo para su despliegue. En nuestro caso, fusiona todos los archivos JavaScript en react-contact-list.js
y todos los archivos CSS en react-contact-list.css
.
Para terminar la parte de React, tenemos que echar un vistazo al archivo src/WebComponents.js
, que es tan simple como aquí:
import defineElement from './custom-element';
import { ContactList } from './contact-list/ContactList';
defineElement(ContactList, 'react-contact-list', ['headerText', 'contacts'], ['onContactSelected']);
Sólo utiliza el wrapper de arriba y el componente React, que debe ser envuelto. Luego llama a defineElement
con los parámetros, atributos y eventos necesarios.
Acabamos de crear un componente web que funciona con React.
Conclusión
En este artículo, hemos visto cómo tres grandes y modernos frameworks SPA pueden ser utilizados para crear Web Components.
Algunos de ellos tienen un soporte directo para hacerlo, mientras que otros frameworks necesitan algo más de dedicación. Sin embargo, es bueno que podamos utilizar cualquiera de los frameworks y utilizar las herramientas que nos gustan para dar un paso más en los Web Components.
Pero, usar un framework SPA para crear Web Components tiene el coste del tamaño del archivo. Para asegurarse de que, dentro de su componente puedes utilizar el marco como se espera, cada Web Components necesita enviar el marco SPA en sí mismo (recuerda --inline-vue
).
Los desarrolladores de los frameworks hacen un buen progreso para conseguir que sus frameworks sean más pequeños con mejores procesos de arbolado. Todavía tomará algún tiempo más hasta que un Web Component construido por un framework SPA sea lo suficientemente pequeño como para tener muy poco impacto en los tiempos de descarga y análisis.
Hay otras técnicas para contrarrestar esto, por ejemplo no incluyendo el framework SPA en cada Web Component, de modo que el host tenga que cargar cada framework una vez.
Si todavía piensas, que la salida produce es demasiado grande para su caso de uso, es posible que desee echar un vistazo más de cerca a las herramientas como StencilJS o LitElement.
Por ejemplo, Palaver con todos sus frameworks utilizados, actualmente tiene casi 12 MB. Eso es demasiado para una aplicación tan sencilla, pero algún día lo conseguiremos.
¡Feliz Codificación!
s