持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
接着上一篇开始,我们继续看一下剩下的几种组件间通信的方式。写文章很花费时间,希望能点个赞哦。
Service注入
通过注入Service的方式来实现组件间通信,这也是使用很频繁的一种方式。而且这种方式不限于父子组件,可以是无直接关系的组件间通信。
可以使用angular-cli去自动创建component和service。
ng g service service/tabs
import { Injectable } from '@angular/core';
//@injectable的意思是声明该服务类可被注入到其他的service、component或者其他实例中去
@Injectable({
providedIn: 'root' //providedIn 声明服务提供给哪个模块使用,
//root 实际上是 AppModule 的别名,因此不需要额外导入 AppModule。
})
export class TabsService {
constructor() { }
get(){
return '这是数据';
}
}
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
//NgModule 最根本的意义是帮助开发者组织业务代码
//开发者可以利用 NgModule 把关系比较紧密的组件组织到一起
//来定义本模块的元数据
@NgModule({
//用来放组件、指令、管道的声明
declarations: [
AppComponent,
],
//用来导入项目中需要的模块
imports: [
BrowserModule,
FormsModule
],
// 对外导出的组件, 指令,管道
exports: [],
//需要使用的 Service 都放在这里。
providers: [
XXXService
],
entryComponents: [],
//定义启动组件
bootstrap: [AppComponent]
})
export class AppModule { }
imports:
The set of NgModules whose exported declarables are available to templates in this module.
declarations:
The set of components, directives, and pipes (declarables) that belong to this module.
exports:
The set of components, directives, and pipes declared in this NgModule that can be used in the template of any component that is part of an NgModule that imports this NgModule. Exported declarations are the module's public API.
A declarable belongs to one and only one NgModule. A module can list another module among its exports, in which case all of that module's public declaration are exported.
entryComponents:
The set of components to compile when this NgModule is defined, so that they can be dynamically loaded into the view.
providers:
The set of injectable objects that are available in the injector of this module.
bootStrap:
The set of components that are bootstrapped when this module is bootstrapped. The components listed here are automatically added to entryComponents.
bootstrap一般用在根模块中AppModule中,比如上面指定了启动的component为AppComponent。
下面是main.ts的代码,并没有指定根组件,只是指定要启动 AppModule,bootstrapModule(AppModule),因为在AppModule里指定了bootstrap 组件,所以Angular知道要启动AppComponent。
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
注意,在其他前端框架里(Vue,React),并没有Module这个概念,所以在启动的时候会明确指出要渲染那个根组件。
比如在React中,index.js中明确指出了要渲染的根组件。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
我们也可以在组件里注入Service:
@Component({
selector: '...',
templateUrl: '....html',
styleUrls: ['...scss'],
providers: [XXXService]
});
然后我们就可以愉快的在component中注入service使用了。
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { TabService } from '../tab.service';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit, AfterViewInit {
...
constructor(private _tabService: TabService) { }
}
Angular Service是一种实现代码抽象的方式。它可以帮助我们将业务逻辑从页面的呈现逻辑中分离出来,以保持Component功能的纯净,以及某些常见功能的重用。Component应该只处理页面呈现以及用户交互方面的逻辑,其他的都应该抽象到service中。
日常工作中,比如我要开发一个页面叫Contact,会创建一个ContactModule,和ContactService。ContactService会在ContactModule的providers中注册一下,这样在Contact相关的Component中注入ContactService,那ContactService是个单例,这些components可以共享ContactService里的function和公共属性。也有些情况不是单例的,本文暂时不讨论。
sessionStorage和localStorage
这种方式使用起来比较简单,而且所涉及的API使用起来也很简单,虽然说这两种storage的存储都有限,但是作为日常开发是够用的。
之前,我写过一篇关于前端存储的文章,详细介绍了5种前端存储的用法以及优缺点,在这里我就不赘述了。
RXJS
在日常工作中,可以用Observable来实现父子组件间的通信,也就是要实现父组件向子组件传值,子组件向父组件传值。一般情况下,我们会在组件的options属性里,定义两个Subject,一个叫“action”subject,用来实现父组件向子组件传值;另一个叫“feedback”subject,用来实现子组件向父组件来传值。
export class UserOptions {
...
actionSubject$?: Subject<UserAction>; // action
feedbackSubject$?: Subject<UserFeedback>; // feedback
}
子组件:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit {
@Input() options!: UserOptions;
constructor() { }
ngOnInit(): void {
if (this.options.actionSubject$) {
this.options.actionSubject$.subscribe((action: UserAction) => {
// 接收父组件传来的值
...
})
}
}
print(name: string){
console.log(`Hello, I am ${name}.`);
if (this.options.feedbackSubject$){
// 子组件向父组件传值
this.options.feedbackSubject$.next(() => {
...
});
}
}
}
父组件:
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { TabService } from '../tab.service';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit, AfterViewInit {
userOptions: UserOptions;
constructor(private _tabService: TabService) { }
ngOnInit(): void {
this.userOptions = {
actionSubject$: new Subject<UserAction>(),
feedbackSubject$: new Subject<UserFeedback>()
};
this.userOptions.feedbackSubject$.subscribe((action: UserFeedback) => {
// 接收子组件传来的值
...
})
}
countAdd(): void{
this.count++;
// 向子组件传值
this.userOptions.actionSubject$.next({
...
});
}
}
@Input和@Output
@Input在日常工作中用的特别多,所以把它放到最后来说。这种方式只适合父子关系组件的通信。
@Input装饰器,用来把某个类字段标记为输入属性,并提供配置元数据。 该输入属性会绑定到模板中的某个 DOM 属性。当变更检测时,Angular 会自动使用这个 DOM 属性的值来更新此数据属性。
子组件:
<div class="container">
<p>user {{options.id}}-{{age}} works!</p>
</div>
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit, OnChanges {
@Input() age?:number;
@Input() options!: any;
constructor() { }
ngOnInit(): void {
if (this.options) {
}
}
}
父组件:
<app-user #user [options]="userInfo" [age]="age"></app-user>
{{userInfo?.id}}
<button (click)="changeUser($event)">改变UserID</button>
<button (click)="changeAge()">改变User Age</button>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private _id: number = 2;
age: number = 21;
userInfo: any = {
id: this._id,
name: 'Tom'
};
changeAge(): void {
this.age++;
}
changeUser(e: any): void {
this.userInfo.id = ++this._id;
}
}
在这种情况下,在父组件中,无论我改变userInfo,或者改变age,子组件都会重新渲染。注意,我改变的是userInfo的id属性,并没有改变它的引用。
但是我发现工作中有些情况下,我改变userInfo的属性,或者改变userInfo的引用,子组件并没有重新渲染,这是为什么呢?难道有什么玄机吗?看了代码后,发现,其实是我们写法的问题造成的。
子组件:
<div class="container">
<p>user {{userID}}-{{age}} works!</p>
</div>
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit, OnChanges {
@Input() age!: number;
@Input() options!: any;
userID?: number;
constructor() { }
ngOnInit(): void {
this.userID = this.options.id;
}
}
这种写法的话,当父组件改变userInfo的id的时候,子组件并不会重新渲染。那如果我想在父组件里改变userInfo的引用的时候,重新渲染子组件,那应该怎么做呢?可以用setter来实现,也就是监听一下输入属性的变化。
子组件:
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit, OnChanges {
private _options: any;
@Input() age!: number;
@Input()
set options(value: any){
this._options = value;
this.userID = this._options.id;
};
get options(): any {
return this._options;
}
userID?: number;
constructor() { }
ngOnInit(): void {
}
}
父组件:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private _id: number = 2;
age: number = 21;
title = 'sample1';
userInfo: any = {
id: this._id,
name: 'Tom'
};
changeAge(): void {
this.age++;
}
changeUser(e: any): void {
this.userInfo = {
id: ++this._id,
name: 'Tom'
};
}
}
注意父组件里,要改变userInfo的引用,setter才能监听的到。虽然功能是实现了,但是如果我多个输入属性,那每个输入属性都需要实现setter,好麻烦啊,有没有好的办法啊,肯定是有的。先来了解一下组件生命周期函数ngOnChanges。
ngOnChanges
一个生命周期钩子函数,当组件的任何一个可绑定属性发生变化时调用。ngOnChanges()方法来处理这些变更。
interface OnChanges {
ngOnChanges(changes: SimpleChanges): void
}
我们可以通过ngOnChanges()来监听输入属性值的变化,可根据变化做出响应。当需要监听多个,交互式输入属性的时候,这种方式比属性的setter更合适。
提到这个钩子函数,不得不说下组件生命周期。当 Angular 实例化组件类并渲染组件视图及其子视图时,组件实例的生命周期就开始了。生命周期一直伴随着变更检测,Angular 会检查数据绑定属性何时发生变化,并按需更新视图和组件实例。当 Angular 销毁组件实例并从 DOM 中移除它渲染的模板时,生命周期就结束了。当 Angular 在执行过程中创建、更新和销毁实例时,指令就有了类似的生命周期。
一旦检测到该组件或指令的输入属性发生了变化,Angular 就会调用它的 ngOnChanges() 方法。ngOnChanges() 方法获取了一个对象,它把每个发生变化的属性名都映射到了一个SimpleChange对象, 该对象中有属性的当前值和前一个值。这个钩子会在这些发生了变化的属性上进行迭代,并记录它们。
所以,我们可以得到一下结论:
- 它的用途?
它可以监听输入属性值得变化。 - 它的触发时机?
在ngOninit()之前,以及所绑定的一个或多个属性的值发生变化的时候会被调用。
比如,我现在有这样的需求,组件有两个输入属性options和age,当任何一个输入属性有变化的时候,我需要通过XHR去读取下数据。可以使用setter来实现这个需求,在options的setter里去执行XHR,在age的setter里也要执行XHR;当某一个属性发生变化时,请求了一次XHR,没有问题。但是当两个属性都发生变化时,那就会执行两次XHR,这并不是我们所期望的。这种情况,我们可以用ngOnChanges()来解决。
改写一下子组件:
<div class="container">
<p>user {{userID}}-{{age}} works!</p>
</div>
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit, OnChanges {
@Input() options!: any;
@Input() age!: number;
userID?: number;
constructor() { }
ngOnInit(): void {
console.log('user: ngOnInit');
}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
let options: any = changes['options'];
if (!options.firstChange) {
this.userID = options.currentValue.id;
}
}
print(){
console.log('Hello!');
}
}
父组件:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private _id: number = 2;
age: number = 21;
title = 'sample1';
userInfo: any = {
id: this._id,
name: 'Tom'
};
changeAge(): void {
this.age++;
}
changeUser(e: any): void {
// this.userInfo = {
// id: ++this._id,
// name: 'Tom'
// };
this.userInfo = Object.assign({},this.userInfo,{
id: ++this._id,
});
}
}
从运行效果来看,可以在ngOnChanges里的changes对象拿到所有输入属性的值的变化,还可以知道是否是第一次变化,还有上一次的值。
注意,输入属性的值变化指的是引用的变化,如果只是某个属性的变化,并不会引起ngOnChanges的调用,这一点是和setter一样的。
@Output
提到@Input,不得不提下@Output,他们俩就像一对亲兄弟一样。Output可以实现子组件的数据传递出去给父组件。
Angular提供了EventEmitter来触发自定义事件。子指令创建一个 EventEmitter 实例,并将其作为输出属性导出。子指令调用已创建的 EventEmitter 实例中的 emit(payload)方法来触发一个事件,父指令通过事件绑定(eventName)的方式监听该事件,并通过 $event 对象来获取payload对象。
子组件:
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit, OnChanges {
@Input() options!: any;
@Input() age!: number;
@Output() sendData: EventEmitter<any> = new EventEmitter<any>();
userID?: number;
constructor() { }
ngOnInit(): void {
console.log('user: ngOnInit');
}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
let options: any = changes['options'];
if (!options.firstChange) {
this.userID = options.currentValue.id;
this.sendData.next('我是子组件的数据');
}
}
}
父组件:
<app-user [options]="userInfo" [age]="age" (sendData)="handleData($event)"></app-user>
{{userInfo?.id}}
<button (click)="changeUser($event)">改变UserID</button>
<button (click)="changeAge()">改变User Age</button>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
...
changeUser(e: any): void {
// this.userInfo = {
// id: ++this._id,
// name: 'Tom'
// };
this.userInfo = Object.assign({},this.userInfo,{
id: ++this._id,
});
}
handleData($event: any): void {
console.log(`父组件获得了来自子组件的值:${$event}`);
}
}
我们需要思考一个问题,如果我们想在子组件里改变options的值,应该怎么办?那就直接改吧,也是可以的,但是非常不建议这么去做,子组件改变options,子组件会更新,也会导致父组件的更新,这样做会让你的数据流变的难以追踪,难以定位问题。
从设计模式上的角度,一般都会采用单项数据流的模式,也就是说,父组件给子组件传递的值,只有父组件可以改变这个值,子组件如果想改变这个值,就把告诉父组件,让父组件来更新这个值。这个并不是Angular的专属模式,在其他框架里也是采用了这种模式。
在Vue2.0中,对于自定义事件,在子组件中使用vm.$emit来触发事件,在父组件中可以监听这个事件。
子组件Child:
<template>
<section>
<el-button type="primary" @click="isClick">点击</el-button>
</section>
</template>
<script>
export default {
methods: {
isClick() {
// 触发子组件上的自定义事件"move"
this.$emit('move', '点击事件!');
}
}
}
</script>
父组件:
<template>
<child @move="moveHandle"></child>
</template>
<script>
export default {
methods: {
moveHandle(e) {
// 响应子组件上的自定义事件"move"
console.log(e);
}
}
}
</script>
在Vue3.0中,去掉了vm.$emit API,在Vue3中去掉了烦人的this。
子组件Child:
<template>
<button @click="emitFn">事件分发</button>
</template>
<script lang='ts'> import { defineComponent } from 'vue';
export default defineComponent({
name: 'Child',
setup(props, { emit }) {
const emitFn = () => {
emit('show');
};
return {
emitFn,
};
},
});
</script>
父组件:
<template>
<child @show="handleShow" />
</template>
<script lang='ts'>
setup() {
const handleShow = () => {
console.log('name:', 'hzw');
};
return {
handleShow
};
},
</script>
ngrx
Ngrx是Angular应用中实现全局状态管理的Redux架构的解决方案,在日常工作中,使用频率也很高。
我之前写过一篇关于Redux的文章,相信介绍了各个API,以及Redux的原理,并且手写了一个简版的Redux,在React里是可以正常工作的,有兴趣的朋友们可以去看下那篇文章,在这里我就跳过了。
总结
至此,我们总结了8种常用的组件间通信的方式,实际上@Input和@Output算是两种通信方式,但是我们把它们总结在了一起,毕竟父组件向子组件传值的时候,我们也应该考虑一下子组件向父组件传值的情况。后续打算会写一下Angular的其他话题,敬请期待。