用vuex構建單頁

原文地址:點我css

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

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

項目源碼:vuex-notes-app;有須要的同窗能夠直接下載源碼查看。webpack

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

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:應用層級狀態,表示同時被多個組件共享的狀態層級。vue-router

假設有這樣一個場景:咱們有一個父組件,同時包含兩個子組件。父組件能夠很容易的經過使用 props 屬性來向子組件傳遞數據。vuex

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

然而,隨着咱們項目的增加:

一、保持對全部的事件追蹤將變得很困難。到底哪一個事件是哪一個組件派發的,哪一個組件該監聽哪一個事件?
二、項目邏輯分散在各個組件當中,很容易致使邏輯的混亂,不利於咱們項目的維護。
三、父組件將變得和子組件耦合愈來愈嚴重,由於它須要明確的派發和監聽子組件的某些事件。

這就是 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 最好

初始化應用

1 vue init webpack vue-notes-app
2 
3 cd vue-notes-app
4 
5 npm install // 安裝依賴包
6 
7 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 文件。

 

  1 import Vue from 'vue';
  2 
  3 import Vuex from 'vuex';
  4 
  5  
  6 
  7 Vue.use(Vuex);
  8 
  9  
 10 
 11 // 須要維護的狀態
 12 
 13 const state = {
 14 
 15  notes: [],
 16 
 17  activeNote: {},
 18 
 19  show: ''
 20 
 21 };
 22 
 23  
 24 
 25 const mutations = {
 26 
 27  // 初始化 state
 28 
 29  INIT_STORE(state, data) {
 30 
 31  state.notes = data.notes,
 32 
 33  state.show = data.show;
 34 
 35  state.activeNote = data.activeNote;
 36 
 37  },
 38 
 39  // 新增筆記
 40 
 41  NEW_NOTE(state) {
 42 
 43  var
 44 newNote = {
 45 
 46  id: +new
 47 Date(),
 48 
 49  title: '',
 50 
 51  content: '',
 52 
 53  favorite: false
 54 
 55  };
 56 
 57  state.notes.push(newNote);
 58 
 59  state.activeNote = newNote;
 60 
 61  },
 62 
 63  // 修改筆記
 64 
 65  EDIT_NOTE(state, note) {
 66 
 67  state.activeNote = note;
 68 
 69  // 修改原始數據
 70 
 71  for
 72 (var
 73 i = 0; i < state.notes.length; i++) {
 74 
 75  if(state.notes[i].id === note.id){
 76 
 77  state.notes[i] = note;
 78 
 79  break;
 80 
 81  }
 82 
 83  };
 84 
 85  },
 86 
 87  // 刪除筆記
 88 
 89  DELETE_NOTE(state) {
 90 
 91  state.notes.$remove(state.activeNote);
 92 
 93  state.activeNote = state.notes[0] || {};
 94 
 95  },
 96 
 97  // 切換筆記的收藏與取消收藏
 98 
 99  TOGGLE_FAVORITE(state) {
100 
101  state.activeNote.favorite = !state.activeNote.favorite;
102 
103  },
104 
105  // 切換顯示數據列表類型:所有 or 收藏
106 
107  SET_SHOW_ALL(state, show){
108 
109  state.show = show;
110 
111  // 切換數據展現,須要同步更新 activeNote
112 
113  if(show === 'favorite'){
114 
115  state.activeNote = state.notes.filter(note => note.favorite)[0] || {};
116 
117  }else{
118 
119  state.activeNote = state.notes[0] || {};
120 
121  }
122 
123  },
124 
125  // 設置當前激活的筆記
126 
127  SET_ACTIVE_NOTE(state, note) {
128 
129  state.activeNote = note;
130 
131  }
132 
133 };
134 
135  
136 
137 export default
138 new  Vuex.Store({
139 
140  state,
141 
142  mutations
143 
144 });

 

