图片编辑器中实现文件上传的三种方式和二进制流及文件头校验文件类型

背景

最近在 vue-design-editor 开源项目中实现 psd 等多种文件格式上传解析成模板过程中, 发现搞定设计文件上传没有使用 input 实现文件上传, 所以我研究了一下相关技术, 总结了以下三种文件上传方法

  1. input 文件选择
  2. window.showOpenFilePicker 和 window.showDirectoryPicker 文件选择和文件夹选择
  3. 拖拽上传

为什么我不使用 file 里面的 type 字段直接当文件类型判断, 下图就是 psd 文件类型

大家发现是不是其实可以通过 type 字段进行判断, 但是告诉大家这是我把 png 图片直接修改了后缀名为 psd, 所以直接通过 type 进行文件类型校验是不准确的, 会有隐患, 如果我们通过二进制流及文件头校验就非常准确了

文件上传

input 文件上传

如果大家实现文件上传第一想到的应该是 input 文件上传, 这里就不多赘述了

js 复制代码
<input type="file" accept=".png, .jpeg, .jpg, .psd" />

缺点就是样式很丑, 定制样式麻烦, 所以实现点击文件上传功能没有直接上手实现

window.showOpenFilePicker 和 window.showDirectoryPicker

支持唤起文件和文件夹, 属于浏览器全局方法,直接调用即可

showSaveFilePicker 保持文件功能

如果有以上三个 api 就可以在浏览器中实现代码编辑器功能

代码实现如下

js 复制代码
const arrFileHandle = await window.showOpenFilePicker({
  types: [
    {
      accept: {
        "image/*": [".psd", ".png", ".gif", ".jpeg", ".jpg", ".webp"],
      },
    },
  ],
  // 可以选择多个图片
  multiple: false,
});
// 遍历选择的文件
for (const fileHandle of arrFileHandle) {
  // 获取文件内容
  const fileData = await fileHandle.getFile();
  fileList.value.push(fileData);
}

不过使用该 api 有限制

  1. 需要 https 环境,如果是本地 localhost 不受此限制。
  2. 不能在 iframe 内使用,因为被认为不安全

所以我在使用该 api 过程中也有兜底逻辑

如果不支持也能点击唤起, 模拟 input 点击唤起

js 复制代码
const input = document.createElement("input");
input.type = "file";
input.multiple = "multiple";
input.accept = ".png, .jpeg, .jpg, .psd";
input.click();
input.addEventListener("change", (file) => {});

拖拽上传

搞定设计支持拖拽上传文件, 所以也支持拖拽上传

js 复制代码
import { tryOnMounted, useEventListener } from "@vueuse/core";
const dragArea = ref();
tryOnMounted(() => {
  useEventListener(dragArea.value, "dragover", onDragOver);
  useEventListener(dragArea.value, "dragleave", onDragLeave);
  useEventListener(dragArea.value, "drop", onDrop);
});
function onDragOver(e: Event) {
  e.preventDefault();
  // 拖拽区域样式提示
  // 也可以通过变量的形式控制dom
  dragArea.value.classList.add("dragover");
  isDrag.value = true;
}

function onDragLeave(e: Event) {
  e.preventDefault();
  dragArea.value.classList.remove("dragover");
  isDrag.value = false;
}

async function onDrop(e: any) {
  e.preventDefault();
  dragArea.value.classList.remove("dragover");
  isDrag.value = false;
  const files = e.dataTransfer.files;
}

比 input 上传的优点是支持上传文件夹

二进制流及文件头校验文件类型

其他校验方法

在背景中也提过通过文件 type 字段校验类型的缺陷

还有一个方法也是有和 type 校验类型一样的缺陷

就是使用文件后缀来判断

js 复制代码
const file = e.files[0];
//获取最后一个.的位置
const index = file.name.lastIndexOf(".");
//获取后缀
const ext = file.name.substr(index + 1);
console.log(ext);

文件后缀名

概念我们了解一下

文件扩展名是文件让电脑识别它的识别器,文件本身的格式是内在的,扩展名是外在的,一般情况下,他们是相互对应的,但如果扩展名被操作或修改,就不能与文件本身的格式对应,就会遇到打不开,打开乱码或无法显示,无法识别等情况。

