Fusion Next 之 Upload 上傳元件設計思路

布達發表於2019-02-14

Upload 元件設計的目標是解決使用者上傳檔案的便利性,但是中後臺 Upload 元件的場景是多種多樣的,所以可擴充套件能力是 Upload 元件不可忽視的另一方面。

同樣為了大家能夠更加容易的理解,我會從最原始的 input 標籤開始說起

<form action="/api/file">
  <input type="file" />
  <button type="submit">submit</button>
</form>  複製程式碼

這段程式碼功能: 先選擇一個檔案,再點提交 POST 一個檔案到一個介面。程式碼雖然不多,但是在實際使用中值得吐槽的點卻不少,這裡重點說兩個點。

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


Fusion Next 之 Upload 上傳元件設計思路

先不說UI不美觀,在每個主流瀏覽器上面的文案基本都不一樣,另外在IE下面變化似乎有點大。我們可能的期望是在任何瀏覽器下互動和UI都一致的元件。

  • 檔案上傳完後頁面會重新整理帶來的體驗問題

原生的檔案上傳都是通過form post 上傳,上傳完成後整個頁面會重定向到 action 的地址。現在大家已經習慣了 ajax 做資料提交,因為可以不需要reload頁面就可以帶來整個頁面的資料更新,無重新整理更新的體驗會提升很多。

我打算整片拆兩個段來講這個問題,拆分點大約從2012年附近開始,因為 html5 差不多在這個時間段開始被現代瀏覽器逐步支援。兩個段分別叫傳統解決方案和現代解決方案

傳統解決方案

  • UI 一致性問題

我們期望在任何瀏覽器下都是一個樣式,比如一種樣式的按鈕

Fusion Next 之 Upload 上傳元件設計思路
<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就能選擇檔案的“假象”。

Fusion Next 之 Upload 上傳元件設計思路

查詢 button 其實定位到了 input。詳細程式碼可以看這裡: github.com/alibaba-fus…

  • 無重新整理上傳

我們期望選擇完檔案立刻執行上傳,上傳完成後直接在頁面上展現上傳狀態


<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 的形式提交到後端介面實現檔案上傳。

  • 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 了,可以是任何元素,這樣我們就能做出任何形狀的上傳按鈕。 下面列舉幾個例子

卡片狀態

Fusion Next 之 Upload 上傳元件設計思路
<div role="upload">
  <input type="file" style="display: none;">
  <div class="selecter">
      <i class="icon-add" />
      <span> Upload File </span>
  </div>
</div>複製程式碼

上傳皮膚

Fusion Next 之 Upload 上傳元件設計思路
<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 方案。

一個通用的 React 上傳元件解決方案

上面我們講了一個檔案上傳一定是至少有兩步:1. 選擇檔案 2. 上傳檔案。並且我們已經有能力根據瀏覽器自動判斷用什麼相容方案。

由此我們做出了兩個通用的元件:

  • Selecter 檔案選擇器。可以讓任何元件變成一個檔案選擇器,並且返回選擇後的 File 物件
  • Uploader 檔案上傳器。可以像掉 api 一樣隨心所欲的上傳選擇的檔案,並且可監控進度。

Selecter 檔案選擇器

封裝後的 Selecter 把 input 和相關事件已經處理好了,你只需要關心往裡面丟什麼

Fusion Next 之 Upload 上傳元件設計思路

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

Fusion Next 之 Upload 上傳元件設計思路
<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 見: codepen.io/frankqian/p…

比如我們可以通過 Uploader 自定義各種功能,比如做一個 貼上上傳元件

Fusion Next 之 Upload 上傳元件設計思路

去除用來裝飾的進度條,不到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和上傳互動沉澱下來,方便大多的場景使用。

Fusion Next 之 Upload 上傳元件設計思路

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% 可能各業務線不一樣,可以通過擴充套件能力讓各業務線去支援。

相關連結


相關文章