例項 - Vue 單頁應用:記事本

MR_LP發表於2017-04-17

請各位讀者新增一下作者的微信公眾號,以後有新的文章,將在微信公眾號直接推送給各位,非常感謝。

例項 - Vue 單頁應用:記事本

0.前言


若文章中存在內容無法載入的情況,請移步作者其他部落格。

最近在看 Vue 的時候,別人給我安利了一個國外的小案例,通過 Vue 和 Vuex 來實現一個記事本。

仔細剖析下,發現“麻雀雖小,五臟俱全”,是一個挺適合初學者學習分析的一個案例,所以自己也將自己的學習過程整理,得出本文。

國際慣例,首先感謝原文作者。

參考案例傳送門:

Learn Vuex by Building a Notes App

之後是內容宣告:

  • 原文是2016年 4 月 20 日就出現了的,所以很多小夥伴可能已經看過了,但是本文的實現過程卻和原文不同,所以,你其實也可以重新看一看的#斜眼笑。
  • 本文僅用於作者記錄使用,請勿轉載,請隨意吐槽。

另請注意,很多童鞋一直在問我,為什麼貼上完程式碼無效,或者報錯的。

請在使用前安裝環境。

另作者已經將完整程式包放在 Git 上了,請點選下方連結進行下載,別忘了給我個 Star 呀!笑。

好了,開始正文。

1. 前期準備


本文中使用了以下內容,在閱讀本文前,請保證您對以下內容有了基礎的瞭解。

之前作者寫過一篇關於 Vue 基礎入門的文章。

裡面介紹了一下關於 Vue 的發展前景,以及 Vue 最基礎的使用,大家可以選擇性的閱讀一下。

2.需求分析


首先,如果我們想要製作一個單頁應用,我們首先要知道,我們要做什麼?

那麼,首先來一個草圖。

例項 - Vue 單頁應用:記事本

這時候,我們一起來分析一下,當前頁面的實現過程。

  • 頁面中分為三個部分
    • 左側工具欄:Toolbar
    • 中間筆記列表:NoteList
    • 右側編輯區域:Editor
  • 頁面樣式的設定
  • 在頁面的實現過程中,需要完成以下幾個方法
    • 新增筆記
    • 修改筆記
    • 刪除筆記
    • 切換筆記的收藏與取消收藏
    • 切換顯示資料列表型別
    • 設定當前啟用的筆記

這時候我們明確了當前內容至少會涉及到頁面,頁面美化,以及資料處理等。

那我們就可以針對特定的需求來進行特定內容的處理了。

  • 結構:用 Vue-cli 來快速生成
  • 美化:想了想,還是自己手動寫吧
  • 資料:選用 Vuex 來集中處理

但是在正式開始文章前,請先了解一下,關於 Vuex 的基礎知識。

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式
它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
這就是 Vuex 背後的基本思想,借鑑了 FluxRedux、和 The Elm Architecture
與其他模式不同的是,Vuex 是專門為 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度資料響應機制來進行高效的狀態更新。

在這裡有一個需要注意的內容,就是關於 Vuex 中的 Store。

每一個 Vuex 應用的核心就是 store(倉庫)。"store" 基本上就是一個容器,它包含著你的應用中大部分的狀態(state)。Vuex 和單純的全域性物件有以下兩點不同:

  • Vuex 的狀態儲存是響應式的。當 Vue 元件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的元件也會相應地得到高效更新。
  • 你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交(commit) mutations。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地瞭解我們的應用。

其實說白了,我們的 state 就是我們專案中所有資料的集合,之後通過 Vuex 來區分開實際應用中的 元件本地狀態應用層級狀態

這裡需要區分一下,關於 元件本地狀態應用層級狀態

  • 元件本地狀態
    • state that is used by only one component (think of the data option you pass to a Vue component)
    • 該狀態表示僅僅在元件內部使用的狀態,有點類似通過配置選項傳入 Vue 元件內部的意思
  • 應用層級狀態
    • state that is used by more than one component
    • 應用層級狀態,表示同時被多個元件共享的狀態層級