解决

同样可以将其他类型的文件上传至服务器,或者文件压根就没有后缀,那又要怎么判断呢?因此前端需要使用一个更加合理的方式。

所以使用二进制及文件头的形式来校验文件格式

虽然文件后缀可以手动改,因此可以直接通过读取文件的二进制来判断。 通常来说固定类型的文件头都是相同的,比如说 jpeg 的文件头是 FF D8 FF E0。

枚举类型相关的文件头

js 复制代码
const signatureList = [
  {
    mime: "video/mp4",
    ext: "mp4",
    offset: 4,
    signature: [0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d],
  },
  {
    mime: "video/mp4",
    ext: "mp4",
    offset: 4,
    signature: [0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34],
  },
  {
    mime: "image/jpeg",
    ext: "jpeg",
    signature: [0xff, 0xd8, 0xff],
  },
  {
    mime: "image/png",
    ext: "png",
    signature: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
  },
  {
    mime: "image/gif",
    ext: "gif",
    signature: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61],
  },
  {
    mime: "image/gif",
    ext: "gif",
    signature: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61],
  },
  {
    mime: "image/vnd.adobe.photoshop",
    ext: "psd",
    signature: [0x38, 0x42, 0x50, 0x53],
  },
];

FileReader 读取文件的二进制,之后判断二进制的前几位是否跟符合相应类型文件的文件头。

js 复制代码
/**
 * @description 校验给出的字节数据是否符合某种MIME Type的signature
 * @param {Array} bufferss 字节数据
 * @param {Object} typeItem 校验项 { signature, offset  }
 */
const check = (bufferss: Buffer, { signature, offset = 0 }: any) => {
  for (let i = 0, len = signature.length; i < len; i++) {
    // 传入字节数据与文件signature不匹配
    // 需考虑有offset的情况以及signature中有值为undefined的情况
    if (bufferss[i + offset] !== signature[i] && signature[i] !== undefined) return false;
  }
  return true;
};

/**
 * @description 获取文件二进制数据
 * @param {File} file 文件对象实例
 * @param {Object} options 配置项,指定读取的起止范围
 */
const getArrayBuffer = (file: File, { start, end }: any) => {
  return new Promise((reslove, reject) => {
    try {
      const reader = new FileReader();
      reader.onload = (e: any) => {
        const buffers = new Uint8Array(e.target.result);
        reslove(buffers);
      };
      reader.onerror = (err) => reject(err);
      reader.onabort = (err) => reject(err);
      reader.readAsArrayBuffer(file.slice(start, end));
    } catch (err) {
      reject(err);
    }
  });
};

/**
 * @description 获取文件的真实类型
 * @param {File} file 文件对象实例
 * @param {Object} options 配置项,指定读取的起止范围
 */
const getFileType = (file: File, options = { start: 0, end: 32 }) =>
  getArrayBuffer(file, options)
    .then((buffers: any) => {
      // 找出签名列表中定义好的类型,并返回
      for (let i = 0, len = signatureList.length; i < len; i++) {
        if (check(buffers, signatureList[i])) {
          const { mime, ext } = signatureList[i];
          return { mime, ext };
        }
      }
      // 未找到则返回file对象中的信息
      return { mime: file.type, ext: "" };
    })
    .catch((err) => err);
相关推荐
可乐鸡翅yeah_4 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒4 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技4 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
星光开发者4 小时前
基于决策树随机森林算法的小说推荐系统的设计与实现-计算机毕设【课程设计】78485
vue.js·算法·微信小程序·django·php·课程设计·express
yivifu5 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
vx_Biye_Design5 小时前
springboot高校选课系统82776-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
niyongsheng5 小时前
后端零改动,给若依换一套现代化前端
vue.js·开源·node.js
默_笙5 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript
zhangzeyuaaa6 小时前
深入理解 Ruby 运算符:本质、分类、坑点与重载实战
开发语言·前端·ruby
一木 之林6 小时前
DeepSeek Agent 开发(一)
开发语言·前端·javascript