Vuex基础:教程与释义

译文地址

Vuex basics: Tutorial and explanation

更新于2016年11月:这篇文章的代码写于2015年12月,并且使用的是非常老的vuex api版本。
但是,这篇文章仍然很有价值,它深入分析了为什么vuex是如此的重要,vuex的工作原理以及vuex是如何让你的应用更加出色并易于保存。

Vuex是由Vue.js作者创作的一种开发中的原型库,它可以帮助你以一种更加可持续的方式构建大型应用,它的规则类似于Facebook的Flux库(随后被redux等社区迭代)。
相对于直接跳入vuex并开始使用它,在本文中,我会解释为什么vuex在可替代方案中是如此的受欢迎和vuex对你而言有什么价值。

我们要构建的是什么?


我们要构建的简单应用包含了一个按钮和一个计数器。按下按钮会增加计数器。这个任务可以很容易的帮助我们理解以下概念。

在这个应用中有两个组件:
1. 一个按钮(这是事件源)
2. 一个计数器(这里会根据原始事件反映更新)

这两个组件互不相识并且无法通信,哪怕在一些非常小的web应用中,这都是一个非常常见的模型。在大型应用中有大量的组件需要互相通信并且保持彼此连接,以下是一个基本的todo list的流程互动:

这篇文章的目标

我们将使用三种方式来探索解决相同的问题:
1. 使用事件广播以使得组件间通信
2. 使用共享的state对象
3. 使用vuex

读完本文后,希望你能够理解:
1. 如何使用vuex的一个基本的工作流程
2. vuex解决的是什么类型的问题
3. 为什么vuex会优于其他的策略,尽管它有点复杂和严格

建立一个开始点

我们将用3种不同的解决同一个问题,在开始之前,我们需要我有一个相同的开始点,如果你想一道开始,我建议你为本教程建立一个git repo,在开始之后创建一个commit并且为不同方法创建分支。

$ npm install -g vue-cli
$ vue init webpack vuex-tutorial
$ cd vuex-tutorial
$ npm install
$ npm install --save vuex
$ npm run dev

现在你可以看到基础的vue脚手架页面,让我们创建并更新一些文件以使其达到我们想要的效果。
首先,我们在src/components/IncrementButton.vue中建立一个IncrementButton组件:

<template>
  <button @click="activate">+1</button>
</template>

<script>
export default {
  methods: {
    activate () {
      console.log('+1 Pressed')
    }
  }
}
</script>

<style>
</style>

接下来我们创建一个CounterDisplay组件来展示计数器,让我们在src/components/CounterDisplay.vue中创建一个新的基础vue组件。

<template>
  Count is {{ count }}
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  }
}
</script>
<style>
</style>

替换App.vue文件:

<template>
  <div id="app">
    <h3>Increment:</h3>
    <increment></increment>
    <h3>Counter:</h3>
    <counter></counter>
  </div>
</template>

<script>
import Counter from './components/CounterDisplay.vue'
import Increment from './components/IncrementButton.vue'
export default {
  components: {
    Counter,
    Increment
  }
}
</script>

<style>
</style>

现在你可以使用npm run dev命令并且在浏览器中打开页面,你会看到一个按钮和一个计数器,点击按钮会在console中输出一个信息,现在我们已经抵达开始点,接下来就是处理过程了。

方法一:事件广播


让我们在组件中进行修改吧。首先,在IncrementButton.vue中我们使用$dispatch来发送一个信息给父节点以表明按钮被触发了。

export default {
  methods: {
    activate () {
      // 发送一个事件给App父节点
      this.$dispatch('button-pressed')
    }
  }
}

App.vue中我们由子节点监听事件并重新广播一个新的增量事件给所有节点。

export default {
  components: {
    Counter,
    Increment
  },
  events: {
    'button-pressed': function () {
      // 发送一个信息给所有子节点
      this.$broadcast('increment')
    }
  }
}

CounterDisplay.vue中我们收听increemnt事件并增加state的值。

export default {
  data () {
    return {
      count: 0
    }
  },
  events: {
    increment () {
      this.count ++
    }
  }
}
该方法的一些缺点

