前几天翻译了基于这篇博客的文章:用 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
// 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
}
}
四、结语
讲得比较粗糙,具体可以拿源码跑一下。如果有什么问题,欢迎评论。