如果你明白了上面的內容,那麼接下來,我們就可以一起來構建我們的新專案了。

3.專案構建


專案推薦直接使用 Vue 官方提供的腳手架(Vue-cli),所以第一步首先是安裝腳手架。

PS: 作者預設大家是對 Vue 有一定的基礎瞭解之後再看的文字,所以如果有哪些步驟不明確,請參考 Vue - 起手式

安裝 Vue-cli

npm install -g vue-cli複製程式碼

注意:

  • -g 是直接安裝在全域性環境下,推薦大家也是如此。
  • 推薦大家確認一下自己當前 node 的版本,儘量是最新版。
  • 如果發生無法安裝,請確認是否是許可權不足。
    • 如果是許可權不足,請在內容前加上 sudo
    • sudo npm install -g vue-cli

建立應用

vue init webpack note複製程式碼
  • webpack 是我們安裝內容時所預設使用的模板。
  • note 是我們建立的專案名稱
  • 安裝過程中,會出現詢問你具體專案資訊的內容
    • 推薦大家都直接選擇拒絕即可。
      • 詢問內容:專案名,描述,作者三項,直接回車即可
      • 檢查測試:語法檢查,單元測試,專案測試三項直接輸入 N

進入當前目錄

cd /Users/lp1/Desktop/notes    (你當前的檔案目錄)複製程式碼

安裝 Vue 的依賴包

npm install複製程式碼

如果不安裝依賴,經常會發生下面這種錯誤。

例項 - Vue 單頁應用:記事本

啟動 Vue 服務

npm run dev複製程式碼

在啟動服務的時候,也有可能會遇到 埠被佔用 的錯誤。

第一種解決方案是進入 Vue 中的 index.js 中修改 預設埠號。

例項 - Vue 單頁應用:記事本

第二種是自己去找到被佔用的埠,kill 掉它(一般 kill node 的就可以)。

例項 - Vue 單頁應用:記事本

例項 - Vue 單頁應用:記事本

如果這時候頁面中已經彈出一個新的頁面,則證明你當前的服務啟動成功了。

這裡就不單純的介紹專案的內容組成了,具體的可以參考我之前的文章。

4. 專案元件劃分


在開始之前,就如我們上面的分析一般,我們需要將我們所要使用的內容進行劃分。

作者留言:
Vue 中最重要的兩個概念,理解了這兩個概念對以後會有很大幫助。

  • 模組化程式設計
  • 資料驅動

根據頁面中的功能,我們可以將頁面分成四個大塊。

例項 - Vue 單頁應用:記事本

首先第一個肯定是最外層的父級,我們一般直接書寫在 App.vue當中。

其次是左中右三部分的元件,我們分別命名並統一放在 components 當中。

例項 - Vue 單頁應用:記事本

  • Toolbar : 工具欄用於對當前內容進行新增和刪除
  • NoteList : 列表通過操作 CSS 來高亮我們選中的內容
  • Editor : 編輯器用於顯示使用者的編輯操作

而最下面的 App.vue 則是所有元件的根。

那我們現在雖然將不同的元件進行了劃分,可以劃分之後我們該如何去處理三個元件之間的通訊呢?

這時候其實就該我們的 Vuex 出馬了,Vuex 作為一個“資料中心”,我們可以提前將我們想要的內容,進行提前設定。

5.狀態管理


著重強調:
vuex 中資料是單向的,只能從 store 獲取,而我們的各種操作也始終都在 store.js 中維護,並以此來給其他元件公用。

那根據我們上面所說,我們需要在 Vuex 資料夾下,建立一個 store.js 檔案。

需要注意,這裡使用很多 ES6 的語法,並且採用了原文不同的實現方法。

//引入vue及vuex
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

//需要維護的狀態
const state = {
    /*
        notes:儲存note項
        activeNote:當前正在編輯的note項
    */
    notes:[],
    activeNote:{}
}

