Skip to content

介绍

File 对象

File 对象FileList 对象 多用于文件上传 <input type="file" /> FileList 就是 File 对象的集合, 简单来说 FileList 就是一个数组, 其中每个元素都是一个 File 对象

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JavaScript</title>
  </head>
  <body>
    <div id="app">
      <input type="file" id="file-upload" />
    </div>
    <script>
      const fileInput = document.getElementById('file-upload');
      fileInput.addEventListener('change', (e) => {
        const files = e.target.files; // FileList 对象
        const file = files[0]; // File 对象
        console.log(file);
      });
    </script>
  </body>
</html>

Blob 对象

Blob 就是 Binary Large OBject 的缩写, 表示一个不可变的, 原始数据类型的文件对象

Blob 可以存储文本或者二进制数据(比如从网络下载的/或者从文件系统读取的)

File 对象继承自 Blob 对象

js
const obj = {
  id: 1001,
  name: 'test-name',
  email: 'test-email@example.com',
  token: 'xxx-token-string',
};
const str = JSON.stringify(obj, null, 2);
const blo = new Blob([str], {
  type: 'application/json;charset=utf-8',
});

FileReader 对象

  • FileReader 可用于异步读取文件(File/Blob)内容
  • FileReaderSync 可用于同步阻塞读取文件(File/Blob)内容
js
const obj = {
  id: 1001,
  name: 'test-name',
  email: 'test-email@example.com',
  token: 'xxx-token-string',
};
const str = JSON.stringify(obj, null, 2);

const blo = new Blob([str], {
  type: 'application/json;charset=utf-8',
});

// 读取 blob 对象内容
// 可以读取 blob 对象, 就可以读取 file 对象
// 因为两个对象可以互相转换
const reader = new FileReader();
reader.readAsText(blo);
reader.onload = (e) => {
  console.log(e.target.result);
};

对象转换

file 对象转 blob 对象

js
function file2blob(file) {
  if (!(file instanceof File)) {
    throw new TypeError('[file2blob]:paramater is not instanceof File');
  }
  return new File([file], file.name, {
    type: file.type,
  });
}

blob 对象装 file 对象

js
function blob2file(blob, filetype = 'text/plain', filename) {
  if (!(blob instanceof Blob)) {
    throw new TypeError('[blob2file]paramater is not instanceof Blob');
  }
  const fname = filename || Math.random().toString(16).substring(2);
  return new File([blob], fname, {
    type: filetype,
  });
}

读取文件方法 Promise 封装

js
function isCallable(fn) {
  return typeof fn === 'function';
}

function read(file, userHandlers = {}, readAsFunc = null) {
  const allowAsFuncs = ['readAsText', 'readAsArrayBuffer', 'readAsDataURL', 'readAsBinaryString'];
  if (!allowAsFuncs.includes(readAsFunc)) {
    throw new Error(`${readAsFunc} is not supported in FileReader API`);
  }

  const handlers = Object.assign(
    {
      onAbort: null,
      onError: null,
      onLoad: null,
      onLoadEnd: null,
      onLoadStart: null,
      onProgress: null,
    },
    userHandlers,
  );

  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    // reject
    reader.addEventListener('abort', (event) => {
      isCallable(handlers.onAbort) && handlers.onAbort();
      reject({ reject_type: 'abort', event, reader, file });
    });
    reader.addEventListener('error', (event) => {
      isCallable(handlers.onError) && handlers.onError();
      reject({ reject_type: 'error', event, reader, file });
    });

    // resolve
    reader.addEventListener('load', (event) => {
      resolve({ event, reader, file });
    });

    // other callbacks
    reader.addEventListener('loadend', (event) => {
      isCallable(handlers.onLoadEnd) && handlers.onLoadEnd(event);
    });
    reader.addEventListener('loadstart', (event) => {
      isCallable(handlers.onLoadStart) && handlers.onLoadStart(event);
    });
    reader.addEventListener('progress', (event) => {
      isCallable(handlers.onProgress) && handlers.onProgress(event);
    });

    reader[readAsFunc].call(reader, file);
  });
}

export function readAsText(file, handlers) {
  return read(file, handlers, 'readAsText');
}

export function readAsBinaryString(file, handlers) {
  return read(file, handlers, 'readAsBinaryString');
}

export function readAsDataURL(file, handlers) {
  return read(file, handlers, 'readAsDataURL');
}

export function readAsArrayBuffer(file, handlers) {
  return read(file, handlers, 'readAsArrayBuffer');
}

应用

直接预览用户选择的图片

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JavaScript</title>
  </head>
  <body>
    <div id="app">
      <input type="file" id="file-upload" />
      <img src="" id="preview" style="display: none" />
    </div>
    <script src="./reader.js"></script>
    <script>
      const fileInput = document.getElementById('file-upload');
      const previewImage = document.getElementById('preview');

      fileInput.addEventListener('change', async (e) => {
        const file = e.target.files[0];
        const { reader } = await readAsDataURL(file); // 使用 reader.js 的方法
        previewImage.setAttribute('src', reader.result);
        previewImage.style.display = 'block';
      });
    </script>
  </body>
</html>

大文件分片上传

  1. 需要将 file 文件转 blob 对象
  2. 使用 blob 的 slice 方法切片
  3. 利用 FormData 对象组合数据
  4. 发送请求携带(FormData)数据
js
const fileInput = document.getElementById('file-upload');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const iter = slice(file);

  const originFileInfo = {
    name: file.name,
    size: file.size,
    type: file.type,
  };

  // 切分一次, 就上传一次
  let item = iter.next();
  let chunkIndex = 0;
  while (!item.done) {
    console.log('此次上传数据:', chunkIndex, item.value);

    const formData = buildFormData(originFileInfo, chunkIndex, iter.value);
    await request(formData);

    chunkIndex++;
    item = iter.next();
  }
});

// 大文件切分
function* slice(blob, chunkSizeMB = 1, sliceType = '') {
  const totalSize = blob.size;
  const chunkSize = Math.pow(1024, 2) * chunkSizeMB;

  let start = 0;
  let end = 0;
  while (start < totalSize) {
    end = start + chunkSize;
    yield blob.slice(start, end, sliceType);
    start = end;
  }
}

// 构建提交表单
function buildFormData(originFileInfo, index, blob) {
  const formData = new FormData();
  formData.append('origin_file_info', JSON.stringify(originFileInfo));
  formData.append('slice_index', index);
  formData.append('data', blob);
  return formData;
}

// 发送请求(模拟)
function request(formData) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('上传成功', formData);
      resolve();
    }, 500);
  });
}

客户端触发文件下载

js
const fileInput = document.getElementById('file-upload');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const filename = Math.random().toString(32) + file.name;
  download(file, filename);
});

function download(file, filename) {
  const url = URL.createObjectURL(file);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.click();
  URL.revokeObjectURL(url);
}

浏览器端压缩图片再上传

需要借助图片压缩库 browser-image-compression

获取文件的MD5

需要借助加密算法库 crypto-js

扩展

浏览器中允许访问用户系统中文件的 File System API 可用于做Web端的文件编辑器, 因为可以读写用户操作系统的文件, 如: 在线版 vscode

Released under the MIT License.