any-loader JS数据加载器中间件

javascript/jquery

浏览数:265

2020-6-12


简介

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 去简单的管理。

未完,待续。

作者:曾建凯