Vuex2.0小米便籤項目實例

本文對Vue和Vuex有必定基礎的同窗更容易掌握,如對Vue和Vuex不是很熟悉的同窗,請先移步Vue官網自行學習css

在這個教程中,咱們會經過構建一個小米便籤應用來學習怎麼使用Vuex,開始我會簡單的介紹Vuex的一些基礎內容,何時使用以及用Vuex怎麼組織代碼,而後一步一步的把這些概念應用到小米便籤應用裏面。html

廢話很少說,先給你們看一下小米便籤應用的截圖:前端

clipboard.png

你能夠從GitHub上下載源碼,這裏是項目源代碼的地址和在線預覽地址,安裝成功後推薦使用chrome的設備模式查看效果更佳。vue

clipboard.png

Vuex概述

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

若是你不太理解 Vue.js 應用裏的狀態是什麼意思的話,你能夠想象一下你此前寫的 Vue 組件裏面的 data 字段。Vuex 把狀態分紅組件內部狀態和應用級別狀態:git

  • 組件內部狀態:僅在一個組件內使用的狀態(data 字段)
  • 應用級別狀態:多個組件共用的狀態

舉個例子:好比說有一個父組件,它有兩個子組件。這個父組件能夠用 props 向子組件傳遞數據,這條數據通道很好理解。es6

那若是這兩個子組件相互之間須要共享數據呢?或者子組件須要向父組件傳遞數據呢?這兩個問題在應用體量較小的時候都好解決,只要用自定義事件便可。github

可是隨着應用規模的擴大:web

  • 追蹤這些事件愈來愈難了。這個事件是哪一個組件觸發的?誰在監聽它?
  • 業務邏輯遍及各個組件,致使各類意想不到的問題。
  • 因爲要顯式地分發和監聽事件,父組件和子組件強耦合。

Vuex 要解決的就是這些問題,Vuex 背後有四個核心的概念:vue-router

  • State: 包含全部應用級別狀態的對象
  • Getters: 在組件內部獲取 store 中狀態的函數
  • Mutations: 修改狀態的事件回調函數
  • Actions: 組件內部用來分發 mutations 事件的函數

下面這張圖完美地解釋了一個 Vuex 應用內部的數據流動:

clipboard.png

這張圖的重點:

數據流動是單向的

  • 組件能夠調用 actions
  • Actions 是用來分發 mutations 的
  • 只有 mutations 能夠修改狀態
  • store 是反應式的,即,狀態的變化會在組件內部獲得反映

搭建項目

項目結構:

clipboard.png

項目主要文件存放於src目錄下:

  • assets/公共圖片,css文件
  • components/包含全部組件
  • libs/擴展文件
  • router/路由文件
  • store/vuex相關文件(state,actions,getters,mutation)
  • App.vue根組件
  • main.js應用總入口

新建項目:

使用vue-cli腳手架,可用於快速搭建大型單頁應用。該工具爲現代化的前端開發工做流提供了開箱即用的構建配置。只需幾分鐘便可建立並啓動一個帶熱重載、保存時靜態檢查以及可用於生產環境的構建配置的項目:

# 安裝vue
npm install vue
# 全局安裝 vue-cli
npm install --global vue-cli
# 建立一個基於 webpack 模板的新項目
vue init webpack notepad-xiaomi
# 安裝依賴,走你
cd notepad-xiaomi
# 安裝依賴
npm install muse-ui vue-awesome --save
# 安裝vuex
npm install vue vuex --save
# 運行
npm run dev

使用vue-cli腳手架建立項目時,必定要安裝vue-router插件。
安裝依賴後再main.js中引用

clipboard.png

建立Vuex Store

在store文件夾下建立第一個index.js:

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutation'
import * as getters from './getters'
import * as actions from './actions'

Vue.use(Vuex)

export default new Vuex.Store({
    state,
    mutations,
    getters,
    actions
})

如今我用下面這張圖把應用分解成多個組件,並把組件內部須要的數據對應到 store.js 裏的 state。

clipboard.png
clipboard.png
clipboard.png
clipboard.png

