用 Vuex 構建一個筆記應用
本文假設讀者熟悉 Vuex 文檔 的內容。如果不熟悉,you definitely should!
在這個教程里面,我們會通過構建一個筆記應用來學習怎么用 Vuex。我會簡單地介紹一下 Vuex 的基礎內容, 什么時候該用它以及用 Vuex 的時候該怎么組織代碼,然后我會一步一步地把這些概念應用到這個筆記應用里面。
這個是我們要構建的筆記應用的截圖:

你可以從 Github Repo 下載源碼,這里是 demo 的地址。
Vuex 概述
Vuex 是一個主要應用在中大型單頁應用的類似于 Flux 的數據管理架構。它主要幫我們更好地組織代碼,以及把應用內的的狀態保持在可維護、可理解的狀態。
如果你不太理解 Vue.js 應用里的狀態是什么意思的話,你可以想象一下你此前寫的 Vue 組件里面的 data 字段。Vuex 把狀態分成組件內部狀態和應用級別狀態:
-
組件內部狀態:僅在一個組件內使用的狀態(data 字段)
-
應用級別狀態:多個組件共用的狀態
舉個例子:比如說有一個父組件,它有兩個子組件。這個父組件可以用 props 向子組件傳遞數據,這條數據通道很好理解。
那如果這兩個子組件相互之間需要共享數據呢?或者子組件需要向父組件傳遞數據呢?這兩個問題在應用體量較小的時候都好解決,只要用自定義事件即可。
但是隨著應用規模的擴大:
-
追蹤這些事件越來越難了。這個事件是哪個組件觸發的?誰在監聽它?
-
業務邏輯遍布各個組件,導致各種意想不到的問題。
-
由于要顯式地分發和監聽事件,父組件和子組件強耦合。
Vuex 要解決的就是這些問題,Vuex 背后有四個核心的概念:
-
狀態樹: 包含所有應用級別狀態的對象
-
Getters: 在組件內部獲取 store 中狀態的函數
-
Mutations: 修改狀態的事件回調函數
-
Actions: 組件內部用來分發 mutations 事件的函數
下面這張圖完美地解釋了一個 Vuex 應用內部的數據流動:

這張圖的重點:
-
數據流動是單向的
-
組件可以調用 actions
-
Actions 是用來分發 mutations 的
-
只有 mutations 可以修改狀態
-
store 是反應式的,即,狀態的變化會在組件內部得到反映
搭建項目
項目結構是這樣的:

-
components/包含所有的組件
-
vuex/包含 Vuex 相關的文件 (store, actions)
-
build.js是 webpack 將要輸出的文件
-
index.html是要渲染的頁面
-
main.js是應用的入口點,包含了根實例
-
style.css
-
webpack.config.js
新建項目:
mkdir vuex-notes-app && cd vuex-note-app
npm init -y
安裝依賴:
npm install\
webpack webpack-dev-server\
vue-loader vue-html-loader css-loader vue-style-loader vue-hot-reload-api\
babel-loader babel-core babel-plugin-transform-runtime babel-preset-es2015\
babel-runtime@5\
--save-dev
npm install vue vuex --save
然后配置 Webpack:
// webpack.config.jsmodule.exports = {
entry: './main.js',
output: {
path: __dirname,
filename: 'build.js'
},
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
}
]
},
babel: {
presets: ['es2015'],
plugins: ['transform-runtime']
}
}
然后在 package.json 里面配置一下 npm script:
"scripts": {
"dev": "webpack-dev-server --inline --hot",
"build": "webpack -p"
}
后面測試和生產的時候直接運行npm run dev和npm run build就行了。
創建 Vuex Store
在 vuex/文件夾下創建一個 store.js:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
notes: [],
activeNote: {}
}
const mutations = { ... }
export default new Vuex.Store({
state,
mutations
})
現在我用下面這張圖把應用分解成多個組件,并把組件內部需要的數據對應到 store.js 里的 state。

