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

背景

最近在 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);
相关推荐
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2136 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy6 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js