App根組件,第一幅圖中的紅色盒子
Header頭部組件,第一幅圖中的綠色盒子
NoteList列表組件,第一幅圖中的橙色盒子
ToolBar工具欄組件,第一幅圖中的藍色盒子(包括刪除和移動按鈕)
Editor編輯組件,第二幅圖,
NoteFolder便籤夾組件,第三幅圖
TrashHeader廢紙簍頭部組件,第四幅圖藍色盒子
TrashNoteList廢紙簍列表組件,第四幅圖灰色盒子
TrashToolBar廢紙簍工具欄組件,第四幅圖黃色盒子

state.js裏面的狀態對象會包含全部應用級別的狀態,也就是各個組件須要共享的狀態。
筆記列表(notes: [])包含了 NodesList 組件要渲染的 notes 對象。當前便籤(activeNote: {})則包含當前編輯的便籤對象,多個組件都須要這個對象。

聊完了狀態state,咱們來看看 mutations, 咱們要實現的 mutation 方法包括:

  • 添加標籤到notes數組中
  • 編輯選中便籤
  • 刪除便籤
  • 便籤佈局
  • 勾選便籤
  • 所有/取消勾選便籤
  • 保存便籤
  • 勾選廢紙簍便籤
  • 所有/取消勾選廢紙簍便籤
  • 恢復廢紙簍便籤

mutation-types中用於將常量放在單獨的文件中,方便協做開發。

export const NEW_NOTE = 'NEW_NOTE'
export const EDIT_NOTE = 'EDIT_NOTE'
export const TOGGLE_NOTE = 'TOGGLE_NOTE'
export const CANCEL_CHECK = 'CANCEL_CHECK'
export const ALL_CHECK = 'ALL_CHECK'
export const DELETE_NOTE = 'DELETE_NOTE'
export const BACK_SAVE = 'BACK_SAVE'
export const TOGGLE_TRASHNOTE = 'TOGGLE_TRASHNOTE'
export const CANCEL_TRASHCHECk = 'CANCEL_TRASHCHECk'
export const ALL_TRASHCHECK = 'ALL_TRASHCHECK'
export const DELETE_TRASHNOTE = 'DELETE_TRASHNOTE'
export const RECOVERY_NOTE = 'RECOVERY_NOTE'

首先,建立一條新的便籤,咱們須要作的是:

  • 新建一個對象
  • 初始化屬性
  • push到state.notes數組中
[types.NEW_NOTE](state) {
    let newNote = {
        id: +new Date(),
        date: new Date().Format('yyyy-MM-dd hh:mm'),
        content: '',
        done: false
    }
    state.notes.push(newNote)
}

而後,編輯便籤須要用筆記內容 content 做參數:

[types.EDIT_NOTE](state, note) {
    state.activeNote = note;
}

剩下的這些 mutations 很簡單就不一一贅述了。整個 store/mutation.js 以下:

import Format from '../libs/dateFormat'
import * as types from './mutation-types';

