InfoPool

私人信息记录

0%

ASP.NET Core 支持使用缓冲模型绑定(针对较小文件)和无缓冲流式传输(针对较大文件)上传一个或多个文件。

小型和大型文件

小型和大型文件的定义取决于可用的计算资源。 应用应对存储方法进行基准测试,以确保它可以处理预期的大小。 基准内存、CPU、磁盘和数据库性能。

虽然无法针对部署的“小”与“大”提供特定边界,但以下是 AspNetCore 针对 FormOptions 的一些相关默认值:

  • 默认情况下, HttpRequest.Form 不会缓冲整个请求正文 (BufferBody) ,但会缓冲包含的任何多部分表单文件。
  • MultipartBodyLengthLimit 是缓冲表单文件的最大大小,默认值为 128MB。
  • MemoryBufferThreshold 指示在转换为磁盘上的缓冲区文件之前,内存中的文件缓冲量,默认为 64KB。 MemoryBufferThreshold 充当小型和大型文件之间的边界,这些文件根据应用资源和方案而引发或

文件上传方案

缓冲和流式传输是上传文件的两种常见方法。

缓冲

将整个文件读入 IFormFile。 IFormFile 是用于处理或保存文件的文件的 C# 表示形式。

文件上传使用的磁盘和内存取决于并发文件上传的数量和大小。 如果应用尝试缓冲过多上传,站点就会在内存或磁盘空间不足时崩溃。 如果文件上传的大小或频率会消耗应用资源,请使用流式传输。

会将大于 64 KB 的所有单个缓冲文件从内存移到磁盘的临时文件。

较大请求的临时文件将写入环境变量中 ASPNETCORE_TEMP 名为 的位置。 如果未 ASPNETCORE_TEMP 定义 ,则文件将写入当前用户的临时文件夹。

流式处理

从多部分请求收到文件,然后应用直接处理或保存它。 流式传输无法显著提高性能。 流式传输可降低上传文件时对内存或磁盘空间的需求。

通过缓冲的模型绑定将小型文件上传到物理存储

要上传小文件,请使用多部分窗体或使用 JavaScript 构造 POST 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
"use strict";

function AJAXSubmit (oFormElement) {
var oReq = new XMLHttpRequest();
oReq.onload = function(e) {
oFormElement.elements.namedItem("result").value =
'Result: ' + this.status + ' ' + this.statusText;
};
oReq.open("post", oFormElement.action);
oReq.send(new FormData(oFormElement));
}
</script>

如下示例中:

  • 循环访问一个或多个上传的文件。
  • 使用 Path.GetTempFileName 返回文件的完整路径,包括文件名称。
  • 使用应用生成的文件名将文件保存到本地文件系统。
  • 返回上传的文件的总数量和总大小。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
    {
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
    if (formFile.Length > 0)
    {
    var filePath = Path.GetTempFileName();

    using (var stream = System.IO.File.Create(filePath))
    {
    await formFile.CopyToAsync(stream);
    }
    }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
    }

使用 Path.GetRandomFileName 生成文件名(不含路径)。 在下面的示例中,从配置获取路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());

using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}

传递给 FileStream 的路径必须包含文件名。 如果未提供文件名,则会在运行时引发 UnauthorizedAccessException。

使用 IFormFile 技术上传的文件在处理之前会缓冲在内存中或服务器的磁盘中。 在操作方法中,IFormFile 内容可作为 Stream 访问。 除本地文件系统之外,还可以将文件保存到网络共享或文件存储服务,如 Azure Blob 存储。

如果在未删除先前临时文件的情况下创建了 65,535 个以上的文件,则 Path.GetTempFileName 将抛出一个 IOException。 65,535 个文件限制是每个服务器的限制。 有关 Windows 操作系统上的此限制的详细信息,请参阅以下主题中的说明:

  • GetTempFileNameA 函数
  • GetTempFileName

使用缓冲的模型绑定将小型文件上传到数据库

要使用实体框架将二进制文件数据存储在数据库中,请在实体上定义 Byte 数组属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}

//为包括 IFormFile 的类指定页模型属性:
public class BufferedSingleFileUploadDbModel : PageModel
{
...

[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }

...
}

public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}

将窗体发布到服务器后,将 IFormFile 复制到流,并将它作为字节数组保存在数据库中。 在下面的示例中,_dbContext 存储应用的数据库上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);

// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};

_dbContext.File.Add(file);

await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}

return Page();
}

通过流式传输上传大型文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error

return BadRequest(ModelState);
}

// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();

var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);

var section = await reader.ReadNextSectionAsync();

while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);

if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);

streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);

if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error

return BadRequest(ModelState);
}

using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();

if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}

formAccumulator.Append(key, value);

if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error

return BadRequest(ModelState);
}
}
}
}

// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}

// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);

if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error

return BadRequest(ModelState);
}

// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.

var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};

_context.File.Add(file);
await _context.SaveChangesAsync();

return Created(nameof(StreamingController), null);
}

Observable 的创建功能

有很多方法可以在Angular中创建Observable对象。您可以使用Observable构造函数,如Observable教程中所示。还有许多函数可以用来创建新的Observable。这些函数帮助我们从数组、字符串、promise等创建Observable。

  • create
  • defer
  • empty
  • from
  • fromEvent
  • interval
  • of
  • range
  • throw
  • timer
    所有与创建相关的操作符都是RxJs核心库的一部分。您可以从“rxjs”库导入它

Create

调用Create方法是创建Observable最简单的方式。Create是Observable对象的内容方法,所以您不必导入它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ngOnInit() {

//Observable from Create Method
const obsUsingCreate = Observable.create( observer => {
observer.next( '1' )
observer.next( '2' )
observer.next( '3' )
observer.complete()
})
obsUsingCreate
.subscribe(val => console.log(val),
error=> console.log("error"),
() => console.log("complete"))
}
****Output *****
1
2
3
Complete

Observable 构造函数

我们在上一个示例中可以看到这一点,Observable.create方法和Observable构造函数之间没有区别。Observable的Create方法实际在幕后调用Observable的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ngOnInit() {
//Observable Using Constructor
const obsUsingConstructor = new Observable( observer => {
observer.next( '1' )
observer.next( '2' )
observer.next( '3' )

observer.complete()
})

obsUsingConstructor
.subscribe(val => console.log(val),
error=> console.log("error"),
() => console.log("complete"))
}


****Output *****
1
2
3
complete

Of 操作符 Of(a,b,c)

1
2
3
4
5
6
7
8
9
10
11
ngOnInit() {
const array=[1,2,3,4,5,6,7]
const obsof1=of(array);
obsof1.subscribe(val => console.log(val),
error=> console.log("error"),
() => console.log("complete"))

}
**** Output ***
[1, 2, 3, 4, 5, 6, 7]
complete

From 操作符 from([1,2,3])

From 操作符受一个可以迭代的参数,并将其转换为Observable的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ngOnInit() {

const array3 = [1, 2, 3, 4, 5, 6, 7]
const obsfrom1 = from(array3);
obsfrom1.subscribe(val => console.log(val),
error => console.log("error"),
() => console.log("complete"))
}
*** Output ****
1
2
3
4
5
6
7
complete

ngOnInit() {
const obsfrom2 = from('Hello World');
obsfrom2.subscribe(val => console.log(val),
error => console.log("error"),
() => console.log("complete"))
}
*** Output ****
H
e
l
l
o

W
o
r
l
d
complete

Of Vs From

Of From
接受变量(并不是参数) 只接受一个参数
在不更改任何内容的情况下按原样发出每个变量 迭代参数并发出每个值

fromEvent 操作符

语法

1
2
3
4
fromEvent<T>(target: FromEventTarget<T>, 
eventName: string,
options: EventListenerOptions,
resultSelector: (...args: any[]) => T): Observable<T>

FromEventsTarget:是fromvevent的第一个参数。它可以是DOM EventTarget、Node.js EventEmitter、类似JQuery的事件目标、NodeList或HTMLCollection。目标必须有一个方法来注册/注销事件处理程序。(如果是DOM事件目标,则为addEventListener/removeEventListener)

eventName:是第二个参数,这是我们想要侦听的事件类型。

eventListenerOptions:是我们在注册事件处理程序(即addEventListener)时要传递给的附加参数

resultSelector:是可选的,在未来的版本中将不推荐使用

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

`<button #btn>Button</button>
`
@ViewChild('btn', { static: true }) button: ElementRef;

buttonClick() {
this.buttonSubscription = fromEvent(this.button.nativeElement, 'click')
.subscribe(res => console.log(res));
}

ngAfterViewInit() {
this.buttonClick();
}


我们可以从ngAfterViewInit方法调用上述方法。请注意,@ViewChild在ngOnInit之前不会初始化btn元素。因此,我们在这里使用ngAfterViewInit。

pipe 操作符

Angular Observable的管道方法用于将多个操作符链接在一起。我们可以将管道作为一个独立的方法使用,这有助于我们在多个地方重用它或将其作为一个实例方法。在本教程中,我们将了解管道,并了解如何在Angular应用程序中使用它。我们将向您展示使用map、filter和tap操作符的管道示例。

使用Pipe 链接操作符

管道方法接受filter、map等运算符作为参数。每个参数必须用逗号分隔。运算符的顺序很重要,因为当用户订阅可观察对象时,管道会按添加运算符的顺序执行运算符。

我们有两种方法可以使用这个管道。一种是作为observable的实例,另一种是作为独立方法使用

pipe作为实例方法

管道作为实例方法如下所示。我们将运算符op1、op2等作为参数传递给管道方法。op1方法的输出变成op2运算符的输入,依此类推。

1
2
3
4
5
6
obs.pipe(
op1(),
op2(),
op3(),
op3(),
)

以下是将管道与map & filter操作符一起使用的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { Component, OnInit } from '@angular/core';
import { Observable, of} from 'rxjs';
import { map, filter, tap } from 'rxjs/operators'


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

