阅读 986

纯前端页面实现oss分片上传

1.原生的文件上传

参考: input type="file"

image.png

<input id="uploadValue" type="file"  multiple="multiple" onchange="fileChange()">
<script>
function fileChange() {
  const value = document.getElementById('uploadValue').value
  const files = document.getElementById('uploadValue').files
  console.log(value, 'value')  // 表示选择文件的路径
  console.log(files, 'files') // 表示已选择文件的数组集合
  const formData = new FormData() 
  formData.append("userfile", files[0]) //拿到第一个文件放到formData里面
  console.log(formData, 'formData has file')
  const res = formData.get('userfile') // 也能通过get去获取对应的文件,也能看到file里面有什么信息
  console.log(res, 'res')
}
</script>
复制代码

image.png 多文件上传,先选择,然后按住ctrl,可以选择多个,那么显示的就是已经选中n个文件

你好呀4.gif

2.form表单以及formData的作用

其实我们在做表单提交的时候,会用到FormData这个构造函数,那么为什么要用这个呢?

2.1 form表单

参考:
表单,FormData 对象
form标签

image.png

<form action="form_action.asp" method="get">
    <p>First name: <input type="text" name="fname" /></p>
    <p>Last name: <input type="text" name="lname" /></p>
    <input type="submit" value="Submit" />
</form>
复制代码

你好呀5.gif

在点击提交的时候(type="submit")的时候,浏览器会自动将form表单里面的数据以键值对的方式,提交给服务器,但是我们平时提交,肯定不希望是浏览器自动去完成这个提交到服务器的操作,我们只需要获取到form表单里面的数据,就行了,什么时候提交到服务器,由我们自己决定,所以浏览器原生提供了 FormData 对象来完成这项工作,FormData()构造函数的参数是一个 DOM 的表单元素,构造函数会自动处理表单的键值对

2.2 FormData 构造函数

参考定义: FormData FormData

image.png

现在来实现,只获取表单中的数据,而不需要进行自动提交到服务器

  <form id="form">
    <p>First name: <input type="text" name="fname" /></p>
    <p>Last name: <input type="text" name="lname" /></p>
    <input type="button" value="Submit" onclick="getFormData()" />
  </form>
  
  function getFormData() {
      const data = document.getElementById('form')
      const formData = new FormData(data);
      console.log(formData.get('fname'), 'First name')
      console.log(formData.get('lname'), 'Last name')
   }
复制代码

你好呀6.gif

FormData是一个构造函数,能够通过set去添加属性,能够通过get去获取属性

3.实现使用element ui两种上传方式

3.1 利用action自动上传
  <el-upload
      ref="upload"
      class="upload-demo"
      :action="getUrl()"
      :headers="uploadHeaders()"
      :on-success="uploadFile"
      :on-remove="handleRemove"
      :before-remove="beforeRemove"
      :file-list="fileList"
      :on-exceed="handleExceed"
      :limit="1"
      accept=".so, .zip"
    >
      <el-button size="small" type="primary">点击上传</el-button>
    </el-upload>
复制代码
getUrl() {
  return `${process.env.API_URL}接口名`
},
uploadHeaders() {
  return {
    'Authorization': `Bearer ${this.token}`,
  }
},
uploadFile(res, file, fileList) {
  console.log(res, 'res')
  //上传结果res,可以根据code判断
},
beforeRemove(file, fileList) {
  return this.$confirm(`确定移除 ${file.name}?`)
},
handleExceed() {
  this.$message.warning(`当前限制选择1个文件,已经存在了1个文件`)
},
复制代码

要注意的是:

image.png

前端用xhr.send(formData)上传,服务端接收body,然后后端去解析formData,所以使用post请求

服务端可以通过解析拿到对应上传的文件 我在项目中(node+sequelize)可以通过ctx.request.files拿到对应的数据 深入浅出 multipart/form-data

阿里云文件上传参考这篇: vue文件上传至阿里云 ali-oss

3.2 自定义上传
 <el-upload
  ref="upload"
  class="upload-demo"
  action=""
  :http-request="customerHttp"
  :on-remove="handleRemove"
  :before-remove="beforeRemove"
  :file-list="fileList"
  :on-exceed="handleExceed"
  :limit="1"
  accept=".so, .zip"
>
  <el-button size="small" type="primary">点击上传</el-button>
</el-upload>
复制代码
data() {
    return {
        packageFile: '',
    }
}
methods: {
    customerHttp(item) {
      const formData = new FormData()
      formData.append('file', item.file)
      // 如果直接上传就不用存储,如果等按确定按钮,就需要存储
      this.packageFile = formData
      const res = await uploadFile(this.packageFile).catch(() => false)
      console.log(res)
    },
    // 删除了之后 应该把this.packageFile进行清空
    handleRemove(file, fileList) {
      if (fileList.length === 0) {
        this.packageFile = ''
      }
    },
}
复制代码

