Ensamblar una biblioteca de componentes angulares como componentes web

Se están escribiendo muchos artículos sobre Elementos Angulares y se leen informes regularmente. Por ejemplo, ya no necesita implementar varios angulares completos, solo recopile los componentes web y utilícelos en su página.

Pero, por regla general, estos materiales se limitan a considerar una situación bastante utópica: hacemos un proyecto separado, creamos un componente angular, configuramos el proyecto para ensamblar Elementos y, finalmente, compilamos varios archivos JS, conectándolos a una página normal nos dará el resultado deseado. ¡Hurra, el componente funciona! ..

imagen

En la práctica, surge la necesidad de extraer varios componentes de un proyecto angular de trabajo listo para usar, además, preferiblemente para no afectar su desarrollo y uso actuales. Este artículo resultó precisamente debido a una de estas situaciones: no solo quería ensamblar elementos individuales del proyecto, sino hacer que el proceso de compilar toda la biblioteca de IU en Angular en un conjunto de archivos con componentes web nativos.

Preparación del módulo


Primero, recordemos cómo debería ser el módulo para compilar Angular Elements.

@NgModule({ imports: [BrowserModule], entryComponents: [SomeComponent], }) export class AppModule { constructor(readonly injector: Injector) { const ngElement = createCustomElement(SomeComponent, { injector, }); customElements.define('some-component', ngElement); } ngDoBootstrap() {} } 

Necesitamos:

  1. Agregue a la entrada Componentes el componente que planeamos hacer un elemento angular, importe los módulos necesarios para el componente.
  2. Cree un elemento angular usando createCustomElement y un inyector.
  3. Declare un componente web en los elementos personalizados de su navegador.
  4. Anule el método ngDoBootstrap para vaciarlo.

El primer elemento es la designación del componente en sí y sus dependencias, y los tres restantes son el proceso necesario para la aparición del componente web en el navegador. Esta separación le permite colocar la lógica de crear un elemento por separado, en una superclase abstracta:

 export abstract class MyElementModule { constructor(injector: Injector, component: InstanceType<any>, name: string) { const ngElement = createCustomElement(component, { injector, }); customElements.define(`${MY_PREFIX}-${name}`, ngElement); } ngDoBootstrap() {} } 

La superclase puede ensamblar un componente nativo completo del inyector, componente y nombre y registrarlo. El módulo para crear un elemento específico se verá así:

 @NgModule({ imports: [BrowserModule, MyButtonModule], entryComponents: [MyButtonComponent], }) export class ButtonModule extends MyElementModule { constructor(injector: Injector) { super(injector, MyButtonComponent, 'button'); } } 

En este ejemplo, recopilamos el módulo de nuestro botón en los metadatos de NgModule y declaramos el componente de este módulo en entryComponents, y también obtenemos el inyector del mecanismo de inyección de dependencia angular.

El módulo está listo para ensamblar y nos dará un conjunto de archivos JS que se pueden plegar en un componente web separado. De esta forma podemos crear varios módulos y turnarnos para ensamblar componentes web a partir de ellos.

Juntando algunos componentes


Ahora tenemos que configurar el proceso de arranque de los módulos resultantes. Sobre todo, me gusta la idea de poner esta lógica en un archivo ejecutable separado, que será responsable de compilar un módulo en particular.

La estructura de los elementos es más o menos así:

imagen

Un archivo de compilación separado en la versión más simple se vería así:

 enableProdMode(); platformBrowserDynamic() .bootstrapModule(ButtonModule) .catch(err => console.error(err)); 

Este enfoque ayudará a evitar fácilmente los módulos preparados y a mantener la estructura del proyecto con elementos claros y simples.

En la configuración de compilación angular.json, especificamos la ruta del archivo recopilado a una determinada carpeta temporal dentro de dist:

 "outputPath": "projects/elements/dist/tmp" 

Caerá un conjunto de archivos de salida después del ensamblaje del módulo.

Para el ensamblaje en sí, use el comando de compilación habitual en angular-cli:

 ng run elements:build:production --main='projects/elements/src/${project}/${component}/compile.ts' 

Un elemento separado será el producto final, por lo que activamos los indicadores de producción con la compilación anticipada, y luego sustituimos la ruta al archivo ejecutable, que consiste en el proyecto y el nombre del componente.

Ahora recopilaremos el resultado en un archivo separado, que será el paquete final de nuestro componente web separado. Para hacer esto, use el gato habitual:

 cat dist/tmp/runtime.js dist/tmp/main.js > dist/tmp/my-${component}.js 