obs = new Observable((observer) => {
observer.next(1)
observer.next(2)
observer.next(3)
observer.next(4)
observer.next(5)
observer.complete()
}).pipe(
filter(data => data > 2), //filter Operator
map((val) => {return val as number * 2}), //map operator
)

data = [];

ngOnInit() {
this.obs1.subscribe(
val => {
console.log(this.data)
}
)
}

}


//result
[6, 8, 10]

import { Component, OnInit } from '@angular/core';
import { Observable, of, pipe } from 'rxjs';
import { map, filter, tap } from 'rxjs/operators'


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

obs = new Observable((observer) => {
observer.next(1)
observer.next(2)
observer.next(3)
observer.next(4)
observer.next(5)
observer.complete()
}).pipe(
tap(data => console.log('tap '+data)), //tap
filter(data => data > 2), //filter
tap(data => console.log('filter '+data)), //tap
map((val) => { return val as number * 2 }), //map
tap(data => console.log('final '+data)), //tap
)


data = [];

ngOnInit() {

this.obs.subscribe(
val => {
this.data.push(val)
console.log(this.data)
}
)

}
}

Pipe 作为单独方法

我们也可以将管道作为一个独立的函数来组成操作符,并在其他地方重用管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
customOperator = pipe(
tap(data => console.log('tap '+data)),
filter(data => data > 2),
tap(data => console.log('filter '+data)),
map((val) => {
return val as number * 2
}),
tap(data => console.log('final '+data)),
);


obs = new Observable((observer) => {
observer.next(1)
observer.next(2)
observer.next(3)
observer.next(4)
observer.next(5)
observer.complete()
})

ngOnInit() {
this.customOperator(this.obs).subscribe();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  customOperator = pipe(
tap(data => console.log('tap '+data)),
filter(data => data > 2),
tap(data => console.log('filter '+data)),
map((val) => {
return val as number * 2
}),
tap(data => console.log('final '+data)),
);


obs = new Observable((observer) => {
observer.next(1)
observer.next(2)
observer.next(3)
observer.next(4)
observer.next(5)
observer.complete()
}).pipe(
this.customOperator,
tap(data => console.log('final '+data)),
)


data = [];

ngOnInit() {

this.obs.subscribe(
val => {
this.data.push(val)
console.log(this.data)
}
)

}
}

总结

我们可以使用Create方法或Observable构造函数来创建一个新的Observable。
当您有类似数组的值时,Of运算符很有用,您可以将其作为单独的参数传递给Of方法以创建可观察的值。
From操作符试图迭代传递给它的任何东西,并从中创建一个可观察的对象。
RxJS库中有许多其他运算符或方法可用于创建和操作Angular observable。我们将在接下来的几个教程中学习其中的一些内容

什么是RxJS

RxJS(Reactive Extensions Library for JavaScript)是一个使用可观察序列编写异步和基于事件的JavaScript库,RxJS 属于响应式编程,其思想是将时间看作数组,随着时间发生的事件被看作是数组的项,然后以操作数组的方式变换事件。

RxJS 中解决异步事件管理的基本概念有:

  • Observable(可观察者):它的本质其实就是一个随时间不断产生数据的一个集合,称之为流更容易理解。

  • Observer(观察者):从行为上来看,无非就是定义了如何处理上述流产生的数据,称之为流的处理方法,更容易理解。

  • Subscription(订阅):表示 Observable 的一次执行,它的本质就是暂存了一个启动后的流,每一个启动后的流都是相互独立的,而这个启动后的流,就存储在subscription中,提供了unsubscribe,来停止这个流。

  • Operator(操作符):是纯函数,可以使用 map、filter、concat、reduce 等操作来以函数式编程风格处理集合。

  • Subject(主体):相当于一个 EventEmitter,也是将一个值或事件多播到多个 Observers 的唯一方式。

  • Scheduler(调度器):是控制并发的集中化调度器,允许我们在计算发生时进行协调,例如 setTimeout 或 requestAnimationFrame 或其它。

Angular在其框架中大量使用RxJS库来实现响应式编程。使用响应式编程的一些示例如下:

  • Angular中响应HTTP请求
  • Angular中表单中值/状态变更
  • Router和Forms模块使用可观察性来监听和响应用户输入事件
  • 通过自定义事件从子组件发送observable输出数据到父组件
  • HTTP模块使用Observable来处理AJAX请求和响应

那么流是指什么呢?举个例子,代码中每1s输出一个数字,用户每一次对元素的点击,就像是在时间这个维度上,产生了一个数据集。这个数据集不像数组那样,它不是一开始都存在的,而是随着时间的流逝,一个一个数据被输出出来。这种异步行为产生的数据,就可以被称之为一个流,在Rxjs中,称之为observable(抛开英文,本质其实就是一个数据的集合,只是这些数据不一定是一开始就设定好的,而是随着时间而不断产生的)。而Rxjs,就是为了处理这种流而产生的工具,比如流与流的合并,流的截断,延迟,消抖等等操作。

理解基本定义: observable, observer, subscription

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Observable } from "rxjs";

// 流的创建
const stream$ = new Observable(subscriber => {
setTimeout(() => {
subscriber.next([1, 2, 3]);
}, 500);
});

// 如何消费流产生的数据,observer
const observer = {
complete: () => console.log("done"),
next: v => console.log(v),
error: () => console.log("error")
};

// 启动流
const subscription = stream$.subscribe(observer);

setTimeout(() => {
// 停止流
subscription.unsubscribe();
}, 1000);

上述过程中,涉及到了3个变量 :

  1. stream$, 对应到Rxjs中,就是一个observable,它的本质其实就是一个随时间不断产生数据的一个集合,称之为流更容易理解。而其对象存在一个subscribe方法,调用该方法后,才会启动这个流(也就是数据才会开始产生),这里需要注意的是多次启动的每一个流都是独立的,互不干扰。

  2. observer,代码中是stream$.subscribe(observer),对应到Rxjs中,也是称之为observer。从行为上来看,无非就是定义了如何处理上述流产生的数据,称之为流的处理方法,更容易理解。

  3. subscription,也就是const subscription = stream$.subscribe(observer); 它的本质就是暂存了一个启动后的流,之前提到,每一个启动后的流都是相互独立的,而这个启动后的流,就存储在subscription中,提供了unsubscribe,来停止这个流。

简单理解了这三个名词observable, observer, subscription后,从数据的角度来思考:observable定义了要生成一个什么样的数据,其subscribe方法,接收一个observer(定义了接收到数据如何处理),并开始产生数据,该方法的返回值,subscription, 存储了这个已经开启的流,同时具有unscbscribe方法,可以将这个流停止。

整理成下面这个公式:

Subscription = Observable.subscribe(observer)

  • observable: 随着时间产生的数据集合,可以理解为流,其subscribe方法可以启动该流
  • observer: 决定如何处理数据
  • subscription: 存储已经启动过的流,其unsubscribe方法可以停止该流

这里有几个点:

  1. subscribe不是订阅,而是启动这个流,可以看到,subscribe后,才会执行next方法
  2. 构建observable的时候,会有一个subscriber.next,这里就是控制这个流中数据的输出。
  3. Observable流可以多次启动,多次启动的流之间是相互独立的。
  4. Observable.subscribe()的返回值subscription上存在一个方法unsubscribe,可以将流停止。

创建 Observable 对象

Observable 是个多值的惰性 Push 集合。他们填补了下表中的缺失点:

单值 多值
Function Iterator
Promise Observable

创建 Observables

Observable 的构造函数接受一个参数:subscribe 函数。

下面的示例创建一个 Observable 以每秒向订阅者发送字符串 ‘hi’。

1
2
3
4
5
6
7
8

import { Observable } from 'rxjs';

const observable = new Observable(function subscribe(subscriber) {
const id = setInterval(() => {
subscriber.next('hi');
}, 1000);
});

订阅 Observables

示例中的 Observable observable 可以被订阅,如下所示:

1
observable.subscribe((x) => console.log(x));

subscribe 调用只是一个启动“ Observable 的执行”并将一些值或事件传递给该执行过程的 Observer 的方法。
同一个 Observable 的多个 Observer 之间是不共享的。对 observable.subscribe 的每次调用都会为给定的订阅者触发其自己的独立处理方法。

执行 Observables

在 Observable 执行中,可能会传递零个到无限个 Next 通知。如果发送了出错或完成通知,则之后将无法发送任何其它通知。

Observable 执行可以传递三种类型的值:

  • “Next(下一个)” 通知:发送数值、字符串、对象等。

  • “Error(出错)” 通知:发送 JavaScript 错误或异常。

  • “Complete(完成)”通知:不发送值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { Observable } from 'rxjs';

    const observable = new Observable(function subscribe(subscriber) {
    try {
    subscriber.next(1);
    subscriber.next(2);
    subscriber.next(3);
    subscriber.complete();
    } catch (err) {
    subscriber.error(err); // delivers an error if it caught one
    }
    });

    取消 Observable 执行

    当 observable.subscribe 被调用时,此 Observer 被附加到新创建的 Observable 执行中。此调用还会返回一个对象 Subscription :

1
2
const subscription = observable.subscribe((x) => console.log(x));

当你订阅时,你会得到一个 Subscription,它代表正在进行的执行。只需调用 unsubscribe() 即可取消执行。

1
2
3
4
5
6
import { from } from 'rxjs';

const observable = from([10, 20, 30]);
const subscription = observable.subscribe((x) => console.log(x));
// Later:
subscription.unsubscribe();

当我们不再需要observable时,我们需要取消订阅以关闭它。如果不取消订阅,可能会导致内存泄漏和性能下降。
要从可观察到的订阅中取消订阅,我们需要对订阅调用Unsubscribe()方法。它将清理所有侦听器并释放内存。

当我们销毁组件时,可观察到的内容将被取消订阅并清除。但是,您不必每次订阅都取消订阅。例如,发出完整信号的可观测对象,自动关闭。

