이 글에서는 angular10의 컴포넌트에 대해 소개하고, 컴포넌트 요소, 컴포넌트 수명주기, 컴포넌트 간 통신, 동적 컴포넌트의 기본 사용법을 함께 살펴보겠습니다.
【관련 튜토리얼 추천: "angular tutorial"】
//从angular主模块中引入Component(组件装饰器或组件注解) import { Component } from '@angular/core'; //装饰器中以json的形式声明元数据 @Component({ //它指定了一个叫 <app-root> 的元素。 该元素是 index.html 文件里的一个占位符 //为什么这个组件跟入口index建立了联系呢?因为入口main.ts中绑定了主模块为appModule selector: 'app-root', //在模板中找对应的标签,找到后创建并插入该组件实例 templateUrl: './app.component.html', // html模板 styleUrls: ['./app.component.css'], // css样式,可以引入多个css文件 // 这个属性(内联模板)和templateUrl(外联模板)二选一,template后面可以直接跟html字符串 // 注意在模板语法(反引号)中是使用插值表达式,不能使用${}插入值 template: `<h1>{{title}}</h1>` }) //组件控制器,写逻辑代码的地方 export class AppComponent { title = 'myAngular'; //构造函数可以用来进行属性的声明和初始化语句 //在angular里面有个特别重要的点要记住:只能通过构造函数注入依赖 constructor() {} }
이 두 가지 조건이 충족되지 않으면 빌드 환경을 참조하세요.
Angular CLI를 사용하여 컴포넌트 생성
ng generate component <project-name> // 创建一个组件 ng g c <project-name> // 缩写
컴포넌트 생성에 일반적으로 사용되는 기타 옵션
ng g c <project-name> --skip-tests // 创建一个组件,且不用安装测试文件 ng g c <project-name> --inline-style // 缩写-s,内联样式 ng g c <project-name> --inline-template // 缩写-t,内联模板 ng g c <project-name> --module= <module-name> // 指定创建的组件在哪个模块引用,在多个模块的项目中会用到
Angular CLI를 통해 자동으로 컴포넌트를 생성하는 것 외에도 수동으로 컴포넌트를 생성할 수도 있습니다(권장하지 않음) ), 여기에는 소개가 없습니다.
작업에서 자주 사용하는 라이프사이클은 ngOninit, ngOnDestroy 2개 뿐이며, 나머지 라이프사이클은 다음과 같습니다. 매우 드물게 사용되지만 구성요소의 수명주기를 마스터할 수 있다면 각도에 대한 더 깊은 이해를 갖게 될 것입니다.
라이프 사이클 의미
컴포넌트 인스턴스의 라이프 사이클은 Angular가 컴포넌트 클래스를 인스턴스화하고 컴포넌트 뷰와 해당 하위 뷰를 렌더링할 때 시작됩니다. 수명 주기에는 항상 변경 감지가 수반되며, 데이터 바인딩된 속성이 변경되면 Angular가 확인하고 필요에 따라 뷰 및 구성 요소 인스턴스를 업데이트합니다. Angular가 구성 요소 인스턴스를 삭제하고 DOM에서 렌더링된 템플릿을 제거하면 수명 주기가 종료됩니다. 지시문은 Angular가 실행 중에 인스턴스를 생성, 업데이트 및 삭제하는 것과 유사한 수명 주기를 갖습니다.
애플리케이션:
앱은 수명 주기 후크 메서드를 사용하여 구성 요소 또는 지시어 수명 주기의 주요 이벤트를 트리거하여 새 인스턴스를 초기화하고, 필요할 때 변경 감지를 시작하고, 변경 감지 중 업데이트에 응답하고, 필요할 때 인스턴스를 삭제할 수 있습니다. .
라이프 사이클 이벤트 구현 방법
모든 구성 요소 또는 지시문은 적절한 시간에 구성 요소 또는 지시문 인스턴스에서 작동할 수 있는 하나 이상의 수명 주기 후크를 구현할 수 있습니다.
각 인터페이스에는 고유한 후크 방법이 있으며 해당 이름은 인터페이스 이름과 ng 접두사로 구성됩니다. 예를 들어 OnInit 인터페이스의 후크 메서드는 ngOnInit()이라고 합니다. 이 메서드를 구성 요소 또는 지시문 클래스에 구현하면 Angular는 먼저 구성 요소 또는 지시문의 입력 속성을 확인한 후 이를 호출합니다.
import { Component } from '@angular/core'; @Component({ selector: 'app-root', styleUrls: ['./app.component.css'], template: `<h1>{{title}}</h1>` }) // 实现OnInit生命周期,可以实现多个生命周期 export class AppComponent implements OnInit{ title = 'myAngular'; constructor() {} ngOnInit(){ console.log("Angular在首次检查完组件的输入属性后,然后调用它,只调用一次") } }
Lifecycle 개요
Hook | Timing | Purpose | Note |
---|---|---|---|
ngOnChanges() | 바인딩된 입력 속성의 값이 변경될 때 호출되며, 첫 번째 호출은 ngOnInit()보다 먼저 발생해야 합니다. | Response 각도일 때 데이터 바인딩된 입력 속성을 설정하거나 재설정합니다. 이 메서드는 현재 및 이전 속성 값의 SimpleChanges 개체를 허용합니다. | 이 일은 매우 자주 발생하므로 여기에서 수행하는 모든 작업은 성능에 큰 영향을 미칩니다. |
ngOnInit() | ngOnChanges()의 첫 번째 라운드가 완료된 후 호출되며 한 번만 호출됩니다. | Angular가 먼저 데이터 바인딩을 표시하고 지시문/구성 요소의 입력 속성을 설정한 후 지시문/구성 요소를 초기화합니다. 컴포넌트가 초기 데이터를 얻기에 좋은 곳입니다 | 변경 감지가 수행될 때마다 ngOnChanges() 직후에, 변경 감지가 수행될 때 ngOnInit() 직후에 한 번만 호출하는 것이 중요합니다 |
ngDoCheck() | 처음으로. | Angular가 자체적으로 감지할 수 없거나 감지할 의사가 없는 변경 사항이 발생하면 감지하고 대응합니다. | ngOnChanges만큼 자주 발생합니다 |
ngAfterContentInit() | 처음 ngDoCheck()가 호출될 때 한 번만. | Angular가 외부 콘텐츠를 구성 요소 뷰 또는 지시어가 있는 뷰에 투영한 후에 호출됩니다. | 한 번만 호출됩니다 |
ngAfterContentChecked() | ngAfterContentInit() 및 각 ngDoCheck() 후에 | Angular가 구성 요소 또는 지시문에 투영된 콘텐츠를 확인할 때마다 호출됩니다. | |
ngAfterViewInit() | ngAfterContentChecked()가 처음 호출될 때 한 번만 호출됩니다. | 구성요소 뷰와 해당 하위 뷰를 초기화한 후 호출됩니다. | 한 번만 호출 |
ngAfterViewChecked() | ngAfterViewInit()하고 ngAfterContentChecked() 이후 매번 호출하세요. | 컴포넌트 뷰와 하위 뷰의 변경 감지를 완료한 후 매번 호출됩니다. | |
ngOnDestroy() | Angular가 지시문/구성 요소를 삭제하기 전에 호출됩니다. | 각 지시문/구성 요소가 Angular에 의해 삭제되기 전에 호출되고 청소됩니다. 메모리 누수를 방지하려면 관찰 가능 항목의 구독을 취소하고 여기에서 이벤트 핸들러를 분리하세요. 관찰 가능한 개체 구독 취소, 타이머 지우기, 이 명령으로 전역적으로 또는 애플리케이션 서비스에 등록된 모든 콜백 등록 취소. | 중요해요 |
重点生命周期详解
初始化组件和指令 ngOnInit
在实例销毁时进行清理 ngOnDestroy
这里是释放资源的地方,这些资源不会自动被垃圾回收。如果你不这样做,就存在内存泄漏的风险。
页面埋点
可以在组件中通过ngOnInit和ngOnDestroy方法统计页面的时长,
更好的做法可以通过指令实现ngOnInit和ngOnDestroy生命周期,用来统计页面时长
也可以通过路由守卫来记录页面出入栈所需要的时间
@Directive({selector: '[appSpy]'}) export class SpyDirective implements OnInit, OnDestroy { constructor(private logger: LoggerService) { } ngOnInit() { this.logIt(`onInit`); } ngOnDestroy() { this.logIt(`onDestroy`); } private logIt(msg: string) { this.logger.log(`Spy #${nextId++} ${msg}`); } }
使用变更检测钩子 ngOnchanges
一旦检测到该组件或指令的输入属性发生了变化,Angular 就会调用它的 ngOnChanges() 方法。
因为这个方法会频繁执行,所以要注意判断的计算量,会影响性能。
// 可以监听输入属性的变化 ngOnChanges(changes: SimpleChanges) { for (const propName in changes) { const chng = changes[propName]; const cur = JSON.stringify(chng.currentValue); const prev = JSON.stringify(chng.previousValue); this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`); } }
一、父子组件的通信(基础)
父传子
1、父组件通过属性绑定向子组件传值
import { Component } from '@angular/core'; @Component({ selector: 'app-parent', template: ` <app-child [msg]="msg"></app-child> ` }) export class ParentComponent { msg = '父组件传的值'; }
2、子组件通过@Input接收数据
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-child', template: ` <p>{{msg}}</p> ` }) export class ChildComponent { @Input() msg: String; }
子传父
1、子组件通过自定义事件,向父组件发送数据
import { Component, Input, EventEmitter, Output} from '@angular/core'; , @Component({ selector: 'app-child', template: ` <p>{{msg}}</p> <button (click)="vote()" >发送</button> ` }) export class ChildComponent { @Input() msg: String; @Output() voted = new EventEmitter<boolean>(); vote() { this.voted.emit("子组件传的值"); } }
2、父组件通过监听自定义事件,接收子组件的传值
import { Component } from '@angular/core'; @Component({ selector: 'app-parent', template: ` <app-child [msg]="msg" (voted)="voted($event)"></app-child> ` }) export class ParentComponent { msg = '父组件传的值'; voted(val){ //监听自定义事件的传值 console.log(val) } }
子组件怎么监听输入属性值的变化?(2种方法)
1、可以使用一个输入属性@Input()的 setter,以拦截父组件中值的变化。
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-child', template: '<h3>"{{name}}"</h3>' }) export class ChildComponent { @Input() get name(): string { return this._name; } set name(name: string) { this._name = (name && name.trim()) || '<no name set>'; } private _name = ''; }
2、通过ngOnChange()来截听输入属性值的变化
当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; @Component({ selector: 'app-version-child', template: ` <h3>Version {{major}}.{{minor}}</h3> <h4>Change log:</h4> <ul> <li *ngFor="let change of changeLog">{{change}}</li> </ul> ` }) export class VersionChildComponent implements OnChanges { @Input() major: number; @Input() minor: number; changeLog: string[] = []; ngOnChanges(changes: SimpleChanges) { //ngOnchange适合监听子组件多个输入属性的变化 const log: string[] = []; for (const propName in changes) { const changedProp = changes[propName]; const to = JSON.stringify(changedProp.currentValue); if (changedProp.isFirstChange()) { log.push(`Initial value of ${propName} set to ${to}`); } else { const from = JSON.stringify(changedProp.previousValue); log.push(`${propName} changed from ${from} to ${to}`); } } this.changeLog.push(log.join(', ')); } }
父组件怎么读取子组件的属性和调用子组件的方法?(2种方法)
1、通过本地变量代表子组件
父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。
思考:父组件可以通过这种方式读取子组件的私有属性和私有方法吗?
父组件
import { Component } from '@angular/core'; import { CountdownTimerComponent } from './countdown-timer.component'; @Component({ selector: 'app-countdown-parent-lv', template: ` <h3>Countdown to Liftoff (via local variable)</h3> <button (click)="timer.start()">Start</button> //调用子组件方法 <button (click)="timer.stop()">Stop</button> <div class="seconds">{{timer.seconds}}</div> //读取子组件属性 <app-countdown-timer #timer></app-countdown-timer> `, styleUrls: ['../assets/demo.css'] }) export class CountdownLocalVarParentComponent { }
子组件
import { Component, OnDestroy } from '@angular/core'; @Component({ selector: 'app-countdown-timer', template: '<p>{{message}}</p>' }) export class CountdownTimerComponent implements OnDestroy { intervalId = 0; message = ''; seconds = 11; ngOnDestroy() { this.clearTimer(); } start() { this.countDown(); } stop() { this.clearTimer(); this.message = `Holding at T-${this.seconds} seconds`; } private clearTimer() { clearInterval(this.intervalId); } private countDown() { this.clearTimer(); this.intervalId = window.setInterval(() => { this.seconds -= 1; if (this.seconds === 0) { this.message = 'Blast off!'; } else { if (this.seconds < 0) { this.seconds = 10; } // reset this.message = `T-${this.seconds} seconds and counting`; } }, 1000); } }
2、父组件调用@viewChild() (基础,推荐使用)
这个本地变量方法是个简单便利的方法。但是它也有局限性(只能在模板html中使用),因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的ts代码对子组件没有访问权。
当父组件类需要访问子组件时,可以把子组件作为 ViewChild,注入到父组件里面。
父组件类中访问子组件的属性和方法:
import { AfterViewInit, ViewChild } from '@angular/core'; import { Component } from '@angular/core'; import { CountdownTimerComponent } from './countdown-timer.component'; //引入子组件 @Component({ selector: 'app-countdown-parent-vc', template: ` <h3>Countdown to Liftoff (via ViewChild)</h3> <button (click)="start()">Start</button> <button (click)="stop()">Stop</button> <div class="seconds">{{ seconds() }}</div> <app-countdown-timer></app-countdown-timer> `, styleUrls: ['../assets/demo.css'] }) export class CountdownViewChildParentComponent implements AfterViewInit { //通过 @ViewChild 属性装饰器,将子组件 CountdownTimerComponent 注入到私有属性 timerComponent 里面。 @ViewChild(CountdownTimerComponent) private timerComponent: CountdownTimerComponent; seconds() { return 0; } // angular创建了组件的子视图后会调用它,注意获取子组件的属性,要在子组件视图加载之后 ngAfterViewInit() { // 访问子组件属性 setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0); } start() { this.timerComponent.start(); } // 访问子组件的方法 stop() { this.timerComponent.stop(); } }
注意:(使用场景很多,必须掌握)
ngAfterViewInit() 生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0.
然后 Angular 会调用 ngAfterViewInit 生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮。
使用 setTimeout() 来等下一轮,然后改写 seconds() 方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。
二、组件通过服务来通信(发布订阅者模式,基础,必须掌握)
父组件和它的子组件共享同一个服务,利用该服务在组件家族内部实现双向通信。
不仅局限于父子组件,只要组件与组件共享同一个服务,就可以实现数据通信。
<!--parent.component.html--> <p style="width: 1000px;margin: auto"> <p class="card" style="width: 500px;float: left"> <p class="card-header"> 父组件</p> <p class="card-body"> <h5 class="card-title">父组件</h5> <p class="form-group"> <label for="serviceoutput">父组件服务输入:</label> <input type="text" class="form-control" id="serviceoutput" placeholder="服务输入" [(ngModel)]="serviceInput" > </p> <button class="btn btn-primary" (click)="clickService()">Service方式</button> </p> </p> <app-child></app-child> </p>
<!--child.component.html--> <p class="card" style="width: 500px;"> <p class="card-header">子组件</p> <p class="card-body"> <h5 class="card-title">子组件</h5> <p class="form-group"> <label for="serviceoutput">子组件服务输入:</label> <input type="text" class="form-control" id="serviceoutput" placeholder="服务输入" [(ngModel)]="serviceInput" > </p> <button class="btn btn-primary" (click)="clickService()">Service方式</button> </p> </p>
//服务重点 //meditor.service.ts import {Injectable} from '@angular/core'; import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; @Injectable() export class MeditorService { private subject = new Subject<MeditorMsg>(); constructor() {} // 获取订阅者 public getObservable(): Observable<MeditorMsg> { return this.subject.asObservable(); } // 推送信息 public push(msg: MeditorMsg) { this.subject.next(msg); } } // 中间者信息 export interface MeditorMsg { id: string; body: any; }
subscription: Subscription = null; //初始化一个订阅对象 //子组件构造函数,用于监听数据推送 constructor(private meditor: MeditorService) { this.subscription = meditor.getObservable().subscribe( msg => { console.log(msg); if (msg.id === 'parent') { //id为parent,获取父组件数据 this.serviceInput = msg.body; } } ); } // 子组件将数据推送到中间着,给订阅者 clickService() { this.meditor.push({id: 'parent', body: this.serviceInput}); } //父组件构造函数,用于监听数据推送 constructor(private meditor: MeditorService) { this.subscription = meditor.getObservable().subscribe( msg => { console.log(msg); if (msg.id === 'child') { //id为child,获取子组件数据 this.serviceInput = msg.body; } } ); } // 父组件将数据推送到中间着,给订阅者 clickService() { this.meditor.push({id: 'parent', body: this.serviceInput}); } // 注意:订阅一个对象,就是在生命周期结束前,要取消订阅。 ngOnDestroy() { this.subscription.unsubscribe(); }
思考: 这种发布订阅者模式适合全局状态管理吗?
三、可以通过本地缓存来实现通信(Cookie,LocalStorage、SessionStorage)
<!-- catch_namae_type.ts --> // 目的是好维护 // 项目当中用到的页面缓存,需要在这里进行声明;key-value保持一致 // 声明规则,不同类型的缓存使用前缀 session_/ local_ / cookie_ // 动态设置缓存不用在这里声明,但是需要在key后面加'_noSetType_'标识 export const CatchNameType = { session_userInfo: 'session_userInfo', // 用户信息 session_toekn: 'session_token', // token local_loginInfo: 'local_loginInfo', // 本地缓存用户名密码 }; <!--catch.ts--> import { Injectable } from '@angular/core'; // 定义这个类,主要是看全局定义了哪些本地缓存 import { CatchNameType } from './catch_namae_type'; // -------------------------------------------------------缓存工具类(三类方法) // Cookie (方法有:set/get/remove) // SStorage(sessionStorage) (方法有:set/get/remove/clear) // LStorage(localStorage) (方法有:set/get/remove/clear) @Injectable({ providedIn: 'root', }) export class Catch { // cookie public static Cookie = { /** * cookie 存贮 * @param key 属性 * @param value 值 * @param String expire 过期时间,单位天 */ set(key: string, value: any, expire: any): void { if (Catch.is_set_catch_name_type(key)) { const d = new Date(); d.setDate(d.getDate() + expire); document.cookie = `${key}=${value};expires=${d.toDateString()}`; } }, get(key: string): string { const cookieStr = unescape(document.cookie); const arr = cookieStr.split('; '); let cookieValue = ''; // tslint:disable-next-line: prefer-for-of for (let i = 0; i < arr.length; i++) { const temp = arr[i].split('='); if (temp[0] === key) { cookieValue = temp[1]; break; } } return cookieValue; }, remove(key: string): void { document.cookie = `${encodeURIComponent(key)}=;expires=${new Date()}`; }, }; // sessionStorage public static SStorage = { set(key: string, value: any): void { if (Catch.is_set_catch_name_type(key)) { sessionStorage.setItem(key, JSON.stringify(value)); } }, get(key: string): any { const jsonString = sessionStorage.getItem(key) === 'undefined' ? undefined : sessionStorage.getItem(key); return jsonString ? JSON.parse(jsonString) : null; }, remove(key: string): void { sessionStorage.removeItem(key); }, clear(): void { sessionStorage.clear(); }, }; // localStorage public static LStorage = { set(key: string, value: any): void { if (Catch.is_set_catch_name_type(key)) { localStorage.setItem(key, JSON.stringify(value)); } }, get(key: string): any { const jsonString = localStorage.getItem(key) === 'undefined' ? undefined : localStorage.getItem(key); return jsonString ? JSON.parse(jsonString) : null; }, remove(key: string): void { localStorage.removeItem(key); }, clear(): void { localStorage.clear(); }, }; // 设置缓存的时候是否在catch_name_type里面声明 static is_set_catch_name_type(key: string): boolean { let allow = false; // 对动态设置缓存不进行检查 if (key.indexOf('_noSetType_') !== -1) { allow = true; console.log('动态设置缓存', key); return allow; } // 对命名规则进行检查 const nameRule = key.indexOf('session_') !== -1 || key.indexOf('local_') !== -1 || key.indexOf('cookie_') !== -1; if (!nameRule) { allow = false; console.log('命名规则错误', key); return allow; } // 静态设置的缓存需要配置类型 Object.values(CatchNameType).forEach((item) => { if (item === key) { allow = true; } }); if (!allow) { console.log('缓存操作失败,请检查配置缓存类型'); } return allow; } }
四、页面路由传参也可以实现单向通信
这部分内容,我会在路由章节整理。
组件通信总结
所以组件通信大概有如下几种:
1、父子组件通信(1、@Input() @output 2、本地变量#val 3、@viewChild())
2、通过服务
3、页面缓存
4、页面级组件传参(两个页面等同于两个组件)
组件的模板不会永远是固定的。应用可能会需要在运行期间按需加载一些新的组件。 通过下面的例子可以了解动态组件的基本使用
1、创建组件,被引入的组件
@Component({ template: `<span>hello world</span>` }) export class DynamicComponent { }
2、创建容器组件,用于加载动态组件
@Component({ selector: 'app-container', template: ` <div #dynamicContainer></div> <button (click)="createComponent()">Create</button> ` }) export class AppContainerComponent { // 声明容器 @ViewChild("dynamicContainer", { read: ViewContainerRef }) container: ViewContainerRef; }
在AppContainerComponent组件中,通过@ViewChild装饰器来获取视图中的模板元素,如果没有指定第二个查询参数,则默认返回 组件实例或相应的DOM元素,但这个示例中,我们需要获取ViewContainerRef实例也就是视图容器。可以在视图容器中创建、插入、删除组件等。
3、动态创建组件
在创建组件之前,需要注入ComponentFactoryResolver服务对象,该服务是动态加载组件的核心,可以将一个组件实例呈现到另一个 组件视图上。
//依赖组件类型获取对应的factory,从名字上可以看出该factory是用来初始化组件的 const componentFactory = this.ComponentFactoryResolver(DynamicComponent); //调用视图容器的createComponent方法并将组件添加到容器上。该方法内部会调用factory的create方法来初始化组件。 const modalContainerRef = this.container.createComponent(componentFactory);
4、为组件属性赋值
通过如下的方式为组件属性进行赋值
modalContainerRef.instance.property = ***;
5、销毁组件
在使用组件后,记得要销毁组件。
modalContainerRef.destroy();
6、组件注册
为了能够动态创建组件需要将组件在模块的entryComponents中声明。因为在entryComponents中声明的组件Angular都会创建一个 ComponentFactory并将其存储在ComponentFactoryResolver中,这是动态加载必需的步骤。
更多编程相关知识,请访问:编程入门!!
위 내용은 Angular10 컴포넌트 관련 개념을 한 글에서 살펴보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!