鉴于该篇文章阅读量大,回复的同窗也挺多的,特意抽空写了一篇 vue2.0 下的 vuex 使用方法,传送门:使用 Vuex + Vue.js 构建单页应用【新篇】javascript
-------------------- 华丽的分割线 --------------------css
原文地址:https://coligo.io/learn-vuex-by-building-notes-app/html
前言:在最近学习 Vue.js 的时候,看到国外一篇讲述了如何使用 Vue.js 和 Vuex 来构建一个简单笔记的单页应用的文章。感受收获挺多,本身在它的例子的基础上进行了一些优化和自定义功能,在这里和你们分享下学习心得。vue
在这篇教程中咱们将经过构建一个笔记应用来学习如何在咱们的 Vue 项目中使用 Vuex。咱们将大概的过一遍什么是 Vuex.js,在项目中何时使用它,和如何构建咱们的 Vue 应用。java
这里放一张咱们项目的预览图片: webpack
项目源码:vuex-notes-app;有须要的同窗能够直接下载源码查看。git
在咱们火烧眉毛的开始项目以前,咱们最好先花几分钟来了解下 Vuex 的核心概念。es6
Vuex 是一个专门为 Vue.js 应用所设计的集中式状态管理架构。它借鉴了 Flux 和 Redux 的设计思想,但简化了概念,而且采用了一种为能更好发挥 Vue.js 数据响应机制而专门设计的实现。github
state
这样概念初次接触的时候可能会感受到有点模糊,简单来讲就是将 state
当作咱们项目中使用的数据的集合。而后,Vuex 使得 组件本地状态(component local state)和 应用层级状态(application state) 有了必定的差别。web
假设有这样一个场景:咱们有一个父组件,同时包含两个子组件。父组件能够很容易的经过使用 props
属性来向子组件传递数据。
可是问题来了,当咱们的两个子组件如何和对方互相通讯的? 或者子组件如何传递数据给他父组件的?在咱们的项目很小的时候,这个两个问题都不会太难,由于咱们能够经过事件派发和监听来完成父组件和子组件的通讯。
然而,随着咱们项目的增加:
这就是 Vuex 用来解决的问题。 Vuex 的四个核心概念分别是:
如何你暂时还不太理解这个四个概念,不用着急,咱们将在后面的项目实战中详细的解释。
下面这张图详细的解释了 Vuex 应用中数据的流向(Vuex 官方图)
简单解释下:
Vuex 规定,属于应用层级的状态只能经过 Mutation 中的方法来修改,而派发 Mutation 中的事件只能经过 action。
从左到又,从组件出发,组件中调用 action,在 action 这一层级咱们能够和后台数据交互,好比获取初始化的数据源,或者中间数据的过滤等。而后在 action 中去派发 Mutation。Mutation 去触发状态的改变,状态的改变,将触发视图的更新。
注意事项
这个应用将使用 webpack 来作模块打包,处理和热重启。使用 Vue 官方提供的脚手架 vue-cli。
npm install -g vue-cli
注:Node.js >= 4.x, 5.x 最好
vue init webpack vue-notes-app cd vue-notes-app npm install // 安装依赖包 npm run dev // 启动服务
初始化一个项目名为vue-notes-app
的应用,并选择使用 webpack 打包方式。在命令行中按照提示选择初始化配置项。其中在选择 JSLint 校验的时候,推荐选择 AirBNB 规范。
使用你最喜欢的编辑器打开咱们刚刚新建的项目,项目的结构大概以下图:
在这个项目中,咱们将总共使用四个组件:根组件 App.vue,操做栏组件 Toolbar.vue,别表组件 NotesList.vue,笔记编辑组件 Editor.vue。
按照上面咱们列出来的功能模块,咱们在 Vuex/ 下面创建一个 store.js 文件。
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); // 须要维护的状态 const state = { notes: [], activeNote: {}, show: '' }; const mutations = { // 初始化 state INIT_STORE(state, data) { state.notes = data.notes, state.show = data.show; state.activeNote = data.activeNote; }, // 新增笔记 NEW_NOTE(state) { var newNote = { id: +new Date(), title: '', content: '', favorite: false }; state.notes.push(newNote); state.activeNote = newNote; }, // 修改笔记 EDIT_NOTE(state, note) { state.activeNote = note; // 修改原始数据 for (var i = 0; i < state.notes.length; i++) { if(state.notes[i].id === note.id){ state.notes[i] = note; break; } }; }, // 删除笔记 DELETE_NOTE(state) { state.notes.$remove(state.activeNote); state.activeNote = state.notes[0] || {}; }, // 切换笔记的收藏与取消收藏 TOGGLE_FAVORITE(state) { state.activeNote.favorite = !state.activeNote.favorite; }, // 切换显示数据列表类型:所有 or 收藏 SET_SHOW_ALL(state, show){ state.show = show; // 切换数据展现,须要同步更新 activeNote if(show === 'favorite'){ state.activeNote = state.notes.filter(note => note.favorite)[0] || {}; }else{ state.activeNote = state.notes[0] || {}; } }, // 设置当前激活的笔记 SET_ACTIVE_NOTE(state, note) { state.activeNote = note; } }; export default new Vuex.Store({ state, mutations });
在 Vuex/ 下面创建一个 action.js,用来给组件使用的函数。
function makeAction(type) { return ({ dispatch }, ...args) => dispatch(type, ...args); }; const initNote = { id: +new Date(), title: '个人笔记', content: '第一篇笔记内容', favorite: false }; // 模拟初始化数据 const initData = { show: 'all', notes: [initNote], activeNote: initNote }; export const initStore = ({ dispatch }) => { dispatch('INIT_STORE', initData); }; // 更新当前activeNote对象 export const updateActiveNote = makeAction('SET_ACTIVE_NOTE'); // 添加一个note对象 export const newNote = makeAction('NEW_NOTE'); // 删除一个note对象 export const deleteNote = makeAction('DELETE_NOTE'); export const toggleFavorite = makeAction('TOGGLE_FAVORITE'); export const editNote = makeAction('EDIT_NOTE'); // 更新列表展现 export const updateShow = makeAction('SET_SHOW_ALL');
在 vuex/ 下面创建一个 getter.js 文件,用来从 store 获取数据。
// 获取 noteList,这里将会根据 state.show 的状态作数据过滤 export const filteredNotes = (state) => { if(state.show === 'all'){ return state.notes || {}; }else if(state.show === 'favorite'){ return state.notes.filter(note => note.favorite) || {}; } }; // 获取列表展现状态 : all or favorite export const show = (state) => { return state.show; }; // 获取当前激活 note export const activeNote = (state) => { return state.activeNote; };
以上就是咱们 Vuex 的全部逻辑了,在定下了咱们须要完成的功能以后,接下来就是只须要在组件中去调用 action 来实现对应的功能了。
在这里咱们将使用 vue-router 来作路由,引用 bootstrap 样式。
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>vuex-notes-app</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> </head> <body> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
全部的入口逻辑咱们都将在 main.js 中编写
main.js
import Vue from 'vue'; import App from './App'; import VueRouter from 'vue-router'; import VueResource from 'vue-resource'; // 路由模块和HTTP模块 Vue.use(VueResource); Vue.use(VueRouter); const router = new VueRouter(); router.map({ '/index': { component: App } }); router.redirect({ '*': '/index' }); router.start(App, '#app');
<template> <div id="app" class="app"> <toolbar></toolbar> <notes-list></notes-list> <editor></editor> </div> </template> <style> html, #app { height: 100%; } body { margin: 0; padding: 0; border: 0; height: 100%; max-height: 100%; position: relative; } </style> <script> import Toolbar from './components/Toolbar'; import NotesList from './components/NotesList'; import Editor from './components/Editor'; import store from './vuex/store'; import { initStore } from './vuex/actions'; export default { components: { Toolbar, NotesList, Editor }, store, vuex: { actions: { initStore } }, ready() { this.initStore() } } </script>
在根组件中引用了三个子组件:Toolbar.vue, NotesList.vue, Editor.vue。
注意:咱们在配置里面加入了 vuex
这么一个选项,这里用来将咱们 action 里面定义的方法给暴露出来,咱们在根组件中只作了一件事情,那就是初始化模拟数据,所以咱们在组件生命周期的 ready 阶段调用了 actions 里面的 initStore 来初始化咱们的 store 里面的 state
<template> <div id="toolbar"> <i class="glyphicon logo"><img src="../assets/logo.png" width="30" height="30"></i> <i @click="newNote" class="glyphicon glyphicon-plus"></i> <i @click="toggleFavorite" class="glyphicon glyphicon-star" :class="{starred: activeNote.favorite}"></i> <i @click="deleteNote" class="glyphicon glyphicon-remove"></i> </div> </template> <script> import { newNote, deleteNote, toggleFavorite } from '../vuex/actions'; import { activeNote } from '../vuex/getters'; export default { vuex: { getters: { activeNote }, actions: { newNote, deleteNote, toggleFavorite } } } </script> <style lang="scss" scoped> #toolbar{ float: left; width: 80px; height: 100%; background-color: #30414D; color: #767676; padding: 35px 25px 25px 25px; .starred { color: #F7AE4F; } i{ font-size: 30px; margin-bottom: 35px; cursor: pointer; opacity: 0.8; transition: opacity 0.5s ease; &:hover{ opacity: 1; } } } </style>
在这里,咱们用到了 Vuex 的一个案例就是咱们须要知道当前的激活的笔记是不是收藏类别的,若是是,咱们须要高亮收藏按钮,那么如何知道呢?那就是经过 vuex 里面的 getters 获取当前激活的笔记对象,判断它的 favorite 是否为 true。
始终牢记一个概念,vuex 中数据是单向的,只能从 store 获取,而咱们这个例子中的 activeNote 也是始终都在 store.js 中维护的,这样子就能够给其余组件公用了
// 须要维护的状态 const state = { notes: [], activeNote: {}, show: '' };
<template> <div id="notes-list"> <div id="list-header"> <h2>Notes | heavenru.com</h2> <div class="btn-group btn-group-justified" role="group"> <!-- all --> <div class="btn-group" role="group"> <button type="button" class="btn btn-default" @click="toggleShow('all')" :class="{active: show === 'all'}">All Notes</button> </div> <!-- favorites --> <div class="btn-group" role="group"> <button type="button" class="btn btn-default" @click="toggleShow('favorite')" :class="{active: show === 'favorite'}">Favorites</button> </div> </div> </div> <!-- 渲染笔记列表 --> <div class="container"> <div class="list-group"> <a v-for="note in filteredNotes" class="list-group-item" href="#" :class="{active: activeNote === note}" @click="updateActiveNote(note)"> <h4 class="list-group-item-heading"> {{note.title.trim().substring(0,30)}} </h4> </a> </div> </div> </div> </template> <script> import { updateActiveNote, updateShow } from '../vuex/actions'; import { show, filteredNotes, activeNote } from '../vuex/getters'; export default { vuex: { getters: { show, filteredNotes, activeNote }, actions: { updateActiveNote, updateShow } }, methods: { toggleShow(show) { this.updateShow(show); } } } </script>
笔记列表组件,主要有三个操做
咱们经过 getters 中的 filteredNotes 方法获取笔记列表
// 获取 noteList,这里将会根据 state.show 的状态作数据过滤 export const filteredNotes = (state) => { if(state.show === 'all'){ return state.notes || {}; }else if(state.show === 'favorite'){ return state.notes.filter(note => note.favorite) || {}; } };
能够看到,咱们获取的列表是依赖于 state.show 这个状态的。而咱们的切换列表操做刚好就是调用 actions 里面的方法来更新 state.show,这样一来,实现了数据列表的动态刷新,并且咱们对树的操做都是经过调用 actions 的方法来实现的。
咱们再看,在切换列表的时候,咱们还须要动态的更新 activeNote。看看咱们在 store.js 中是如何作的:
// 切换显示数据列表类型:所有 or 收藏 SET_SHOW_ALL(state, show){ state.show = show; // 切换数据展现,须要同步更新 activeNote if(show === 'favorite'){ state.activeNote = state.notes.filter(note => note.favorite)[0] || {}; }else{ state.activeNote = state.notes[0] || {}; } }
触发这些操做的是咱们给两个按钮分别绑定了咱们自定义的函数,经过给函数传入不一样的参数,而后调用 actions 里面的方法,来实现对数据的过滤,更新。
<template> <div id="note-editor"> <div class="form-group"> <input type="text" name="title" class="title form-control" placeholder="请输入标题" @input="updateNote" v-model="currentNote.title"> <textarea v-model="currentNote.content" name="content" class="form-control" row="3" placeholder="请输入正文" @input="updateNote"></textarea> </div> </div> </template> <script> import { editNote } from '../vuex/actions'; import { activeNote } from '../vuex/getters'; export default { vuex: { getters: { activeNote }, actions: { editNote } }, computed: { // 经过计算属性获得的一个对象,这样子咱们就能愉快的使用 v-model 了 currentNote: activeNote }, methods: { // 为何这么作? 由于在严格模式中不容许直接在模板层面去修改 state 中的值 updateNote() { this.editNote(this.currentNote); } } } </script>
在 Editor.vue 组件中,咱们须要可以实时的更新当前的 activeNote 组件和列表中对应的咱们正在修改的笔记对象的内容。
因为咱们前面提到过,在组件中是不容许直接修改 store.js在里面的状态值的,因此在这里的时候,咱们经过一个计算属性,将 store 里面的状态值赋值给一个对象,而后在自定义的 updateNotes() 方法中,去调用 action,同时传入 currentNote 对象。
在 store.js 中,咱们是这么作的,找到对应的 id 的对象,从新赋值,由于前面提到过,咱们的数据是响应式的,在这里进行了改变,对应的视图也将刷新改变,这样一来就实现了实时编辑,实时渲染的功能了。
// 修改笔记 EDIT_NOTE(state, note) { state.activeNote = note; // 修改原始数据 for (var i = 0; i < state.notes.length; i++) { if(state.notes[i].id === note.id){ state.notes[i] = note; break; } }; },
在这个项目中,咱们并无引入 vue-resource 插件,只是本身模拟了部分的数据,有兴趣的同窗能够本身去试试。
因为咱们的例子相对简单,没有涉及到很深刻的东西,更深层次的研究须要你们花更多的时间去实践了。
最后,再说一句,在 action 里面,咱们其实能够作的还有更多,好比根据 id 动态的异步获取笔记内容等等,这些有兴趣的同窗能够本身去尝试,一点点的丰富这个例子。