观察者(Observer)

什么是 Observer? Observer 是 Observable 传递的各个值的消费者。 Observer 只是一组回调,对应于 Observable 传递的每种类型的通知:next、error 和 complete。下面是一个典型的 Observer 对象的例子:

1
2
3
4
5
const observer = {
next: (x) => console.log('Observer got a next value: ' + x),
error: (err) => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};

要使用 Observer,请将其提供给 Observable 的 subscribe :

1
observable.subscribe(observer);

Observer 只是具有三个回调的对象,分别用于 Observable 可能传递的每种类型的通知。

RxJS 中的 Observer 也可能是部分的。如果你不提供其中一个回调,Observable 的执行仍然会正常进行,除了某些类型的通知会被忽略,因为它们在 Observer 中没有对应的回调。

1
2
3
4
5
6
7
8
ngOnInit() {

this.obs.subscribe(
val => { console.log(val) }, //next callback
error => { console.log("error") }, //error callback
() => { console.log("Completed") } //complete callback
)
}

Observable 操作符

操作符是对Observable进行操作并返回新Observable的函数。

有两种操作符:

  • 可联入管道的操作符:本质上是一个纯函数,它将一个 Observable 作为输入并生成另一个 Observable 作为输出。订阅此输出 Observable 也会同时订阅其输入 Observable。
  • 创建操作符:是另一种操作符,可以作为独立函数调用以创建新的 Observable。例如: of(1, 2, 3) 创建一个 observable,它将一个接一个地发出 1、2 和 3。

以下示例显示了filer和map运算符使用管道链接到一起,filter运算符删除所有小于等于2的数据,map运算符将该值乘以2。
输入流为[1,2,3,4,5],而输出流为[6,8,10]。

1
2
3
4
5
6
7
8
9
10
11
12
obs.pipe(
obs = new Observable((observer) => {
observer.next(1)
observer.next(2)
observer.next(3)
observer.next(4)
observer.next(5)
observer.complete()
}).pipe(
filter(data => data > 2), //filter Operator
map((val) => {return val as number * 2}), //map operator
)

弹珠图

要解释操作符的工作原理,文字描述通常是不够的。许多操作符都与时间有关,他们可能以不同的方式延迟、采样、节流或防抖后发出。图表通常是更好的工具。弹珠图是操作符如何工作的可视化表示,包括输入 Observable、操作符及其参数以及输出 Observable。

Subject

Subject 是一种特殊类型的 Observable,它允许将值多播到多个 Observer。

  • 每个 Subject 都是 Observable。给定一个 Subject,你可以 subscribe 它,提供一个 Observer,它将开始正常接收值。
  • 每个 Subject 也都是 Observer。它是一个具有方法 next(v)、error(e) 和 complete() 的对象。要为 Subject 提供一个新值,只需调用 next(theValue),它将被多播到注册进来监听 Subject 的 Observer。

Subject也是Rxjs中比较重要的概念,从英文上不太好理解,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Subject } from "rxjs";

// 创建subject
const subject = new Subject();

// 订阅一个observer
subject.subscribe(v => console.log("stream 1", v));
// 再订阅一个observer
subject.subscribe(v => console.log("stream 2", v));
// 延时1s再订阅一个observer
setTimeout(() => {
subject.subscribe(v => console.log("stream 3", v));
}, 1000);
// 产生数据1
subject.next(1);
// 产生数据2
subject.next(2);
// 延时3s产生数据3
setTimeout(() => {
subject.next(3);
}, 3000);
// output
// stream 1 1 //立即输出
// stream 2 1 //立即输出
// stream 1 2 //立即输出
// stream 2 2 //立即输出
// stream 1 3 //3s后输出
// stream 2 3 //3s后输出
// stream 3 3 //3s后输出

可以看到,Subject的行为和发布订阅模式非常接近,subscribe去订阅,next触发。事件的订阅通过subscribe,事件的触发使用next,从而实现一个发布订阅的模式。

由于 Subject 是 Observer,这也意味着你可以提供 Subject 作为任意 Observable subscribe 的参数,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Subject, from } from 'rxjs';

const subject = new Subject<number>();

subject.subscribe({
next: (v) => console.log(`observerA: ${v}`),
});
subject.subscribe({
next: (v) => console.log(`observerB: ${v}`),
});

const observable = from([1, 2, 3]);

observable.subscribe(subject); // You can subscribe providing a Subject

// Logs:
// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2
// observerA: 3
// observerB: 3

使用上述方法,我们基本上只是通过 Subject 将单播 Observable 执行转换为多播。这展示了 Subjects 是让任何 Observable 执行共享给多个 Observers 的唯一方法。

BehaviorSubject

它存储发送给其消费者的最新值,并且每当有新的 Observer 订阅时,它将立即从 BehaviorSubject 接收到“当前值”。

在下面的示例中,BehaviorSubject 使用第一个 Observer 在订阅时收到的值 0 进行初始化。第二个 Observer 接收到值 2,即使它是在发送值 2 之后订阅的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { BehaviorSubject } from 'rxjs';
const subject = new BehaviorSubject(0); // 0 is the initial value

subject.subscribe({
next: (v) => console.log(`observerA: ${v}`),
});

subject.next(1);
subject.next(2);

subject.subscribe({
next: (v) => console.log(`observerB: ${v}`),
});

subject.next(3);

// Logs
// observerA: 0
// observerA: 1
// observerA: 2
// observerB: 2
// observerA: 3
// observerB: 3

ReplaySubject

ReplaySubject 与 BehaviorSubject 类似,它可以将旧值发送给新订阅者,但它也可以记录 Observable 执行结果的一部分。

创建 ReplaySubject 时,你可以指定要重播的值的数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { ReplaySubject } from 'rxjs';
const subject = new ReplaySubject(3); // buffer 3 values for new subscribers

subject.subscribe({
next: (v) => console.log(`observerA: ${v}`),
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
next: (v) => console.log(`observerB: ${v}`),
});

subject.next(5);

// Logs:
// observerA: 1
// observerA: 2
// observerA: 3
// observerA: 4
// observerB: 2
// observerB: 3
// observerB: 4
// observerA: 5
// observerB: 5

除了缓冲区大小之外,你还可以指定一个以毫秒为单位的窗口时间,以确定记录的值可以存在多长时间。在以下示例中,我们使用 100 个元素的大型缓冲区,但窗口时间参数仅为 500 毫秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { ReplaySubject } from 'rxjs';
const subject = new ReplaySubject(100, 500 /* windowTime */);

subject.subscribe({
next: (v) => console.log(`observerA: ${v}`),
});

let i = 1;
setInterval(() => subject.next(i++), 200);

setTimeout(() => {
subject.subscribe({
next: (v) => console.log(`observerB: ${v}`),
});
}, 1000);

// Logs
// observerA: 1
// observerA: 2
// observerA: 3
// observerA: 4
// observerA: 5
// observerB: 3
// observerB: 4
// observerB: 5
// observerA: 6
// observerB: 6
// ...

AsyncSubject

AsyncSubject 是一种变体,其中仅将 Observable 执行的最后一个值发送给其 Observer,并且仅在执行完成时发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { AsyncSubject } from 'rxjs';
const subject = new AsyncSubject();

subject.subscribe({
next: (v) => console.log(`observerA: ${v}`),
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
next: (v) => console.log(`observerB: ${v}`),
});

subject.next(5);
subject.complete();

// Logs:
// observerA: 5
// observerB: 5

总结

响应式编程就是对流进行编程。RxJS库将响应式编程引入了Angular。使用RxJs,我们可以创建一个可观测对象,它可以向观测对象的订阅者发出值、错误和完成信号。

我们使用装饰器为Angular中的类声明、方法、访问器、属性和参数提供元数据。我们使用它来装饰组件、指令、模块等。在本文中,让我们了解什么是装饰器,为什么需要它,以及如何创建自定义装饰器。我们还要了解Angular支持的内置装饰器。

Angular 装饰器

Angular 装饰器是一个函数,我们使用它将元数据附加到类、方法、访问器、属性或参数。

我们以@expression形式使用装饰器,其中expression是装饰器的名称。

例如,@Component是一个装饰器,我们将其附加到一个Angular组件。当Angular看到@Component装饰器应用于一个类时,它将该类视为一个组件类。在下面的示例中,正是@Component装饰器使AppComponent成为一个Angular组件。如果没有装饰器,AppComponent就像其他类一样。

1
2
3
4
5
@Component({
})
export class AppComponent {
title = 'app';
}

decorator装饰器是一个Typescript特性,它仍然不是Javascript的一部分。它仍处于提案阶段。

要启用Angular 装饰器,我们需要将experialDecorators添加到tsconfig.json文件中。ng-new命令会自动为我们添加此内容。

1
2
3
4
5
6
7

{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}

创建新的装饰器

在下面的例子中,我们创建了一个函数simpleDecorator。我们将使用它来装饰AppComponent类。该函数不接受任何参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Component, VERSION } from '@angular/core';

@simpleDecorator
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = 'Angular ' + VERSION.major;

constructor() {
console.log('Hello from Class constructor');
}

ngOnInit() {
console.log((this as any).value1);
console.log((this as any).value2);
}
}

function simpleDecorator(target: any) {
console.log('Hello from Decorator');

Object.defineProperty(target.prototype, 'value1', {
value: 100,
writable: false
});

Object.defineProperty(target.prototype, 'value2', {
value: 200,
writable: false
});
}



**** Console ***

Hello from Decorator
Hello from Class constructor
100
200

正如我们前面所说,d装饰器 是一个常规的JavaScript函数。由于我们在类上使用它,因此它获取AppComponent的实例作为参数(目标)

1
2
3
4
5

