原文:Learn Vuex by Building a Notes App,有刪改。css
本文假設讀者熟悉 Vuex 文檔 的內容。若是不熟悉,you definitely should!html
在這個教程裏面,咱們會經過構建一個筆記應用來學習怎麼用 Vuex。我會簡單地介紹一下 Vuex 的基礎內容, 何時該用它以及用 Vuex 的時候該怎麼組織代碼,而後我會一步一步地把這些概念應用到這個筆記應用裏面。vue
這個是咱們要構建的筆記應用的截圖:node
你能夠從 Github Repo 下載源碼,這裏是 demo 的地址。webpack
Vuex 是一個主要應用在中大型單頁應用的相似於 Flux 的數據管理架構。它主要幫咱們更好地組織代碼,以及把應用內的的狀態保持在可維護、可理解的狀態。git
若是你不太理解 Vue.js 應用裏的狀態是什麼意思的話,你能夠想象一下你此前寫的 Vue 組件裏面的 data 字段。Vuex 把狀態分紅組件內部狀態和應用級別狀態:github
組件內部狀態:僅在一個組件內使用的狀態(data 字段)web
應用級別狀態:多個組件共用的狀態vuex
舉個例子:好比說有一個父組件,它有兩個子組件。這個父組件能夠用 props 向子組件傳遞數據,這條數據通道很好理解。npm
那若是這兩個子組件相互之間須要共享數據呢?或者子組件須要向父組件傳遞數據呢?這兩個問題在應用體量較小的時候都好解決,只要用自定義事件便可。
可是隨着應用規模的擴大:
追蹤這些事件愈來愈難了。這個事件是哪一個組件觸發的?誰在監聽它?
業務邏輯遍及各個組件,致使各類意想不到的問題。
因爲要顯式地分發和監聽事件,父組件和子組件強耦合。
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.js module.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.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。
最後這個小結,咱們來實現四個組件 (App, Toolbar, NoteList 和 Editor) 並學習怎麼在這些組件裏面獲取 Vuex store 裏的數據以及調用 actions。
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 會 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" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> <link rel="stylesheet" href="styles.css"> </head> <body> <app></app> <script src="build.js"></script> </body> </html>
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 組件主要有三個功能:
把筆記列表渲染出來
容許用戶選擇"全部筆記"或者只顯示"收藏的筆記"
當用戶點擊某一條時,調用updateActiveNote
action 來更新 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 組件是最簡單的,它只作兩件事:
從 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 文檔裏面有詳細的說明。
至此,這個應用的代碼就寫完了,不明白的地方能夠看源代碼, 而後動手操練一遍。