const mutations = {
    [types.NEW_NOTE](state) {
        let newNote = {
            id: +new Date(),
            date: new Date().Format('yyyy-MM-dd hh:mm'),
            content: '',
            done: false
        }
        state.notes.push(newNote)
    },
    [types.EDIT_NOTE](state, note) {
        state.activeNote = note;
    },
    [types.TOGGLE_NOTE](state, note) {
        state.notes.map((item, i) => {
            if (item.id == note.id) {
                item.done = !note.done;
            }
        })
        if (note.done) {
            state.deleteNotes.push(note);
        } else {
            state.deleteNotes.splice(state.deleteNotes.indexOf(note), 1);
        }
    },
    [types.CANCEL_CHECK](state) {
        state.notes.map((item, i) => {
            item.done = false;
        })
        state.deleteNotes = [];
        state.isCheck = false;
    },
    [types.ALL_CHECK](state, done) {
        state.deleteNotes = [];
        state.notes.map((item, i) => {
            item.done = done;
            if (done) {
                state.deleteNotes.push(item);
            } else {
                state.deleteNotes = [];
            }
        })
    },
    [types.DELETE_NOTE](state) {
        state.deleteNotes.map((item, i) => {
            item.done = false;
            state.notes.splice(state.notes.indexOf(item), 1);
            state.trashNotes.push(item)
        })
        state.isCheck = false;
        state.deleteNotes = [];
    },
    [types.BACK_SAVE](state, note) {
        if (note.content != '') return;
        state.notes.splice(state.notes.indexOf(note), 1);
    },
    [types.TOGGLE_TRASHNOTE](state, note) {
        state.trashNotes.map((item, i) => {
            if (item.id == note.id) {
                item.done = !note.done;
            }
        })
        if (note.done) {
            state.deleteTrashNotes.push(note);
        } else {
            state.deleteTrashNotes.splice(state.deleteTrashNotes.indexOf(note), 1);
        }
    },
    [types.CANCEL_TRASHCHECk](state) {
        state.trashNotes.map((item, i) => {
            item.done = false;
        })
        state.deleteTrashNotes = [];
        state.isTrashCheck = false;
    },
    [types.ALL_TRASHCHECK](state, done) {
        state.deleteTrashNotes = [];
        state.trashNotes.map((item, i) => {
            item.done = done;
            if (done) {
                state.deleteTrashNotes.push(item);
            } else {
                state.deleteTrashNotes = [];
            }
        })
    },
    [types.DELETE_TRASHNOTE](state) {
        state.deleteTrashNotes.map((item, i) => {
            state.trashNotes.splice(state.trashNotes.indexOf(item), 1);
        })
        state.deleteTrashNotes = [];
        state.isTrashCheck = false;
    },
    [types.RECOVERY_NOTE](state) {
        state.deleteTrashNotes.map((item, i) => {
            item.done = false;
            state.notes.unshift(item)
            state.trashNotes.splice(state.trashNotes.indexOf(item), 1);
        })
        state.deleteTrashNotes = [];
        state.isTrashCheck = false;
    }
}

export default mutations;

接下來聊 actions, actions 是組件內用來分發 mutations 的函數。它們接收 store 做爲第一個參數。比方說,當用戶點擊 Toolbar 組件的添加按鈕時,咱們想要調用一個能分發NEW_NOTE mutation 的 action。如今咱們在 store/文件夾下建立一個 actions.js 並在裏面寫上 newNote函數:

// 建立新便籤
export const newNote = ({ commit }) => {
    commit(types.NEW_NOTE)
}

其餘的這些actions都相似,整個store/actions.js以下:

import * as types from './mutation-types';

//建立新便籤
export const newNote = ({ commit }) => {
    commit(types.NEW_NOTE)
}

//編輯便籤
export const editNote = ({ commit }, note) => {
    commit(types.EDIT_NOTE, note)
}

//勾選便籤
export const toggleNote = ({ commit }, note) => {
    commit(types.TOGGLE_NOTE, note)
}

//取消勾選便籤
export const cancelCheck = ({ commit }) => {
    commit(types.CANCEL_CHECK)
}

//所有勾選
export const allCheck = ({ commit }, done) => {
    commit(types.ALL_CHECK, done)
}

//刪除便籤
export const deleteNote = ({ commit }) => {
    commit(types.DELETE_NOTE)
}

//返回自動保存
export const backSave = ({ commit }, note) => {
    commit(types.BACK_SAVE, note)
}

//勾選廢紙簍便籤
export const toggleTrashNote = ({ commit }, note) => {
    commit(types.TOGGLE_TRASHNOTE, note)
}

//取消勾選廢紙簍便籤
export const cancelTrashCheck = ({ commit }) => {
    commit(types.CANCEL_TRASHCHECk)
}

//全選廢紙簍便籤
export const allTrashCheck = ({ commit }, done) => {
    commit(types.ALL_TRASHCHECK, done)
}

//刪除廢紙簍便籤
export const deleteTrashNote = ({ commit }) => {
    commit(types.DELETE_TRASHNOTE)
}

//恢復便籤
export const recoveryNote = ({ commit }) => {
    commit(types.RECOVERY_NOTE)
}