function simpleDecorator(target: any) {
console.log('Hello from Decorator');

//target is instance of AppComponent

在函数内部,我们将两个自定义属性value1和value2添加到AppComponent。请注意,我们使用defineProperty属性向组件类添加一个新属性。此外,我们将其添加到类的原型属性中。

1
2
3
4
5
6
7
8
9
10
Object.defineProperty(target.prototype, 'value1', {
value: 100,
writable: false
});

Object.defineProperty(target.prototype, 'value2', {
value: 200,
writable: false
});

现在,我们可以使用它来装饰我们的AppComponent

1
2
3
4
5
6
7
@simpleDecorator
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

在组件内部,我们可以使用关键字“this”访问新的属性。

1
2
3
4
ngOnInit() {
console.log((this as any).value1);
console.log((this as any).value2);
}

带参数的装饰器

要创建一个带参数的装饰器,我们需要创建一个工厂函数,该函数返回装饰器函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Component, VERSION } from '@angular/core';

@simpleDecorator({
value1: '100',
value2: '200'
})
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = 'Angular ' + VERSION.major;

constructor() {
console.log('Hello from Class constructor');
}

ngOnInit() {
console.log((this as any).value1);
console.log((this as any).value2);
}
}

function simpleDecorator(args) {
console.log(args);

return function(target: any) {
console.log('Hello from Decorator');
console.log(typeof target);
console.log(target);

Object.defineProperty(target.prototype, 'value1', {
value: args.value1,
writable: false
});

Object.defineProperty(target.prototype, 'value2', {
value: args.value2,
writable: false
});
};
}

simpleDecorator将args作为参数,并返回 装饰器函数。除了使用参数填充财产之外,其余代码与上面的代码相同。

1
2
3
4
function simpleDecorator(args) {
console.log(args);

return function(target: any) {

我们在组件上应用simpleDecorator,如下所示

Angular 装饰器 列表

Angular提供了几个内置的装饰器。我们可以把它们分为四类

以下是Angular中装饰器的完整列表。

  • 类装饰器
    • @NgModule
    • @Component
    • @Injectable
    • @Directive
    • @Pipe
  • 属性装饰器
    • @Input
    • @Output
    • @HostBinding
    • @ContentChild
    • @ContentChildren
    • @ViewChild
    • @ViewChildren
  • 方法装饰器
    • @HostListener
  • 参数装饰器
    • @Inject
    • @Self
    • @SkipSelf
    • @Optional
    • @Host

类装饰器

AfterViewInit、AfterContentInit、AfterViewChecked和AfterContentChecked是生命周期钩子。Angular在组件的生命周期中,调用它们。在本教程中,我们将了解它们是什么以及Angular何时调用它们。我们还了解了AfterViewInit和AfterContentInit与AfterViewChecked和AfterContentChecked之间的差异。

生命周期回顾

当Angular实例化组件时,组件(或指令)的生命就开始了。实例化从调用组件的构造函数开始,并通过依赖注入服务。

一旦Angular实例化了组件,它就会启动组件的变更检测,它检查并更新组件的输入数据绑定属性,并初始化组件。然后,它会引发以下生命周期钩子。

Onchanges,如果Angular检测到Input属性的任何更改或angular检测到输入变化时,Onchanges都会被运行。

OnInit,它告诉我们组件已经准备好了。这个钩子让我们有机会运行初始化逻辑,更新属性等。这个钩子只运行一次。

DoCheck,这允许我们运行自定义更改检测,因为更改检测可能会忽略一些更改。该挂钩在每个变化检测周期中运行。

在此之后,Angular又调用了四个钩子。它们是AfterContentInit、AfterContentChecked、AfterViewInit和AfterViewChecked。我们将详细研究它们。

最后,当我们移除组件时,Angular调用ngOnDestroy钩子,销毁组件。

Content Vs View

在深入研究这些挂钩之前,我们需要了解内容(Content)和视图(View)之间的区别。钩子AfterConentInit和AfterContentChecked处理内容(Content),而AfterViewInit、AfterViewChecked处理视图(View)。

内容

内容是指使用内容投影注入到该组件中的外部内容。
内容投影是将HTML内容从父组件传递到子组件的一种方式。子组件将在指定位置显示传入的内容。我们使用ng-content在子组件的模板中创建一个点,如下所示。

1
2
<h2>Child Component</h2>
<ng-content></ng-content> <!-- place hodler for content from parent -->

父元素在开始元素和结束元素之间注入内容。Angular将此内容传递给子组件。

1
2
<h1>Parent Component</h1>
<app-child> This <b>content</b> is injected from parent</app-child>

视图

视图是指组件的模板。

AfterContentInit

AfterContentInit是在组件的内容完全初始化并注入组件视图后,angular调用的生命周期挂钩。

Angular在调用AfterContentInit之前,先更新被ContentChild和ContentChildren装饰的属性。

即使组件中没有使用内容投影,Angular也会调用AfterContentInit,此挂钩在ngDoCheck挂钩之后启动。

AfterContentInit仅调用一次(在组件创建后第一个变更检测后)。

AfterContentChecked

AfterContentChecked 在 ngDoCheck 和 AfterContentInit调用之后触发。
Angular在调用AfterContentChecked之前,先更新被ContentChild和ContentChildren装饰的属性。

AfterViewInit

仅在组件创建后的第一个变更检测周期内触发一次。

Angular 在完成组件视图及其子视图的初始化后,在变更检测期间调用的生命周期挂钩。

在引发AfterViewInit之前,Angular 还会更新用 ViewChild 和 ViewChildren 属性装饰的属性。

AfterViewChecked

在变更检测器完成对组件视图和子视图变更的检查后,Angular 调用的生命周期挂钩。

在引发此挂钩之前,Angular 还会更新用 ViewChild 和 ViewChildren 属性装饰的属性。

Init Vs Checked

当第一次初始化内容或视图时,Angular 会触发 AfterContentInit 和 AfterViewInit 钩子。 这发生在第一个变化检测周期中,Angular在组件实例化后立即调用。

Angular 触发 AfterContentChecked 和 AfterViewChecked 钩子,Angular 在其中检查内容或视图是否已更改。 即先前呈现的内容或视图与当前内容或视图相同。

Renderer2允许我们在不直接访问DOM的情况下操作DOM元素。它在DOM元素和组件代码之间提供了一层抽象。使用Renderer2,我们可以创建一个元素,向其中添加一个文本节点,使用appendchild方法附加子元素。我们还可以添加或删除样式、HTML属性、CSS类和属性等。我们还可以附加和侦听DOM事件。

为什么不直接使用 ElementRef

ElelemtRef的nativeElement属性包含对底层DOM对象的引用,这使我们可以绕过Angular直接访问DOM,我们可以使用nativeElement属性来直接操作DOM元素。但由于以下原因,直接操作DOM是不建议的。

  1. Angular使用模板、数据绑定和更改检测等保持组件和视图同步。当我们直接更新DOM时,所有这些都会被绕过。
  2. DOM操作仅适用于浏览器。您将无法在其他平台中使用该应用程序,例如在服务器(服务器端渲染)、桌面或移动应用程序等中。
  3. DOM API不会对数据进行净化。因此,可以通过注入脚本,打开我们的应用程序,成为XSS注入攻击的目标。

使用 Renderer2

首先从 @angular/core 导入 Renderer2.

1
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

在组件中注入 Renderer2

1
2
constructor(private renderer:Renderer2) {
}

使用 ElementRef & ViewChild 获取我们想操作的DOM元素实例

1
@ViewChild('hello', { static: false }) divHello: ElementRef;

使用setProperty、setStyle等方法更改元素的属性和样式,如下所示

1
2
3
this.renderer.setProperty(this.divHello.nativeElement,'innerHTML',"Hello Angular")

this.renderer.setStyle(this.divHello.nativeElement, 'color', 'red');

设置&移除 Styles

使用setStyle和RemoveStyle添加或删除样式,它接受四个参数。第一个参数是我们要应用样式的元素,第二个参数是样式的名称,第三个参数是样式的值,最后一个参数是可选的,他是样式变量的标志

1
2
3
abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void

abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Template
<div #hello>Hello !</div>

//Component
@ViewChild('hello', { static: false }) divHello: ElementRef;

setStyle() {
this.renderer.setStyle(this.divHello.nativeElement, 'color', 'blue');
}

removeStyle() {
this.renderer.removeStyle(this.divHello.nativeElement, 'color');
}

使用最后一个选项RendererStyleFlags2指定渲染器特有样式修饰符

添加&删除CSS 样式

使用 addClass / removeClass 添加&删除CSS样式。

1
2
3
abstract addClass(el: any, name: string): void

abstract removeClass(el: any, name: string): void

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Template
<div #hello>Hello !</div>

//Component
@ViewChild('hello', { static: false }) divHello: ElementRef;

addClass() {
this.renderer.addClass(this.divHello.nativeElement, 'blackborder' );
}

removeClass() {
this.renderer.removeClass(this.divHello.nativeElement, 'blackborder');
}

添加删除 Attributes

使用 setAttribute & removeAttribute 添加&移除 attribute样式。

元素没有此 attribute,也可以添加上去
attribute,还可以做动词,表示赋予,属于人为赋予的可改变的属性

1
2
3
setAttribute(el: any, name: string, value: string, namespace?: string): void

removeAttribute(el: any, name: string, namespace?: string): void

设置Property

使用setProperty方法设置DOM元素的任何属性。

元素没有此 property 不会添加上去
property是 物体本身自带属性,不能改变的(一旦改了就是另外一个东西了)

1
setProperty(el: any, name: string, value: any): void

AppendChild createElement createText

使用appendChild将一个新元素(子元素)附加到任何现有元素(父元素)。

1
appendChild(parent: any, newChild: any): void

它接受两个参数。第一个参数是父节点(我们希望在其中附加一个新的子节点)。第二个参数是要添加的子节点。

创建一个新元素

1
2
3
4
5
6
7
8
const div = this.renderer.createElement('div');

const text = this.renderer.createText('Inserted at bottom');

this.renderer.appendChild(div, text);

this.renderer.appendChild(this.div.nativeElement, div);

InsertBefore

我们还可以使用insertBefore方法在DOM元素中的元素之前插入新元素。insertBefore的语法如下所示

1
insertBefore(parent: any, newChild: any, refChild: any): void

parent是父节点,newChild是要插入的新节点,refChild是插入newChild之前的现有子节点。

添加注释

createComment创建注释节点。它接受注释作为自变量。然后可以使用appendChild或insertBefore将其插入DOM中的任何位置。

1
createComment(value: string): any

ParentNode & NextSibling

ParentNode方法返回宿主元素DOM中给定节点的父节点。

1
2
/Returns the parent Node of div3
this.renderer.parentNode(this.div3.nativeElement);

nextSibling方法返回宿主元素DOM中给定节点的下一个同级节点。

1
2
//Returns the next Sibling node of div2
this.renderer.nextSibling(this.div2.nativeElement);

SelectRootElement

我们也可以使用selectRoomElement来选择基于选择器的节点元素。

1
selectRootElement(selectorOrNode: any, preserveContent?: boolean)

第一个参数是选择器或节点。Renderer2使用选择器来搜索DOM元素并返回它。

第二个参数是preserveContent,如果是no或undefined,renderer2将删除所有子节点。如果是yes,则不会删除子节点。

监听 DOM 事件

您也可以使用listen方法来侦听DOM事件。

listen方法接受三个参数,第一个参数是DOM元素(目标),第二个参数是事件的名称(eventName),第三个参数是回调函数

1
abstract listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void

Angular ElementRef是一个围绕原生DOM元素(HTML元素)对象的包装器。它包含属性nativeElement,该属性保存对底层DOM对象的引用。我们可以使用它来操作DOM。我们组件中使用ViewChild来获取模板HTML元素的ElementRef实例。Angular还在组件或指令的构造函数中注入宿主元素的ElementRef实例。在本教程中,让我们探讨如何使用ElementRef来获得HtmlElement的引用并在Angular Applications中操作DOM。

ElementRef

DOM对象由浏览器创建和维护。它们代表了文件的结构和内容。在原生JavaScript代码中,我们访问这些DOM对象来操作View。我们可以创建文档,以及添加、修改或删除元素和内容。

Angular提供了许多工具和技术来操作DOM。我们可以添加/删除组件。它提供了许多指令,如类指令或样式指令,来操纵他们的风格等。

在某些情况下,我们可能仍然需要访问DOM元素。这就需要用到ElementRef。

在组件中获取ElementRef

要使用ElementRef操作DOM,我们需要在组件/指令中获取它对DOM元素的引用。

  1. 获取对组件中DOM元素的引用
  • 为组件/指令中的元素创建模板引用变量。
  • 使用ViewChild或ViewChildren通过模板变量在组件中直接获取DOM元素引用。
  1. 获取组件/指令的宿主DOM元素
  • 组件或指令的构造函数中注入宿主元素的ElementRef引用(Angular Dependency注入)

例如,在下面的代码中,变量hello引用了HTML元素div。

1
<div #hello>Hello Angular</div>

我们可以在模板中使用hello这个模板引用变量。

在Component类中,我们使用ViewChild来注入hello元素。Angular将hello作为ElementRef类型注入。

1
@ViewChild('hello', { static: false }) divHello: ElementRef;

读取令牌

考虑以下示例

1
<input #nameInput [(ngModel)]="name">

nameInput 模板引用变量现在绑定到input输入元素。但与此同时,我们也将ngModel指令绑定到它上面。

在这种情况下,我们可以使用read令牌让angular知道我们需要ElementRef引用到谁,如下所示

1
2
3
4
5
6
7
8
//ViewChild returns ElementRef i.e. input HTML Element

@ViewChild('nameInput',{static:false, read: ElementRef}) elRef;


//ViewChild returns NgModel associated with the nameInput
@ViewChild('nameInput',{static:false, read: NgModel}) inRef;

ElementRef 例子

一旦我们有了ElementRef,我们就可以使用nativeElement属性来操作DOM,如下所示。
在访问ViewChild变量之前,我们需要等待Angular初始化视图。因此,我们要等到AfterViewInit生命周期挂钩之后,才能开始使用该变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component,ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
template: '<div #hello>Hello</div>'
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {

@ViewChild('hello', { static: false }) divHello: ElementRef;

ngAfterViewInit() {
this.divHello.nativeElement.innerHTML = "Hello Angular";
}

}

我们可以非常容易得操作DOM元素

1
2
3
4
5
ngAfterViewInit() {
this.divHello.nativeElement.innerHTML = "Hello Angular";
this.divHello.nativeElement.className="someClass";
this.divHello.nativeElement.style.backgroundColor="red";
}

在自定义指令中使用 ElementRef

ElementRef的一个用例是Angular指令。我们学习了如何在Angular中创建自定义指令。以下是ttClass自定义属性指令的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Directive, ElementRef, Input, OnInit } from '@angular/core'

@Directive({
selector: '[ttClass]',
})
export class ttClassDirective implements OnInit {

@Input() ttClass: string;

constructor(private el: ElementRef) {
}

ngOnInit() {
this.el.nativeElement.classList.add(this.ttClass);
}

}

请注意,我们在构造函数中注入ElementRef。每当我们访问构造函数中注入的ElementRef时,Angular就会注入对指令的宿主DOM元素的引用。

谨慎使用
当需要直接访问DOM时,使用此API作为最后的手段。请改用Angular提供的模板和数据绑定。或者,您可以看看Renderer2,它提供了即使不支持直接访问本机元素也可以安全使用的API。
依赖于直接DOM访问会在应用程序和渲染层之间产生紧密耦合,这将使您无法将两者分离并将应用程序部署到web工作者中。

ElementRef和XSS注入攻击

ElementRef的不当使用可能导致XSS注入攻击。例如,在下面的代码中,我们正在使用elementRef注入一个脚本。当包含此类代码的组件运行时,将执行脚本

1
2
3
4
5
6
constructor(private elementRef: ElementRef) {
const s = document.createElement('script');
s.type = 'text/javascript';
s.textContent = 'alert("Hello World")';
this.elementRef.nativeElement.appendChild(s);
}

ViewChild或ViewChildren装饰器用于查询和获取组件中DOM元素的引用。ViewChild返回第一个匹配的元素,ViewChildren以QueryList形式返回所有匹配的元素。

ViewChild

ViewChild查询从DOM返回第一个匹配元素,并更新它在组件中绑定的变量。

语法

1
ViewChild(selector: string | Function | Type<any>, opts: { read?: any; static: boolean; }): any

我们在组件属性上应用viewChild装饰器。它需要提供两个参数,selector和opts。

  • selector(查询选择器):可以是字符串、类型或返回字符串或类型的函数。变更检测查找与选择器匹配的第一个元素,并使用对该元素的引用更新组件属性。如果DOM发生更改,并且有一个新元素与选择器匹配,则变更检测会更新组件属性。

  • opts:有两个选项。

    • static:选项确定ViewChild查询解析的时间。

      • static:true 将在每次变更改检测之前解析ViewChild(对象静态生成时)。
      • static:false 将在每次变更改检测之后解析ViewChild(对象动态渲染时)。
    • read:使用它从查询的元素中读取不同的令牌。

各类selector例子

通过class(组件和指令)

这个class也是有条件的,必须是@Component或者@Directive修饰的clas。

通过@ViewChild在父组件中获取子组件的引用并操作其属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component } from '@angular/core';

