我是如何設計 Upload 上傳組件的

Upload 組件設計的目標是解決用戶上傳文件的便利性,可是中後臺 Upload 組件的場景是多種多樣的,因此可擴展能力是 Upload 組件不可忽視的另外一方面。html

一樣爲了你們可以更加容易的理解,我會從最原始的 input 標籤開始提及html5

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

這段代碼功能: 先選擇一個文件,再點提交 POST 一個文件到一個接口。代碼雖然很少,可是在實際使用中值得吐槽的點卻很多,這裏重點說兩個點。git

  • 在每一個瀏覽器上面的表現是各不同的。

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

先不說UI不美觀,在每一個主流瀏覽器上面的文案基本都不同,另外在IE下面變化彷佛有點大。咱們可能的指望是在任何瀏覽器下交互和UI都一致的組件。github

  • 文件上傳完後頁面會刷新帶來的體驗問題

原生的文件上傳都是經過form post 上傳,上傳完成後整個頁面會重定向到 action 的地址。如今你們已經習慣了 ajax 作數據提交,由於能夠不須要reload頁面就能夠帶來整個頁面的數據更新,無刷新更新的體驗會提高不少。ajax

我打算整片拆兩個段來說這個問題,拆分點大約從2012年附近開始,由於 html5 差很少在這個時間段開始被現代瀏覽器逐步支持。兩個段分別叫傳統解決方案和現代解決方案後端

傳統解決方案

  • UI 一致性問題

咱們指望在任何瀏覽器下都是一個樣式,好比一種樣式的按鈕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就能選擇文件的「假象」。瀏覽器

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

查找 button 其實定位到了 input。詳細代碼能夠看這裏: https://github.com/alibaba-fu...app

  • 無刷新上傳

咱們指望選擇完文件馬上執行上傳,上傳完成後直接在頁面上展示上傳狀態post

<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...

相關文章
相關標籤/搜索