鑑於該篇文章閱讀量大,回覆的同窗也挺多的,特意抽空寫了一篇 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 動態的異步獲取筆記內容等等,這些有興趣的同窗能夠本身去嘗試,一點點的豐富這個例子。