使用 Vuex + Vue.js 構建單頁應用

鑑於該篇文章閱讀量大,回覆的同窗也挺多的,特意抽空寫了一篇 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 狀態管理機制的使用
  • Vue.js 的基礎 api
  • Vue-cli 腳手架的安裝及使用
  • vur-router 的使用
  • ES6 的語法,這裏推薦看下阮一峯的入門教程

Vuex 概述

在咱們火燒眉毛的開始項目以前,咱們最好先花幾分鐘來了解下 Vuex 的核心概念。es6

Vuex 是一個專門爲 Vue.js 應用所設計的集中式狀態管理架構。它借鑑了 Flux 和 Redux 的設計思想,但簡化了概念,而且採用了一種爲能更好發揮 Vue.js 數據響應機制而專門設計的實現。github

state 這樣概念初次接觸的時候可能會感受到有點模糊,簡單來講就是將 state 當作咱們項目中使用的數據的集合。而後,Vuex 使得 組件本地狀態(component local state)和 應用層級狀態(application state) 有了必定的差別web

  • 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 數據流圖

簡單解釋下:

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 動態的異步獲取筆記內容等等,這些有興趣的同窗能夠本身去嘗試,一點點的豐富這個例子。

相關文章
相關標籤/搜索