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,
  });
}

dataURL(base64)字符串转 blob 对象

js
function dataURL2blob(dataURI) {
  let byteString = atob(dataURI.split(",")[1]);
  let mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
  let ab = new ArrayBuffer(byteString.length);
  let ia = new Uint8Array(ab);
  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }
  return new Blob([ab], { type: mimeString });
}

读取文件方法 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");
}

应用

直接预览用户选择的图片

  1. 直接使用 URL.createObjectURL(blob) 获取文件的链接
  2. 读取图片文件的内容(base64编码)设置给图片元素的 src
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];

        // 1. 使用 URL.createObjectURL
        // previewImage.src = URL.createObjectURL(file);
        // previewImage.style.display = "block";

        // 2. 读取文件内容的方式
        const { reader } = await readAsDataURL(file); // 使用 reader.js 的方法
        previewImage.setAttribute("src", reader.result);
        previewImage.style.display = "block";
      });
    </script>
  </body>
</html>

大文件分片上传

这个需要服务端的配合才可以 具体实现, 请查看这个章节

客户端触发文件下载

一般来说, 下载都是用服务端实现来实现的(添加响应头), 但是有些情况下, 需要临时下载一些文本数据, 且数据已经获取到了, 在浏览器端直接触发下载就可以了, 就不用再发送一次请求去触发下载了

服务端实现
js
app.get("/download", function (req, res) {
  const filepath = "./public/uploads/1.png";
  const filename = path.basename(filepath);
  const mimetype = mime.getType(filepath);
  // 设置响应头, 返回一个可读流 stream
  res.setHeader("Content-disposition", "attachment; filename=" + filename);
  res.setHeader("Content-type", mimetype);
  fs.createReadStream(filepath).pipe(res);
});

这里的实现没有管低版本浏览器兼容问题, 只是为了笔记记录下载的实现原理, 你可以使用 fileSaver.js 兼容低版本浏览器

js
// 生成文件名
function filenameGenerator(fileExt = "") {
  const filename = `${Date.now()}_${Math.random().toString(16).substring(2)}`;
  return fileExt ? `${filename}.${fileExt}` : filename;
}

// 触发下载
function triggerDownload(blob, filename) {
  const link = document.createElement("a");
  const url = URL.createObjectURL(blob);
  link.href = url;
  link.download = filename;
  link.style.display = "none";
  document.body.appendChild(link);
  link.click(); // mock person click
  URL.revokeObjectURL(url);
}

// 下载文本数据
export function downloadText(text, customFilename = "") {
  if (typeof text !== "string") {
    throw new Error("[downloadText]text must be a string");
  }
  const filename = customFilename || filenameGenerator(".txt");
  const blob = new Blob([text], { type: "text/plain" });
  triggerDownload(blob, filename);
}

// 下载 blob 对象
export function downloadBlob(blob, customFilename) {
  if (typeof blob !== "object" || !(blob instanceof Blob)) {
    throw new Error("[downloadBlob]blob must be a Blob object");
  }
  const fileExt = blob.type.split("/").pop();
  const filename = customFilename || filenameGenerator(fileExt);
  triggerDownload(blob, filename);
}

// 应用:
downloadText(`{"id": 1, "name": "test"}`, "some-data.json");
fetch("https://echo.apifox.com/get")
  .then((res) => res.blob())
  .then((blob) => {
    downloadBlob(blob, "some-api-data.json");
  });

浏览器端压缩图片再上传

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

获取文件的MD5

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

扩展

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

Released under the MIT License.