使用MVVM升級您的React架構(gòu)

您是否曾經(jīng)打開過一個(gè)項(xiàng)目并遭受了痛苦折磨揩悄,因?yàn)槟吹搅思词故枪铝⒌臈U也不想觸及的丈积,難以理解且難以維護(hù)的JavaScript代碼捕传?因?yàn)槿绻|摸它惠拭,一切都會崩潰,就像一個(gè)大的積木塊一樣庸论。

JavaScript很容易拿起并從編碼開始职辅,但是以錯(cuò)誤的方式做起來甚至更容易。對于小型項(xiàng)目聂示,低質(zhì)量的代碼不會給公司帶來高風(fēng)險(xiǎn)域携,但是,如果一個(gè)項(xiàng)目規(guī)模變大鱼喉,您最終將承擔(dān)技術(shù)債務(wù)秀鞭,這些債務(wù)將在每個(gè)截止日期前消失趋观,并最終吞噬您。沒有人會想碰這種代碼锋边。因此皱坛,在本文中,我們將看到如何將Model-View-ViewModel(MVVM)架構(gòu)模式應(yīng)用到React項(xiàng)目中豆巨,并顯著提高代碼質(zhì)量剩辟。

根據(jù)定義,架構(gòu)模式提供了一組預(yù)定義的子系統(tǒng)往扔,指定了它們的職責(zé)贩猎,并包括用于組織它們之間的關(guān)系的規(guī)則和準(zhǔn)則。

許多架構(gòu)模式都在嘗試解決與MVVM相同的挑戰(zhàn)-使您的代碼松散耦合萍膛,可維護(hù)且易于測試融欧。

有人可能會問:“如果我已經(jīng)知道如何使用FluxRedux,為什么還要煩惱自己學(xué)習(xí)MVVM或任何其他架構(gòu)模式卦羡?”
-答案是:你不就得了噪馏!例如,如果Redux非常適合您的項(xiàng)目和團(tuán)隊(duì)绿饵,請堅(jiān)持使用欠肾。另一方面,如果您不知道其他任何模式拟赊,您怎么能百分百確定Redux是您項(xiàng)目的理想選擇呢刺桃?即使可能有更好的選擇,您也將迫使Redux進(jìn)入每個(gè)項(xiàng)目吸祟。這里唯一明智的決定是學(xué)習(xí)新的建筑模式瑟慈。讓我們從MVVM開始。

了解模式的最佳方法是弄臟雙手屋匕,然后嘗試一下葛碧。我們將創(chuàng)建pokemon go演示應(yīng)用陣營和MobX(在這個(gè)完整的代碼)。MobX是用于簡單和可擴(kuò)展?fàn)顟B(tài)管理的庫过吻。它的作用與Redux相同进泼,但與Redux不同,它沒有為我們提供有關(guān)如何構(gòu)建應(yīng)用程序的準(zhǔn)則纤虽。MobX 為我們提供了可觀察的功能(觀察者模式)以及一種將依賴項(xiàng)注入到我們的組件中的方法乳绕。它跟MVVM就像面包去與黃油。

深入MVVM

MVVM有四個(gè)主要模塊:

  • 用戶與之交互的view -UI層逼纸,
  • ViewController —可以訪問ViewModel并處理用戶輸入洋措,
  • ViewModel -可以訪問Model 并處理業(yè)務(wù)邏輯,
  • Model -應(yīng)用程序數(shù)據(jù)源

繼續(xù)閱讀以了解MVVM中的這些組件如何相互關(guān)聯(lián)以及它們的職責(zé)是什么杰刽。

View

借助React菠发,我們正在構(gòu)建用戶界面王滤,而這正是我們大多數(shù)人已經(jīng)熟悉的。該view是與您的應(yīng)用程序的用戶的唯一接觸點(diǎn)雷酪。用戶將與您的View交互,這將根據(jù)事件(例如鼠標(biāo)移動涝婉,按鍵等)觸發(fā)ViewController方法哥力。該View不僅用于用戶輸入,還用于顯示輸出(某些操作的結(jié)果)墩弯。
view不能交互吩跋,是React.Component這意味著它應(yīng)該只用于顯示數(shù)據(jù)和從ViewController觸發(fā)所述傳遞事件中使用的。這樣渔工,我們使組件可重復(fù)使用且易于測試锌钮。在MobX的幫助下, 我們將轉(zhuǎn)向 React.Component變成反應(yīng)式組件引矩,它將觀察到任何變化并相應(yīng)地自動更新梁丘。

