这是 Pastate.js 响应式 react state 管理框架系列教程,欢迎关注,持续更新。 javascript
Pastate.js Github 欢迎 star。css
这一章,咱们将讲解在 pastate 应用中多模块应该如何协做。java
在多模块应用中,有些组件的视图须要引用多个模块的 store 假设有个比较复杂的应用的模块依赖关系以下:react
模块之间的互相依赖分为两种:git
所以,在多模块应用中,咱们先把各个模块的 store 链接一棵 store 树, 以下: github
而后指定各 component 引用这颗 store 树的哪些(一个或多个)节点的 state, 生成 Container: 编程
咱们先把上一章的 StudentPanel 板块改成 storeTree 模式。
首先,在 src/index.js
中建立一个 store 树, 咱们在 pastate 中使用普通的对象模式来描述 store 树:redux
... import * as StudentPanel from './StudentPanel'; const storeTree = { student: StudentPanel.store } ...
这样,就成功地把 StudentPanel 模块的 store 挂载到 storeTree 的 student
节点,而后在 makeApp 函数中把 原来的 StudentPanel.store 改成 storeTree:segmentfault
... ReactDOM.render( makeApp(<StudentPanel.view />, storeTree), document.getElementById('root') ); ...
接着,咱们在 StudentPanel.view.jsx
中指明该视图组件引用的 storeTree 的节点:设计模式
class StudentPanel extends React.PureComponent { ... } export default makeContainer(StudentPanel, 'student')
咱们在 makeContainer 函数的第二个参数中对引用的节点路径进行指定。
接下来,咱们来建立班级信息管理系统的第二个模块课程模块。与 StudentPanel 相似,咱们建立一个 ClassPanel 文件夹来保存该模块的文件,文件夹的文件目录以下:
ClassPanel
一样,在 ClassPanel.view.js
中咱们指定把组件链接到 storeTree 的节点 'class':
class ClassPanel extends React.PureComponent{ ... } export default makeContainer(ClassPanel, 'class')
对应地,咱们在 src/index.js
中须要把 ClassPanel.store 挂载到 storeTree 的 'class' 节点:
... import * as ClassPanel from './ClassPanel'; const storeTree = { student: StudentPanel.store, class: ClassPanel.store } ...
你能够在 makeApp 时把以前的 StudentPanel.view 改成 ClassPanel.view 来预览 ClassPanel 模块:
Pastate 的 storeTree 支持多层管理模式,你可使用下面的格式建立多层嵌套的 storeTree:
... const storeTree = { student: StudentPanel.store, class: ClassPanel.store, common: { login: LoginPanel.store, inform: InformPanel.store, } } ...
在容器定义中,你能够简单的 '.'点格式来指明对这些多层嵌套内的 store 节点的引用:
export default makeContainer(LoginPanel, 'common.login') export default makeContainer(InformPanel, 'common.inform')
在实际应用中,咱们通常会使用一个具备 “导航属性” 的容器做为根容器来控制应用多个模块的显示,在咱们的班级信息管理系统中就是导航栏模块 Navigator :
咱们一样建立一个 Navigator 模块的文件夹,在 Navigator.model.js 中,咱们这样定义应用的 state 结构, 而且定义一个 action 来修改选中的标签:
import { createStore } from 'pastate'; const initState = { /** @type {'student' | 'class'} */ selected: 'student' } const actions = { /** @param {'student' | 'class'} tab */ selectTab(tab){ state.selected = tab } } ...
在 Navigator.view.jsx
中,咱们引入另外两个模块的视图容器,并根据 state.selected 的值进行渲染,同时咱们定义导航按钮的响应 action:
... class Navigator extends React.PureComponent{ render(){ /** @type {initState} */ const state = this.props.state; return ( <div> <div className="nav"> <div className="nav-title">班级信息管理系统</div> <div className="nav-bar"> <span className={"nav-item" + (state.selected == 'student' ? " nav-item-active" : "" )} onClick={() => actions.selectTab('student')} > 学生 </span> <div className={"nav-item" + (state.selected == 'class' ? " nav-item-active" : "" )} onClick={() => actions.selectTab('class')} > 课程 </div> </div> </div> <div className="main-panel"> { state.selected == 'student' ? <StudentPanel.view /> : <ClassPanel.view /> } </div> </div> ) } } export default makeContainer(Navigator, ’nav')
接着,咱们把 Navigator.store 挂载到 storeTree,并把 Navigator.view 做为根容器渲染出来:
src/index.js
const storeTree = { nav: Navigator.store, student: StudentPanel.store, class: ClassPanel.store } ReactDOM.render( makeApp(<Navigator.view />, storeTree), document.getElementById('root') );
基本完成,咱们能够到浏览器看看效果!
咱们还差最后一个功能,在导航栏显示学生和课程数量:
这时,咱们遇到了要在一个模块里使用其余模块的 state 状况!
假设没有 storeTree, 咱们通常会想到 “冗余数据” 方法来实现这个功能:在 Navigator 模块的 state 中定义两个值来分别表示学生数和课程数,并定义对应的修改方法供其余模块来修改这两个值,大体以下:
const initState = { /** @type {'student' | 'class'} */ selected: 'student', studentCount: 0, classCount: 0 } const actions = { /** @param {'student' | 'class'} tab */ selectTab(tab){ state.selected = tab }, // 供其余模块调用 updateStudentCount(count){ state.selected = tab }, // 供其余模块调用 updateStudentCount(count){ state.selected = tab } }
若是你不是使用 pastate 或 redux 等独立于 react component 的 state 管理管理框架,在多个组件互相配合联动时或许不得不使用这种模式。但这种模式具备较大的缺陷:当应用日益复杂,你会发现当你作一个简单的操做后,须要去更新不少相关的冗余数据,并且,你很容易忘了去完备地更新冗余数据或重复更新冗余数据。这样一来,应用变得难以开发、修改和维护。
所以,咱们须要使用 “惟一数据源 (SSOT, Single source of truth)” 模式来实现多组件的协做,pastate 的 storeTree 正是惟一数据源模式的一种实现方式。
基本原理:学生和课程的数量信息在学生和课程板块已经包含,若是有哪一个地方须要用到这些数量信息,则在那个地方经过 “惟一源数据” 来 “引用” 或 “计算” 出来。
实现方式:咱们不在 Navigator 的 store 中定义这些数量信息并实现数据同步逻辑,而是直接在 storeTree 中把数量信息引用或计算出来,咱们修改 Navigator.view.js
的 makeContainer 函数的参数:
... export default makeContainer(Navigator, state => ({ state: state.nav, studentCount: state.student.students.length, classCount: state.class.classes.length }))
makeContainer 的第二个参数实际上是一个 state-props 映射器 (mapper), 你能够经过一个映射函数来定义把 store 树的某些 state 节点映射到 Navigator 组件的 props 上。咱们以前使用的 makeContainer(Navigator, 'nav')
是一种简写,等价于下面的形式:
makeContainer(Navigator, state => ({ state: state.nav }))
定义 state-props 映射后,咱们在组件中这样简单地获取 props 中映射入的值:
class Navigator extends React.PureComponent{ render(){ const state = this.props.state; const studentCount = this.props.studentCount; const classCount = this.props.classCount; // 或者使用对象展开语法一块儿获取 const {state, studentCount, classCount} = this.props; return ( <div> ... </div> ) } }
这种模式直接从惟一数据源获取 / 计算出与其余模块的 state 相关的数据,使咱们避免了容易出问题的“冗余数据” 模式。Pastate 模块机制包含了按需渲染引擎,当且仅当任何一个以上的 props "根属性" 节点的值改变时,pastate 会且才会触发组件视图进行从新渲染。
咱们把从惟一数据源经过 组合 或 计算 出的数据称为 storeTree (stateTree) 的衍生数据。若是你使用下面的模式映射 Navigator 组件的 props, 在每次学生和课程的元素内部发生改变但学生和课程数量都没有改变时,也会触发 Navigator 组件的渲染动做:
export default withRouter(makeContainer(Navigator, state => ({ state: state.nav, count: { student: state.student.students.length, class: state.class.classes.length } })))
若是采用这种写法,每次计算生成 count 属性时,都会动态计算出一个匿名的 {student:..., class:...}
对象,假设 count 属性的计算逻辑比较消耗资源,或者 Navigator 的渲染逻辑比较消耗资源的状况下,这会使应用的性能变差。Pastate 实现了一个 衍生数据的缓存工具makeCacheable
,可用来建立可记忆的衍生数据:
import { makeContainer, makeCacheable } from 'pastate'; // getCount 是一个具备记忆功能的 {student:..., class:...} 对象生成器 / 或称为选择器 const getCount = makeCacheable((studentLength, classLength) => ({ student: studentLength, class: classLength })) export default makeContainer(Navigator, state => ({ state: state.nav, count: getCount( state.student.students.length, state.class.classes.length ) }))
makeCacheable 函数参考了函数式编程(Functional programming)的理念,它能够把一个普通计算函数转化为一个具备缓存记忆功能的纯函数(Pure function),而后当咱们在映射 props 值时,把 “计算式” 替代为 “纯函数” (记忆函数)。当调用记忆函数的参数与上一次调用的参数同样时,记忆函数会直接绕过逻辑运算,直接返回上一次计算的结果;当调用记忆函数的参数发生改变时,它才会运行原函数的运算逻辑从新生成一个新的结果,把其缓存起来,并返回新的结果。
使用 makeCacheable 有两个好处:
const mapper = state => ({ boys: state.student.students.map( student => student.isBoy == true) })
那么当学生模块的 state.selected 的值发生改变时(在实际使用过程当中,这种操做会频繁发生改变),都会从新触发数组 map 函数的运行,这是一个性能隐患。咱们引入 makeCacheable 能够避免这个问题:
const getBoys = makeCacheable(students => students.map( student => student.isBoy == true)) const mapper = state => ({ boys: getBoys(state.student.students) })
Pastate 的 makeCacheable 函数的功能与 reselect 库的基础功能相似。若是 makeCacheable 不能你的需求,能够看看 reselect 库。
若是在你的应用存在公共模块,如登陆模态窗、提示模态窗等, 那么你会有这个需求:在多个模块的 actions 引用或改变公共模块的 state,如:
当应用很是简单时, 能够直接把公共模块的 store 引入到其余模块的 actions 中使用:
import { store as loginStore } from '../LoginPanel' const loginState = loginStore.state; const actions = { handleBtn1Click(){ if(loginState.isLogined){ ... } } handleBtn2Click(){ loginState.modelOpened = true } }
这种形式虽然可行,但致使应用难以管理,特别是当应用逐渐复杂时:
这使得模块之间的耦合性加强,使得应用难以升级维护。特别是当不一样模块是由不一样开发者开发的时候,管理这个应用简直就是恶梦。
Pastate 使用面向对象的封装思惟,把模块当作是一个抽象的对象,把模块的 state 当作是其私有(private)成员, 只有在模块内部才能够直接访问和修改。若是其余模块的 actions 须要引用或修改本模块的 state, 须要把相关的引用或修改逻辑封装在本模块的 actions 或 actions.public 中,供其余模块调用:
// LoginPanel.model.js const initState = {...} // 1. Pastate 建议把公用操做或调用放在 actions.public 节点中, // 这样既方便管理,又方便其余模块调用,同时还会获得 pastate 相关中间件的功能支持 // 2. Pastate 建议把对 actions.public 中的每一项使用 jsDoc 作比较详细的注释 const actions = { handleClick(){ ... }, public:{ /** 打开登陆面板 */ openLoginModal(){ ... }, /** * 获取登陆状态 * @return {boolean} 表示是否已登陆 */ isLogined(){ return state.isLogined } }, mutations:{ ... } }
// OtherPanel.model.js import { actions as loginActions } from '../LoginPanel' const actions = { handleBtn1Click(){ if(loginActions.public.isLogined()){ ... } } handleBtn2Click(){ loginActions.public.openLoginModal() } }
这种 public actions 模式定义了一种对于 state 的外部访问和操做的受权机制;每一个 state 的外部访问和操做的具体实现都是在模块自身内部完成的,增长了模块的内聚性;且每一个 state 的外部访问和操做都有具体的命名,使多模块协做的条例更加清晰。
其实,public actions 是一种 “面向接口(Interface))编程” 或 “面向协议(Protocol))编程” 的设计模式。外部模块只需把接口当成一种抽象的模块间通信方式,模块外部只须要知道当前模块通信接口的功能和参数/返回值,不须要知道接口内部的实现逻辑。
下一章,咱们将介绍如何在 pastate 中使用路由等功能来实现大规模的应用程序。