本文主要闡述瞭如何結合阿里雲 OSS 服務實現一個支持斷點續傳和多文件上傳的 web SDK。文章內容配合代碼食用風味更佳:https://github.com/polyv/vod-... 。javascript
其中,分片上傳和斷點續傳技術由阿里雲 OSS Browser.js SDK(下面簡稱 OSS SDK)提供,具體調用方法可參見阿里雲 OSS 的 相關文檔。html
阿里雲 OSS 提供的分片上傳(Multipart Upload)和斷點續傳功能,能夠將要上傳的文件分紅多個數據塊(OSS 裏又稱之爲 Part)來分別上傳,上傳完成以後再調用 OSS 的接口將這些 Part 組合成一個 Object 來達到斷點續傳的效果。
web 上傳 SDK 所作的是結合業務邏輯將相關的上傳邏輯封裝起來,並提供相關調用方法,以便於其餘開發者快速集成。java
先來了解一下咱們即將要作的是一個怎樣的產品 :webpack
支持兩種 SDK 集成方式:git
script
標籤引入在線資源 ( http://player.polyv.net/resp/... )npm install @polyv/vod-upload-js-sdk
更詳細的使用文檔能夠查看這裏: @polyv/vod-upload-js-sdkgithub
正文開始前,先來看下 src 目錄下的全部文件。其中,UploadManager
、 Pool
和 PlvVideoUpload
三個類會在後面着重分析。web
文件 | 說明 |
---|---|
index.js | SDK 入口文件,返回 PlvVideoUpload 類 |
pool.js | 實現一個控制多個任務同時執行的任務池(Pool ) |
queue.js | 實現一個普通隊列類(Queue ) |
upload.js | 實現一個管理單個文件上傳的類(UploadManager ) |
utils.js | 工具函數 |
Web 端常見的上傳方法是用戶在瀏覽器或 APP 端上傳文件到應用服務器,應用服務器再把文件上傳到 OSS。相對於這種上傳慢、擴展性差、費用高的方式,阿里雲官方更推薦將數據直傳到 OSS。ajax
阿里雲 OSS 直傳的三種方案:算法
- JavaScript 客戶端簽名後直傳。
- 服務端簽名後直傳。
- 服務端簽名後直傳並設置上傳回調。
這裏採用的是第三種直傳方案,具體流程以下:npm
能夠看到,Web 端須要作的只有兩步:
接下來,咱們來了解一下更具體的上傳流程。
因爲上傳過程當中使用了由 OSS SDK 提供的分片上傳和斷點續傳技術,咱們須要先安裝該 SDK,具體的安裝方式點擊 這裏 查看。該 SDK 經過提供 OSS 對象及相關方法來支持上傳。
上傳的主要流程包括:
這裏的上傳流程包括了從未開始和暫停兩種狀態開始的上傳。若是是從暫停狀態開始上傳,則能夠跳過嚮應用服務器請求 Policy 和回調、上傳以前的業務邏輯處理等步驟。具體流程參考下圖:
爲了方便調用,圖中的流程封裝成一個 UploadManager
類,提供開始上傳、暫停上傳等方法。
下面來詳細講一下如何實現圖中提到的斷點續傳、記錄斷點信息,以及爲何要更新臨時訪問憑證。
正如前面提到的,咱們須要先初始化 OSS 實例,而後調用 multipartUpload()
方法開始上傳,經過參數能夠設置分片大小、上傳進度回調、callback等。
相關代碼以下:
// upload.js import OSS from 'ali-oss/dist/aliyun-oss-sdk.min'; import PubSub from 'jraiser/pubsub/1.2/pubsub'; class UploadManager extends PubSub { // ... // 分片上傳 _multipartUpload() { // 初始化 OSS 實例 this.ossClient = new OSS(this.ossConfig); // 從瀏覽器本地存儲獲取checkpoint const checkpoint = getLocalFileInfo(this.fileData.id); if (checkpoint) { checkpoint.file = this.fileData.file; } // 斷點續傳 return new Promise((resolve, reject) => { this.ossClient.multipartUpload( this.filenameOss, // Object名稱 this.fileData.file, // File 對象 { // 額外參數 parallel: this.parallel, partSize: this.partSize || getPartSize(this.fileData.file.size), // 分片大小 progress: this._updateProgress.bind(this), // 上傳進度回調函數 checkpoint, // 斷點記錄點 callback: this.callbackBody // callback回調設置 } ).then(() => { // 完成上傳 // ... }).catch((err) => { // 異常處理 // ... }); }); } // ... } export default UploadManager;
對於上傳同一份文件,咱們但願即使是關閉頁面再從新打開,也能從斷點處續傳,所以須要將斷點信息記錄在 localstorage
中。因爲每一個文件對應的 localstorage
的鍵名應該高度惟一,咱們最好能根據用戶信息、文件名、文件大小、文件類型等綜合角度去作惟一性標識。
考慮到要將這麼多信息拼成一個字符串後長度可能會很長,而且可能會包含了一些特殊字符,因此選擇了用 md5
將這個拼接獲得的字符串進行加密,加密後的字符串就做爲文件 id 使用。
相關代碼以下:
// 根據文件信息及用戶信息對每一個不一樣的文件生成具備必定長度的惟一標識 function _generateFingerprint(fileData, userData) { const { cataid, file } = fileData; return md5(`polyv-${userData.userid}-${cataid}-${file.name}-${file.type}-${file.size}`); }
上圖中提到的臨時訪問憑證是由於咱們使用了阿里雲STS進行臨時受權。
OSS能夠經過阿里雲 STS(Security Token Service)進行臨時受權訪問。阿里雲STS是爲雲計算用戶提供臨時訪問令牌的Web服務。經過 STS,能夠爲第三方應用或子用戶(即用戶身份由您本身管理的用戶)頒發一個自定義時效和權限的訪問憑證。
臨時訪問憑證有必定的有效期,過時以後上傳過程會 catch 到錯誤並中止上傳,這時須要更新憑證才能繼續上傳文件。
本 SDK 還須要實現多個文件同時上傳,並限制同時處於上傳狀態的文件不能超過 5 個,以下面的 demo 截圖所示:
圖中是點擊了"所有開始"的效果截圖,雖然一共添加了 9 個文件,但只有前面 5 個文件真正處於上傳狀態。這裏我將這些已經處於開始狀態(包括未真正開始上傳)的文件都添加到一個特殊的上傳任務隊列中,經過一個控制多個任務同時執行的任務池(Pool
類)來實現對上傳隊列的管理。
那麼該如何限制多個文件同時上傳呢?
按照上述的兩種狀態,咱們將任務池中的文件分爲兩類,分別對應:正在執行任務的列表(執行列表)和等待執行任務的列表(等待列表)。
要注意的是,雖然 Pool
類內部須要管理兩個列表,可是對外表現爲一個列表,因此一些隊列方法都是對總體進行操做的。
Pool
類的關鍵方法Pool
類是用於管理上傳隊列,因此須要具備隊列的管理方法,如入隊、出隊、查找、移除。此外,pool
還須要具備控制任務的執行,所以須要 _check()
方法檢查執行列表是否已經"滿員"、是否存在下一個等待執行的任務。
如下爲 Pool
類的一些關鍵方法及說明:
enqueue()
:將元素添加到等待列表的尾部,並檢查是否能夠當即執行該任務。dequeue()
:從隊列中刪除第一個元素,並返回該元素的值。remove(id)
:移除隊列中的指定元素,並返回該元素的值或 null
。_check()
:檢查是否還有下一個任務能夠執行。_run(item)
:執行指定的任務,並在執行完成後,調用 _check()
檢查是否存在下一個等待執行的任務。UploadManager
類和 Pool
類都不會直接提供給外部調用,而是經過一個 PlvVideoUpload
類來整合文件列表的上傳邏輯,以及對外提供接口。這裏咱們來介紹一下這個 PlvVideoUpload
類的部分功能和實現。
上面提到的上傳隊列只是用於管理能夠開始上傳的文件,可是若是這時暫停了某個文件的上傳,這個文件就應該從上傳隊列中出隊。對於這個暫停狀態的文件,咱們還須要一個隊列來管理相似的狀況。
在文件隊列中,除了用於管理開始上傳或準備上傳的文件的上傳隊列 uploadPool
( Pool
類的實例),還應該有一個等待隊列( waitQueue
)用於管理已經添加到文件隊列但未容許開始上傳的文件。
有兩種狀況須要將文件添加到等待隊列中進行管理:
waitQueue
;不然添加到 uploadPool
。waitQueue
;繼續上傳時能夠從 waitQueue
中根據 id
找到對應的上傳管理器(UploadManager
類的實例)。因爲 SDK 既支持全部文件的統一操做(開始、暫停),也支持對單個文件的操做,因此還要考慮操做單個文件對整個上傳流程的影響。
比較複雜的狀況是,總體的文件隊列在上傳時,操做單個文件須要對上傳隊列和等待隊列從新進行調整,如今總結了幾種操做對應的處理以下:
除了上述的狀況,隊列處於暫停狀態時操做單個文件也須要對等待隊列作相應調整,比較簡單,這裏就不贅述了 。
文件隊列處於上傳狀態時可操做單個文件上傳狀態帶來的另外一個問題是,該如何判斷全部文件都已經結束上傳呢?
將文件信息添加到上傳隊列以後,會返回一個 Promise
實例。可使用 Promise.all()
方法將多個 Promise
實例,包裝成一個新的 Promise
實例。當全部文件都上傳成功後,會觸發這個新 Promise
實例。
從而有了如下的思路:
// upload.js /** * 開始上傳全部文件 */ startAll() { const uploadPromiseList = []; while (this.waitQueue.size > 0) { const uploader = this.waitQueue.dequeue(); uploadPromiseList.push(this.uploadPool.enqueue(uploader)); } // 判斷全部文件上傳是否結束 Promise.all(uploadPromiseList) .then(() => { // TODO: 觸發 UploadComplete 事件 }); }
可是,這樣作的問題顯而易見。若是文件隊列處於上傳狀態,對某個文件前後執行暫停和繼續操做後,就沒法監控到這個文件上傳結束的事件;若是文件隊列上傳結束,這時添加一個文件並單獨對該文件執行上傳操做,文件上傳結束後,也不會觸發 UploadComplete
事件。
咱們來從新整理一下思路:
uploader
。uploader
入隊到上傳隊列時會返回一個 Promise 實例。uploader
入隊到上傳隊列有如下幾種狀況:
promise
都集中起來處理。咱們這裏定義一個數組 newUploadPromiseList
來存放這些上傳 promise
,以及一個 _onPromiseEnd()
方法來監聽 newUploadPromiseList
中全部 promise
的結束。執行 _onPromiseEnd
方法的時機:
第 4 點中提到的 _onPromiseEnd()
代碼以下:
// index.js class PlvVideoUpload extends PubSub { // ... _onPromiseEnd() { const uploadPromiseList = [...this.newUploadPromiseList]; this.newUploadPromiseList = []; // 判斷全部文件上傳是否結束 Promise.all(uploadPromiseList) .then(() => { if (this.newUploadPromiseList.length > 0) { // 還有未監聽到的 promise this._onPromiseEnd(); } else if (this.uploadPool.size === 0) { // 上傳隊列長度爲0 this.status = STATUS.NOT_STARTED; // 文件隊列的上傳狀態改成暫停 if (this.waitQueue.size === 0 && this.fileQueue.size !== 0) { // 等待隊列長度爲0,但文件隊列長度不爲0 // TODO: 上傳結束,觸發 UploadComplete 事件 } } }); // 處理文件上傳狀態發生改變或上傳報錯的狀況 for (let i = 0; i < uploadPromiseList.length; i++) { uploadPromiseList[i] .then(res => { if (!res || !res.code) return; this._handleUploadStatusChange(res); }) .catch(err => { // TODO: 上傳報錯,觸發 Error 事件 }); } } }
以上代碼還包括了上傳狀態發生改變或上傳報錯時的處理。其中 _handleUploadStatusChange(res)
用於處理文件上傳狀態發生改變的狀況。res.code
是由 uploadManager
實例(uploader
)返回的錯誤代碼,能夠用於區分各類狀況致使的上傳中斷,以便於對不一樣狀況的中斷作後續處理。
最後,咱們使用 webpack 進行打包。
經過 output.libraryTarget
,咱們能夠決定如何暴露 SDK 。
output
選項主要用於配置文件輸出規則,而 output.library
選項能夠用於輸出時將文件暴露爲一個變量,能夠說是爲了打包 SDK 文件而生的一個配置項。另外一個選項 output.libraryTarget
則能夠配置如何輸出變量,默認值是 var
。
output.libraryTarget
的部分可選值:
libraryTarget: 'umd'
- This exposes your library under all the module definitions, allowing it to work with CommonJS, AMD and as global variable.
libraryTarget: 'commonjs2'
- The return value of your entry point will be assigned to themodule.exports
. As the name implies, this is used in CommonJS environments.更多可選值能夠參考 webpack 模塊定義系統的文檔。
下面是 SDK 中使用的兩種配置方式,主要區別在於 output
選項的配置:
// webpack.prod.config.js const merge = require('webpack-merge'); const config = require('./webpack.config.js'); module.exports = merge(config, { mode: 'none', entry: './src/index.js', output: { filename: 'main.js', libraryTarget: 'commonjs2' } });
// webpack.umd.config.js const merge = require('webpack-merge'); const prodConfig = require('./webpack.prod.config.js'); module.exports = merge(prodConfig, { mode: 'production', devtool: false, entry: ['./src/index.js'], output: { filename: 'vod-upload-js-sdk.min.js', library: 'PlvVideoUpload', libraryTarget: 'umd', libraryExport: 'default' } });
因爲 SDK 自己不包含界面,爲了方便開發過程當中進行調試,加入了一個簡單的 demo 來調用 SDK。而且但願可以在開發過程當中不管是修改了 demo 仍是 SDK 中的代碼,均可以實時從新加載。爲此,咱們引入了 HtmlWebpackPlugin
插件來建立一個 HTML 文件:
// webpack.dev.config.js const merge = require('webpack-merge'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const config = require('./webpack.config.js'); module.exports = merge(config, { devtool: 'inline-source-map', mode: 'development', entry: { polyfill: 'babel-polyfill', main: './demo/dev.js' }, plugins: [ new HtmlWebpackPlugin({ template: './demo/dev.html', inject: true }) ], devServer: { host: '0.0.0.0', port: 14002, compress: true, overlay: true, proxy: { } } });
上面的兩個 webpack 入口點(polyfill
和 main
),都會出如今生成的 HTML 文件中的 script
標籤中。
開發過程當中還發現了一個頗有用的 javascript 基礎庫——jraiser,SDK 須要的事件驅動機制、ajax 請求接口、MD5 加密算法均可以在這個 npm 插件中找到相應的模塊來引入到項目中。更多的介紹能夠查看這篇 npm上的文檔 以及它的 API 文檔。