any-loader

Middleware of data loader. Easy to manage and wrap data API in your application. Support async/await and Promise. Compatible with future ES features.

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

any-loader 旨在为 node.js 和其他的 javascript 提供一个可定制程度较高的数据加载器中间件类库。本身并不实现任何数据加载器的实现逻辑,只界定了数据流走向的标准接口 newLoadStrem -> setup -> beforeLoad -> doLoad -> afterLoad ,调用顺序(不可逆),以及此过程中的异常错误处理机制。

any-loader 支持并实现了以下编程特性:

  • 基于AOP设计,支持异步(Promise)。
  • 中间件形态,不干涉业务逻辑和底层实现。
  • 使用OOP进行扩展,使用继承和方法重载,来进行子类的开发,并提供丰富的方法以控制的粒度。
  • 接口基于 Promise 封装,向后兼容 async/await 语法
  • 数据流(LoadStream)部分,使用 fp 编程,数据流持有的 input, output 等数据,只在接口中流转,结束后即作废。 Loader 本身无状态,不持有过程数据。

本类库代码优先发布 码云 - Gitee,github只用作ci,用中国人自己的代码仓库,专业私有云代码托管。

安装说明

npm install any-loader
yarn add any-loader

使用说明

一般实现了自己的 Loader 后,使用时主要使用 load 方法:

class UserLoader extends Loader {
    doLoad() {
        // do something...
    }
}

const loader = new UserLoader();
loader.load({/* 一些参数 */}).then().catch();

load 方法,主要按照以下流程执行:

newLoadStream() => newInput() => newOutput() => setup(loadStream)
                                                      |
                                           beforeLoad(loadStream) => reject(err)?
                                                      |
                                               doLoad(loadStream) => reject(err)?
                                                      |
                                            afterLoad(loadStream) => reject(err)?

load 过程发生错误时,不管用户在实际代码层面抛出(和reject)任何数据或者异常,进入到 Loader 层面,会将其再次包裹成一个 LoadStreamError 的对象。

const err: LoadStreamError = {
    error: * // 错误异常的实例,可以是任何类型不一定非要是一个Error
    name: string, // 默认值 LoadStreamError
    process: string // 字符串类型,表示错误属于哪个过程中抛出的,[setup|before|doing|after]
    stream: { // 发生错误时的 LoadStream 实例
        input: {},   // 输入参数,最起码应该是一个 plainObject 
        output: {},  // 输出对象,最起码应该是一个 plainObject 
        errors: [],  // 错误堆
        error: LoadStreamError // 当前错误异常
    },
    message: [getter],
}

决定 Loader 在处理是否将异常抛出,由方法 isThrowError 决定。

Loader 可重载的方法说明

setup(loadStream)

// or 

setup({input, output, errors, error})

LoadStream 初始化时的接口,Loader的默认行为,该接口执行过程中的异常错误不会被抛出和reject,不管什么情况都会朝下一个接口去执行(可通过重载 isThrowError 方法来改变其是否抛出setup的异常)。

但在setup中使用 throw 可能会导致 setup 内的结构控制失效,所以请确定的确了解 throw 可能带来的影响。

beforeLoad(loadStream)

load 前置接口,该接口中的抛出错误和reject会阻止下一个接口的执行。

doLoad(loadStream)

load 的实际执行方法,继承的子类应该优先重载该方法以实现 Loader 具体的逻辑。

但目前来说,beforeLoader, doLoad, afterLoad 并没有明确区别,只是先后执行的顺序。

afterLoad(loadStream)

load 的后置接口,参考 beforeLoaddoLoad

newInput(input)

newLoadStream 时候,生成作为 loadinput 输入参数实例,可重载这个方法,以返回实际开发场景中所需要的输入实例。

但最好返回类实例或者一个 plainObject,而不要是值类型(string,boolean, number),函数也不推荐。

newOutput(input, output)

