前言:在最近學習 Vue.js 的時候,看到國外一篇講述瞭如何使用 Vue.js 和 Vuex 來構建一個簡單筆記的單頁應用的文章。感受收穫挺多,本身在它的例子的基礎上進行了一些優化和自定義功能,在這裏和你們分享下學習心得。javascript
在這篇教程中咱們將經過構建一個筆記應用來學習如何在咱們的 Vue 項目中使用 Vuex。咱們將大概的過一遍什麼是 Vuex.js,在項目中何時使用它,和如何構建咱們的 Vue 應用。css
這裏放一張咱們項目的預覽圖片:html
項目源碼:vuex-notes-app;有須要的同窗能夠直接下載源碼查看。vue
Vuex 狀態管理機制的使用java
Vue.js 的基礎 apiwebpack
Vue-cli 腳手架的安裝及使用git
vur-router 的使用es6
ES6 的語法,這裏推薦看下阮一峯的入門教程github
在咱們火燒眉毛的開始項目以前,咱們最好先花幾分鐘來了解下 Vuex 的核心概念。web
Vuex 是一個專門爲 Vue.js 應用所設計的集中式狀態管理架構。它借鑑了 Flux 和 Redux 的設計思想,但簡化了概念,而且採用了一種爲能更好發揮 Vue.js 數據響應機制而專門設計的實現。
state
這樣概念初次接觸的時候可能會感受到有點模糊,簡單來講就是將 state
當作咱們項目中使用的數據的集合。而後,Vuex 使得 組件本地狀態(component local state)和 應用層級狀態(application state) 有了必定的差別。
component local state:該狀態表示僅僅在組件內部使用的狀態,有點相似經過配置選項傳入 Vue 組件內部的意思。
application level state:應用層級狀態,表示同時被多個組件共享的狀態層級。
假設有這樣一個場景:咱們有一個父組件,同時包含兩個子組件。父組件能夠很容易的經過使用 props
屬性來向子組件傳遞數據。
可是問題來了,當咱們的兩個子組件如何和對方互相通訊的? 或者子組件如何傳遞數據給他父組件的?在咱們的項目很小的時候,這個兩個問題都不會太難,由於咱們能夠經過事件派發和監聽來完成父組件和子組件的通訊。
然而,隨着咱們項目的增加:
保持對全部的事件追蹤將變得很困難。到底哪一個事件是哪一個組件派發的,哪一個組件該監聽哪一個事件?
項目邏輯分散在各個組件當中,很容易致使邏輯的混亂,不利於咱們項目的維護。
父組件將變得和子組件耦合愈來愈嚴重,由於它須要明確的派發和監聽子組件的某些事件。
這就是 Vuex 用來解決的問題。 Vuex 的四個核心概念分別是:
The state tree:Vuex 使用單一狀態樹,用一個對象就包含了所有的應用層級狀態。至此它便做爲一個『惟一數據源(SSOT)』而存在。這也意味着,每一個應用將僅僅包含一個 store 實例。單狀態樹讓咱們可以直接地定位任一特定的狀態片斷,在調試的過程當中也能輕易地取得整個當前應用狀態的快照。
Getters:用來從 store 獲取 Vue 組件數據。
Mutators:事件處理器用來驅動狀態的變化。
Actions:能夠給組件使用的函數,以此用來驅動事件處理器 mutations
如何你暫時還不太理解這個四個概念,不用着急,咱們將在後面的項目實戰中詳細的解釋。
下面這張圖詳細的解釋了 Vuex 應用中數據的流向(Vuex 官方圖)
簡單解釋下:
Vuex 規定,屬於應用層級的狀態只能經過 Mutation 中的方法來修改,而派發 Mutation 中的事件只能經過 action。
從左到又,從組件出發,組件中調用 action,在 action 這一層級咱們能夠和後臺數據交互,好比獲取初始化的數據源,或者中間數據的過濾等。而後在 action 中去派發 Mutation。Mutation 去觸發狀態的改變,狀態的改變,將觸發視圖的更新。
注意事項
數據流都是單向的
組件可以調用 action
action 用來派發 Mutation
只有 mutation 能夠改變狀態
store 是響應式的,不管 state 何時更新,組件都將同步更新
這個應用將使用 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 規範。
使用你最喜歡的編輯器打開咱們剛剛新建的項目,項目的結構大概以下圖:
components/ 文件夾用來存放咱們的 Vue 組件
vuex/ 文件夾存放的是和 Vuex store 相關的東西(state object,actions,mutators)
build/ 文件是 webpack 的打包編譯配置文件
config/ 文件夾存放的是一些配置項,好比咱們服務器訪問的端口配置等
dist/ 該文件夾一開始是不存在,在咱們的項目通過 build 以後纔會產出
App.vue 根組件,全部的子組件都將在這裏被引用
index.html 整個項目的入口文件,將會引用咱們的根組件 App.vue
main.js 入口文件的 js 邏輯,在 webpack 打包以後將被注入到 index.html 中
新增筆記,新增一篇筆記,編輯區顯示空的筆記內容
刪除筆記,刪除一篇筆記以後,編輯區域顯示當前筆記類別的第一項
筆記列表切換,分爲所有筆記和收藏筆記兩種,在切換以後,編輯區域顯示當前列表的第一條筆記
收藏筆記,給當前激活的筆記打上收藏的標籤
在這個項目中,咱們將總共使用四個組件:根組件 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>
筆記列表組件,主要有三個操做
渲染筆記
切換渲染筆記
點擊列表 title,切換 activeNote
咱們經過 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 動態的異步獲取筆記內容等等,這些有興趣的同窗能夠本身去嘗試,一點點的豐富這個例子。