type=file 读取、压缩、上传

前端 file 上传全流程实战:读取、压缩、阿里云OSS上传

最近做文件/图片上传功能,踩了不少坑——从文件读取、类型大小校验,到图片压缩适配iOS/安卓,再到对接阿里云OSS上传,整理出了一套完整的实战方案。今天结合项目里的代码,把 type="file" 处理的核心逻辑拆解开,新手也能直接复用。

一、核心需求与整体流程

我们要实现的功能:

  1. 选择文件后,先做基础校验(类型、大小);
  2. 图片文件自动压缩(超过阈值才压缩,iOS特殊处理);
  3. 对接阿里云OSS 上传(先获取上传Policy,再构造FormData上传);
  4. 兼容不同浏览器(比如低版本Blob兼容、Base64转File)。

整体流程:

1
选择文件 → 基础校验(类型/大小/系统) → 图片压缩(Canvas) → 构造FormData → 阿里云OSS上传 → 回调处理

二、核心功能拆解(附完整可运行代码)

先修正原代码中的语法错误(比如少运算符、引号不匹配、变量名笔误),再按功能模块拆解:

1. 全局变量与基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全局存储文件信息(跨函数传递)
let imgFile = {};
// 配置项(可根据业务调整)
const CONFIG = {
// 允许的文件类型(图片)
allowImgTypes: ['jpeg', 'png', 'gif', 'jpg'],
// 图片最大限制(10MB)
imgMaxSize: 10 * 1024 * 1024,
// 图片压缩阈值(超过200KB才压缩)
imgCompressThreshold: 200 * 1024,
// 压缩质量(0-1,越小压缩越狠)
compressQuality: 0.2,
// 阿里云OSS上传接口(获取Policy)
ossPolicyUrl: '//www.*.com/apis/aliyun-oss-file-service/v1/oss/post-policy'
};

2. 第一步:文件读取与基础校验

选择文件后,先校验类型、大小,区分iOS/安卓做不同处理(iOS无需压缩,直接上传):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 文件选择事件处理(入口函数)
* @param {Event} event - input[type=file]的change事件
*/
function handleInputChange(event) {
// 获取选中的第一个文件
const file = event.target.files[0];
if (!file) return;

// 1. 校验文件类型(仅图片)
const fileType = file.type.split("/")[1];
if (CONFIG.allowImgTypes.indexOf(fileType) < 0) {
alert(`文件类型仅支持 ${CONFIG.allowImgTypes.join('/')}!`);
return;
}

// 2. 校验文件大小
if (file.size > CONFIG.imgMaxSize) {
alert(`文件大小不能超过 ${CONFIG.imgMaxSize / 1024 / 1024}MB!`);
return;
}

// 3. 系统兼容:iOS直接上传,安卓压缩后上传
const isIOS = !!window.navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
if (isIOS) {
// iOS无需压缩,直接构造FormData上传
transformFileToFormData(file);
} else {
// 安卓:先转DataURL,再判断是否压缩
transformFileToDataUrl(file);
}
}

3. 第二步:图片压缩(Canvas核心实现)

超过200KB的图片,用Canvas压缩(降低质量,不改变尺寸),兼容低版本浏览器Blob构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* File转DataURL(用于压缩前的读取)
* @param {File} file - 选中的文件
*/
function transformFileToDataUrl(file) {
// 存储文件基础信息(跨函数使用)
imgFile = {
type: file.type || 'image/jpeg', // 兼容安卓获取不到type的情况
size: file.size,
name: file.name,
lastModifiedDate: file.lastModifiedDate
};

const reader = new FileReader();
// File转DataURL是异步操作,逻辑写在回调里
reader.onload = function(e) {
const dataURL = e.target.result;
// 小于阈值不压缩,大于阈值压缩
if (dataURL.length < CONFIG.imgCompressThreshold) {
compress(dataURL, processData, false);
} else {
compress(dataURL, processData, true);
}
};
reader.readAsDataURL(file);
}

/**
* Canvas压缩图片
* @param {String} dataURL - 图片的DataURL
* @param {Function} callback - 压缩后的回调
* @param {Boolean} shouldCompress - 是否压缩
*/
function compress(dataURL, callback, shouldCompress = true) {
const img = new Image();
// 解决跨域问题(本地文件无需,线上图片需配置)
img.crossOrigin = 'anonymous';
img.src = dataURL;

img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 保持原图尺寸
canvas.width = img.width;
canvas.height = img.height;
// 绘制图片到Canvas
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

// 压缩:quality越小,压缩率越高
const compressedDataUrl = canvas.toDataURL(
imgFile.type,
shouldCompress ? CONFIG.compressQuality : 1
);
// 压缩完成,处理成Blob/File
callback(compressedDataUrl);
};
}

/**
* 压缩后的数据处理:DataURL转Blob→File→FormData
* @param {String} compressedDataUrl - 压缩后的DataURL
*/
function processData(compressedDataUrl) {
// 1. DataURL转二进制字符串
const binaryString = window.atob(compressedDataUrl.split(',')[1]);
// 2. 二进制字符串转ArrayBuffer
const arrayBuffer = new ArrayBuffer(binaryString.length);
const intArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < binaryString.length; i++) {
intArray[i] = binaryString.charCodeAt(i);
}