newLoadStream 时候,生成用于承载 loadoutput 输出实例,可重载这个方法,以返回实际开发场景中所需要的输出实例。

newInput ,最好返回一个类实例或者 plainObject。

并且该方法的参数中的 input 必然是基于用户重载 newInput 返回的结果。

isThrowError(error, process)

用于判断是否将某个过程,或者某些特定的 Error 抛出(或reject),用户可重载该方法,使 Loader 彻底成为静默的模式,或者根据 Error 类来决定是否抛出。

isPlainObject(obj)
getPlainObject(obj, defaultObj)
isValidArgs(args)
getValidArgs(args, defaultArgs)

这几个方法的重载,也会对 Loader 的形成产生比较大的影响,属于 Loader 比较基础的方法群, 但一般而言,直接重载 newInputnewOutput 能满足大多数场景使用需求。

不推荐newLoadStreamload 方法重载。

简单的例子

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 => {  
});
// 我们先定义了一个远程的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 => {  
});  

注意事项

setup beforeLoad doLoad afterLoad 中,异常的处理,和异步的异常处理,需要特别注意:

class MyLoader extends Loader {  

    setup(stream) {
        if (true)
            throw new Error('就是任性抛出错误');
        // 因为上面已经抛出错误了,所以后面这里就不会再被执行到了,
        // setup 本身默认是不会抛出异常的,这就会导致 setup 的逻辑没有完全被执行
    }

   beforeLoad(stream) {  
      return new Promise((resolve, reject) => {
          // 这里我偷个懒,以 timeout 模拟一个异步请求
          setTimeout(() => {
              // 这里如果出现异常,应该使用 reject(xxxx) 的方式
              if (stream.input.anything === true) {
                  reject(new Error('就是出错了!'));
              }
              resolve(); // 最终这个 promise 必须有一个明确的 resolve ,否则这个 promise 就一直被挂起的状态(视乎不同的平台实现,还有ES的版本,但就Promise而言,不resolve是错误的。)
              // 特别特别,不要在这样的回调中使用 throw new Error()
              // 这表示这个回调函数抛出了一个异常,但并不直接和 Promise 有直接的关系,这在不同的JS平台,会有不同的处理方式,所以尽量在这里使用 throw
          }, 3000);
      });
   }  
}  

在将一个 new Promise 的实例提供给别人使用时,存在许多技术黑洞和坑(不同JS平台的实现和当前ES的Promise实现的版本的缺陷导致),所以本类库已经转用 bluebird 所提供的 Promise。

mjs和commonjs的问题

mjs 和 commonjs 两个规范这是一个很头疼的问题。

index.js 目前以默认的 commonjs 作为基础输出(用rollup构建)。

也即,如下的代码,都是生效的:

const Loader = require('any-loader'); // 仅在 nodejs 环境下

import Loader from 'any-loader'; // babel 或者 ES 环境下

如果项目使用了 babel-runtime 的,可以考虑引用 any-loader/Loader ,只针对 babel 环境。

import Loader from 'any-loader/Loader'

相应的,在对应项目中的 babel 设置,要针对 exclude 进行如下的设置:

webpack.config.js

const babel = {
    test   : /\.m?jsx?$/,
    exclude: function(modulePath) {
        // 针对 mjs ,不算在排除的范围内
        if (/.mjs$/.test(modulePath)) {
            return false;
        }
        return /node_modules/.test(modulePath);
    },
    use    : {
        loader : 'babel-loader',
        options: {
            presets: [ /* your presets */ ],
            plugins: [ /* your plugins */ ]
        }
    }
};
  • dist/any-loader.bundle.js dist/any-loader.bundle.umd.js 打包所有类库为一个文件
  • dist/any-loader.without-bluebird.js dist/any-loader.without-bluebird.umd.js 不包含 bluebird

TODO

  • 是否将 deepmerge 替换为 lodash/merge ,并打包集成。

Repository

https://gitee.com/janpoem/any-loader.git


相关推荐