アイデア: 分割アップロードとは、大きなファイルをいくつかの等しい部分に分割し、フロントエンドがループしてアップロード インターフェイスを呼び出してアップロードすることです。セグメント化されたダウンロードについても同様で、フロントエンドはインターフェイスを呼び出してファイルの合計サイズを取得し、ファイルが分割される部分の数を計算し、ダウンロード インターフェイスを周期的に呼び出して各セグメントのファイル ストリームを取得し、すべてのセグメントを取得します。ファイルセグメントを分割し、結合ダウンロードを実行します。
1. インストールの依存関係
これは、ファイルの一意の識別子を取得するために使用されます。バックエンドは、この識別子に従ってファイルが渡されたかどうかを判断します。渡された場合は、ファイル パスを直接返し、アップロードが成功したことを示します
npm install spark-md5
メソッドのカプセル化
import SparkMD5 from 'spark-md5'
// 获取文件的唯一MD5标识码
export function getFileMd5(file) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
const spark = new SparkMD5.ArrayBuffer()
fileReader.readAsArrayBuffer(file)
fileReader.onload = e => {
spark.append(e.target.result)
let md5 = spark.end()
resolve(md5)
}
})
}
2. 部分アップロード
<el-upload :show-file-list="false" class="upload-demo" :auto-upload="false" :limit="1" :on-change="handleChange" :on-exceed="handleExceed" :multiple="false">
<el-button type="primary" :loading="modelObj.loading">{
{ modelObj.loading ? '上传中...' : buttonTitle_ }}</g-button>
</el-upload>
アップロードロジック
import {
getFileMd5 } from './method'
import {
computed, reactive} from 'vue'
const modelObj = reactive({
fileList: {
},
loading: false,
percentage: 0,
})
// 上传之后重新点击上传
const handleExceed = uploadFile => {
modelObj.fileList = {
}
handleChange({
raw: uploadFile[0] })
}
//后端接口
const {
upLoadBypiece, downLoadbyPiece } = api
// 文件上传 选择文件时触发(:on-change事件)
const handleChange = async (uploadFile, uploadFiles) => {
modelObj.percentage = 0
// 文件信息
let fileRaw = uploadFile.raw
modelObj.fileName = fileRaw.name
console.log(fileRaw, 'fileRaw')
modelObj.loading = true
// 获取 文件的 MD5唯一标识码
let fileMd5 = null
try {
fileMd5 = await getFileMd5(fileRaw)
} catch (e) {
console.error('[error]', e)
}
if (!fileMd5) return
// 每片的大小为 5M 可调整
const chunkSize = 5 * 1024 * 1024
// 文件分片储存
let chunkList = []
function chunkPush(page = 1) {
chunkList.push(fileRaw.slice((page - 1) * chunkSize, page * chunkSize))
if (page * chunkSize < fileRaw.size) {
chunkPush(page + 1)
}
}
chunkPush()
saveFileChunk(chunkList, fileMd5, fileRaw.name)
}
// 保存文件片段到后台
const saveFileChunk = async (chunkList, fileMd5, fileName) => {
for (let i = 0; i < chunkList.length; i++) {
let formData = new FormData()
formData.append('filePath', props.filePath) // minio存储的路径
formData.append('chunk', i) // 当前片段的索引
formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字节进行分片的,这里是5M)
formData.append('chunks', chunkList.length) // 共有多少分片
formData.append('chunkFile', chunkList[i]) // 当前分片的文件流
formData.append('md5', fileMd5) // 整个文件的MD5唯一标识码,不是分片
formData.append('fileName', fileName) // 文件的名称
formData.append('size', chunkList[i].size) // 当前切片的大小(最后一片不一定是5M)
try {
const data = await upLoadBypiece(formData)
//计算当前上传进度百分比,展示进度条
modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100)
//成功的时候接口会返回文件的相关信息,当有data.fileName,说明上传成功了
if (data.fileName) {
modelObj.percentage = 100
modelObj.loading = false
modelObj.fileList = data
emit('getFile', modelObj.fileList)
console.log(modelObj.fileList, 'modelObj.fileList')
message.success(`上传成功`)
return
}
} catch (e) {
modelObj.loading = false
}
}
}
レンダリングは次のとおりです
3. フラグメントのダウンロード
(1) シャードダウンロードとマージコアの疑似コード
let fileBlob=[]
for (let index = 0; index < 5; index++) {
const params={
}
const config={
}
const data = await downLoadbyPiece(params, config)
//存储每一片文件流
fileBlob.push(data.data)
}
//合并
const blob = new Blob(fileBlob, {
type:fileBlob[0].type,
})
//下载
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = fileName
link.click()
window.URL.revokeObjectURL(link.href)
(2) アイデア:
1. フロントエンドが初めてインターフェイスを呼び出すとき、リクエスト ヘッダーはデフォルトで Range:bytes=0-chunkSize (最初のセグメントのファイル サイズ) に設定されます。インターフェイスは、応答ヘッダーで Content-Range: バイト 0-5242880/534107865 を返します。534107865 は合計ファイル サイズです。合計ファイル サイズと各部分のチャンクサイズに従って、ダウンロードするセグメントの数を計算する必要があります。
バックエンド参照コード: https://www.jianshu.com/p/e8dee3dbc409
リクエストヘッダー
最初の範囲: bytes=0-5242880、2 番目のリクエスト範囲: bytes=5242880-10485760、順番に増加します。
応答ヘッダー
Content-Disposition はファイル名を取得するために使用され、Content-Range は合計ファイル サイズを取得します。
- ネットワークに Content-Disposition、Content-Range があるが、フロントエンドが値を取得できない場合は、「vue
axios が応答ヘッダーの Content-Disposition フィールドを取得できない」を参照してください。
2. 合計ファイル サイズと各部分の chunkSize に従って、それらを計算し、配列 UploadRange にマージします。形式は次のとおりです。
3. 最初の呼び出しは既に Range: bytes=0-5242880 を渡しているため、ループは配列の 2 番目の位置からダウンロード インターフェイスを呼び出します。
(3) コード
<g-button style="margin-top: 20px" type="primary" :loading="downloadObj.downloading" @click="download"> {
{ downloadObj.downloading ? '下载中...' : '下载文件' }}</g-button>
<span> 下载进度({
{ downloadObj.percentage }}%)</span>
//下载逻辑
const downloadObj = reactive({
fileName: '',
downloading: false,
range: 0,
fileBlob: [],
percentage: 0,
})
const download = async () => {
downloadObj.fileBlob = []//存接口返回的每一段的文件流
downloadObj.downloading = true
downloadObj.range = 0 //文件总大小
downloadObj.percentage = 0 //下载进度
const params = {
md5: '73333a4795dfdfgv266454bbbgfdge41f',
}
const chunkSize = 5 * 1024 * 1024
//第一次调接口获取到响应头的content-range,文件总大小,用于计算下载切割
const config = {
headers: {
Range: `bytes=0-${
chunkSize}`,
},
}
const data = await downLoadbyPiece(params, config)
//获取文件总大小
const arr = data.headers['content-range'].split('/')
downloadObj.range = Number(arr[1])
//存储每片文件流
downloadObj.fileBlob.push(data.data)
//获取文件名称
let fileName = ''
let cd = data.headers['content-disposition']
if (cd) {
let index = cd.lastIndexOf('=')
fileName = decodeURI(cd.substring(index + 1, cd.length))
}
await chunkUpload(params, fileName, chunkSize)
}
//拿到文件总大小downloadObj.range,计算分为多少都段下载
const chunkUpload = async (params, fileName, chunkSize) => {
//获取分段下载的数组
let chunkList = []
function chunkPush(page = 1) {
chunkList.push((page - 1) * chunkSize)
if (page * chunkSize < downloadObj.range) {
chunkPush(page + 1)
}
}
chunkPush()
chunkList.push(downloadObj.range)
console.log(chunkList, 'chunkList')
//分段组合传参格式处理 0-1024 1024-2048
let uploadRange = []
chunkList.forEach((item, i) => {
if (i == chunkList.length - 1) return
uploadRange.push(`${
chunkList[i]}-${
chunkList[i + 1]}`)
})
console.log(uploadRange, 'uploadRang')
for (let index = 0; index < uploadRange.length; index++) {
if (index > 0) {
const config = {
headers: {
Range: `bytes=${
uploadRange[index]}`,
},
}
const data = await downLoadbyPiece(params, config)
//计算下载进度
downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100)
emit('getDownloadpercent', downloadObj.percentage)
//存储每一片文件流
downloadObj.fileBlob.push(data.data)
}
}
//合并
const blob = new Blob(downloadObj.fileBlob, {
type: downloadObj.fileBlob[0].type,
})
downloadObj.downloading = false
//下载
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = fileName
link.click()
window.URL.revokeObjectURL(link.href)
}
4. 完全なコードをアップロードおよびダウンロードする
<template>
<div class="fsc-slice-upload">
<div class="upload">
<el-upload :show-file-list="false" class="upload-demo" :auto-upload="false" :limit="1" :on-change="handleChange" :on-exceed="handleExceed" :multiple="false">
<g-button type="primary" :loading="modelObj.loading">{
{
modelObj.loading ? '上传中...' : buttonTitle_ }}</g-button>
</el-upload>
</div>
<div v-show="modelObj.fileName && showPercentage" class="upload-percent">
<span class="file-name">{
{
modelObj.fileName }}</span>
<span>{
{
modelObj.percentage == 100 ? '上传完成' : '上传中' }}({
{
modelObj.percentage }}%)</span>
<!-- 使用进度条 -->
<!-- <el-progress :stroke-width="10" :percentage="modelObj.percentage" /> -->
</div>
<g-button style="margin-top: 20px" type="primary" :loading="downloadObj.downloading" @click="download"> {
{
downloadObj.downloading ? '下载中...' : '下载文件' }}</g-button>
<span> 下载进度({
{
downloadObj.percentage }}%)</span>
</div>
</template>
<script setup>
import {
computed, reactive, ref } from 'vue'
import {
getFileMd5 } from './method'
import {
useMessage } from 'ui'
const message = useMessage()
const props = defineProps({
buttonTitle: {
type: String,
default: '上传文件',
},
//minio存储的路径
filePath: {
type: String,
default: 'data/data',
},
//是否展示进度条
showPercentage: {
type: Boolean,
default: true,
},
})
const buttonTitle_ = computed(() => props.buttonTitle)
const modelObj = reactive({
fileList: {
},
loading: false,
percentage: 0,
})
const emit = defineEmits(['getFile', 'getDownloadpercent'])
const {
upLoadBypiece, downLoadbyPiece } = api
// 上传之后重新点击上传
const handleExceed = uploadFile => {
modelObj.fileList = {
}
handleChange({
raw: uploadFile[0] })
}
// 文件上传 选择文件时触发(:on-change事件)
const handleChange = async (uploadFile, uploadFiles) => {
modelObj.percentage = 0
// 文件信息
let fileRaw = uploadFile.raw
modelObj.fileName = fileRaw.name
modelObj.loading = true
// 获取 文件的 MD5唯一标识码
let fileMd5 = null
try {
fileMd5 = await getFileMd5(fileRaw)
} catch (e) {
console.error('[error]', e)
}
if (!fileMd5) return
// 每片的大小为 5M 可调整
const chunkSize = 5 * 1024 * 1024
// 文件分片储存
let chunkList = []
function chunkPush(page = 1) {
chunkList.push(fileRaw.slice((page - 1) * chunkSize, page * chunkSize))
if (page * chunkSize < fileRaw.size) {
chunkPush(page + 1)
}
}
chunkPush()
saveFileChunk(chunkList, fileMd5, fileRaw.name)
}
// 保存文件片段到后台
const saveFileChunk = async (chunkList, fileMd5, fileName) => {
for (let i = 0; i < chunkList.length; i++) {
let formData = new FormData()
formData.append('filePath', props.filePath) // minio存储的路径
formData.append('chunk', i) // 当前片段的索引
formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字节进行分片的,这里是5M)
formData.append('chunks', chunkList.length) // 共有多少分片
formData.append('chunkFile', chunkList[i]) // 当前分片的文件流
formData.append('md5', fileMd5) // 整个文件的MD5唯一标识码,不是分片
formData.append('fileName', fileName) // 文件的名称
formData.append('size', chunkList[i].size) // 当前切片的大小(最后一片不一定是5M)
try {
const data = await upLoadBypiece(formData)
//计算当前上传进度百分比,展示进度条
modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100)
//成功的时候接口会返回文件的相关信息,当有data.fileName,说明上传成功了
if (data.fileName) {
modelObj.percentage = 100
modelObj.loading = false
modelObj.fileList = data
emit('getFile', modelObj.fileList)
console.log(modelObj.fileList, 'modelObj.fileList')
message.success(`上传成功`)
return
}
} catch (e) {
modelObj.loading = false
}
}
}
//下载逻辑
const downloadObj = reactive({
fileName: '',
downloading: false,
range: 0,
fileBlob: [],
percentage: 0,
})
const download = async () => {
downloadObj.fileBlob = []
downloadObj.downloading = true
downloadObj.range = 0 //文件总大小
downloadObj.percentage = 0 //下载进度
const params = {
md5: '7343784583fsdufhusdfgsudfe8934',
}
const chunkSize = 5 * 1024 * 1024
//第一次调接口获取到响应头的content-range,文件总大小,用于计算下载切割
const config = {
headers: {
Range: `bytes=0-${
chunkSize}`,
},
}
const data = await downLoadbyPiece(params, config)
//获取文件总大小
const arr = data.headers['content-range'].split('/')
downloadObj.range = Number(arr[1])
//存储每片文件流
downloadObj.fileBlob.push(data.data)
//获取文件名称
let fileName = ''
let cd = data.headers['content-disposition']
if (cd) {
let index = cd.lastIndexOf('=')
fileName = decodeURI(cd.substring(index + 1, cd.length))
}
await chunkUpload(params, fileName, chunkSize)
}
//拿到文件总大小downloadObj.range,计算分为多少都段下载
const chunkUpload = async (params, fileName, chunkSize) => {
//获取分段下载的数组
let chunkList = []
function chunkPush(page = 1) {
chunkList.push((page - 1) * chunkSize)
if (page * chunkSize < downloadObj.range) {
chunkPush(page + 1)
}
}
chunkPush()
//加上文件大小在最后一位
chunkList.push(downloadObj.range)
console.log(chunkList, 'chunkList')
//分段组合传参格式处理 0-1024 1024-2048
let uploadRange = []
chunkList.forEach((item, i) => {
if (i == chunkList.length - 1) return
uploadRange.push(`${
chunkList[i]}-${
chunkList[i + 1]}`)
})
console.log(uploadRange, 'uploadRang')
for (let index = 0; index < uploadRange.length; index++) {
//第一次调接口已经传过了第一组,从第二位开始
if (index > 0) {
const config = {
headers: {
Range: `bytes=${
uploadRange[index]}`,
},
}
const data = await downLoadbyPiece(params, config)
//计算下载进度
downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100)
emit('getDownloadpercent', downloadObj.percentage)
//存储每一片文件流
downloadObj.fileBlob.push(data.data)
}
}
//合并
const blob = new Blob(downloadObj.fileBlob, {
type: downloadObj.fileBlob[0].type,
})
downloadObj.downloading = false
//下载
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = fileName
link.click()
window.URL.revokeObjectURL(link.href)
}
</script>
<style lang="scss">
.fsc-slice-upload {
.upload {
display: flex;
}
.upload-percent {
margin-top: 10px;
display: flex;
.file-name {
margin-right: 15px;
max-width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.el-progress {
flex: 1;
}
}
}
</style>
参考記事:
https://blog.csdn.net/m0_51431448/article/details/127953473
https://www.jianshu.com/p/64694675ca95