从一个实战项目,看懂 `new DataTransfer()` 的三大妙用

最近,我写了一份文件上传组件的代码(Jquery😄,先别喷)。但在前端开发中,处理<input type="file">一直是个麻烦事,主要是因为它的files属性是只读的,我们没法用JavaScript去直接修改用户选择的文件列表。

这导致很多常见的需求,比如"带删除按钮的预览列表",实现起来都非常别扭。 如下图:

而这个组件,只用了一个我们平时不太注意的API------new DataTransfer(),就优雅地解决了这个问题。

这篇文章,我们就来逐行分析这组件代码,从中提炼出几个能直接用在项目里的技巧。


new DataTransfer(),实现对input.files的"写"操作

我们先来看这个组件里的最核心的函数:

javascript 复制代码
// 代码片段 1: 核心函数
function setInputFiles(fileList) {
  // 步骤 1: 创建一个空的 DataTransfer 对象
  const dataTransfer = new DataTransfer();

  // 步骤 2: 遍历你自己的文件数组,把文件添加到这个对象里
  for (let file of fileList) {
    dataTransfer.items.add(file);
  }

  // 步骤 3: 把这个对象的 .files 属性,赋值给 input 元素
  document.getElementById('custom-upload-input').files = dataTransfer.files;
}

这段代码揭示了解决问题的关键: 虽然input.files本身是只读的,我们不能对它进行pushsplice操作,但浏览器允许我们用一个新的FileList对象去整个替换它

new DataTransfer()构造函数,就是那个能帮我们凭空创建出所需FileList的工具。

这就是我们需要掌握的,也是最重要的技巧:通过创建一个DataTransfer实例并填充它,我们可以间接地实现对<input type="file">文件列表的程序化控制。


构建"可控的"自定义预览列表

我们再来看组件中的其他部分,它是如何构建出一个带"删除"功能的预览列表的。

javascript 复制代码
// 代码片段 2: 全局变量与删除逻辑
const files = new Map(); // 用一个Map来管理我们自己的文件状态

// ... (addFileToList 函数中)
$item.find('.custom-upload-remove').on('click', function() {
  $item.remove();
  files.delete(id); // 1. 从我们自己的Map中删除文件

  // 2. 如果文件都删完了,就清空input
  if (files.size === 0) {
    const dataTransfer = new DataTransfer();
    document.getElementById('custom-upload-input').files = dataTransfer.files;
  }
  // 注意:更完整的逻辑应该是在删除后,用Map中剩余的文件去更新input
});

这里的代码非常清晰:

  1. 分离"数据状态"与"DOM状态" :代码没有直接操作input.fisles,而是创建了一个独立的files变量(这里用Map,非常适合通过唯一ID进行增删),作为唯一可信的数据源(Single Source of Truth)。
  2. UI渲染基于自己的数据状态 :页面上的文件预览列表,完全是根据files这个Map来渲染的。
  3. 响应用户操作,先更新数据状态 :当用户点击"删除"时,代码首先操作的是files.delete(id),更新我们自己维护的数据。
  4. 最后,同步DOM状态 :在数据状态更新后,再调用技巧一 中的setInputFiles函数(或者一个简化的清空逻辑),用files中最新的文件集合,去覆盖inputfiles属性,保证两者同步。

最后就是:通过"自有状态 -> 更新UI -> 同步input"这个单向数据流,我们可以构建出任何我们想要的、文件上传交互界面。


统一处理多种上传方式,简化逻辑

在这个组件中,同时处理了用户的"点击选择"和"拖拽上传"两种情况。

javascript 复制代码
// 代码片段 3: 事件处理
// 点击选择
$('#custom-upload-input').on('change', function(e) {
  handleFiles(e.target.files);
});

// 拖拽上传
$('#custom-upload-area').on('drop', function(e) {
  e.preventDefault();
  handleFiles(e.originalEvent.dataTransfer.files);
});

