普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月9日首页

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

作者 ErpanOmer
2025年7月8日 19:16

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

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

image.png

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

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


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

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

// 代码片段 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">文件列表的程序化控制。


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

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

// 代码片段 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”这个单向数据流,我们可以构建出任何我们想要的、文件上传交互界面。


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

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

// 代码片段 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对象作为参数。这样,无论是点击上传还是拖拽上传,最终都可以调用这个统一的函数,避免了代码重复,让逻辑更清晰。


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

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有更深入的、更贴近实战的理解。

谢谢大家❀

❌
❌