小巧玲瓏的react框架(第二彈)正式命名--aomini

FE_xuer發表於2017-10-01

前戲

曾經我以為已經過了看偶像劇的年紀,後來無意中看了泰版浪漫滿屋,然後控寄不住寄己的體內的洪荒之力,繼續追完了泰版的惡作劇之吻,--有顏+演技時刻線上(雖然還是偶像劇狗血套路,不過不影響我被圈粉)。我忽然發現,原來熱情還在,只是現在大多國內的流量小生小花們已經挑逗不到我了。看看國內現在的電視劇,生硬的臺詞,僵硬的表情,像一盆盆冷水一點點澆涼我對劇的熱情,有時候不禁會想,難道沒人對自己的作品負責嗎?哦,大家並沒有進錯房間,這不是在聊電視劇,我們這還是技術文,所以我就不繼續吐槽置評了,我曾經下定過決心,只要一天還在出作品,就要對自己的東西負點責任。情懷,是個好東西,得留著點。回到我們的主題,我們這個小框架再不濟也是個框架,得起個名字先。我被這兩部泰劇圈粉主要是因為女主aom可愛又認真的戲,乾脆就叫aom了,而且我希望這是個方便任意玩耍,輕量迷你的小框架,所以,我給它起個名字叫做--aomini。大家不要被我帶跑偏了,我安利的不是這兩個泰劇,是介紹我的aomini,?。
先回顧上一篇我只是用最簡單的程式碼介紹了一下aomini的思路,那樣的破磚爛瓦不修整一番怎麼能用呢?好了,接下來我就帶著大家一起來修整一番。

激情

如果大家不記得上一篇講的啥破磚爛瓦,可以戳這裡回去看看。上一篇完全是以糊塗流水賬的形式蓋的破瓦房,房屋結構啥的都是一股腦疊上去的,毫無結構可言,接下來我們來將這個破房子分解一下。首先,底層結構跟上層結構得先拆開,底層結構屬於libs,上層結構就是業務邏輯,也就是我們肉眼可見的那一層,入口檔案,以及那幾個模組檔案肯定都屬於業務邏輯了,至於store,Provider和connect函式肯定屬於libs範疇了。拆解後檔案結構如下所示,


相信使用過react-redux的同學對libs下的這三個名稱應該很熟悉了,不過我們這裡跟react-redux中還是有很大區別,雖然思路還是類似的,閱讀過react-redux原始碼的同學可能會比較清楚一點,那接下來就對我們的框架的實現思路進行深入講解一下。雖然並沒有那麼複雜,但是為了照顧不那麼熟悉的同學,分析之前,我們先搞清楚這三個東西的具體指責是什麼,其中Store主要可以說是state的大總管;Provider可以說是入口,store會從這個入口props的方式傳入,通過react context 上下文的方式進行全域性定義,方便業務下的其他元件獲取呼叫,這種方式就是為了解決任意元件之間通訊非相關元件需要感知的問題;connect其實是個HOC高階元件。好了,再讓我們回顧一下我們簡單的框架模型,store<=>UI,對比一下react原生的框架模型是這樣的state<=>UI,對比這兩個模型可以得出一個簡單的Store的設計思路,store管理了state,並且當UI觸發Store中狀態更新時,由Store來呼叫元件setState進行模組的UI更新。根據這麼個直白的思路設計的Store物件程式碼如下,與之前出入不大:

let store = {
    //目標元件物件
    contexts:[],
    //state資料
    state:{
        m1Var:"111"
    },
    //註冊繫結元件與狀態資料,用於setState
    bindContext(_context){
        this.contexts.push(_context);
        return this.state;
    },
    unbindContext(_context){
        for(let i = 0; i < this.contexts.length; i++){
            if(this.contexts[i] === _context){
                this.contexts[i] = null;
                console.log(_context);
            }
        }
    },
    setState(bs_state,context){
        let newState = Object.assign(this.state,bs_state);
        console.log(".......",newState)
        // this.state = {};
        this.contexts.map((item)=>{
            if(!item) return;
            React.Component.prototype.setState.call(item,bs_state);
        })

    }
}複製程式碼

store沒有做太大變化,只是完善了一下,元件裝載時,呼叫bindContext將元件的上下文關係新增到contexts列表,當資料中心狀態變化時,呼叫store的setState方法,該方法實質是將所有繫結的元件進行setState呼叫。如此,便簡單實現了狀態變化與UI的資料流關係。
每個元件都需要接收store物件,因此就需要將store放入頂層父元件的context中,也就是Provider這個入口元件,該元件程式碼如下:

class Provider extends React.Component{
    getChildContext(){
        return {
            store:this.props.store
        }
    }
    constructor(props){
        super(props);
    }
    render(){
        return React.Children.only(this.props.children);
    }
}
Provider.childContextTypes = {
    store:PropTypes.object
}複製程式碼

該元件與redux中一樣,子元件只接受單個元素。然後就是將props中傳入的store元素進行變數的全域性化,當然這裡暫時實現比較粗暴,我們還沒有做到像redux一樣過多考慮個性化擴充套件,但說白了redux實現核心也就是這麼個玩意兒。
接下來我們再回過頭來觀察一下上一篇的元件程式碼,每一個模組都有一堆樣板程式碼,store.bindContext(this);尤其之前只是單檔案簡單的合併寫法,模組拆成獨立檔案後,store又是需要從上下文中引用的,因此樣板程式碼至少又這些:
1,元件裝載的時候