const mutations = {
    //新增筆記
    ADD_NOTE(state){
        const newNote = {
            /*
                text:預設文字內容
                favorite:收藏
            */
            text:"new Note",
            favorite:false
        }
        state.notes.push(newNote)
        state.activeNote = newNote
    },
    //編輯筆記
    EDIT_NOTE(state,text){
        state.activeNote.text = text
    },
    // 設定當前啟用的筆記
    SET_ACTIVE_NOTE(state,note){
        state.activeNote = note
    },
    // 切換筆記的收藏與取消收藏
    TOGGLE_FAVORITE(state){
        state.activeNote.favorite = !state.activeNote.favorite
    },
    //刪除筆記
    DELETE_NOTE(state){

        for (var i=0; i<state.notes.length; i++){
            if (state.notes[i] == state.activeNote){
                state.notes.splice(i, 1)
            }
        }
        state.activeNote = state.notes[0]
    }
}

const actions = {
    /*
        actions處理函式接受一個 context 物件
        {
          state,     // 等同於 store.state, 若在模組中則為區域性狀態
          rootState, // 等同於 store.state, 只存在於模組中
          commit,    // 等同於 store.commit
          dispatch,  // 等同於 store.dispatch
          getters    // 等同於 store.getters
        }
    */
    addNote({commit}){
        commit('ADD_NOTE')
    },
    editNote({commit},text){
        commit("EDIT_NOTE",text)
    },
    updateActiveNote({commit},note){
        commit('SET_ACTIVE_NOTE',note)
    },
    toggleFavorite({commit}){
        commit('TOGGLE_FAVORITE')
    },
    deleteNote({commit}){
        commit('DELETE_NOTE')
    }
}
const getters = {
    /*
        Getters 接受 state 作為其第一個引數
        state => state.notes為箭頭函式等價於:
        function (state){
            return state.notes
        }
    */
  notes: state => state.notes,
  activeNote: state => state.activeNote
}

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
})複製程式碼

記得處理完我們所需要的資料之後,在 main.js 當中將我們的 store 新增上去。

import Vue from 'vue'
import App from './App'
import store from '../vuex/store'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  store,
  template: '<App/>',
  components: { App }
})複製程式碼

6. 根元件


對於整個 APP 的根,也就是 App.vue 來說,它需要處理的事情非常簡單,就是在對應的位置去呼叫對應的元件即可。

<template>
  <div id="app">
    <toolbar></toolbar>
    <note-list></note-list>
    <editor></editor>
  </div>
</template>
<!--
  李鵬 QQ:3206064928
-->
<script>
import Toolbar from './components/Toolbar'
import NoteList from './components/NoteList'
import Editor from './components/Editor'

export default {
  components:{
    Toolbar,
    NoteList,
    Editor
  }
}
</script>
<style type="text/css">
html, #app {
  height: 100%;
}

body {
  margin: 0;
  padding: 0;
  border: 0;
  height: 100%;
  max-height: 100%;
  position: relative;
}
</style>複製程式碼

至於呼叫的元件內部,具體是如何實現的 App.vue 並不關心。

7. Toolbar.vue


關於 Toolbar.vue 的設定就比較簡單了,我們只需要呼叫我們之前設定好的內容就可以。

<template>
  <div id="toolbar">
    <i @click="addOne" class="glyphicon glyphicon-plus"></i>
    <i @click="toggleFavorite" class="glyphicon glyphicon-star" v-bind:class="{starred:activeNote.favorite}"></i>
    <i @click="deleteNote" class="glyphicon glyphicon-remove"></i>
  </div>
</template>

<script>
export default {
  computed:{
    activeNote(){
      return this.$store.getters.activeNote
    }
  },
  methods:{
    addOne(){
      //通過dispatch分發到actions中的addNote
      this.$store.dispatch('addNote')
    },
    toggleFavorite(){
      this.$store.dispatch('toggleFavorite')
    },
    deleteNote(){
      this.$store.dispatch('deleteNote')
    }
  }
}
</script>
<style type="text/css">