4.大文件分片上传(element ui 纯前端实现)

知识点汇总
VUE前端分片直传大文件到OSS方法
阿里云OSS文件上传(分片上传、断点续传)前后端实现

一定要看api 分片上传官方案例(node.js)

你好呀8.gif

模拟两个文件上传,都是205兆,为什么要分片上传?

之前我们上传的文件,是大概几十兆,所以上传虽然慢了点,也不是不能忍,但是后面兆数增加到400多兆,因为之前文件上传到oss,是在node端处理的,但是node.js有2分钟超时限制,所以为了减少node层转换,直接采用在前端进行oss上传

1.template
<el-upload
  ref="upload"
  class="upload-demo"
  action=""
  :http-request="customerHttp"
  :on-remove="handleRemove"
  :before-remove="beforeRemove"
  :file-list="fileList"
  :on-exceed="handleExceed"
  :limit="1"
  accept=".so, .zip"
>
  <el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<div style="padding: 10px 10px 10px 0px; width: 320px">
  <el-progress v-if="percentage" :text-inside="true"
  :stroke-width="24" :percentage="percentage" status="success" />
</div>
复制代码
2. 数据定义
import moment from 'moment'
import OSS from 'ali-oss'
const client = new OSS({
  region: 'xxxxx',
  accessKeyId: 'xxxxx',
  accessKeySecret: 'xxxxx',
  endpoint: 'xxxxx',
  bucket: 'xxxxxx',
})
data() {
    return {
      percentage: 0,
      savaData: {},
    }
}
复制代码

如果在node端去拿配置项,那么可以直接在config.local.js文件里面去定义ossConfig配置项,如果是纯前端实现的话,肯定不能直接定义(很不安全),要从接口拿,我是模拟,所以直接在页面写了

const res = await this.sts.assumeRole(acs:ram::${accountId}:role/${roleName}, 'SessionTest')

1627215411(1).png

3.分片上传参数获取
async customerHttp(item) {
  // 拿到上传到的file
  const uploadFile = item.file
  // 拿到上传的size
  const uploadFileSize = uploadFile.size // 这里拿到的单位是字节(uploadFileSize/ 1024 / 1024
  // = 多少兆)
  // 设置每一片的大小,partSize 指定上传的每个分片的大小,范围为100 KB~5 GB。
  const partSize = Math.ceil(uploadFileSize / 1024 / 1024 / 1000) * 1024 * 1024
  // 设置所有的文件上传所有的唯一的saveFileId
  const { name, size, lastModified, type } = uploadFile
  const saveFileId = `${lastModified}_${size}_${name}_${type}`
  this.multipartUpload(partSize, saveFileId, uploadFile)
},
复制代码

partSize可以根据业务需求设定,最好就是设定一个最大的片数,看node的官方的例子是1000片,我这里设置最大的片数也是1000片,所以我的逻辑是如果超过1000片,我就将兆数放大,如果小于1000片,我就还是使用1兆,Math.ceil(uploadFileSize / 1024 / 1024 / 1000)是为了知道是不是大于1000片

为什么我要去创建一个saveFileId,可以叫做他文件的唯一的id,其实我看过很多例子,很多人的断点续传只做到了我传a文件,网页崩了,我再续传a文件,而我这里去创建saveFileId是为了,我传a文件,网页崩了,我又传b文件,网页崩了,过一会,我再传a文件或者b文件都要有续存的效果,所以我会将文件的信息形成唯一的id进行存储,然后放到内存里面,将所有的中断信息变成一个对象存储,对象的key是saveFileId(文件唯一的信息)

4.分片上传
  async multipartUpload(partSize, saveFileId, uploadFile) {
      try {
        // object-name目前我是用的uploadFile.name,其实也是要根据你们的项目而定,
        有没有具体的规定,要不要加项目名,要不要加对应的环境
        // 上传的参数
        const uploadParams = {
          partSize,
          progress: (percentage, checkpoint) => {
            this.savaData[saveFileId] = checkpoint
            console.log(checkpoint)
            this.savaData['lastSaveTime'] = new Date()
            this.percentage = parseInt(percentage * 100)
            // 在上传过程中,把已经上传的数据存储下来
            this.saveFinishedData(this.savaData)
          },
        }
        // 断点续传
        await this.resumeUpload(uploadParams, saveFileId)
        const { res: { status }} = await client.multipartUpload(uploadFile.name, 
        uploadFile, uploadParams)
        if (status === 200) {
          // 重新去掉某个缓存进行设置
          delete this.savaData[saveFileId]
          this.saveFinishedData(this.savaData)
        }
      } catch (e) {
        // 捕获超时异常。
        if (e.code === 'ConnectionTimeoutError') {
          console.log('TimeoutError')
          // do ConnectionTimeoutError operation
        }
      }
    },
