# 关于文件对象的一些描述

在前端处理文件时会经常遇到File、Blob、ArrayBuffer、FileReader、FormData这些文件操作相关的名词,有时候很熟悉但是具体怎么用还是要去搜索一番,为了以后在处理文件时能有一个清晰的认知,我们来搞清楚这些有关于文件对象和方法的基础概念和使用。

# 各种文件类型和方法

# Blob对象

Blob(Binary Large Object)对象表示一个不可变、原始数据的类文件对象,用来存储二进制数据。它可以按文本、二进制或者文件流的格式进行读取。可以用于a标签下载文件和导出文件。

创建Blob

我们可以通过Blob构造函数Blob(array[, options])创建一个Blob对象,也可以通过接口请求把文件读取成Blob对象进行返回。

// type是以什么的格式识别
const blob = new Blob(['hello'], { type: 'text/html'})
const blob1 = new Blob(['hello'], { type: 'application/pdf'})
const blob2 = new Blob(['a', 'b'])
const blob3 = new Blob(['<div style='color:red;'>This is a blob</div>']);
const blob4 = new Blob([{ "name": "abc" }])

实例属性和方法

  • Blob.size 包含数据的大小(字节)。

  • Blob.type 一个字符串,返回Blob数据的MIME 类型 (opens new window)

  • Blob.arrayBuffer() 返回一个 promise,返回二进制ArrayBuffer数据。

  • Blob.bytes() 返回一个 promise,返回Uint8Array格式数据。

  • Blob.slice() 截取指定范围内的Blob数据。

  • Blob.stream() 返回一个ReadableStream可读文件流。

  • Blob.text() 返回一个 promise,返回UTF-8 格式的字符串。

# File文件对象

File文件对象继承自Blob,表示文件的相关信息,通过 <input type="file"> 元素选择文件或者通过拖放操作得到,常用于从本地选择文件后进行文件上传、拖拽上传、文件预览、大文件分片等操作。

const dom = document.getElementById('input');
dom.addEventListener('change', function(event){
  const file = event.files[0];
  console.log(file);
});

# ArrayBuffer对象

ArrayBuffer就是文件的二进制数据缓冲区对象,是以数组进行处理的二进制数据,也称二进制数组。ArrayBuffer对象代表储存二进制数据的一段内存,所以无法直接操作,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。常用于图形渲染音视频处理等。

const buffer = new ArrayBuffer(32);

ArrayBuffer无法通过JSON序列化和保存到缓存中,也无法用于子组件传值,要想传值需要转成base64字符串传入,组件接收再转成ArrayBuffer。

# FileReader对象

FileReader可以把File或Blob文件对象读取成其他格式类型,如文本、数据URL(Base64)或者ArrayBuffer等,它主要就是用来转换文件格式。FileReader是一个构造函数,实例化后通过实例方法设置对应的操作,然后通过监听事件返回对应的结果。

事件监听:

  • abort 读取操作被中断时触发。 reader.onabort = function(event) {}

  • error 在读取操作发生错误时触发。 reader.onerror = function(event) {}

  • load 读取操作完成时触发。 reader.addEventListener('load', function(event) {})

  • loadstart 读取操作开始时触发。 reader.onloadstart = function(event) {}

  • loadend 读取操作结束时触发。 reader.onloadend = function(event) {}

  • progress 在读取Blob时触发。 reader.onprogress = function(event) {}

实例方法:

  • reader.abort() 中止读取操作。

  • reader.readAsArrayBuffer(file) 将文件内容读取成ArrayBuffe二进制数据。

  • reader.readAsDataURL(file) 将文件内容读取成base64编码字符串。

  • reader.readAsText(file, encoding) 将文件内容读取成指定特殊编码字符串,默认为utf-8。

通过FileReader构造函数生成实例进行读取文件对象:

var file = e.files[0];
// FileReader使用
var reader = new FileReader();
// 以URL格式读取文件,base64格式
reader.readAsDataURL(file)
// 读取成ArrayBuffer
reader.readAsArrayBuffer(file)
// 读取成特殊编码
reader.readAsText(file, {type: 'utf-8'})


// 异步读取
reader.onload = function(e) {
  const result = e.target.result;
  console.log(result);
}

# FormData

FormData 是JS内置对象,常用于表单提交上传文件或发送二进制数据。

const file = event.files[0];


// 创建一个空的 FormData 对象
const formData = new FormData();


