为react赋能的concent是什么何以值得一试

43次阅读

共计 14998 个字符,预计需要花费 38 分钟才能阅读完成。

welcome star

你的 star 将是我最大的精神鼓励,欢迎 star????????????

concent 是一个专为 react 提供状态管理服务的框架,提炼现有各大框架的精华,以及社区公认的最佳实践,通过良好的模块设计,既保证 react 的最佳性能又允许用户非常灵活的解耦 UI 逻辑与业务逻辑的关系,从整体上提高代码的 可读性 可维护性 可扩展性

concent 携带以下特性

  • 核心 api 少且简单,功能强大,上手容易,入侵小,容易调试
  • 提供全局模块化的单一数据源
  • 支持 0 入侵的方式,渐进式的重构已有 react 代码
  • 对组件扩展了事件总线、computed、watch、双向绑定等特性
  • 完美支持 function 组件
  • 基于引用定位和状态广播,支持细粒度的状态订阅,渲染效率出众
  • 支持中间件,可以扩展你的个性化插件处理数据变更
  • 支持 react 0.10+ 任意版本;

为用户提供更舒适和简单的 react 编码体验

精心的模块设计理念

state

concent 对模块的定义是经过对实际业务场景反复思考和推敲,最终得出的答案,首先,数据是模块的灵魂,承载着对你的功能模块的最基础的字符描述,离开数据,一切上层业务功能都是空谈,所以 state 是模块里的必包含的定义。

reducer

修改数据的方式灵活度是 concent 提供给用户惊喜之一,因为 concent 的核心是通过接管 setState 做状态管理,所以用户接入 concent 那一刻可以无需立即改造现有的代码就能够享受到状态管理的好处,同样的,concent 也支持用户定义 reducer 函数修改状态,这也是推荐的最佳实践方式,可以彻底解耦 UI 渲染与业务逻辑,因为 reducer 函数本质上只是 setState 的变种写法,所以强调的是总是返回需要更新的片段状态,而且由于 concent 支持 reducer 函数之间相互调用,任意组合,所以可以允许用户按需任意切割 reducer 函数对状态的更新粒度, 然后形成链式调用关系,然后通过 dispatch 句柄来触发 reducer 函数

如果链式调用层级过深,会造成很多次渲染,从上图中可以看出有 3 个函数返回新的片段状态,造成 3 次渲染,所以 concent 也同样提供 lazyDispatch 句柄来让用户可以有多一种选择来触发对 reducer 函数的调用,concent会在调动过程中自动缓存当前调用链上所有属于同一个模块的状态并做合并,直到调用链结束,才做一次性的提交

computed

computed提供一个入口定义需要对发生变化的 key 做计算的函数,通常来说,大部分 state 的数据并非是 UI 渲染直接需要的数据,我们通常需要对其做一些格式化或者转换操作,但是这些操作其实没有必要再每次渲染前都做一遍,computed将只对发生了变化的 key 计算并将其结果缓存起来。

watch

watchcomputed 最大的不同是,不需要返回一个具体的结果,通常用于在关心某些 key 变化时,做一些异步操作,就可以对这些 key 定义 watch 函数

init

我们知道 state 的定义是同步的,init允许用户有一次对 state 做异步获取并改写的机会,注意,如果此时存在着该模块的实例,改写了模块的状态后,concent会自动将这些状态广播到对应的实例上,同样的,如果不存在,在有些的该模块的实例生成时,这些实例将同步到模块最新的状态,所以当我们有一些状态不是需要依赖实例挂载上且触发 componentDidMount 来获取的时候,就可以将状态的初始化提升到模块的 init

灵活的模块和组件映射关系

模块是先于组件存在的概念,当我们有了模块的定义后,便可以对组件提供强有力的支持,concent里通过 register 函数将 react 组件注册为 concent 组件(也称之为 concent 类)

注册的时候,可以指定专属的模块,理论来说,我们应该保持组件与模块干净的对应关系,即一个组件专属于某个模块,消费的是该模块的数据,操作的所属模块的 reducer 函数,但是实际场景可能有不少组件都是跨多个模块消费和修改数据的,所以 concent 也允许用户通过 connect 定义来指定组件连接的其他模块,唯一不同的是调用句柄默认带的上下文是指向自己专属模块的,如果需要调用其他模块的方法,则需要显示指定模块名