import React from 'react'
import PokemonList from './UI/PokemonList'
import PokemonForm from './UI/PokemonForm'

class PokemonView extends React.Component {
    render() {
        const {
            pokemons,
            pokemonImage,
            pokemonName,
            randomizePokemon,
            setPokemonName,
            addPokemon,
            removePokemon,
            shouldDisableSubmit
        } = this.props

        return (
            <React.Fragment>
                <PokemonForm
                    image={pokemonImage}
                    onInputChange={setPokemonName}
                    inputValue={pokemonName}
                    randomize={randomizePokemon}
                    onSubmit={addPokemon}
                    shouldDisableSubmit={shouldDisableSubmit}
                />
                <PokemonList
                    removePokemon={removePokemon}
                    pokemons={pokemons}
                />
            </React.Fragment>
        )
    }
}

export default PokemonView

注意: PokemonList組件是用@observer裝飾器裝飾的,而不是使用常規(guī)函數(shù)的observer(class PokemonList {...})
裝飾器默認(rèn)情況下不支持裝飾器旺韭,因此氛谜,如果要使用它們,則需要babel插件区端。

ViewController

ViewControllerview的大腦-它擁有所有查看相關(guān)邏輯和擁有的一個(gè)對應(yīng)的ViewModel值漫。該view是不知道ViewModel的,它是依靠ViewController织盼,以通過所有必要的數(shù)據(jù)和事件杨何。 ViewControllerViewModel之間的關(guān)系是一對多的-一個(gè)ViewController可以引用不同的ViewModel
處理用戶輸入不應(yīng)留給ViewModel沥邻,而應(yīng)在ViewController會將干凈的準(zhǔn)備好的數(shù)據(jù)傳遞給ViewModel危虱。

import React from 'react'
import PokemonView from './PokemonView'

class PokemonController extends React.Component {
    state = {
        pokemonImage: '1.gif',
        pokemonName: ''
    }

    setRandomPokemonImage = () => {
        const rand = Math.ceil(Math.random() * 10)
        this.setState({ pokemonImage: `${rand}.gif` })
    }

    setPokemonName = (e) => {
        this.setState({ pokemonName: e.target.value })
    }

    clearPokemonName() {
        this.setState({ pokemonName: '' })
    }

    savePokemon = () => {
        this.props.ViewModel.addPokemon({
            image: this.state.pokemonImage,
            name: this.state.pokemonName
        })
    }

    addPokemon = () => {
        this.savePokemon()
        this.clearPokemonName()
    }

    removePokemon = (pokemon) => {
        this.props.ViewModel.removePokemon(pokemon)
    }

    render() {
        const { ViewModel } = this.props

        return (
            <PokemonView
                pokemons={ViewModel.getPokemons()}
                pokemonImage={this.state.pokemonImage}
                randomizePokemon={this.setRandomPokemonImage}
                setPokemonName={this.setPokemonName}
                addPokemon={this.addPokemon}
                removePokemon={this.removePokemon}
                pokemonName={this.state.pokemonName}
                shouldDisableSubmit={!this.state.pokemonName}
            />
        )
    }
}

export default PokemonController

ViewModel

ViewModel是誰生產(chǎn)商,并不關(guān)心誰消耗的數(shù)據(jù); 它可以是React組件唐全,Vue組件槽地,飛機(jī)甚至是母牛,根本不在乎芦瘾。由于ViewModel只是一個(gè)常規(guī)的JavaScript類捌蚊,因此可以使用不同的UI輕松地在任何地方重用。ViewModel所需的每個(gè)依賴項(xiàng)都將通過構(gòu)造函數(shù)注入近弟,從而使其易于測試缅糟。該ViewModel與直接交互模式,并且只要ViewModel更新它祷愉,所有的變化會自動反映回View。

class PokemonViewModel {
    constructor(pokemonStore) {
        this.store = pokemonStore
    }

    getPokemons() {
        return this.store.getPokemons()
    }

    addPokemon(pokemon) {
        this.store.addPokemon(pokemon)
    }

    removePokemon(pokemon) {
        this.store.removePokemon(pokemon)
    }
}