在本方法中并没有什么技术错误,对于在一个文件中书写你的所有应用逻辑,该方法也是没有技术问题的,但问题是可维护性,以下说明了这种方法是多么的难以维护。
1. 对于每次行为,父组件必须与正确的组件连接并调度事件。
2. 对于一个大型应用很难分析事件是从何发出的。
3. 没有明确的空间给”业务逻辑”,this.count++CounterDisplay中,但业务逻辑可能在任何地方,这就造成了难以维护性。

我来给出这个应用可能引起bug的一个例子:
1. 你聘请了两个实习生:Alice和Bob,你告诉Alice你需要一个新的组件中的计数器,你告诉Bob区写一个Reset按钮。
2. Alice写下一个新的组件FormattedCounterDisplay并订阅了increment事件来增加他自己的state,Alice很高兴并提交了代码。
3. Bob写了一个新的”Reset”组件并添加一个reset事件到App来重新调度,他在CounterDisplay实现了reset以使得计数重置为0,但他不知道Alice的组件也订阅了该计数。
4. 你的用户按下”+1”,发现应用可以正常工作,但当他们按下”reset”,只有一个计数重置了。

这是一个很简单的例子,但它说明了使用事件将分散state和业务逻辑整合在一起时会发生错误。

方法二:共享state

让我们还原到方法一开始前,我们新建一个新的文件src/store.js

export default {
  state: {
    counter: 0
  }
}

我们第一次修改CounterDisplay.vue

<template>
  Count is {{ sharedState.counter }}
</template>

<script>
import store from '../store'

export default {
  data () {
    return {
      sharedState: store.state
    }
  }
}
</script>

我在在这个做了一些有趣的事情:
1. 我们取得store对象,这是一个不变的对象,但它是其他文件定义的。
2. 在我们的本地data中我们创建了一个名为sharedState的新变量,映射到store.state
3. vue使得data反映了store.state,意味着vue会在store.state改变时自动更新sharedState

至此该工程还没法工作,但现在,我们可以修改IncrementButton.vue

import store from '../store'

export default {
  data () {
    return {
      sharedState: store.state
    }
  },
  methods: {
    activate () {
      this.sharedState.counter += 1
    }
  }
}
  1. 在这里,我们输入store并添加了它的映射就像之前的例子。
  2. activate函数被调用时,经过sharedState反映到store.state并增加计数器。
  3. 所有订阅counter的组件和计算属性现在会更新。
这种方法比第一种好在哪

让我们重新回到两位实习生的问题-Alice和Bob。
1. Alice写下FormattedComponentDisplay并订阅Shared State Counter,这个计数器会始终展示最新的计数结果。
2. Bob的ResetButton组件设置Shared State Counter为0,这会影响CounterDisplay和Alice写的FormattedCounterDisplay
3. 用户发现reset按钮能够正常工作了。

这种方法还有哪些不足
  1. 在实习的过程中,Alice和Bob写下更多的计数器展示、reset按钮、以及不同类型的增加按钮,所有这些都会更新同一个共享计数器。Life is good.
  2. 一旦他们回到学校,你需要维护他们的代码。
  3. Carol-新来的领导,他要求”我不希望计数器超过100”。

现在你怎么办?
1. 你会找出所有更新计数器的组件?那真令人沮丧。
2. 你想增加一个filter或formatter?那也很麻烦。

现在就出现了问题,业务逻辑分散在应用的各个部分,这个问题在原理上很简单,但对于维护来说就是一个大问题。

一个稍微好一点的方法

现在你重构了所有格原始代码并重写了store.js

var store = {
  state: {
    counter: 0
  },
  increment: function () {
    if (store.state.counter < 100) {
      store.state.counter += 1;
    }
  },
  reset: function () {
    store.state.counter = 0;
  }
}

export default store

这样做可以使得代码更清晰,你可以明确的调用increment,并且所有的业务逻辑都存储在其中,但是,一个新的实习生并不知道这些,他发现从该应用的其他部分可以很容易的修改store.state.counter,这时就很难定位错误了。
之后你又加了很多严格的规则、指南、代码review来确保所有人都使用store.js中的函数来修改state,当这些都不管用后,你不得不让人力砍掉这个实习生计划。