@register('Foo', {module:'foo', connect:{bar:'*'}})
class Foo extends Component(){onNameChange = (name)=>{this.$$dispatch('changeName', name);// 默认调用的 foo 模块 reducer 里的 changeName 方法

    this.$$dispatch('bar/changeName', name);// 指定 bar 模块, 调用 bar 模块的 reducer 里的 changeName 方法修改 bar 模块的数据
  }
}


对于 CcClass 来说,因为调用 setState 就能够修改 store,所以数据是直接注入到 state 里的,对于其他模块的数据,是注入到connectedState, 这样既保持了所属模块和其他模块的数据隔离,又能够让用户非常方便消费多个模块的数据。

所以整体来说,组件与 store 之间将构成一张关系明确和清晰的结构网,有利于用户为大型的 react 工程初期整齐的划分业务模块,中期灵活的调整模块定义

更友好的 function 支持

hook 提案落地后,现有的 react 社区,已经从 class component 慢慢转向 function component 写法,但是正如 Vue Function-based API RFC 所说,hook显而易见的要创建很多临时函数和产生大量闭包的问题,以及通过提供辅助函数 useMemo/useCallback 等来解决过度更新或者捕获了过期值等问题,提出了 setup 方案,每个组件实例只会在初始化时调用一次,状态通过引用储存在 setup() 的闭包内。

综合上述的 setup 思路和好处,concent针对 react 的函数组件引入 setup 机制并对其做了更优的改进,同样在在组件实例化时只调用一次,可以定义各种方法并返回,这些方法将收集在上下文的 settings 对象里,还额外的允许 setup 里定义 effectcomputedwatch 函数(当然,这些是实例级别的 computedwatch了)

在线示例

UI 定义

const AwardPanelUI = (props) => {
  return (<div style={stBox}>
      {/** 其他略 */}
      <div>displayBonus: {props.displayBonus}</div>
    </div>
  );
};

setup 定义

const setup = ctx => {
  // 定义副作用,第二位参数写空数组,表示只在组件初次挂载完毕后执行一次
  ctx.defineEffect(ctx => {ctx.dispatch('init');
    // 返回清理函数,组件卸载时将触发此函数
    return () => ctx.dispatch('track', 'user close award panel')
  }, []);

  /** 也支持函数式写法
    ctx.defineWatch(ctx=>{return {...}
    });
   */
  ctx.defineWatch({
    //key inputCode 的值发生变化时,触发此观察函数
    'inputCode':(nevVal)=> ctx.setState({msg:'inputCode 变为'+nevVal})
  });
  ctx.defineComputed({
    //key inputCode 的值发生变化时,触发此计算函数
    'inputCode':(newVal)=>`${newVal}_${Date.now()}`
  });

  // 定义 handleStrChange 方法
  const handleStrChange = (e) => {
    const inputCode = e.currentTarget.value;

    // 两种写法等效
    ctx.dispatch('handleInputCodeChange', inputCode);
    // ctx.reducer.award.handleInputCodeChange(inputCode);
  }

  // 定义 init 函数
  const init = ctx.reducer.award.init;
  //const init = ()=> ctx.dispatch('init');

  //setup 会将返回结果放置到 settings
  return {handleStrChange, init};
}

mapProps 定义

// 函数组件每次渲染前,mapProps 都会被调用,帮助用户组装想要的 props 数据
const mapProps = ctx => {
  // 将 bonus 的计算结果取出
  const displayBonus = ctx.moduleComputed.bonus;
  // 将 settings 里的 handleStrChange 方法、init 方法 取出
  const {handleStrChange, init} = ctx.settings;
  // 将 inputCode 取出
  const {inputCode, awardList, mask, msg} = ctx.moduleState;

  // 从 refConnectedComputed 获取实例对模块 key 的计算值
  const {inputCode:cuInputCode} = ctx.refComputed.award;

  // 该返回结果会映射到组件的 props 上
  return {msg, cuInputCode, init, mask, inputCode, awardList, displayBonus, handleStrChange}
}

连接函数组件