export default PokemonViewModel

Model

Model充當(dāng)數(shù)據(jù)源,即付材。應(yīng)用程序的全局存儲逐虚。它可以組合來自網(wǎng)絡(luò)層,數(shù)據(jù)庫赠制,服務(wù)的所有數(shù)據(jù),并以簡單的方式為它們提供服務(wù)。它不應(yīng)該具有任何其他邏輯扇苞,除了可以實(shí)際更新Model并且沒有任何副作用的邏輯。

import { observable, action } from 'mobx'
import uuid from 'uuid/v4'

class PokemonModel {
    @observable pokemons = []

    @action addPokemon(pokemon) {
        this.pokemons.push({
            id: uuid(),
            ...pokemon
        })
    }

    @action removePokemon(pokemon) {
        this.pokemons.remove(pokemon)
    }

    @action clearAll() {
        this.pokemons.clear()
    }

    getPokemons() {
        return this.pokemons
    }
}

export default PokemonModel

注意:在上面的代碼片段中寄纵,我們在View @observable將要觀察的每個(gè)屬性上使用decorator 鳖敷。Model中更新了某些可觀察值的任何代碼段都應(yīng)使用裝飾器@action 進(jìn)行裝飾。

Provider

不在MVVM中但可以將所有內(nèi)容粘合在一起的組件稱為Provider程拭。該組件將實(shí)例化ViewModel并為其提供所有必需的依賴關(guān)系定踱。此外,ViewModel的實(shí)例通過props傳遞給ViewController組件恃鞋。
Provider應(yīng)該是干凈的崖媚,沒有任何邏輯,因?yàn)槠淠康闹皇菫榱诉B接所有東西恤浪。

import React from 'react'
import { inject } from 'mobx-react'
import PokemonController from './PokemonController'
import PokemonViewModel from './PokemonViewModel'
import RootStore from '../../models/RootStore'

@inject(RootStore.type.POKEMON_MODEL)
class PokemonProvider extends React.Component {
    constructor(props) {
        super(props)
        const pokemonModel = props[RootStore.type.POKEMON_MODEL]
        this.ViewModel = new PokemonViewModel(pokemonModel)
    }

    render() {
        return (
            <PokemonController ViewModel={this.ViewModel}/>
        )
    }
}

export default PokemonProvider

注意:在上面的代碼片段中至扰,@inject decorator用于將所有需要的依賴項(xiàng)注入Provider道具。

回顧

借助MVVM资锰,您可以清晰地將關(guān)注點(diǎn)分離開來敢课,測試將變得像夏日的輕風(fēng)一樣。

參考

Level up your React architecture with MVVM

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绷杜,一起剝皮案震驚了整個(gè)濱河市直秆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鞭盟,老刑警劉巖圾结,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異齿诉,居然都是意外死亡筝野,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門粤剧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來歇竟,“玉大人,你說我怎么就攤上這事抵恋』酪椋” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵弧关,是天一觀的道長盅安。 經(jīng)常有香客問我唤锉,道長,這世上最難降的妖魔是什么别瞭? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任窿祥,我火速辦了婚禮,結(jié)果婚禮上蝙寨,老公的妹妹穿的比我還像新娘晒衩。我一直安慰自己,他們只是感情好籽慢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布浸遗。 她就那樣靜靜地躺著猫胁,像睡著了一般箱亿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弃秆,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天届惋,我揣著相機(jī)與錄音,去河邊找鬼菠赚。 笑死脑豹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的衡查。 我是一名探鬼主播瘩欺,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拌牲!你這毒婦竟也來了俱饿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤塌忽,失蹤者是張志新(化名)和其女友劉穎拍埠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體土居,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枣购,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了擦耀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棉圈。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖眷蜓,靈堂內(nèi)的尸體忽然破棺而出迄损,到底是詐尸還是另有隱情,我是刑警寧澤账磺,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布芹敌,位于F島的核電站痊远,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏氏捞。R本人自食惡果不足惜碧聪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望液茎。 院中可真熱鬧逞姿,春花似錦、人聲如沸捆等。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栋烤。三九已至谒养,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間明郭,已是汗流浹背买窟。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留薯定,地道東北人始绍。 一個(gè)月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像话侄,于是被迫代替她去往敵國和親亏推。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容