您是否曾經(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)知道如何使用Flux和Redux,為什么還要煩惱自己學(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
該ViewController是view的大腦-它擁有所有查看相關(guān)邏輯和擁有的一個(gè)對(duì)應(yīng)的ViewModel。該view是不知道ViewModel的,它是依靠ViewController,以通過(guò)所有必要的數(shù)據(jù)和事件。 ViewController和ViewModel之間的關(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)一樣。