Angular 应用里 NullInjectorError - No provider for XX 错误的一个场景和分析过程

139 阅读3分钟

最近处理客户 incident,有个客户从 Spartacus 4 升级到 5.2 之后,启动 Storefront,console 遇到了一个错误消息:

NullInjectorError - No provider for AnonymousConsentTemplatesAdapter!

引起这个错误消息的场景有很多,这个 incident 最后的场景是:

以前的 module 通过 loaded for root 完成,现在改成了 loaded with module 来完成。

在Angular中,模块(Module)是组织代码的基本单元。在使用Angular模块时,有两种方式可以将模块加载到应用程序中:通过 "loaded for root"(根加载)和通过 "loaded with module"(模块加载)。这两种加载方式有一些区别,下面我将详细说明它们,并提供一些例子来说明其用法和效果。

  1. Loaded for root(根加载):
    当我们将一个模块加载为根模块时,它意味着该模块将被整个应用程序共享和使用。根模块在应用程序启动时被加载,并且在整个应用程序的生命周期中保持加载状态。

例子:
假设我们有一个名为 "SharedModule" 的模块,其中定义了一些常用的组件、指令和服务,我们希望这些功能在整个应用程序中都可用。我们可以将 "SharedModule" 模块加载为根模块。

                  @NgModule({
                    declarations: [SharedComponent, SharedDirective],
                      imports: [CommonModule],
                        providers: [SharedService],
                          exports: [SharedComponent, SharedDirective]
                          })
                          export class SharedModule { }" aria-label="复制" data-bs-original-title="复制"></button>
</div>
      </div><pre class="typescript hljs language-typescript"><span class="hljs-comment">// shared.module.ts</span>
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">NgModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">CommonModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">SharedComponent</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shared.component'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">SharedDirective</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shared.directive'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">SharedService</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shared.service'</span>;
      
      <span class="hljs-meta">@NgModule</span>({
        <span class="hljs-attr">declarations</span>: [<span class="hljs-title class_">SharedComponent</span>, <span class="hljs-title class_">SharedDirective</span>],
          <span class="hljs-attr">imports</span>: [<span class="hljs-title class_">CommonModule</span>],
            <span class="hljs-attr">providers</span>: [<span class="hljs-title class_">SharedService</span>],
              <span class="hljs-attr">exports</span>: [<span class="hljs-title class_">SharedComponent</span>, <span class="hljs-title class_">SharedDirective</span>]
              })
              <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SharedModule</span> { }</pre><div class="widget-codetool" style="display: none;">
      <div class="widget-codetool--inner">
                  <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="// app.module.ts
                  import { NgModule } from '@angular/core';
                  import { BrowserModule } from '@angular/platform-browser';
                  import { SharedModule } from './shared/shared.module';
                  
                  @NgModule({
                    imports: [BrowserModule, SharedModule],
                      declarations: [AppComponent],
                        bootstrap: [AppComponent]
                        })
                        export class AppModule { }" aria-label="复制" data-bs-original-title="复制"></button>
</div>
      </div><pre class="typescript hljs language-typescript"><span class="hljs-comment">// app.module.ts</span>
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">NgModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">BrowserModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/platform-browser'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">SharedModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shared/shared.module'</span>;
      
      <span class="hljs-meta">@NgModule</span>({
        <span class="hljs-attr">imports</span>: [<span class="hljs-title class_">BrowserModule</span>, <span class="hljs-title class_">SharedModule</span>],
          <span class="hljs-attr">declarations</span>: [<span class="hljs-title class_">AppComponent</span>],
            <span class="hljs-attr">bootstrap</span>: [<span class="hljs-title class_">AppComponent</span>]
            })
            <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">AppModule</span> { }</pre><p>在这个例子中,我们将 "SharedModule" 加载为根模块,并将其导入到应用程序的根模块 "AppModule" 中。这样,"SharedModule" 中定义的组件、指令和服务就可以在整个应用程序中使用,而不需要每个模块都去导入和配置它们。</p><ol start="2"><li>Loaded with module(模块加载):<br>当我们将一个模块加载到另一个模块中时,它意味着该模块仅在该特定模块的上下文中可用。模块加载发生在特定的功能模块中,并且只在该模块及其子模块中生效。</li></ol><p>例子:<br>假设我们有一个名为 "FeatureModule" 的模块,其中包含了一些特定的功能组件、指令和服务,我们希望这些功能仅在 "FeatureModule" 及其子模块中可用。我们可以将 "FeatureModule" 加载到另一个模块中。</p><div class="widget-codetool" style="display: none;">
      <div class="widget-codetool--inner">
                  <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="// feature.module.ts
                  import { NgModule } from '@angular/core';
                  import { CommonModule } from '@angular/common';
                  import { FeatureComponent } from './feature.component';
                  import { FeatureDirective } from './feature.directive';
                  import { FeatureService } from './feature.service';
                  
                  @NgModule({
                    declarations: [FeatureComponent, FeatureDirective],
                     
                     
                      imports: [CommonModule],
                        providers: [FeatureService],
                          exports: [FeatureComponent, FeatureDirective]
                          })
                          export class FeatureModule { }" aria-label="复制" data-bs-original-title="复制"></button>