方法三:Vuex

让我们还原到方法二开始之前的状态,从原理上vuex有点类似方法二,以下是一个有点复杂的图:

我们首先重新新建一个src/store.js,这次我们的代码改为:

import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)

var store = new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    INCREMENT (state) {
      state.counter ++
    }
  }
})

export default store

我看看代码中发生了什么:
1. 我们获得Vuex模块并指导Vue来使用它启动插件。
2. 我们的存储不再是一个问题json对象,而是一个Vuex.Store的实例。
3. 我们在state中创建一个counter
并设置为0。
4. 我们有一个新的mutations对象,它有一个INCREMENT方法,输入一个state并修改这个值。

这段代码中有一些有趣的东西:
1. 所有使用require('../store.js')import store from '../store.js'的代码将由相同的store实例。
2. 我们从不编辑store.state.counter,但我们得到了这个state的一个可更新和修改的拷贝,这将会非常重要。

现在我们已经解决了store问题,让我们修改IncrementButton.vue

import store from '../store'

export default {
  methods: {
    activate () {
      store.commit('INCREMENT')
    }
  }
}

这个组件还没有任何数据,但在点击后会执行store.commit('INCREMENT')。我们会很快回到这里。
现在,更新CounterDisplay.vue

<template>
  Count is {{ counter }}
</template>

<script>
import store from '../store'

export default {
  computed: {
    counter () {
      return store.state.counter
    }
  }
}
</script>

这时事情变得有趣了,我们不再订阅共享state,我们使用vue的computed属性从store中获得计数。
Vue清楚counter的computed属性依赖于store.state.counter,因此当store更新时,它会更新所有的相关项,这就是我们要的!
如果你要重载页面,你会发现计数器正常工作,这里是一步步发生的事情:
1. Vue的事件处理器成为activate,我们写的这个函数是store.dispatch('INCREMENT')
2. 在这里,INCREMENT是一个行为的名称,它代表“这是一种可改变state的类型”的标示符,同时,我们可以将额外的参数传递给调度函数。
3. Vue能够清楚计数器的调度,同样的,我们也能够让它更加复杂从而服务于更大的应用。
4. 调度器接收state的拷贝并更新它,Vue保存了一份state的旧的拷贝,而这会被用于之后更高级的特征。
5. state更新时,vue自动更新所有依赖于该state的组件。
6. 如果你这样做了,这会让你的代码更具可测试性。

为什么这种方法比方法二更好:

  1. 如果所有states的一个拷贝存在于开发的整个流程中,vue开发者就可以潜在的构建一个”Time Travelling Debugger”,除了有一个很cool的名字,它还允许你在应用中撤销行为并修改逻辑,提高开发效率。
  2. 你可以构建一个中间件并使其工作在state改变时,例如,你可以构建一个logger,它可以留存用户的所有行为日志。如果发现了bug,你可以调出日志来查看到底是哪一步出了问题。
  3. 对比让你自己去监视所有的行为,让你的团队成员都能改变应用的state会变得更加高效。

仍有很多工作要做

这里只展示了vuex的皮毛,现在仅仅只是构建的开始,我相信这种开发模式要发展成熟仍然需要很多年。
你可以发现更多资料去组织你的store,在vuex的文档中学习更多的信息,消化所有的概念可能需要一些时间,我们可能也要做出一些错误的尝试才能找出正确的方法。

结语:处理实习生的代码

你可以移植你的应用到vue.js,你的实习生发现可以在他的组件中很便捷的写入store.state.counter,你现在已经拿到了最后一根稻草,现在在你的store.js中加一行。

var store = new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    INCREMENT (state) {
      state.counter ++
    }
  },
  strict: true // Vuex申请专利的反实习装置
})

现在所有人可以直接写入store,这也会引起一个问题,注意这会降低你的应用的速率,你可以在产品发布时安全的去掉它,去看看文档中例子是怎么做的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值