复制代码

很多人说进度条不能实时更新,其实是因为别人的例子,直接用的function,无法改变父级的this.percentage,所以这里应该用箭头函数(percentage, checkpoint) =>

5.断点续传
  async resumeUpload(uploadParams, saveFileId) {
      if (localStorage.getItem('upload-function-name')) {
        const obj = JSON.parse(localStorage.getItem('upload-function-name'))
        if (Object.keys(obj).includes(saveFileId)) {
          uploadParams.checkpoint = obj[saveFileId]
        }
      }
    },
    // 存储到内存
   saveFinishedData(finishedData) {
      localStorage.setItem(
        'upload-function-name',
        JSON.stringify(finishedData)
      )
    },
复制代码

1627220519(1).png 主要是判断内存里面的文件和我现在上传文件是不是一致.是的话把对应的checkpoint传过去

upload-function-name这个名字,我觉得按照功能随便取个名字就好

6.初始化拿到内存里面的数据
 mounted() {
    this.initPage()
 },
 methods: {
  initPage() {
      // 判断是不是有缓存
      const localData = localStorage.getItem('upload-function-name')
      if (!localData) return
      this.savaData = JSON.parse(localData)
      // 当前时间 > 存储时间(1000 * 60 * 60表示1h,意思就是这些数据你要存多久,
      // 可以是1h也可以是多少天,随意)
      if (moment(new Date()).diff(moment(this.savaData.lastSaveTime)) > 1000 * 60 * 60) {
        localStorage.removeItem('upload-function-name')
      }
    },
 }
复制代码
7.知识点梳理
1. 分片上传,主要是把一个大文件,分成一小片上传,而按道理,后端应该把前端传的一片片合起来,这一步阿里云帮我们做了
2. oss上传中,同一个文件,用saveFileId做文件的唯一标识
3. 因为想要做到多个文件中断,都能够进行分别续存,所以将这些信息存到内存里面
4. 只要上传成功了,应该将对应的内存中的信息删除
5. 为了不让上传信息一直停留在内存,做了一个定期的删除
6. doneParts 是已经上传的片数的集合
复制代码
8.开发过程中一些思考(可不看)

在做分片续存这里,首先我在想checkpoint是什么?我靠什么信息知道哪些已经上传了,哪些没上传,是只用把中断的最后一片知道了就行了嘛,checkpoint是上传每一片的信息嘛?我以为checkpoint每次打印出来,应该uploadId是不一样的,每个checkpoint表示的是已上传的那一片,结果我错了,或者我觉得至少每个checkpoint有个东西完全不一样,结果每次上传完了,我去看checkpoint,好像每个都一样(我查了后面几十个都一样)

1627217629(1).png

04b02e2e68d0cb8890c6abcf64fd41b.png

于是我很困惑,这没有一个标志值怎么搞?看了看源码: checkpoint 返回的数据:

file 就是我们传的file
fileSize 也就是文件总大小
name文件名
partSize也就是我们传的portSize,没有就是1兆

那么uploadId是不是唯一的呢?是代表了每一片的上传了的id还是说只要是这个文件上传,所有的uploadId是一样的呢?答案就是只要是同一个文件上传,uploadId是一致的,不是代表每一片

源码:

1627218644(1).png

1627218810(1).png

第一次创建uploadId还是根据前端传的参数name和options,在上传调用时,这些参数都是不变的,所以不管request里面怎么写,uploadId应该是根据name和option出来的一个唯一值,所以我存储的断点以上提到的几个数都是固定的,那么唯一有可能变化的就是doneParts

1627219147(1).png

源码证明,变化的应该是doneParts,而为什么我所有的数据doneParts都是206片呢???其实是因为我每次去分析数据都是全部上传完看最后的那些数据,只有在上传的过程中,整个doneParts的数量才是按照0->206从小到大数组在变化,而结束后的打印是没有规律的,大部分变成了206片,这里的原因还没有搞懂。。。。

因为doneParts上传中规律增加,中断时,我们把对应的checkpoint存储,oss解析到传过来的checkpoint.uploadId与当前传的一致就会进行续存.

开心每一天-_-

文章分类
前端
文章标签