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