传统的redux项目里,咱们写在reducer里的状态必定是要打通到store的,咱们一开始就要规划好state、reducer等定义,有没有什么方法,既可以快速享受ui与逻辑分离的福利,又不须要照本宣科的从条条框框开始呢?本文从普通的react写法开始,当你一个收到一个需求后,脑海里有了组件大体的接口定义,而后丝滑般的接入到concent世界里,感觉渐进式的快感以及全新api的独有魅力吧!
上周天气其实不是很好,记得下了好几场雨,不过北京总部大厦的隔音太好了,以至于都没有感觉到外面的摇摇欲坠,在工位上正在思索着整理下现有代码时,接到一个普通的需求,大体是要实现一个弹窗。react
这是一个很是普通的需求,我相信很多码神看完后,脑海里已经把代码雏形大体写完了吧,嘿嘿,可是还请耐性看完本篇文章,来看看在concent的加持下,你的react
应用将如何变得更加灵活与美妙,正如咱们的slogan: git
concent, power your reactes6
产品同窗指望快速见到通常效果原型,而我但愿原型是能够持续重构和迭代的基础代码,固然要认真对待了,不能为了交差而乱写一版,因此要快速整理需求并开始准备工做了。github
由于项目大量基于antd
来书写UI,听完需求后,脑海里冒出了一个穿梭框模样的组件,但由于右侧是一个可拖拽列表,查阅了下没有相似的组件,那就本身实现一个吧,初步整理下,大概列出了如下思路。chrome
ColumnConfModal
,基于antd
的Modal
, Card
实现布局,antd
的List
来实现左侧的选择列表,基于react-beautiful-dnd
的可拖拽api来实现右侧的拖拽列表。ColumnConfModal
内部。1 moveToSelectedList(移入到已选择列表 )
2 moveToSelectableList(移入到可选择列表)
3 saveSelectedList(保存用户的已选择列表)
4 handleDragEnd(处理已选择列表顺序调整完成时)
5 其余略.....redux
由于注册为concent
组件后天生拥有了emit&on
的能力,并且不须要手动off
,concent
在实例销毁前自动就帮你解除其事件监听,因此咱们能够注册完成后,很方便的监听openColumnConf
事件了。后端
咱们先抛弃各类store和reducer定义,快速的基于class
撸出一个原型,利用register
接口将普通组件注册为concent
组件,伪代码以下api
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
的监听注册。antd
根据需求,显然的咱们还要在这里书写获取表格列定义元数据和获取用户的个性化列定义数据的业务逻辑
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
里的effect
api语法是否是和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'; import * as reducer from 'reducer'; import state from './state'; // 配置模块ColumnConf configure('ColumnConf', { state, reducer, });
还记得上面的setup
吗,setup
能够返回一个对象,返回结果将收集在settiings
里,如今咱们稍做修改,而后来看看class吧,世界是否是清静多了呢?
import { register } from 'concent'; class ColumnConfModal extends React.Component { $$setup(ctx) { //这里定义on监听,在组件挂载完毕后开始真正监听on事件 ctx.on('openColumnConf', () => { this.setState({ visible: true }); }); //标记依赖列表为空数组,在组件初次渲染只执行一次 //模拟componentDidMount ctx.effect(() => { ctx.dispatch('initSelectedList', this.props.tid); }, []); return { moveToSelectedList: (payload) => { ctx.dispatch('moveToSelectedList', payload); }, moveToSelectableList: (payload) => { ctx.dispatch('moveToSelectableList', payload); }, saveSelectedList: (payload) => { ctx.dispatch('saveSelectedList', payload); }, handleDragEnd: (payload) => { ctx.dispatch('handleDragEnd', payload); } } } render() { //从settings里取出这些方法 const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = this.ctx.settings; } }
react
社区轰轰烈烈推进了Hook
革命,让你们逐步用Hook
组件代替class
组件,可是本质上Hook
逃离了this
,精简了dom
渲染层级,可是也带来了组件存在期间大量的临时匿名闭包重复建立。
来看看concent
怎么解决这个问题的吧,上面已提到setup
支持返回结果,将被收集在settiings
里,如今让稍微的调整下代码,将class
组件吧变身为Hook
组件吧。
import { useConcent } from 'concent'; const setup = (ctx) => { //这里定义on监听,在组件挂载完毕后开始真正监听on事件 ctx.on('openColumnConf', (tid) => { ctx.setState({ visible: true, tid }); }); //标记依赖列表为空数组,在组件初次渲染只执行一次 //模拟componentDidMount ctx.effect(() => { ctx.dispatch('initSelectedList', ctx.state.tid); }, []); return { moveToSelectedList: (payload) => { ctx.dispatch('moveToSelectedList', payload); }, moveToSelectableList: (payload) => { ctx.dispatch('moveToSelectableList', payload); }, saveSelectedList: (payload) => { ctx.dispatch('saveSelectedList', payload); }, handleDragEnd: (payload) => { ctx.dispatch('handleDragEnd', payload); } } } const iState = { _myPrivKey: 'myPrivate state', tid:null }; export function ColumnConfModal() { const ctx = useConcent({ module: 'ColumnConf', setup, state: iState }); const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = ctx.settings; const { selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey } = ctx.state; // return your ui }
在这里要感谢尤雨溪老师的这篇Vue Function-based API RFC,给了我很大的灵感,如今你能够看到因此的方法的都在setup
里定义完成,当你的组件不少的时候,给gc减少的压力是显而易见的。
因为二者的写法高度一致,从class
到Hook
是否是很是的天然呢?咱们其实不须要争论该用谁更好了,按照你的我的喜爱就能够,就算某天你看class
不顺眼了,在concent
的代码风格下,重构的代价几乎为0。
上面咱们定义了一个on事件openColumnConf
,那么咱们在其余页面里引用组件ColumnConfModal
时,固然须要触发这个事件打开其弹窗了。
import { emit } from 'concent'; class Foo extends React.Component { openColumnConfModal = () => { //若是这个类是一个concent组件 this.ctx.emit('openColumnConfModal', 3); //若是不是则能够调用顶层api emit emit('openColumnConfModal', 3); } render() { return ( <div> <button onClick={this.openColumnConfModal}>配置可见字段</button> <Table /> <ColumnConfModal /> </div> ); } }
上述写法里,若是有其余不少页面都须要引入ColumnConfModal
,都须要写一个openColumnConfModal
,咱们能够把这个打开逻辑抽象到modalService
里,专门用来打开各类弹窗,而避免在业务见到openColumnConfModal
这个常量字符串
//code in service/modal.js import { emit } from 'concent'; export function openColumnConfModal(tid) { emit('openColumnConfModal', tid); }
如今能够这样使用组件来触发事件调用了
import * as modalService from 'service/modal'; class Foo extends React.Component { openColumnConfModal = () => { modalService.openColumnConfModal(6); } render() { return ( <div> <button onClick={this.openColumnConfModal}>配置可见字段</button> <Table /> <ColumnConfModal /> </div> ); } }
以上代码在任何一个阶段都是有效的,想要了解渐进式重构的在线demo能够点这里,更多在线示例列表点这里
因为本篇主题主要是介绍渐进式
重构组件,因此其余特性诸如sync
、computed$watch
、高性能杀手锏renderKey
等等内容就不在这里展开讲解了,留到下一篇文章,敬请期待。
若是看官以为喜欢,就来点颗星星呗,concent
致力于为react
带来全新的编码体验和功能强化,敬请期待更多的特性和生态周边。