dingDang ( 一个管理react数据层的产品 )
github地址
我们将dingDang定义为一个产品而不是一个框架亦或工具包 。
因为dingDang在设计之初就将用户的开发体验放在第一位,而性能问题则放在第二位。
当然这并不意味着我们就是一个性能很糟糕的产品,只是说我们会在性能能够接受的范围内尽可能的去让开发者的开发体验更好。
事实上dingDang的内部数据全部都采用了immutable数据类型。
dingDang中使用了大量的 Promise 于 immutableJS , 如果你对此不太熟悉,可以先熟悉一下 这两个知识点。
认识 DingDang
DingDang装饰器 , 一切故事的开始 , 目前 仅仅只有 sceneStorage 一个参数 。
主要用来定义场景的临时数据存储方案。
比如说:
在react-web 中 你应该采用sessionStorage 或者 localStorage ,
而在react-native 中,你应该采用AsyncStorage 。
在sceneStorage中的3个函数 所有的返回值 要求是标准的 JS Object,set 方法的 key 我们也会传输一个标准的 JS Object
@DingDang({
sceneStorage: {
set: (k,v) => sessionStorage.setItem(k,JSON.stringify(v)),get: (k) => JSON.parse(sessionStorage.getItem(k)),del: (k) => sessionStorage.removeItem(k)
}
})
class App extends React.Component {
render() {
return (
<HashRouter>
<Layout>
</Layout>
</HashRouter>
)
}
}
render(
<App />,document.querySelector("#app")
)
认识一个完整的Store
const store = {
namespace: 'demoStore',state: {},rules: {},reducer: {},effect: {},initialization: () => false,destroy: () => false
}
事实上,store中只有namespace字段是必须的,其他的字段如果你不需要的话,可以不写。
namespace(命名空间)
该属性的值是一个字符串 , 在dingDang中请务必保证namespace的唯一性,因为我们需要以此为依据去判定用户是否进入了一个全新的场景。
state (状态机)
该属性中主要用来存储store中的所有数据集 , 一般来说dingDang会将这里的数据都转换为immutable类型。
rules (规则)
首先让我们来看一个完整的rules的案例:
export default {
namespace: 'demoStore',state: {
name: '张三'
},rules: {
name: [
{
validate: (state,name) => name ? true : false,error: '用户姓名不能为空',miss:false
},{
validate: (state,name) => name.length < 6,error: '用户姓名最大长度不能超过6!',miss:true
}
]
}
}
rules 中配置的其实是针对state 中数据的校验机制 ,比如在我们的state 中有name这个字段 ,
那么你可以在 rules中也配置一个 对应的name字段 , 但是在rules中 , 该字段的值必须为数组,
具体的校验规则,我们会按照数据的先后顺序来执行。
数据中存储的是每一个 独立的 rule ,一个标准的 rule如下:
{
validate: (state,miss:false
}
validate : 值为一个判断函数 , 该函数我们会自动的注入全量的state参数, 以及你即将赋予字段的新值 ,
例如:name 。 这里的校验逻辑为: 如果满足你所需的格式 ,那么返回true , 否则false
error: 错误提示 , 在执行validate得到false之后抛出的异常信息
miss : 定义是否丢失, 这里的含义是指, 如果即将赋予的新值不能满足校验条件, 那么新的值是否丢失掉(即不存储进state)
如不定义该字段, 那么该字段默认为false , 即不丢失 ,依然赋予state。
reducer (折叠器)
该字段内存储的应该都是纯函数 ,在这里你可以直接去针对state赋予全新的值 , 首先让我们来看一段相对完整的reducer代码案例:
export default {
namespace: 'orderStore',state: {
goodsList: [
{
id: 1,name: '方便面',price: 2
},{
id: 2,name: '笔记本电脑',price: 3281
}
],cart: []
}
reducer: {
addToCart: (state,goods) => {
return state.set('cart',state.get('cart').push(goods));
}
}
}
正如上面的demo所示 , 我们会为reducer 中的每一个函数的 第一个参数都注入一个全量的 immutable 类型的state,而第二个参数则是你在调用的时候传入的。我们对reducer的要求是,最后你必须返回一个你期望的、全新的、全量的state。
下面我们看一段调用reducer 的案例代码:
@Page(Store)
export default class GoodsList extends React.Component {
static injectProps = {
goodsList: [],cart: []
}
_addToCart = async (n) => {
const {execReducer} = this.props;
try {
await execReducer('addToCart')(n)
} catch (err) {
console.log(err)
}
}
render() {
const {injectProps: {goodsList}} = this.props;
return (
<Layout>
<Row>
<Col>
<Table rowKey={'id'} dataSource={goodsList}>
<Column title="商品名称" dataIndex="name"/>
<Column title="商品价格" dataIndex="price"/>
<Column title="操作" render={
(n) => <a href="javascript:void(0);" onClick={
() => this._addToCart(n)
}>加入购物车</a>
}/>
</Table>
</Col>
</Row>
<Row>
<Col>
<Link to="/balance"><Button>去结算</Button></Link>
</Col>
</Row>
</Layout>
)
}
}
上述的demo中 , 我们希望你不要被过多的去关注可能对于你来说未知的代码影响,你只需要关注我们希望阐述的内容即可(即_addToCart函数),
首先我们应该明确_addToCart函数是一段调用reducer的代码 , 其次我们观察到 , _addToCart函数采用了async await 的方式来书写,
说明该函数是异步的 , 并且返回了一个Promise对象给我们 。
事实上,在你执行reducer时,dingDang内部有可能会去rules中取到对应的规则进行校验,如果出现异常的话会reject出来给你 , 所以这里
catch到的err其实是你的rules中所配置的信息 , 同时如果你的reducer没有任何毛病, dingDang依然会有返回值给你,这个返回值是reducer
之后的一个全新的state。
effect (副作用的)
我们将这里定义为副作用的 , 也就是希望你将所有的非纯函数在这里定义(依赖于外部因素的函数,相同输入并非能保障得到相同的结果 ,比如调用API)。
惯例,让我们看一段完整的effect案例:
export default {
namespace: 'orderStore',state: {
goodsList: [],state.get('cart').push(goods));
}
},effect:{
queryGoodsList: async ({resolve,reject,state,execReducer,onChangeState},params) => {
const result = await queryGoodsListByAPI(params);
if( result.success ){
await onChangeState(result.goodsList).catch(err => reject(err));
resolve(true)
}else {
reject( result.error )
}
}
}
}
effect 的参数,我们注入的比较多, resolve 、 reject 这里就不作解释了 , 如果你不是很清楚, 建议去温习一下Promise 。state 也不作过多的解释了。
这里我们稍微来提一下 execReducer 和 onChangeState 这两个函数 。 execReducer 在前面我们也见过 , 这个函数属于dingDang的系统级函数,主要用来
辅助你去调用reducer。 onChangeState 也是一个dingDang系统级函数 , 这个函数是便于你直接去修改state中的数据 。
我们注意区分一下reducer 和 onChangeState 的区别 , 一般来说reducer的调用方式如下:
execReducer('addToCart')(params);
而onChangeState的调用方式
onChangeState(jsObjectParams)
我们总结概括一下,reducer其实是你用来去调用你自己声明的指定的一个reducer函数 , 而onChangeState 其实是直接去修改的state中的值 。 一般来说,
如果你无需做业务逻辑, 仅仅只是想把一个数据放进state , 我们推荐你使用 onChangeState 的方式, 请不要担心rules 的校验问题 onChangeState 依然
会去执行rules的校验代码 , 而如果您不仅仅是将一个数据放进state , 在这中间可能还有很多业务逻辑代码, 那么我们推荐你自己去写一个reducer , 然后使用
execReducer 来进行调用
initialization (store生命周期中的初始化)
initialization 是一个初始化函数 , 这里不想过多的说什么 , 主要重点说一下 , 我们为这个函数注入了
execReducer 、 execEffect 、 onChangeState
destroy (store生命周期中的销毁)
与initialization同理 , 注入的参数也相同
系统级函数介绍
execReducer
高阶函数 , 主要用来调用 reducer , 返回Promise
execEffect
高阶函数 , 主要用来调用 effect , 返回Promise
onChangeState
简化修改state中值的方式
Page 装饰器
用来装饰一个页面的入口 ,装饰之后的页面 可以使用声明式注入 demo案例如下
@Page(store)
class Home extends React.Component {
static injectProps = {
initName: null,name: null
}
render() {
const {injectProps: {initName,name},onChangeState} = this.props;
return (
<div>
<h1> {initName} </h1>
<h1> {name} </h1>
<button onClick={() => onChangeState({initName: '首屏文字被变化之后'})}>变首屏文字</button>
<Card/>
</div>
)
}
}
Component装饰器
用来装饰一个页面下的子组件,装饰之后的页面 可以使用声明式注入 demo案例如下
@Component
class Card extends React.Component {
constructor(props) {
super(props)
this.state = {
error: ''
}
}
static injectProps = {
name: null
}
render() {
const {injectProps: {name}} = this.props;
return (
<div>
{name}<span style={{marginLeft: 20,color: 'red'}}>{this.state.error}</span>
<button onClick={this._onChangeName}>变换</button>
</div>
)
}
_onChangeName = () => {
const {onChangeState} = this.props;
onChangeState({name: '李四李四李四李四李四李四'}).catch(e => this.setState({error: e}))
}
}
injectProps (声明式注入)
在组件中使用 static injectProps 的方式可以来声明 让dingDang 从store中为你注入哪些数据。
ScenePage (场景页面)
在某些业务背景下,我们可能需要多个页面来完整一组业务。
假设我们需要 A B C 3个页面来完成一组业务 , 在这组业务中 数据其实基本上是相同的, 针对数据的某些业务操作也是相同的。
在这种背景下,我们提出了场景的概念。
我们对场景的定义:多个页面之间针对同一组业务数据进行的一连串行为动作来完成的一组业务,我们称之为场景。
普通的Page装饰器装饰之后的页面只能取自己独立的那份store , 而使用ScenePage装饰之后的多个页面 , 只要绑定的Store
的namespace相同,那么这个Store的声明周期将一直存在,直到用户跳出当前场景。
使用
npm install 0.1.1-alpha --save