0.前言


若文章中存在内容无法加载的情况,请移步作者其他博客。

最近在看 Vue 的时候,别人给我安利了一个国外的小案例,通过 Vue 和 Vuex 来实现一个记事本。

仔细剖析下,发现“麻雀虽小,五脏俱全”,是一个挺适合初学者学习分析的一个案例,所以自己也将自己的学习过程整理,得出本文。

国际惯例,首先感谢原文作者。

参考案例传送门:

Learn Vuex by Building a Notes App

之后是内容声明:

  • 原文是2016年 4 月 20 日就出现了的,所以很多小伙伴可能已经看过了,但是本文的实现过程却和原文不同,所以,你其实也可以重新看一看的#斜眼笑。
  • 本文仅用于作者记录使用,请勿转载,请随意吐槽。

好了,开始正文。

1. 前期准备


本文中使用了以下内容,在阅读本文前,请保证您对以下内容有了基础的了解。

之前作者写过一篇关于 Vue 基础入门的文章。

里面介绍了一下关于 Vue 的发展前景,以及 Vue 最基础的使用,大家可以选择性的阅读一下。

2.需求分析


首先,如果我们想要制作一个单页应用,我们首先要知道,我们要做什么?

那么,首先来一个草图。

这时候,我们一起来分析一下,当前页面的实现过程。

  • 页面中分为三个部分
    • 左侧工具栏: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 服务

npm run dev

在启动服务的时候,也有可能会遇到 端口被占用 的错误。

第一种解决方案是进入 Vue 中的 index.js 中修改 默认端口号。

第二种是自己去找到被占用的端口,kill 掉它(一般 kill node 的就可以)。

如果这时候页面中已经弹出一个新的页面,则证明你当前的服务启动成功了。

这里就不单纯的介绍项目的内容组成了,具体的可以参考我之前的文章。

4. 项目组件划分


在开始之前,就如我们上面的分析一般,我们需要将我们所要使用的内容进行划分。

作者留言:
Vue 中最重要的两个概念,理解了这两个概念对以后会有很大帮助。

  • 模块化编程
  • 数据驱动

根据页面中的功能,我们可以将页面分成四个大块。

首先第一个肯定是最外层的父级,我们一般直接书写在 App.vue当中。

其次是左中右三部分的组件,我们分别命名并统一放在 components 当中。

  • 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 当中调用的。

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日