最後說一下getters,在Store倉庫裏,state就是用來存放數據,如果對數據進行處理輸出,好比數據要過濾,通常咱們能夠寫到computed中。可是若是不少組件都使用這個過濾後的數據,好比餅狀圖組件和曲線圖組件,咱們是否能夠把這個數據抽提出來共享?這就是getters存在的意義。咱們能夠認爲,getters是store的計算屬性

// 搜索過濾便籤
export const filterNote = (state) => {
    if (state.search != '' && state.notes.length > 0) {
        return state.notes.filter(note => note.content.indexOf(state.search) > -1) || {}
    } else {
        return state.notes || {}
    }
}
// 當前編輯的便籤
export const activeNote = (state) => {
    return state.activeNote
}
// 便籤列表佈局
export const layout = state => state.layout
// 便籤選中狀態
export const isCheck = state => state.isCheck
// 廢紙簍便籤選中狀態
export const isTrashCheck = state => state.isTrashCheck

這樣,在 store文件夾裏面要寫的代碼就都寫完了。這裏麪包括了 state.js 中的 state 和 mutation.js中的mutations,以及 actions.js 裏面用來分發 mutations 的 actions,和getters.js中的處理輸出。

構建Vue組件

最後這個小結,咱們來實現四個組件 (App, Header,Toolbar, NoteList 和 Editor) 並學習怎麼在這些組件裏面獲取 Vuex store 裏的數據以及調用 actions。

建立根實例 - main.js

main.js是應用的入口文件,裏面有根實例,咱們要把 Vuex store 加到到這個根實例裏面,進而注入到它全部的子組件裏面:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store/index'

/* 第三方插件 */
import MuseUI from 'muse-ui'
import 'muse-ui/dist/muse-ui.css'
import 'muse-ui/dist/theme-teal.css'
import Icon from 'vue-awesome/components/Icon'
import 'vue-awesome/icons/flag'
import 'vue-awesome/icons'

Vue.use(MuseUI)
Vue.component('icon', Icon);
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
    el: '#app',
    router,
    store,
    components: { App },
    template: '<App/>'
})

App - 根組件

根組件 App 做爲總的路由入口:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

Notepad

Notepad 組件會 import 其他三個組件:Header,NoteList和ToolBar:

<template>
    <div class="notepad">
        <Header />
        <NoteList />
        <ToolBar />
    </div>
</template>

<script>
import Header from './Header'
import NoteList from './NoteList'
import ToolBar from './ToolBar'
export default {
    name: 'Notepad',
    data () {
        return {
        }
    },
    components:{
        Header,
        NoteList,
        ToolBar,
    }
}
</script>

Header

Header組件提供搜索和便籤勾選和取消,並統計勾選數量功能,如圖:

clipboard.png

對於Header組件來講,搜索框中輸入查詢內容時,須要對便籤列表中的數據進行過濾,在建立state.js的時候就添加了search字段,用於存儲搜索內容,而在getters.js中經過filterNote方法對便籤列表進行過濾,篩選出符合條件的便籤並返回,這時候咱們在NoteList組件中就直接遍歷filterNote方法就能夠實現搜索功能。

store/getters中實現filterNote方法

// 搜索過濾便籤
export const filterNote = (state) => {
    if (state.search != '' && state.notes.length > 0) {
        return state.notes.filter(note => note.content.indexOf(state.search) > -1) || {}
    } else {
        return state.notes || {}
    }
}

NoteList組件中遍歷filterNote

<li v-for="note in filterNote" :key="note.id" @mousedown="gtouchstart(note)" @mouseup="gtouchend(note)" @touchstart="loopstart(note)" @touchend="clearLoop">
    <h4>{{note.date}}</h4>
    <p>{{note.content}}</p>
    <mu-checkbox label="" v-model="note.done" class="checkbox" v-show="isCheck"/>
</li>

Header組件:

...mapGetters中的...是es6的擴展運算符,不懂的能夠查閱es6文檔

