Angular实现的图片预览组件

这篇文章记录了使用 angular 实现 web 前端选择文件的预览组件。Angular 版本为 7.3.7

环境

使用ng new --routing --style scss hello-ng创建新的 angular 项目。

--routing 项目使用 angular 路由

--style scss项目使用 sass

切换到src/app目录,使用 ng g c image-preview创建 angular 组件

ng g cng generate component 的缩写,ng generate可用于快速创建module,component,directive等,具体用法可使用ng g --help查看

初始版本

深度截图_选择区域_20190330211601
图 1

代码如下

html

<input type="file" accept="image/*" hidden (change)="change(inputRef)" #inputRef />
<div (click)="inputRef.click()">
  <img *ngIf="preview" [src]="preview" />
  <span>点击上传</span>
</div>

scss

div {
  width: 200px;
  height: 200px;
  border: 2px gray dashed;
  display: flex;
  justify-content: center;
  align-items: center;
}
img[src] + span {
  display: none;
}

模板文件共有四个元素,分别是 input,div,img,span。input 用hidden属性隐藏,关联 div 和 input 的点击事件,实现点击 div 时,弹出文件选择对话框。#inputRef是 angular 的模板引用变量,在此处引用了 input 元素,两者相等。在 input 上还绑定了 change 事件,并且把 input 元素传入。下面在组件的 change 方法里处理图片文件,以实现预览。

  change(input: HTMLInputElement) {
    if (input.files.length < 1) {
      return;
    }
    this.preview = window.URL.createObjectURL(input.files[0]);
  }

这里使用URL.createObjectURL创建 blob 链接,远比以前的 base64 image 方法要高效。特别是图片文件较大时,base64 image 的方式会因为页面插入大量编码数据,造成页面卡顿,而 blob 链接无论文件大小,都会生成类似blob:http://127.0.0.1:4200/8aa78310-7e5f-429f-a00f-316e416fb0e2这种短链接。
preview是组件类的一个普通属性,之前已经在模板中绑定在 img 元素的 src 上,所以这样就大功告成了!?为什么图片不显示

深度截图_选择区域_20190330214715

安全

原来 angular 为了防止XSS攻击

默认把所有值都当做不可信任的。angular 安全相关文档

结果误伤了 blob 链接,在浏览器中审查元素可看到链接前面添加了 unsafe 标示
深度截图_选择区域_20190331135331

这种情况就要DomSanitizer出马了。

import { Component, OnInit } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';

@Component({
  selector: 'app-image-preview',
  templateUrl: './image-preview.component.html',
  styleUrls: ['./image-preview.component.scss'],
})
export class ImagePreviewComponent implements OnInit {
  preview: SafeUrl;
  constructor(private safe: DomSanitizer) {}

  ngOnInit() {}

  change(input: HTMLInputElement) {
    if (input.files.length < 1) {
      return;
    }
    const file = input.files[0];
    const url = window.URL.createObjectURL(file);
    this.preview = this.safe.bypassSecurityTrustUrl(url);
  }
}

在这里使用DomSanitizer.bypassSecurityTrustUrl让生成的 blob 链接绕过安全检查,因为我们信任浏览器生成的链接(当链接来源于用户输入,就要小心你的信任别被辜负了)
到此为止,它已经可以正常工作了,但图片上传预览只是表单的一小部分,下面来把它封装为 Angular 表单组件。

ControlValueAccessor

Angular 中提供了两种方式处理表单输入

  1. 模板驱动表单
  2. 响应式表单

两种控制方式虽然各有优劣,但底层都是通过ControlValueAccessor
充当angular和dom元素的桥梁,所以只要组件实现ControlValueAccessor接口,就可在两种表单中使用。

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  setDisabledState(isDisabled: boolean)?: void
}

要继承ControlValueAccessor接口,需要实现四个方法。
writeValuesetDisabledState类似,只不过一个用于设置值,一个用于设置禁用状态。这两个方法提供给外部调用,需要组件在这两个函数被调用时,设置组件自身的值或状态。

registerOnChangeregisterOnTouched类似,用于注册回调函数,这里的fn类似addEventListener(***,callback)传递的callback函数,当组件内发生值的变动或者组件被触碰时,组件需要主动调用传递进来的fn函数。
最终组件实现

@Component({
  selector: 'app-image-preview',
  templateUrl: './image-preview.component.html',
  styleUrls: ['./image-preview.component.scss'],
  providers: [
    {
      multi: true,
      provide: NG_VALUE_ACCESSOR,
      useExisting: ImagePreviewComponent,
    },
  ],
})
export class ImagePreviewComponent implements OnInit, ControlValueAccessor {
  preview: SafeUrl;
  constructor(private safe: DomSanitizer) {}
  onChange: (value: string) => void;
  ngOnInit() {}

  change(input: HTMLInputElement) {
    if (input.files.length < 1) {
      return;
    }
    const file = input.files[0];
    const url = window.URL.createObjectURL(file);
    this.onChange(url);
    this.preview = this.safe.bypassSecurityTrustUrl(url);
  }

  writeValue(obj: any): void {
    this.preview = obj;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {}
  setDisabledState?(isDisabled: boolean): void {}
}

这里组件并不支持禁用和触碰(其实是偷懒)就不必实现registerOnTouchedsetDisabledState

发表评论

电子邮件地址不会被公开。 必填项已用*标注