const AwardPanel = connectDumb({setup, mapProps, module:'award'})(AwardPanelUI);

hook 真的是答案吗

有了 setup 的支持,可以将这些要用到方法提升为静态的上下文 api,而不需要反复重定义,也不存在大量的临时闭包问题,同时基于函数式的写法,可以更灵活的拆分和组合你的 U 代码与业务代码,同时这些 setup 函数,经过进一步抽象,还可以被其他地方复用。

同时函数式编程也更利于 typescript 做类型推导,concent对函数组件友好支持,让用户可以在 classfunction之间按需选择,concent还允许定义 state 来做局部状态管理,所以经过 connectDumb 包裹的 function 组件,既能够读写本地状态,又能够读写 store 状态,还有什么更好的理由非要使用 hook 不可呢?

const AwardPanel = connectDumb({
  // 推荐写为函数式写法,因为直接声明对象的话,concent 也会对其做深克隆操作
  //state:()=>({localName:1});
  state:{localName:1},
  setup, 
  mapProps, 
  connect:{award:'*'}
})(AwardPanelUI);

//code in setup
const setup = ctx =>{const changeLocalName = name => ctx.setState({localName});
  return {changeLocalName};
}

//code in mapProps
const mapProps = ctx =>{
  const localName = ctx.state.localName;
  return {localName}; 
}

更加注重使用体验的架构

concent 接入 react 应用是非常轻松和容易的,对于已存在的 react 应用,不需要你修改现有的 react 应用任何代码,只需要先将 concent 启动起来,就可以使用了,不需要在顶层包裹 Provider 之类的组件来提供全局上下文,因为启动 concent 之后,concent自动就维护着一个自己的全局上下文,所以你可以理解 concentreact应用是一个平行的关系,而非嵌套或者包裹的关系,唯一注意的是在渲染 react 应用之前,优先将 concent 启动就可以了。

分离式的模块配置

concent并非要求用户在启动时就配置好各个模块的定义,允许用户定义某些组件时,调用 configure 函数配置模块,这将极大提高定义 page model 或者 component model 的编码体验。

.
|____page
| |____Group
| | |____index.js
| | |____model// 定义 page model
| |   |____reducer.js // 可选
| |   |____index.js
| |   |____computed.js // 可选
| |   |____state.js // 必包含
| |   |____watch.js // 可选
| |   |____init.js // 可选
| |
| |____...// 各种 page 组件定义
|
|____App.css
|____index.js
|____utils
| |____...
|____index.css
|____models// 各种业务 model 的定义
| |____home
| | |____reducer.js
| | |____index.js
| | |____computed.js
| | |____state.js
|
|____components
| |____Nav.js
|
|____router.js
|____logo.png
|____assets
| |____...
|____run-cc.js // 启动 concent,在入口 index.js 里第一行就调用
|____App.js
|____index.js // 项目入口文件
|____services
| |____...

以上图代码文件组织结构为例,page 组件 Group 包含了一个自己的 model, 在model/index.js 里完成定义模块到 concent 的动作,

// code in page/Group/model/index.js
import state form './state';
import * as reducer form './reducer';
import * as computed form './computed';
import * as watch form './watch';
import init form './init';
import {configure} from 'concent';

// 配置模块到 `concent` 里,命名为 'group'
configure('group', {state, reducer, computed, watch, init});

在 Group 组件对外暴露前,引入一下 model 就可以了

import './model';

@register('GroupUI', {module:'group'})
export default class extends Component {}

这种代码组织方式为用户发布携带完整 model 定义的 concent 组件到 npm 成为了可能,其他用户只需安装它的 concent 应用里,安装了该组件就能直接使用该组件,甚至不使用组件的 UI 逻辑,只是注册他新写的组件到该组件携带的模块里,完完全全复用模块的除了 ui 的其他所有定义。

模块克隆

对于已有的模块,有的时候我们想完全的复用里面的所有定义但是运行时是彻底隔离的,如果用最笨的方法,就是完全 copy 目标模块下的所有代码,然后起一个新的名字,配置到 concent 就好了,可是如果有 10 个、20 个甚至更多的组件想复用逻辑但是保持运行时隔离怎么办呢?显然复制多份代码是行不通的,concent提供 cloneModule 函数帮助你完成此目的,实际上 cloneModule 函数只是对 state 做了一个深拷贝,其他的因为都是函数定义,所以只是让新模块指向那些函数的引用。