<template>
    <header class="header" :class="{visible:isVisible}">
        <mu-flexbox class="headerTool" :class="{visible:isVisible}">
            <mu-flexbox-item order="0" class="flex">
                <mu-raised-button v-if="isCheck" label="取消" @click="cancelCheck" class="raised-button"/>
                <span v-else class="icon" @click="openFolder"><icon name="folder-open"></icon></span>
            </mu-flexbox-item>
            <mu-flexbox-item order="1" class="flex" style="text-align:center">
                <span v-if="isCheck">{{checkTitle}}</span>
                <span v-else>{{title}}</span>
            </mu-flexbox-item>
            <mu-flexbox-item order="2" class="flex" style="text-align:right">
                <mu-raised-button v-if="isCheck" :label="checkBtnTxt" @click="allCheck(!allChecked)" class="raised-button"/>
                <span v-else>
                    <span class="icon" v-if="layout=='grid'" @click="changeLayout"><icon name="list"></icon></span>
                    <span class="icon" v-else @click="changeLayout"><icon name="th-large"></icon></span>
                </span>
            </mu-flexbox-item>
        </mu-flexbox>
        <div class="search">
            <div class="icon"><icon name="search"></icon></div>
            <input type="text" v-model="searchTxt" @keyup="search" @focus="searchFocus" @blur="searchBlur"/>
        </div>
    </header>
</template>

<script>
import { mapActions,mapGetters } from 'vuex'
export default {
    name: 'Header',
    data(){
        return {
            title:'便籤',
            checkBtnTxt:'全選',
            searchTxt:'',
            isVisible:false
        }
    },
    computed:{
        ...mapGetters([
            'layout',
            'isCheck'
        ]),
        //獲取便籤勾選狀態
        allChecked(){
            return this.$store.state.notes.every(note => note.done)
        },
        //便籤選中數量提示
        checkTitle(){
            return `已選擇${this.$store.state.deleteNotes.length}項`
        }
    },
    methods:{
        //顯示搜索框
        searchFocus(){
            this.isVisible = true;
        },
        //隱藏搜索框
        searchBlur(){
            this.isVisible = false;
        },
        //搜索
        search(){
            this.$store.state.search = this.searchTxt
        },
        //切換佈局
        changeLayout(){
            if(this.$store.state.layout == 'list'){
                this.$store.state.layout = 'grid'
            }else{
                this.$store.state.layout = 'list'
            }
            
        },
        //取消勾選
        cancelCheck(){
            this.$store.dispatch('cancelCheck')
        },
        //全選切換
        allCheck(done){
            this.checkBtnTxt = done?'取消全選':'全選'
            this.$store.dispatch('allCheck',done)
        },
        //打開便籤夾
        openFolder(){
            this.$router.push({path:'noteFolder'})
        }
    }
}
</script>

NoteList

NotesList 組件主要有三個功能:

  • 渲染便籤列表
  • 對便籤進行勾選或取消
  • 點擊編輯便籤
<template>
    <ul class="noteList" :class="layout">
        <li v-for="note in filterNote" :key="note.id" @mousedown="gtouchstart(note)" @mouseup="gtouchend(note)" @touchstart="loopstart(note)" @touchend="clearLoop">
            <h4>{{note.date}}</h4>
            <p>{{note.content}}</p>
            <mu-checkbox label="" v-model="note.done" class="checkbox" v-show="isCheck"/>
        </li>
    </ul>
</template>

<script>
import { mapGetters,mapActions } from 'vuex'
export default {
    name: 'NoteList',
    data(){
        return {
            timeOutEvent: 0,
            Loop:null
        }
    },
    computed:{
        ...mapGetters([
            'filterNote',
            'layout',
            'isCheck'
        ])
    },
    methods:{
        //編輯&選中
        editNote(note){
            if(this.isCheck){
                this.$store.dispatch('toggleNote',note);
            }else{
                this.$store.dispatch('editNote',note);
                this.$router.push({path:'/editor'})
            }
            
        },
        //鼠標按下,模擬長按事件
        gtouchstart(note){
            var _this = this;
            this.timeOutEvent = setTimeout(function(){
                _this.longPress(note)
            },500);//這裏設置定時器,定義長按500毫秒觸發長按事件,時間能夠本身改,我的感受500毫秒很是合適
            return false;
        },
        //鼠標放開,模擬長按事件
        gtouchend(note){
            clearTimeout(this.timeOutEvent);//清除定時器
            if(this.timeOutEvent!=0){
                //這裏寫要執行的內容(尤如onclick事件)
                this.editNote(note);
            }
            return false;
        },
        longPress(note){
            this.timeOutEvent = 0;
            this.$store.state.isCheck = true;
            this.$store.dispatch('toggleNote',note);
        },
        //手按住開始,模擬長按事件
        loopstart(note){
            var _this = this;
            clearInterval(this.Loop);
       this.Loop = setTimeout(function(){
             _this.$store.state.isCheck = true;
                _this.$store.dispatch('toggleNote',note);
      },500);
        },
        //手放開結束,模擬長按事件
        clearLoop(){
            clearTimeout(this.Loop);
        }
    }
}
</script>

