技术博文2016/05/03

【APP开发】 Vuex + Firebase 构建 Notes App

Vuex + Firebase 构建 Notes App – chenyiqiao – SegmentFault

前几天翻译了基于app/" target="_blank">这篇博客的文章:用 Vuex 构建一个笔记应用。在此基础上我对它做了一些更新:

  • 把数据同步到 Firebase 上,不会每次关掉浏览器就丢失数据。
  • 加了笔记检索功能
  • 为保证代码整洁,加上了 eslint

你可以从 app-vuejs-vuex" target="_blank">Github Repo 下载源码,和 Firebase 的同步效果看下面这个 gif:

一、把数据同步到 Firebase

可能你也知道 Vue.js 和 Firebase 合作搞出了一个 Vuefire, 但是在这里并不能用它,因为用 Vuex 管理数据的结果就是组件内部只承担基本的View层的职责,而数据基本上都在 store 里面。所以我们只能把数据的存取放在 store 里面。

1.1 Firebase 概述

如果熟悉 Firebase 的使用,可以放心地跳过这一段。

如果你还没有 Firebase 的账号,可以去注册一个,注册号之后会自动生成一个”MY FIRST APP”,这个初始应用给的地址就是用来存数据的地方。

Firebase 存的数据都是 JSON 对象。我们向 JSON 树里面加数据的时候,这条数据就变成了 JSON 树里的一个键。比方说,在/user/mchen下面加上widgets属性之后,数据就变成了这个样子:

{
  "users": {
    "mchen": {
      "friends": { "brinchen": true },
      "name": "Mary Chen",
      "widgets": { "one": true, "three": true }
    },
    "brinchen": { ... },
    "hmadi": { ... }
  }
}

创建数据引用

要读写数据库里的数据,首先要创建一个指向数据的引用,每个引用对应一条 URL。要获取其子元素,可以用child API, 也可以直接把子路径加到 URL 上:

// referene 
new Firebase(https://docs-examples.firebaseio.com/web/data)

// 子路径加到 URL 上
new Firebase("https://docs-examples.firebaseio.com/web/data/users/mchen/name")

// child API
rootRef.child('users/mchen/name')

Firebase 数据库中的数组

Firebase 数据库不能原生支持数组。如果你存了一个数组,实际上是把它存储为一个用数组作为键的对象:

// we send this
['hello', 'world']
// firebase database store this
{0: 'hello', 1: 'world'}

存储数据

set()

set() 方法把新数据放到指定的引用的路径下,代替那个路径下原有的数据。它可以接收各种数据类型,如果参数是 null 的话就意味着删掉这个路径下的数据。

举个例子:

// 新建一个博客的引用
var ref = new Firebase('https://docs-examples.firebaseio.com/web/saving-data/fireblog')

var usersRef = ref.child('users')

usersRef.set({
  alanisawesome: {
  date_of_birth: "June 23, 1912",
  full_name: "Alan Turing"
  },
  gracehop: {
    date_of_birth: "December 9, 1906",
    full_name: "Grace Hopper"
  }
})

当然,也可以直接在子路径下存储数据:

usersRef.child("alanisawesome").set({
  date_of_birth: "June 23, 1912",
  full_name: "Alan Turing"
})

usersRef.child("gracehop").set({
  date_of_birth: "December 9, 1906",
  full_name: "Grace Hopper"
})

不同之处在于,由于分成了两次操作,这种方式会触发两个事件。另外,如果usersRef下本来有数据的话,那么第一种方式就会覆盖掉之前的数据。

update()

上面的set()对数据具有”破坏性”,如果我们并不想改动原来的数据的话,可能update()是更合适的选择:

var hopperRef = userRef.child('gracehop')
hopperRef.update({
  'nickname': 'Amazing Grace'
})

这段代码会在 Grace 的资料下面加上 nickname 这一项,如果我们用的是set()的话,那么full_namedate_of_birth就会被删掉。

另外,我们还可以在多个路径下同时做 update 操作:

usersRef.update({
  "alanisawesome/nickname": "Alan The Machine",
  "gracehop/nickname": "Amazing Grace"
})
push()

前面已经提到了,由于数组索引不具有独特性,Firebase 不提供对数组的支持,我们因此不得不转而用对象来处理。

在 Firebase 里面,push方法会为每一个子元素根据时间戳生成一个唯一的 ID,这样就能保证每个子元素的独特性:

var postsRef = ref.child('posts')

// push 进去的这个元素有了自己的路径
var newPostRef = postsRef.push()

// 获取 ID
var uniqueID = newPostRef.key()

// 为这个元素赋值
newPostRef.set({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming language'
})

// 也可以把这两个动作合并
postsRef.push().set({
  author: 'alanisawesome',
  title: 'The Turing Machine'
})

最后生成的数据就是这样的:

{
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "-JRHTHaKuITFIhnj02kE": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

这篇博客聊到了这个 ID 是怎么回事以及怎么生成的。

获取数据

获取 Firebase 数据库里的数据是通过对数据引用添加一个异步的监听器来完成的。在数据初始化和每次数据变化的时候监听器就会触发。value事件用来读取在此时数据库内容的快照,在初始时触发一次,然后每次变化的时候也会触发:

// Get a database reference to our posts
var ref = new Firebase("https://docs-examples.firebaseio.com/web/saving-data/fireblog/posts")

// Attach an asynchronous callback to read the data at our posts reference
ref.on("value", function(snapshot) {
  console.log(snapshot.val());
}, function (errorObject) {
  console.log("The read failed: " + errorObject.code);
});

简单起见,我们只用了 value 事件,其他的事件就不介绍了。

1.2 Firebase 的数据处理方式对代码的影响

开始写代码之前,我想搞清楚两个问题:

  • Firebase 是怎么管理数据的,它对组件的 View 有什么影响
  • 用户交互过程中是怎么和 Firebase 同步数据的

先看第一个问题,这是我在 Firebase 上保存的 JSON 数据:

{
  "notes" : {
    "-KGXQN4JVdopZO9SWDBw" : {
      "favorite" : true,
      "text" : "change"
    },
    "-KGXQN6oWiXcBe0a54cT" : {
      "favorite" : false,
      "text" : "a"
    },
    "-KGZgZBoJJQ-hl1i78aa" : {
      "favorite" : true,
      "text" : "little"
    },
    "-KGZhcfS2RD4W1eKuhAY" : {
      "favorite" : true,
      "text" : "bit"
    }
  }
}

这个乱码一样的东西是 Firebase 为了保证数据的独特性而加上的。我们发现一个问题,在此之前 notes 实际上是一个包含对象的数组:

[
  {
    favorite: true,
    text: 'change'
  },
  {
    favorite: false,
    text: 'a'
  },
    {
    favorite: true,
    text: 'little'
  },
    {
    favorite: true,
    text: 'bit'
  },
]

显然,对数据的处理方式的变化使得渲染 notes 列表的组件,也就是 NotesList.vue 需要大幅修改。修改的逻辑简单来说就是在思路上要完成从数组到对象的转换。

举个例子,之前 filteredNotes 是这么写的:

filteredNotes () {
  if (this.show === 'all'){
    return this.notes
  } else if (this.show === 'favorites') {
    return this.notes.filter(note => note.favorite)
  }
}

现在的问题就是,notes 不再是一个数组,而是一个对象,而对象是没有 filter 方法的:

filteredNotes () {
  var favoriteNotes = {}
  if (this.show === 'all') {
    return this.notes
  } else if (this.show === 'favorites') {
    for (var note in this.notes) {
      if (this.notes
['favorite']) { favoriteNotes
= this.notes
} } return favoriteNotes } }

另外由于每个对象都对应一个自己的 ID,所以我也在 state 里面加了一个activeKey用来表示当前笔记的 ID,实际上现在我们在TOGGLE_FAVORITE,SET_ACTIVE这些方法里面都需要对相应的activeKey赋值。

再看第二个问题,要怎么和 Firebase 交互:

// store.js
let notesRef = new Firebase('https://crackling-inferno-296.firebaseio.com/notes')

const state = {
  notes: {},
  activeNote: {},
  activeKey: ''
}

// 初始化数据,并且此后数据的变化都会反映到 View
notesRef.on('value', snapshot => {
  state.notes = snapshot.val()
})

// 每一个操作都需要同步到 Firebase
const mutations = {

  ADD_NOTE (state) {
    const newNote = {
      text: 'New note',
      favorite: false
    }
    var addRef = notesRef.push()
    state.activeKey = addRef.key()
    addRef.set(newNote)
    state.activeNote = newNote
  },
  
  EDIT_NOTE (state, text) {
    notesRef.child(state.activeKey).update({
      'text': text
    })
  },

  DELETE_NOTE (state) {
    notesRef.child(state.activeKey).set(null)
  },

  TOGGLE_FAVORITE (state) {
    state.activeNote.favorite = !state.activeNote.favorite
    notesRef.child(state.activeKey).update({
      'favorite': state.activeNote.favorite
    })
  },

  SET_ACTIVE_NOTE (state, key, note) {
    state.activeNote = note
    state.activeKey = key
  }
}

二、笔记检索功能

效果图:

这个功能比较常见,思路就是列表渲染 + 过滤器:

// NoteList.vue

<!-- filter -->
<div class="input">
  <input v-model="query" placeholder="Filter your notes...">
</div>

<!-- render notes in a list -->
<div class="container">
  <div class="list-group">
    <a v-for="note in filteredNotes | byTitle query"
      class="list-group-item" href="#"
      :class="{active: activeKey === $key}"
      @click="updateActiveNote($key, note)">
      <h4 class="list-group-item-heading">
        {{note.text.substring(0, 30)}}
      </h4>
    </a>
  </div>
</div>
// NoteList.vue

filters: {
  byTitle (notesToFilter, filterValue) {
    var filteredNotes = {}
    for (let note in notesToFilter) {
      if (notesToFilter
['text'].indexOf(filterValue) > -1) { filteredNotes
= notesToFilter
} } return filteredNotes } }

三、在项目中用 eslint

如果你是个 Vue 重度用户,你应该已经用上 eslint-standard 了吧。

"eslint": "^2.0.0",
"eslint-config-standard": "^5.1.0",
"eslint-friendly-formatter": "^1.2.2",
"eslint-loader": "^1.3.0",
"eslint-plugin-html": "^1.3.0",
"eslint-plugin-promise": "^1.0.8",
"eslint-plugin-standard": "^1.3.2"

把以上各条添加到 devDependencies 里面。如果用了 vue-cli 的话, 那就不需要手动配置 eslint 了。

// webpack.config.js
module: {
  preLoaders: [
    {
      test: /\.vue$/,
      loader: 'eslint'
    },
    {
      test: /\.js$/,
      loader: 'eslint'
    }
  ],
  loaders: [ ... ],
  eslint: {
    formatter: require('eslint-friendly-formatter')
  }
}

如果需要自定义规则的话,就在根目录下新建.eslintrc,这是我的配置:

module.exports = {
  root: true,
  // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
  extends: 'standard',
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  // add your custom rules here
  'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    'no-undef': 0,
    'one-var': 0,
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}

本文来自开发者头条