這是 Pastate.js 響應(yīng)式 react state 管理框架系列教程,歡迎關(guān)注趣斤,持續(xù)更新。Pastate.js Github
模塊化實戰(zhàn)任務(wù)
如果應(yīng)用比較復(fù)雜杏节,有很多個頁面唬渗,且一個界面具有比較多的組件和操作時典阵,我們需要對應(yīng)用劃分模塊 (Module) 進行管理。
下面我們以一個 班級信息管理系統(tǒng) 為例镊逝,介紹 pastate 應(yīng)用的模塊化機制壮啊。
實際體驗:https://birdleescut.github.io/pastate-demo
應(yīng)用源碼: https://github.com/BirdLeeSCUT/pastate-demo
應(yīng)用的原型如下:
(1) 學(xué)生板塊
- 獲取并顯示學(xué)生信息
- 修改學(xué)生信息
(2) 課程板塊: 顯示課程信息
這個應(yīng)用相對比較簡單,在實際開發(fā)中我們不一定需要對其進行模塊化設(shè)計撑蒜,在此我們只是用于介紹 pastate 模塊化機制歹啼,讓你知道如何用 pastate 處理足夠復(fù)雜的應(yīng)用。
模塊劃分
模塊化設(shè)計的第一步就是模塊劃分座菠,我們先從對我們的班級信息管理系統(tǒng)進行模塊劃分:
- 導(dǎo)航模塊: Navigator
- 學(xué)生信息模塊: StudentPanel
- 課程信息模塊: ClassPanel
pastate 模塊構(gòu)成
pastate 的模塊機制是一種簡潔的 flux 模式實現(xiàn)狸眼,一個 pastate 模塊由三個基本元素構(gòu)成:
- 狀態(tài) (state):保存模塊當(dāng)前的狀態(tài)
- 視圖 (view):模塊狀態(tài)的顯示邏輯
- 動作 (action):模塊動作的處理邏輯
這三個模塊元素遵循如下的單向數(shù)據(jù)流過程:
模塊結(jié)構(gòu)
我們通過一個文件夾組織一個模塊,以 “學(xué)生信息模塊” StudentPanel 為例浴滴,新建一個 StudentPanel 文件夾拓萌,并在文件夾下創(chuàng)建以下文件:
-
StudentPanel
模塊文件夾-
StudentPanel.model.js
模型文件:用于定義應(yīng)用的 state 和 actions -
StudentPanel.view.jsx
視圖文件:用于定義的視圖組件(組件渲染邏輯) -
StudentPanel.css
樣式文件:用于定義用于的樣式(可以改用 less 或 sass)
-
模型文件 *.model.js
(1)設(shè)計模塊的 state
我們先在模型文件 StudentPanel.model.js 下定義模塊的 state 結(jié)構(gòu):
StudentPanel.model.js
const initState = {
initialized: false, // 初始化狀態(tài)
/** @type { "loading" | "ok" | "error" } */
status: 'loading', // 加載狀態(tài)
isEditting: false, // 是否在編輯中
selected: 0, // 選中的學(xué)生
/** @type {studentType[]} */
students: [] // 學(xué)生數(shù)組
}
const studentType = {
name: '張小明',
studentNumber: '2018123265323',
age: 22,
isBoy: true,
introduction: '我是簡介'
}
...
與之前一樣,我們通過配合 jsDoc 注釋升略,把 state 結(jié)構(gòu)的定義和初始值得定義一起進行微王。
Tips: 建議使用上面定義 status 屬性的模式定義 “枚舉字符串” 類型,對這種枚舉值進行賦值時盡量采用 intelSence 的 “選擇” 方法而非直接輸入字符串品嚣,這可以為應(yīng)用的開發(fā)帶來方便并減少無畏的錯誤:賦值時把輸入光標(biāo)在等號后的引號中間按下 “觸發(fā)提示” 快捷鍵即可顯示選項:
State mock 區(qū)域: 我們在開發(fā)視圖時炕倘,需要對 state 的狀態(tài)進行完備測試,比如要讓 state.status 分別等于 "loading" | "ok" | "error" 翰撑、讓 state.isEditting 等于 true | false 去完備地測試模塊的渲染邏輯罩旋,這時我們不要直接更改 initState 的值,而是把 initState 下方作為一個 state mock 測試區(qū)域, 對 state 進行修改以實現(xiàn) mock :
const initState = {...}
const studentType ={...}
/***** MOCK AREA *****/
// initState.status = 'ok'
// initState.isEditting = true
// initState.students = [studentType, studentType]
你可以根據(jù)開發(fā)調(diào)試需求新建 mock 行眶诈,或通過注釋控制某個 mock 行是否生效涨醋,以此來使應(yīng)用處于某個中間 state 狀態(tài),方便調(diào)試册养。并在模塊開發(fā)完成時把 mock 區(qū)域全部注釋即可东帅,這種模式可以有效地管理 mock 過程压固。
模塊的 initState 是對模塊的一種 “定義”球拦,它具有“文檔屬性”,而 mock state 是對應(yīng)用進行調(diào)試時執(zhí)行的臨時動態(tài)操作帐我,如果通過直接修改 initState 來進行 mock坎炼,我們會破壞模塊的定義,然后又嘗試憑記憶對定義進行恢復(fù)拦键,這個過程容易出錯或遺漏谣光,特別是當(dāng) state 變得復(fù)雜的時候。所以我們推薦采用 MOCK AREA 對 state 進行 mock 調(diào)試芬为。
(2)定義模塊的 actions
之前我們是把應(yīng)用的動作邏輯實現(xiàn)為視圖組件的成員函數(shù)萄金,在應(yīng)用簡單時這種模式會比較直接方便蟀悦,而當(dāng)應(yīng)用復(fù)雜且某些操作邏輯需要在不同組件甚至模塊間共享時,原理的模式無法實現(xiàn)氧敢。因此我們把模塊的相關(guān)操作邏輯統(tǒng)一放在 actions 中進行管理:
StudentPanel.model.js
const actions = {
init(){ },
loadStudents(){ },
switchEditting(){ },
/** @param {number} index 學(xué)生數(shù)組索引號 */
selectStudent(index){ },
increaseAge(){ },
decreaseAge(){ }
}
在初步的 actions 聲明階段日戈,我們只需把 actions 的名字和參數(shù)聲明出來,在應(yīng)用開發(fā)過程中再逐漸實現(xiàn)其業(yè)務(wù)邏輯孙乖。你可以考慮使用 jsDoc 對 action 的用途和參數(shù)進行注釋說明浙炼。當(dāng)模塊簡單的時候,你可以直接在 actions 中直接實現(xiàn)同步更新 state 的操作和異步從后臺獲取數(shù)據(jù)等操作唯袄,pastate 不對 actions 的實現(xiàn)的內(nèi)容做限制弯屈,不需要像 redux 或 vuex 一樣規(guī)定一定要把同步和異步邏輯的分開實現(xiàn),在 pastate 中恋拷,當(dāng)你認(rèn)為有必要時才那樣做就好了资厉。
多級 actions 管理: 當(dāng)模塊的 actions 比較多的時候,我們可以采用多級屬性的模式對 actions 進行分類管理, 具體的分類方法和分類級別根據(jù)具體需要自行定義即可蔬顾,如下:
const actions = {
init(){ },
handle:{
handleBtnClick(){ },
handleXxx1(){ },
handleXxx2(){ }
},
ajax:{
getStudentsData(){ },
getXxx(){ },
postXxx(data){ }
}
}
** mutations 模式**: 如果你的模塊比較復(fù)雜酌住,想遵循 redux 或 vuex 把對 state 同步操作 和 異步動作兩類操作分類管理的模式,那么你可以對 state 的同步操作放在 actions.mutations 分類下阎抒,pastate 提供特殊中間件對 mutations 提供而外的開發(fā)調(diào)試支持酪我,詳見 規(guī)模化 章節(jié)且叁。
const actions = {
init(){ },
handleBtnClick(){ },
getStudentsData(){ },
mutations:{
increaseAge(){ },
decreaseAge(){ }
}
}
Mutations 其實就是一些同步的 state 更新函數(shù),你可以通過其他普通 actions 調(diào)用 mutations, 或直接在視圖中調(diào)用 mutations。比起 redux dispatch actions to reducers 和 vuex commit mutations 通過字符串 mutations 名稱發(fā)起(dispatch) 的模式未妹,這種函數(shù)調(diào)用的方式在開發(fā)時更加方便且不易出錯:
- 無需為了調(diào)用方便埋凯,定義 actions / mutation 的常量名稱
- 可以友好的支持 編輯器/ IDE 的智能提示
如果你選擇使用 pastate 的 mutations 機制, 那么每個 mutation 都要使用同步函數(shù),不要在 mutation 中使用 ajax 請求或 setTimeout 或 Promise 等異步操作塞关。這樣相關(guān)的瀏覽器 devtools 才能夠顯示 有準(zhǔn)確意義 的信息:
這種 actions 分類管理的設(shè)計體現(xiàn)了 pastate 的精益原則:你能在需要某些高級特性的時候 才去 且 能夠 使用這些高級特性蜻牢。
(3)創(chuàng)建并配置模塊的 store
我們可以像之前那樣簡單地創(chuàng)建 store:
StudentPanel.model.js
import { Pastore } from 'pastate'
const initState = {...}
const actions = {...}
...
const store = new Pastore(initState);
/** @type {initState} */
let state = store.state;
export { initState, actions, store}
Pastate 采用一種 可選的 的 actions 注入模式,你可以自愿決定是否把 actions 注入 store框往。 把 actions 注入 store 后清焕,可利用 pastate 的中間件機制對 actions 進行統(tǒng)一管理控制,具有較強的可擴展性键畴。例如我們可以使用 logActions 中間件對每次 actions 的調(diào)用在控制臺進行 log突雪,并使用 dispalyActionNamesInReduxTool 中間件 對把 mutations 名稱顯示出來咏删,以便于調(diào)試:
import { ..., logActions, dispalyActionNamesInReduxTool } from 'pastate'
...
const store = new Pastore(initState);
store.name = 'StudentPanel';
store.actionMiddlewares = [logActions(), dispalyActionNamesInReduxTool(true)]
store.actions = actions;
/** @type {initState} */
let state = store.state;
export { initState, actions, store}
如果你覺得上面的定義方式比較瑣碎督函,你可以直接使用 pastate 提供的工廠函數(shù) createStore 來定義一個完整地 store:
import { ..., createStore, logActions, dispalyActionNamesInReduxTool } from 'pastate'
const store = createStore({
name: 'StudentPanel',
initState: initState,
actions: actions,
middlewares: [logActions(), dispalyActionNamesInReduxTool(true)]
})
const { state } = store // createStore 具有良好的泛型定義侨核,無需額外的 jsdoc 注釋即可獲取 state 的結(jié)構(gòu)信息
你也可以進一步把中間件配置為僅在開發(fā)環(huán)境下生效的模式, 生產(chǎn)環(huán)境下無效搓译。Pastate 中間件的詳細(xì)內(nèi)容請查看規(guī)耐慵Γ化章節(jié)涯冠。
視圖部分
我們創(chuàng)建 StudentPanel.view.jsx 文件來保存我們的模塊視圖, 視圖定義和原來的模式類似:
StudentPanel.view.jsx
import React from 'react'
import { makeContainer, Input, Select} from 'pastate'
import { initState, actions } from './StudentPanel.model'
import './StudentPanel.css'
const isBoyOptions = [{
value: true,
tag: '男'
},{
value: false,
tag: '女'
}]
class StudentPanel extends React.PureComponent {
componentDidMount(){
actions.init()
}
render() {
let state = this.props.state
return (
<div className="info-panel">
{this['view_' + state.status](state)}
</div>
)
}
view_loading() {
return (
<div className="info-panel-tip-loading">
加載中...
</div>
)
}
view_error() {
return (
<div className="info-panel-tip-error">
加載失敗, 請刷新重試
</div>
)
}
/** @param {initState} state */
view_ok(state) {
let selectedStudent = state.students[state.selected];
return (
<div className="info-panel-ok">
...
</div>
)
}
}
export default makeContainer(StudentPanel)
Pastate 模塊化需要實現(xiàn)一種多模塊可以互相協(xié)作的機制。因此我們不再使用 makeOnyContainer 唯一地綁定一個視圖組件與對應(yīng)的 store派任。首先,我們會用各模塊的 store 生成一個全局的 store 樹,并使用 makeContainer 把模塊的視圖封裝為引用全局 store 的某些節(jié)點的容器篓像。
我們目前只有一個模塊遗淳,此處簡單地調(diào)用 makeContainer(StudentPanel)
讓 StudentPanel 引用全局的 store 樹 的根節(jié)點的 state ,我們可以為 makeContainer
指定第二個參數(shù)屈暗,指明引用 store 樹 的哪些子節(jié)點,詳情會在下一章介紹脂男。
在上面視圖組件的代碼中,我們引入了 model 中的 actions:
import { store, initState, actions } from './StudentPanel.model'
這些 actions 可以直接賦值到組件的 onClick 或 onChange 等位置:
<button className="..." onClick={actions.increaseAge} > + </button>
這些 actions 也可以在組件的生命周期函數(shù)中調(diào)用:
...
componentDidMount(){
actions.init()
}
...
視圖部分還包含樣式文件 StudentPanel.css 宰翅,在此就不列出了。
如果該模塊要需要封裝一些當(dāng)前模塊專用的子組件汁讼,把子組件定義為獨立的文件淆攻,并放在與 StudentPanel 模塊相同的文件夾下即可。如果需要封裝一些多個模塊通用的非容器組件瓶珊,可以考慮把它們放在獨立于模塊文件夾的其他目錄。
導(dǎo)出模塊
最后,為了方便調(diào)用蝉娜,我們來為模塊做一個封裝文件 StudentPanel / index.js南缓,導(dǎo)出模塊的元素:
export { default as view } from './StudentPanel.view'
export { store, actions, initState } from './StudentPanel.model'
pastate 模塊向外導(dǎo)出 view, initState, actions, store 四個元素荧呐。
大功告成!這時我們可以嘗試在 src / index.js 中引入該模塊并渲染出來:
import ReactDOM from 'react-dom';
import { makeApp } from 'pastate';
import * as StudentPanel from './StudentPanel';
ReactDOM.render(
makeApp(<StudentPanel.view />, StudentPanel.store),
document.getElementById('root')
);
...
我們使用 makeApp 函數(shù)創(chuàng)建一個 pastate 應(yīng)用并渲染出來获雕,makeApp 的第一個參數(shù)是 根容器薄腻,第二個參數(shù)是 store 樹收捣, 我們現(xiàn)在只有一個模塊届案,所以應(yīng)用的 store 樹只有 StudentPanel 的 store。
自此罢艾,我們的第一個模塊 StudentPanel 構(gòu)建完成楣颠。
模塊的模板文件
我們可以使用模板文件快速創(chuàng)建模塊,一個模塊的模板文件非常簡單咐蚯,下面以 TemplateModule
模塊為例完整給出:
/index.js
export { default as view } from './TemplateModule.view'
export { initState, actions, store } from './TemplateModule.model'
/TemplateModule.model.js
import { createStore } from 'pastate';
const initState = {
}
const actions = {
}
const store = createStore({
name: 'TemplateModule',
initState,
actions
})
const { state } = store
export { initState, actions, store }
/TemplateModule.view.jsx
import React from 'react';
import { makeContainer } from 'pastate';
import { initState, actions } from './ClassPanel.model';
import './TemplateModule.css'
class TemplateModule extends React.PureComponent{
render(){
/** @type {initState} */
const state = this.props.state;
return (
<div>
TemplateModule
</div>
)
}
}
export default makeContainer(TemplateModule, 'template')
/.css
// css 樣式文件初始為空童漩,你也可以選用 less 或 sass 來定義樣式
這個例子的 demo 源碼已包含該模板模塊 src/TemplateModule, 你只需把它復(fù)制到你的 src 目錄下,并右鍵點擊模塊文件夾春锋,選擇 “在文件夾中查找”矫膨,然后把 TemplateModule 字符串全部替換為你想要的模塊名稱即可:
點擊替換之后保存文件。不過目前還不能自動替換文件名期奔,需要手動替換一下侧馅。
Pastate 以后將會實現(xiàn)相關(guān)的命令行工具,實現(xiàn)一行命令創(chuàng)建新模塊等功能呐萌,加速 pastate 應(yīng)用的開發(fā)馁痴。
下一章,我們來創(chuàng)建另外的模塊肺孤,并介紹不同模塊之間如何協(xié)作罗晕。