时间: 2020-09-03 00:08:26 人气: 2288 评论: 0
传统的redux项目里,我们写在reducer里的状态一定是要打通到store的,我们一开始就要规划好state、reducer等定义,有没有什么方法,既能够快速享受ui与逻辑分离的福利,又不需要照本宣科的从条条框框开始呢?本文从普通的react写法开始,当你一个收到一个需求后,脑海里有了组件大致的接口定义,然后丝滑般的接入到concent世界里,感受渐进式的快感以及全新api的独有魅力吧!

上周天气其实不是很好,记得下了好几场雨,不过北京总部大厦的隔音太好了,以致于都没有感受到外面的风雨飘摇,在工位上正在思索着整理下现有代码时,接到一个普通的需求,大致是要实现一个弹窗。
这是一个非常普通的需求,我相信不少码神看完后,脑海里已经把代码雏形大致写完了吧,嘿嘿,但是还请耐性看完本篇文章,来看看在concent的加持下,你的react应用将如何变得更加灵活与美妙,正如我们的slogan:
concent, power your react
产品同学期望快速见到一般效果原型,而我希望原型是可以持续重构和迭代的基础代码,当然要认真对待了,不能为了交差而乱写一版,所以要快速整理需求并开始准备工作了。
因为项目大量基于antd来书写UI,听完需求后,脑海里冒出了一个穿梭框模样的组件,但因为右侧是一个可拖拽列表,查阅了下没有类似的组件,那就自己实现一个吧,初步整理下,大概列出了以下思路。
ColumnConfModal,基于antd的Modal, Card实现布局,antd的List来实现左侧的选择列表,基于react-beautiful-dnd的可拖拽api来实现右侧的拖拽列表。ColumnConfModal内部。因为注册为concent组件后天生拥有了emit&on的能力,而且不需要手动off,concent在实例销毁前自动就帮你解除其事件监听,所以我们可以注册完成后,很方便的监听openColumnConf事件了。
我们先抛弃各种store和reducer定义,快速的基于class撸出一个原型,利用register接口将普通组件注册为concent组件,伪代码如下
import { register } from 'concent'; class ColumnConfModal extends React.Component { state = { selectedColumnKeys: [], selectableColumnKeys: [], visible: false, }; componentDidMount(){ this.ctx.on('openColumnConf', ()=>{ this.setState({visible:true}); }); } moveToSelectedList = ()=>{ //code here } moveToSelectableList = ()=>{ //code here } saveSelectedList = ()=>{ //code here } handleDragEnd = ()=>{ //code here } render(){ const {selectedColumnKeys, selectableColumnKeys, visible} = this.state; return ( <Modal title="设置显示字段" visible={state._visible} onCancel={settings.closeModal}> <Head /> <Card title="可选字段"> <List dataSource={selectableColumnKeys} render={item=>{ //...code here }}/> </Card> <Card title="已选字段"> <DraggableList dataSource={selectedColumnKeys} onDragEnd={this.handleDragEnd}/> </Card> </Modal> ); } } // es6装饰器还处于实验阶段,这里就直接包裹类了 // 等同于在class上@register( )来装饰类 export default register( )(ColumnConfModal)
可以发现,这个类的内部和传统的react类写法并无区别,唯一的区别是concent会为每一个实例注入一个上下文对象ctx来暴露concent为react带来的新特性api。
因为事件的监听只需要执行一次,所以例子中我们在componentDidMount里完成了事件openColumnConf的监听注册。
根据需求,显然的我们还要在这里书写获取表格列定义元数据和获取用户的个性化列定义数据的业务逻辑
componentDidMount() { this.ctx.on('openColumnConf', () => { this.setState({ visible: true }); }); const tableId = this.props.tid; tableService.getColumnMeta(`/getMeta/${tableId}`, (columns) => { userService.getUserColumns(`/getUserColumns/${tableId}`, (userColumns) => { //根据columns userColumns 计算selectedList selectableList }); }); }
所有的concent实例可以定义setup钩子函数,该函数只会在初次渲染前调用一次。
现在让我们来用setup代替掉此生命周期
//class 里定义的setup加$$前缀 $$setup(ctx){ //这里定义on监听,在组件挂载完毕后开始真正监听on事件 ctx.on('openColumnConf', () => { this.setState({ visible: true }); }); //标记依赖列表为空数组,在组件初次渲染只执行一次 //模拟componentDidMount ctx.effect(()=>{ //service call balabala..... }, []); }
如果已熟悉hook的同学,看到setup里的effectapi语法是不是和useEffect有点像?
effect和useEffect的执行时机是一样的,即每次组件渲染完毕之后,但是effect只需要在setup调用一次,相当于是静态的,更具有性能提升空间,假设我们加一个需求,每次vibible变为false时,上报后端一个操作日志,就可以写为
//依赖列表填入key的名称,表示当这个key的值发生变化时,触发副作用 ctx.effect( ctx=>{ if(!ctx.state.visible){ //当前最新的visible已是false,上报 } }, ['visible']);
关于effect就点到为止,说得太多扯不完了,我们继续回到本文的组件上。
我们希望组件的状态变更可以被记录下来,方便观察数据变化,so,我们先定义一个store的子模块,名为ColumnConf,
定义其sate为
// code in ColumnConfModal/model/state.js export function getInitialState() { return { selectedColumnKeys: [], selectableColumnKeys: [], visible: false, }; } export default getInitialState();
然后利用concent的configure接口载入此配置
// code in ColumnConfModal/model/index.js import { configure } from 'concent'; import state from './state'; // 配置模块ColumnConf configure('ColumnConf', { state, });
注意这里,让
model跟着组件定义走,方便我们维护model里的业务逻辑。
整个store已经被concent挂载到了window.sss下,为了方便查看store,当当当当,你可以打开console,直接查看store各个模块当前的最新数据。
然后我们把class注册为'配置模ColumnConf的组件,现在class里的state声明可以直接被我们干掉了。
import './model';//引用一下model文件,触发model配置到concent @register('ColumnConf') class ColumnConfModal extends React.Component { // state = { // selectedColumnKeys: [], // selectableColumnKeys: [], // visible: false, // }; render(){ const {selectedColumnKeys, selectableColumnKeys, visible} = this.state; } }
大家可能注意到了,这样暴力的注释掉,render里的代码会不会出问题?放心吧,不会的,concent组件的state和store是天生打通的,同样的setState也是和store打通的,我们先来安装一个插件concent-plugin-redux-devtool。
import ReduxDevToolPlugin from 'concent-plugin-redux-devtool'; import { run } from 'concent'; // storeConfig配置略,详情可参考concent官网 run(storeConfig, { plugins: [ ReduxDevToolPlugin ] });
注意哦,concent驱动ui渲染的原理和redux完全不一样的,核心逻辑部分也不是在redux之上做包装,和redux一点关系都没有的^_^,这里只是桥接了redux-dev-tool插件,来辅助做状态变更记录的,小伙伴们千万不要误会,没有redux,concent一样能够正常运作,但是由于concent提供完善的插件机制,为啥不利用社区现有的优秀资源呢,重复造无意义的轮子很辛苦滴(⊙﹏⊙)b......
现在让我们打开chrome的redux插件看看效果吧。
上图里是含有大量的ccApi/setState,是因为还有不少逻辑没有抽离到reducer,dispatch/***模样的type就是dispatch调用了,后面我们会提到。
这样看状态变迁是不是要比window.sss好多了,因为sss只能看当前最新的状态。
这里既然提到了redux-dev-tool,我们就顺道简单了解下,concent提交的数据长什么样子吧
上图里可以看到5个字段,renderKey是用于提高性能用的,可以先不作了解,这里我们就说说其他四个,module表示修改的数据所属的模块名,committedState表示提交的状态,sharedState表示共享到store的状态,ccUniqueKey表示触发数据修改的实例id。
为什么要区分committedState和sharedState呢?因为setState调用时允许提交自己的私有key的(即没有在模块里声明的key),所以committedState是整个状态都要再次派发给调用者,而sharedState是同步到store后,派发给同属于module值的其他cc组件实例的。
这里就借用官网一张图示意下:
所以我们可以在组件里声明其他非模块的key,然后在this.state里获取到了
@register('ColumnConf') class ColumnConfModal extends React.Component { state = { _myPrivKey:'i am a private field value, not for store', }; render(){ //这里同时取到了模块的数据和私有的数据 const {selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey} = this.state; } }
虽然代码能够正常工作,状态也接入了store,但是我们发现class已经变得臃肿不堪了,利用setState怼固然快和方便,但是后期维护和迭代的代价就会慢慢越来越大,让我们把业务抽到reduder吧
export function setLoading(loading) { return { loading }; }; /** 移入到已选择列表 */ export function moveToSelectedList() { } /** 移入到可选择列表 */ export function moveToSelectableList() { } /** 初始化列表 */ export async function initSelectedList(tableId, moduleState, ctx) { //这里可以不用基于字符串 ctx.dispatch('setLoading', true) 去调用了,虽然这样写也是有效的 await ctx.dispatch(setLoading, true); const columnMeta = await tableService..getColumnMeta(`/getMeta/${tableId}`); const userColumsn = await userService.getUserColumns(`/getUserColumns/${tableId}`); //计算 selectedColumnKeys selectableColumnKeys 略 //仅返回需要设置到模块的片断state就可以了 return { loading: false, selectedColumnKeys, selectableColumnKeys }; } /** 保存已选择列表 */ export async function saveSelectedList(tableId, moduleState, ctx) { } export function handleDragEnd() { }
利用concent的configure接口把reducer也配置进去
// code in ColumnConfModal/model/index.js import { configure } from 'concent'; 技术沙龙 教程文章 热点综合