-
App, 根組件,就是最外面那個紅色的盒子
-
Toolbar 是左邊的綠色豎條,包括三個按鈕
-
NotesList 是包含了筆記標題列表的紫色框。用戶可以點擊所有筆記(All Notes)或者收藏筆記(Favorites)
-
Editor 是右邊這個可以編輯筆記內容的黃色框
store.js 里面的狀態對象會包含所有應用級別的狀態,也就是各個組件需要共享的狀態。
筆記列表(notes: [])包含了 NodesList 組件要渲染的 notes 對象。當前筆記(activeNote: {})則包含當前選中的筆記對象,多個組件都需要這個對象:
-
Toolbar 組件的收藏和刪除按鈕都對應這個對象
-
NotesList 組件通過 CSS 高亮顯示這個對象
-
Editor 組件展示及編輯這個筆記對象的內容。
聊完了狀態(state),我們來看看 mutations, 我們要實現的 mutation 方法包括:
-
添加筆記到數組里 (state.notes)
-
把選中的筆記設置為「當前筆記」(state.activeNote)
-
刪掉當前筆記
-
編輯當前筆記
-
收藏/取消收藏當前筆記
首先,要添加一條新筆記,我們需要做的是:
-
新建一個對象
-
初始化屬性
-
push 到
state.notes里去 -
把新建的這條筆記設為當前筆記(activeNote)
ADD_NOTE (state) {
const new Note = {
text: 'New note',
favorite: fals
}
state.notes.push(newNote)
state.activeNote= newNote
}
然后,編輯筆記需要用筆記內容 text 作參數:
EDIT_NOTE (state, text) {
state.activeNote.text = text
}
剩下的這些 mutations 很簡單就不一一贅述了。整個 vuex/store.js 是這個樣子的:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
note: [],
activeNote: {}
}
const mutations = {
ADD_NOTE (state) {
const newNote = {
text: 'New Note',
favorite: false
}
state.notes.push(newNote)
state.activeNote = newNote
},
EDIT_NOTE (state, text) {
state.activeNote.text = text
},
DELETE_NOTE (state) {
state.notes.$remove(state.activeNote)
state.activeNote = state.notes[0]
},
TOGGLE_FAVORITE (state) {
state.activeNote.favorite = !state.activeNote.favorite
},
SET_ACTIVE_NOTE (state, note) {
state.activeNote = note
}
}
export default new Vuex.Store({
state,
mutations
})
接下來聊 actions, actions 是組件內用來分發 mutations 的函數。它們接收 store 作為第一個參數。比方說,當用戶點擊 Toolbar 組件的添加按鈕時,我們想要調用一個能分發ADD_NOTE mutation 的 action。現在我們在 vuex/文件夾下創建一個 actions.js 并在里面寫上 addNote函數:
// actions.js
export const addNote = ({ dispatch }) => {
dispatch('ADD_NOTE')
}
剩下的這些 actions 都跟這個差不多:
export const addNote = ({ dispatch }) => {
dispatch('ADD_NOTE')
}
export const editNote = ({ dispatch }, e) => {
dispatch('EDIT_NOTE', e.target.value)
}
export const deleteNote = ({ dispatch }) => {
dispatch('DELETE_NOTE')
}
export const updateActiveNote = ({ dispatch }, note) => {
dispatch('SET_ACTIVE_NOTE', note)
}
export const toggleFavorite = ({ dispatch }) => {
dispatch('TOGGLE_FAVORITE')
}
這樣,在 vuex 文件夾里面要寫的代碼就都寫完了。這里面包括了 store.js 里的 state 和 mutations,以及 actions.js 里面用來分發 mutations 的 actions。
構建 Vue 組件
最后這個小結,我們來實現四個組件 (App, Toolbar, NoteList 和 Editor) 并學習怎么在這些組件里面獲取 Vuex store 里的數據以及調用 actions。
創建根實例 - main.js
main.js是應用的入口文件,里面有根實例,我們要把 Vuex store 加到到這個根實例里面,進而注入到它所有的子組件里面:
import Vue from 'vue'
import store from './vuex/store'
import App from './components/App.vue'
new Vue({
store, // 注入到所有子組件
el: 'body',
components: { App }
})
App - 根組件
根組件 App 會 import 其余三個組件:Toolbar, NotesList 和 Editor:
<template>
<div id="app">
<toolbar></toolbar>
<notes-list></notes-list>
<editor></editor>
</div></template>
<script>
import Toolbar from './Toolbar.vue'
import NotesList from './NotesList.vue'
import Editor from './Editor.vue'
export default {
components: {
Toolbar,
NotesList,
Editor
}
}
</script>
把 App 組件放到 index.html 里面,用 BootStrap 提供基本樣式,在 style.css 里寫組件相關的樣式:
<!-- index.html -->
<!DOCTYPE html><html lang="en">
<head>
<meta charset="utf-8">
<title>Notes | coligo.io</title>
<link rel="stylesheet" >
<link rel="stylesheet" href="styles.css">
</head>
<body>
<app></app>
<script src="build.js"></script>
</body></html>
Toolbar
Toolbar 組件提供給用戶三個按鈕:創建新筆記,收藏當前選中的筆記和刪除當前選中的筆記。

