模块初览
在concent里,提供一个全局唯一的store,而store是由多个模块一起组成的,模块是一个非常重要的概念,每个模块又分别由state、reducer、computed、watch、init组成。
注意 :除了state是必需的,其他都是可选项,按需配置就好
- state定义模块下的数据。
- reducer定义模块修改数据的业务逻辑,因为对于concent来说,setState就可以修改模块的数据,所以reducer不是必需的,对于简单的业务逻辑你可以直接使用setState来完成数据修改,但是通常项目的功能会越来越复杂,而修改数据前的处理过程代码就对应着我们的业务逻辑,这时候为了解耦业务逻辑与ui渲染,建议将其抽离到reducer,了解更多关于reducer。
- computed定义各个stateKey的值发生变化时,要触发的计算函数,并将其结果缓存起来,仅当stateKey的值再次变化时,才会触发计算,了解更多关于computed。
- watch定义各个stateKey的值发生变化时,要触发的回调函数,仅当stateKey的值再次变化时,才会触发,通常用于一些异步的任务处理,了解更多关于watch。
- init可以对state完成一次异步的初始化过程,如果模块的state是需要异步的被赋值的时候,可以对其定义init函数,返回的状态将被合并到state里,如果此时此模块下已经实例化了一些组件,init返回的状态也会被分发到各个实例上,了解更多关于init。
创建store子模块
声明一个模块foo,只包含state定义
// code
const foo = {
state: {
name:'concent',
firstName:'',
lastName:'',
age:0,
hobbies:[]
}
}
载入模块,启动concent
使用concent提供的run接口,配置模块启动concent。
import { run } from 'concent';
run({ foo });
注册组件
书写一个class组件,使用使用concent提供的register接口,将其注册为concent组件,指定其属于foo模块。
import React, { Component } from 'react';
import { register } from 'concent';
@register('foo')
class HelloConcent extends Component {
state = { name: 'this value will been overwrite by foo module state' }
render() {
const { name, age, hobbies } = this.state;
return (
<div>
name: {name}
age: {age}
hobbies: {hobbies.map((v, idx) => <span key={idx}>{v}</span>)}
</div>
);
}
}
指定组件属于foo模块后,concent会在组件初次渲染前将其所属模块的state合并到实例的this.state上,实例state里声明了和模块state同名的key的话,其值将会被覆盖,所以上面的示例里state = { name: 'this value will been overwrite by foo module state' },其name值在render最终将是模块state里初始值concent;
提示
当然,你可以声明额外的key在实例上,不同于模块state的key,这些key的值对于实例来说就是私有的,改变它们的值,不会影响到其他实例。
添加修改数据行为
加入一个输入框,修改名称
@register('foo')
class HelloConcent extends Component {
changeName = (e)=>{
this.setState({name:e.currentTarget.value})
}
render() {
const { name, age, hobbies } = this.state;
return (
<div>
name: <input value={name} onChange={this.changeName} />
age: {age}
hobbies: {hobbies.map((v, idx) => <span key={idx}>{v}</span>)}
</div>
);
}
}
实例化组件
一切工作准备就绪,我们渲染多个HelloConcent看看效果吧。
import ReactDOM from 'react-dom';
function App(){
return (
<div>
<HelloConcent />
<HelloConcent />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
运行时的依赖收集策略
concent组件在每一次渲染时都会收集当前渲染逻辑对数据的依赖列表,所以推荐渲染逻辑里遵循用到什么数据就解构什么的原则,让组件保持最小范围的依赖
// 渲染未用到age,但只要age改变了依然会触发当前组价实例渲染
function BadCase(){
const { state:{name, age} } = useConcent('foo');
return <h1>only use name: {name} </h1>
}
// 删掉age,保持组件最小的依赖列表
function GoodCase(){
const { state:{name} } = useConcent('foo');
return <h1>only use name: {name} </h1>
}
人工指定依赖
通过设定watchedKeys可以人工指定组件的依赖列表,从而替换掉默认的运行时依赖收集策略
优先考虑依赖收集
人工指定会有额外的维护成本,推荐用户不指定watchedKeys,让concent采用默认的运行时依赖收集策略
@register({ module: 'foo', watchedKeys: ['age'] })
class FooComp extends Component {
render() {
const { name, age, hobbies } = this.state;
}
}
定义reducer
关于actionCtx.setState
所有的模块reducer里如果没有定义setState函数,concent会自动为其生成一个,书写actionCtx.setState(state)等同于书写actionCtx.dispatch('setState', state);
当我们提交变更数据前有不少的处理过程的时候,组件的代码会越来越臃肿,为了解耦业务逻辑也ui渲染,我们需要合理的剥离相关处理过程过程到reducer
。 在concent里,触发reducer特别简单,因为concent为每一个组件实例都构建了一个实例上下文对象ctx,
该对象上提供了concent为组件能力增强的api,你可以用this.ctx.dispatch('reducerFnName', payload)直接呼叫reducer函数,从而避免各种mapToProps和相关的配套action定义。
- reducer函数可以是纯函数,也可以是async函数
- 可以返回一个部分状态,可以调用其他reducer函数后再返回一个部分状态,也可以啥都不返回,只是组合其他reducer函数来调用。
const foo = {
state: { ... },
reducer: {
changeName(name) {
return { name };
},
async changeNameAsync(name) {
await api.track(name);
return { name };
},
async changeNameCompose(name, moduleState, actionCtx) {
await actionCtx.setState({ loading: true });
await actionCtx.dispatch('changeNameAsync', name);
return { loading: false };
}
}
}
建议的做法是将reducer函数独立放一个文件,在暴露出来给module配置,这样的reducer里函数间的相互调用可以不用基于字符串了,同时因为concent的module是包含多个可选定义项的,分离它们有利于后期维护和扩展。
├── modules
├── foo
├── state.js
├── reducer.js
├── computed.js
├── watch.js
├── lifecycle.js
├── index.js
├── bar
├── ...
此时reducer文件里,可以基于函数引用调用其他reducer函数了
// code in models/foo/reducer.js
export function changeName(name) {
return { name };
}
export async function changeNameAsync(name) {
await api.track(name);
return { name };
}
export async function changeNameCompose(name, moduleState, actionCtx) {
await actionCtx.setState({ loading: true });
await actionCtx.dispatch(changeNameAsync, name);//基于函数引用调用
return { loading: false };
}
在组件里触发reducer
@register('foo')
class HelloComp extends Component {
changeName = (e)=>{
// this.setState({name:e.currentTarget.value})
// 替代dispatch字符串调用方式
this.ctx.mr.changeName(e.currentTarget.value);
// this.ctx.dispatch('changeName', e.currentTarget.value);
}
}
定义模块computed
concent正确地修改数据行为是提交片段state,即变化了数据就提交什么,这与react的setState是一致的理念,真因为如此,concent可以精确的感知到哪些key的值发生了变化,所以允许你定义计算函数,concent会将其返回结果缓存起来。
// code in models/foo/computed.js
//当age发生变化时,对age做计算,
export function age({age}) {
return age * 2;
}
//对firstName, lastName任意一个值发生变化时,计算新的fullName
export function fullName(newState, oldState, fnCtx){
// fnCtx.setted查看提交的状态key列表
// fnCtx.changed查看提交的状态key列表里发生了变化的key列表
// fnCtx.retKey查看当前函数的计算结果对应key,当前示例为 fullName
return `${newState.firstName}_${newState.lastName}`;
}
// 更推荐上面的写法,在参数里解构时确定输入的数据依赖
export const fullName = {
fn(newState, oldState, fnCtx) {
// logic code
},
depKeys: ['firstName', 'lastName'],//这里定义触发fullName计算的依赖key列表
}
定义模块watch
同computed一样,可以对key做一些watch定义,当key的值发生改变时触发其watch回调,适用于一些需要处理异步任务的场景。
// code in models/foo/watch.js
import { defWatch } from 'concent';
**// age和stateKey同名,表示当age发生变化时触发此函数**
export function age() {
api.track('ageChanged');
}
//对firstName, lastName任意一个值发生变化时,触发此函数
export const fullName = defWatch(
(newState, oldState, fnCtx) => {
// fnCtx.changed查看提交的状态key列表里发生了变化的key列表
const { changed } = fnCtx;
if(changed.includes('firstName'))api.track('firstNameChanged');
if(changed.includes('lastName'))api.track('lastNameChanged');
},
**// 人工定义触发fullName watch回调的依赖key列表**
{
depKeys: ['firstName', 'lastName'],
compare:false, // 表示即使你值并没有变化;但是你对某个stateKey设了值就触发回调
immediate:true
}
)
**// 或写为解构时确定依赖**
export function fullName2({firstName, lastName}, oldState, fnCtx){
const { changed } = fnCtx;
if(changed.includes('firstName'))api.track('firstNameChanged');
if(changed.includes('lastName'))api.track('lastNameChanged');
}
export const compareTest = {
fn:(newState, oldState, fnCtx) => {
// fnCtx.changed查看提交的状态key列表里发生了变化的key列表
const { compareTest } = newState
const { changed } = fnCtx;
console.log('compareTest watch ---',compareTest,newState,oldState,changed)
},
// 人工定义触发fullName watch回调的依赖key列表
compare:false,
immediate: true,// 载入模块配置时,就触发此watch函数
}
// 全局配置
import { run } from 'concent';
run(
//store配置
{
foo:{},
bar:{},
},
//可选配置
{
watchCompare: true,//修改模块watch的compare默认值
watchImmediate: false,//修改模块watch的immediate默认值
}
);
定义模块lifecycle[v2.9+]
提供initState、initStateDone、loaded、mounted、willUnmount五个可选的生命周期函数
(返回覆盖state;但是只能修改;不能新增). 但是可以通过掉用reducer 去修改新增reducer
initState
适合做一些异步的初始化状态工作
// code in models/foo/lifecyle.js
import * as rd from './reducer';
export async function initState(){
const data = await api.fetData();
return data;
}
initStateDone
initState执行结束后的业务逻辑
// code in models/foo/lifecyle.js
import * as rd from './reducer';
export function initStateDone(dispatch, moduleState){
dispatch(rd.nextStep);
}
loaded
模块载入完毕时执行的工作,当我们的异步状态初始化工作需要放置到reducer内部方便可重用时,推荐使用loaded替代initState
/ code in models/foo/lifecyle.js
import * as rd from './reducer';
export function loaded(dispatch, moduleState){
dispatch(rd.initState);//调用reducer里的状态初始化函数
}
mounted
当该模块的第一个组件挂载完毕时需要触发的函数,和loaded的最大区别是执行时机不同,mounted是由组件实例挂载完毕驱动触发,而loaded是模块载入完毕就触发执行。
当你的状态初始化流程是依赖组件实例存在时才开始执行,则可考虑mounted替代loaded
// code in models/foo/lifecyle.js
import * as rd from './reducer';
export function mounted(dispatch, moduleState){
dispatch(rd.initState);//调用reducer里的状态初始化函数
}
mounted默认只触发一次,即组件如果销毁再次挂载回来并不会触发,如果需要满足条件时反复执行,则需要返回false
export function mounted(dispatch, moduleState){
dispatch(rd.initState);
return false;
}
willUnmount
当该模块的最后一个组件卸载时需要触发的函数,通常用于清理工作,如果需要满足条件时反复执行,需要返回false
export function willUnmount(dispatch, moduleState){
dispatch(rd.clearUp);
return false;
}
跨多个模块的组件
如果我们的组件还要消费其他模块的数据,则需要注册是定义connect来连接其他模块,以便达到消费其他模块数据的目的。
注意
属于和连接是两个不同的概念,组件dispatch行为在没有指定目标模块时,都自动地修改的是自己模块数据,同时数据是诸如到this.state里的,而且一个组件只能属于一个模块,但是可以连接多个其他模块,连接的模块其数据是注入到this.ctx.connectedState.{moduleName}下的
如下我们将定义一个BarComp,指定其属于bar模块,同时连接foo和baz模块
@register({ module: 'bar', connect: ['foo', 'baz'] })
class BarComp extends Component {
render() {
const bazState = this.state;
//获得连接模块的状态
const { foo:fooState, bar:bazState } = this.ctx.connectedState;
//获得连接模块的计算结果
const { foo:fooCu, baz:bazCu } = this.ctx.connectedComputed;
}
}
如果我们需人工挑选foo模块的部分key做观察,而baz模块保持默认的依赖收集策略,则可以写为
@register({ module: 'bar', connect: {foo:['key1', 'key2'], baz:'-'} })
如果不指定组件属于任何模块,仅连接其他模块,则可写为
@register({ connect: {foo:['key1', 'key2'], baz:'-'} })
此时组件会被concent指定属于内置模块$default,这是一个空模块,除非你显式地去重定义该模块相关配置项,在没有对$default模块重定义前,组件里的**this.state和模块state****将不再有关联,**组件的this.setState也不再能够触发修改模块state的数据,组件自定义的state相当于变成完全私有的了。
上述示例中,注册的组件都指定了属于bar模块,所以实例上下文对象调用this.ctx.dispatch('reducerFnName', payload)时,知道触发的是bar模块的reducer函数,去修改bar模块的数据。
this.ctx.dispatch('changeName', 'newName');
//等同于写为
this.ctx.dispatch('bar/changeName', 'newName');
// 推荐写为直接调用的方式
this.ctx.moduleReducer.changeName('newName');
// or
this.ctx.mr.changeName('newName');
//如果我们要显示的去触发其他模块的reducer函数,可以写为
this.ctx.dispatch('foo/changeName', 'newName');
调用其他模块的方法时,推荐使用this.ctx.connectedReducer.{moduleName} 或其缩写 this.ctx.cr.{moduleName} 来发起调用**
this.ctx.dispatch('foo/changeName', 'newName');
// 改为
this.ctx.cr.foo.changeName('newName');
定义setup
setup定义是针对实例的,触发时机是组件构造器函数执行结束后,组件将要首次渲染前,所以只会被执行一次,其返回结果将搜集到this.ctx.settings里,配合上下文对象提供的effectapi,还可以达到在类里消灭生命周期函数的效果
@register('foo')
class HelloComp extends Component {
$setup(ctx) {//$setup会将ctx传递到参数列表里,和this.ctx是同一个对象
//第二为参数是依赖key名称列表,填写空表示只在首次渲染结束后触发一次,模拟componentDidMount
ctx.effect(ctx => {
// api.fetchData()
}, []);
//不传递第二位参数的话,每一次组件渲染结束后都会触发其执行
ctx.effect(ctx => {
// trigger after every render
});
//第二为参数依赖key名称列表指定了name,
ctx.effect(ctx => {
// 首次渲染结束时,触发此副作用函数执行
// 之后只要name变化,就会在组件渲染结束后触发此副作用函数执行
}, ['name']);
//第三位参数immediate默认是true,设置为false
ctx.effect(ctx => {
// 首次渲染结束时,不触发此副作用函数执行
// 之后只要name变化,就会在组件渲染结束后触发此副作用函数执行
}, ['age'], false);
//第二为参数依赖key名称列表设定了多个值
ctx.effect(ctx => {
// 首次渲染结束时,不触发此副作用函数执行
// 之后只要name或者age任意一个发生变化时,会在组件渲染结束后触发此副作用函数执行
}, ['age', 'name'], false);
const changeName = e => ctx.dispatch('changeName', e.currentTarget.value);
return { changeName };
}
render() {
//可用于绑定在ui上
const { changeName } = this.ctx.settings;
return <input value={this.state.name} onChange={changeName} />
}
}
定义实例computed
和模块computed不同,实例computed是针对实例的,每一个实例都会触发自己的计算函数,当你的不同实例需要有不同的计算逻辑的时候,才需要定义实例computed,因为实例computed只能定义一次,结合上面提到的setup,我们可以在setup里完成定义,其计算结果将从this.ctx.refComputed里获得
@register('foo')
class HelloComp extends Component {
$setup(ctx) {
ctx.computed('name', ({name}) => {
return name.split('').reverse().join();
});
ctx.computed('fullName', (newState) => {
return `${newState.firstName}_${newState.lastName}`
});
}
render() {
// 从refComputed里获得计算结果
const { name: reversedName } = this.ctx.refComputed;
}
}
定义实例watch
同样的,我们也可以对实例定义watch,以方便处理一些异步任务,如以下示例,当type发生变化时,抓取一次数据
@register('foo')
class HelloComp extends Component {
$setup(ctx) {
ctx.watch('type', ({type})=>{
ctx.dispatch('fetchDataWhileTypeChanged', type);
});
}
render() {
//这里用了一个sync语法糖函数,自动将onChange事件里的value值同步到state.type下
return (
<select value={this.state.type} onChange={this.ctx.sync('type')}>
<option value="1">1</option>
<option value="2">2</option>
</select>
);
}
}
注意 实例watch和实例effect执行时机不一样,前者是指组件渲染前触发,后者是指组件渲染后触发
拥抱hook,定义函数组件
concent之所以成为渐进式的框架,是因为在类组件里的所有概念,都可以平滑的过度到hook函数组件,使用useConcent接口,可以让它们拥有完全一致的api调用体验,useConcent返回一个实例上下文对象ctx,和类里的this.ctx无论是数据结构还是使用方式都是一模一样的。
import { register } from 'concent';
@register('foo')
class HelloComp extends Component {
$setup(ctx) {
//call ctx.effect、ctx.computed、ctx.watch etc
}
render() {
const {
refComputed, moduleComputed, connectedComputed,
connectedState, state, settings, dispatch, sync
// etc ...
} = ctx;
// return ui
}
}
// ----------------------------
import { useConcent } from 'concent';
const setup = ctx => {
//call ctx.effect、ctx.computed、ctx.watch etc
}
function HelloHookComp() {
const ctx = useConcent({ module: 'foo', setup });
const {
refComputed, moduleComputed, connectedComputed,
connectedState, state, settings, dispatch, sync
// etc ...
} = ctx;
// return ui
}
模块