Двусторонняя привязка Angular нарушена в производственной сборке - TypeError «st не является функцией»

У меня есть проект, в котором используется Angular 17, сборщик приложений, автономные компоненты и импорт NgModule. В разработке оно собирается и работает нормально, но при сборке в производстве приложение вылетает со следующей ошибкой:

core.mjs:6373 ERROR TypeError: st is not a function
    at HTMLElement.addEventListener (zone.js:1907:21)
    at n.addEventListener (platform-browser.mjs:785:15)
    at n.addEventListener (platform-browser.mjs:212:21)
    at Dd.listen (platform-browser.mjs:665:30)
    at sf.listen (browser.mjs:4309:26)
    at G0 (core.mjs:24973:34)
    at _t (core.mjs:26820:3)
    at Kle (createAccount.component.html:31:53)
    at o0 (core.mjs:10988:5)
    at LA (core.mjs:12150:7)

createAccount.comComponent.html:31:53 выглядит так, строка 31 — [(value)] = "model.Username"

<my-textbox [label] = "ResourceKey.Portals_EditProfile_Username"
    [(value)] = "model.Username"
    [readonly] = "model.IsSSO" 
    [required] = "true">
</my-textbox>

Если я закомментирую это текстовое поле, то та же ошибка появится в строке [(value)] следующего текстового поля.

Из того, что я могу сказать, копаясь в стеке, st, похоже, сопоставлен с подготовитьEventNames вzone.js:

if (!symbolEventNames) {
    prepareEventNames(eventName, eventNameToString);
    symbolEventNames = zoneSymbolEventNames[eventName];
}

попытка войти в метод подготовкиEventNames() приводит к ошибке.

Так что же это значит? Оптимизированная сборка изменила слишком многое? Как исправить/найти проблему?

Среда:

Angular CLI: 17.3.8
Node: 20.16.0
Package Manager: npm 10.8.1
OS: win32 x64

Angular: 17.3.12
... animations, common, compiler, compiler-cli, core, forms
... localize, platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1703.8
@angular-devkit/build-angular   17.3.8
@angular-devkit/core            17.3.8
@angular-devkit/schematics      17.3.8
@angular/cdk                    17.3.10
@angular/cli                    17.3.8
@angular/flex-layout            15.0.0-beta.42
@angular/material               17.3.10
@schematics/angular             17.3.8
ng-packagr                      17.3.0
rxjs                            7.8.1
typescript                      5.3.3
zone.js                         0.14.4

Конфигурация приложения:

export const appConfig: ApplicationConfig = {
    providers: [
        { provide: LocalizationSource, useClass: MinIDLocalizationSource },
        provideRouter(routes),
        importProvidersFrom(BrowserAnimationsModule),
        provideHttpClient(
            withInterceptors([
                DateInterceptorFn
            ])
        )
    ]
};

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

Обновление: если я собираю с отключенной оптимизацией, приложение работает нормально. ng build {project} --configuration=production --optimization=false

🤔 А знаете ли вы, что...
Angular предлагает мощную систему инъекции зависимостей для управления зависимостями и компонентами приложения.


1
64
2

Ответы:

Насколько я понимаю, исходя из вашего кода, у вас есть собственный компонент с именем my-textbox, а у этого есть (судя по всему) input с именем value, независимо от того, используете ли вы ввод как signals или @Input(), вы не можете ожидать, что иметь двустороннюю привязку для value, просто используя [()]. Вы должны получить сообщение об ошибке, вы не сможете его скомпилировать.

Поскольку вы не опубликовали соответствующий код для my-textbox, если вы хотите отправить и изменить model.Username, попробуйте добавить output для прослушивания вашего специального события.

// using input/output annotations
@Componet({...})
export class MyTextBox {
  @Input() value!: string;
  @Output() valueChanged = new EventEmitter<string>();

  onChange(val: string): void {
    this.valueChanged.emit(val);
  }
}

// using input/output signals
@Componet({...})
export class MyTextBox {
  value = input<string>();
  valueChanged = output<string>();

  onChange(val: string): void {
    this.valueChanged.emit(val);
  }
}

// a parent component using my-texbox
@Component({
  template: `<my-textbox 
               [value] = "model.Username" 
               (valueChanged) = "onUsername($event)">
             </my-textbox>`,
  ...
})
export class ParentComponent {
   model!: any; // I don't know the type, that's why I use any

   onUsername(name: string): void {
     this.model.Username = name;
   }
}

Таким образом, my-texbox будет реагировать на изменения, внесенные пользовательским событием valueChanged.

Другой способ обработки ввода/вывода компонента уровня — использование сигнала model, который заменяет @Input и @Output, но он ведет себя не совсем так, как ngModel:

@Componet({...})
export class MyTextBox {
  value = model<string>();

  onChange(val: string): void {
    this.value.set(val);
  }
}

Затем в родительском компоненте вам нужно будет прикрепить сигнал к вашему [(value)] следующим образом:

@Component({
  template: `<my-textbox [(value)] = "username"> </my-textbox>
             <!-- without parenthesis -->
  `,             
  ...
})
export class ParentComponent {
  person = { username: 'Bob' }
  username = signal(this.person.username);
}

ОБНОВЛЕНО ДЕМО


Решено

В поисках любой соответствующей информации по этой проблеме я столкнулся с этим: Включение оптимизации ломает сайт Angular без видимой причины

И вот, вот оно. Теперь моя производственная сборка работает нормально.

Подведем итоги проблемы и ее исправления:

  • Казалось, что производственные сборки были сломаны. Функции в Zone.js не определены или имеют скалярные значения, что приводит к ошибкам типа и неработоспособности приложения.
  • В сообщении SO, упомянутом выше, говорится о различных файлах javascript, которые забивают функции и переменные друг друга.
  • Исправление заключалось в добавлении type = "module" к ссылкам на скрипт. от <script src = "main.js"></script> до <script src = "main.js" type = "module"></script>

Определенно стоит запомнить на будущее