基于 cloneModule 可以在运行时任意时间调用的特性,你甚至可以写一个工厂函数,动态创解绑定了新模块的组件!

//makeComp.js
import existingModule from './demoModel';
import {register, cloneModule} from 'concent';

const module_comp_= {};// 记录某个模块有没有对应的组件

class Comp extends Component(){//......}

export makeComp(module, CompCcClassName){let TargetComp = module_comp_[module];
  if(TargetComp) return TargetComp;

  // 先基于已有模块克隆新模块
  cloneModule(module, existingModule);

  // 因为 module 是不能重复的,ccClassName 也是不能重复的,// 所有用户如果没有显式指定 ccClassName 值的话,可以默认 ccClassName 等于 module 值
  const ccClassName = CompCcClassName || module;

  // 注册 Comp 到新模块里
  TargetComp = register(ccClassName, {module})(Comp);
  // 缓存起来
  module_comp_[module] = TargetComp;

  return TargetComp;
}

concent 组件工作流程

concent 组件并非魔改了 react 组件,只是在 react 组件上做了一层语法糖封装,整个 react 组件的生命周期依然需要被你了解,而 concentDumb 将原生命周期做了巧妙的抽象,才得以使用 defineEffectdefineWatchdefineComputed 等有趣的功能而无需在关注类组件的生命周期,无需再和 this 打交道,让函数组件和类组件拥有完全对等的功能。

对比主流状态管理方案

我们知道,现有的状态框架,主要有两大类型,一个是 redux 为代表的基于对数据订阅的模式来做状态全局管理,一种是以 mobx 为代表的将数据转变为可观察对象来做主动式的变更拦截以及状态同步。

vs redux

我们先聊一聊 redux,这个当前react 界状态管理的一哥。

redux 难以言语的 reducer

写过 redux 的用户,或者 redux wrapper(如dvarematch 等)的用户,都应该很清楚 redux的一个约定:reducer必需是纯函数,如果状态改变了,必需解构原 state 返回一个新的state

// fooReducer.js
export default (state, action)=>{switch(action.type){
    case 'FETCH_BOOKS':
      return {...state, ...action.payload};
    default:
      return state;
  }
}

纯函数没有副作用,容易被测试的特点被提起过很多次,我们写着写着,对于 actionCreatorreducer,有了两种流派的写法,

  • 一种是将异步的请求逻辑以及请求后的数据处理逻辑,都放在 actionCreator 写完了,然后将数据封装为payload, 调用dispatch,

讲数据派发给对应的reducer

此种流派代码,慢慢变成 reducer 里全是解构 payload 然后合成新的 state 并返回的操作,业务逻辑全部在
actionCreator 里了,此种有一个有一个严重的弊端,因为业务逻辑全部在 actionCreator 里,reducer函数里的 type 值全部变成了一堆类似 CURD 的命名方式,saveXXModelupdateXXModelXXFieldsetXXXdeleteXXX等看起来已经和业务逻辑全然没有任何关系的命名,大量的充斥在 reducer 函数里,而我们的状态调试工具记录的 type 值恰恰全是这些命名方式,你在调试工具里看到变迁过程对应的 type 列表,只是获取到了哪些数据被改变了的信息,但全然不知这些状态是从哪些地方派发了 payload 导致了变化,甚至想知道是那些 UI 视图的什么交互动作导致了状态的改变,也只能从代码的 reducertype关键字作为搜索条件开始,反向查阅其他代码文件。

