1.0.2 • Published 6 years ago

gent-data v1.0.2

Weekly downloads
3
License
MIT
Repository
github
Last release
6 years ago

Gent-Data 优雅的数据管理工具/思想

基于RxJS@6,函数式响应式编程,集中管理数据流。

前言1:为什么要gent-data

现在的vuex,redux,mobx等工具都很优秀很成熟了,为什么还要自己写一个?

所谓工具,自己觉得好用的才是最好的。写这个工具的初衷,不是为了追赶潮流,主要是为了整理一个很适合自己使用习惯的数据管理工具,当然如果能帮到他人就更好了。

虽然业界有很多十分强大的状态管理工具,但在实际项目中我们发现,用起来也存在着一些烦恼:

  • 概念相对复杂,使用相对很繁琐,考验团队规范和开发者记忆力
  • 分层太多,逻辑分拆后不是很清晰,小项目使用的时候有大炮打蚊子的感觉
  • 规范太死,不够灵活,比如特殊情况下,我要获取一个action的执行状态,以显示一个loading状态,只能通过定义一个额外的state上的数据。
  • 还有就是,我们不得不针对不同的项目(vue,react等)使用一套完全不同的状态管理工具。

以下是在使用当下流行的状态管理工具时遇到的一些烦恼:

  • Mobx:上手难度相对较大,容易踩坑,虽然各种坑在文档上都已经列出来了,但使用的时候还是感觉容易踩。
  • Flux or Redux:使用起来比较繁琐,各种action,reducer写起来很考验记忆力,对于接盘的人就更痛苦了。虽然可以通过命名规范来解决,但是有点繁琐
  • Vuex:和vue绑定,其他项目没法用。

还有,这些各种工具都有一个共同的烦恼:

  • 我执行一个action,没办法直接获取这个执行的结果(比如我也显示隐藏loading)。单向数据流没错,但很多情况为了一个action的执行情况去额外定义一个state状态属性。

前言2:gent-data数据管理思想和原则

gent-data设计的几个目标:

  • 化繁为简,使用简单,概念简单。
  • 设计灵活,便于扩展和集成。
  • 业务逻辑清晰,使用的时候不要记很多东西,便于调试。
  • 一套数据,适用于各种场景(React,Vue,纯JS等……),很方便与各种工具集成。

gent-data里,我们发现,所有数据管理归纳起来,无外乎都可以划分为以下几部分:

  • 【数据存储】:将数据存在一个地方,供之后或者其他消费者取出使用,它们有:自定义store(比如vuex的store), LocalStorage, SessionStorage, Cookie 等。
  • 【数据流动、和变换】:一个事件激发一个数据,这个数据沿着一条或者多条路径不断改变、传输,最终到达目的地。比如,点击按钮=>参数=>服务器=>数据=>store改变=>通知页面更新
  • 【变更通知】:数据或者发起的时候通知相关的消费者。

那么,我们就针对这两部分来管理就行了:

  • gent-data(rxjs): 解决数据流动变换, RxJS有着响应式数据流管理的很强大的功能,gent-data是一个扩展工具,帮助我们在rxjs的基础上更好的管理数据流。
  • gent-store: 解决数据存储。
  • 对于变更通知的问题,gent-storegent-data都有涉及。

安装使用

安装:

npm i gent-data rxjs -S

使用:

import {
  logMiddleware,
  observePromise,
  observeValue,
  sourceToObservable,
  concatMapSource,
  mergeMapSource,
  switchMapSource,
  FlowGroup
} from 'gent-data';

store:gent-store的情况gent-store文档

开始使用之前,先理解一下两个基本的RxJS的概念。

Observable: 可观察对象。

  • 描述一个未来的,有一定发射规律的,可调用的,数据或者事件的集合。
  • 一个Observable可被订阅(subscribe)N次,每次被订阅即产生一个独立的执行(subscription),然后开始按照规律发射数据。
  • 一次执行可以发射多个数据(next),期间可能发射next,error,complete三个事件。
  • 执行可以被取消(unsubscribe)。
  • 具体参见RxJS Observable 或者 RxJs Beta Docs

Observer: 数据观察者。

  • 描述数据的接收者。
  • 一个观察者是一个有三个回调函数的对象,用来监听数据: { next(value){}, error(err){}, complete(){} }
  • 具体参见RxJS Observer 或者 RxJs Beta Docs

    • next: 接收每一个数据。
    • error: 错误时调用。
    • complete: 完成时调用。

然后,再了解下gent-data加入很少的几个概念。

Dynamic Source: 动态数据源,

  • 一个动态数据源,是一个可以根据传入条件,动态制造可观察新数据的机器。
  • 动态数据源是一个制造数据的方法,每次调用接收两个参数,resource:制造数据的资源,observer:数据的观察者。
  • 动态数据源可以被多次执行。
  • 每次执行可发送多个数据。
  • 执行可以被取消(可选)。
  • 具有Observable的特性(发送多个数据,有next,error,complete三个事件,可以取消执行...),可以很方便地转换为Observable