</div>
      </div><pre class="typescript hljs language-typescript"><span class="hljs-comment">// feature.module.ts</span>
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">NgModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">CommonModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">FeatureComponent</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./feature.component'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">FeatureDirective</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./feature.directive'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">FeatureService</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./feature.service'</span>;
      
      <span class="hljs-meta">@NgModule</span>({
        <span class="hljs-attr">declarations</span>: [<span class="hljs-title class_">FeatureComponent</span>, <span class="hljs-title class_">FeatureDirective</span>],
         
         
          <span class="hljs-attr">imports</span>: [<span class="hljs-title class_">CommonModule</span>],
            <span class="hljs-attr">providers</span>: [<span class="hljs-title class_">FeatureService</span>],
              <span class="hljs-attr">exports</span>: [<span class="hljs-title class_">FeatureComponent</span>, <span class="hljs-title class_">FeatureDirective</span>]
              })
              <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">FeatureModule</span> { }</pre><div class="widget-codetool" style="display: none;">
      <div class="widget-codetool--inner">
                  <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="// main.module.ts
                  import { NgModule } from '@angular/core';
                  import { BrowserModule } from '@angular/platform-browser';
                  import { FeatureModule } from './feature/feature.module';
                  
                  @NgModule({
                    imports: [BrowserModule, FeatureModule],
                      declarations: [AppComponent],
                        bootstrap: [AppComponent]
                        })
                        export class MainModule { }" aria-label="复制" data-bs-original-title="复制"></button>
</div>
      </div><pre class="typescript hljs language-typescript"><span class="hljs-comment">// main.module.ts</span>
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">NgModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">BrowserModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/platform-browser'</span>;
      <span class="hljs-keyword">import</span> { <span class="hljs-title class_">FeatureModule</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./feature/feature.module'</span>;
      
      <span class="hljs-meta">@NgModule</span>({
        <span class="hljs-attr">imports</span>: [<span class="hljs-title class_">BrowserModule</span>, <span class="hljs-title class_">FeatureModule</span>],
          <span class="hljs-attr">declarations</span>: [<span class="hljs-title class_">AppComponent</span>],
            <span class="hljs-attr">bootstrap</span>: [<span class="hljs-title class_">AppComponent</span>]
            })
            <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">MainModule</span> { }</pre><p>在这个例子中,我们将 "FeatureModule" 加载到另一个模块 "MainModule" 中。这样,"FeatureModule" 中定义的组件、指令和服务仅在 "MainModule" 及其子模块中可用,不会对应用程序的其他模块产生影响。</p><p>区别总结:</p><ul><li>"Loaded for root"(根加载)适用于那些希望在整个应用程序中共享和使用的模块,可以被所有模块访问和调用。</li><li>"Loaded with module"(模块加载)适用于那些希望在特定模块及其子模块中使用的模块,可以提供特定功能的模块封装,避免全局污染。</li></ul><p>根加载可以使模块的功能在整个应用程序中易于访问和使用,而模块加载可以实现模块化的功能封装和局部可用性。根据项目需求和代码组织的目标,可以选择适当的加载方式来实现最佳的代码结构和可维护性。</p>