這對于 Vuex 來說是個絕佳的用例,因為 Toolbar 組件需要知道「當前選中的筆記」是哪一條,這樣我們才能刪除、收藏/取消收藏它。前面說了「當前選中的筆記」是各個組件都需要的,不應該單獨存在于任何一個組件里面,這時候我們就能發現共享數據的必要性了。
每當用戶點擊筆記列表中的某一條時,NodeList 組件會調用updateActiveNote() action 來分發 SET_ACTIVE_NOTE mutation, 這個 mutation 會把當前選中的筆記設為 activeNote。
也就是說,Toolbar 組件需要從 state 獲取 activeNote 屬性:
vuex: {
getters: {
activeNote: state => state.activeNote
}
}
我們也需要把這三個按鈕所對應的 actions 引進來,因此 Toolbar.vue 就是這樣的:
<template>
<div id="toolbar">
<i @click="addNote" 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 { addNote, deleteNote, toggleFavorite } from '../vuex/actions'
export default {
vuex: {
getters: {
activeNote: state => state.activeNote
},
actions: {
addNote,
deleteNote,
toggleFavorite
}
}
}
</script>
注意到當 activeNote.favorite === true的時候,收藏按鈕還有一個 starred 的類名,這個類的作用是對收藏按鈕提供高亮顯示。

NotesList
NotesList 組件主要有三個功能:
-
把筆記列表渲染出來
-
允許用戶選擇"所有筆記"或者只顯示"收藏的筆記"
-
當用戶點擊某一條時,調用
updateActiveNoteaction 來更新 store 里的activeNote
顯然,在 NoteLists 里需要 store 里的notes array和activeNote:
vuex: {
getters: {
notes: state => state.notes,
activeNote: state => state.activeNote
}
}
當用戶點擊某一條筆記時,把它設為當前筆記:
import { updateActiveNote } from '../vuex/actions'
export default {
vuex: {
getters: {
// as shown above
},
actions: {
updateActiveNote
}
}
}
接下來,根據用戶點擊的是"所有筆記"還是"收藏筆記"來展示過濾后的列表:
import { updateActiveNote } from '../vuex/actions'
export default {
data () {
return {
show: 'all'
}
},
vuex: {
// as shown above
},
computed: {
filteredNotes () {
if (this.show === 'all'){
return this.notes
} else if (this.show === 'favorites') {
return this.notes.filter(note => note.favorite)
}
}
}
}
在這里組件內的 show 屬性是作為組件內部狀態出現的,很明顯,它只在 NoteList 組件內出現。
以下是完整的 NotesList.vue:
<template>
<div id="notes-list">
<div id="list-header">
<h2>Notes | coligo</h2>
<div class="btn-group btn-group-justified" role="group">
<!-- All Notes button -->
<div class="btn-group" role="group">
<button type="button" class="btn btn-default"
@click="show = 'all'"
:class="{active: show === 'all'}">
All Notes
</button>
</div>
<!-- Favorites Button -->
<div class="btn-group" role="group">
<button type="button" class="btn btn-default"
@click="show = 'favorites'"
:class="{active: show === 'favorites'}">
Favorites
</button>
</div>
</div>
</div>
<!-- render notes in a list -->
<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.text.trim().substring(0, 30)}}
</h4>
</a>
</div>
</div>
</div></template>
<script>
import { updateActiveNote } from '../vuex/actions'
export default {
data () {
return {
show: 'all'
}
},
vuex: {
getters: {
notes: state => state.notes,
activeNote: state => state.activeNote
},
actions: {
updateActiveNote
}
},
computed: {
filteredNotes () {
if (this.show === 'all'){
return this.notes
} else if (this.show === 'favorites') {
return this.notes.filter(note => note.favorite)
}
}
}
}
</script>
這個組件的幾個要點:
-
用前30個字符當作該筆記的標題
-
當用戶點擊一條筆記,該筆記變成當前選中筆記
-
在"all"和"favorite"之間選擇實際上就是設置 show 屬性
-
通過
:class=""設置樣式
Editor
Editor 組件是最簡單的,它只做兩件事:
-
從 store 獲取當前筆記
activeNote,把它的內容展示在 textarea -
在用戶更新筆記的時候,調用
editNote()action
以下是完整的 Editor.vue:
<template>
<div id="note-editor">
<textarea
:value="activeNoteText"
@input="editNote"
class="form-control">
</textarea>
</div></template>
<script>
import { editNote } from '../vuex/actions'
export default {
vuex: {
getters: {
activeNoteText: state => state.activeNote.text
},
actions: {
editNote
}
}
}
</script>
這里的 textarea 不用 v-model 的原因在 vuex 文檔里面有詳細的說明。
至此,這個應用的代碼就寫完了,不明白的地方可以看源代碼, 然后動手操練一遍。
來源:https://segmentfault.com/a/1190000005015164