Analisis ringkas reka bentuk suntikan kebergantungan berbilang peringkat dalam Angular

青灯夜游
Lepaskan: 2022-04-24 21:13:57
ke hadapan
1855 orang telah melayarinya

Artikel ini membawa anda melalui pembelajaran kod sumber sudut dan memperkenalkan reka bentuk suntikan kebergantungan berbilang peringkat saya harap ia akan membantu anda!

Analisis ringkas reka bentuk suntikan kebergantungan berbilang peringkat dalam Angular

Sebagai rangka kerja hadapan yang direka "untuk projek bahagian hadapan berskala besar", Angular sebenarnya mempunyai banyak reka bentuk yang patut dirujuk dan dipelajari reka bentuk dan fungsi ini prinsip. Artikel ini memfokuskan pada ciri terbesar Sudut - suntikan kebergantungan, dan memperkenalkan reka bentuk suntikan kebergantungan berbilang peringkat dalam Sudut. [Tutorial berkaitan yang disyorkan: "tutorial sudut"]

Dalam artikel sebelumnya, kami memperkenalkan Injectot penyuntik, Provider pembekal dan mekanisme penyuntik dalam Angular. Jadi, dalam aplikasi Angular, bagaimanakah komponen dan modul berkongsi kebergantungan Bolehkah perkhidmatan yang sama dibuat beberapa kali?

Proses suntikan kebergantungan komponen dan modul tidak dapat dipisahkan daripada reka bentuk suntikan kebergantungan berbilang peringkat Angular Mari kita lihat.

Suntikan kebergantungan berbilang peringkat

Terdahulu kami mengatakan bahawa penyuntik dalam Angular boleh diwarisi dan berhierarki.

Dalam Sudut, terdapat dua hierarki penyuntik:

  • ModuleInjectorPenantik modul: Konfigurasikan dalam hierarki ini menggunakan anotasi @NgModule() atau @Injectable()ModuleInjector
  • ElementInjectorPenuntik elemen: Dicipta secara tersirat pada setiap elemen DOM

Kedua-dua penyuntik modul dan penyuntik elemen adalah berstruktur pokok, tetapi struktur hierarkinya tidak konsisten sepenuhnya.

Modul Penyuntik

Struktur hierarki penyuntik modul bukan sahaja berkaitan dengan reka bentuk modul dalam aplikasi, tetapi juga penyuntik modul platform (PlatformModule) dan Hierarki aplikasi penyuntik modul (AppModule).

Platform Module Injector

Dalam terminologi Angular, platform ialah konteks di mana aplikasi Angular dijalankan. Platform yang paling biasa untuk aplikasi Angular ialah pelayar web, tetapi ia juga boleh menjadi sistem pengendalian peranti mudah alih atau pelayan web.

Apabila aplikasi Angular bermula, ia akan mencipta lapisan platform:

  • Platform ialah titik masuk Angular pada halaman web dan setiap halaman hanya mempunyai satu platform
  • halaman Untuk setiap aplikasi Sudut yang dijalankan pada platform, semua perkhidmatan biasa terikat dalam platform

Platform Sudut, yang terutamanya merangkumi fungsi seperti mencipta contoh modul dan memusnahkannya:

@Injectable()
export class PlatformRef {
  // 传入注入器,作为平台注入器
  constructor(private _injector: Injector) {}

  // 为给定的平台创建一个 @NgModule 的实例,以进行离线编译
  bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):
      Promise<NgModuleRef<M>> {}

  // 使用给定的运行时编译器,为给定的平台创建一个 @NgModule 的实例
  bootstrapModule<M>(
      moduleType: Type<M>,
      compilerOptions: (CompilerOptions&BootstrapOptions)|
      Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {}

  // 注册销毁平台时要调用的侦听器
  onDestroy(callback: () => void): void {}

  // 获取平台注入器
  // 该平台注入器是页面上每个 Angular 应用程序的父注入器,并提供单例提供程序
  get injector(): Injector {}

  // 销毁页面上的当前 Angular 平台和所有 Angular 应用程序,包括销毁在平台上注册的所有模块和侦听器
  destroy() {}
}
Salin selepas log masuk

Malah, apabila platform dimulakan (dalam kaedah bootstrapModuleFactory), ngZone.run dicipta dalam ngZoneInjector untuk mencipta semua perkhidmatan instantiated dalam kawasan Sudut dan ApplicationRef (Sudut aplikasi yang berjalan pada program halaman) akan dibuat di luar kawasan Sudut.