建立 Vuex Actions
在 Vuex/ 下面創建一個 action.js,用來給組件使用的函數。


  1 function
  2 makeAction(type) {
  3 
  4  return
  5 ({ dispatch }, ...args) => dispatch(type, ...args);
  6 
  7 };
  8 
  9  
 10 
 11 const initNote = {
 12 
 13  id: +new
 14 Date(),
 15 
 16  title: '個人筆記',
 17 
 18  content: '第一篇筆記內容',
 19 
 20  favorite: false
 21 
 22 };
 23 
 24  
 25 
 26 // 模擬初始化數據
 27 
 28 const initData = {
 29 
 30  show: 'all',
 31 
 32  notes: [initNote],
 33 
 34  activeNote: initNote
 35 
 36 };
 37 
 38  
 39 
 40 export const initStore = ({ dispatch }) => {
 41 
 42  dispatch('INIT_STORE', initData);
 43 
 44 };
 45 
 46 // 更新當前activeNote對象
 47 
 48 export const updateActiveNote = makeAction('SET_ACTIVE_NOTE');
 49 
 50  
 51 
 52 // 添加一個note對象
 53 
 54 export const newNote = makeAction('NEW_NOTE');
 55 
 56  
 57 
 58 // 刪除一個note對象
 59 
 60 export const deleteNote = makeAction('DELETE_NOTE');
 61 
 62 export const toggleFavorite = makeAction('TOGGLE_FAVORITE');
 63 
 64 export const editNote = makeAction('EDIT_NOTE');
 65 
 66  
 67 
 68 // 更新列表展現
 69 
 70 export const updateShow = makeAction('SET_SHOW_ALL');
 71 
 72 建立 Vuex Getters
 73 
 74 在 vuex/ 下面創建一個 getter.js 文件,用來從 store 獲取數據。
 75 
 76  
 77 
 78 // 獲取 noteList,這裏將會根據 state.show 的狀態作數據過濾
 79 
 80 export const filteredNotes = (state) => {
 81 
 82  if(state.show === 'all'){
 83 
 84  return
 85 state.notes || {};
 86 
 87  }else
 88 if(state.show === 'favorite'){
 89 
 90  return
 91 state.notes.filter(note => note.favorite) || {};
 92 
 93  }
 94 
 95 };
 96 
 97  
 98 
 99  
100 
101 // 獲取列表展現狀態 : all or favorite
102 
103 export const show = (state) => {
104 
105  return
106 state.show;
107 
108 };
109 
110  
111 
112 // 獲取當前激活 note
113 
114 export const activeNote = (state) => {
115 
116  return
117 state.activeNote;
118 
119 };

 

以上就是咱們 Vuex 的全部邏輯了,在定下了咱們須要完成的功能以後,接下來就是隻須要在組件中去調用 action 來實現對應的功能了。

路由配置
在這裏咱們將使用 vue-router 來作路由,引用 bootstrap 樣式。

index.html

 1 <!DOCTYPE html>
 2 
 3 <html>
 4 
 5  <head>
 6 
 7  <meta
 8 charset="utf-8">
 9 
10  <title>vuex-notes-app</title>
11 
12  <link
13 rel="stylesheet"
14 href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
15 
16  </head>
17 
18  <body>
19 
20  <div
21 id="app"></div>
22 
23  <!-- built files will be auto injected -->
24 
25  </body>
26 
27 </html>

 

全部的入口邏輯咱們都將在 main.js 中編寫

main.js

 
 1 import Vue from 'vue';
 2 
 3 import App from './App';
 4 
 5  
 6 
 7 import VueRouter from 'vue-router';
 8 
 9 import VueResource from 'vue-resource';
10 
11  
12 
13 // 路由模塊和HTTP模塊
14 
15 Vue.use(VueResource);
16 
17 Vue.use(VueRouter);
18 
19  
20 
21 const router = new
22 VueRouter();
23 
24  
25 
26 router.map({
27 
28  '/index': {
29 
30  component: App
31 
32  }
33 
34 });
35 
36  
37 
38 router.redirect({
39 
40  '*': '/index'
41 
42 });
43 
44  
45 
46 router.start(App, '#app');

 


根組件 App.vue

 1 <template>
 2 
 3  <div id="app"
 4 class="app">
 5 
 6  <toolbar></toolbar>
 7 
 8  <notes-list></notes-list>
 9 
