Angular 中两种父子组件的组织方式

371 阅读5分钟

本文介绍两种在 Angular 中,关于处理父子组件之间关系的代码文件的组织形式。第一种形式是重量级的,我们给每一个组件外面都包裹成模块;第二种则相对比较简单,无需给子组件包裹成模块,只需要在父组件模块中对所需使用的子组件声明即可!

1. 父子组件都具有自己的模块,本质上是合作关系

组件及其模块之间的代码结构如下所示:

src/  
|-- app/  
|   |-- parent/  
|   |   |-- parent.component.ts  
|   |   |-- parent.component.html  
|   |   |-- parent.component.css  
|   |   |-- parent.module.ts  # 父组件的NgModule  
|   |   |-- parent.component.spec.ts  
|   |-- child/  
|   |   |-- child.component.ts  
|   |   |-- child.component.html  
|   |   |-- child.component.css  
|   |   |-- child.module.ts    # 子组件的NgModule  
|   |   |-- child.component.spec.ts  
|   |-- shared.service.ts  
|   |-- app.module.ts          # 根模块  
|-- assets/  
|-- index.html  
|-- styles.css  
|-- ...  

在这个结构中,父组件和子组件各自有自己的NgModule,即parent.module.tschild.module.ts。这样做的原因在于:使它们位于不同的模块或需要组织代码以便于更好的维护和可扩展性,那么为每个组件创建单独的NgModule是有益的。

父组件的NgModule (parent.module.ts):

import { NgModule } from '@angular/core';  
import { CommonModule } from '@angular/common';  
import { ParentComponent } from './parent.component';  
import { ChildModule } from './child/child.module'// 导入子组件的模块  
  
@NgModule({  
  declarations: [  
    ParentComponent // 声明父组件  
  ],  
  imports: [  
    CommonModule// 导入公共模块  
    ChildModule // 导入子组件的模块,以便父组件可以使用子组件  
  ],  
  exports: [  
    ParentComponent // 如果需要在其他NgModule中使用父组件,可以在这里导出  
  ]  
})  
export class ParentModule { }  

子组件的NgModule (child.module.ts):

import { NgModule } from '@angular/core';  
import { CommonModule } from '@angular/common';  
import { ChildComponent } from './child.component';  
  
@NgModule({  
  declarations: [  
    ChildComponent // 声明子组件  
  ],  
  imports: [  
    CommonModule // 通常需要导入公共模块  
  ],  
  exports: [  
    ChildComponent // 如果子组件需要在其他NgModule中使用,可以在这里导出  
  ]  
})  
export class ChildModule { }  

parent.module.ts中,我们导入了ChildModule,这样父组件模块就能够使用子组件。同时,ParentComponentdeclarations数组中声明,表示它是父模块的一部分。如果父组件需要在其他模块中使用,可以在exports数组中导出它。

总结

上述的组织方式可以概括为:父子组件都有自己的模块。在子组件模块中,声明然后导出子组件;在父组件模块中声明父组件,导入子组件模块;最后在父组件模板中就可以直接使用子组件了!

2. 子组件就是父组件的下级

当子组件无需形成自己的模块,且只会被一个父组件使用时,你可以直接将子组件声明在父组件的模块中,而无需为子组件创建一个单独的模块。这种情况下,代码文件的组织结构会更为简洁,因为你减少了子组件模块的相关文件。

代码文件的组织结构

src/
|-- app/
    |-- parent/
        |-- parent.component.ts
        |-- parent.component.html
        |-- parent.component.css
        |-- parent.module.ts  # 父组件的NgModule,同时声明父组件和子组件
        |-- parent.component.spec.ts
        |-- child/
            |-- child.component.ts
            |-- child.component.html
            |-- child.component.css
            |-- child.component.spec.ts
    |-- shared.service.ts
    |-- app.module.ts         # 根模块
|-- assets/
|-- index.html
|-- styles.css
|-- ...

在这个结构中,子组件的文件仍然被组织在父组件的目录下,但并没有为子组件创建单独的NgModule

对应的代码

父组件的NgModule (parent.module.ts):

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child/child.component'; // 直接导入子组件