  • 还有一种是让 actionCreator 尽可能的薄,派发同步的 action 就直接 return,异步的 action 使用 thunk 函数或者 redux-saga 等第三方库做处理,拿到数据都尽可能早的做成 action 对象,派发到 reducer 函数里,

此种模式下,我们的 actionCreator 薄了,做的事情如其名字一样,只是负责产生 action 对象,同时因为我们对数据处理逻辑在 reducer 里了,我们的 type 值可以根据调用方的动机或者场景来命名了,如 formatTimestamphandleNameChangedhandelFetchedBasicData 等,但是由于 redux 的架构导致,你的 ui 触发的动作避免不了的要要经过两个步骤,一步经过 actionCreator 生成 action,第 2 步进过经过reducer 处理 payload 合成新的 state,所以actionCreator 的命名和 reducerType 的命名通常为了方便以后阅读时能够带入上下文信息很有可能会变成一样的命名,如 fetchProductList,在actionCreator 里出现一遍,然后在 reducerType 又出现一遍

concent 化繁为简的 reducer

concent 里 reducer 担任的角色就是负责返回一个新的片段视图,所以你可以认为它就是一个 partialStateGenerator 函数,你可以声明其为普通函数

//code in fooReducer.js
function fetchProductList(){}

也可以是 async 函数或者 generator 函数

async function fetchProductList(){}

如果,你的函数需要几步请求才能完成全部的渲染,但是每一步都需要及时触发视图更新,concent 允许你自由组合函数,如果同属于一个模块里的 reducer 函数,相互之间还可以直接基于函数签名来调用

function _setProductList(dataList){return {dataList};
}

// 获取产品列表计基础数据
async function fetchProductBasicData(payload, moduleState, ctx){const dataList = await api.fetchProductBasicData();
  return {dataList};// 返回数据,触发渲染
  // or ctx.dispatch(_setProductList, dataList);
}

// 获取产品列表计统计数据,统计数据较慢,分批拉取 (伪代码)
async function fetchProductStatData(payload, moduleState, ctx){
  const dataList = moduleState.dataList;
  // 做分批拉取统计数据的 ids,逻辑略...... 
  const batchIdsList = [];
  const len = batchIds.length;

  for(let i=0; i<len; i++){const ids = batchIdsList[i];
    const statDataList = await api.fetchProductBasicData(ids);

    // 逻辑略...... 游标开始和结束,改变对应的 data 的统计数据
    let len = statDataList.length;
    for(let j=0; j<len; j++){dataList[j+cursor].stat = statDataList[j];// 赋值统计数据
    }
    await ctx.dispatch(_setProductList, dataList);// 修改 dataList 数据,触发渲染
  }
}

// 一个完整的产品列表既包含基础数据、也包含统计数据,分两次拉取,其中统计数据需要多次分批拉取
async function fetchProductList(payload, moduleState, ctx){await ctx.dispatch(fetchProductBasicData);
  await ctx.dispatch(fetchProductStatData);
}

现在你只需要视图实例里调用 $$dispatch 触发更新即可

// 属于 product 模块的实例调用
this.$$dispatch('fetchProductList');

// 属于其他模块的实例调用
this.$$dispatch('product/fetchProductList');

可以看到,这样的代码组织方式,更符合调用者的使用直觉,没有多余的操作,相互调用或者多级调用,可以按照开发者最直观的思路组织代码,并且很方便后期不停的调整后者重构模块里的 reducer。

concent 强调返回欲更新的片段状态,而不是合成新的状态返回,从工作原理来说,因为 concent 类里标记了观察 key 信息,reducer 提交的状态越小、越精准,就越有利于加速查找到关心这些 key 值变化的实例,还有就是 concent 是允许对 key 定义 watchcomputed函数的,保持提交最小化的状态不会触发一些冗余的 watchcomputed函数逻辑;从业务层面上来说,你返回的新状态是需要符合函数名描述的,我们直观的解读一段函数时,大体知道做了什么处理,最终返回一个什么新的片段状态给 concent,是符合线性思维的 ^_^,剩下的更新 UI 的逻辑就交给 concent 吧。

可能读者留意到了,redux所提倡的纯函数容易测试、无副作用的优势呢?在 concent 里能够体现吗,其实这一点担心完全没有必要,因为你观察上面的 reducer 示例代码可以发现,函数有无副作用,完全取决于你声明函数的方式,async(or generator)就是副作用函数,否则就是纯函数,你的 ui 里可以直接调用纯函数,也可以调用副作用函数,根据你的使用场景具体决定,函数名就是 type,没有了actionCreator 是不是世界清静了很多?

进一步挖掘 reducer 本质,上面提到过,对于 concent 来说,reducer就是 partialStateGenerator 函数,所以如果讨厌走 dispatch 流派的你,可以直接定义一个函数,然后调用它,而非必需要放置在模块的 reducer 定义下。

function inc(payload, moduleState, ctx){ctx.dispatch('bar/recordLog');// 这里不使用 await,表示异步的去触发 bar 模块 reducer 里的 recordLog 方法
  return {count: moduleState.count +1};
}

@register('Counter', 'counter')(Counter)
class Counter extends Component{render(){return <div onClick={()=> this.$$invoke(inc}>count: {this.state.count}</div>
  }
}

concent不仅书写体验友好,因为 concent 是以引用收集为基础来做状态管理,所以在 concent 提供的状态调试工具里可以精确的定位到每一次状态变更提交了什么状态,调用了哪些方法,以及由哪些实例触发。

redux 复杂的使用体验

尽管 redux 核心代码很简单,提供 composeReducersbindActionCreators 等辅助函数,作为桥接 reactreact-redux提供 connect 函数,需要各种手写 mapStateToPropsmapDispatchToProps等操作,整个流程下来,其实已经让代码显得很臃肿了,所以有了 dvarematchredux wrapper做了此方面的改进,化繁为简,但是无论怎么包装,从底层上看,对于 redux 的更新流程来说,任何一个 action 派发都要经过所有的 reducerreducer 返回的状态都要经过所有 connect 到此 reducer 对应状态上的所有组件,经过一轮浅比较(这也是为什么 redux 一定要借助解构语法,返回一个新的状态的原因),决定要不要更新其包裹的子组件。

const increaseAction = {type: 'increase'};

const mapStateToProps = state => {return {value: state.count}
};

const mapDispatchToProps = dispatch => {
  return {onIncreaseClick: () => dispatch(increaseAction);
  }
};


const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter);

concent 简单直接的上手体验

注册成为 concent 类的组件,天生就有操作 store 的能力,而且数据将直接注入state

//Counter 里直接可以使用 this.$$dispatch('increase')
class Counter extends Component{render(){return <div onClick={()=> this.$$dispatch('increase')}>count: {this.state.count}</div>
  }
}

const App = register('Counter', 'counter')(Counter);

你可以注意到,concent 直接将 $$dispatch 方法,绑定在 this 上,因为 concent 默认采用的是反向继承策略来包裹你的组件,这样产生更少的组件嵌套关系从而使得 Dom 层级更少。

store 的 state 也直接注入了 this 上,这是因为从 setState 调用开始,就具备了将转态同步到 store 的能力,所以注入到 state 也是顺其自然的事情。

当然 concent 也允许用户在实例的 state 上声明其他非 store 的 key,这样他们的值就是私有状态了,如果用户不喜欢 state 被污染,不喜欢反向继承策略,同样的也可以写为

class Counter extends Component{constructor(props, context){super(props, context);
    this.props.$$attach(this);
  }
  render(){
    return(<div onClick={()=> this.props.$$dispatch('increase')}>
        count: {this.props.$$connectedState.counter.count}
      </div>
    )
  }
}

const App = register('Counter', {connect:{counter:'*'}, isPropsProxy:true} )(Counter);

vs mobx

mobx是一个函数响应式编程的库,提供的桥接库 mobx-reactreact变成彻底的响应式编程模式,因为 mobx 将定义的状态的转变为可观察对象,所以
用户只需要修改数据,mobx会自动将对应的视图更新,所以有人戏称 mobxreact变成类似 vue 的编写体验,数据自动映射视图,无需显示的调用 setState 了。

本质上来说,所有的 mvvm 框架都在围绕着数据和视图做文章,react 把单项数据流做到了极致,mobxreact 打上数据自动映射视图的补丁,提到自动映射,自动是关键,框架怎么感知到数据变了呢?mobx采用和 vue 一样的思路,采用 push 模式来做变化侦测,即对数据 gettersetter做拦截,当用户修改数据那一刻,框架就知道数据变了,而 react 和我们当下火热的小程序等采用的 pull 模式来做变化侦测,暴露 setStatesetData接口给用户,让用户主动提交变更数据,才知道数据发生了变化。

concent本质上来说没有改变 react 的工作模式,依然采用的是 pull 模式来做变化侦测,唯一不同的是,让 pull 的流程更加智能,当用户的组件实例创建好的那一刻,concent已知道如下信息:

