最近正写个小项目复习 Angular,在 DOM 对象的获取上遇到了个离奇的问题,目前虽然已经解决,但原理还没完全搞懂,先记录下来。


问题

Angular 组件间通信会用到 @Input()@ViewChild() 等装饰器,正常的 Angular 组件通信不出意外都是没问题的,不再多讲。

由于临时想到记录下来怕以后忘记,而问题已经解决,当时没有截图,所以只是“简单地”用文字记录下

如果要获取原生的 DOM 对象,随便查查就能看到类似下面的写法(伪代码)

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

@Component({
  selector: 'app-file-preview',
  templateUrl: './file-preview.component.html',
  styleUrls: ['./file-preview.component.scss']
})
export class FilePreviewComponent implements AfterViewInit {
  @ViewChild("imageViewer") imageViewer!: ElementRef;

  ngAfterViewInit(): void {
    this.imageViewer.nativeElement.src = "https://example.com/xxx.jpg";
  }

简单的使用貌似没什么问题,如果遇到获取的对象是 undefined 在视图初始化之后再处理对象就行,也就是使用 ngAfterViewInit 代替 onInit

而我的需求稍微麻烦点:

  1. 根据文件对象判断文件类型,得到不同的组件初始化方法
  2. 根据文件对象获取远端的文件地址,得到可观察对象
  3. 得到可观察对象后,根据 1 得到的初始化方法对组件初始化

前两步正常运行,第三步出现 undefined,报错的部分相关代码如下

 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
import {...} from ...;

@Component({
  ...
})
export class FilePreviewComponent implements OnInit, AfterViewInit {
  @Input() file!: File;
  @ViewChild("mediaViewer") mediaViewer!: ElementRef;

  fileUrl!: string;

  groupId = environment.groupId;
  isVideo!: boolean;
  isAudio!: boolean;
  isImage!: boolean;

  constructor(private fileService: FileService) { }

  ngAfterViewInit(): void {
    console.log(this.mediaViewer.nativeElement);  // => [object Object]
    this.isVideo = ["mp4", "mkv", "flv", "wmv", "avi"]
      .indexOf(this.file.getFileType()) != -1;
    this.isAudio = ["mp3", "wav", "flac", "m4a", "aac", "pcm"]
      .indexOf(this.file.getFileType()) != -1;
    this.isImage = ["png", "jpg", "jpeg", "gif"]
      .indexOf(this.file.getFileType()) != -1;

    const mediaInitializer: any = null;

    if (this.isVideo) {
      mediaInitializer = this.initVideoPlayer;
    } else if (this.isAudio) {
      mediaInitializer = this.initAudioPlayer;
    } else if (this.isImage) {
      mediaInitializer = this.initImageViewer;
    }

    if (mediaInitializer) {
      this.fileService.getGroupFileUrl(this.groupId, this.file.fileId, this.file.busid)
        .subscribe(resp => {
          this.fileUrl = resp.data.url;
          mediaInitializer();
        })
    }
  }

  ngOnInit(): void { }

  /**
   * 视频播放器初始化
   * 
   * @param fileUrl 
   */
  initVideoPlayer(fileUrl?: string): void {
    console.log(this.mediaViewer.nativeElement);  // => cannot read property 'mediaViewer' of undefined.
  }

  /**
   * 音频播放器初始化
   * 
   * @param fileUrl 
   */
  initAudioPlayer(fileUrl?: string): void {
    ...
  }

  /**
   * 图片预览初始化
   * 
   * @param fileUrl 
   */
  initImageViewer(fileUrl?: string): void {
    ...
  }
}

分析

根据上面在 ngAfterViewInitinitVideoPlayer 分别打印的结果来分析, 视图初始化后 this.mediaViewer 已经能获取到了,那么在视图初始化之后才调用的方法中, 理论上 this.mediaViewer 应该也是存在的,然而却获取报错。

如果网上查一查,大多数说的都是同样的解决办法,他们的问题都是一样的:DOM 对象初始化之前就使用了变量。 所以只需要换个事件监听就好。

但我这个问题貌似有点不同,仔细看一下报错: cannot read property 'mediaViewer' of undefined. 凭记忆敲的,可能和原来的 message 有点出入

报错并不是 this.mediaViewerundefined,而是说无法从 undefined 获取名为 mediaViewer 的属性…… 明明是从 this 获取的自定义属性,这么说的话,initVideoPlayerthis 发生了什么奇怪的变化? 试着打印一下(代码略),果然前者还是对象,后者就成了 undefined

更多的,由于本人 javascript 经验不多,无法做出更确切的分析。 猜测或许是因为这段代码的赋值,导致获取到的方法由 method 转换成了 function 导致?

1
2
3
4
5
6
7
8
9
    const mediaInitializer: any = null;

    if (this.isVideo) {
      mediaInitializer = this.initVideoPlayer;
    } else if (this.isAudio) {
      mediaInitializer = this.initAudioPlayer;
    } else if (this.isImage) {
      mediaInitializer = this.initImageViewer;
    }

解决

顺着上面的分析,将 mediaInitializer 作为属性赋值,而不是局部变量,以此解决问题。

 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
import {...} from ...;

@Component({
  ...
})
export class FilePreviewComponent implements OnInit, AfterViewInit {
  @Input() file!: File;
  @ViewChild("mediaViewer") mediaViewer!: ElementRef;

  fileUrl!: string;

  groupId = environment.groupId;
  isVideo!: boolean;
  isAudio!: boolean;
  isImage!: boolean;
  mediaInitializer: any = null;

  constructor(private fileService: FileService) { }

  ngAfterViewInit(): void {
    console.log(this.mediaViewer.nativeElement);  // => [object Object]
    if (this.mediaInitializer) {
      this.fileService.getGroupFileUrl(this.groupId, this.file.fileId, this.file.busid)
        .subscribe(resp => {
          this.fileUrl = resp.data.url;
          this.mediaInitializer();
        })
    }
  }

  ngOnInit(): void {
    this.isVideo = ["mp4", "mkv", "flv", "wmv", "avi"]
      .indexOf(this.file.getFileType()) != -1;
    this.isAudio = ["mp3", "wav", "flac", "m4a", "aac", "pcm"]
      .indexOf(this.file.getFileType()) != -1;
    this.isImage = ["png", "jpg", "jpeg", "gif"]
      .indexOf(this.file.getFileType()) != -1;

    if (this.isVideo) {
      this.mediaInitializer = this.initVideoPlayer;
    } else if (this.isAudio) {
      this.mediaInitializer = this.initAudioPlayer;
    } else if (this.isImage) {
      this.mediaInitializer = this.initImageViewer;
    }
  }

  /**
   * 视频播放器初始化
   * 
   * @param fileUrl 
   */
  initVideoPlayer(fileUrl?: string): void {
    console.log(this.mediaViewer.nativeElement);  // => [object Object]
  }

  /**
   * 音频播放器初始化
   * 
   * @param fileUrl 
   */
  initAudioPlayer(fileUrl?: string): void {
    ...
  }

  /**
   * 图片预览初始化
   * 
   * @param fileUrl 
   */
  initImageViewer(fileUrl?: string): void {
    ...
  }
}