@Component({
selector: 'child-component',
template: `<h2>Child Component</h2>
current count is {{ count }}`
})
export class ChildComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}

我们可以在父组件中使用@ViewChild引用子组件。

1
@ViewChild(ChildComponent, {static:true}) child: ChildComponent;

在上面的代码中,@ViewChild在父组件的视图中,查找第一个出现的ChildComponent组件,并更新父组件中的child变量。现在我们可以从父组件调用子组件(ChildComponent)中的Increment和Decrement方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Component, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-root',
template: `
<h1>{{title}}</h1>
<p> current count is {{child.count}} </p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">decrement</button>
<child-component></child-component>
` ,
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Parent calls an @ViewChild()';
@ViewChild(ChildComponent, {static:true}) child: ChildComponent;

increment() {
this.child.increment();
}

decrement() {
this.child.decrement();
}

}

通过子组件provider提供的类

比如我们有一个子组件。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {Component, OnInit} from '@angular/core';
import {ChildService} from './child.service';
@Component({
selector: 'app-child',
template: `
<h1>自定义的一个子组件</h1>`,
providers: [
ChildService
]
})
export class ChildComponent implements OnInit {
constructor(public childService: ChildService) {
}
ngOnInit() {
}
}

在上面的子组件里面provider里面提供了一个ChildService类。我们也是可以通过@ViewChild来拿到这个ChildService类的。代码如下

1
@ViewChild(ChildService) childService: ChildService;

子组件provider通过string token提供的类

子组件的pprovider通过 string token valued的形式提供了一个StringTokenValue类,string token 对应的是tokenService。

1
2
3
4
5
6
7
8
9
10
11
12
13
import {Component} from '@angular/core';
import {StringTokenValue} from './string-token-value';
@Component({
selector: 'app-child',
template: `
<h1>自定义的一个子组件</h1>`,
styleUrls: ['./child.component.less'],
providers: [
{provide: 'tokenService', useClass: StringTokenValue}
]
})
export class ChildComponent {
}

在父组件里面我们也是可以拿到子组件provider的这个StringTokenValue类的。方式如下:

1
2
@ViewChild('tokenService') tokenService: StringTokenValue;

从子组件注入提供程序

您还可以注入子组件中提供的服务。

1
2
3
4
5
6
7
8
9
10
import { ViewChild, Component } from '@angular/core';

@Component({
selector: 'app-child',
template: `<h1>Child With Provider</h1>`,
providers: [{ provide: 'Token', useValue: 'Value' }]
})

export class ChildComponent{
}

在父组件中,可以使用read属性访问提供程序

1
2
3
4
5
6
7
8
9
10
import { ViewChild, Component } from '@angular/core';

@Component({
selector: 'app-root',
template: `<app-child></app-child>`,
})

export class AppComponent{
@ViewChild(ChildComponent , { read:'Token', static:false } ) childToken: string;
}

通过模板变量

例如:

1
<child-component #child></child-component>

里面的child就是模板变量。

在ViewChild查询中使用它来获取对该组件的引用。

1
@ViewChild("child", { static: true }) child: ChildComponent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';

@Component({
selector: 'htmlelement',
template: `
<p #child>Some text</p>
`,
})
export class HTMLElementComponent implements AfterViewInit {

@ViewChild('child',{static:false}) para: ElementRef;

ngAfterViewInit() {
console.log(this.para.nativeElement.innerHTML);
this.para.nativeElement.innerHTML="new text"
}
}

通过 TemplateRef

 当选择器是TemplateRef的时候,则会获取到html里面所有的ng-template的节点。实际例子如下:

1
2
3
4
/**** @ViewChild(TemplateRef) @ViewChildren(TemplateRef)获取页面上的ng-template节点信息 ****/
@ViewChild(TemplateRef) template: TemplateRef<any>;
@ViewChildren(TemplateRef) templateList: QueryList<TemplateRef<any>>;

ViewChild 返回 Undefined

ViewChild返回Undefined是我们在使用它们时遇到的常见错误之一。

该错误是由于我们试图在ViewChild初始化之前使用该值。

例如,下面的代码导致无法读取未定义的属性“increment”。当构造函数运行前,组件的视图尚未初始化。因此,Angular无法通过引用ChildComposet变量来更新child变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
export class AppComponent {
title = 'Parent calls an @ViewChild()';

@ViewChild(ChildComponent, {static:true}) child: ChildComponent;

constructor() {
this.child.increment()
}

}

//
Cannot read property 'increment' of undefined

解决方案是等待Angular初始化视图。Angular在完成视图初始化后会引发AfterViewInit生命周期挂钩。因此,我们可以使用ngAfterViewInit来访问子变量。

1
2
3
ngAfterViewInit() {
this.child.increment()
}

现在,代码没有给出任何错误。

上面的代码也将与ngOnInit生命周期挂钩一起使用。但它不能保证一直工作,因为Angular可能不会在引发ngOnInit钩子之前初始化视图的所有部分。因此,最好使用ngAfterViewInit钩子。

此外,ViewChild更新值的时间也取决于静态选项

在ViewChild中使用Static选项

我们在上面的代码中使用了{static:true}。

static选项确定ViewChild查询解析的时间。

  • static:true 将在每次变更改检测之前解析ViewChild(对象静态生成时)。

  • static:false 将在每次变更改检测之后解析ViewChild(对象动态渲染时)。

例如,考虑下面的代码,我们子组件放在ngIf中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//child.component.html

<h1>ViewChild Example</h1>

<input type="checkbox" id="showCounter" name="showCounter" [(ngModel)]="showCounter">

<ng-container *ngIf="showCounter">

<p> current count is {{child?.count}} </p>

<button (click)="increment()">Increment</button>
<button (click)="decrement()">decrement</button>

<child-component></child-component>

</ng-container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//child.component.ts

import { Component, ViewChild, AfterViewInit, OnInit, ChangeDetectorRef } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
selector: 'app-root',
templateUrl: 'app.component.html' ,
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ViewChild Example)';

showCounter: boolean = true

@ViewChild(ChildComponent, { static: true }) child: ChildComponent;

increment() {
this.child.increment();
}

decrement() {
this.child.decrement();
}
}

上述代码导致TypeError:无法读取未定义的属性“increment”。即使我们将true赋值给showCounter,也会发生错误。
因为在上述情况下,Angular不会立即渲染子组件。但在第一次变更检测之后,angular会检测showCounter的值并渲染子组件。

由于我们使用了static:true,angular将在运行第一次更改检测之前尝试解析ViewChild。因此,child变量总是未定义的。
现在,更改static:false。现在,代码将正常工作。也就是说,因为在每次检测到更改之后,Angular都会更新ViewChild。

在 ViewChild 中使用 Read选项

单个元素可以与多种类型相关联。

例如,考虑以下代码#nameInput模板变量现在与input和ngModel都关联

1
<input #nameInput [(ngModel)]="name">

下面的viewChild代码将input元素的实例作为elementRef返回。

1
@ViewChild('nameInput',{static:false}) nameVar;

如果我们想获得ngModel的实例,那么我们使用Read选项指定需要的令牌类型。

1
2
3
@ViewChild('nameInput',{static:false, read: NgModel}) inRef;
@ViewChild('nameInput',{static:false, read: ElementRef}) elRef;
@ViewChild('nameInput', {static:false, read: ViewContainerRef }) vcRef;

Angular中的每个元素总是有一个ElementRef和ViewContainerRef与之关联。如果该元素是一个组件或指令,那么总是有组件或指令实例。您也可以对一个元素应用多个指令。
不带read令牌的ViewChild默认返回值类型为组件,如果返回值不是组件类型则返回elementRef类型。

多个实例

模板中可能存在同一组件或元素的多个实例。

1
2
<child-component></child-component>
<child-component></child-component>

ViewChild 总是返回第一个匹配的组件。

1
@ViewChild(ChildComponent, {static:true}) child: ChildComponent;

要获取子组件的所有实例,我们可以使用ViewChildren,我们将在本教程后面介绍它。

ViewChildren

ViewChildren装饰器用于从View中获取元素引用的列表。ViewChildren与ViewChild不同。ViewChild始终返回对单个元素的引用。如果存在多个元素,则ViewChild返回第一个匹配元素,ViewChildren总是以QueryList的形式返回所有元素。您可以遍历列表并访问每个元素。

语法

viewChildren的语法如下所示。除了static选项之外,它与viewChild的语法非常相似。

1
ViewChildren(selector: string | Function | Type<any>, opts: { read?: any; }): any

ViewChildren总是在运行更改检测之后解析。即为什么它没有静态选项。而且,您不能在ngOnInit钩子中引用它,因为它还没有初始化。

QueryList

QueryList将viewChildren或contentChildren返回的项存储在列表中。
只要应用程序的状态发生变化,Angular就会更新此列表。它在每次检测到变化时都会这样做。
QueryList还实现了一个可迭代的接口。这意味着您可以使用for(var i of items)对其进行迭代,也可以在template*ngFor=“let i of item”中与ngFor一起使用。
可以通过订阅可观察的更改来观察更改。

您可以使用以下方法和属性。

  • first:返回列表中的第一个项目。
  • last:获取列表中的最后一项。
  • length:获取项目的长度。
  • changes:是可观察的。每当Angular添加、删除或移动子元素时,它都会发出一个新值。

实例

在下面的示例中,输入元素使用ngModel指令,我们使用ViewChildren来获取所有输入元素并存储在QueryList中。

最后,我们可以使用this.modelRefList.forEach循环查询列表并访问每个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

import { ViewChild, Component, ViewChildren, QueryList, AfterViewInit } from '@angular/core';
import { NgModel } from '@angular/forms';

@Component({
selector: 'app-viewchildren2',
template: `
<h1>ViewChildren Example</h1>

<input *ngIf="showFirstName" name="firstName" [(ngModel)]="firstName">
<input *ngIf="showMiddleName" name="midlleName" [(ngModel)]="middleName">
<input *ngIf="showlastName" name="lastName" [(ngModel)]="lastName">


<input type="checkbox" id="showFirstName" name="showFirstName" [(ngModel)]="showFirstName">
<input type="checkbox" id="showMiddleName" name="showMiddleName" [(ngModel)]="showMiddleName">
<input type="checkbox" id="showlastName" name="showlastName" [(ngModel)]="showlastName">

<button (click)="show()">Show</button>`,
})

export class ViewChildrenExample2Component implements AfterViewInit {

firstName;
middleName;
lastName;

showFirstName=true;
showMiddleName=true;
showlastName=true;

@ViewChildren(NgModel) modelRefList: QueryList<NgModel>;

ngAfterViewInit() {

this,this.modelRefList.changes
.subscribe(data => {
console.log(data)
}
)
}


show() {
this.modelRefList.forEach(element => {
console.log(element)
//console.log(element.value)
});

}
}

指令

       @ViewChild、@ViewChildren也是可以获取到指令对象的。指令对象的获取和组件对象的获取差不多,唯一不同的地方就是用模板变量名获取指令的时候要做一些特殊的处理。我们还是用具体的实例来说明。我们自定义一个非常简单的指令TestDirective,添加exportAs属性。代码如下。

exportAs属性很关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import {Directive, ElementRef} from '@angular/core';

/**
* 指令,测试使用,这里使用了exportAs,就是为了方便我们精确的找到指令
*/
@Directive({
selector: '[appTestDirective]',
exportAs: 'appTest'
})
export class TestDirective {

constructor(private elementRef: ElementRef) {
elementRef.nativeElement.value = '我添加了指令';
}

}

获取TestDirective指令,注意单个指令对象获取的时候,模板变量名的写法。比如下面的代码中#divTestDirective=’appTest’,模板变量名等号右边的就是TestDirective指令exportAs对应的名字。