#toolbar {
  float: left;
  width: 80px;
  height: 100%;
  background-color: #30414D;
  color: #767676;
  padding: 35px 25px 25px 25px;
}
#toolbar i {
  font-size: 30px;
  margin-bottom: 35px;
  cursor: pointer;
  opacity: 0.8;
  transition: opacity 0.5s ease;
}

#toolbar i:hover {
  opacity: 1;
}
.starred {
  color: #F7AE4F;
}
</style>複製程式碼

需要注意,在這裡,我呼叫了一下 bootstrap 的圖示樣式。

這個是在 index.js 當中呼叫的。

例項 - Vue 單頁應用:記事本

8. NoteList.vue


由於我們之前已經將關於資料部分的內容處理過了,所以在這裡,我們只需要進行一下簡單的判斷,將特定的內容載入即可。

<template>
  <div id="notes-list">
    <div id="list-header">
      <h2>Notes</h2>
      <div class="btn-group btn-group-justified" role="group">
        <!-- All Notes button -->
        <div class="btn-group" role="group">
          <button @click="show='all'" type="button" class="btn btn-default" v-bind:class="{active:show=='all'}">
            All Notes
          </button>
        </div>
        <!-- Favorites Button -->
        <div class="btn-group" role="group">
          <button @click="show='favorites'" type="button" class="btn btn-default" v-bind:class="{active:show=='favorites'}">
            Favorites
          </button>
        </div>
      </div>
    </div>
    <!-- render notes in a list -->
    <div class="container">
      <div class="list-group">
        <a v-for="item in notes" class="list-group-item" v-bind:class="{active:activeNote == item}" v-on:click="updateActiveNote(item)" href="#">
          <h4 class="list-group-item-heading">
            {{item.text}}
          </h4>
        </a>
      </div>
    </div>

  </div>
</template>

<script>
export default {
  data(){
    return {
      show:'all'
    }
  },
  computed:{
    notes(){
      if (this.show=='all'){
        return this.$store.getters.notes
      }else if(this.show=='favorites'){
        return this.$store.getters.notes.filter(note=>note.favorite)
      }
    },
    activeNote(){
      return this.$store.getters.activeNote
    }
  },
  methods:{
    updateActiveNote(note){
      console.log(note)
      this.$store.dispatch('updateActiveNote',note)
    }
  }
}
</script>
<style type="text/css">
#notes-list {
  float: left;
  width: 300px;
  height: 100%;
  background-color: #F5F5F5;
  font-family: 'Raleway', sans-serif;
  font-weight: 400;
}

#list-header {
  padding: 5px 25px 25px 25px;
}

#list-header h2 {
  font-weight: 300;
  text-transform: uppercase;
  text-align: center;
  font-size: 22px;
  padding-bottom: 8px;
}

#notes-list .container {
  height: calc(100% - 137px);
  max-height: calc(100% - 137px);
  overflow: auto;
  width: 100%;
  padding: 0;
}

#notes-list .container .list-group-item {
  border: 0;
  border-radius: 0;
}
.list-group-item-heading {
  font-weight: 300;
  font-size: 15px;
}
</style>複製程式碼

9. Editor.vue


關於編輯區域,只需要做一件事,就是獲取當前對應內容的文字即可。

<template>
  <div id="note-editor">
    <textarea v-bind:value="activeNoteText" v-on:input="editNote" class="form-control"></textarea>
  </div>
</template>

<script>
export default {
  computed:{
    activeNoteText(){
      return this.$store.getters.activeNote.text
    }
  },
  methods:{
      editNote(e){
          this.$store.dispatch('editNote',e.target.value)
      }
  }
}
</script>
<style type="text/css">  
#note-editor {
  height: 100%;
  margin-left: 380px;
}

#note-editor textarea {
  height: 100%;
  border: 0;
  border-radius: 0;
}
</style>複製程式碼

10.後記


本文主要是用於記錄一下自己的分析過程,如果有哪裡出錯了,歡迎大家指出。

謝謝大家。

李鵬(MR_LP)
2017年04月17日

相關文章