如何使用Vuex+Vue.js構建單頁應用
前言:在最近學習 Vue.js 的時候,看到國外一篇講述瞭如何使用 Vue.js 和 Vuex 來構建一個簡單筆記的單頁應用的文章。感覺收穫挺多,自己在它的例子的基礎上進行了一些優化和自定義功能,在這裡和大家分享下學習心得。
在這篇教程中我們將通過構建一個筆記應用來學習如何在我們的 Vue 專案中使用 Vuex。我們將大概的過一遍什麼是 Vuex.js,在專案中什麼時候使用它,和如何構建我們的 Vue 應用。
這裡放一張我們專案的預覽圖片:
專案原始碼:vuex-notes-app;有需要的同學可以直接下載原始碼檢視。
主要知識點
Vuex 概述
在我們迫不及待的開始專案之前,我們最好先花幾分鐘來了解下 Vuex 的核心概念。
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。
安裝 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
按照上面我們列出來的功能模組,我們在 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 Actions
在 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 Getters
在 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');
根元件 App.vue
<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
Toolbar.vue
<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: ''
};
NotesList.vue
<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 裡面的方法,來實現對資料的過濾,更新。
Editor.vue
<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