文件上传

大约 6 分钟

文件上传

文件上传实质上就是将文件内容通过http请求传递到服务器,至于上传类型多种多样,比如:

  1. FormData:这种方式要设置请求头为multipart/form-data
  2. Base64:这种方式要设置请求头为application/json
  3. 二进制数据:这种方式要设置请求头为application/octet-stream

至于其他类型,可以根据接口的具体要求来。

于前端而言,文件上传这个功能分为两部分,第一是文件上传这个组件的样式和交互,第二就是实现上传文件到服务器。我们这里不纠结样式问题,只关注上传文件这一本质的内容。

下面实现功能是,都采用原生的Ajax,且都是基本的点击input标签实现,至于其他的接口请求方式如axios | fetch都大差不差。

FormData

/**
 * @description: 单个文件上传函数
 * @param {File} file 文件数据
 * @param {Function} onProgress 上传进度处理函数
 * @param {Function} onFinished 上传完成处理函数
 * @return {Function} 取消上传的方法
 */
function upload(file, onProgress, onFinish) {
  // 使用 FormData 构造函数,它就会自动设置请求头的一些配置
  const form = new FormData()
  // 往 form 中添加字段,可以添加多个。这里是单文件,所以就一个
  // 这里的 "file" 是上传文件的字段名,根据接口要求来
  form.append("file", file)

  const xhr = new XMLHttpRequest()
  // 监听请求完成
  xhr.onload = () => {
    // 这里获取服务器返回结果
    const res = JSON.parse(xhr.responseText)
    console.log(res)
    // 返回服务器发送过来的数据
    onFinish(res)
  }
  // 监听请求进度
  xhr.upload.onprogress = (e) => {
    // 通过这个参数 e,就能拿到目前上传的一些信息,
    // 如文件总大小(total)、目前已上传的大小(loaded)
    console.log(e)
    // 调用进度处理函数
    onProgress(e)
  }
  // 请求配置:类型,路径
  xhr.open("post", "http://test.com:8888/upload")
  xhr.send(form)

  // 返回取消函数
  return function () {
    // 取消请求
    xhr.abort()
  }
}

Base64

这种方式上传一般是一个json类型的数据,需要告诉服务器文件的后缀名、文件的内容,这里我们规定数据格式如下:

{
    "ext": "", // 如 .png  .jpg等
    "file": "" // 文件内容,base64 格式字符串
}

这个方式与FormData的区别就在于处理文件格式、请求头,其他的请求流程等都一致,如下:

/**
 * @description: 单个文件上传函数
 * @param {File} file 文件数据
 * @param {Function} onProgress 上传进度处理函数
 * @param {Function} onFinished 上传完成处理函数
 * @return {Function} 取消上传的方法
 */
function upload(file, onProgress, onFinish) {
  // 全局提供 xhr,以便返回取消函数
  let xhr = null

  // 获取文件内容的 base64 格式
  // FileReader 用来读取文件
  const reader = new FileReader()
  reader.onload = (e) => {
    // 获取文件后缀名
    const ext = "." + file.name.split(".").pop()
    // e.target.result 就是文件的 base64 格式的本地路径
    // 我们需要去除这个路径前面的部分,只拿 base64 格式的内容部分
    const base64 = e.target.result.split(",").pop()
    const fileData = JSON.stringify({
      ext: ext,
      file: base64
    })

    xhr = new XMLHttpRequest()
    // 监听请求完成
    xhr.onload = () => {
      // 这里获取服务器返回结果
      const res = JSON.parse(xhr.responseText)
      console.log(res)
      // 返回服务器发送过来的数据
      onFinish(res)
    }
    // 监听请求进度
    xhr.upload.onprogress = (e) => {
      // 通过这个参数 e,就能拿到目前上传的一些信息,
      // 如文件总大小(total)、目前已上传的大小(loaded)
      console.log(e)
      // 调用进度处理函数
      onProgress(e)
    }

    // 设置请求头
    xhr.setRequestHeader("content-type", "application/json")
    // 请求配置:类型,路径
    xhr.open("post", "http://test.com:8888/upload")
    xhr.send(fileData)
  }
  // 它是异步的方法,因此在上面使用 onload 获取完成后的文件数据并发送请求
  reader.readAsDataURL(file)

  // 返回取消函数
  return function () {
    // 取消请求
    xhr.abort()
  }
}

这里使用了FileReader,关于它可以查看:FileReader - Web API 接口参考 | MDN (mozilla.org)open in new window

直接发送二进制内容

这种方式与上面的方式的区别也就是请求头和文件处理格式不一致,如下

/**
 * @description: 单个文件上传函数
 * @param {File} file 文件数据
 * @param {Function} onProgress 上传进度处理函数
 * @param {Function} onFinished 上传完成处理函数
 * @return {Function} 取消上传的方法
 */
function upload(file, onProgress, onFinish) {
  // 获取文件后缀名
  const ext = "." + file.name.split(".").pop()

  const xhr = new XMLHttpRequest()
  // 监听请求完成
  xhr.onload = () => {
    // 这里获取服务器返回结果
    const res = JSON.parse(xhr.responseText)
    console.log(res)
    // 返回服务器发送过来的数据
    onFinish(res)
  }
  // 监听请求进度
  xhr.upload.onprogress = (e) => {
    // 通过这个参数 e,就能拿到目前上传的一些信息,
    // 如文件总大小(total)、目前已上传的大小(loaded)
    console.log(e)
    // 调用进度处理函数
    onProgress(e)
  }

  // 设置请求头
  xhr.setRequestHeader("content-type", "application/octet-stream")
  // 设置文件后缀名
  xhr.setRequestHeader("x-ext", ext)
  // 请求配置:类型,路径
  xhr.open("post", "http://test.com:8888/upload")
  xhr.send(file)

  // 返回取消函数
  return function () {
    // 取消请求
    xhr.abort()
  }
}

拓展

推拽上传

拖拽上传这种方式,我们觉得没思路的地方可能就是拖拽这个方式,但其实<input />原生就支持拖拽选择文件。因此要实现这种方式其实我们只需要将input的宽高设置为100%即可。

但是,这种方式其实也是有一点小问题的,因为可能有的浏览器不支持input拖拽选择文件,那么我们就可以有另一种方式:

假设我们的input的父元素为一个div,那么我们就要将div设置为一个可拖放的元素,这就需要用到HTML 拖放 APIopen in new window

const div = document.querySelector("div")
// 拖动进入这个元素时触发一次
div.addEventListener("dragenter", function (e) {
  // 阻止事件默认行为,因为默认情况下 div 不是一个可拖放目标
  e.preventDefault()
})

// 拖动在这个元素上悬停时持续触发
div.addEventListener("dragover", function (e) {
  // 阻止事件默认行为,因为默认情况下 div 不是一个可拖放目标
  e.preventDefault()
})

// 拖动在这个元素上时松开鼠标,即将内容放置在这个元素上时触发
div.addEventListener("drop", function (e) {
  // 阻止事件默认行为,因为默认情况下 div 不是一个可拖放目标
  e.preventDefault()
  // 获取拖放到这个元素上的内容,这里我们获取文件
  const files = e.dataTransfer.files
  console.log(files)
  // 限制只能放置一个文件
  if (files.length !== 1) {
    alert("只能放置一个文件")
    return false
  }
  // 限制只能放文件,因为页面上有别的元素默认是可以拖拽的,如图片
  const fileTypes = e.dataTransfer.types
  if (!fileTypes.includes("files")) {
    alert("只能放置文件")
    return false
  }
    
  // ... 其他的根据具体要求写,这里也要调用其他方法来触发上传文件这个操作
})
上次编辑于: