技術博文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
  }
}

本文來自開發者頭條