将角度组件库组装为Web组件

正在撰写许多有关Angular Elements的文章,并定期阅读报告。 就像,您不再需要部署多个完整的角度-只需收集Web组件并在页面上使用它们即可。

但是,通常,这些材料仅限于考虑相当乌托邦的情况:我们制作一个单独的项目,创建一个角度组件,配置该项目以组装Elements,最后编译几个JS文件,将它们连接到常规页面将为我们提供所需的结果。 万岁,该组件工作正常!

图片

在实践中,此外,优选地,需要从现成的工作角度项目中拉出几个部件,以便不影响其当前的开发和使用。 正是由于以下一种情况,本文才被证实:我不仅要组装项目的各个元素,而且要完成将Angular上的整个UI库编译为一组具有本地Web组件的文件的过程。

模块准备


首先,让我们回顾一下用于编译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() {} } 

我们需要:

  1. 将我们计划制作一个角度元素的组件添加到entryComponents中,导入该组件所需的模块。
  2. 使用createCustomElement和注入器创建角度元素。
  3. 在浏览器的customElements中声明一个Web组件。
  4. 重写ngDoBootstrap方法为空。

第一项是组件本身及其依赖项的名称,其余三项是在浏览器中显示Web组件所需的过程。 这种分离允许您将创建单独元素的逻辑放在抽象超类中:

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

该超类能够从注入器,组件和名称中组装出完整的本机组件,并对其进行注册。 用于创建特定元素的模块如下所示:

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

在此示例中,我们将按钮的模块收集到NgModule元数据中,并在entryComponents中从该模块声明组件,还从Angular依赖项注入机制获取注入器。

该模块已准备好进行组装,并将为我们提供一组JS文件,这些文件可以折叠到单独的Web组件中。 这样,我们可以创建几个模块,然后轮流从中组装Web组件。

汇总一些组件


现在,我们需要设置结果模块的引导过程。 最重要的是,我喜欢将这种逻辑放入一个单独的可执行文件中的想法,该文件将负责编译特定模块。

元素的结构如下所示:

图片

最简单的版本中的一个单独的编译文件如下所示:

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

这种方法将有助于轻松绕过已准备好的模块,并通过Elements的简单明了地维护项目结构。

在angular.json构建设置中,我们指定收集的文件到dist中某个临时文件夹的路径:

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

组装模块后,会有一组输出文件。

对于程序集本身,请在angular-cli中使用通常的build命令:

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

最终产品将是一个单独的元素,因此我们使用Ahead-of-Time-compilation打开生产标志,然后将路径替换为可执行文件的路径,该文件由项目和组件名称组成。

现在,我们将结果收集到一个单独的文件中,这将是我们单独的Web组件的最终捆绑包。 为此,请使用普通的猫:

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

在此处需要注意的重要一点是,我们不要将polyfills.js文件放在每个组件的捆绑包中,因为如果将来在同一页面上使用多个组件,则会得到重复。 当然,您应该在angular.json中禁用outputHashing选项。

我们将把结果包从一个临时文件夹转移到一个用于存储组件的文件夹。 例如,像这样:

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

剩下的只是将所有内容放在一起-编译脚本已准备就绪:

 // 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}`); } 

现在,我们有了一个简洁的父亲,其中包含一组Web组件:

图片

将组件连接到常规页面


我们组装的Web组件可以独立插入页面中,并根据需要连接其JS包:

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

为了不拖拽每个组件的整个zone.js,我们在文档开始处将其连接一次:

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

该组件显示在页面上,一切都很好。

然后添加一个按钮:

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

我们启动页面并...

图片

哦,坏了!

如果我们查看捆绑包,就会发现这样一条不起眼的行:

 window.webpackJsonp=window.webpackJsonp||[] 

图片

Webpack补丁程序窗口,以免重复加载相同的模块。 事实证明,只有第一个添加到页面的组件才能将自身添加到customElements。

要解决此问题,我们需要使用custom-webpack:

  1. 使用元素将custom-webpack添加到项目中:

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

  2. 配置angular.json:

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

  3. 创建一个自定义webpack配置文件:

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

    在其中,我们需要以任何方便的方式为每个程序集生成唯一的ID。 我利用了uuid。

您可以再次运行构建脚本-新组件在同一页面上运行良好。

带来美丽


我们的组件使用全局CSS变量来指定组件的颜色主题和大小。

在使用我们的库的角度应用程序中,它们放置在项目的根组件中。 对于独立的Web组件,这是不可能的,因此只需编译样式并连接到使用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`,; ); } 

我们使用的更少,因此只需编译我们的lessc变量,然后将结果文件放入helpers文件夹。

这种方法使您可以控制页面中所有Web组件的样式,而无需重新编译它们。

最终脚本


实际上,组装上述元素的整个过程可以简化为一组动作:

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

仍然仅需从main package.json调用此脚本,以减少编译实际角度分量的整个过程,而只需执行一个命令即可。

可以在github上找到上述所有脚本以及使用angular和native组件的演示。

合计


我们组织了一个过程,其中添加新的Web组件仅需花费几分钟,同时保持从中获取主要角度项目的结构。

任何开发人员都可以将组件添加到一组元素中,并将它们组装到一组单独的JS Web组件包中,而无需研究使用Angular Elements的细节。

Source: https://habr.com/ru/post/zh-CN456462/


All Articles