// 3. 构造Blob(兼容低版本浏览器)
let blob;
try {
blob = new Blob([intArray], { type: imgFile.type });
} catch (error) {
// 兼容IE/旧版Chrome
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
if (error.name === 'TypeError' && window.BlobBuilder) {
const builder = new BlobBuilder();
builder.append(arrayBuffer);
blob = builder.getBlob(imgFile.type);
} else {
alert("浏览器版本过低,不支持上传图片!");
throw new Error('Blob构造失败');
}
}

// 4. Blob转File(保持文件名)
const fileOfBlob = new File([blob], imgFile.name, { type: imgFile.type });
// 5. 构造FormData上传
transformFileToFormData(fileOfBlob);
}

/**
* 构造FormData(压缩/未压缩文件通用)
* @param {File} file - 原文件/压缩后的File
*/
function transformFileToFormData(file) {
const formData = new FormData();
// 追加文件基础信息(可选,根据后端需求)
formData.append('type', file.type);
formData.append('size', file.size);
formData.append('name', file.name);
formData.append('lastModifiedDate', file.lastModifiedDate);
formData.append('file', file);

// 上传到阿里云OSS
uploadToOSS(formData, file.name);
}

4. 第三步:阿里云OSS上传(核心)

先获取OSS上传Policy(包含AccessKey、签名等),再构造FormData上传,上传成功后更新预览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 上传文件到阿里云OSS
* @param {FormData} formData - 包含文件的FormData
* @param {String} fileName - 文件名
*/
function uploadToOSS(formData, fileName) {
// 1. 获取阿里云OSS上传Policy
axios.get(`${CONFIG.ossPolicyUrl}?object=${encodeURIComponent('/' + fileName)}`)
.then(function (res) {
const ossData = res.data;
// 2. 追加OSS所需参数到FormData
formData.append('OSSAccessKeyId', ossData.OSSAccessKeyId);
formData.append('policy', ossData.policy);
formData.append('Signature', ossData.Signature);
formData.append('key', ossData.key);
formData.append('success_action_status', ossData.success_action_status);

// 3. 配置请求头(axios会自动处理multipart/form-data,无需手动设置)
const config = {
headers: { 'Content-Type': 'multipart/form-data' },
// 可选:监听上传进度
onUploadProgress: function (e) {
const progress = (e.loaded / e.total) * 100;
console.log(`上传进度:${progress.toFixed(2)}%`);
}
};

// 4. 上传到OSS服务器
axios.post(ossData.server, formData, config)
.then(function (uploadRes) {
// 上传成功:更新预览图(示例)
// 假设传入的DOM ID用于显示预览
// document.getElementById(imgPreviewId).src = ossData.url;
alert('上传成功!');
console.log('OSS返回:', uploadRes);
})
.catch(function (err) {
alert('上传失败!');
console.error('OSS上传错误:', err);
});
})
.catch(function (err) {
alert('获取OSS Policy失败!');
console.error('Policy请求错误:', err);
});
}

5. HTML调用示例

1
2
3
4
<!-- 文件选择框 -->
<input type="file" accept="image/jpeg,image/png,image/gif,image/jpg" onchange="handleInputChange(event)">
<!-- 可选:图片预览 -->
<img id="imgPreview" src="" alt="预览图" style="width: 200px; margin-top: 20px;">

三、关键避坑点(实战总结)

  1. iOS兼容:iOS系统下图片压缩容易出问题,直接跳过压缩步骤更稳定;
  2. Blob构造兼容:低版本浏览器(如IE、旧版Chrome)不支持new Blob(),需用BlobBuilder兜底;
  3. DataURL转File:压缩后的DataURL需先转Blob,再转File(保持文件名和类型);
  4. axios上传进度:无需自己写XHR,axios的onUploadProgress可直接监听进度;
  5. OSS上传注意
    • 必须先获取Policy(包含签名),否则上传会被拒绝;
    • Content-Type无需手动拼接,axios会自动加boundary
    • 文件名需encodeURIComponent,避免中文/特殊字符报错。

四、拓展:非图片文件上传(简化版)

如果是文档(Word/Excel/PDF)等非图片文件,无需压缩,直接读取+校验+上传即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 非图片文件上传(如PDF/Word)
* @param {Event} event - input change事件
* @param {String} previewId - 预览DOM ID
*/
function uploadDocFile(event, previewId) {
const file = event.target.files[0];
if (!file) return;

// 允许的文档类型
const allowDocTypes = [
'application/msword', 'application/vnd.ms-powerpoint',
'application/vnd.ms-excel', 'application/pdf'
];
// 校验类型
if (allowDocTypes.indexOf(file.type) < 0) {
alert('仅支持Word/Excel/PDF/PPT格式!');
return;
}

// 直接上传到OSS
const formData = new FormData();
formData.append('file', file);
uploadToOSS(formData, file.name);
}

五、完整代码总结

核心逻辑梳理:

  1. 文件读取FileReader 转DataURL(图片压缩用);
  2. 压缩核心:Canvas绘制+toDataURL 调整质量;
  3. OSS上传:先获取Policy→构造FormData→POST上传;
  4. 兼容处理:iOS跳过压缩、低版本Blob构造、文件名编码。

这套方案覆盖了前端file上传的核心场景,图片压缩适配移动端,OSS上传对接企业级存储,直接复制即可复用(只需调整OSS Policy接口和配置项)。


type=file 读取、压缩、上传
https://cszy.top/2017-12-12 图片-读取、压缩、上传/
作者
csorz
发布于
2017年12月12日
许可协议