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>
)
}
}
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下的组件名称
这样小伙伴尽情玩耍就可以了。
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,是不是有什么关联? 其实就是一个是出口 一个是入口
所以想到这里,我们就可以设计一个 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')
更复杂的结构,
const Actions = actionType({
PDF:'application/pdf',
Text:'text/plain',
JPEG:'image/jpeg'
})
所以直接使用就好了,推荐使用常量字符串枚举,为什么?
export const enum Command{
LOADING="loading",
FILTER_TEXT="filter:text"
}
金无足赤人无完人,在实
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。