  • 实例属于哪个模块
  • 实例观察这个模块的哪些 key 值变化
  • 实例还额外连接其他哪些模块

同时实例的引用将被收集到并保管起来,直到卸载才会被释放。

所以可以用 0 改造成本的方式,直接将你的 react 代码接入到concent,然后支持用户可以渐进式的分离你的 ui 和业务逻辑。

需要自动映射吗

这里我们先把问题先提出来,我们真的需要自动映射吗?

当应用越来越大,模块越来越多的时候,直接修改数据导致很多不确定的额外因素产生而无法追查,所以 vue 提供了 vuex 来引导和规范用户在大型应用的修改状态的方式,而 mobx 也提供了 mobx-state-tree 来约束用户的数据修改行为,通过统一走 action 的方式来让整个修改流程可追查,可调试。

改造成本

所以在大型的应用里,我们都希望规范用户修改数据的方式,那么 concent 从骨子里为 react 而打造的优势将体现出来了,可以从 setState 开始享受到状态管理带来的好处,无需用户接入更多的辅助函数和大量的装饰器函数(针对字段级别的定义),以及完美的支持用户渐进式的重构,优雅的解耦和分离业务逻辑与 ui 视图,写出的代码始终还是 react 味道的代码。

结语

concent围绕 react 提供一种了更舒适、更符合阅读直觉的编码体验,同时新增了更多的特性,为书写 react 组件带来更多的趣味性和实用性,不管是传统的 class 流派,还是新兴的 function 流派,都能够在 concent 里享受到统一的编码体验。

依托于 concent 的以下 3 点核心工作原理:

