【教程】Pastate.js 響應式 react 框架(六)多模塊應用

這是 Pastate.js 響應式 react state 管理框架系列教程,歡迎關注,持續更新。 javascript

Pastate.js Github 歡迎 star。css

這一章,咱們將講解在 pastate 應用中多模塊應該如何協做。java

store 被多模塊消費

在多模塊應用中,有些組件的視圖須要引用多個模塊的 store 假設有個比較複雜的應用的模塊依賴關係以下:react

view 與 store 多對多依賴關係

  • Module1 和 module2 是最普通的模塊,其 store1 和 store2 只被自身的 view1 和 view2 依賴使用;
  • Module3 具備一些信息須要在其餘模塊同步顯示,如聊天列表要告訴導航欄目前多少條未讀信息,其 store3 還被 view2 引用;
  • Module4 是系統的公用模塊,如登陸模塊,其狀態要被其餘模塊獲知。

模塊之間的互相依賴分爲兩種:git

  1. state 依賴:須要使用到其餘模塊的 state, 如根據是否已登陸實現不一樣的視圖渲染邏輯或作不一樣的 action 處理
  2. actions 依賴:須要觸發其餘應用的 actions, 如觸發登陸模態窗或通知模態窗

所以,在多模塊應用中,咱們先把各個模塊的 store 鏈接一棵 store 樹, 以下: github

store 樹

而後指定各 component 引用這顆 store 樹的哪些(一個或多個)節點的 state, 生成 Container: 編程

使用多個 store 的數據

構建 storeTree 並引用其節點

咱們先把上一章的 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.model.js
    • ClassPanel.view.js
    • ClassPanel.css
    • index.js

一樣,在 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

咱們還差最後一個功能,在導航欄顯示學生和課程數量:

在導航欄顯示學生和課程數量

這時,咱們遇到了要在一個模塊裏使用其餘模塊的 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 的衍生數據

咱們把從惟一數據源經過 組合計算 出的數據稱爲 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 有兩個好處:

  • 省略計算過程:在咱們的班級系統例子中,假設咱們要衍生出一個男生數組,若是咱們直接進行 map:
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)
})
  • 保持對象或數組結果的引用:仍是這個衍生出男生數組的例子,若是不使用 makeCacheable,那麼即便 state.student.students 不變, 每次計算出的 boys 數組的引用是不同的,都會觸發組件從新渲染;若是使用 makeCacheable, 那麼只要 state.student.students 不變,計算出的 boys 數組的引用是同樣的,不會觸發組件進行沒必要要的渲染。

Pastate 的 makeCacheable 函數的功能與 reselect 庫的基礎功能相似。若是 makeCacheable 不能你的需求,能夠看看 reselect 庫。

調用其餘模塊的 actions

若是在你的應用存在公共模塊,如登陸模態窗、提示模態窗等, 那麼你會有這個需求:在多個模塊的 actions 引用或改變公共模塊的 state,如:

  • 根據是否已登陸,作不一樣的 actions 邏輯處理(引用其餘模塊的 state)
  • 在其餘模塊觸發登陸模態窗讓用戶進行登陸 (修改其餘模塊的 state)

當應用很是簡單時, 能夠直接把公共模塊的 store 引入到其餘模塊的 actions 中使用:

import { store as loginStore } from '../LoginPanel'
const loginState = loginStore.state;

const actions = {
    handleBtn1Click(){
        if(loginState.isLogined){
            ...
        }
    }
    handleBtn2Click(){
        loginState.modelOpened = true
    }
}

這種形式雖然可行,但致使應用難以管理,特別是當應用逐漸複雜時:

  • 你很容易不知道公共 state 在哪裏被如何改變了;
  • 當你須要更新公共 state 的定義時,須要同時去更新多個模塊中引用到該 state 的 actions 。

這使得模塊之間的耦合性加強,使得應用難以升級維護。特別是當不一樣模塊是由不一樣開發者開發的時候,管理這個應用簡直就是惡夢。

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 中使用路由等功能來實現大規模的應用程序。

相關文章
相關標籤/搜索