Es importante tener en cuenta aquí que no colocamos el archivo polyfills.js en el paquete de cada componente, porque obtenemos duplicación si usamos varios componentes en la misma página en el futuro. Por supuesto, debe deshabilitar la opción outputHashing en angular.json.

Transferiremos el paquete resultante de una carpeta temporal a una carpeta para almacenar componentes. Por ejemplo, así:

 cp dist/tmp/my-${component}.js dist/components/ 

Solo queda juntar todo, y el script de compilación está listo:

 // compileComponents.js projects.forEach(project => { const components = fs.readdirSync(`src/${project}`); components.forEach(component => compileComponent(project, component)); }); function compileComponent(project, component) { const buildJsFiles = `ng run elements:build:production --aot --main='projects/elements/src/${project}/${component}/compile.ts'`; const bundleIntoSingleFile = `cat dist/tmp/runtime.js dist/tmp/main.js > dist/tmp/my-${component}.js`; const copyBundledComponent = `cp dist/tmp/my-${component}.js dist/components/`; execSync(`${buildJsFiles} && ${bundleIntoSingleFile} && ${copyBundledComponent}`); } 

Ahora tenemos un padre ordenado con un conjunto de componentes web:

imagen

Conecte componentes a una página normal


Nuestros componentes web ensamblados se pueden insertar de forma independiente en la página, conectando sus paquetes JS según sea necesario:

 <my-elements-input id="input"> </<my-elements-input> <script src="my-input.js"></script> 

Para no arrastrar el zone.js completo con cada componente, lo conectamos una vez al comienzo del documento:

 <script src="zone.min.js"></script> 

El componente se muestra en la página y todo está bien.

Y agreguemos un botón:

 <my-elements-button size="l" onclick="onClick()"></my-elements-button> <script src="my-button.js"></script> 

Lanzamos la página y ...

imagen

¡Oh, está roto!

Si observamos el paquete, encontramos una línea tan discreta:

 window.webpackJsonp=window.webpackJsonp||[] 

imagen

Ventana de parches de Webpack, para no duplicar la carga de los mismos módulos. Resulta que solo el primer componente agregado a la página puede agregarse a elementos personalizados.

Para resolver este problema, necesitamos usar custom-webpack:

  1. Agregue un paquete web personalizado al proyecto con elementos:

    ng add @angular-builders/custom-webpack --project=elements

  2. Configurando angular.json:

     "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { "path": "./projects/elements/elements-webpack.config.js" }, ... 

  3. Cree un archivo de configuración de paquete web personalizado:

     module.exports = { output: { jsonpFunction: 'myElements-' + uuidv1(), library: 'elements', }, }; 

    En él, necesitamos generar una identificación única para cada ensamblaje de cualquier manera conveniente. Me aproveché de uuid.

Puede volver a ejecutar el script de compilación: los nuevos componentes se llevan bien en la misma página.

Trayendo belleza


Nuestros componentes usan variables CSS globales que especifican el tema de color y el tamaño de los componentes.

En aplicaciones angulares que usan nuestra biblioteca, se colocan en el componente raíz del proyecto. Con componentes web independientes, esto no es posible, así que solo compile los estilos y conéctese a la página donde se usan los componentes web.

 // compileHelpers.js compileMainTheme(); function compileMainTheme() { const pathFrom = `../../main-project/styles/themes`; const pathTo = `dist/helpers`; execSync( `lessc ${pathFrom}/theme-default-vars.less ${pathTo}/main-theme.css`,; ); } 

Usamos less, así que solo compilamos nuestras variables lessc y colocamos el archivo resultante en la carpeta de ayudantes.

Este enfoque le permite controlar el estilo de todos los componentes de la página web sin la necesidad de volver a compilarlos.

Guión final


De hecho, todo el proceso de ensamblar los elementos descritos anteriormente se puede reducir a un conjunto de acciones:

 #!/bin/sh rm -r -f dist/ && mkdir -p dist/components && node compileElements.js && node compileHelpers.js && rm -r -f dist/tmp 

Solo queda llamar a este script desde el paquete principal.json para reducir todo el proceso de compilación de los componentes angulares reales para el lanzamiento de un solo comando.

Todos los scripts descritos anteriormente, así como demostraciones para usar componentes angulares y nativos, se pueden encontrar en github .

Total


Organizamos un proceso en el que agregar un nuevo componente web lleva solo un par de minutos, mientras se mantiene la estructura de los principales proyectos angulares de los que se toman.

Cualquier desarrollador puede agregar un componente a un conjunto de elementos y ensamblarlos en un conjunto de paquetes JS separados de componentes web sin entrar en los detalles de trabajar con Angular Elements.

Source: https://habr.com/ru/post/456462/


All Articles