下面是一个动态数据源的例子:每次执行,根据传入的page,count等参数,发射一个从服务器拉取的用户列表数据。

import request from 'superagent';
import { observePromise } from 'gent-data';

// 获取用户列表
function userList(resource, observer) {
  const params = {page: 1, count: 20, ...resource};
  const req = request.post('/user')
    .send(params);

  // 将promise 和观察者绑定,根据promise的状态自动调用observer的方法
  observePromise(req, observer);

  // 可以返回一个取消执行的方法
  return function cancel() {
    req.abort();
  }
}

// 等价于:
// 获取用户列表
function userList(resource, observer) {
  const params = {page: 1, count: 20, ...resource};
  const req = request.post('/user')
    .send(params);

  req
    .then(v => {
      observer.next(v);
      observer.complete();
    })
    .catch(err => {
      observer.error(err);
      observer.complete();
    });

  // 可以返回一个取消执行的方法
  return function cancel() {
    req.abort();
  }
}

下面是将动态数据源转换为Observable的例子:

import { userList } from '../source/user';
import { sourceToObservable } from 'gent-data';

// 转换
let observable = sourceToObservable(userList, {page: 1, count: 10});

// 订阅
let subscription = observable.subscribe({
  next(data) {
    //
  },
  error(err) {
    //
  },
  complete() {
    //
  }
});

// 可以取消订阅
subscription.unsubscribe();
// 取消订阅的时候如果这个source还没有完成,会调用上面source返回的cancel方法:
// return function cancel() {
//   req.abort();
// }

Flow: 数据流。

下面pipe的操作请参考RxJS pipe文档

  • 定义一个有一定规律的,数据流动变换的轨迹,比如点击按钮获取用户列表,并加入store,更新界面的一系列数据流动,可以看作一个数据流。
  • 每个数据流是一个方法,所做的工作就是将一个Observable转换为另外一个Observable
  • 接受参数为,变换前的Observable
  • 返回结果为,变换后的Observable
  • 每个数据流可以进行多次数据变换,也可以流过多个数据源。
  • flow只是一定数据流动变换的集合,一个操作反馈中的数据变动,可以用一个flow,也可以了流经多个flow
import { Observable } from 'rxjs';
import { map, filter, scan, concatMap } from 'rxjs/operators';
import { sourceToObservable } from 'gent-data';
import { userList } from '../source/user';

// 订阅获取用户列表的数据流,just a sample
function getUserListFlow(observable) {
  return observable
    .pipe(
      map((args) => { 
        args.keywords += ' xxx';
        return args;
      })
    )
    .pipe(
      concatMap((v) => {
        return sourceToObservable(userList, v);
      })
    );
}

// 调用flow
let params = { page: 1, count: 20, keywords: 'test' };
let observable = Observable.from([params]);

// 流变换
observable = getUserListFlow(observable);

// 订阅数据流
let subscription = observable.subscribe({
  next(users) {
    console.log(users)
  },
  error(err) {
    //
  },
  complete() {
    //
  }
});

FlowGroup:数据流集合。

  • 数据流集合主要用来将一组数据流集中起来,便于管理。
  • 一个数据流集合可以为添加在上面的每条数据流flow,统一添加前置和后置中间件,以便于打印调试和统一变换。
  • 可以将动态数据源直接添加到上面成为数据流
  • 一个项目可以使用多个FlowGroup

推荐:小型项目,一个项目使用一个FlowGroup, 大型项目,一个模块一个FlowGroup

import { FlowGroup } from 'gent-data';

const testFlows = new FlowGroup({
  name: 'test', // 便于调试
  beforeMiddlewares: [],
  afterMiddlewares: []
});

Middleware: 中间件。

  • 一个中间件也是一个数据流flow,但是flow作为中间件的时候会额外接收一个参数。

下面是一个简单的打印日志的中间件:

function logFlow(observable, middlewareOptions={}) {
  let {
    // 分组名
    groupName,
    // 流的名字
    flowName,
    // 中间件类型:before / after
    middleware
  } = middlewareOptions;

  // 不是作为中间件,而是直接作为flow调用时
  if (!middleware) return observable;

  return observable
    .pipe(
      map((data) => { 
        if (middleware == 'before') {
          console.log(`${groupname}.${flowName} in:`);
        }else if (middleware == 'after') {
          console.log(`${groupname}.${flowName} out:`);
        }

        console.log(JSON.stringify(data))
      })
    );

API

  • Dynamic Source: 动态数据源,一个函数,接收两个参数, res(数据源产生的条件或者资源),observer(数据的观察者),可以选择性返回一个用于终止数据源的方法。
export function canCancelSource(res, observer) {
  let timer = null;
  let promise = new Promise((resolve, reject) => {
    timer = setTimeout(() => {
      resolve(`${res}-cancel`);
    }, 2000);
  });

  observePromise(promise, observer);

  return function cancel() {
    clearTimeout(timer);
  }
}
  • observePromise(promise, observer): 将promise实例观察者绑定,resolve或者reject的时候自动调用observer.nextobserver.error, 并且调用observer.complete
import { observePromise } from 'gent-data';

export function promiseSource(res, observer) {
  let promise = Promise.resolve(`${res}-promise`);
  observePromise(promise, observer);
}
  • observeValue(value, observer): 将一个值观察者绑定,自动调用observer.nextobserver.complete
import { observeValue } from 'gent-data';

export function syncSource(res, observer) {
  observeValue(`${res}-sync`, observer);
}
  • sourceToObservable(source, res): 将source转换为Observable,转换的时候里面的数据发射逻辑并不会会执行,只有observable被订阅(subscribe)的时候才会执行。
function userListSource(resource, observer) {
  const params = {page: 1, count: 20, ...resource};
  const req = request.post('/user')
    .send(params);

  // 将promise 和观察者绑定,根据promise的状态自动调用observer的方法
  observePromise(req, observer);

  // 可以返回一个取消执行的方法
  return function cancel() {
    req.abort();
  }
}

// 转换的为Observable的时候,请求还未发出
let observable = sourceToObservable(userListSource, { page: 1, count: 10 });

// userListSource执行一次,发出请求
let subscription1 = observable.subscribe({
  //...
});

// userListSource再执行一次,发出另一个请求
let subscription2 = observable.subscribe({
  //...
});

// 终止1发出的请求 req.abort();
subscription.unsubscribe();
  • concatMapSource(source): 将source转换为数据流flow。调用这个flow的时候,流入的observable每一个数据,会根据source生成一个新的observable,然后通过concatMap操作符将这些observable连接为一个流出的observable
import { of } from 'rxjs';
import { concatMapSource } from 'gent-data';
import { userListSource } from '../source/user';

let observable = of({page: 1, count: 10});
let userListFlow = concatMapSource(userListSource);

let newObservable = userListFlow(observable);
// 还可以应用更多flow:
// newObservable = ooxxFlow(newObservable); 

let subscription = newObservable.subscribe({
  // ...
});
  • mergeMapSource(source): 和concatMapSource一样,只不过通过mergeMap操作符连接observable。
  • switchMapSource(source): 和concatMapSource一样,只不过通过switchMap操作符连接observable。

  • logMiddleware: 日志中间件,打印日志信息。

FlowGroup

FlowGroup流程图.

  • 创建实例
import { FlowGroup, logMiddleware } from 'gent-data';

const myFlows = new FlowGroup({
  name: 'MyFlows',
  beforeMiddlewares: process.env.NODE_ENV === 'production' ? [] : [logMiddleware],
  afterMiddlewares: process.env.NODE_ENV === 'production' ? [] : [logMiddleware]
});

import { testFlow } from '../flows/test';
import * as TestSoureces from '../sources/test';

// 添加flow
myFlows.addFlow(testFlow, 'test');

// 批量添加source
myFlows.addSources(TestSoureces, 'ooxx');

export { myFlows };
  • 使用
import { of } from 'rxjs';
import { myFlows } from '../flowGroups/myFlows';

let observable = of('hello');
// 应用test flow
observable = myFlows.flow('test')(observable);
// 应用 source.test flow
observable = myFlows.flow('source.test')(observable);

observable.subscribe({
  //
});
  • group.addFlow(fn, name="Anonymous"): 注册flow到group, fn: flow方法。
  • group.addFlows(flowMap={}, context=""): 批量注册flow,以keyname,如果context不为空,则namecontext.key
  • group.addSource(source, name="Anonymous", operatorType="concatMap"):将source转换为flow,再注册到group,operatorType[concatMap,mergeMap,switchMap]中的一个,默认concatMap
  • group.addSources(sourceMap, context="", operatorType="concatMap"): 将一组source转换为flow,再注册到group,默认concatMap
  • group.setBeforeMiddlewares(middlewares=[]): 设置前置中间件。
  • group.setAfterMiddlewares(middlewares=[]): 设置后置中间件。
  • group.flow(name): 获取中间件。

推荐目录结构

---- data
  |---- sources
  |---- flows
  |---- flowGroups

辅助工具

  • gent-store
import mainStore from '../stores/main';

export function storeSource(res, observer) {
  return mainStore.subscribe(observer);
}
  • gent-vue
  • gent-react

完整实例

  • gent-demo
1.0.2

6 years ago

1.0.1

6 years ago

1.0.0

6 years ago