Apabila dilancarkan dalam penyemak imbas, platform penyemak imbas dicipta:

export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef =
    createPlatformFactory(platformCore, &#39;browser&#39;, INTERNAL_BROWSER_PLATFORM_PROVIDERS);

// 其中,platformCore 平台必须包含在任何其他平台中
export const platformCore = createPlatformFactory(null, &#39;core&#39;, _CORE_PLATFORM_PROVIDERS);
Salin selepas log masuk

Apabila platform dicipta menggunakan kilang platform (seperti createPlatformFactory di atas), platform halaman secara tersirat dimulakan :

export function createPlatformFactory(
    parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, name: string,
    providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef {
  const desc = `Platform: ${name}`;
  const marker = new InjectionToken(desc); // DI 令牌
  return (extraProviders: StaticProvider[] = []) => {
    let platform = getPlatform();
    // 若平台已创建,则不做处理
    if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
      if (parentPlatformFactory) {
        // 若有父级平台,则直接使用父级平台,并更新相应的提供者
        parentPlatformFactory(
            providers.concat(extraProviders).concat({provide: marker, useValue: true}));
      } else {
        const injectedProviders: StaticProvider[] =
            providers.concat(extraProviders).concat({provide: marker, useValue: true}, {
              provide: INJECTOR_SCOPE,
              useValue: &#39;platform&#39;
            });
        // 若无父级平台,则新建注入器,并创建平台
        createPlatform(Injector.create({providers: injectedProviders, name: desc}));
      }
    }
    return assertPlatform(marker);
  };
}
Salin selepas log masuk

Melalui proses di atas, kita tahu bahawa apabila aplikasi Angular mencipta platform, ia mencipta penyuntik modul platform ModuleInjector. Kita juga boleh melihat daripada definisi Injector dalam bahagian sebelumnya bahawa NullInjector berada di atas semua penyuntik:

export abstract class Injector {
  static NULL: Injector = new NullInjector();
}
Salin selepas log masuk

Oleh itu, di atas penyuntik modul platform, terdapat juga NullInjector(). Di bawah penyuntik modul platform, terdapat juga penyuntik modul aplikasi.

Modul Akar Aplikasi (AppModule) Penyuntik

Setiap aplikasi mempunyai sekurang-kurangnya satu modul Sudut dan modul akar ialah modul yang digunakan untuk memulakan aplikasi:

@NgModule({ providers: APPLICATION_MODULE_PROVIDERS })
export class ApplicationModule {
  // ApplicationRef 需要引导程序提供组件
  constructor(appRef: ApplicationRef) {}
}
Salin selepas log masuk

AppModuleModul aplikasi root dieksport semula oleh BrowserModule dan disertakan secara automatik dalam root new apabila kami mencipta aplikasi baharu menggunakan perintah AppModule CLI. Dalam modul akar aplikasi, pembekal dikaitkan dengan token DI terbina dalam yang digunakan untuk mengkonfigurasi penyuntik akar untuk bootstrap.

Angular juga menambah ComponentFactoryResolver pada penyuntik modul akar. Penghurai ini menyimpan kilang siri entryComponents, jadi ia bertanggungjawab untuk mencipta komponen secara dinamik.

Hierarki penyuntik modul

Pada ketika ini, kita hanya boleh menyusun perhubungan hierarki penyuntik modul:

  • peringkat atas pokok penyuntik modul ialah penyuntik modul akar aplikasi (AppModule), yang dipanggil akar.

  • Terdapat dua penyuntik di atas akar, satu penyuntik modul platform (PlatformModule) dan satu lagi ialah NullInjector().

Oleh itu, struktur hierarki penyuntik modul adalah seperti berikut:

Analisis ringkas reka bentuk suntikan kebergantungan berbilang peringkat dalam Angular

Dalam aplikasi sebenar kami, ia berkemungkinan seperti ini daripada:

Analisis ringkas reka bentuk suntikan kebergantungan berbilang peringkat dalam Angular

Angular DI 具有分层注入体系,这意味着下级注入器也可以创建它们自己的服务实例。

元素注入器

前面说过,在 Angular 中有两个注入器层次结构,分别是模块注入器和元素注入器。

元素注入器的引入

当 Angular 中懒加载的模块开始广泛使用时,出现了一个 issue:依赖注入系统导致懒加载模块的实例化加倍。

在这一次修复中,引入了新的设计注入器使用两棵并行的树,一棵用于元素,另一棵用于模块

Angular 会为所有entryComponents创建宿主工厂,它们是所有其他组件的根视图。

这意味着每次我们创建动态 Angular 组件时,都会使用根数据(RootData)创建根视图(RootView):

class ComponentFactory_ extends ComponentFactory<any>{
  create(
      injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any,
      ngModule?: NgModuleRef<any>): ComponentRef<any> {
    if (!ngModule) {
      throw new Error(&#39;ngModule should be provided&#39;);
    }
    const viewDef = resolveDefinition(this.viewDefFactory);
    const componentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex;
    // 使用根数据创建根视图
    const view = Services.createRootView(
        injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);
    // view.nodes 的访问器
    const component = asProviderData(view, componentNodeIndex).instance;
    if (rootSelectorOrNode) {
      view.renderer.setAttribute(asElementData(view, 0).renderElement, &#39;ng-version&#39;, VERSION.full);
    }
    // 创建组件
    return new ComponentRef_(view, new ViewRef_(view), component);
  }
}
Salin selepas log masuk

该根数据(RootData)包含对elInjectorngModule注入器的引用:

function createRootData(
    elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2,
    projectableNodes: any[][], rootSelectorOrNode: any): RootData {
  const sanitizer = ngModule.injector.get(Sanitizer);
  const errorHandler = ngModule.injector.get(ErrorHandler);
  const renderer = rendererFactory.createRenderer(null, null);
  return {
    ngModule,
    injector: elInjector,
    projectableNodes,
    selectorOrNode: rootSelectorOrNode,
    sanitizer,
    rendererFactory,
    renderer,
    errorHandler,
  };
}
Salin selepas log masuk

引入元素注入器树,原因是这样的设计比较简单。通过更改注入器层次结构,避免交错插入模块和组件注入器,从而导致延迟加载模块的双倍实例化。因为每个注入器都只有一个父对象,并且每次解析都必须精确地寻找一个注入器来检索依赖项。

元素注入器(Element Injector)

在 Angular 中,视图是模板的表示形式,它包含不同类型的节点,其中便有元素节点,元素注入器位于此节点上:

export interface ElementDef {
  ...
  // 在该视图中可见的 DI 的公共提供者
  publicProviders: {[tokenKey: string]: NodeDef}|null;
  // 与 visiblePublicProviders 相同,但还包括位于此元素上的私有提供者
  allProviders: {[tokenKey: string]: NodeDef}|null;
}
Salin selepas log masuk

默认情况下ElementInjector为空,除非在@Directive()@Component()providers属性中进行配置。

当 Angular 为嵌套的 HTML 元素创建元素注入器时,要么从父元素注入器继承它,要么直接将父元素注入器分配给子节点定义。

如果子 HTML 元素上的元素注入器具有提供者,则应该继承该注入器。否则,无需为子组件创建单独的注入器,并且如果需要,可以直接从父级的注入器中解决依赖项。

元素注入器与模块注入器的设计

那么,元素注入器与模块注入器是从哪个地方开始成为平行树的呢?

我们已经知道,应用程序根模块(AppModule)会在使用 CLI 的new命令创建新应用时,自动包含在根AppModule中。

当应用程序(ApplicationRef)启动(bootstrap)时,会创建entryComponent

const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
Salin selepas log masuk

该过程会使用根数据(RootData)创建根视图(RootView),同时会创建根元素注入器,在这里elInjectorInjector.NULL

在这里,Angular 的注入器树被分成元素注入器树和模块注入器树,这两个平行的树了。

Angular 会有规律的创建下级注入器,每当 Angular 创建一个在@Component()中指定了providers的组件实例时,它也会为该实例创建一个新的子注入器。类似的,当在运行期间加载一个新的NgModule时,Angular 也可以为它创建一个拥有自己的提供者的注入器。

子模块和组件注入器彼此独立,并且会为所提供的服务分别创建自己的实例。当 Angular 销毁NgModule或组件实例时,也会销毁这些注入器以及注入器中的那些服务实例。

Angular 解析依赖过程

上面我们介绍了 Angular 中的两种注入器树:模块注入器树和元素注入器树。那么,Angular 在提供依赖时,又会以怎样的方式去进行解析呢。

在 Angular 种,当为组件/指令解析 token 获取依赖时,Angular 分为两个阶段来解析它:

  • 针对ElementInjector层次结构(其父级)
  • 针对ModuleInjector层次结构(其父级)

其过程如下(参考多级注入器-解析规则):

  • 当组件声明依赖项时,Angular 会尝试使用它自己的ElementInjector来满足该依赖。

  • 如果组件的注入器缺少提供者,它将把请求传给其父组件的ElementInjector

  • 这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先ElementInjector

  • 如果 Angular 在任何ElementInjector中都找不到提供者,它将返回到发起请求的元素,并在ModuleInjector层次结构中进行查找。

  • 如果 Angular 仍然找不到提供者,它将引发错误。

为此,Angular 引入一种特殊的合并注入器。

合并注入器(Merge Injector)

合并注入器本身没有任何值,它只是视图和元素定义的组合。

class Injector_ implements Injector {
  constructor(private view: ViewData, private elDef: NodeDef|null) {}
  get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
    const allowPrivateServices =
        this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false;
    return Services.resolveDep(
        this.view, this.elDef, allowPrivateServices,
        {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);
  }
}
Salin selepas log masuk

当 Angular 解析依赖项时,合并注入器则是元素注入器树和模块注入器树之间的桥梁。当 Angular 尝试解析组件或指令中的某些依赖关系时,会使用合并注入器来遍历元素注入器树,然后,如果找不到依赖关系,则切换到模块注入器树以解决依赖关系。

class ViewContainerRef_ implements ViewContainerData {
  ...
  // 父级试图元素注入器的查询
  get parentInjector(): Injector {
    let view = this._view;
    let elDef = this._elDef.parent;
    while (!elDef && view) {
      elDef = viewParentEl(view);
      view = view.parent!;
    }

    return view ? new Injector_(view, elDef) : new Injector_(this._view, null);
  }
}
Salin selepas log masuk

解析过程

注入器是可继承的,这意味着如果指定的注入器无法解析某个依赖,它就会请求父注入器来解析它。具体的解析算法在resolveDep()方法中实现:

export function resolveDep(
    view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef,
    notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
  //
  //          mod1
  //         /
  //       el1   mod2
  //         \  /
  //         el2
  //
  // 请求 el2.injector.get(token)时,按以下顺序检查并返回找到的第一个值:
  // - el2.injector.get(token, default)
  // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module
  // - mod2.injector.get(token, default)
}
Salin selepas log masuk

如果是<child></child>这样模板的根AppComponent组件,那么在 Angular 中将具有三个视图:

<!-- HostView_AppComponent -->
    <my-app></my-app>
<!-- View_AppComponent -->
    <child></child>
<!-- View_ChildComponent -->
    some content
Salin selepas log masuk

依赖解析过程,解析算法会基于视图层次结构,如图所示进行:

Analisis ringkas reka bentuk suntikan kebergantungan berbilang peringkat dalam Angular

如果在子组件中解析某些令牌,Angular 将:

  • 首先查看子元素注入器,进行检查elRef.element.allProviders|publicProviders

  • 然后遍历所有父视图元素(1),并检查元素注入器中的提供者。

  • 如果下一个父视图元素等于null(2),则返回到startView(3),检查startView.rootData.elnjector(4)。

  • 只有在找不到令牌的情况下,才检查startView.rootData module.injector( 5 )。

由此可见,Angular 在遍历组件以解析某些依赖性时,将搜索特定视图的父元素而不是特定元素的父元素。视图的父元素可以通过以下方法获得:

// 对于组件视图,这是宿主元素
// 对于嵌入式视图,这是包含视图容器的父节点的索引
export function viewParentEl(view: ViewData): NodeDef|null {
  const parentView = view.parent;
  if (parentView) {
    return view.parentNodeDef !.parent;
  } else {
    return null;
  }
}
Salin selepas log masuk

总结

本文主要介绍了 Angular 中注入器的层级结构,在 Angular 中有两棵平行的注入器树:模块注入器树和元素注入器树。

元素注入器树的引入,主要是为了解决依赖注入解析懒加载模块时,导致模块的双倍实例化问题。在元素注入器树引入后,Angular 解析依赖的过程也有调整,优先寻找元素注入器以及父视图元素注入器等注入器的依赖,只有元素注入器中无法找到令牌时,才会查询模块注入器中的依赖。

更多编程相关知识,请访问:编程入门!!

Atas ialah kandungan terperinci Analisis ringkas reka bentuk suntikan kebergantungan berbilang peringkat dalam Angular. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:juejin.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!