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

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

JavaScript很容易拿起并從編碼開始,但是以錯(cuò)誤的方式做起來(lái)甚至更容易。對(duì)于小型項(xiàng)目,低質(zhì)量的代碼不會(huì)給公司帶來(lái)高風(fēng)險(xiǎn),但是,如果一個(gè)項(xiàng)目規(guī)模變大,您最終將承擔(dān)技術(shù)債務(wù),這些債務(wù)將在每個(gè)截止日期前消失,并最終吞噬您。沒(méi)有人會(huì)想碰這種代碼。因此,在本文中,我們將看到如何將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ù)且易于測(cè)試。

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

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

深入MVVM

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

  • 用戶與之交互的view -UI層,
  • ViewController —可以訪問(wèn)ViewModel并處理用戶輸入,
  • ViewModel -可以訪問(wèn)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)移動(dòng),按鍵等)觸發(fā)ViewController方法。該View不僅用于用戶輸入,還用于顯示輸出(某些操作的結(jié)果)。
view不能交互,是React.Component這意味著它應(yīng)該只用于顯示數(shù)據(jù)和從ViewController觸發(fā)所述傳遞事件中使用的。這樣,我們使組件可重復(fù)使用且易于測(cè)試。在MobX的幫助下, 我們將轉(zhuǎn)向 React.Component變成反應(yīng)式組件,它將觀察到任何變化并相應(yīng)地自動(dò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è)對(duì)應(yīng)的ViewModel。該view是不知道ViewModel的,它是依靠ViewController,以通過(guò)所有必要的數(shù)據(jù)和事件。 ViewControllerViewModel之間的關(guān)系是一對(duì)多的-一個(gè)ViewController可以引用不同的ViewModel。
處理用戶輸入不應(yīng)留給ViewModel,而應(yīng)在ViewController會(huì)將干凈的準(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是誰(shuí)生產(chǎn)商,并不關(guān)心誰(shuí)消耗的數(shù)據(jù); 它可以是React組件,Vue組件,飛機(jī)甚至是母牛,根本不在乎。由于ViewModel只是一個(gè)常規(guī)的JavaScript類,因此可以使用不同的UI輕松地在任何地方重用。ViewModel所需的每個(gè)依賴項(xiàng)都將通過(guò)構(gòu)造函數(shù)注入,從而使其易于測(cè)試。該ViewModel與直接交互模式,并且只要ViewModel更新它,所有的變化會(huì)自動(dòng)反映回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)用程序的全局存儲(chǔ)。它可以組合來(lái)自網(wǎng)絡(luò)層,數(shù)據(jù)庫(kù),服務(wù)的所有數(shù)據(jù),并以簡(jiǎn)單的方式為它們提供服務(wù)。它不應(yīng)該具有任何其他邏輯,除了可以實(shí)際更新Model并且沒(méi)有任何副作用的邏輯。

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

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

Provider

不在MVVM中但可以將所有內(nèi)容粘合在一起的組件稱為Provider。該組件將實(shí)例化ViewModel并為其提供所有必需的依賴關(guān)系。此外,ViewModel的實(shí)例通過(guò)props傳遞給ViewController組件。
Provider應(yīng)該是干凈的,沒(méi)有任何邏輯,因?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)分離開來(lái),測(cè)試將變得像夏日的輕風(fēng)一樣。

參考

Level up your React architecture with MVVM

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容