Angular Angular 应用中的元素在优化时会产生无限重新加载问题
Angular Element in an Angular app creates an infinite reloading issue when optimized
TL;DR:我正在尝试使用 Angular Elements 作为 的插件Angular申请。如果我使用 --prod
构建元素,它可以在我的应用程序(开发设置)上与 ng serve
一起使用,但是当我在我的应用程序上将它与 ng serve --prod
一起使用时或在 ng build --prod
我的应用程序(生产设置)。
不过,如果我构建添加 --optimization=false
的元素,则可以使用我的高效应用程序,但不能在我的开发设置中使用。
事实是,我原以为用 --prod
构建 Angular 元素 对这两种情况都没有问题。
问题:有办法解决吗?
现在,长篇大论。
在工作中,我们正尝试在我们的 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
然后我使用这个命令创建了一个 plugin/angular-element:
ng new plugin --skip-git --style scss --skip-tests --minimal
插件:
完成所有创建后,我进入我的插件并在 polyfills.ts
中评论了这一行,我在该站点的某处读到它解决了 NgZone
已经加载的问题,这是真的:
// import 'zone.js/dist/zone'; // Included with Angular CLI.
在 tsconfig.json
中,我将 "target": "es5"
更改为 "target": "es2015"
以解决 Angular 如何创建元素的问题。不太确定它是如何工作的,但是 Whosebug 建议它并且它成功了。
我根据教程中的想法将 app.module.ts
编辑成类似这样的东西,我失去了 link:
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() {
}
}
注意:我添加了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',
}
}
app.component.html
看起来像这样:
Some content:
<pre>{{content | json}}</pre>
在 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"
}
}
文件 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'));
})();
核心:
对于主机应用程序,我添加了一个名为 embedded
的组件,默认路由转到它。
然后我用 Bootstrap 类:
把 embedded.component.html
改成了这样
<div id="embedded-container" class="container border border-primary rounded mt-5 p-3" #container>
<pre>Loading...</pre>
</div>
最后,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);
});
}
}
运行:
如果我 运行 ng serve
并浏览到 http://localhost:4200
,页面加载没有问题,注入插件,将新元素添加到 DOM 并显示来自我的插件的消息。
如果您调试该应用程序,您会看到它加载了 /assets/my-plugin.js
,这是为生产而构建的。这不会是一个问题,除非可能用于调试。
然后,如果我 运行 ng serve --prod
(或为生产构建它)它也可以正常工作,但它加载 /assets/my-plugin-no-optimization.js
,为 "debugging" 构建的那个。
这是我最终在实际应用程序中使用的解决方案,但如您所见,我没有在我的生产插件中使用优化代码,这一点都不好……。
为了证明我的观点,如果我浏览到 http://localhost:4200/?how-it-should-be
,它会尝试加载 ng serve --prod
的优化插件和 ng serve
的调试插件。请注意,这会让您无限重载,打开您的浏览器开发人员工具查看它。
我们使用的最终产品要复杂得多,但这些示例的基本逻辑并没有真正发挥作用。
我还用它创建了一个 GitHub 存储库,您可以在其中看到这些代码,然后自己尝试解决问题,或者将其用作自己想法的示例。
如果你想知道我是怎么发现使用 --optimization=false
有点修复它的,好吧,我正在尝试调试这个问题(事实证明这是不可能的)并且突然加载了。
我看了看时间,对于生产部署来说已经晚了两个小时,所以我添加了那个丑陋的机制来根据环境加载不同的构建。
它在开发和生产中都有效,但我并不为此感到自豪。
对不起,如果我的英语不好...不不不,我的英语不好,对不起^__^
我找到了解决方案!
原来问题出在同一上下文中有多个 webpack 项目 运行。我对这个问题的理解本质上是当你使用 webpack 构建项目时,它们会在运行时进行一些 webpack 引导,并依赖于全局上下文中定义的某个函数 (webpackJsonp
)。当多个 webpack 配置尝试在相同的 DOM 上下文中执行此引导时,它会产生此处定义的症状。 (在这里可以找到更详细的解释 - https://github.com/angular/angular/issues/23732#issuecomment-388670907)
此 GitHub 评论描述了一个解决方案,而不是 how-to - https://github.com/angular/angular/issues/30028#issuecomment-489758172。下面我具体是怎么解决的。
我们可以使用 webpack 配置为我们的 web 组件重命名 webpackJsonp,这样 Angular 项目(或任何使用 webpack 构建的项目)就不会相互干扰。
解决方案
首先我们安装@angular-builders/custom-webpack 包使我们能够在Angular CLI 中修改built-in webpack 配置。
npm install --save-dev @angular-builders/custom-webpack
接下来我们更新 angular.json 文件以使用我们的新构建器和名为 customWebpackConfig
的选项中的新 属性 值。这包括我们将要创建的新文件的路径和 mergeStrategy。这个合并策略说我们要将配置附加到 webpack 配置的 output
部分。
angular.json
...
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js",
"mergeStrategies": { "output": "append"}
},
...
最后,我们只需将 extra-webpack.config.js
文件添加到包含 angular.json 的同一目录中。该文件仅包含以下内容:
module.exports = {
output: {
jsonpFunction: '<webcomponent-prefix>-webpackJsonp'
}
}
jsonpFunction
的值默认为 webpackJsonp
,如果您将其更改为其他任何值,它将起作用。我决定保留函数名称,但为我的 Web 组件应用程序添加一个前缀。理论上你可以在相同的 DOM 上下文中有 N 个 webpack 配置 运行 只要每个配置都有唯一的 jsonpFunction
再次构建您的 Web 组件,瞧,它成功了!
TL;DR:我正在尝试使用 Angular Elements 作为 的插件Angular申请。如果我使用 --prod
构建元素,它可以在我的应用程序(开发设置)上与 ng serve
一起使用,但是当我在我的应用程序上将它与 ng serve --prod
一起使用时或在 ng build --prod
我的应用程序(生产设置)。
不过,如果我构建添加 --optimization=false
的元素,则可以使用我的高效应用程序,但不能在我的开发设置中使用。
事实是,我原以为用 --prod
构建 Angular 元素 对这两种情况都没有问题。
问题:有办法解决吗?
现在,长篇大论。
在工作中,我们正尝试在我们的 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
然后我使用这个命令创建了一个 plugin/angular-element:
ng new plugin --skip-git --style scss --skip-tests --minimal
插件:
完成所有创建后,我进入我的插件并在 polyfills.ts
中评论了这一行,我在该站点的某处读到它解决了 NgZone
已经加载的问题,这是真的:
// import 'zone.js/dist/zone'; // Included with Angular CLI.
在 tsconfig.json
中,我将 "target": "es5"
更改为 "target": "es2015"
以解决 Angular 如何创建元素的问题。不太确定它是如何工作的,但是 Whosebug 建议它并且它成功了。
我根据教程中的想法将 app.module.ts
编辑成类似这样的东西,我失去了 link:
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() {
}
}
注意:我添加了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',
}
}
app.component.html
看起来像这样:
Some content:
<pre>{{content | json}}</pre>
在 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"
}
}
文件 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'));
})();
核心:
对于主机应用程序,我添加了一个名为 embedded
的组件,默认路由转到它。
然后我用 Bootstrap 类:
把embedded.component.html
改成了这样
<div id="embedded-container" class="container border border-primary rounded mt-5 p-3" #container>
<pre>Loading...</pre>
</div>
最后,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);
});
}
}
运行:
如果我 运行 ng serve
并浏览到 http://localhost:4200
,页面加载没有问题,注入插件,将新元素添加到 DOM 并显示来自我的插件的消息。
如果您调试该应用程序,您会看到它加载了 /assets/my-plugin.js
,这是为生产而构建的。这不会是一个问题,除非可能用于调试。
然后,如果我 运行 ng serve --prod
(或为生产构建它)它也可以正常工作,但它加载 /assets/my-plugin-no-optimization.js
,为 "debugging" 构建的那个。
这是我最终在实际应用程序中使用的解决方案,但如您所见,我没有在我的生产插件中使用优化代码,这一点都不好……。
为了证明我的观点,如果我浏览到 http://localhost:4200/?how-it-should-be
,它会尝试加载 ng serve --prod
的优化插件和 ng serve
的调试插件。请注意,这会让您无限重载,打开您的浏览器开发人员工具查看它。
我们使用的最终产品要复杂得多,但这些示例的基本逻辑并没有真正发挥作用。
我还用它创建了一个 GitHub 存储库,您可以在其中看到这些代码,然后自己尝试解决问题,或者将其用作自己想法的示例。
如果你想知道我是怎么发现使用 --optimization=false
有点修复它的,好吧,我正在尝试调试这个问题(事实证明这是不可能的)并且突然加载了。
我看了看时间,对于生产部署来说已经晚了两个小时,所以我添加了那个丑陋的机制来根据环境加载不同的构建。 它在开发和生产中都有效,但我并不为此感到自豪。
对不起,如果我的英语不好...不不不,我的英语不好,对不起^__^
我找到了解决方案!
原来问题出在同一上下文中有多个 webpack 项目 运行。我对这个问题的理解本质上是当你使用 webpack 构建项目时,它们会在运行时进行一些 webpack 引导,并依赖于全局上下文中定义的某个函数 (webpackJsonp
)。当多个 webpack 配置尝试在相同的 DOM 上下文中执行此引导时,它会产生此处定义的症状。 (在这里可以找到更详细的解释 - https://github.com/angular/angular/issues/23732#issuecomment-388670907)
此 GitHub 评论描述了一个解决方案,而不是 how-to - https://github.com/angular/angular/issues/30028#issuecomment-489758172。下面我具体是怎么解决的。
我们可以使用 webpack 配置为我们的 web 组件重命名 webpackJsonp,这样 Angular 项目(或任何使用 webpack 构建的项目)就不会相互干扰。
解决方案
首先我们安装@angular-builders/custom-webpack 包使我们能够在Angular CLI 中修改built-in webpack 配置。
npm install --save-dev @angular-builders/custom-webpack
接下来我们更新 angular.json 文件以使用我们的新构建器和名为 customWebpackConfig
的选项中的新 属性 值。这包括我们将要创建的新文件的路径和 mergeStrategy。这个合并策略说我们要将配置附加到 webpack 配置的 output
部分。
angular.json
...
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js",
"mergeStrategies": { "output": "append"}
},
...
最后,我们只需将 extra-webpack.config.js
文件添加到包含 angular.json 的同一目录中。该文件仅包含以下内容:
module.exports = {
output: {
jsonpFunction: '<webcomponent-prefix>-webpackJsonp'
}
}
jsonpFunction
的值默认为 webpackJsonp
,如果您将其更改为其他任何值,它将起作用。我决定保留函数名称,但为我的 Web 组件应用程序添加一个前缀。理论上你可以在相同的 DOM 上下文中有 N 个 webpack 配置 运行 只要每个配置都有唯一的 jsonpFunction
再次构建您的 Web 组件,瞧,它成功了!