Read rc-upload
Opened this issue · 0 comments
目录
引子
最近在使用上传组件的时候,感觉跟一般的组件用起来不太一样。于是,结合个人想法去收集相关的一些资料,形成一个大概实现思路,然后再去看 rc-upload 源码,进行对比看看有那些不同。
rc-upload 版本 2.8.1 。
功能分析
在常见的上传插件中,主要的功能点有:
- 上传之前处理
- 上传过程进度处理
- 上传成功处理
- 上传失败处理
- 单/多文件上传
- 请求头设置
- 请求
body
参数设置 - 拖拽上传
根据以上功能点,初步判断涉及到的知识点有:
input
标签type="file"
类型相关特性。- 异步请求使用
XMLHttpRequest
或Fetch
,考虑到兼容性,优先选择前者。 File
对象- HTML 拖放
Drag and Drop
。
下面分别进行相关介绍。
相关知识点
input
用户选择本地的文件,就需要使用到 type="file"
的 input
元素。这种类型的元素除了拥有共享的属性外,还支持下面的一些属性:
- accept : 定义
input
接受的文件类型,如果是多种类型,则用逗号,
隔开。 - capture : 指定捕获图像或视频的设备。目前是一个布尔类型属性,当设置时,会请求设备的媒体捕捉设施,例如相机。
- files : 选择的文件列表对象。
- multiple : 表示是否允许用户一次选择多个文件。
- webkitdirectory : 表示是否允许用户选择目录,这是一个非标准属性。
更多信息见 MDN input file 。
XMLHttpRequest
涉及到对象 XMLHttpRequest
的方法有:
- open : 初始化请求。
- send : 发送请求。
- setRequestHeader : 设置 HTTP 请求头的值,必须在
open
之后、send
之前调用。 - abort : 中止请求。
涉及到对象 XMLHttpRequest 的事件有:
- abort : 当请求中断时触发,例如执行了
XMLHttpRequest.abort()
。 - error : 当请求出现错误时触发。也可以使用
onerror
属性。 - load :
XMLHttpRequest
请求完成时触发。也可以使用onload
属性。 - loadend : 当一个请求完成,无论是否成功都会触发,在
abort
、error
之后触发。也可以使用onloadend
属性。 - progress : 从接收到数据开始,周期触发。也可以使用
onprogress
属性。 - timeout : 在预设时间内没有接收到响应时触发。也可以使用
ontimeout
属性。
更多信息见 MDN XMLHttpRequest 。
File
用户选择文件后返回的文件对象就是 File
对象,该对象拥有下面的属性:
- lastModified : 当前 File 对象所引用文件最后修改时间,自 UNIX 时间起始值(1970年1月1日 00:00:00 UTC)以来的毫秒数。
- name : 文件名称。
- size : 文件所拥有字节大小。
- type : 文件的 MIME 类型 。
- webkitRelativePath : 相关的相对路径或 URL,非标准属性。
更多信息见 MDN File 。
Drag and Drop
拖动文件上传,可能涉及到的事件有:
- dragover : 当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次)。
- ondragleave : 当拖动元素或选中的文本离开一个可释放目标时触发。
- drop : 当元素或选中的文本在可释放目标上被释放时触发。
DragEvent
接口有个 dataTransfer
属性,是一个 DataTransfer
对象。这个对象包含了拖拽事件的状态,例如拖动事件类型、拖动的数据。使用拖动方式上传文件的数据,就是从这个对象中获取。
更多信息见 MDN Drag and Drop 。
发现以上的知识点,跟大部分的功能点相对应:
- 上传过程进度处理对应
XMLHttpRequest
的progress
事件处理。 - 上传成功对应
XMLHttpRequest
的load/loadend
事件处理。 - 上传失败对应
XMLHttpRequest
的error/timeout/abort
事件处理。 - 单/多文件上传对应
input
的multiple
属性和File
对象处理。 - 请求头设置对应
XMLHttpRequest
的setRequestHeader
方法。 - 请求
body
参数设置对应XMLHttpRequest
的send
方法。 - 拖拽上传对应
Drag/Drop
事件。
至于上传之前处理的功能在触发请求之前应该都可以。
接下来看 rc-upload 中的实现。
实现
这种基础组件类型,里面有非常多的类型检查。代码不是很多,主要逻辑在 Upload.jsx
、AjaxUploader.jsx
、IframeUpload.jsx
三个文件中,其中 IframeUpload
组件是在浏览器不支持 File
对象时使用,暂不考虑。
Upload.jsx
import AjaxUpload from './AjaxUploader';
import IframeUpload from './IframeUploader';
class Upload extends Component {
static propTypes = {
// 类型检查
}
static defaultProps = {
// 默认值
}
state = {
Component: null,
}
componentDidMount() {
if (this.props.supportServerRender) {
/* eslint react/no-did-mount-set-state:0 */
this.setState({
Component: this.getComponent(),
}, this.props.onReady);
}
}
getComponent() {
return typeof File !== 'undefined' ? AjaxUpload : IframeUpload;
}
abort(file) {
this.uploader.abort(file);
}
saveUploader = (node) => {
this.uploader = node;
}
render() {
if (this.props.supportServerRender) {
const ComponentUploader = this.state.Component;
if (ComponentUploader) {
return <ComponentUploader {...this.props} ref={this.saveUploader} />;
}
return null;
}
const ComponentUploader = this.getComponent();
return <ComponentUploader {...this.props} ref={this.saveUploader} />;
}
}
逻辑很清晰,支持服务器端渲染,主要的不同是服务器端渲染会考虑到 onReady
方法执行。
AjaxUploader.jsx
简化代码
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import defaultRequest from './request'; // 请求
import getUid from './uid'; // 生成一个唯一的字符串标识
import attrAccept from './attr-accept'; // 文件过滤
import traverseFileTree from './traverseFileTree'; // 文件遍历
class AjaxUploader extends Component {
static propTypes = {
// 类型检查
}
state = { uid: getUid() }
reqs = {}
// 上传文件变化
onChange = e => {
const files = e.target.files;
this.uploadFiles(files);
this.reset();
}
// 主动触发上传的 click 事件
onClick = () => {
const el = this.fileInput;
if (!el) {
return;
}
el.click();
}
onKeyDown = e => {
if (e.key === 'Enter') {
this.onClick();
}
}
// 文件拖拽
onFileDrop = e => {
const { multiple } = this.props;
e.preventDefault();
if (e.type === 'dragover') {
return;
}
if (this.props.directory) {
traverseFileTree(
e.dataTransfer.items,
this.uploadFiles,
_file => attrAccept(_file, this.props.accept)
);
} else {
let files = Array.prototype.slice
.call(e.dataTransfer.files)
.filter(file => attrAccept(file, this.props.accept));
if (multiple === false) {
files = files.slice(0, 1);
}
this.uploadFiles(files);
}
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
this.abort();
}
// 统一的处理文件,先每个文件添加标识,触发上传请求
uploadFiles = (files) => {
const postFiles = Array.prototype.slice.call(files);
postFiles
.map(file => {
file.uid = getUid();
return file;
})
.forEach(file => {
this.upload(file, postFiles);
});
};
// 文件上传 beforeUpload 处理
upload(file, fileList) {
const { props } = this;
if (!props.beforeUpload) {
// always async in case use react state to keep fileList
return setTimeout(() => this.post(file), 0);
}
const before = props.beforeUpload(file, fileList);
if (before && before.then) {
before.then((processedFile) => {
const processedFileType = Object.prototype.toString.call(processedFile);
if (processedFileType === '[object File]' || processedFileType === '[object Blob]') {
return this.post(processedFile);
}
return this.post(file);
}).catch(e => {
console && console.log(e); // eslint-disable-line
});
} else if (before !== false) {
setTimeout(() => this.post(file), 0);
}
}
// 组装请求参数,自定义请求也在这里
post(file) {
if (!this._isMounted) {
return;
}
const { props } = this;
let { data } = props;
const {
onStart,
onProgress,
transformFile = (originFile) => originFile,
} = props;
new Promise(resolve => {
const { action } = props;
if (typeof action === 'function') {
return resolve(action(file));
}
resolve(action);
}).then(action => {
const { uid } = file;
const request = props.customRequest || defaultRequest;
const transform = Promise.resolve(transformFile(file));
transform.then((transformedFile) => {
if (typeof data === 'function') {
data = data(file);
}
const requestOption = {
action,
filename: props.name,
data,
file: transformedFile,
headers: props.headers,
withCredentials: props.withCredentials,
onProgress: onProgress ? e => {
onProgress(e, file);
} : null,
onSuccess: (ret, xhr) => {
delete this.reqs[uid];
props.onSuccess(ret, file, xhr);
},
onError: (err, ret) => {
delete this.reqs[uid];
props.onError(err, ret, file);
},
};
this.reqs[uid] = request(requestOption);
onStart(file);
});
});
}
reset() {
this.setState({
uid: getUid(),
});
}
abort(file) {
const { reqs } = this;
if (file) {
let uid = file;
if (file && file.uid) {
uid = file.uid;
}
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
delete reqs[uid];
} else {
Object.keys(reqs).forEach((uid) => {
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
delete reqs[uid];
});
}
}
saveFileInput = (node) => {
this.fileInput = node;
}
render() {
const {
component: Tag, prefixCls, className, disabled, id,
style, multiple, accept, children, directory, openFileDialogOnClick,
} = this.props;
const cls = classNames({
[prefixCls]: true,
[`${prefixCls}-disabled`]: disabled,
[className]: className,
});
const events = disabled ? {} : {
onClick: openFileDialogOnClick ? this.onClick : () => { },
onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => { },
onDrop: this.onFileDrop,
onDragOver: this.onFileDrop,
tabIndex: '0',
};
return (
<Tag
{...events}
className={cls}
role="button"
style={style}
>
<input
id={id}
type="file"
ref={this.saveFileInput}
key={this.state.uid}
style={{ display: 'none' }}
accept={accept}
directory={directory ? 'directory' : null}
webkitdirectory={directory ? 'webkitdirectory' : null}
multiple={multiple}
onChange={this.onChange}
/>
{children}
</Tag>
);
}
}
export default AjaxUploader;
以上逻辑中有下面几个特点:
- 隐藏了
input
元素,用另外一个元素触发上传事件。 - 每一个文件都有唯一的标识
uid
,方便请求和中断查找处理。 - 记录了组件的渲染状态,在请求时会进行判断。
- 整个上传过程主要分成了 3 部分:
uploadFiles
、upload
、post
。uploadFiles
中是为文件添加标识,upload
中处理上传前的逻辑,post
中包含了请求成功、进度、失败逻辑。 - 每个请求返回的是
XMLHttpRequest
对象的abort
方法。 - 文件信息都放在
FormData
对象里面。
对比
跟自己最初的思路相比,发现了下面的不足:
- 没有考虑服务器端渲染和兼容性
- 没有考虑统一的触发方式
- 缺乏文件状态记录和销毁方式
- 各状态阶段没有清晰的界定,该在什么时候触发什么方法
- 没有考虑上传文件传输方式
- 缺少自定义请求的方式
小结
看似一个简单的功能,想要做的比较完善要考虑很多方面,需要不断的打磨。
参考资料
🗑️
以下是一些无关紧要的内容。
算是按照老早的计划写完了这篇总结,写完之后想着结尾该写点什么东西,突然想到《虫师》。
《虫师》中的“虫”跟我们一般认知的虫子不太一样,在里面的旁白是这样说的:
于人外之轮回,栖息着一群低等而奇异的,与我们常见的动植物截然不同之物,对这些异形之物,人们自古以来便满怀敬畏,不知不觉将它们统称为“虫”。
这种虫有自己的生存方式,而这种方式却可能有悖于人类的常识,甚至危害人类的生存,于是就出现了“虫师”这种职业,他们云游四方,对虫的生命形态,生存方式进行研究,并接受人们的委托,解决可能是由虫引起的怪异事件。
到处旅行,见多识广的银古
这种设定听起来还是蛮合理的,毕竟还是有很多人类并不知道的微生物。得益于这点,一些日常中奇怪的现象或者传说,都可以从虫的角度去进行另外一种形式演绎。
《虫师》中描绘了各种各样的奇妙的虫和现象,比如类似神笔马良的虫、会将梦境映射到现实的虫、喜欢“吃”声音的虫、会形成类似彩虹到处“旅行”的虫。这些没有台词的主角,在与其它生物产生关联时,述说的故事奇幻而独特,让人对大自然多了一份敬畏。