any-loader JS數據加載器中間件 原 薦

簡介

any-loader 旨在爲 node.js 和其他的 javascript 提供一個可定製程度較高的數據加載器中間件類庫。本身並不實現任何數據加載器的實現邏輯,只界定了數據 流走向的標準接口 newLoadStrem -> setup -> beforeLoad -> doLoad -> afterLoad ,調用順序(不可逆),以及此過程中的異常錯誤處理機制。

any-loader 支持並實現了以下編程特性:

  • 基於AOP設計,支持異步(Promise)。
  • 中間件形態,不干涉業務邏輯和底層實現。
  • 使用OOP進行擴展,使用繼承和方法重載,來進行子類的開發,並提供豐富的方法以控制的粒度。
  • 接口基於 Promise 封裝,向後兼容 async/await 語法
  • 數據流(LoadStream)部分,使用 fp 編程,數據流持有的 inputoutput 等數據,只在接口中流轉,結束後即作廢。 Loader 本身無狀態,不持有過程數據。

碼雲倉庫地址:https://gitee.com/janpoem/any-loader

設計初衷

在決定將 any-loader 作爲獨立的項目前,正忙於一個基於 React.js Web 實現的後臺文件管理系統,因爲前端環境和服務器環境,需要在前端集成比較多的數據接口。

  • 一般的 Ajax 拉取文件列表、單個文件、更新文件修改等。
  • 前端的 FileReader ,識別和檢測用戶上傳文件的安全性,以及圖片和視頻客戶端生成預覽(嗯,現在這些都轉移到前端實現了,沒必要交給後端做了,以後有空再把這一塊開源)。
  • 客戶端直接上傳到 CDN,沒必要再從服務器走一趟了,根據文件類型,還要通知 CDN 對文件進行各種處理(如視頻轉碼、壓縮分辨率,圖片生成縮略圖等)。
  • 上傳完畢,需要更新服務器端,記錄文件信息,以及有效的 CDN 資源地址。

前端需要異步調用的地方很多,最初的想法是將功能和資源點接近形成一個加載器組,進行封裝管理。可是隨着開發的代碼增加,就越發發現加載器組控制粒度不夠細。

  1. 隨着接口越來越多,應用層面、界面層面的調用代碼越來越多,越來越多的結構控制,更別提在不改變調用代碼的前提下,去擴展和細化加載器的中間邏輯代碼,只能不斷的增加應用層的代碼量。
  2. 異步環調用情況更惡劣。項目裏有對Ajax的請求封裝,但這隻適合單次Ajax(適合網站前臺)請求,後臺的請求,特別是文件管理系統,往往在執行一個操作,往往涉及到一個系列的異步調用環。比如上傳到 CDN,要先從服務器端拿到 token ,上傳完畢後,還需要將信息保存到服務器端。
  3. 基於 JS 老掉牙的事件驅動,應用層的代碼會臃腫不堪,不斷嵌套的事件註冊,不利於後續的擴展和開發。
  4. 缺乏統一調度管理,這裏說的調度,即同類接口的併發策略,是等待、取消還是延後等。嗯,是的,當更大程度的使用 React.js ,對於各種數據加載,其實隱性的存在這一個併發調度管理的需求,現有的各種工具類庫,並沒有在這些方面有很好的着力點,可以說完全爲零。在C#和Java等靜態語言有線程安全一說,可是在過去20多年的JS開發中,並沒有這個概念。但隨着現在前端技術發展的程度,前端的異步調度安全性,成爲一個非常重要的內容(特別是Web Worker、Service Worker、大量的 Promise 環境下)。
  5. 基於 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 去簡單的管理。

未完,待續。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章