// 设置上传表单字段
formData.append('file ', file);
formData.append('type', 'upload');


// 其他方法
formData.get('type');
formData.delete('type');
formData.has('type');


// 上传文件
uploadFileApi(formData).then(res => {
  console.log(res);
});

FormData不能直接设置数组,需要for循环一个一个进行设置。并且无法直接通过打印进行查看。

var arr = [1, 2, 3];
const formData = new FormData();


// 如果是数组,需要循环添加,并且要加[]
arr.forEach(item => {
  formData.append("arr", item);
})


// 或者
arr.forEach((item, index) => {
  formData.append(`arr[${index}]`, item);
});


// 如果是对象需使用JSON.stringify进行序列化
const obj = {a: 1};
formData.append('obj', JSON.stringify(obj));

# URL.createObjectURL

URL.createObjectURL() 是一个非常有用的API,用于创建一个指向 Blob、File 或 MediaSource 对象的临时 URL,这个 URL 可以被用作a/img/video/audio等元素的 src 属性。常用于文件下载、文件临时预览等功能。

// 基础用法
const objectURL = URL.createObjectURL(Blob|File|MediaSource);


const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
console.log(url); // 输出类似于 "blob:http://example.com/12345678-1234-1234-1234-1234567890ab"


// 图片预览
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');


fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  if (file) {
    const url = URL.createObjectURL(file);
    preview.src = url;


    // 释放临时 URL(当不再需要时)
    preview.onload = () => {
      URL.revokeObjectURL(url);
    };
  }
});

# 各种文件类型相互转换

# File、Blob转其他类型

使用FileReader的方法将File或Blob转成Base64和ArrayBuffer。

// 使用readAsDataURL方法转base64
function fileOrBlobToBase64(blob, callback) {
    const reader = new FileReader();
    reader.onload = function(e) {
        callback(e.target.result); // 这是一个包含 base64 数据 URL 的字符串
    };
    reader.readAsDataURL(blob);
}


// 使用readAsArrayBuffer方法转ArrayBuffer
function blobToArrayBuffer(blob, callback) {
    const reader = new FileReader();
    reader.onload = function(e) {
        callback(e.target.result);
    };
    reader.readAsArrayBuffer(blob);
}

使用Blob对象的arrayBuffer()方法(推荐)

// Blob转arrayBuffe
async function blobToArrayBuffer(blob) {
    const arrayBuffer = await blob.arrayBuffer();
    return arrayBuffer;
}

# ArrayBuffer转Blob

要将 ArrayBuffer 转换为 Blob,你可以直接使用 Blob 构造函数,指定你想要的 MIME 类型。

/**
 * @desc ArrayBuffer 转换为 Blob
 * @param {ArrayBuffer} buffer 二进制文件流
 * @param {String} type MIME 类型
 * */ 
function arrayBufferToBlob(buffer, type) {
    return new Blob([buffer], {type: type});
}

# ArrayBuffer转File

把ArrayBuffer转成File对象,首先将其转换为 Blob,然后使用 new File() 构造函数。需要注意的是,需要提供文件名和 MIME 类型等信息。

/**
 * @desc ArrayBuffer 转换为 File
 * @param {ArrayBuffer} arrayBuffer 二进制文件流
 * @param {String} fileName 文件名
 * @param {String} mimeType MIME 类型
 * */ 
function arrayBufferToFile(arrayBuffer, fileName, mimeType) {
    const blob = new Blob([arrayBuffer], {type: mimeType});
    return new File([blob], fileName, {type: mimeType});
}

# ArrayBuffer和Base64互转

通过手动遍历Uint8Array并结合btoa和atob实现,btoa将字符串编码为Base64 格式。atob将Base64 编码的字符串解码回原始字符串。

// ArrayBuffer转Base64
function arrayBufferToBase64(arrayBuffer) {
  // 将 ArrayBuffer 转换为 Uint8Array(字节数组)
  const uint8Array = new Uint8Array(arrayBuffer);
  
  // 将字节数组转换为二进制字符串
  let binaryString = '';
  for (let i = 0; i < uint8Array.length; i++) {
    binaryString += String.fromCharCode(uint8Array[i]);
  }
  
  // 使用 btoa 将二进制字符串转换为 Base64
  return btoa(binaryString);
}


// Base64 转 ArrayBuffer
function base64ToArrayBuffer(base64String) {
  // 使用 atob 将 Base64 字符串解码为二进制字符串
  const binaryString = atob(base64String);
  
  // 创建一个 Uint8Array 来存储字节数据
  const uint8Array = new Uint8Array(binaryString.length);
  
  // 将二进制字符串的每个字符转换为字节
  for (let i = 0; i < binaryString.length; i++) {
    uint8Array[i] = binaryString.charCodeAt(i);
  }
  
  // 返回 ArrayBuffer
  return uint8Array.buffer;
}

通过使用 FileReader 实现互转

// ArrayBuffer转Base64
function arrayBufferToBase64UsingFileReader(arrayBuffer) {
  return new Promise((resolve, reject) => {
    const blob = new Blob([arrayBuffer]);
    const reader = new FileReader();
    reader.onload = () => {
      const dataURL = reader.result; // Data URL 格式
      resolve(dataURL.split(',')[1]); // 去掉前缀
    };
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}


// Base64 转 ArrayBuffer
function base64ToArrayBufferUsingFileReader(base64String) {
  return new Promise((resolve, reject) => {
    const byteString = atob(base64String); // 解码 Base64
    const uint8Array = new Uint8Array(byteString.length);
    for (let i = 0; i < byteString.length; i++) {
      uint8Array[i] = byteString.charCodeAt(i);
    }
    const blob = new Blob([uint8Array]);
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsArrayBuffer(blob);
  });
}

# 获取文件内容

获取文件内容可以直接从本地文件系统上传,也可以通过读取本地地址和网络地址进行获取。从本地文件系统上传可以直接通过input获取;本地地址和网络地址则需要通过Ajax或fetch请求把内容下载下来才可以。

1、Ajax通过responseType参数来设置响应数据类型。

  • text 默认 返回text文本类型。

  • document Document对象,返回 XML 格式数据时使用。

  • json 返回JSON数据格式。

  • blob 返回二进制数据的Blob对象。

  • arrayBuffer 返回二进制数据的ArrayBuffer对象。

// 获取文件内容
function downloadUrlFile(url) {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', url, true)
  xhr.responseType = 'blob'
  xhr.onload = () => {
    if (xhr.status === 200) {
      // 获取文件blob数据
      console.log(xhr.response);
    }
  }
  xhr.send()
}

2、fetch设置响应数据类型。

  • response.text():返回文本字符串。

  • response.json():返回JSON 对象。

  • response.blob():返回二进制 Blob 对象。

  • response.formData():返回FormData 表单对象。

  • response.arrayBuffer():返回二进制 ArrayBuffer 对象。

// 获取文件内容
function downloadUrlFile(url) {
  fetch(url)
  .then((res) => res.blob())
  .then(data => {
    console.log(data);
  })
}

# 文件下载

文件下载一般是通过接口请求把文件读取成blob格式数据返回,然后通过URL.createObjectURL()将Blob对象转成临时URL,并手动创建a标签模拟点击进行保存到本地。

// 文件下载
function downloadFile(url, params) {
  axios({
    url,
    method: 'get',
    params,
    responseType: 'blob'
  }).then(res => {
    let blob = new Blob([res]);
    let url = URL.createObjectURL(blob);
    let a = document.createElement("a");
    a.href = url;
    a.download = "文件名";
    a.click();
    // 释放临时 URL
    URL.revokeObjectURL(url);
  })
}


/**
 * 根据文件url获取文件名
 * @param url 文件url
 */
getFileName(url) {
  const num = url.lastIndexOf('/') + 1
  let fileName = url.substring(num)
  // 把参数和文件名分割开
  fileName = decodeURI(fileName.split('?')[0])
  return fileName
}

# 文件上传

文件上传需要通过<input type="file">获取文件内容后,通过FormData格式进行上传,我们借助Element 的上传组件来实现文件上传功能。

<template>
  <el-dialog title="上传文件" :visible="true" width="500px" @close="closeDialog">
    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
      <el-form-item label="选择文件" prop="filePath">
        <div class="upload">
          <div class="upload-wrap" v-if="!fileList.length">
            <!-- 上传组件 -->
            <el-upload
              action="#"
              :auto-upload="false"
              :show-file-list="false" 
              :on-change="handleUploadChange"
            >
              <!-- 自定义上传样式 -->
              <slot>
                <el-button type="primary" plain icon="el-icon-upload2" size="small" :loading="uploading">点击上传文件</el-button>
              </slot>
            </el-upload>
          </div>
          <div class="uplod-file" v-if="fileList.length">
            <div class="file-item" v-for="(item, index) in fileList" :key="index">
              <div class="item-name text-line-1">{{ item.name }}</div>
              <div class="item-del">
                <i class="el-icon-delete" @click="delUploadFile(index)"></i>
              </div>
            </div>
          </div>
        </div>
      </el-form-item>
    </el-form>
    <template slot="footer">
      <el-button @click="closeDialog">取 消</el-button>
      <el-button type="primary" @click="submitForm">确 认</el-button>
    </template>
  </el-dialog>
</template>
<script>
/**
 * @desc 文件上传
 * @author cxx
 * */
import { uploadFileApi } from '@/api/public'
export default {
  name: 'UploadFile',
  data() {
    return {
      uploading: false,
      saveLoad: false,
      // 基础信息表单
      formData: {
        filePath: '',
        percent: 0 // 上传进度
      },
      // 基础信息商品校验
      formRules: {
        filePath: [{ required: true, message: '请选择', trigger: ['change'] }]
      },
      fileList: []
    }
  },
  methods: {
    // 上传前验证
    handleBeforeUpload(file) {
      const { name, size } = file
      const fileExtension = name.split('.').pop()
      // 类型验证
      const limitTypeList = ['pdf']
      const limitType = limitTypeList.includes(fileExtension)
      if (!limitType) {
        this.$modal.msgError(`不支持${name}文件类型上传!`)
        // return false
      }
      // 文件大小验证
      const limitSize = size / 1024 / 1024 < 10
      if (!limitSize) {
        this.$modal.msgError(`${name}文件不可大于 10MB!`)
        // return false
      }
      return limitType && limitSize
    },
    // 上传文件内容
    handleUploadChange(elFile) {
      const that = this
      const file = elFile.raw
      console.log('文件对象', file)
      // 上传前格式大小验证
      if (!this.handleBeforeUpload(file)) return
      
      // 通过FormData格式上传
      const formData = new FormData()
      formData.append('file', file)
      formData.append('bizType', 'content')
      this.uploading = true
      uploadFileApi(formData, {
        //获取文件上传的进度
        onUploadProgress(e) {
          let completeProgress = ((e.loaded / e.total * 100) | 0)
          that.formData.percent = completeProgress
        }
      }).then(data => {
          console.log(data)
          this.fileList.push(file)
          this.formData.filePath = data.data
          this.$refs.formRef.clearValidate()
        }).finally(() => {
          this.uploading = false
        })
    },
    // 删除文件
    delUploadFile(index) {
      this.$confirm(`确定删除该文件?`, '提示', {
        customClass: 'el-confirm-custom',
        cancelButtonText: '取消',
        confirmButtonText: '删除',
        showClose: false,
        center: true
      }).then(() => {
        this.fileList.splice(index, 1)
        this.formData.filePath = ''
      })
    },
    // 点击完成按钮
    submitForm() {
      this.$refs.formRef.validate(valid => {
        // 验证不通过
        if (!valid) return
        const params = {
          fileUrl: this.formData.filePath,
          id: this.studentId
        }
        this.saveLoad = true
        testApi(params)
          .then(res => {
            this.saveLoad = false
            this.$message.success('操作成功!')
            this.$emit('closeDialog', true)
          })
          .catch(err => {
            this.saveLoad = false
          })
      })
    },
    // 返回列表页面
    closeDialog() {
      this.$emit('closeDialog')
    }
  }
}
</script>
<style lang="scss" scoped>
// 上传组件
.upload {
  width: 100%;
  .upload-wrap {
    margin-bottom: 10px;
  }
  .uplod-file {
    width: 100%;
    .file-item {
      display: flex;
      align-items: center;
      width: 100%;
      overflow: hidden;
      .item-name {
        max-width: 300px;
        font-weight: 400;
        font-style: 14px;
        color: #6B5CDA;
        line-height: 22px;
      }
      .item-del {
        margin-left: 12px;
        color: #888888;
        cursor: pointer;
      }
    }
  }
}
</style>

接口请求设置

// 上传文件接口,onUploadProgress用户获取上传进度
export function uploadFileApi(parameter, {
  onUploadProgress
}) {
  return request({
    url: api.uploadFile,
    method: 'POST',
    data: parameter,
    onUploadProgress
  })
}

参考链接:文件 API (opens new window)