import {AfterViewInit, Component, QueryList, ViewChild, ViewChildren} from ‘@angular/core’;
import {TestDirective} from ‘./test.directive’;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component({
selector: 'app-root',
template: `
<!-- view child for Directive -->
<input appTestDirective/>
<br/>
<input appTestDirective #divTestDirective='appTest'/>
`,
styleUrls: ['./app.component.less']
})
export class AppComponent implements AfterViewInit {
/**
* 获取html里面所有的TestDirective
*/
@ViewChildren(TestDirective) testDirectiveList: QueryList<TestDirective>;
/**
* 获取模板变量名为divTestDirective的TestDirective的指令,这个得配合指令的exportAs使用
*/
@ViewChild('divTestDirective') testDirective: TestDirective;

ngAfterViewInit(): void {
console.log(this.testDirective);
if (this.testDirectiveList != null && this.testDirectiveList.length !== 0) {
this.testDirectiveList.forEach(elementRef => console.log(elementRef));
}
}
}

 总结:@ViewChild、@ViewChildren获取子元素的的时候,我们用的最多的应该就是通过模板变量名,或者直接通过class来获取了。还有一个特别要注意的地方就是获取单个指令对象的时候需要配合指令的exportAs属性使用,并且把他赋值给模板变量名。

HostBinding和HostListener是Angular中的装饰器。HostListener侦听宿主控件事件,而HostBinding允许我们绑定宿主控件的属性。

宿主控件是我们将要附加组件或指令到它上面的目标控件。此功能允许我们在用户对宿主控件执行某些操作或修改样式时采取一些措施;

宿主控件

宿主控件是我们附加指令或组件的元素。请记住,组件是带有视图(模板)的指令。

例如:

请考虑以下ttToggle指令。我们将其附加到按钮组件。这里的按钮组件是宿主组件。

1
<button ttToggle>Click To Toggle</button>

在下面的例子中apphighlight是指令,p控件是宿主控件

1
2
3
4
5
<div>
<p apphighlight>
<div>This text will be highlighted</div>
</p>
</div>

HostBinding

Host Binding将宿主控件属性绑定到指令或组件中的变量

例如:

以下appHighLight指令 通过 HostBinding 将父元素的style.border属性绑定到指令中的border属性。

因此我们更改指令中 border的值,angular将会更新父元素border样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Directive, HostBinding, OnInit } from '@angular/core'

@Directive({
selector: '[appHighLight]',
})
export class HighLightDirective implements OnInit {

@HostBinding('style.border') border: string;

ngOnInit() {
this.border="5px solid blue"
}

}
1
2
3
4
5
6
<div>
<h2>appHighLight Directive</h2>
<p appHighLight>
This Text has blue Border
</p>
</div>

HostListener

HostListener 装饰器用来监听宿主元素上的DOM事件。它还提供了一个在事件发生时调用的处理程序。

例如,在以下代码中,HostListener侦听mouseover和mouseleave事件。我们使用HostListner在宿主元素的MouseOver和MouseLeave事件上绑定相应事件处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Directive, HostBinding, OnInit, HostListener } from '@angular/core'

@Directive({
selector: '[appHighLight]',
})
export class HighLightDirective implements OnInit {

@HostBinding('style.border') border: string;

ngOnInit() {
}

@HostListener('mouseover')
onMouseOver() {
this.border = '5px solid green';
console.log("Mouse over")
}

@HostListener('mouseleave')
onMouseLeave() {
this.border = '';
console.log("Mouse Leave")
}

}

我们在宿主元素p上面使用appHighLight指令,每当鼠标移动到p元素上时,鼠标悬停事件都会被HostListener捕获,它运将行我们附加到它的onMouseOver方法。该方法使用HostBinding为p元素添加一个绿色边界。

类似地,在mouseleave事件中,边框被移除。

1
2
3
4
5
6
<div>
<h2>appHighLight Directive</h2>
<p appHighLight>
This Text has blue Border
</p>
</div>

使用HostBinding附加样式

将样式附加到宿主元素是HostBinding装饰器的常见用法之一。

例如,以下示例将highlight&box样式添加到宿主元素中

1
2
3
4
5
@HostBinding('class') class: string;

ngOnInit() {
this.class="highlight box"
}

同样可以使用 getter 方法

1
@HostBinding('class')  get class() {  return "highlight box"  }

其他例子

1
2
@HostBinding('class.highlight') get hasHighlight () { return true; } 
@HostBinding('class.box') get hasBox () { return false }

HostBinding添加的样式必须存在于宿主元素的作用域中。即highlight&box必须存在于全局样式或我们添加指令的组件中。

在组件中使用 HostBinding & HostListner

这些组件不过是带有模板的指令。因此,我们也可以在组件中同时使用HostBinding和HostListner。

以下是一个BoxComponent组件,它将highlight & box样式应用于宿主元素。样式highlight & box定义在组件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, HostBinding, HostListener } from '@angular/core';

@Component({
selector: 'app-box',
template: `
<h2> This is Box Component</h2> `,
styles: [
`
.highlight {
color:green;
display: block;
}

.box {
border: 1px dashed green;
}
`
],
})
export class BoxComponent {
title = 'hostBinding';

@HostBinding('class.highlight') get hasHighlight() { return true; }
@HostBinding('class.box') get hasBox() { return true }
}

在父组件中添加如下代码

1
<app-box></app-box>

运行该应用程序,您将不会看到任何边框,也不会突出显示文本。即,因为宿主元素存在于父组件(AppComponent)范围中而不存在于BoxComponent范围中。因此,BoxComponent中的任何CSS样式都不会产生任何影响

1
2
3
4
5
6
7
8
.highlight {
color:blue;
display: block;
}

.box {
border: 1px solid red;
}

打卡父组件样式文件并添加以上样式,这样宿主组件可以正常显示了

让我们探索如何使用ng-content在模板中添加外部内容。我们知道如何使用@Input装饰器将数据从父组件传递到子组件。但它仅限于数据,我们不能使用该技术将包含HTML、CSS等元素的内容传递给子组件。要做到这一点,我们必须利用内容投影。

内容投影是将HTML内容从父组件传递到子组件的一种方式。子组件将在指定位置显示模板内容。

我们使用ng-content素在子组件的模板中指定一个位置。ng-content还允许我们使用选择器属性创建多个插入位置。

什么是 Ng-Content

ng-content标记充当占位符,用于插入外部或动态内容。父组件将外部内容传递给子组件。Angular解析模板时,会在子组件模板中ng-content出现的位置插入外部内容。

我们可以使用内容投影来创建一个可重用的组件。具有类似逻辑和布局的组件,可以在应用程序的许多地方使用。

以卡片组件为例。它有页眉部分、页脚部分和正文部分。这些部分的内容会有所不同。ng-content将允许我们将这些部分从父组件传递到卡片组件。这使我们能够在应用程序的许多地方使用卡片组件。

要了解内容投影是如何使用ng-content工作的,首先让我们构建一个没有ng-content的简单按钮组件。

无Ng-Content例子

创建一个新的angular应用程序,并创建一个新的btn.component.ts组件。这是一个简单的组件,它显示一个标题为“点击我”的按钮

1
2
3
4
5
6
7
8
9
10
import { Component } from '@angular/core';

@Component({
selector: 'app-btn',
template: `<button>
Click Me
</button>`
})
export class BtnComponent {
}

现在切换到app.component.html.

1
2
3
<h2>Simple Button Demo</h2>
<app-btn></app-btn>
<app-btn></app-btn>

在上面的代码中,我们添加了两个标题为Click Me的按钮组件,他们按预期显示在屏幕上。

如果我们想从父组件更改标题,该怎么办。我们可以使用@Input属性来实现这一点。但使用@input,我们只能设置按钮的标题,无法改变子组件的外观。

ng-content 例子

创建新的组件(FancyBtnComponent),从上面的例子中复制全部代码,删除Click Me并添加<ng-content></ng-content>。此标记充当占位符。您也可以将其视为组件的一个参数。

打开 app.component.html 文件并修改内容如下:

1
2
3
<h2>Button Demo With ng-content</h2>
<app-fancybtn>Click Me</app-fancybtn>
<app-fancybtn><b>Submit</b></app-fancybtn>

<app-fancybtn></app-fancybdn>之间的内容将传递给我们的FancyBtnComponent组件。该组件将其显示在ng-content的位置。

这种解决方案的优点是可以传递任何HTML内容。

事件

点击、输入等事件都会向上冒泡传递,因此可以在父对象中捕获,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
**app.component.html**

<h2>Button with click event</h2>
<app-fancybtn (click)="btnClicked($event)"><b>Submit</b></app-fancybtn>


** App.component.ts ***

btnClicked($event) {
console.log($event)
alert('button clicked')
}

但是,如果您有多个按钮,那么您可以通过检查$event参数来,确定是哪个按钮触发的该事件。

自定义事件

你可以使用@output来创建自定义事件,如下所示

1
2
3
4
5
6
 
@Output() someEvent:EventEmitter =new EventEmitter();

raiseSomeEvent() {
this.someEvent.emit(args);
}

在父组件中

1
<app-fancybtn (someEvent)=”DoSomething($event)”><b>Submit</b></app-fancybtn>

使用ng-content实现多投影

如下例所示, 创建一个新的组件card.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component } from '@angular/core';

