使用MVVM升级您的React架构

您是否曾经打开过一个项目并遭受了痛苦折磨,因为您看到了即使是孤立的杆也不想触及的,难以理解且难以维护的JavaScript代码?因为如果您触摸它,一切都会崩溃,就像一个大的积木块一样。

JavaScript很容易拿起并从编码开始,但是以错误的方式做起来甚至更容易。对于小型项目,低质量的代码不会给公司带来高风险,但是,如果一个项目规模变大,您最终将承担技术债务,这些债务将在每个截止日期前消失,并最终吞噬您。没有人会想碰这种代码。因此,在本文中,我们将看到如何将Model-View-ViewModel(MVVM)架构模式应用到React项目中,并显着提高代码质量。

根据定义,架构模式提供了一组预定义的子系统,指定了它们的职责,并包括用于组织它们之间的关系的规则和准则。

许多架构模式都在尝试解决与MVVM相同的挑战-使您的代码松散耦合,可维护且易于测试。

有人可能会问:“如果我已经知道如何使用FluxRedux,为什么还要烦恼自己学习MVVM或任何其他架构模式?”
-答案是:你不就得了!例如,如果Redux非常适合您的项目和团队,请坚持使用。另一方面,如果您不知道其他任何模式,您怎么能百分百确定Redux是您项目的理想选择呢?即使可能有更好的选择,您也将迫使Redux进入每个项目。这里唯一明智的决定是学习新的建筑模式。让我们从MVVM开始。

了解模式的最佳方法是弄脏双手,然后尝试一下。我们将创建pokemon go演示应用阵营和MobX(在这个完整的代码)。MobX是用于简单和可扩展状态管理的库。它的作用与Redux相同,但与Redux不同,它没有为我们提供有关如何构建应用程序的准则。MobX 为我们提供了可观察的功能(观察者模式)以及一种将依赖项注入到我们的组件中的方法。它跟MVVM就像面包去与黄油。

深入MVVM

MVVM有四个主要模块:

  • 用户与之交互的view -UI层,
  • ViewController —可以访问ViewModel并处理用户输入,
  • ViewModel -可以访问Model 并处理业务逻辑,
  • Model -应用程序数据源

继续阅读以了解MVVM中的这些组件如何相互关联以及它们的职责是什么。

View

借助React,我们正在构建用户界面,而这正是我们大多数人已经熟悉的。该view是与您的应用程序的用户的唯一接触点。用户将与您的View交互,这将根据事件(例如鼠标移动,按键等)触发ViewController方法。该View不仅用于用户输入,还用于显示输出(某些操作的结果)。
view不能交互,是React.Component这意味着它应该只用于显示数据和从ViewController触发所述传递事件中使用的。这样,我们使组件可重复使用且易于测试。在MobX的帮助下, 我们将转向 React.Component变成反应式组件,它将观察到任何变化并相应地自动更新。

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装饰器装饰的,而不是使用常规函数的observer(class PokemonList {...})
装饰器默认情况下不支持装饰器,因此,如果要使用它们,则需要babel插件

ViewController

ViewControllerview的大脑-它拥有所有查看相关逻辑和拥有的一个对应的ViewModel。该view是不知道ViewModel的,它是依靠ViewController,以通过所有必要的数据和事件。 ViewControllerViewModel之间的关系是一对多的-一个ViewController可以引用不同的ViewModel
处理用户输入不应留给ViewModel,而应在ViewController会将干净的准备好的数据传递给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是谁生产商,并不关心谁消耗的数据; 它可以是React组件,Vue组件,飞机甚至是母牛,根本不在乎。由于ViewModel只是一个常规的JavaScript类,因此可以使用不同的UI轻松地在任何地方重用。ViewModel所需的每个依赖项都将通过构造函数注入,从而使其易于测试。该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充当数据源,即。应用程序的全局存储。它可以组合来自网络层,数据库,服务的所有数据,并以简单的方式为它们提供服务。它不应该具有任何其他逻辑,除了可以实际更新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将要观察的每个属性上使用decorator 。Model中更新了某些可观察值的任何代码段都应使用装饰器@action 进行装饰。

Provider

不在MVVM中但可以将所有内容粘合在一起的组件称为Provider。该组件将实例化ViewModel并为其提供所有必需的依赖关系。此外,ViewModel的实例通过props传递给ViewController组件。
Provider应该是干净的,没有任何逻辑,因为其目的只是为了连接所有东西。

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用于将所有需要的依赖项注入Provider道具。

回顾

借助MVVM,您可以清晰地将关注点分离开来,测试将变得像夏日的轻风一样。

参考

Level up your React architecture with MVVM

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容