头图

Hello, plume2.

import React, { Component } from 'react'

import {Actor,Store,StoreProvider,Relax,ViewAction} from 'plume2'


//MapReduce  
class HelloActor extends Actor{
    defaultState(){
        return {text:"Hello,plume2"}
    }
}

//reactive ui event 反应式ui事件
class AppViewAction extends ViewAction{
    sayHello = (text:any) =>{
        this.store.dispatch('say:hello',text)
    }
}


// Single Data Source 单一数据源
class AppStore extends Store{
    //bind data transform 绑定数据转换
    bindActor(){
        //after plume2   directly pass Actor class 直接通过Actor类
        return [HelloActor]
    }

    //bind ui event 绑定ui事件
    bindViewAction(){
        return {
            AppViewAction
        }
    }
}

//Auto compute relaxProps 自动计算打开Props
@Relax 
 class Text extends React.Component{
    static relaxProps = {
        //auto injected by store.state().get('text')  由store.state().get('text')自动注入
        text:"text",
        //auto injected by store's bindViewAction 由store的bindViewAction自动注入
        ViewAction:'viewAction'
    }
    _handleClick = () =>{
        const {text,viewAction} = this.props.relaxProps
        viewAction.AppViewAction.sayHello(text)
    }
    render(){
        const {text,viewAction} = this.props.relaxProps;
        return (
            <div onClick={this._handleClick}>{text}</div>
        )
    }
}

@StoreProvider(AppStore) //应用程序入口
export default class Index extends Component {
    render() {
        return (
            <div>
                <Text/>
            </div>
        )
    }
}

image.png

API Actor

Actor 计算模型,一个独立的计算单元,主要作用就是转换我们的状态数据

import { Actor,Action} from 'plume2'

// 是的  这就是一个Actor简单世界
class HelloActor extends Actor{
    // 领域的初始数据  该数据会被自动的转换为immutable
    defaultState(){
        // 返回的对象会被自动的转化为immutable
        // 除非有特殊数据结构如(set OrderedMap之类)
        //不需要特殊指定immutable数据结构
        return {text:'hello plume2'}
    }
    //**  通过@Action来建立store的dispatch和actor的handler(处理程序)之间的关联
    // API规范
    // @param state actor上一次的immutable状态
    // @param text  store dispatch的参数值  尽量保持单值设计
    @Action('change:text')
    change(state,text){
        // immutable api
        return state.set('text',text)
    }

}

Store

什么是 Store?
Store 我们的数据状态容器中心 管理着整个app的数据的生命周期
我们坚守单根数据源的思想(single data source),store 中保持着完整的业务与UI状态
Stor的主要职责:
1 聚合actor
2 分派actor(单分派 事务分派)
3 通过bigQuery 计算我们的查询语言(QL/PQL)
4 响应页面的事件(ViewAction)
5 注册响应 RL

import {Store,ViewAction} from 'plume2'
import LoadingActor from 'loading-actor'
import UserActor from 'user-actor'
import TodoActor from 'todo-actor'


// 响应页面事件的逻辑处理
class AppViewAction extends ViewAction {
    //show simple dispatch 显示简单分派
    // when dispatch finished if status had changed,  如果状态已更改,则在调度完成时,
    //each Relax component received message 每个组件都接收到消息
    update = () => {
        //将计算的任务分派的到actor
        //然后根据actor的返回值 重新聚合新的store的state
        //该为单分派  当dispatch结束 store的state发生改变的时候
        //UI容器组件(StoreProvider,Relax) 会收到通知重新re-render UI
        this.store.dispatch('update')
    }
//  show multiple dispatch in a transaction  在事务中显示多个分派
    save = () =>{
        //事务分派
        //很多场景下  计算应该是原子类型的  我们想一想 dispatch 结束才通知UI去re-render
        //这个时候我们就可以开启事务控制
        // transaction  会返回值来判断在dispatch  过程中有没有发生错误
        // 如果发生错误  数据会自动回滚到上一次的状态  避免脏数据
        // 我们也可以指定  自定义的回滚处理
        // this.transaction(()=>{/*正常逻辑*/},()=>{/*自定义的回滚函数*/})
        this.store.transaction(()=>{
            this.store.dispatch('loading:end')
            //这个地方可以得到上一次的dispatch之后的结果
            //如:
            const loading = this.state().get('loading')
            this.store.dispatch('init:user',{id:1,name:'plume2'})
            this.store.dispatch('save')
        })
    }
}


class AppStore extends Store{
//   聚合Actor
//   通过reduce各个actor的defaultState
// 聚合出store的state作为source data
bindActor(){
    //plume2  直接传递Actor的class
    return [LoadingActor,UserActor,TodoActor]
}
bindViewAction(){
    return{
        AppViewAction
    }
}
}

Store public-API

// 绑定需要聚合的Actor
bindActor(): Array<Actor | typeof Actor>

bindViewAction(): IViewActionMapper

// 事务控制dispatch
// dispatch: 正常逻辑
// rollBack: 自定义回滚逻辑  默认是自动回滚到上一次的状态
//返回是否发生回滚

transaction(dispatch:Dispatch,rollBack:RollBack):boolean;

