Angular指令之ngComponentOutlet解析

1,531 阅读3分钟

Angular指令之ngComponentOutlet解析

前言

上一篇文章我们介绍了ngTemplateOutlet的使用,本文继续介绍另一个类似指令ngComponentOutlet

前提你已了解Angular框架的基本使用,知晓指令等相关概念及作用。

  • ng版本:v13.3.x
  • 源码:地址

功能

ngComponentOutlet:实例化单个Component组件,并将其(宿主视图)插入到当前视图中。

用法

基本用法

@Component({
  selector: 'app-hello',
  template: `
    <p>hello,EnochGao!</p>
  `,
  styles: []
})
export class HelloComponent{  // hello模板组件
}

@Component({
  selector: 'app-root',
  template: `
    <ng-container *ngComponentOutlet="component"></ng-container>
  `,
  styles: []
})
export class AppComponent{ // app组件
  component = HelloComponent; // 引用模板组件
}

效果图:

image.png 可以看到ngComponentOutlet指令将hello组件渲染到app组件中。

其他控制项

它还有一些其他配置项,可以精确实现组件的创建:

  • ngComponentOutletInjector:可选的,自定义 Injector,将用作此组件的父级。默认为当前视图容器的注入器。
  • ngComponentOutletContent:可选的,要插入到组件内容部分的可投影节点列表(如果存在)
  • ngComponentOutletNgModuleFactory: 可选的,模块工厂允许动态加载其他模块,然后从该模块加载组件。(v14版本将会废弃,改用ngComponentOutletNgModule)
@Injectable()
export class NameService { // 一个服务
  myName = 'EnochGao';
}

@Component({
  selector: 'app-content',
  template: `
    <span>{{text}}</span>
    <div>
      name:{{nameService.myName}}
    </div>
    <ng-content></ng-content>
    <ng-content></ng-content>
  `,
  styles: [`
    :host{
      display:block;
      border:1px solid red;
    }
   `
  ]
})
export class ContentComponent { // content模板组件,依赖服务
  text = 'content组件已加载!';
  constructor(public nameService: NameService) { }
}


@Component({
  selector: 'app-root',
  template: `
  <ng-container *ngComponentOutlet="contentComponent;injector: myInjector;content: myContent">
  </ng-container>
  `,
  style: []
})
export class AppComponent { // app组件
  contentComponent = ContentComponent;
  myInjector: Injector;
  myContent: any[][];

  constructor(
    private injector: Injector,
    @Inject(DOCUMENT) private document: Document
  ) {
    const div1 = this.document.createElement('div');
    const text1 = this.document.createTextNode('投影内容1...');
    div1.appendChild(text1);

    const div2 = this.document.createElement('div');
    const text2 = this.document.createTextNode('投影内容2...');
    div2.appendChild(text2);

    this.myContent = [[div1], [div2]];
    this.myInjector = Injector.create({
    providers: [{ provide: NameService, deps: [] }],
    parent: this.injector
    });
  }
}

效果图: image.png

此外我们还可以指定ngComponentOutletNgModuleFactory(v14版本中模块工厂会被删除,将会直接传递moduleRef)

@Component({
  selector: 'app-core-demo',
  template: `
    <span>{{text}}</span>
  `,
  styles: [`
    :host{
      display:block;
      border:1px solid green;
    }`
  ]
})
export class CoreDemoComponent { // core组件
  text = 'core组件已加载!';

}

@NgModule({
  declarations: [
    CoreDemoComponent
  ],
  imports: [
    CommonModule,
  ]
})
export class CoreModule { // core模块,注意后续我们不会将coreModule引入到AppModule中
// 看看是否会加载成功
}


@Component({
  selector: 'app-root',
  template: `
    <ng-container *ngComponentOutlet="coreComponent;ngModuleFactory:factory"></ng-container>
  `,
  style: []
})
export class AppComponent { // app组件
  coreComponent = CoreDemoComponent; // 引入core组件
  factory: NgModuleFactory<any>;

  constructor(
    private compiler: Compiler,
  ) {
   this.factory = this.compiler.compileModuleSync(CoreModule);
  }
}

效果图:

image.png

源码分析

@Directive({ selector: '[ngComponentOutlet]' })
export class NgComponentOutlet implements OnChanges, OnDestroy {
  @Input() ngComponentOutlet!: Type<any>;
  @Input() ngComponentOutletInjector!: Injector;
  @Input() ngComponentOutletContent!: any[][];
  @Input() ngComponentOutletNgModuleFactory!: NgModuleFactory<any>;

  private _componentRef: ComponentRef<any> | null = null;
  private _moduleRef: NgModuleRef<any> | null = null;

  constructor(private _viewContainerRef: ViewContainerRef) { }

  ngOnChanges(changes: SimpleChanges) {

    this._viewContainerRef.clear();
    this._componentRef = null;

    if (this.ngComponentOutlet) {
      const elInjector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector;

      if (changes['ngComponentOutletNgModuleFactory']) {
        if (this._moduleRef) {
          this._moduleRef.destroy();
        }

        if (this.ngComponentOutletNgModuleFactory) {
          const parentModule = elInjector.get(NgModuleRef);
          this._moduleRef = this.ngComponentOutletNgModuleFactory.create(parentModule.injector);
        } else {
          this._moduleRef = null;
        }
      }

       // 找到解析组件工厂
      const componentFactoryResolver = this._moduleRef ?
        this._moduleRef.componentFactoryResolver :
        elInjector.get(ComponentFactoryResolver);

      const componentFactory = componentFactoryResolver.resolveComponentFactory(this.ngComponentOutlet);

      this._componentRef = this._viewContainerRef.createComponent(
        componentFactory,
        this._viewContainerRef.length,
        elInjector,
        this.ngComponentOutletContent
      );
    }
  }

  ngOnDestroy() {
    if (this._moduleRef) {
      this._moduleRef.destroy();
    };
  }
}

其实代码的核心实现思路是:找到传入组件的componentFactoryResolver(组件工厂解析器)这个工厂对象,然后通过ViewContainerRef的createComponent方法,将组建渲染到视图容器中。至于传入模块工厂的方式:首先找到传入组件对应的模块引用,模块引用下有对应组件的组件工厂解析器。

可见ViewContainerRef这个类功能强大,ngTemplateOutlet模板渲染,最终也是通过它来创建的。

思维扩展

实际项目中是否存在动态加载组件的需求,比如一个广告位轮播功能(每一个广告是一个组件,通过ngComponentOutlet我们可以实现广告的动态轮播)。

总结

我们可以通过ngComponentOutlet指令的形式加载组件,而不是在模板中通过标签的形式加载。它可以实现通过代码进行灵活加载组件,将父组件与子组件进行解耦,符合开闭原则。

后续

可见ViewContainerRef的强大,下次为大家介绍它。