10  <editor></editor>
11 
12  </div>
13 
14 </template>
15 
16  
17 
18 <style>
19 
20  html,
21 #app {
22 
23  height: 100%;
24 
25  }
26 
27  
28 
29  body {
30 
31  margin: 0;
32 
33  padding: 0;
34 
35  border: 0;
36 
37  height: 100%;
38 
39  max-height: 100%;
40 
41  position: relative;
42 
43  }
44 
45 </style>
46 
47  
48 
49 <script>
50 
51  import Toolbar from './components/Toolbar';
52 
53  import NotesList from './components/NotesList';
54 
55  import Editor from './components/Editor';
56 
57  import store from './vuex/store';
58 
59  import { initStore } from './vuex/actions';
60 
61  
62 
63  export default
64 {
65 
66  components: {
67 
68  Toolbar,
69 
70  NotesList,
71 
72  Editor
73 
74  },
75 
76  store,
77 
78  vuex: {
79 
80  actions: {
81 
82  initStore
83 
84  }
85 
86  },
87 
88  ready() {
89 
90  this.initStore()
91 
92  }
93 
94  }
95 
96 </script>

 


在根組件中引用了三個子組件:Toolbar.vue, NotesList.vue, Editor.vue。

注意:咱們在配置裏面加入了 vuex 這麼一個選項,這裏用來將咱們 action 裏面定義的方法給暴露出來,咱們在根組件中只作了一件事情,那就是初始化模擬數據,所以咱們在組件生命週期的 ready 階段調用了 actions 裏面的 initStore 來初始化咱們的 store 裏面的 state

Toolbar.vue

  1 <template>
  2 
  3  <div id="toolbar">
  4 
  5  <i class="glyphicon logo"><img src="../assets/logo.png"
  6 width="30"
  7 height="30"></i>
  8 
  9  <i @click="newNote"
 10 class="glyphicon glyphicon-plus"></i>
 11 
 12  <i @click="toggleFavorite"
 13 class="glyphicon glyphicon-star"  :class="{starred: activeNote.favorite}"></i>
 14 
 15  <i @click="deleteNote"
 16 class="glyphicon glyphicon-remove"></i>
 17 
 18  </div>
 19 
 20 </template>
 21 
 22  
 23 
 24 <script>
 25 
 26 import { newNote, deleteNote, toggleFavorite } from '../vuex/actions';
 27 
 28 import { activeNote } from '../vuex/getters';
 29 
 30  
 31 
 32 export default
 33 {
 34 
 35  vuex: {
 36 
 37  getters: {
 38 
 39  activeNote
 40 
 41  },
 42 
 43  actions: {
 44 
 45  newNote,
 46 
 47  deleteNote,
 48 
 49  toggleFavorite
 50 
 51  }
 52 
 53  }
 54 
 55 }
 56 
 57 </script>
 58 
 59  
 60 
 61 <style lang="scss"
 62 scoped>
 63 
 64  #toolbar{
 65 
 66  float: left;
 67 
 68  width: 80px;
 69 
 70  height: 100%;
 71 
 72  background-color:
 73 #30414D;
 74 
 75  color:
 76 #767676;
 77 
 78  padding: 35px 25px 25px 25px;
 79 
 80  
 81 
 82  .starred {
 83 
 84  color:
 85 #F7AE4F;
 86 
 87  }
 88 
 89  
 90 
 91  i{
 92 
 93  font-size: 30px;
 94 
 95  margin-bottom: 35px;
 96 
 97  cursor: pointer;
 98 
 99  opacity: 0.8;
100 
101  transition: opacity 0.5s ease;
102 
103  
104 
105  &:hover{
106 
107  opacity: 1;
108 
109  }
110 
111  }
112 
113  }
114 
115 </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 方法獲取筆記列表

 1 // 獲取 noteList,這裏將會根據 state.show 的狀態作數據過濾
 2 
 3 export const filteredNotes = (state) => {
 4 
 5  if(state.show === 'all'){
 6 
 7  return
 8 state.notes || {};
 9 
10  }else
11 if(state.show === 'favorite'){
12 
13  return
14 state.notes.filter(note => note.favorite) || {};
15 
16  }
17 
18 };

 

