图片上传
/ / 点击 / 阅读耗时 14 分钟过去一个月开发了公司了图片上传和图片选择两个模块,基本明白了图片上传经历的各个阶段和处理方式,现总结如下。
图片上传
1. 基本结构
参考
标签
1
<input type="file" accept="image/*">
2. 图片文件
参考
属性(只读)
File.lasterModified
File.lastModifiedDate
File.name
File.webkitRelativePath
File.size
File.type
3. 图片处理
获取图片内容
FileReader
参考
用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const getBase64 = (img, cb) => {
const reader = new FileReader();
reader.addEventListener('load', () => {
const size = {};
const image = new Image();
image.src = reader.result;
image.addEventListener('load', () => {
size.width = image.width;
size.height = image.height;
cb(reader.result, size, img.name);
});
});
reader.addEventListener('error', e => {
console.log(e);
});
reader.readAsDataURL(img);
};
URL.createObjectURL
参考
用法
1
img.src = window.URL.createObjectURL(file);
图片压缩
方法
- 使用canvas重绘
- 图片输出时降低图片质量
参考
用法
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
40const MEASURE_SIZE = 200;
export function previewImage(file: File | Blob): Promise<string> {
return new Promise(resolve => {
if (!isImageFileType(file.type)) {
resolve('');
return;
}
const canvas = document.createElement('canvas');
canvas.width = MEASURE_SIZE;
canvas.height = MEASURE_SIZE;
canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE}px; height: ${MEASURE_SIZE}px; z-index: 9999; display: none;`;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
const { width, height } = img;
let drawWidth = MEASURE_SIZE;
let drawHeight = MEASURE_SIZE;
let offsetX = 0;
let offsetY = 0;
if (width < height) {
drawHeight = height * (MEASURE_SIZE / width);
offsetY = -(drawHeight - drawWidth) / 2;
} else {
drawWidth = width * (MEASURE_SIZE / height);
offsetX = -(drawWidth - drawHeight) / 2;
}
ctx!.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
const dataURL = canvas.toDataURL();
document.body.removeChild(canvas);
resolve(dataURL);
};
img.src = window.URL.createObjectURL(file);
});
}
图片输出
- base64:字符串
- Blob:文件对象
图片预览
方法
- 将base64编码赋值给img.src
- 直接画到canvas画布上
用法
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
29componentDidUpdate() {
const { listType, items, previewFile } = this.props;
if (listType !== 'picture' && listType !== 'picture-card') {
return;
}
(items || []).forEach(file => {
const isValidateFile =
file.originFileObj instanceof File || file.originFileObj instanceof Blob;
if (
typeof document === 'undefined' ||
typeof window === 'undefined' ||
!(window as any).FileReader ||
!(window as any).File ||
!isValidateFile ||
file.thumbUrl !== undefined
) {
return;
}
file.thumbUrl = '';
if (previewFile) {
previewFile(file.originFileObj as File).then((previewDataUrl: string) => {
// Need append '' to avoid dead loop
file.thumbUrl = previewDataUrl || '';
this.forceUpdate();
});
}
});
}
4. 图片上传
上传策略
并发无控制传输
http1.1和http2协议下的并发数量限制
实现
1
2
3
4
5
6
7
8
9
10
11uploadFiles = (files) => {
const postFiles = Array.prototype.slice.call(files);
postFiles
.map(file => {
file.uid = getUid();
return file;
})
.forEach(file => {
this.upload(file, postFiles);
});
};
并发控制
实现
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
80class Transmition {
constructor(num, data, func, handleSuccess, handleErr, handleEnd) {
this.num = num; // 这个属性用来控制transmitter的数量
this.data = data; // 这个属性是数组类型,保存任务队列
this.func = func; // 这个函数用来实际处理异步任务
this.index = -1; // 这个属性用来存储当前的任务指针
this.handleSuccess = handleSuccess || function() {}; // 所有任务处理完成的回调函数
this.len = data.length; // 任务的数量
this.handleErr = handleErr || function() {}; // 任务失败的处理函数
this.handleEnd = handleEnd || function() {}; // 所有任务执行完一次后的处理函数
this.currentTask = []; // 这个属性用来保存当前正在执行的三个任务,随index的变化动态替换
this.currentIndex = -1; // 这个属性用来用来当停止任务时,记录执行到的文件指针,用于后面恢复,此处没有具体用到,仅留出这个字段
}
// 这个函数拆出来是十分有必要的
// 写成这种形式,就把图片的上传过程单独封装了
// 上传成功也好,失败也好都封装在自己的过程里
// 对于整个对列的上传过程而言都是经历了上传并且已经结束
handleTask = async (data, index) => {
const { func } = this;
try {
const response = await func(data, index);
this.handleSuccess(response);
} catch (e) {
this.handleErr(e, data[index], index);
}
};
// 这里保存当前队列执行的上传任务
// 并启动任务
// 在上传队列没有文件的时候
// 还会判断所有上传过程是否完成,并执行最终的回调函数
executeTask = async (index, data, i) => {
this.currentTask[i] = this.handleTask(data, index);
await this.currentTask[i];
if (index + 1 === this.len) {
await Promise.all(this.currentTask);
this.handleEnd();
}
};
// 定义的每个上传队列执行的规则
// 有任务时,执行任务并等待任务结束
// 结束后如果文件队列中还有文件就取文件开始上传
transmit = async i => {
const { data } = this;
let currentData = data[++this.index];
while (currentData) {
await this.executeTask(this.index, currentData, i);
currentData = data[++this.index];
}
};
// 这里同步创建上传队列
// 并启动上传过程
createTransmitor = () => {
const { num } = this;
for (let i = 0; i < num; i++) {
this.transmit(i);
}
};
// 这里用来等待已经触发的异步过程
// 当已经触发的异步过程结束
// 停止队列操作
stopTransmite = async () => {
this.currentIndex = this.index;
this.index = this.len;
await Promise.all(this.currentTask);
};
// 这个方法实现了暂停后继续操作
// 目前还没有使用
continueTransmite = () => {
this.index = this.currentIndex;
this.createTransmitor();
};
}
export default Transmition;
两种方式的比较
- 无控制并发不一定快
- 并发控制可减小后台压力
无并发
无并发指的是所有图片使用一个请求进行上传。
- 一个链接无法最大限度利用网络性能,传输时长会比较长
- 错误无法隔离,一个错误会导致整个重传
传输的数据格式
传输介质使用FormData,以base64或者Blob的格式都可以,后者方便后端处理。
Antd的图片上传实现
1. 基本结构
- 底层处理逻辑
- 基本结构
- 图片文件
- 图片上传
- 上层处理样式、交互及使用场景
- 图片处理
- 压缩
- 列表展示
- 属性添加
- 定义传输过程中交互
2. 暴露的接口
Antd的Upload组件一共暴露了21个接口:
暴露位置 | 接口 |
---|---|
上层 | defaultFileList、fileList、listType、previewFile、onPreview、showUploadList、onChange、onRemove、supportServerRender |
底层 | directory、disabled、multiple、withCredentials、beforeUpload、customRequest、accept、action、data、headers、name、openFileDialogOnClick |
3. 传输策略
默认并发无控制
给出了改变策略的接口
4. Antd中一个详细ajax传输的实现
1 | function getError(option, xhr) { |
参考:
全文完。