  • 引用收集
  • 观察 key 标记
  • 状态广播

基于引用收集和观察 key 标记,就可以做到热点更新路径缓存,理论上来说,某一个 reducer 如果返回的待更新片段对象形状是不变的,初次触发渲染的时候还有一个查找的过程(尽管已经非常快),后面的话相同的 reducer 调用都可以直接命中并更新,有点类似 v8 里的热点代码缓存,不过 concent 缓存的 reducer 返回数据形状和引用之间的关系,所以应用可以越运行越快,尤其是那种一个界面上百个组件,n 个模块的应用,将体现出更大的优势,这是下一个版本 concent 正在做的优化项,为用户带来更快的性能表现和更好的编写体验是 concent 始终追求的目标。

彩蛋 Ant Design Pro powered by concent ????????????

尽管 concent 有一套自己的标准的开发方式,但是其灵活的架构设计非常的容易与现有的项目集成,此案例将 concent 接入到antd-pro(js 版本的最后一个版本 2.2.0),源代码业务逻辑没有做任何改动,只是做了如下修改,lint-staged 验收通过:

  • 在 src 目录下加入 runConcent 脚本
  • models 全部替换为 concent 格式定义的,因为 umi 会自动读取 model 文件夹定义注入到 dva 里,所以所有 concent 相关的 model 都放在了 model-cc 文件夹下
  • 组件层的装饰器,全部用 concent 替换了dva,并做了少许语法的修改
  • 引入 concent-plugin-loading 插件,用于自动设置 reducer 函数的开始和结束状态
  • 引入 react-router-concent,用于连接react-routerconcent
  • 引入 concent-middleware-web-devtool(第一个可用版本,比较简陋 ^_^),用于查看状态concent 状态变迁过程

注意,运行期项目后,可以打开 console,输入 sss, 查看 store,输入cc.dispatchcc.reducer.**直接触发调用,更多 api 请移步 concent 官网文档查看,更多 antd-pro 知识了解请移步 antd-pro 官网

如何运行

  • 下载源代码
git clone git@github.com:concentjs/antd-pro-concent.git
  • 进入根目录,安装依赖包
npm i
  • 运行和调试项目
npm start

默认 src 目录下放置的是 concent 版本的源代码,如需运行 dva 版本,执行npm run start:old,切换为concent, 执行npm run start:cc

其他

happy coding, enjoy concent ^_^
欢迎 star
_
<div align=”center”>

An out-of-box UI solution for enterprise applications as a React boilerplate.

正文完
 0