@NgModule({
  declarations: [
    ParentComponent, // 声明父组件
    ChildComponent   // 同时声明子组件
  ],
  imports: [
    CommonModule // 导入公共模块
  ],
  exports: [
    ParentComponent // 如果需要在其他NgModule中使用父组件,可以在这里导出
    // 注意:如果子组件不需要在其他模块中使用,则无需导出
  ]
})
export class ParentModule { }

子组件 (child.component.ts):

子组件的TypeScript代码不需要改变,它仍然是一个标准的Angular组件。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
  constructor() { }

  ngOnInit() { }
}

在这种情况下,由于子组件没有自己的模块,它会被直接绑定到父组件的模块中。这样做的好处是减少了模块的复杂性,但缺点是如果子组件需要在多个地方复用,你可能需要将它移动到一个单独的模块中,或者在其他需要它的模块中重新声明。这通常适用于紧密耦合的组件,其中子组件是专门为父组件设计的,并且不太可能在其他上下文中使用。

总结

在上面的第二种做法中,无需给子组件包裹成模块;只需在父组件所在目录中创建 children 子目录然后将子组件放置其中即可!然后只需要在父组件所在的模块中声明此子组件即可!

2024年7月12日 补充 封装组件的时候使用 ng-content 和 ng-container

使用 ng-content 和 ng-container 能够实现类似于 vue 中插槽的效果,如下代码所示:

// 封装的 container 组件
import { Component, Input } from "@angular/core";

@Component({
  selector: 'alarm-container',
  template: `
    <div class="container-fluid mycard card-1" [ngClass]="outerClass">
      <div class="card-title">
        {{ title | uppercase }}
      </div>
      <div class="card-content-container">
        <ng-content></ng-content>
      </div>
    </div>
    `,
  styles: [`
    .mycard {
      min-height: 250px;
      box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0),0px 1px 5px 0px rgba(0, 0, 0, 0.12),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 1px 0px rgba(0, 0, 0, 0.2);
      padding-bottom: 12px;
    }

    .card-1 {
      min-height: 558px;
    }

    .card-2 {
      min-height: unset;
    }

    .card-title {
      font-size: 14px;
      line-height: 50px;
      color: #9E9E9E;
    }

    .card-content-container {
      padding: 0 16%;
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
    }
  `],
})
export class AlarmContainerComponent {
  @Input('title') title: string = '';
  @Input('outerClass') outerClass: string = '';
}
// 封装组件的调用者
  <div class="container-fluid pr">
    <div #mask1 class="spin-mask" [hidden]="!showListSpin">
      <mat-spinner class="list-spin" diameter="40" color="self"></mat-spinner>
    </div>

    <alarm-container title="base Detail" *ngIf="alarmData as _">
      <ng-container>
        <div class="w-49">
          <alarm-card title="Alarm ID" [content]="_.id"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Alarm Status"
            [content]="_.status"
            [isStatus]="true"
            [active]="_.status === 'Active'"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card title="Alarm Type" [content]="_.type"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Alarm Priority"
            [content]="_.priority"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Alarm Date"
            [content]="_.alarmDate"
            [isDate]="true"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card [blank]="true"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Alarm Description"
            [content]="_.description"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card [blank]="true"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card title="Error Code" [content]="_.errorCode"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Error Description"
            [content]="_.errorDesp"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Work Order Number"
            [content]="_.orderNum"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Acknowledgement ID"
            [content]="_ackId"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card title="Created By" [content]="_.createdBy"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Created Time"
            [content]="_.createdTime"
            [isDate]="true"
          ></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card title="Updated By" [content]="_.updatedBy"></alarm-card>
        </div>
        <div class="w-49">
          <alarm-card
            title="Updated Time"
            [content]="_.updatedTime"
            [isDate]="true"
          ></alarm-card>
        </div>
      </ng-container>
    </alarm-container>
  </div>

这里有两个点需要注意一下:

  1. 使用 [ngClass]="outerClass" 的方式接受来自外部的类名,这可以在样式上给与封装组件大的自由度。
  2. 在封装的组件中,我们仍然可以通过 *ngIf="alarmData as _" 这样的方式完成数据变量的重命名,以及向下传递到子组件中。