簡介
any-loader 旨在爲 node.js 和其他的 javascript 提供一個可定製程度較高的數據加載器中間件類庫。本身並不實現任何數據加載器的實現邏輯,只界定了數據 流走向的標準接口 newLoadStrem -> setup -> beforeLoad -> doLoad -> afterLoad
,調用順序(不可逆),以及此過程中的異常錯誤處理機制。
any-loader 支持並實現了以下編程特性:
- 基於AOP設計,支持異步(Promise)。
- 中間件形態,不干涉業務邏輯和底層實現。
- 使用OOP進行擴展,使用繼承和方法重載,來進行子類的開發,並提供豐富的方法以控制的粒度。
- 接口基於 Promise 封裝,向後兼容 async/await 語法
- 數據流(LoadStream)部分,使用 fp 編程,數據流持有的
input
,output
等數據,只在接口中流轉,結束後即作廢。 Loader 本身無狀態,不持有過程數據。
碼雲倉庫地址: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 去簡單的管理。
未完,待續。