let store = context.store;
store.bindContext(this);複製程式碼

2,元件解除安裝的時候需要移除被解除安裝的元件繫結關係

componentWillUnmount(){
    this.store.unbindContext(this);
}複製程式碼

每個元件都要在邏輯中如此呼叫,需要封裝一下,況且,業務元件最好只關注業務點,這種非業務邏輯的樣板程式碼不該出現在業務元件中,那這種通用邏輯呼叫的情況下,我們的高階元件就需要派上用場了。類似redux中的connect元件程式碼如下:

let connect = function(wrappedCompo){
    class ConnectHoC extends React.Component{
        constructor(props,context){
            super();
            this.store = context.store;
            this.store.bindContext(this);
        }
        render(){
            return React.createElement(wrappedCompo,{store:this.store});
        }
        componentWillUnmount(){
            this.store.unbindContext(this);
        }
    }
    ConnectHoC.contextTypes = {
        store:PropTypes.object
    }

    return ConnectHoC;
}複製程式碼

這個connect高階元件接收一個實際業務元件為引數,返回一個包含該元件的新元件,並實現了store通過props的傳遞,實際業務元件獲取store直接從props即可,也許你會問store已經存在context中,直接去取就是了,為啥還要通過props傳入,還是一樣的道理,那樣的話業務元件會多一些獲取全域性變數的樣板程式碼,並且違背了業務元件只需要關注業務的原則。好了,現在這麼一來,我們的業務元件只需要關注自己的業務點以及資料在哪裡獲取,無需過度關心其他。
並且就跟react-redux中使用是一樣的,元件都使用connect進行包裝即可,虛擬碼如下:

class Module extends React.Component{
    constructor(){
        super();
    }
    render(){

    }
}
let ModuleHoC = connect(Module);複製程式碼

因此,Provider元件下的所有經過connect包裝過的元件裝載都會自動進行上下文繫結,並使用store集中管理資料狀態,當狀態改變,所有繫結的元件進行setState操作,即完成了UI的更新。

高潮

很明顯,這個設計思路就是來源於react-redux的設計思想,react-redux是個好東西,只是我在實踐中發覺,react本身其實已經相當好用了,只是有些地方比如狀態管理部分只用原生react的話,開發過程會變得十分不優雅,既然如此,那我們或許只需要將react中不那麼優雅的東西適當摺疊修整一番,就能變得更好用了。react-redux中強調邏輯與檢視之間完全分離,這是很好的思想,但是無論flux或者redux的設計初衷,其實都是為了更合理地管理狀態而來的,react的設計原本就並未將UIrender與邏輯完全割裂,並非像vue中將template完全進行了剝離。況且,邏輯與UI的分離並非必須分到兩個不同的檔案中去才是分離,這樣反而從編碼層面上帶來不少的干擾,缺少碼程式碼的快感。
不過目前來說,aomini雖然實現了功能,但是絕對比不上react-redux在整體設計上的優雅。react-redux是基於redux的動作訂閱模式,使用了監聽者模式,每一個模組的設計都是根據單一職責的原則,UI中觸發state的更新必須通過派發一個action,通過監聽器進行監聽每一個action,不過這裡的監聽器是弱化了的,並沒有針對每一個action進行細分監聽,這裡大家需要注意,其實這個設計是很巧妙且又合理的,因為react是基於狀態變化的,而非我們之前熟悉的命令式開發模式,一切都是圍繞整體的狀態,而非某個動作,因此無論action如何指示,最終只是轉化成整體狀態變化的讀取。reducers進行狀態計算,並使用connect元件的引數mapStateToProps函式作為狀態選擇器,進行子狀態select,完美地解決了特定狀態的讀取,以上處理就優雅地完成了UI=>store=>action=>reducer=>UI這樣一個完整閉環。aomini上有flux和react-redux的影子,但是本質上來說,到目前為止,aomini跟後兩者並不是同一種東西,flux是一套完整的web架構思想,react-redux是從flux發展而來的比較完善的開發框架,而aomini是從兩者之中取出的部分思想,變得更加靈活,儘管不算是一個完整的框架,不過我從實際開發中感受到,配合原本已經足夠完美的react,已經夠用了。

事後--結語

框架上的東西沒有絕對的好與不好,殺雞用牛刀未嘗不可,只是這把刀不可太重,不然殺只雞比宰一頭牛還累,虧本買賣。一個小工程,總共業務幾句程式碼,非得上一堆牛逼哄哄的框架,最後出來幾大m的程式碼量,反而拖累頁面效率,這不就是得不償失。以上對aomini的核心程式碼設計思路介紹了一下,目前只是一些核心骨幹程式碼,後續接下來就是需要繼續完善其中的細節,程式碼已上傳github,並實時進行更新,有興趣的同學們就敬請關注了。希望跟大家一起交流。
需要的同學,可以戳這裡簽出程式碼:react-aomini
會繼續實時更新,歡迎各位拍磚!

相關文章