需求背景: h5内嵌app壳子的时候上传文件压缩的时候格式问题
文件上传 formData 格式 采用的是二进制, 部分手机webview 不支持 new File 构造函数, 所以这里可以采用blob格式 废话不多说, 直接见代码
下边js文件为封装的公共方法
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化后的大小
*/
const formatFileSize = bytes => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
}
/**
* 打印压缩结果对比
*/
const printCompressResult = (
fileName,
beforeSize,
beforeSizeFormatted,
originalWidth,
originalHeight,
afterSize,
afterSizeFormatted,
compressedWidth,
compressedHeight,
quality
) => {
console.log(`压缩前:${fileName}`)
console.log(` 尺寸: ${originalWidth} × ${originalHeight} px`)
console.log(` 大小: ${beforeSizeFormatted} (${beforeSize} 字节)`)
console.log('')
console.log(`压缩后:${fileName}`)
console.log(` 尺寸: ${parseInt(compressedWidth)} × ${parseInt(compressedHeight)} px`)
console.log(` 大小: ${afterSizeFormatted} (${afterSize} 字节)`)
console.log(` 质量: ${(quality * 100).toFixed(0)}%`)
}
/**
* 获取图片的 EXIF 方向信息(移动端图片旋转问题)
* @param {File} file - 图片文件
* @returns {Promise<number>} 方向值
*/
const getImageOrientation = file => {
return new Promise(resolve => {
const reader = new FileReader()
reader.onload = e => {
const view = new DataView(e.target.result)
if (view.getUint16(0, false) !== 0xffd8) {
resolve(-1)
return
}
const length = view.byteLength
let offset = 2
while (offset < length) {
if (view.getUint16(offset, false) === 0xffe1) {
const marker = view.getUint16(offset + 2, false)
if (marker !== 0xe1ff) {
offset += 2
continue
}
if (view.getUint32(offset + 4, false) !== 0x45786966) {
offset += 2
continue
}
const little = view.getUint16(offset + 10, false) === 0x4949
const tags = view.getUint16(offset + 18, little)
for (let i = 0; i < tags; i++) {
const tag = view.getUint16(offset + 20 + i * 12, little)
if (tag === 0x0112) {
resolve(view.getUint16(offset + 22 + i * 12, little))
return
}
}
}
offset += 2
}
resolve(-1)
}
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
})
}
/**
* 根据 EXIF 方向旋转 canvas
* @param {HTMLCanvasElement} canvas - canvas 元素
* @param {number} orientation - EXIF 方向值
*/
const applyImageOrientation = (canvas, orientation) => {
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
switch (orientation) {
case 2:
// 水平翻转
ctx.translate(width, 0)
ctx.scale(-1, 1)
break
case 3:
// 旋转 180 度
ctx.translate(width, height)
ctx.rotate(Math.PI)
break
case 4:
// 垂直翻转
ctx.translate(0, height)
ctx.scale(1, -1)
break
case 5:
// 顺时针 90 度 + 水平翻转
ctx.translate(height, 0)
ctx.rotate(Math.PI / 2)
ctx.scale(-1, 1)
break
case 6:
// 顺时针 90 度
ctx.translate(height, 0)
ctx.rotate(Math.PI / 2)
break
case 7:
// 逆时针 90 度 + 水平翻转
ctx.translate(0, width)
ctx.rotate(-Math.PI / 2)
ctx.scale(-1, 1)
break
case 8:
// 逆时针 90 度
ctx.translate(0, width)
ctx.rotate(-Math.PI / 2)
break
default:
break
}
}
/**
* 压缩图片
* @param {File} file - 原始图片文件
* @param {Object} options - 压缩选项
* @param {number} options.maxWidth - 最大宽度,默认 1920
* @param {number} options.maxHeight - 最大高度,默认 1920
* @param {number} options.quality - 压缩质量 0-1,默认 0.8
* @param {number} options.maxSize - 最大文件大小(字节),默认 2MB
* @returns {Promise<File>} 压缩后的文件
*/
const compress = async (file, options = {}) => {
const {
maxWidth = 1920,
maxHeight = 1920,
quality = 0.8,
maxSize = 1 * 1024 * 1024 // 2MB
} = options
// 记录压缩前信息
const beforeSize = file.size
const beforeSizeFormatted = formatFileSize(beforeSize)
return new Promise((resolve, reject) => {
// 如果文件已经是小文件,直接返回
if (file.size <= maxSize) {
resolve(file)
return
}
const reader = new FileReader()
reader.onload = async e => {
try {
const img = new Image()
img.onload = async () => {
try {
// 记录原始尺寸
const originalWidth = img.width
const originalHeight = img.height
// 获取 EXIF 方向信息(移动端)
const orientation = await getImageOrientation(file)
// 计算压缩后的尺寸
let { width, height } = img
const needResize = width > maxWidth || height > maxHeight
if (needResize) {
const ratio = Math.min(maxWidth / width, maxHeight / height)
width = width * ratio
height = height * ratio
}
// 创建 canvas
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 根据方向调整 canvas 尺寸
if (orientation > 4) {
canvas.width = height
canvas.height = width
} else {
canvas.width = width
canvas.height = height
}
// 应用方向旋转
applyImageOrientation(canvas, orientation)
// 绘制图片
ctx.drawImage(img, 0, 0, width, height)
// 转换为 blob
canvas.toBlob(
blob => {
if (!blob) {
reject(new Error('图片压缩失败'))
return
}
// 如果压缩后仍然大于目标大小,继续降低质量压缩
if (blob.size > maxSize && quality > 0.1) {
const newQuality = Math.max(0.1, quality - 0.1)
canvas.toBlob(
newBlob => {
if (!newBlob) {
reject(new Error('图片压缩失败'))
return
}
printCompressResult(
file.name,
beforeSize,
beforeSizeFormatted,
originalWidth,
originalHeight,
newBlob.size,
formatFileSize(newBlob.size),
width,
height,
newQuality
)
resolve(newBlob)
},
'image/jpeg',
newQuality
)
} else {
printCompressResult(
file.name,
beforeSize,
beforeSizeFormatted,
originalWidth,
originalHeight,
blob.size,
formatFileSize(blob.size),
width,
height,
quality
)
resolve(blob)
}
},
'image/jpeg',
quality
)
} catch (error) {
console.error('图片压缩处理失败:', error)
// 压缩失败时返回原文件
resolve(file)
}
}
img.onerror = () => {
reject(new Error('图片加载失败'))
}
img.src = e.target.result
} catch (error) {
console.error('图片读取失败:', error)
reject(error)
}
}
reader.onerror = () => {
reject(new Error('文件读取失败'))
}
reader.readAsDataURL(file)
})
}
export default compress
文件上传的地方
async afterRead(file) {
const formData = new FormData()
const filesToUpload = Array.isArray(file) ? file : [file]
// 先设置上传状态,确保 UI 立即更新
filesToUpload.forEach(fileItem => {
fileItem.status = 'uploading'
})
// 使用 $nextTick 确保状态更新到 UI
await this.$nextTick()
// 压缩所有图片文件
const compressedFiles = await Promise.all(
filesToUpload.map(async fileItem => {
// 只压缩图片文件
if (fileItem.file && fileItem.file.type.startsWith('image/')) {
try {
const compressedFile = await compress(fileItem.file)
return {
...fileItem,
file: compressedFile,
name: fileItem.file.name // 保留文件名
}
} catch (error) {
console.error('图片压缩失败,使用原文件:', error)
return fileItem
}
}
return fileItem
})
)
// 将压缩后的文件添加到 FormData
compressedFiles.forEach(fileItem => {
formData.append('file', fileItem.file, fileItem.name)
})
this.$response(
uploadApi(formData),
null,
data => {
if (Array.isArray(file)) {
file.forEach(fileItem => {
fileItem.status = 'done'
})
} else {
file.status = 'done'
}
},
() => {
// 上传失败处理
if (Array.isArray(file)) {
file.forEach(() => {
this.fileList?.pop()
})
} else {
this.fileList?.pop()
}
}
)
},