Upload 组件设计的目标是解决用户上传文件的便利性,可是中后台 Upload 组件的场景是多种多样的,因此可扩展能力是 Upload 组件不可忽视的另外一方面。html
一样为了你们可以更加容易的理解,我会从最原始的 input 标签开始提及html5
<form action="/api/file">
<input type="file" />
<button type="submit">submit</button>
</form> 复制代码
这段代码功能: 先选择一个文件,再点提交 POST 一个文件到一个接口。代码虽然很少,可是在实际使用中值得吐槽的点却很多,这里重点说两个点。git
先不说UI不美观,在每一个主流浏览器上面的文案基本都不同,另外在IE下面变化彷佛有点大。咱们可能的指望是在任何浏览器下交互和UI都一致的组件。github
原生的文件上传都是经过form post 上传,上传完成后整个页面会重定向到 action 的地址。如今你们已经习惯了 ajax 作数据提交,由于能够不须要reload页面就能够带来整个页面的数据更新,无刷新更新的体验会提高不少。ajax
我打算整片拆两个段来说这个问题,拆分点大约从2012年附近开始,由于 html5 差很少在这个时间段开始被现代浏览器逐步支持。两个段分别叫传统解决方案和现代解决方案后端
咱们指望在任何浏览器下都是一个样式,好比一种样式的按钮api
<form action="/api/file" method="post">
<!-- input 设置为透明,覆盖在 button 上面 --->
<input type="file" style="opacity: 0; position:absolute;zindex:9999;top:0;right:0;"/>
<button type="submit">Upload File</button>
</form>复制代码
经过把 input 设置为透明覆盖在 button 按钮上面,让用户觉得本身点击的是 button,其实点击的是 button 上面的 input。这样就能够作成用户点击button就能选择文件的“假象”。浏览器
查找 button 其实定位到了 input。详细代码能够看这里: github.com/alibaba-fus…bash
咱们指望选择完文件马上执行上传,上传完成后直接在页面上展示上传状态app
<iframe name="uploadiframe" style="display:none"></iframe>
<form action="/api/file" method="post" target="uploadiframe">
<input type="file" style="opacity: 0; position:absolute;zindex:9999;top:0;right:0;"/>
<button type="submit">Upload File</button>
</form>复制代码
在提交的时候 form 经过 target 指定到对应的 iframe 去上传数据,让form 的数据经过隐藏的 iframe 来提交。
const doc = this.refs.iframe.contentDocument; // 取 iframe
const script = doc.getElementsByTagName('script')[0]; // 清除 iframe 内无用 script
if (script && script.parentNode === doc.body) {
doc.body.removeChild(script);
}
const response = JSON.parse(doc.body.innerHTML); // 取返回内容解析成 JSON复制代码
由于 iframe 完成上传后页面会总体刷新,再经过监听 iframe 的 onLoad 事件获取返回的结果。关于获取返回内容如何再给主页面作反馈展现的代码能够看这里: github.com/alibaba-fus…
html5 出来后,能够经过 input 能够直接拿到 File 文件对象,再把 File 封装到 FormData,经过 ajax 的形式提交到后端接口实现文件上传。
<script>
function selectFile() {
$('#inputfile').click();
}
function onSelect(target) {
console.log(target.files); // 获取文件对象
}
</script>
<div role="upload" onclick="selectFile()">
<input type="file" style="display: none;" id="inputfile" onchange="onSelect(this)">
<button>Upload File</button>
</div>复制代码
我其实能够在 div 里面放的不只仅是 button 了,能够是任何元素,这样咱们就能作出任何形状的上传按钮。 下面列举几个例子
卡片状态
<div role="upload">
<input type="file" style="display: none;">
<div class="selecter">
<i class="icon-add" />
<span> Upload File </span>
</div>
</div>复制代码
上传面板
<div role="upload">
<input type="file" style="display: none;">
<div class="selecter">
<i class="icon-upload" />
<span class="title"> 点击或者拖动文件到虚线框内上传 </span>
<span class="desc"> 支持 docx, xls, PDF, rar, zip, PNG, JPG 等类型文件 </span>
</div>
</div>复制代码
原理是把 File 对象封装到 FormData,再经过 ajax 的形式提交到后端接口。直接上代码:
function upload(file) {
const xhr = new XMLHttpRequest();
// 上传进度
xhr.upload.onprogress = function progress(e) {
};
// 上传状态
xhr.onload = function onload() {
};
const formData = new FormData();
// 往 formData 里面增长要上传的文件对象
formData.append('filename', file);
// 指定 api 接口和上传方式
xhr.open('POST', '/api/upload', true);
// 开始发送数据
xhr.send(formData);
}复制代码
以上是把一个 file 对象加到 formData 中,再经过 XMLHttpRequest 把 formData 发送到指定的接口 /api/upload 的一个大体过程。详细代码能够查看这里 github.com/alibaba-fus…
咱们现实中为了可能为了兼容 ie9 , 因此还须要封装一个 uploader,优先支持 html5 可是在 ie9 下自动切换为 iframe 方案。
上面咱们讲了一个文件上传必定是至少有两步:1. 选择文件 2. 上传文件。而且咱们已经有能力根据浏览器自动判断用什么兼容方案。
由此咱们作出了两个通用的组件:
封装后的 Selecter 把 input 和相关事件已经处理好了,你只须要关心往里面丢什么
import {Upload, Button} from '@alifd/next';
const Selecter = Upload.Selecter;
class App extends React.Comonent {
handleSelect = (files) => {
// get files
}
render() {
return <Selecter onSelect={this.handleSelect}>
<Button type="primary">Upload File</Button>
</Selecter>
}
}复制代码
若是要换成卡片样式,只要把 children 换掉便可,以下
<Selecter onSelect={this.handleSelect}>
<Icon type="add" />
<span> Upload File </span>
</Selecter>复制代码
把 Selecter 选择后的File 给 Uploader ,能够很方便的把文件上传到指定接口。
import {Upload, Button} from '@alifd/next';
const Selecter = Upload.Selecter; // 文件选择器
const Uploader = Upload.Uploader; // 文件上传器
class App extends React.Comonent {
uploader = new Uploader({
action: '/api/upload',
//onProgress: this.onProgress // 进度监控
});
handleSelect = (files) => {
// 上传文件
this.uploader.startUpload(files);
}
render() {
return <Selecter onSelect={this.handleSelect}>
<Button type="primary">Upload File</Button>
</Selecter>
}
}复制代码
由于Selecter的UI可定制,Uploader 的文件上传时机能够随便控制。是的 Selecter 和 Uploader 的组合得以适配任何场景和交互。调试demo 见: codepen.io/frankqian/p…
好比咱们能够经过 Uploader 自定义各类功能,好比作一个 粘贴上传组件
去除用来装饰的进度条,不到20行代码就写完了整个组件:
import { Upload, Input } from '@alifd/next';
const Uploader = Upload.Uploader; // 文件上传器
class App extends React.Component {
uploader = new Uploader({
action: '/api/upload',
});
// 处理粘贴事件
onPaste = e => {
const files = e.clipboardData.files; // 获取粘贴的文件数据
this.uploader.startUpload(files); // 上传文件
};
render() {
return <Input.TextArea onPaste={this.onPaste} placeholder="粘贴截图到这里" />;
}
}复制代码
能够在这里调试代码:codepen.io/frankqian/p…
解决易用性的问题
Selecter 和 Uploader 使用起来虽然很是灵活,可是仍是要本身写一些逻辑,把取到的 File 对象和 Uploader 作上传关联。而咱们在文件上传最经常使用的交互方式是选择完就开始上传、上传完成后给反馈。因此咱们把常见的交互进一步作提取,单按钮、卡片、拖拽面板 等,主要把经常使用UI和上传交互沉淀下来,方便大多的场景使用。
import {Upload, Button} from '@alifd/next';
class App extends React.Comonent {
handleChange = (file) => {
console.log(file.url); // 直接获取图片 url
}
render() {
return <div>
<Upload action="/api/file" onChange={this.handleChange}>
<Button type="primary">Upload File</Button>
</Upload>
<Upload action="/api/file" shape="card" onChange={this.handleChange}>
Upload File
</Upload>
<Upload.Dragger action="/api/file" onChange={this.handleChange}/>
</div>
}
}复制代码
以上就结合业务线经常使用的上传方案和交互提取的上传方式,咱们把 Selecter 和 Uploader 进行进一步封装,获得一个UI和交互相对固定的组件,使用起来更便捷。
阿里内部各个业务线上传的需求是多种多样的,Fusion Next 的 Upload 组件要考虑效率和能力以前的平衡。一个好的组件应该经过固定组件去解决 80% 的通用问题;剩下的 20% 可能各业务线不同,能够经过扩展能力让各业务线去支持。