Angular 应用程序中的 Angular 元素在优化时会产生无限重新加载问题

DAE*_*aco 8 javascript plugins typescript angular angular-elements

TL;DR:我正在尝试使用Angular Elements作为Angular应用程序的插件。如果我在我的应用程序上使用--prod它构建元素ng serve(开发设置),但是当我ng serve --prod在我的应用程序上使用它时或在ng build --prod我的应用程序(生产设置)之后使用它时它会无限重新加载。

但是,如果我构建了元素添加--optimization=false,则适用于我的高效应用程序,但不适用于我的开发设置。

关键是,我期待的是建立一个角形元件--prod就可以了,对于两种情况。

问题有没有办法解决这个问题?


现在,长读。

在工作中,我们试图在我们的Angular站点中使用可配置的插件,其中服务器是告诉哪个插件处于活动状态的。

我们尝试动态加载Angular模块,但这是一个完全不同的问题,我们又把它搁置一旁。

所以,我们接下来想要尝试的是Angular Elements,它有点工作,除非我们按照它应该的方式构建所有东西。

首先,我开始关注本教程https://scotch.io/tutorials/build-a-reusable-component-with-angular-elements/amp并忽略了所有内容,okta因为我的功能有所不同。

创建:

我使用下一个命令创建了我的核心应用程序,这将是托管插件的应用程序:

  • ng new core --routing --skip-git --style scss --skip-tests --minimal

然后我使用这个命令创建了一个插件/角度元素:

  • ng new plugin --skip-git --style scss --skip-tests --minimal

插入:

所有的创作后,我走进我的插件,并在评论这条线polyfills.ts,我在这个网站阅读的地方,它解决的问题NgZone已经加载,这是正确的:

// import 'zone.js/dist/zone';  // Included with Angular CLI.
Run Code Online (Sandbox Code Playgroud)

tsconfig.json我更改"target": "es5""target": "es2015"以解决Angular如何创建元素的问题。不太确定这是如何工作的,但是stackoverflow建议了它并且它成功了。

app.module.ts将教程中的以下想法和其他一些我失去链接的想法编辑为类似的内容:

import { BrowserModule } from '@angular/platform-browser';
import { CUSTOM_ELEMENTS_SCHEMA, Injector, NgModule } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { AppComponent } from './app.component';

@NgModule({
    declarations: [
        AppComponent,
    ],
    imports: [
        BrowserModule,
    ],
    providers: [
    ],
    schemas: [
        CUSTOM_ELEMENTS_SCHEMA,
    ],
    entryComponents: [
        AppComponent,
    ],
})
export class AppModule {
    constructor(private injector: Injector) {
        const elem = createCustomElement(AppComponent, { injector: this.injector });
        customElements.define('my-plugin', elem);
    }

    ngDoBootstrap() {
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:我添加了CUSTOM_ELEMENTS_SCHEMA因为我在某处找到了它,但它没有解决它(另外,我不确定它是做什么的)。

app.component.ts我这样做是为了在其模板中显示一些属性:

import { Component } from '@angular/core';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
})
export class AppComponent {
    public content: any = {
        a: 10,
        b: '20',
    }
}
Run Code Online (Sandbox Code Playgroud)

app.component.html看起来像这样:

Some content:
<pre>{{content | json}}</pre>
Run Code Online (Sandbox Code Playgroud)

package.json文件中,我有三个脚本来构建所有:

{
    "scripts": {
        "build": "npm run build:opt && npm run build:noopt",
        "build:opt": "ng build --prod --output-hashing none && node build-elements.js",
        "build:noopt": "ng build --prod --output-hashing none --optimization=false && node build-elements.noopt.js"
    }
}
Run Code Online (Sandbox Code Playgroud)

该文件build-elements.js如下所示(build-elements.noopt.js与不同的目标名称相同):

'use strict';

const concat = require('concat');
const fs = require('fs-extra');
const path = require('path');

(async function build() {
    const files = [
        './dist/plugin/runtime.js',
        './dist/plugin/polyfills.js',
        './dist/plugin/scripts.js',
        './dist/plugin/main.js',
    ];

    const destinationDir = path.join(__dirname, '../core/src/assets');
    await fs.ensureDir(destinationDir);
    await concat(files, path.join(destinationDir, 'my-plugin.js'));
})();
Run Code Online (Sandbox Code Playgroud)

核:

对于主机应用程序,我添加了一个名为的组件embedded,默认路由会转到它。

然后我embedded.component.html使用一些Bootstrap类将其更改为类似的内容:

<div id="embedded-container" class="container border border-primary rounded mt-5 p-3" #container>
    <pre>Loading...</pre>
</div>
Run Code Online (Sandbox Code Playgroud)

