C_C welcom to cc world
quick-start demo: github.com/fantasticso…
简介
- 硝烟四起
众所周知,react本身只是非常优雅的解决了视图层渲染工作,但是随着应用越来越大,庞大的react组件群体之间状态相互其实并不是孤立的,需要一个方案管理把这些状态集中管理起来,从而将model和view的边界划分得更加清楚,针对于此,facebook官方对于react状态管理给了一个flux架构并有一套自己的实现,但是社区里并不满足于此,基于对flux的理解各个第三方做着给出了的自己的解决方案,状态管理框架的战争从此拉开序幕,随着redux横空出世,大家默默接受了redux的 dispatch action、hit reducer、comibine new state、render new view的理念,在redux世界里,组件需要关心的状态变化都由props注入进来,connect作为中间的桥梁将react组件与redux关联起来,通过mapStateToProps将redux里定义好的state映射到组件的props上,以此达到让react组件订阅它需要关心的state变化的目的。
- 一统天下
随着redux生态逐渐完善,大家默认的把redux当做了react状态管理的首选解决方案,所以redux已经在react状态管理框架里一统天下,通过github star发现,另一个流行的状态管理框架mobx页已经锁定第二的位置,用另一种思路来给大家展示原来状态可以这么管理,状态管理的格局似乎基本已经洗牌完成,可是作为redux重度使用者的我并不满足与此,觉得在redux世界里,通过props层层穿透数据,通过provider包裹整个react app应用,只是解决了状态的流动问题,而组件的通信任然非常间接与尴尬,依靠redux来完成不是不可以,只是对比vue,任然觉得少了些什么......
- why cc
react-control-center并不是简单的立足于状态管理,而是想为你提供更多的有趣玩法,因为现有的状态管理解决方案已经非常成熟(但是在某些场景未必真的好用),所以cc从一开始设计就让其api对现有的组件入侵非常之小,你可以在redux项目里局部使用cc来把玩cc的状态管理思路,可以从一个组件开始,慢慢在开始渐进式的修改到其他地方,仅仅使用一个register函数,将你的react类注册为cc类,那么从cc类生成的cc实例,将给你带来以下新的特性、新的概念、新的思路。
1 所有cc实例都拥有 emit 和 on 的能力,无论组件间嵌套关系多复杂,实现组件间通信将会是如此轻松。
实际上实例还拥有更精准的emitIdentity, emitWith, onIdentity方法,让用户基于更精准的力度去发射或者接收。
emitIdentity(eventName:string, identity:string, ...args), 第一位参数是事件名,第二位参数是认证串,剩余参数是on的handler函数的实际接收参数,当很多相同组件(如以CcClass:BookItem生成了多个CcInstance)订阅了同一个事件,但是你只希望通知其中一个触发handler调用时,emitIdentity就能派上用场了。
onIdentity(eventName:string, identity:string, hancler:function),监听emitIdentity发射的事件。
emitWith(eventName:string, option?:{module?:string, ccClassKey?:string, identity?:string})从一个更精准的角度来发射事件,寻找指定模块下,指定cc类名的,指定identity的监控函数去触发执行,具体过程这里先略过,先看下面关于模块和cc类的介绍,在回过头来理解这里更容易。
off(eventName:string, option?:{module?:string, ccClassKey?:string, identity?:string}),取消监听。
这些函数在cc的顶层api里都有暴露,当你的cc app运行起来之后,你可以打开console,输入cc并回车,你会发现这些函数已经全部绑定在window.cc对象下了,你可以直接调用他们来完成快速验证哦,而非通过ccInstance去触发^_^
import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';
@cc.register('Foo')
class Foo extends Component{
componentDidMount(){
this.$$on('fooSignal',(signal, from)=>{
this.setState({signal, from});
});
//cc是不允许一个cc实例里对同一个事件名监听多次的,这里fooSignal监听了两次,cc会默认使用最新的监听函数,所以上面个监听变成了无效的监听
this.$$on('fooSignal',(signal, from)=>{
this.setState({signal, from:`--${from}--`});
});
this.$$on('fooSignalWithIdentity', 'xxx_id_wow',()=>{
this.setState({signal, from});
})
}
}
@cc.register('Bar')
class Bar extends Component{
render(){
<div>
<button onClick={()=>this.$$emit('fooSignal', 'hello', 'Bar')}>emit</button>
<button onClick={()=>this.$$emit('fooSignal', 'xxx_id_wow', hello', 'Bar')}>emitIdentity</button>
<button onClick={()=>this.$$off('fooSignal')}>off event fooSignal</button>
</div>
}
}
复制代码
2 所有cc实例都可以针对自己的state的任意key定义 computed 函数,cc会在key的值发生变化自动计算新的computed值并缓存起来,在实例里定义的computed会收集到实例的refComputed对象里。
import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';
@cc.register('Foo')
class Foo extends Component{
constructor(props, context){
super(props, context);
this.state = {woo:'woo cc!'};
}
$$computed(){
return {
wow(wow){
return `computed wow ${wow}`;
}
}
}
componentDidMount(){
this.$$on('fooSignal',(signal, from)=>{
this.setState({signal, from});
});
}
changeWow = (e)=>{
this.setState({wow: e.currentTarget.value});
}
render(){
return (
<div>
<span>{this.state.wow}</span>
<span>{this.$$refComputed.wow}</span>
<input value={this.state.wow} onChange={this.changeWow}/>
</div>
);
}
}
复制代码
3 注册为cc类的时候,为该cc类设定了一个该cc类所属的 模块 ,并通过sharedStateKeys声明关心该模块里哪些key(可以是任意的key,也可以是这个模块的所有key)的变化,则由改cc类产生的cc实例共同监听着这些key对应值的变化,任何一个cc实例改变了这些sharedStateKeys里的值,其他cc实例都能感知到它的变化并自动被cc触发渲染。
import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';
class Foo extends Component{
render(){
return <div>any jsx fragment here</div>
}
}
//将Foo注册为一个共享FooOfM1模块所有key变化的cc类FooOfM1
const FooOfM1 = cc.register('FooOfM1', {module:'M1', sharedStateKeys:'all'})(Foo);
//将Foo注册为一个共享FooOfM2模块key1和key2变化的cc类FooOfM2
const FooOfM2 = cc.register('FooOfM2', {module:'M2',sharedStateKeys:['key1','key2']})(Foo);
//将Foo注册为一个共享FooOfM2模块key1和key2变化,且共享global模块g1变化的cc类FooOfM2G
const FooOfM2G = cc.register('FooOfM2', {module:'M2',sharedStateKeys:['key1','key2','key3'],globalStateKeys:['g1']})(Foo);
//不设定任何参数,只写cc类名,cc会把Foo注册为一个属于default模块的cc类
const JustWantToOwnCcAbility = cc.register('JustWantToOwnCcAbility')(Foo);
//cc同时也为register提供简写函数
//const FooOfM2G = cc.r('FooOfM2',{m:'M2',s:['key1','key2','key3'],g:['g1']})(Foo})
复制代码
4 注意在3里我们提到一个概念 模块,对于cc来说一个完整的模块包括以下属性:state、reducer、init、computed,这些参数都是调用cc.startup时注入,注意,cc虽然不需要用户像redux那样要给顶层App组件包裹一层<Provider/>但是要求用户在app入口文件的第一句话那里触发cc.startup 让整个cc运行起来,store、reducer、init、computed就是cc.startup需要的参数
- store是一个object对象,store里的各个key就表示模块名,对应的值就是各个模块对应的state,一个cc实例除了setState方法能够触发修改state,还可以通过dispatch方法派发action对象去修改state,此时具体的数据合成逻辑就体现在下面要说的reducer里了
- recuder是一个object对象,recuder里的各个key表示reducer的模块名,通常用户可以定义和state的模块名保持一致,但是可以定义另外的模块名,所以这里的模块指的是reducerModuel,不强求用户定义时和stateModule保持一致,stateModule对应的值一个普通的json对象,key为函数名,值为处理函数,即处理旧state并合成新state的方法,cc支持函数为普通函数、生成器函数、async函数。
上面提到了dispatch函数需要传递一个action对象,一个标准的action必须包含type、payload 2个属性,表示cc要去查recuder里某个模块下type映射函数去修改某个模块的state,具体是什么模块的type映射函数和什么模块对应的state,参见action剩余的两个可缺省的属性module和reducerModule的设定规则,注意,这里再一次提到了reducerModule,以下规则就体现了为什么cc允许reducer模块名可以*定义:
不指定module和reducerModule的话,cc去查reducer里当前cc实例所属模块下的type映射函数去修改当前cc实例所属模块的state。
指定了module,而不指定reducerModule的话,cc去查reducer里module下的type映射函数去修改module模块的state。
不指定module,指定reducerModule的话,cc去查reducer里reducerModule下type映射函数去修改当前触发dispatch函数的cc实例所属的module模块的state。 指定了module,同时也指定了reducerModule的话,cc去查reducer里reducerModule下type映射函数去修改module模块的state。
之所以这样设计是因为考虑到让用户可以*选择reducer的模块描述方式,因为对于cc来说,dispatch派发的action只是为了准确找到reducer里的处理函数,而reducer的模块定义并不需要强制和state保持一致给了用户更多的选择去划分reducer的领域
- init是一个object对象,key是模块名,严格对应stateModule,值是一个函数,如果用户为某个模块定义了init函数表示用户希望有机会再次初始化某个模块的state,通常是异步请求后端来的数据重新赋值给模块对应的state
- computed是一个object对象,key是模块名,严格对应stateModule,值是一个moduleComputedObject,moduleComputedObject的key指的就是某个module的某个key,value就是为这个key定义的计算函数,函数的第一为参数就是key的原始值,cc实例里通过moduleComputed对象取到计算后的新值,特别地,为global模块定义的moduleComputedObject对象,在cc实例里通过globalComputed对象取到计算后的新值
//code in index.js
import api from '@foo/bar/api';
cc.startup({
isModuleMode:true,//表示cc以模块化方式启动,默认是false,cc鼓励用户使用模块化管理状态,更容易划分领域的边界
store:{
$$global{//$$global是cc的内置模块,用户如果没有显式的定义,cc会自动注入一个,只不过是一个不包含任何key的对象
themeColor:'pink',
},
m1:{
name:'zzk',
age:30,
books:[],
error:'',
},
m2:{
wow:'wow',
signal:'haha',
}
},
reducer:{
m1:{
//state表示调用dispatch的cc实例对应的state,moduleState只描述的是cc实例所属的模块的state,更多的解释看下面的4 5 6 7 8这些点。
//特别的注意,如果该方法是因为某个reducer的函数里调用的dispatch函数而被触发调用的,此时的state始终指的是最初的那个在cc实例里触发dispatch时那个cc实例的state,而moduleState始终指向的是指定的module的的state!!!
changeName:function({payload,state,moduleState,dispatch}){
const newName = payload;
dispatch({module:'m2',type:'changeSignal',payload:'wow!dispatch in reducer function block'});
return {name:newName};
},
//支持生成器函数
changeAge:function*({payload,state,moduleState,dispatch}){
const newAge = payload;
const result = yield api.verifyAge(newAge);
if(result.error)return({error:result.error});
else return {name:newName};
},
//支持async
changeAge:async function({payload:{pageIndex,pageSize}}){
const books = yield api.getBooks(pageIndex, pageSize);
return {books};
}
},
m2:{
changeSignal:function({payload:signal,dispatch}){
//注意m1/changeName里指定了修改m2模块的数据,其实这里可以一次性return {signal, wow:'just show reducerModule'}来修改数据,
//但是故意的调用dispatch找whatever/generateWow来触发修改m2的wow值,是为了演示显示的指定reducerModule的作用
dispatch({module:'m2',reducerModule:'whatever',type:'generateWow',payload:'just show reducerModule'})
return {signal};
}
},
whatever:{//一个刻意和stateModule没有保持一致的reducerModule
generateWow:function({payload:wow}){
return {wow};
}
},
$$global:{//为global模块指定reducer函数
changeThemeColor:function({payload:themeColor}){
return {themeColor}
}
}
},
init:{
$$global:setState=>{//为global模块指定state的初始化函数
api.getThemeColor().then(themeColor=>{
setState({themeColor})
}).catch(err=>console.log(err))
}
},
computed:{
m1:{
name(name){//reverse name
return name.split('').reverse().join('');
}
}
}
})
复制代码
4 注意第3点里,注册一个react类到某个模块里成为cc类时,sharedStateKeys可以是这个模块里的任意key,因为cc允许注册不同的react类到同一个模块,例如模块M里拥有5个key为f1、f2、f3、f4、f5, ccClass1通过sharedStateKeys观察模块M的f1、f2, ccClass2通过sharedStateKeys观察模块M的f2、f3、f4,当ccClass1的某个实例改变了f2的值,那么ccClass1的其他实例和ccClass2的所有实例都能感知到f2的变化并被cc触发渲染。
5 cc有一个内建的global模块,所有的ccClass都天生的拥有观察global模块key值变化的能力,注册成为cc类时通过globalStateKeys观察模块global里的任意key。
6 所有cc实例上可以通过prop ccOption设定storedStateKeys,表示该实例上的这些key是需要被cc存储的,这样在该cc实例销毁然后再次挂载回来的时候,cc可以把这些key的值恢复回来。
7 一个cc实例的state的key除了上面所提到的global、 shared、stored这三种类型,剩下的一种key就是默认的temporary类型了,这种key对应的值随着组件销毁就丢失了,再次挂载cc实例时会读取state里的默认值。
8 结合4 5 6 7来看,cc实例里的state是由cc类上申明的sharedStateKeys、globalStateKeys,和cc实例里ccOption申明的storedStateKeys对应的值,再加上剩下的默认的temporaryStateKeys对应的值合并得出来。
9 和react实例一样,触发cc实例render方法,依然是经典的setState方法,以及上面提到的dispatch定位reducer方法去修改,除了这两种cc还有更多*的选择,如invoke,effect,xeffect允许用户直接调用自己定义的函数去修改state,同reducer函数一样,可以是普通函数、generator函数、async函数。
这样的方式让用户有了更多的选择去触发修改state,cc并不强制用户使用哪一种方式,让用户自己摸索和组合更多的最佳实践invoke一定是修改当前cc实例的state,只需要传入第一位参数为具体的用户自定义执行函数,剩余的其他参数都是执行函数需要的参数。
effect允许用户修改其他模块的state,第一位参数是moduleName,第二位参数为具体的用户自定义执行函数,剩余的其他参数都是执行函数需要的参数。
xeffect和effect一样,允许用户修改其他模块的state,第一位参数是moduleName,第二位参数为具体的用户自定义执行函数,剩余的其他参数都是执行函数需要的参数,和effect不一样的地方是xeffect调用的执行函数的参数列表,第一位是cc注入的ExecuteContext对象,里面包含了module, state, moduleState, xeffect,剩下的参数才对应的是是用户调用xeffect是除第一第二位参数以外的其他参数
import React,{Component, Fragment} from 'react';
import cc from 'react-control-center';
function* loginFn(changedBy, p1, p2 ,p3){
return {changedBy, p1:p1+'--tail', p2:'head--'+p2 ,p3}
}
function* forInvoke(changedBy, p1, p2 ,p3){
const result = yield loginFn(changedBy, p1, p2 ,p3);
return result;
}
function* forEffect(changedBy, p1, p2 ,p3){
const result = yield loginFn(changedBy, p1, p2 ,p3);
return result;
}
function* forXeffect({module, state, moduleState, xeffect}, changedBy, p1, p2 ,p3){
const result = yield loginFn(changedBy, p1, p2 ,p3);
return result;
}
@cc.register('Foo')
class Foo extends Component{
constructor(props, context){
super(props, context);
this.state = {changedBy:'none', p1:'', p2:'' ,p3:''};
}
render(){
const {changedBy, p1, p2, p3} = this.state;
//注,该cc类模块没有显式的声明模块,会被cc当做$$default模块的cc类
return (
<Fragment>
<div>changedBy {changedBy}</div>
<div>p1 {p1} p2 {p2} p3 {p3}</div>
<button onClick={()=>this.$$invoke(forInvoke, 11,22,33)}>invoke</button>
<button onClick={()=>this.$$effect('$$default',forEffect, 11,22,33)}>effect</button>
<button onClick={()=>this.$$xeffect('$$default',forXeffect, 11,22,33)}>xeffect</button>
</Fragment>
);
}
}
复制代码
10 cc定位的容器型组件的状态管理,通常情况一些组件和model非常的有业务关系或者从属关系,我们会把这些react类注册为某个moudle的cc类,观察这个模块中的状态变化,但是有些组件例如一个Workpace类的确需要观察很多模块的状态变化,不算是某个模块对应的视图组件,此时除了用上面说的sharedToGlobalMapping功能,将需要观察各个模块的部分状态映射到global里,然后注册Workpace时为其设定globalStateKeys,就能达到观察多个模块的状态变化的目的之外,cc还提供另一种思路,注册Workpace时设定stateToPropMapping,就可以观察恩义模块的任意key的值变化,和sharedToGlobalMapping不同之处在于,stateToPropMapping要从this.$$propState里取值,sharedToGlobalMapping是从this.state取值,当然stateToPropMapping不需要模块主动的将某些key映射到global里,就能达到跨模块观察状态变化的目录,cc鼓励用户精确对状态归类,并探索最佳组合和最佳实践
// these code written in https://github.com/fantasticsoul/rcc-simple-demo/tree/master/src/cc-use-case/WatchMultiModule
// you can update the rcc-simple-demo lastest version, and run it, then switch tab watch-multi-module, you will see what happen
import React from 'react';
import cc from 'react-control-center';
class WatchMultiModule extends React.Component {
render() {
console.log('%[email protected]@@ WatchMultiModule', 'color:green; border:1px solid green;');
console.log(`type cc.setState('todo',{todoList:[{id:Date.now()+'_1',type:'todo',content:'nono'},{id:Date.now()+'_2',type:'todo',content:'nono'}]}) in console`);
const { gbc, alias_content, counter_result, todoList } = this.$$propState;
return (
<div style={{width:'100%',height:'600px', border:'1px solid darkred'}}>
<div>open your console</div>
<div>type and then enter to see what happen <span style={{paddingLeft:'28px',color:'red'}}>cc.setState('counter',{result : 888} )</span></div>
<div>type and then enter to see what happen <span style={{paddingLeft:'28px',color:'red'}}>cc.setGlobalState( {content:'wowowo'} );</span></div>
<div>{gbc}</div>
<div>{alias_content}</div>
<div>{counter_result}</div>
<div>{todoList.length}</div>
</div>
);
}
}
const stateToPropMapping = {
'$$global/borderColor': 'gbc',
'$$global/content': 'alias_content',
'counter/result': 'counter_result',
'todo/todoList': 'todoList',
};
//two way to declare watching multi module cc class
export default cc.connect('WatchMultiModule', stateToPropMapping)(WatchMultiModule);
//export default cc.register('WatchMultiModule', {stateToPropMapping})(WatchMultiModule);
复制代码
github地址:github.com/fantasticso…
gitee地址:gitee.com/nick_zhong/…
quick-start demo: github.com/fantasticso…
期待大家试用并给出修改意见,真心希望能够亲爱你的能够感受的cc的魅力和强大,因为作为redux使用者的我(3年了快),不管是用了原生的redux还是dva封装后的redux,个人都觉得没有cc使用那么的爽快......先从示例项目开始体验吧^_^,期待着你和我有一样的感受