当前位置: 首页 > news >正文

我是如何设计 Upload 上传组件的

Upload 组件设计的目标是解决用户上传文件的便利性,但是中后台 Upload 组件的场景是多种多样的,所以可扩展能力是 Upload 组件不可忽视的另一方面。

同样为了大家能够更加容易的理解,我会从最原始的 input 标签开始说起

<form action="/api/file">
  <input type="file" />
  <button type="submit">submit</button>
</form>  

这段代码功能: 先选择一个文件,再点提交 POST 一个文件到一个接口。代码虽然不多,但是在实际使用中值得吐槽的点却不少,这里重点说两个点。

  • 在每个浏览器上面的表现是各不一样的。

1550121699861-f22892d5-b799-47ab-a8d6-8bb64a42e829.png

先不说UI不美观,在每个主流浏览器上面的文案基本都不一样,另外在IE下面变化似乎有点大。我们可能的期望是在任何浏览器下交互和UI都一致的组件。

  • 文件上传完后页面会刷新带来的体验问题

原生的文件上传都是通过form post 上传,上传完成后整个页面会重定向到 action 的地址。现在大家已经习惯了 ajax 做数据提交,因为可以不需要reload页面就可以带来整个页面的数据更新,无刷新更新的体验会提升很多。

我打算整片拆两个段来讲这个问题,拆分点大约从2012年附近开始,因为 html5 差不多在这个时间段开始被现代浏览器逐步支持。两个段分别叫传统解决方案和现代解决方案

传统解决方案

  • UI 一致性问题

我们期望在任何浏览器下都是一个样式,比如一种样式的按钮

<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就能选择文件的“假象”。

1550129218663-bb81f35f-ec5e-4411-93ab-e2f236ff008c.png

查找 button 其实定位到了 input。详细代码可以看这里: https://github.com/alibaba-fu...

  • 无刷新上传

我们期望选择完文件立刻执行上传,上传完成后直接在页面上展现上传状态

<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 事件获取返回的结果。关于获取返回内容如何再给主页面做反馈展示的代码可以看这里: https://github.com/alibaba-fu...

现代上传方案

html5 出来后,可以通过 input 可以直接拿到 File 文件对象,再把 File 封装到 FormData,通过 ajax 的形式提交到后端接口实现文件上传。

  • UI 一致性问题

不需要再把 input 盖在 button 上面,而是通过监听父节点的点击事件,在事件里面触发 input 的 click 方法。

<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 的一个大致过程。详细代码可以查看这里 https://github.com/alibaba-fu...

我们现实中为了可能为了兼容 ie9 , 所以还需要封装一个 uploader,优先支持 html5 但是在 ie9 下自动切换为 iframe 方案。

一个通用的 React 上传组件解决方案

上面我们讲了一个文件上传一定是至少有两步:1. 选择文件 2. 上传文件。并且我们已经有能力根据浏览器自动判断用什么兼容方案。

由此我们做出了两个通用的组件:

  • Selecter 文件选择器。可以让任何组件变成一个文件选择器,并且返回选择后的 File 对象
  • Uploader 文件上传器。可以像掉 api 一样随心所欲的上传选择的文件,并且可监控进度。

Selecter 文件选择器

封装后的 Selecter 把 input 和相关事件已经处理好了,你只需要关心往里面丢什么

1550121699895-0f58240e-fc8b-49df-b315-056688969720.png

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 换掉即可,如下

1550121699899-0de11939-68db-428e-9917-563f8764ebfd.png

<Selecter onSelect={this.handleSelect}>
  <Icon type="add" />
  <span> Upload File </span>
</Selecter>

Uploader 文件上传器

把 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 见: https://codepen.io/frankqian/...

比如我们可以通过 Uploader 自定义各种功能,比如做一个 粘贴上传组件

1550131189249-e1193c86-4419-4e3d-b83b-232068e9bf36.png

去除用来装饰的进度条,不到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="粘贴截图到这里" />;
  }
}

可以在这里调试代码:https://codepen.io/frankqian/...

进一步提取更通用的使用方式

解决易用性的问题

Selecter 和 Uploader 使用起来虽然非常灵活,但是还是要自己写一些逻辑,把取到的 File 对象和 Uploader 做上传关联。而我们在文件上传最常用的交互方式是选择完就开始上传、上传完成后给反馈。所以我们把常见的交互进一步做提取,单按钮、卡片、拖拽面板 等,主要把常用UI和上传交互沉淀下来,方便大多的场景使用。

1550123392700-a241a107-d6a2-4fa5-b3e3-566f73695b5c.png

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% 可能各业务线不一样,可以通过扩展能力让各业务线去支持。

相关链接
Fusion Upload: https://fusion.design/compone...
github: https://github.com/alibaba-fu...

相关文章:

  • 团队项目第一阶段冲刺站立会议6(4月23日)
  • You must use the Role Management Tool to install or configure Microsoft .NET Framework 3.5 SP1
  • 云HBase Spark分析引擎对接云数据库POLARDB
  • Hive基本操作
  • IDEA之配置svn
  • iPhone6 Plus、iPhone6、iPhone5S和之前版本真实分辨率
  • 云计算读书笔记(四)
  • python调用百度AI提取图片文字
  • 利用新浪微博API的Search接口做微博锐推榜
  • java中的多线程你只要看这一篇就够了
  • 查看Linux版本信息
  • Android 插件化原理-好文收集(陆续中。。。)
  • C#6.0 十大常用特性
  • 无向图的最短路径算法JAVA实现
  • 火掌柜iOS端基于CocoaPods的组件二进制化实践
  • 08.Android之View事件问题
  • Apache Pulsar 2.1 重磅发布
  • exif信息对照
  • FineReport中如何实现自动滚屏效果
  • JAVA多线程机制解析-volatilesynchronized
  • Js基础——数据类型之Null和Undefined
  • Linux中的硬链接与软链接
  • MYSQL 的 IF 函数
  • React中的“虫洞”——Context
  • SAP云平台里Global Account和Sub Account的关系
  • vue 配置sass、scss全局变量
  • 编写高质量JavaScript代码之并发
  • 程序员最讨厌的9句话,你可有补充?
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 构建工具 - 收藏集 - 掘金
  • 前端知识点整理(待续)
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 学习使用ExpressJS 4.0中的新Router
  • 一、python与pycharm的安装
  • 一些css基础学习笔记
  • ​ 全球云科技基础设施:亚马逊云科技的海外服务器网络如何演进
  • ​水经微图Web1.5.0版即将上线
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • (HAL)STM32F103C6T8——软件模拟I2C驱动0.96寸OLED屏幕
  • (附表设计)不是我吹!超级全面的权限系统设计方案面世了
  • (接口自动化)Python3操作MySQL数据库
  • (三)centos7案例实战—vmware虚拟机硬盘挂载与卸载
  • .describe() python_Python-Win32com-Excel
  • .mat 文件的加载与创建 矩阵变图像? ∈ Matlab 使用笔记
  • .net Application的目录
  • .NET Remoting学习笔记(三)信道
  • .NetCore部署微服务(二)
  • .NET大文件上传知识整理
  • /usr/bin/env: node: No such file or directory
  • @Valid和@NotNull字段校验使用
  • []AT 指令 收发短信和GPRS上网 SIM508/548
  • [C#小技巧]如何捕捉上升沿和下降沿
  • [CareerCup] 12.3 Test Move Method in a Chess Game 测试象棋游戏中的移动方法
  • [DAX] MAX函数 | MAXX函数
  • [IE编程] WebBrowser控件的多页面浏览(Tabbed Browsing)开发接口