这里无论是e.target.files,还是拖拽事件中的e.originalEvent.dataTransfer.files,它们返回的都是一个FileList对象。

DataTransfer这个API,其本职工作就是处理拖拽事件中的数据。event.dataTransfer是浏览器原生提供的实例。而new DataTransfer()允许我们自己创建一个,这恰好为我们统一这两种上传方式提供了可能性。

因此,我们:将文件处理逻辑封装在一个独立的函数(如handleFiles)中,这个函数接收一个FileList对象作为参数。这样,无论是点击上传还是拖拽上传,最终都可以调用这个统一的函数,避免了代码重复,让逻辑更清晰。


完整代码, 可以用在你们项目中供参考:

javascript 复制代码
const files = new Map()
const allowedTypes = [
'image/jpeg','image/png','image/gif','image/jpg','image/webp',
'video/mp4','video/quicktime','video/mov','video/avi','video/mpeg'
];
const maxSize = 20 * 1024 * 1024; // 20MB

function formatSize(size) {
if (size > 1024*1024) return (size/1024/1024).toFixed(1)+'MB';
if (size > 1024) return (size/1024).toFixed(1)+'KB';
return size+'B';
}


$(document).on('submit', `#form`, function (event) {
  event.preventDefault(); // 防止表单提交(为了演示)

  const values = {}
  // 获取表单的所有数据
  for (const { name, value } of $(this).serializeArray()) {
      values[name] = value
  }

  values.media_list = []

  for (const element of files.values()) {
    values.media_list.push(element)
  }

  console.log(values)

  this.reset();

  $('.thankyou').addClass('er-flex').removeClass('er-hidden');
  $(this).hide();

  fetch('https://demo.com/third_part/product_activation', {
     'method': 'POST',
     'headers': {
       'Content-Type': 'application/json'
     },
     body: JSON.stringify(values)
  })
  return false
});

// 点击上传区域,触发文件选择
$('#custom-upload-area').on('click', function(e) {
  // 避免点击input本身时重复触发
  if (e.target.id === 'custom-upload-input') return;
  $('#custom-upload-input').trigger('click');
});

// 选择文件后处理
$('#custom-upload-input').on('change', function(e) {
  handleFiles(e.target.files);
  // 不要在这里再触发 click,否则会死循环
});

$('#custom-upload-area').on('dragover', function(e) {
  e.preventDefault();
  $(this).addClass('dragover');
});
$('#custom-upload-area').on('dragleave', function(e) {
  e.preventDefault();
  $(this).removeClass('dragover');
});
$('#custom-upload-area').on('drop', function(e) {
  e.preventDefault();
  $(this).removeClass('dragover');
  handleFiles(e.originalEvent.dataTransfer.files);
});

function setInputFiles(fileList) {
  const dataTransfer = new DataTransfer();
  for (let file of fileList) {
    dataTransfer.items.add(file);
  }
  document.getElementById('custom-upload-input').files = dataTransfer.files;
}

// 在 handleFiles 里调用
function handleFiles(fileList) {
  let filesArr = [];
  for (let file of fileList) {
    addFileToList(file);
    filesArr.push(file);
  }
  setInputFiles(filesArr);
}

