DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 如何使用Vuex+Vue.js構建單頁應用
如何使用Vuex+Vue.js構建單頁應用
編輯:關於JavaScript     

前言:在最近學習 Vue.js 的時候,看到國外一篇講述了如何使用 Vue.js 和 Vuex 來構建一個簡單筆記的單頁應用的文章。感覺收獲挺多,自己在它的例子的基礎上進行了一些優化和自定義功能,在這裡和大家分享下學習心得。

在這篇教程中我們將通過構建一個筆記應用來學習如何在我們的 Vue 項目中使用 Vuex。我們將大概的過一遍什麼是 Vuex.js,在項目中什麼時候使用它,和如何構建我們的 Vue 應用。

這裡放一張我們項目的預覽圖片:

項目源碼:vuex-notes-app;有需要的同學可以直接下載源碼查看。

主要知識點
Vuex 狀態管理機制的使用
Vue.js 的基礎 api
Vue-cli 腳手架的安裝及使用
vur-router 的使用
ES6 的語法,這裡推薦看下阮一峰的入門教程

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 屬性來向子組件傳遞數據。

但是問題來了,當我們的兩個子組件如何和對方互相通信的? 或者子組件如何傳遞數據給他父組件的?在我們的項目很小的時候,這個兩個問題都不會太難,因為我們可以通過事件派發和監聽來完成父組件和子組件的通信。

然而,隨著我們項目的增長:

1、保持對所有的事件追蹤將變得很困難。到底哪個事件是哪個組件派發的,哪個組件該監聽哪個事件?
2、項目邏輯分散在各個組件當中,很容易導致邏輯的混亂,不利於我們項目的維護。
3、父組件將變得和子組件耦合越來越嚴重,因為它需要明確的派發和監聽子組件的某些事件。

這就是 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
 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;
 }
 };
},

Q&A

在這個項目中,我們並沒有引入 vue-resource 插件,只是自己模擬了部分的數據,有興趣的同學可以自己去試試。

由於我們的例子相對簡單,沒有涉及到很深入的東西,更深層次的研究需要大家花更多的時間去實踐了。

最後,再說一句,在 action 裡面,我們其實可以做的還有更多,比如根據 id 動態的異步獲取筆記內容等等,這些有興趣的同學可以自己去嘗試,一點點的豐富這個例子。

原文地址:https://coligo.io/learn-vuex-by-building-notes-app/

本文已被整理到了《Vue.js前端組件學習教程》,歡迎大家學習閱讀。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved