Vuex + Firebase 構建 Notes App
前幾天翻譯了基于 這篇博客 的文章: 用 Vuex 構建一個筆記應用 。在此基礎上我對它做了一些更新:
-
把數據同步到 Firebase 上,不會每次關掉瀏覽器就丟失數據。
-
加了筆記檢索功能
-
為保證代碼整潔,加上了 eslint
你可以從 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_name 和 date_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[note]['favorite']) {
favoriteNotes[note] = this.notes[note]
}
}
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[note]['text'].indexOf(filterValue) > -1) {
filteredNotes[note] = notesToFilter[note]
}
}
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
}
}
四、結語
講得比較粗糙,具體可以拿 源碼 跑一下。如果有什么問題,歡迎評論。