Angular-组件交互进阶
Angular组件进阶
前几天, 解决了困扰许久的Angular组件自定义部分模板问题, 才准备梳理下知识, 有了这篇文章.
老生常谈- 父子组件交互的多种形式
用过React的人大部分都能清晰地感知到, 我们所说的组件其实就是一个函数, 一个可承载Html, Css, Js逻辑的载体, 可以有效地进行功能逻辑样式的复用, 那基与这样逻辑的复用, 正常函数就会传参, 通过不同的参数定义不同的处理逻辑, 更有甚者, 通过回调函数自定义了函数进行到一个阶段的下一部动作. 其实上面这个过程就是我们所说的组件传参, 只不过是把组件换成了函数罢了. 按照这个思路, 我们就可以推理出在学习一个框架的时候, 尤其是前端目前的这种推崇组件化框架的时候, 要注意到的两个点, 参数 和 回调函数, 对于Vue和React来说, 其实网上的文章很多, Vue和react相同的props, 以及Vue的插槽, 以及两者都有的render函数, 就已经决定了这部分的难易程度, 百度百度, 看看官网总是能够解决的. 这个时候就不得不说Angular了, 真的, Angular的官网文档教程设计的比较基础, 对于进阶来说只能耐着性子去看一些优秀组件的源码, 去看看Api文档, 网上的文章真的不多. 接下来就一起来看看吧.
Angular的组件
组件生成
angular的组件生成一般情况下都是使用@angular/cli
提供的 ng generate component 'component name'
来生成的, 具体如何使用的话需要使用ng generate component --help
查看官方提供的文档, 具体如下:
options:
--defaults
When true, disables interactive input prompts for options with a default.
--dry-run (-d)
When true, runs through and reports activity without writing out results.
--force (-f)
When true, forces overwriting of existing files.
--help
Shows a help message for this command in the console.
--interactive
When false, disables interactive input prompts.
Help for schematic component
Creates a new generic component definition in the given or default project.
arguments:
name
The name of the component.
options:
--change-detection (-c)
The change detection strategy to use in the new component.
--entry-component
When true, the new component is the entry component of the declaring NgModule.
--export
When true, the declaring NgModule exports this component.
--flat
When true, creates the new files at the top level of the current project.
--inline-style (-s)
When true, includes styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file.
--inline-template (-t)
When true, includes template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file.
--lint-fix
When true, applies lint fixes after generating the component.
--module (-m)
The declaring NgModule.
--prefix (-p)
The prefix to apply to the generated component selector.
--project
The name of the project.
--selector
The HTML selector to use for this component.
--skip-import
When true, does not import this component into the owning NgModule.
--skip-tests
When true, does not create "spec.ts" test files for the new component.
--spec
When true (the default), generates a "spec.ts" test file for the new component.
--style
The file extension or preprocessor to use for style files.
--styleext
The file extension to use for style files.
--view-encapsulation (-v)
The view encapsulation strategy to use in the new component.
挑出来几个比较常用的来说一下具体用处, --skip-import
的作用是调过自动为我们引入到module
层的一步, 由我们自己去选择引入到哪个module里面. --prefix
的作用是命名生成组件selector
的前缀, 默认的话是angular.json
中定义的, 一般情况下是 app
, --inline-(style|template)
是指用行内的方式维护Html
和Css
, 不生成新的文件, --view-encapsulation
指定视图封装模式, 可以是ShadowDom Native Emulated None
四个值, 具体的用法之后再说, 其实就是指定了当前组件的css样式的作用域, 以及如何做到这种作用域(作用域相同, 实现原理不同).
生成文件分析
输入ng genernate component tab
之后会生成四个文件, tab.componet.(html|css|ts|spec.ts)
, 其中 html 是模板, css是样式, ts是组件最主要的文件, spec.ts是测试文件, 暂且不管, 当我们生成组件之后, @angular/cli
会默认给我们引入到当前生成的组件所在目录的父级module中, 然后我们就可以在当前module范围内使用selector
对应的属性值进行组件的实例化, 或者绑定在路由中.
组件的输入输出属性
Angular中的父子组件传参借助的是装饰器Input
和Output
或者@Component
的元数据inputs
outputs
实现的, 具体的使用如下:
输入属性的使用如下
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-tab',
templateUrl: './tab.component.html',
styleUrls: ['./tab.component.scss']
})
export class TabComponent implements OnInit {
@Input() name = '';
@Input() cb: (context) => void;
constructor() { }
ngOnInit() {
this.cb(this);
console.log(this.name);
}
}
// html 实例化组件
<app-tab name='hello Component' [cb]='cb'></app-tab>
// cb需要是一个函数
具体解析如下: app-tab
组件主要有两个输入属性, name
和 cb
, name是一个字符串, cb接收一个函数, 由上面的例子可以得出, 输入属性可以是任意数据类型, 但是值得注意的一点是: **输入属性如果是基本数据类型时, 组件可以通过ngOnChanges
或者set函数监听到输入属性值的变化, 但如果输入属性是引用数据类型时(即存放在栈内存的是地址, 具体的存放在堆内存中), 输入属性属性值的变化只可以通过ngDocheck
监听到, 而且这个方法极其耗费性能, 一般上我们只要改变输入属性的引用地址就可以让ngOnChanges
以及set
函数触发, 具体如下: **
<app-tab [data]='data' name='hello Component' [cb]='cb'></app-tab>
<button (click)='data.push(4)'>Click</button>
// data是一个数组
export class TabComponent implements OnInit, OnChanges {
private $data: any;
@Input() set data(value) {
console.log('set: ', value);
this.$data = value;
}
ngOnChanges(changes: SimpleChanges) {
console.log('changes: ', changes);
}
}
// 在这个时候, 点击button是不会触发任何tabcomponent身上的方法的, 因为引用地址并没有发生变化, 我们可以这样做:
<app-tab [data]='data' name='hello Component' [cb]='cb'></app-tab>
<button (click)='click()'>Click</button>
click() {
this.data.push(4);
this.data = JSON.parse(JSON.stringify(this.data));
}
这个时候通过JSON.parse(JSON.stringify())我们简单的进行了this.data的深拷贝, 并且将结果重新赋予给this.data, 这个时候, 由于引用地址发生了变化, 所以会触发到ngOnChanges和set函数
输出属性的使用:
export class TabComponent implements OnInit, OnChanges {
@Output() clickEv: EventEmitter<any> = new EventEmitter();
click() {
this.clickEv.emit('click');
}
}
<app-tab [data]='data' name='hello Component' [cb]='cb' (clickEv)='handelClick($event)'></app-tab>
handelClick($event) {
console.log($event);
}
双向绑定的奥秘
Angular中的双向绑定是指[()]
, 这种双向绑定是一种语法糖, 当你的组件同时存在property
输入属性和propertyChange
输出属性时, 就可以使用[(property)]
来简写, 如下:
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>
// ===
<app-sizer [(size)]="fontSizePx"></app-sizer>
父组件获取子组件实例并且调用子组件方法
目前我们可以通过输入输出属性和子组件进行交互, 但是现在的交互很明显在一个条条框框之内, 限制了子组件的发挥, 接下来就让我们通过@ViewChild
和模板引用变量一起获取到子组件实例, 用来调用子组件的公共方法, 属性
<app-tab #tab [data]='data' name='hello Component' [cb]='cb' (clickEv)='handelClick($event)'></app-tab>
@ViewChild('tab') tab: TabComponent;
值得注意的是, 这种方式获取到的组件实例, 只可以调用组件身上使用public
关键字声明的属性方法, 通过获取组件实例的方法, 使得我们可以更自由的封装组件, 而不拘泥于输入输出属性, 向外暴露一个又一个属性方法, 可以使我们的数据流向更加统一, 父组件只是做触发的操作, 具体的事还是交由子组件完成
组件通过service传参
service传参主要适用于兄弟级组件传参, 或者跨多层级组件传参, 其实通过输入输出属性也可以做到跨层级组件传参以及兄弟组件传参, 但是输入输出属性如果要这样做的话, 需要先给目标组件找到同一个父组件, 在父组件内进行事件的传递, 这样做的话未免太过于麻烦, 在正常项目开发中涉及到的组件也会格外的多, 事件传递会很麻烦, 所以就有了service传参, 具体如下:
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MessageService {
// 事件源, 靠这个发送消息
private message = new Subject<any>();
// Observable对象, 靠这个接受消息
public message$ = this.message.asObservable();
constructor() { }
emit(message) {
this.message.next(message);
}
}
// 发送消息
export class TabComponent implements OnInit, OnChanges {
constructor(
public message: MessageService
) { }
ngOnInit() {
console.log(this.name);
}
click() {
this.message.emit('click');
}
}
// 接收消息
export class TemplateComponent implements OnInit, AfterViewInit, AfterViewChecked {
constructor(
public message: MessageService
) { }
ngOnInit() {
this.message.message$.subscribe(
res => {
console.log('res' , res)
}
)
}
}
值得注意的是, 这个方法的service需要是同一个实例
进阶
接下来主要围绕以下几个api进行重点讲述
<ng-content></ng-content>
<ng-container></ng-container>
<ng-template></ng-template>
*ngTemplateOutlet
- 首先先说
ng-content
的具体用法
ng-content
用来做父组件到子组件的内容投影, 如果想获取投影内容对象可以通过@ContentChild
和@ContentChildren
来做, 对于投影内容的加载也有对应的生命周期函数ngContentInit
和ngContentChecked
, 具体使用如下:
<app-tab #tab [data]='data' name='hello Component' [cb]='cb' (clickEv)='handelClick($event)'>
<span>前缀</span>
<div>后缀</div>
</app-tab>
// app-tab.component.html
<p>
<ng-content select='span'></ng-content>
tab works!
<ng-content select='div'></ng-content>
</p>
<button (click)='click()'>Click</button>
// select属性为querySelector接受的参数
ng-content的缺点: 作用域为父组件, 不能有效地得到子组件的各种状态, 这导致无法基于数据自定义子组件的某部分内容(可能有方法, 但是我还没找到)
-
ng-container
意为占位符,容器, 主要用三个作用- 可以做一层无意义的标签, 类似于Vue中的
template
标签和React新版本中的Fragments
标签, 可以搭配结构型指令一起使用, 使一个标签上可以有两个结构型指令(最常见的NgIf
和NgFor
的搭配). - 通过
ViewContainerRef
对象和ViewChild
读取到当前的容器组件并且往容器内塞入动态组件或者模版(具体使用请看下文) - 通过
*ngTemplateOutlet
指令将模版内容渲染到指定位置并且定义上下文(具体使用请看下文)
- 可以做一层无意义的标签, 类似于Vue中的
-
ng-template
模版,主要使用请看:
// 最简单的使用
<ng-template #temp>
<h1>hello</h1>
</ng-template>
<ng-container [ngTemplateOutlet]='temp'>
</ng-container>
// 还可以指定ng-template内的上下文对象, 即给ng-template传参
<ng-container *ngTemplateOutlet='customTitle; context: {$implicit: "自定义标题", currIndex: 1}'>
</ng-container>
<ng-template #customTitle let-title let-index='currIndex'>
<h1>{{index}}{{title}}</h1>
</ng-template>
如上面的第二部分代码所示, 当使用ng-template
并且使用模版输入变量时(let-变量名称
),如果变量名称没有值, 则在*ngTemplateOutlet
指定上下文对象时, 则使用默认值$implicit
来给模版传参定义上下文,如果变量名称有值, 则这个值对应的就是上文对象中的属性, 我们只需要给上下文对象传值就可以了。还有一种做法如下:
<ng-template #customTitle let-title let-index='currIndex'>
<h1>{{title}}</h1>
</ng-template>
<ng-container #container></ng-container>
export class ContainerComponent implements OnInit {
@ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
@ViewChild('temp') temp: TemplateRef<any>;
constructor() { }
ngOnInit() {
this.container.createEmbeddedView(this.temp, {$implicit: 'TsTitle', currIndex: 2});
}
}
- 如何进行组件传参呢, 请看
// 父组件html
<app-list [list]='[{name: "第一个"},{name: "第二个"}]'>
<ng-template #afterTemplate let-item let-index='currIndex'>
<span>当前的后缀为: {{item.name}} {{index}}</span>
</ng-template>
</app-list>
// 子组件ts
export class ListComponent implements OnInit {
@ContentChild('afterTemplate', {static: false}) afterTemplate: TemplateRef<any>; // 通过ContentChild和模版变量获取到当前的自定义模版
@Input() list = [];
constructor() { }
ngOnInit() {
}
}
// 子组件html
<ul>
<li *ngFor='let i of list; index as index'>
{{i.name}}
<ng-container *ngTemplateOutlet='afterTemplate; context: {$implicit: i, currIndex: index}'></ng-container>
</li>
</ul>
以上就是组件进阶的全部内容, 大家有任何问题欢迎提问