bnorth 数据流

日期: 25 Dec 2018   

bnorth 参考了主流的单向数据流思想,使用 redux 方案,并在 react 升级到 16 以后改用 react 自带的 context 方案进行数据管理。并且创造了数据单元概念,代理了数据的操作,有效的解决了 redux 方案的代码冗余等问题。使数据管理更集中有效,减少代码提供开发效率,同时数据单元有自己的生命周期事件和逻辑控制。

数据管理参考手册

数据单元参考手册

数据流与 redux,启示与问题

react 的思想是单向数据流的思想,从 react 推崇的 flux 到 react 社区推崇的 redux,任何一篇 react 最佳实践类的文章都已详细描述。它给我们带来了启示,Web 应用是一个状态机,视图与状态是一一对应的,然后将所有的状态,保存在一个对象里面,叫做数据仓库。简单的思想配合 react 的虚拟 dom 算法,极大提高了开发的效率。但是 redux 在使用中也存在一些问题,比如:

  1. redux 的思想是简单,但是编码上的体验却并不简单,有点麻烦,有点冗余。比如想在每个页面上加个弹出小帮助按钮的功能,则需要给每个页面建立一个 state,每个页面建立一个 reduxer,每个页面在 connect 里面定义 map 和 action 去触发,其中大部分代码是冗余的
  2. redux 希望自己是纯粹的,所以实现了中间件机制,将很多本来必要的功能外包出去。设计上无可厚非,但是用户使用上就需要额外的选择和学习。比如连基本的异步处理和数据深比较都需要自己选择,十分晦涩对初学者是个很大的负担。

bnorth 的解决方案

redux 的思想是简单纯粹,适应性强,因为 redux 不仅属于 js 和 redux。但是 bnorth 是自己的体系,可以深度包装提供易用性。同时深度定制,提供灵活性。

bnorth 实现数据流分为两个部分,数据管理和数据单元。数据管理是数据仓库相当于是 redux 的 store,数据单元是对数据操作的代理者,相当于是对 state,redexer 和 action 的封装。

Layer 1 数据单元 数据单元 id 属性与方法 数据单元 数据单元 id 属性与方法 数据仓库 数据块... page plugin

数据单元的使用

数据单元的声明

数据单元不能直接构造,而是在能处理数据单元的拥有者的声明函数中声明,并由拥有者负责数据单元的生命周期管理。在拥有者的声明对象中增加一个属性,属性名需要以 state,而属性值是数据单元的声明,是个普通的对象。比如:

/* 在页面的声明中声明了一个名称为 Data,初值为 {time: '12:00'} 的数据单元 */
let Component.controller = {
  stateData: {
    initialization: { time: '12:00' }
  }
}

生命时,除了指定 initialization 初始化数据特定的属性,还可以设定其他属性,数据单元实例能够通过 options 属性访问和使用。

绑定已有的数据单元

如果数据单元的声明对象是一个字符串,则绑定到该字符串为 id 的数据单元上。

/* 页面的 Data 数据单元绑定到父页面的 Data 数据单元上 */
let Component.controller = (app,page){
  stateData: page.getParrentPage().stateData._id,
}

页面对于数据单元的映射

页面的数据单元的数据将自动映射到页面组件的属性上的同名属性中。比如上面上面页面控制器声明了 stateData 数据单元后,页面组件可以直接使用其数据。

let Component = props=>{
  let { stateData } = props;
  return <div>{stateData.time}</div>
}

当页面或者其他逻辑对数据单元进行操作后,数据仓库将发生改变,并触发页面的更新,页面进行了数据更新检测后,触发页面组件的更新。页面组件更新时可以直接描绘无需考虑具体是什么数据,这得益于 react 的算法思想。

数据单元的操作

数据单元的拥有者保有数据单元实例的指针,页面和插件实例可以直接访问数据单元,页面组件也可以通过 page 属性访问页面实例再访问数据单元实例。除此之外,还可以通过数据单元的静态函数,通过名称获取数据单元实例,具体参见参考手册。

数据单元提供了一些属性和方法可以操作相关数据,比如读取数据 data 函数,修改数据的 update 和 delete 函数,清除数据的 clear 函数,同时还提供了以 json path 方式读写数据的 get 和 set 方法。在生命数据单元时,还可以定义其他属性和方法,也可以使用。

数据单元的事件与生命周期

数据单元是由页面或者插件创建与管理,具有如下生命周期事件:

其中 onStateUpdating 事件处理中,如果返回了数据,将修改更新的数据,比如如下代码无论如何修改数据,都将时间固定为 11:00。

let Component.controller = {
  stateData: {},
  onDataUpdating_stateData: ()=>({time: '11:00'}),
}

数据单元拥有者声明对应数据单元的事件处理函数

在页面或者插件的声明对象中设置数据单元数据处理函数,格式为 [数据单元事件名称]_[数据单元名称]: ()=>{}

let Component.controller = {
  stateData: {},
  onDataUpdated_stateData: ()=>{console.log('updated')},
}

数据单元的扩展

数据单元可以通过继承的方式,扩展形成新的具有新特性的数据单元,以实现特定的功能。

定义一个扩展的数据单元

一般是在插件里定义一个继承于 State 的类,并挂载在 App 的实例上,比如实现一个具有数据请求能力的数据单元。事实上 bnorth 已经实现了此类插件 plugin-request,除了实现基本的数据请求外,还与生命周期对接,实现了自动获取等功能。

export default (app, options)=>{
  class Request extends app.State {
    fetch() {
      let xmlhttp = new XMLHttpRequest();
      xmlhttp.onreadystatechange=()=>{
        if (xmlhttp.readyState==4 && xmlhttp.status==200) this.update(xmlhttp.responseText);
      }
      xmlhttp.open("GET",this.options.url,true);
      xmlhttp.send();
    }
  }

  return {
    _id: 'request',
    onPluginMount(app, plugin) {
      app.Request = Request;
    },
  }
}

使用扩展的数据单元

使用上面定义的 Request 数据单元类时,需要通过声明对象的 state 属性,例如:

let Component.controller = (app, page){
  stateData: {
    state: app.Request,
    url: 'xxx',
  }
}

数据单元的定制

声明对象的 state 属性除了可以指定数据单元类,还可以定制数据单元的属性和方法,例如在使用 Request 数据单元时,同时修改其 fetch 函数的默认行为。

let Component.controller = (app, page){
  stateData: {
    state: {
      constructor: app.Request,
      fetch: ()=>{
        console.log('fetching');
        app.Request.prototype.fetch.apply(page.stateData);
      }
    }
    url: 'xxx',
  }
}

页面默认数据单元

如果页面没有声明 stateData ,页面将自动声明一个默认的 stateData 数据单元,可以直接使用.