原 薦 any-loader JS資料載入器中介軟體
簡介
any-loader 旨在為 node.js 和其他的 javascript 提供一個可定製程度較高的資料載入器中介軟體類庫。本身並不實現任何資料載入器的實現邏輯,只界定了資料 流走向的標準介面 newLoadStrem -> setup -> beforeLoad -> doLoad -> afterLoad
,呼叫順序(不可逆),以及此過程中的異常錯誤處理機制。
any-loader 支援並實現了以下程式設計特性:
- 基於AOP設計,支援非同步(Promise)。
- 中介軟體形態,不干涉業務邏輯和底層實現。
- 使用OOP進行擴充套件,使用繼承和方法過載,來進行子類的開發,並提供豐富的方法以控制的粒度。
- 介面基於 Promise 封裝,向後相容 async/await 語法
- 資料流(LoadStream)部分,使用 fp 程式設計,資料流持有的
input
,output
等資料,只在介面中流轉,結束後即作廢。 Loader 本身無狀態,不持有過程資料。
碼雲倉庫地址: ofollow,noindex" target="_blank">https://gitee.com/janpoem/any-loader
設計初衷
在決定將 any-loader 作為獨立的專案前,正忙於一個基於 React.js Web 實現的後臺檔案管理系統,因為前端環境和伺服器環境,需要在前端整合比較多的資料介面。
- 一般的 Ajax 拉取檔案列表、單個檔案、更新檔案修改等。
- 前端的 FileReader ,識別和檢測使用者上傳檔案的安全性,以及圖片和視訊客戶端生成預覽(嗯,現在這些都轉移到前端實現了,沒必要交給後端做了,以後有空再把這一塊開源)。
- 客戶端直接上傳到 CDN,沒必要再從伺服器走一趟了,根據檔案型別,還要通知 CDN 對檔案進行各種處理(如視訊轉碼、壓縮解析度,圖片生成縮圖等)。
- 上傳完畢,需要更新伺服器端,記錄檔案資訊,以及有效的 CDN 資源地址。
前端需要非同步呼叫的地方很多,最初的想法是將功能和資源點接近形成一個載入器組,進行封裝管理。可是隨著開發的程式碼增加,就越發發現載入器組控制粒度不夠細。
- 隨著介面越來越多,應用層面、介面層面的呼叫程式碼越來越多,越來越多的結構控制,更別提在不改變呼叫程式碼的前提下,去擴充套件和細化載入器的中間邏輯程式碼,只能不斷的增加應用層的程式碼量。
- 非同步環呼叫情況更惡劣。專案裡有對Ajax的請求封裝,但這隻適合單次Ajax(適合網站前臺)請求,後臺的請求,特別是檔案管理系統,往往在執行一個操作,往往涉及到一個系列的非同步呼叫環。比如上傳到 CDN,要先從伺服器端拿到 token ,上傳完畢後,還需要將資訊儲存到伺服器端。
- 基於 JS 老掉牙的事件驅動,應用層的程式碼會臃腫不堪,不斷巢狀的事件註冊,不利於後續的擴充套件和開發。
- 缺乏統一排程管理,這裡說的排程,即同類介面的併發策略,是等待、取消還是延後等。嗯,是的,當更大程度的使用 React.js ,對於各種資料載入,其實隱性的存在這一個併發排程管理的需求,現有的各種工具類庫,並沒有在這些方面有很好的著力點,可以說完全為零。在C#和Java等靜態語言有執行緒安全一說,可是在過去20多年的JS開發中,並沒有這個概念。但隨著現在前端技術發展的程度,前端的非同步排程安全性,成為一個非常重要的內容(特別是Web Worker、Service Worker、大量的 Promise 環境下)。
- 基於 React.js 的一些特性,想將資料載入介面傳遞進元件內被使用,是一個比較頭疼的問題。當然可以選擇使用 Redux 等,Redux 開拓了一個全新的編碼區間,以解決這方面的問題。但我並不是太喜歡這種動不動就開啟一個新的編碼空間的做法,太多框架一再用事實告訴我們,如果不解決問題本身,而為了解決某型別問題去開拓一個新的編碼區間,最終那個區間只會成為一個無王法、無規範,程式碼質量差,問題成堆的集中地,所以還是要回到問題本質。
經過一番思索和準備,我決定將 any-loader 作為一個獨立的類庫來實現。any-loader 不旨在解決實際載入器的業務流程的複雜度,也不提供 Loader 的實現,更不會考慮對任何資料載入方式做封裝。any-loader 只定義了一個數據載入流的介面呼叫順序,並將足夠多的方法和介面進行暴露,提供給子類更多細化調節和擴充套件的空間。同時,在不改變應用層的呼叫程式碼的前提下,隨著專案的開發程度和需求細化程度,可以逐步對專案實際的 Loader 進行漸進式的升級和擴充套件,而不需要一再的去調整應用層的呼叫程式碼。
簡單示例
class ImageLoader extends Loader { // 預設形態下,input, output 是 {} doLoad({input, output, errors}) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = function(ev) { output.image = this; output.width = this.width; output.height = this.height; resolve(); }; image.onerror = (ev) => { reject(new Error('圖片載入失敗!')); }; image.src = input.url; }); } } const loader = new ImageLoader(); loader.load({url: 'https://www.oschina.net/build/oschina/components/imgs/header/logo.svg'}).then(({output}) => { }).catch(error => { });
這裡定義了一個圖片載入器,通過 doLoad 方法的過載,來實現該載入器的具體實現。當然這個例子看起來很簡單,市面上大把這樣的圖片載入器的類庫。下來我們接著擴充套件。
// 我們先定義了一個遠端的URL類,或者你的專案本身就有類似的設定 class RemoteURL { constructor() { // .... } toURL() { return '...'; } } // 再定義一個遠端的圖片類 class RemoteImage { constructor(remoteUrl) { this.url = remoteUrl; // 這是一個RemoteURL的例項 this.isLoad = false; this.image = null; this.error = null; } load(image) { this.isLoad = true; this.image = image; } error(error) { this.error = error; } } class ImageLoader extends Loader { // 我們將 RemoteURL 的例項,作為 LoadStream 的 input newInput(input) { return new RemoteURL(this.mergeArgs(input)); } newOutput(input, output) { // 到這裡時,input已經變為 RemoteURL 的例項 return new RemoteImage(input); } // input => RemoteURL, output => RemoteImage doLoad({input, output, errors}) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = function(ev) { output.load(this); resolve(); }; image.onerror = (ev) => { output.error(new Error('圖片載入失敗!')); reject(output.error); }; image.src = input.toURL(); }); } } // 呼叫程式碼 const loader = new ImageLoader(); loader.load({url: 'https://www.oschina.net/build/oschina/components/imgs/header/logo.svg'}).then(({output}) => { }).catch(error => { });
第二個例子中,我們增加了兩個中間類,以對ImageLoader 的輸入、輸出,進行更細的控制。同時,為ImageLoader過載了兩個方法,以將輸入、輸出的例項繫結到ImageLoader 標準流程中去。在應用層呼叫的程式碼不變的前提下,通過增加中間層的程式碼,實現了對Loader更多的控制。
更多例子,後續更新
版本說明
現階段,不考慮基於類庫層面解決併發策略的問題,而在具體的專案裡實現的 子類Loader 去簡單的管理。
未完,待續。