// 计算QL

bigQuery(ql:QueryLang):any

// 当前store聚合的状态
state():IMap;

// 定义store状态更新通知
subscribe(cb:Handler):void;

// 取消订阅
unsubscribe(cb:Handler):void;

StoreProvider

StoreProvider 容器组件衔接我们的React组件和AppStore。向React组件提供数据源

在StoreProvider中的主要任务是:
1 初始化我们的AppStore
2 将AppStore的对象绑定到React组件的上下文
3 Relay 就是通过上下文取的store对象
4 监听Store的state变化

友情提示:我们还提供了 debug模式

开启debug 模式 我们就可以对数据进行全链路跟踪

跟踪store的dispatch actor的处理 relax对QL的计算等

import React, { Component } from 'react'
import {StoreProvider} from 'iflux2'
import AppStore from './store'

//enable debug
@StoreProvider(AppStore,{debug:true})
class ShoppingCart extends Component{
    render(){
        return(
            <Scene>
                <HeaderContainer/>
                <ShoppingCart/>
                <BottomToolBarContainer/>
            </Scene>
        )
    }
}

Relax

Relax是plume2中非常重要的容器组件 类似Spring容器的依赖注入一样
核心功能会根据子组件的relaxProps中声明的数据,
通过智能计算属性的值 然后作为this.props.relaxProps透传给子组件

以此来解决React的props层层透传的verbose的问题

计算的规则
1 store 的state的值,直接给出值 得 immutable的路径,如 count:'count',todoText:['todo',1,'text']
2 store 的method 直接和method同名就ok 如:destroy:noop, 我们更希望通过ActionCreator来单独处理UI的side effect
3 如果属性是'viewAction' 直接注入store 中绑定的ViewAction
4 如果属性是QL 注入QL 计算之后的结果 如果PQL会自动绑定store的上下文

@Relax
export default class Footer extends React.Component{
    static relaxProps = {
        changeFilter:noop,
        clearCompleted:noop,
        count:countQL,
        loadingPQL:loadingPQL,
        filterStatus:'filterStataus',
        viewAction:'viewAction'
    }
    render(){
        const{
            changeFilter,
            clearCompleted,
            count,
            filterStataus,
            viewAction
        } = this.props.relaxProps
    }
    //...
}

QL/PQL

为什么我们需要一个QL
1 我们把store state 看成source data,因为UI展示的数据,可能需要根据我们的源数据进行组合
2 我们需要UI的数据具有reactive的能力 当source data变化的时候 @Relax 会去重新计算我们的QL
3 命令式的编程手动的精确的处理数据之间的依赖和更新 Reactive会自动处理数据的依赖 但是同一个QL 可能会被执行多次 造成计算上的浪费
不过不需要担心 QL支持cache 确保path对应的数据没有变化的时候 QL 不会重复计算

QL = Query Lang
自定义查询语法 数据的源头是store的state返回的数据

Syntax QL(displayName,[string|array|QL...RelaxContainer,fn])

displayName,主要是帮助我们在debug状态更好的日志跟踪

string array QL:string array 都是immutable的get的path,QL其他的QL(支持无线嵌套)

fn:可计算状态的回调函数 bigQuery会取得所有的所有的数组中的path对应的值 作为参数传递给fn


// 返回:{
//     id:1,
//     name:'iflux2',
//     address:{
//         city:'南京'
//     }
// }

store.state()

//QL计算的结果值是'iflux2南京'
const helloQL = QL('helloQL',[
    'name',
    ['address','city'],
    (name,city)=>`${name}${city}`
])

Store.bigQuery(helloQL)

QL in QL

import {QL} from 'plume2'
const loadingQL = QL('loading',['loading',loading=>loading])
const userQL = QL('userQL',[
    //query lang 支持嵌套
    loadingQL,
    ['user','id'],
    (loading,id)=>({id,loading})
])

在0.3.2版本中我们做了些较大的改变

plume2 是我们的一个新的起点 是我们走向typescript的起点 plume2完全站在typescript 静态和编译角度去思考框架的特点和实现我们希望plume2足够轻量 简单 一致 同时给出优雅的代码检查错误提示
全链路的log跟踪 就想我们的开发能够轻松一点
在我们实践过程中 也会一些不够细致的地方 我们需要不断的去改进 在怎么去思考改进都不为过 划重点 开发体验同用户体验一样重要

inprovements

1 干掉DQL,DQL有些鸡肋 这就是现实有理想的差别 实现过程中需要动态递归的替换模板变量 也是比较受罪
更重要的事 DQL的动态数据的来源只能是React的Component的props,这就带来了一些不够灵活 比较受限 我们设计DQL
或者QL的本意是什么 是获取数据声明式(Declarative) 以及数据本身的反应式(reactive) 为了解决这个问题 我们设计了更简单的
PQL(partial Query Lang)

import {PQL,Relax} from 'plume2'
const helloPQL = PQL(index =>QL([
    ['user',index,'name'],
    (name)=>name
]))

@Relax 
class HelloContainer extends React.Component{
    static relaxProps = {  
        hello:helloPQL   //自动绑定store的上下文
    }

