Angular无第三方插件的文件操作(保存/预览/下载)

本文旨在不依靠第三方插件如pdfJsviewerJs 来进行文件的保存,预览和下载主要功能实现。

关键词: Blob FileReader Base64 Iframe

一. 预备知识

1. Blob

对Blob的解释如下:

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。 File接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

重点:

  1. 不可不,原始数据的类文件对象
  2. File继承了Blob

也就是说,我们通过的File对象的操作,本质上是对一个Blob对象的操作。因此大家不要疑问,为什么我不直接操作File而是针对一个Blob。当然不是说不可以,但从通用性出发使用Blob,而真正需要使用 File 的时候,使用构造函数可以快速的创建。

看一下.d.ts:

interface Blob {
    readonly size: number;
    readonly type: string;
    arrayBuffer(): Promise;
    slice(start?: number, end?: number, contentType?: string): Blob;
    stream(): ReadableStream;
    text(): Promise;
}

declare var Blob: {
    prototype: Blob;
    new(blobParts?: BlobPart[], options?: BlobPropertyBag): Blob;
};

interface File extends Blob {
    readonly lastModified: number;
    readonly name: string;
}

declare var File: {
    prototype: File;
    new(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
};

type BlobPart = BufferSource | Blob | string;

interface BlobPropertyBag {
    endings?: EndingType;
    type?: string;
}

是不是一看声明更加直观,注意几个地方:

  1. 无论是BlobFile,构造函数的第一个参数是数组;
  2. Blob可以没有文件名,但File必须有文件名,另外File多了两个read only: name & lastModified;
  3. optons虽然是可选,但是为了日后操作的简单,建议至少加上type字段,附上Mime 类型列表

2. FileReader()

参考链接:在web应用程序中使用文件


上面的代码在Html页面中创建一个文件上传的入口,它会渲染出一个File Input Button,用户点击这个上传按钮选择本地文件进行上传。DOM 提供 FileList 对象列出了用户通过选择的所有文件,每一个文件被指定为一个File对象。

代码中对它的定义:

interface FileList {
    readonly length: number;
    item(index: number): File | null;
    [index: number]: File;
}

declare var FileList: {
    prototype: FileList;
    new(): FileList;
};

很好理解FileList就是File的数组,const file: File = fileList[0]

我们获取文件对象方法很多,直接的办法:

const selectedFile = document.getElementById('input').files[0];

或通过事件监听:


那这样得到的File对象就是我们需要的么?

很抱歉,并不是,我们这样获得的File仅仅是对文件的描述信息,包括:

file = {
  lastModified:number // 表示最近一次修改时间的毫秒数;
  lastModifiedDate:Date; // 表示最后一次表示最近一次修改时间的Date对象
  name:string; // 文件名;
  size:number; // 文件的字节大小;
  type:string; // 文件的MIME类型;
}

所以,我们需要借助Html提供的FileReader来获取文件的内容。

也是先看定义(我们简单看最常用的):

interface FileReader extends EventTarget {
    readonly error: DOMException | null;
    readonly readyState: number;
    readonly result: string | ArrayBuffer | null;
    abort(): void; // 中止读取操作。在返回时,readyState属性为DONE。
    onabort: ((this: FileReader, ev: ProgressEvent) => any) | null; // Event Handler
    onerror: ((this: FileReader, ev: ProgressEvent) => any) | null; // Event Handler
    onload: ((this: FileReader, ev: ProgressEvent) => any) | null; // Event Handler
    readAsArrayBuffer(blob: Blob): void; // 开始读取指定的 Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象
    readAsDataURL(blob: Blob): void; // 开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的Base64字符串以表示所读取文件的内容
    readAsText(blob: Blob, encoding?: string): void; // 开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容。

    // ...
}

3. Base64

Base64 是一组相似的二进制到文本(binary-to-text)的编码规则,使得二进制数据在解释成 radix-64 的表现形式后能够用 ASCII 字符串的格式表示出来。Base64 这个词出自一种 MIME 数据传输编码。

Base64编码普遍应用于需要通过被设计为处理文本数据的媒介上储存和传输二进制数据而需要编码该二进制数据的场景。这样是为了保证数据的完整并且不用在传输过程中修改这些数据。Base64 也被一些应用(包括使用 MIME 的电子邮件)和在 XML 中储存复杂数据时使用。

在 JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:

  • atob(): 够解码通过base-64编码的字符串数据。
  • btoa():从二进制数据“字符串”创建一个base-64编码的ASCII字符串。

了解以上信息我们就可以直接开始了。

二. 文件的上传与保存

直接上代码:

 

// .ts only sample for single file upload
public onfileSelected(files: FileList) {
  const file = files[0];
  const fileReader = new FileReader();
  fileReader.onload = () => {
    const uploadedFile = {
      ...file,
      data: this.fileReader.result as string
    };
    // better add the validation and also the error handler
  };
  fileReader.readAsDataURL(files[0]);
}

// post data to server
public onSubmit() {
  this.http.post(url, {
    file: JSON.stringify(this.selectedFile);
  }, options);
}

重点:

  1. 通过创建上传入口;
  2. 通过FileReader.readAsDataURL(File)得到文件内容的Base64字符串;
  3. 将需要的信息整合,最终以字符串形式保存。

三. 文件的下载与预览

基本代码如下:

// ts
public getObjUrl() {
  const blob = this.getPdfBlob();
  return URL.createObjectURL(blob);
 
}

private getfileBlob(dataString: string): Blob {
  try {
    const dataStr = atob(dataString.split(',')[1]); // 去除头部的 type 等非文件内容的字符串
    const u8arr = new Uint8Array(dataStr.length);
    return new Blob([u8arr.map((byte, index) => dataStr.charCodeAt(index))], { type: 'application/xxx' }); // MIME类型
  } catch (error) {
    throw new Error(error);
  }
}

public downloadFile(url: string) {
  if (window.navigator && window.navigator.msSaveBlob) {
    window.navigator.msSaveBlob(blob, this.previewData.name);
  } else {
    const link = document.createElement('a'); // 创建一个标签
    if (link.download !== undefined) {
      link.setAttribute('href', url);  
      link.setAttribute('download', this.previewData.name); // 下载文件名
      link.style.visibility = 'hidden';  // 文件
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    }
  }
}

pulic previewFile(url: string) {
  window.open(url)
}

重点:

  1. 创建Blob时,只需要文件内容的信息,头部的 type 等非文件内容的字符串需要去除;
  2. 字符串转换成Unicode 编码;
  3. 需要声明文件类型;
  4. 下载方法不唯一,在上面的实例中仅是一个参考;
  5. 预览采用的直接打开新界面,通过浏览器自带的浏览功能,可能部分文件无法打开,但对于常见的img, pdf有效,具体预览方法可根据自己需求来进行更改。

提醒:

如果采用