能夠看到,咱們獲取的列表是依賴於 state.show 這個狀態的。而咱們的切換列表操做剛好就是調用 actions 裏面的方法來更新 state.show,這樣一來,實現了數據列表的動態刷新,並且咱們對樹的操做都是經過調用 actions 的方法來實現的。

咱們再看,在切換列表的時候,咱們還須要動態的更新 activeNote。看看咱們在 store.js 中是如何作的:

 1 // 切換顯示數據列表類型:所有 or 收藏
 2 
 3 SET_SHOW_ALL(state, show){
 4 
 5  state.show = show;
 6 
 7  // 切換數據展現,須要同步更新 activeNote
 8 
 9  if(show === 'favorite'){
10 
11  state.activeNote = state.notes.filter(note => note.favorite)[0] || {};
12 
13  }else{
14 
15  state.activeNote = state.notes[0] || {};
16 
17  }
18 
19 }

 

觸發這些操做的是咱們給兩個按鈕分別綁定了咱們自定義的函數,經過給函數傳入不一樣的參數,而後調用 actions 裏面的方法,來實現對數據的過濾,更新。

Editor.vue

 1 <template>
 2 
 3  <div id="note-editor">
 4 
 5  <div class="form-group">
 6 
 7  <input type="text"
 8 name="title"
 9 
10  class="title form-control"
11 
12  placeholder="請輸入標題"
13 
14  @input="updateNote"
15 
16  v-model="currentNote.title">
17 
18  <textarea
19 
20  v-model="currentNote.content"
21 name="content"
22 
23  class="form-control"
24 row="3"
25 placeholder="請輸入正文"
26 
27  @input="updateNote"></textarea>
28 
29  </div>
30 
31  </div>
32 
33 </template>
34 
35  
36 
37 <script>
38 
39  import { editNote } from '../vuex/actions';
40 
41  import { activeNote } from '../vuex/getters';
42 
43  
44 
45  export default
46 {
47 
48  vuex: {
49 
50  getters: {
51 
52  activeNote
53 
54  },
55 
56  actions: {
57 
58  editNote
59 
60  }
61 
62  },
63 
64  computed: {
65 
66  // 經過計算屬性獲得的一個對象,這樣子咱們就能愉快的使用 v-model 了
67 
68  currentNote: activeNote
69 
70  },
71 
72  methods: {
73 
74  // 爲何這麼作? 由於在嚴格模式中不容許直接在模板層面去修改 state 中的值
75 
76  updateNote() {
77 
78  this.editNote(this.currentNote);
79 
80  }
81 
82  }
83 
84  }
85 
86 </script>

 

在 Editor.vue 組件中,咱們須要可以實時的更新當前的 activeNote 組件和列表中對應的咱們正在修改的筆記對象的內容。

因爲咱們前面提到過,在組件中是不容許直接修改 store.js在裏面的狀態值的,因此在這裏的時候,咱們經過一個計算屬性,將 store 裏面的狀態值賦值給一個對象,而後在自定義的 updateNotes() 方法中,去調用 action,同時傳入 currentNote 對象。

在 store.js 中,咱們是這麼作的,找到對應的 id 的對象,從新賦值,由於前面提到過,咱們的數據是響應式的,在這裏進行了改變,對應的視圖也將刷新改變,這樣一來就實現了實時編輯,實時渲染的功能了。

 1 // 修改筆記
 2 
 3 EDIT_NOTE(state, note) {
 4 
 5  state.activeNote = note;
 6 
 7  // 修改原始數據
 8 
 9  for
10 (var
11 i = 0; i < state.notes.length; i++) {
12 
13  if(state.notes[i].id === note.id){
14 
15  state.notes[i] = note;
16 
17  break;
18 
19  }
20 
21  };
22 
23 },

 

 

Q&A

在這個項目中,咱們並無引入 vue-resource 插件,只是本身模擬了部分的數據,有興趣的同窗能夠本身去試試。

因爲咱們的例子相對簡單,沒有涉及到很深刻的東西,更深層次的研究須要你們花更多的時間去實踐了。

最後,再說一句,在 action 裏面,咱們其實能夠作的還有更多,好比根據 id 動態的異步獲取筆記內容等等,這些有興趣的同窗能夠本身去嘗試,一點點的豐富這個例子。

相關文章
相關標籤/搜索