最后,embedded.component.ts以这样的方式结束以显示实际的加载机制:

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';

import { environment } from '../../environments/environment';

@Component({
    selector: 'app-embedded',
    templateUrl: './embedded.component.html',
})
export class EmbeddedComponent implements OnInit {
    @ViewChild('container') public container: ElementRef;

    constructor(protected activatedRoute: ActivatedRoute) {
    }

    ngOnInit() {
        this.activatedRoute.queryParams.subscribe((params: Params) => {
            const script = document.createElement('script');
            if (params['how-it-should-be'] !== undefined) {
                script.src = environment.production ? '/assets/my-plugin.js' : '/assets/my-plugin-no-optimization.js';
            } else {
                script.src = environment.production ? '/assets/my-plugin-no-optimization.js' : '/assets/my-plugin.js';
            }
            document.body.appendChild(script);

            const div = document.createElement('div');
            div.innerHTML = '<my-plugin></my-plugin>';

            this.container.nativeElement.innerHTML = '';
            this.container.nativeElement.appendChild(div);
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

跑步:

如果我运行ng serve并浏览到http://localhost:4200,页面加载没有问题,注入插件,将新元素添加到 DOM 并显示来自我的插件的消息。如果您调试应用程序,您会看到它加载了/assets/my-plugin.js,即为生产而构建的应用程序。除了可能用于调试之外,这不会成为问题。

然后,如果我运行ng serve --prod(或为生产而构建)它也可以正常工作,但它会加载/assets/my-plugin-no-optimization.js,即为“调试”而构建的。

这是我最终在我们的实际应用程序中使用的解决方案,但正如您所看到的,我没有在我的插件中使用优化的代码进行生产,这并不好......

为了证明我的观点,如果我浏览到http://localhost:4200/?how-it-should-be,它将尝试加载优化插件ng serve --prod和调试插件ng serve。请注意,这会让您无限重载,打开浏览器开发人员工具即可查看。


我们使用的最终产品要复杂得多,但这些示例的基本逻辑并不真正有效。

我还用这个创建了一个 GitHub 存储库,您可以在其中查看这些代码,并自己尝试解决问题,或者作为您自己想法的示例。

如果你想知道我是如何发现使用--optimization=false有点修复它的,好吧,我试图调试这个问题(这被证明是不可能的)并突然加载了它。

我看了看时间,发现生产部署晚了两个小时,所以我添加了这个丑陋的机制,根据环境加载不同的构建。它在开发和生产中都有效,但我并不为此感到自豪。


对不起,如果我的英语不好...不不不,我的英语不好,对不起^__^

小智 6

我找到了解决方案!

事实证明,问题在于在同一上下文中运行多个 webpack 项目。我对这个问题的理解本质上是当你使用 webpack 构建项目时,它们会在运行时进行一些 webpack 引导,并依赖于在全局上下文 ( webpackJsonp) 中定义的某个函数。当多个 webpack 配置尝试在同一个 DOM 上下文中执行此引导时,它会创建此处定义的症状。(更详细的解释在这里找到 - https://github.com/angular/angular/issues/23732#issuecomment-388670907

此 GitHub 评论描述了解决方案,但未描述操作方法 - https://github.com/angular/angular/issues/30028#issuecomment-489758172。下面我将展示我是如何具体解决它的。

我们可以使用 webpack 配置为我们的 web 组件重命名 webpackJsonp,这样两个 Angular 项目(或任何使用 webpack 构建的项目)不会相互干扰。

解决方案

首先,我们安装 @angular-builders/custom-webpack 包,使我们能够在 Angular CLI 中修改内置的 webpack 配置。

npm install --save-dev @angular-builders/custom-webpack

接下来,我们更新 angular.json 文件,以使用我们的新构建器和名为 的选项中的新属性值customWebpackConfig。这包括我们将要创建的新文件的路径和一个合并策略。这个合并策略是说我们要将配置附加到outputwebpack 配置部分。

angular.json
...
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "./extra-webpack.config.js",
              "mergeStrategies": { "output": "append"}
            },
...
Run Code Online (Sandbox Code Playgroud)

最后,我们简单地将我们的extra-webpack.config.js文件添加到包含 angular.json 的同一目录中。该文件仅包含以下内容:

module.exports = {
    output: {
        jsonpFunction: '<webcomponent-prefix>-webpackJsonp'
    }
}
Run Code Online (Sandbox Code Playgroud)

的值jsonpFunction默认为webpackJsonp,如果您将其更改为其他任何内容,它将起作用。我决定保留函数名称,但为我的 Web 组件应用程序添加前缀。理论上你可以在同一个 DOM 上下文中运行 N 个 webpack 配置,只要每个配置都有一个唯一的jsonpFunction

再次构建您的 Web 组件,瞧,它可以工作了!