@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="header">
<ng-content select="header" ></ng-content>
</div>
<div class="content">
<ng-content select="content" ></ng-content>
</div>
<div class="footer">
<ng-content select="footer" ></ng-content>
</div>
</div>
`,
styles: [
` .card { min- width: 280px; margin: 5px; float:left }
.header { color: blue}
`
]
})
export class CardComponent {
}

在上面的例子中,我们有三个ng-content 标签,他们的选择器分别是 header、content、footer。

现在我们打开app.component.html文件,添加如下代码

1
2
3
4
5
6
7
8
9
10
11
<app-card>
<header><h1>Angular</h1></header>
<content>One framework. Mobile & desktop.</content>
<footer><b>Super-powered by Google </b></footer>
</app-card>

<app-card>
<header><h1 style="color:red;">React</h1></header>
<content>A JavaScript library for building user interfaces</content>
<footer><b>Facebook Open Source </b></footer>
</app-card>

Select属性是CSS选择器

您可以使用任何CSS选择器作为ng-content的select属性。比如class、element、id属性等。例如,下面使用CSS类的卡片组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component } from '@angular/core';

@Component({
selector: 'card',
template: `
<div class="card">
<div class="header">
<ng-content select=".header" ></ng-content>
</div>
<div class="content">
<ng-content select=".content" ></ng-content>
</div>
<div class="footer">
<ng-content select=".footer" ></ng-content>
</div>
</div>
`,
styles: [
` .card { width: 280px; margin: 5px; float:left; border-width:1px; border-style:solid ; }
.header { color: blue}
`
]
})
export class CardComponent {

我们可以如下使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<card>
<div class="header">
<h1>Angular</h1>
</div>
<div class="content">One framework. Mobile & desktop.</div>
<div class="footer"><b>Super-powered by Google </b></div>
</card>

<card>
<div class="header">
<h1 style="color:red;">React</h1>
</div>
<div class="content">A JavaScript library for building user interfaces</div>
<div class="footer"><b>Facebook Open Source </b></div>
</card>

类似地,您可以使用如下所示的各种CSS选择器

1
2
3
<ng-content select="custom-element" ></ng-content>
<ng-content select=".custom-class" ></ng-content>
<ng-content select="[custom-attribute]" ></ng-content>

不带select属性的 Ng-Content 会捕获全部内嵌HTML

在下面的示例中,最后一段HTML不属于任何ng-content,因此,ng-content不会投影最后一段,因为它无法确定添加到哪里。

1
2
3
4
5
6
 <card>
<div class="header"><h1>Typescript</h1></div>
<div class="content">Typescript is a javascript for any scale</div>
<div class="footer"><b>Microsoft </b></div>
<p>This text will not be shown</p>
</card>

为了解决上述问题,我们可以添加一个没有select属性的Ng-Content。它将显示那些不能投影到其他Ng-Content中的HTML。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Component } from '@angular/core';


@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="header">
<ng-content select="header" ></ng-content>
</div>
<div class="content">
<ng-content select="content" ></ng-content>
</div>
<div class="footer">
<ng-content select="footer" ></ng-content>
</div>
<ng-content></ng-content>
</div>
`,
styles: [
` .card { min- width: 280px; margin: 5px; float:left }
.header { color: blue}
`
]
})
export class CardComponent {
}

ngProjectAs

有时候需要使用 ng-container 包装 组件,这种情况多数是使用结构指令 如 ngif、ngSwitch等

在下面的示例中,我们将标头封装在ng-container中。

1
2
3
4
5
6
7
8
9
<card>
<ng-container>
<div class="header">
<h1 style="color:red;">React</h1>
</div>
</ng-container>
<div class="content">A JavaScript library for building user interfaces</div>
<div class="footer"><b>Facebook Open Source </b></div>
</card>

由于ng-container的原因,标头部分不会投影到标头标记位置。相反,它被投影到没有select选择器的ng-content中。

您可以使用ngProjectAs属性,来解决这种情况,如下所示。

1
2
3
4
5
6
7
8
9
<card>
<ng-container ngProjectAs="header">
<div>
<h1 style="color:red;">React</h1>
</div>
</ng-container>
<div class="content">A JavaScript library for building user interfaces</div>
<div class="footer"><b>Facebook Open Source </b></div>
</card>

总结

ng-content允许我们在模板中添加外部内容。与@Input不同,使用ng-content,我们可以传递包括HTML元素、CSS等的数据,这也被称为内容投影。我们还可以使用选择器属性定义不同的插入位置。这些选择器允许我们向不同的ng-content添加不同的内容。

Contentchild & ContentChilden

ContentChild和ContentChildren是装饰器,我们使用它们来查询和获取对DOM中投影内容的引用。投影内容是组件从父组件接收的内容。

ContentChild和ContentChildren与ViewChild和ViewChildren非常相似。我们使用ViewChild或ViewChildren来查询和获取组件中DOM元素的引用。DOM元素可以是HTML元素、子组件或指令等。但是,我们不能使用ViewChild或ViewChildren来获取使用投影插入的模板实例。

内容投影回顾

内容投影是将HTML内容从父组件传递到子组件的一种方式。子组件将在指定位置显示投影进来的模板。我们使用ng-content元素在子组件的模板中为投影进来的模板指定一个位置。ng-content还允许我们使用选择器属性创建多个投影位置。父组件可以向每个投影位置发送不同的内容。

ContentChild and ContentChildren 例子

为了理解ContentChild和ContentChildren是如何工作的,让我们创建一个简单的卡片应用程序。该应用程序有一个CardComponent,它显示单个卡片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component} from '@angular/core';


@Component({
selector: 'card',
template: `

<div class="card">
<ng-content select="header"></ng-content>
<ng-content select="content"></ng-content>
<ng-content select="footer"></ng-content>
</div>

`,
styles: [
` .card { min- width: 280px; margin: 5px; float:left }
`
]
})
export class CardComponent {

}

该组件通过三个Ng-Content定义了多个插槽。插槽的名称分别为页眉、内容和页脚。使用组件的用户可以将内容发送到这三个插槽中的任何一个或全部。

以下代码来CardListComponent组件的,CardListComponent组件实例化了三个CardComponent组件,并分别发送了页眉、内容和页脚的内容。

此外,请注意,我们在页眉内容的h1标签上有#header模板引用变量。现在让我们在CardComponent组件中使用ContentChild来访问h1元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Component } from '@angular/core';

@Component({
selector: 'card-list',
template: `

<h1> Card List</h1>

<card>
<header><h1 #header>Angular</h1></header>
<content>One framework. Mobile & desktop.</content>
<footer><b>Super-powered by Google </b></footer>
</card>

<card>
<header><h1 #header style="color:red;">React</h1></header>
<content>A JavaScript library for building user interfaces</content>
<footer><b>Facebook Open Source </b></footer>
</card>

<card>
<header> <h1 #header>Typescript</h1> </header>
<content><a href="https://www.tektutorialshub.com/typescript-tutorial/"> Typescript</a> is a javascript for any scale</content>
<footer><i>Microsoft </i></footer>
</card>

`,
})
export class CardListComponent {

}

使用 ContentChild 和 ContentChildren

让我们返回 CardComponent 组件

首先,导入 ContentChild 元素

1
import { Component, ContentChild, ContentChildren, ElementRef, Renderer2,  ViewChild } from '@angular/core';

然后,使用它在投影内容中查询 header元素

1
@ContentChild("header") cardContentHeader: ElementRef;

这里,cardContentHeader是变量。我们对该变量应用@ContentChild装饰器。header是我们想要读取的模板变量,它应用于h1元素上。

cardContentHeader变量无法立即使用。因为组件生命周期挂钩,angular首先初始化组件,然后它会引发ngOnChanges、ngOnInit和ngDoCheck挂钩。接下来将初始化投影的组件,然后Angular抛出AfterContentInit和AfterContentChecked钩子。因此,cardContentHeader只能在AfterContentInit挂钩之后使用。

一旦我们引用了DOM元素,我们就可以使用renderor2来操纵它的样式等。

1
2
3
4
5
6

ngAfterContentInit() {

this.renderor.setStyle(this.cardContentHeader.nativeElement,"font-size","20px")

}

ViewChild Vs ContentChild

例如,在CardComponent组件中,使用ViewChild查询来读取页眉元素。您会发现cardViewHeader是未定义

1
@ViewChild("header") cardViewHeader: ElementRef;

ContentChild语法

ContentChild查询DOM并返回第一个匹配元素,然后更新组件中对应的变量

语法

ContentChild的语法如下所示。

1
ContentChild(selector: string | Function | Type<any>, opts: { read?: any; static: boolean; }): any

我们在组件属性上应用contentChild装饰器,它有两个参数,第一个参数是selector选择器,第二个参数是opts选项。

selector(查询选择器):用于查询的指令类型或查询字符串名称

opts:有两个选项。

static:设置为True:解析查询结果在变更检测之前执行,设置为false:解析查询结果在更改检测之后执行,默认为false。

read:使用它从查询的元素中读取不同的令牌

变更检测查找第一个与selecter匹配的元素,并使用该元素更新组件中的参数。如果DOM发生更改,并且有一个新元素与selecter匹配,则变更检测会更新组件中的对应参数。

Selector

查询选择器可以是字符串、类型或返回字符串或类型的函数。支持以下选择器。

  • 组件或指令类型
  • 作为字符串的模板引用变量
1
2
3
4
5
/Using a Template Reference Variable
@ContentChild("header") cardContentHeader: ElementRef;

//Using component/directive as type
@ContentChild(childComponent) cardChildComponent: childComponent;

Static

确定何时解析查询。设置为True:当视图首次初始化(在第一次更改检测之前)解析,设置为False:在每次变更检测之后解析。

Read

使用它可以从查询的元素中读取不同的令牌。

例如,考虑以下投影内容。nameInput可以是输入元素,也可以是ngModel指令。

1
<input #nameInput [(ngModel)]="name">

以下代码中的ContentChild将输入元素作为elementRef返回。

1
@ContentChild('nameInput',{static:false}) nameVar;

您可以使用read令牌来要求ContentChild返回正确的类型。

1
2
3
@ContentChild('nameInput',{static:false, read: NgModel}) nameVarAsNgModel;
@ContentChild('nameInput',{static:false, read: ElementRef}) nameVarAsElementRef;
@ContentChild('nameInput', {Static:false, read: ViewContainerRef }) nameVarAsViewContainerRef;

ContentChildren

使用ContentChildren装饰器从投影的内容中获取元素引用的列表。

ContentChildren与ContentChild不同。ContentChild总是返回对单个元素的引用。如果存在多个元素,则ContentChild返回第一个匹配元素,ContentChildren总是将所有匹配的元素作为QueryList返回。您可以遍历列表并访问每个元素。

语法

contentChildren的语法如下所示。它与contentChild的语法非常相似,它没有Static选项,但又descendants选项

将descendants设为True以包括所有子元素,否则仅包括直接子元素。

ContentChildren总是在更改检测之后解析。即为什么它没有static选项。而且,您不能在ngOnInit钩子中引用它,因为它还没有初始化。