function addFileToList(file) {
  $('#custom-upload-list').find('.custom-upload-error-msg').remove();
  $('#custom-upload-list').find('.error').remove();
  const id = 'file_' + Math.random().toString(36).substr(2,9);
  let error = '';
  if (!allowedTypes.includes(file.type)) {
    error = `${file.name} has invalid extension. Only jpg, jpeg, png, gif, webp, mp4, mov, avi, mpeg allowed.`;
  } else if (file.size > maxSize) {
    error = `${file.name} is too large. Max 20MB allowed.`;
  }

  if (error) {
    return $('#custom-upload-list').append(`<div class="custom-upload-error-msg">${error}</div>`);
  }

  const isImage = file.type.startsWith('image/');
const fileContent = isImage
  ? `<img src="${URL.createObjectURL(file)}" alt="${file.name}" class="custom-upload-thumb"/>`
  : `<video src="${URL.createObjectURL(file)}" alt="${file.name}" class="custom-upload-thumb"></video>`;

  const $item = $(`
    <div class="custom-upload-file${error ? ' error' : ''}" id="${id}">
      <div class="text-size12 er-mb-2 er-break-all">${file.name}</div>
      <div class="er-flex er-items-center">
        ${fileContent}
        <span class="er-flex-1"></span>
        <span class="custom-upload-filesize">${formatSize(file.size)}</span>
        <div class="custom-upload-progress"><div class="custom-upload-progress-bar"></div></div>
        <svg class="custom-upload-remove" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#d16b8a"><path d="M267.33-120q-27.5 0-47.08-19.58-19.58-19.59-19.58-47.09V-740H160v-66.67h192V-840h256v33.33h192V-740h-40.67v553.33q0 27-19.83 46.84Q719.67-120 692.67-120H267.33Zm425.34-620H267.33v553.33h425.34V-740Zm-328 469.33h66.66v-386h-66.66v386Zm164 0h66.66v-386h-66.66v386ZM267.33-740v553.33V-740Z"/></svg>
      </div>
    </div>
  `);
  $('#custom-upload-list').append($item);

  uploadFile(file, id);
  $item.find('.custom-upload-remove').on('click', function() {
    $item.remove();
    files.delete(id);

    if (files.size === 0) {
      const dataTransfer = new DataTransfer();
      document.getElementById('custom-upload-input').files = dataTransfer.files;
    }
  });
}

function uploadFile(file, id) {
  const $bar = $(`#${id} .custom-upload-progress-bar`);
  const $item = $(`#${id}`);
  // 这里用演示用的模拟上传,实际请替换为你的上传接口
  const fakeUrl = 'https://httpbin.org/post';
  const formData = new FormData();
  formData.append('file', file);
  formData.append('file_from', 'photo-contest');

  $.ajax({
    url: fakeUrl,
    type: 'POST',
    data: formData,
    processData: false,
    contentType: false,
    xhr: function() {
      let xhr = new window.XMLHttpRequest();
      xhr.upload.addEventListener('progress', function(e) {
        if (e.lengthComputable) {
          let percent = (e.loaded / e.total) * 100;
          $bar.css('width', percent + '%');
        }
      }, false);
      return xhr;
    },
    success: function(res) {
      files.set(id, res.url);
      $bar.css('width', '100%');
      $item.removeClass('error');
    },
    error: function() {
      $item.addClass('error');
      $item.html(`<div class="custom-upload-error-msg">${file.name} Upload failed. Please try again.</div>`);
    }
  });
}

好了分析完毕🙂

我们再来回顾一下,从这段代码里,我们能学到关于new DataTransfer()的哪些技巧:

  1. 核心 :利用new DataTransfer()来创建一个可控的FileList对象,并将其赋值给input.files,从而实现对这个"只读"列表的程序化"写入"。
  2. UI操作 :将文件状态与DOM状态分离。在JavaScript中维护一份自己的文件列表,UI交互都只操作这份自有数据,然后再将这份数据同步回input元素。
  3. 兼容:抽象出统一的文件处理函数,用它来兼容点击、拖拽等多种不同的文件来源,保持代码的整洁和可复用性。

new DataTransfer()确实不是一个我们每天都会用到的API,但它在处理文件上传这个特定场景时,是一个非常有效且规范的解决方案。

希望这次的分析,能让你对这个API有更深入的、更贴近实战的理解。

谢谢大家❀

相关推荐
GISer_Jing2 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪3 小时前
CSS复习
前端·css
咖啡の猫5 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲7 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5817 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路8 小时前
GeoTools 读取影像元数据
前端
ssshooter8 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友8 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry9 小时前
Jetpack Compose 中的状态
前端
dae bal10 小时前
关于RSA和AES加密
前端·vue.js