ToolBar

Toolbar組件提供給用戶三個按鈕:建立便籤,編輯便籤和移動便籤(移動便籤功能尚未作):

<template>
    <div class="toolBar">
        <div class="toolBtn" v-if="isCheck">
            <span class="icon" @click="deleteNote"><icon name="trash-alt"></icon></span>
            <span class="icon"><icon name="dolly"></icon></span>
        </div>
        <div class="addNote" v-else>
            <div class="float-button mu-float-button" @click="addNote"><icon name="plus"></icon></div>
        </div>
        <mu-dialog :open="dialog" title="刪除便籤" @close="close">
            您肯定刪除所選便籤嗎?
            <mu-flat-button slot="actions" @click="close" primary label="取消"/>
            <mu-flat-button slot="actions" primary @click="deleteConfirm" label="肯定"/>
        </mu-dialog>
    </div>
</template>

<script>
import { mapGetters,mapActions } from 'vuex'
export default {
    name: 'ToolBar',
    data(){
        return {
            dialog: false
        }
    },
    computed:{
        ...mapGetters([
            'isCheck'
        ])
    },
    methods:{
        //添加便籤
        addNote(){
            this.$store.dispatch('newNote');
            this.$router.push({path:'editor'});
        },
        //刪除便籤
        deleteNote(){
            this.dialog = true;
        },
        //關閉窗口
        close () {
            this.dialog = false;
        },
        //肯定刪除
        deleteConfirm(){
            this.dialog = false;
            this.$store.dispatch('deleteNote');
        }
    }
}
</script>

Editor

Editor 組件是最簡單的,它只作兩件事:

從 store 獲取當前筆記activeNote,把它的內容展現在 textarea
在用戶更新筆記的時候,調用 editNote() action
如下是完整的 Editor.vue:

<template>
    <div class="edit-panel">
        <div class="edit-tool">
            <span class="back-list" @click="backList"><icon name="angle-left"></icon></span>          
            <span class="date" v-text="activeNote.date"></span>
            <span class="saveNote" v-show="isShow" @click="backList">完成</span>
        </div>
        <textarea v-focus class="edit-area" v-model="activeNote.content" @keyup="editorNote"></textarea>
    </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
    name: 'Editor',
    data(){
        return {
            content:'',
            isShow:false
        }
    },
    created(){
        this.content = this.activeNote.content
    },
    computed:{
        //獲取正在操做的便籤
        ...mapGetters([
            'activeNote'
        ])
    },
    directives:{
        focus:{
            inserted(el){
                el.focus();
            }
        }
    },
    methods:{
        //返回便籤列表
        backList(){
            this.$router.push({path:'/'})
            this.$store.dispatch('backSave',this.activeNote)
        },
        //完成按鈕顯示&隱藏
        editorNote(){
            if(this.content != this.activeNote.content){
                this.isShow = true;
            }else{
                this.isShow = false;
            }
        }
    }
}
</script>

這就是一個小米便籤的建立和編輯,還有刪除以及廢紙簍功能這裏就很少說了,功能都很簡單不明白的地方能夠看源代碼,而後本身實戰操做一番,若有寫的不對的地方你們提出來,互相學習互相幫助嘛,謝謝!

來都來了點一下贊吧,你的贊是對我最大的鼓勵^_^

相關文章
相關標籤/搜索