    render(){
        const value = hello(1)
        return <div>{value}</div>
    }
}

简单清晰实现 更灵活的参数入口 目前不支持QPL嵌套QPL.

1 更舒服的开发体验
有时候我们为了快速的在浏览器的控制台 如(chrome console) 去快速测试我们的一些store的方法 我们会写

  class AppStore extends Store {
    constructor(props: IOptions){
        if(_DEV_){
            window['store'] = this
        }
    }
}

这样可以在控制台直接调用_store 去快速测试 但是经常这样写 每个页面这样写 就有点小烦躁。
无缘无故去写个构造方法 。也挺无趣。
在一些SPA或者react-native的多页面中_store会被重复覆盖。
可从框架层面去解决这个问题。当开启应用的debug特定的时候 框架自动绑定。来简化这个流程

开启debug-mode

@StoreProvder(AppStore,{debug:true})
class HelloApp extends React.Component{
    render(){
        return <div/>
    }
}

plume2 会自动在window上面绑定_plume2App,各个key就是storeprovider下的组件名称
image.png
这样小伙伴尽情玩耍就可以了。

1 更好的事件处理模块目前我们的UI交互的事件的handler都在store中,因为我们希望UI是less-logic这样才好通用我们业务层。
之前都是通过relax和relaxProps去injected我们store的方法给UI 的交互逻辑如:


const noop = ()=>{}
@Relax 
class HelloApp extends React.Component{
    props:{
        relaxProps?:{
            OnInt:Function;
            onReady:Function;
            onShow:Function
        }
    }
    static relaxProps = {
        onInit:Function,
        onReady:Function,
        onShow:Function
    }
}

这样特点是简单 通过relax注入就完事了 就一个规则只要 方法的名字和store的method名字相同就OK 但是实际操作发现写一遍注入 再写一遍typeScript类型定义 心里真是万马奔腾 太重太累。
更有甚者 我们可能某个叶子节点的组件 仅仅是想回调一个事件 都要通过relax来注入 如果有列表数据的场景设计的不当,
如果每个item都是relax 页面200条数据 那就是200relax组件。
relax本质是subscribe container Component,
它会实时监听store的变化 这200个哗啦啦的性能下降 。
SO 我们需要思考 在react中 怎么定义UI UI = s(state) 其实这个并不完整这个仅仅是定义了UI的展现部分 UI还有交互 交互在函数式观点事件就是副作用 因此更完整的定义应该是UI = f(state,action),
继续往下思考 是么是state ? 站在flux的角度去看 state = store(initState,action),是不是很熟悉 都有Action,是不是有什么关联? 其实就是一个是出口 一个是入口

image.png
所以想到这里,我们就可以设计一个 ActionCreator 模块。

来来来,上代码。

const actionCreator = ActionCreator()
actionCreator.creat('INIT',(store,num:string)=>{
    //biz logic
    store.dispatch('init',num)
})

class AppStore extends Store{
    //将store绑定到AcitonCreator
    bindActionCreator(){
        return actionCreator;
    }

    // 除了在用actionCreator.create创建
    //event  handler 也可以直接在store中
    @Action("INIT")
    init(num:string){
        this.dispatch('init',num)
    }
}

const HelloApp = () =>{
    <div>
        <a onClick={actionCreator.fire('INIT',1)}>吼吼吼</a>
    </div>
}

这种方式有个问题就是ActionCreator是个单例 这样会导致多次重复render一个页面的时候 会有事件被store的上下文覆盖的问题
基于这样的考虑还是通过上下文注入绑定 所以在1.0.0设计了ViewAction来解决这个问题

ViewAction

import {ViewAction,Store} from 'plume2'

class LoadingViewAction extends ViewAction{
    loading = () =>{
        this.store.dispatch('loading:start')
    }
}

class FilterViewAction extends ViewAction{
    filter = (text:string) =>{
        this.store.dispatch('filter:text',text)
    }
}


//bind to store
class AppStore extends Store{
    bindViewAction(){
        return{
            LoadingViewAction,
            FilterViewAction
        }
    }
}

//how to injected to ui

class Filter extends React.Component{
    props:{
        relaxProps?:{
            //代码自动提示  参考example中的例子
            viewAction:TViewAction<typeof {LoadingViewAction,FilterViewAction}>
        }
    }
    static relaxProps = {
        viewAction:'viewAction'
    }

    render(){
        const {viewAction} = this.props.relaxProps
    }
}
都什么年代了 你还裸用字符串,你这是魔鬼字符串。。

是的 我们加 我们加字符串的枚举类型 一次来解决dispatch到actor等各种常量字符串

export default ActionType('INCREMENT','DECREMENT')

image.png
更复杂的结构,

const Actions = actionType({
    PDF:'application/pdf',
    Text:'text/plain',
    JPEG:'image/jpeg'
})

所以直接使用就好了,推荐使用常量字符串枚举,为什么?

export const enum Command{
    LOADING="loading",
    FILTER_TEXT="filter:text"
}

金无足赤人无完人,在实image.png
image.png
image.png


HappyCodingTop
526 声望847 粉丝

Talk is cheap, show the code!!