前几天翻译了基于这篇博客的文章:用 Vuex 构建一个笔记应用。在此基础上我对它作了一些更新:javascript
把数据同步到 Firebase 上,不会每次关掉浏览器就丢失数据。html
加了笔记检索功能vue
为保证代码整洁,加上了 eslintjava
你能够从 Github Repo 下载源码,和 Firebase 的同步效果看下面这个 gif:webpack
可能你也知道 Vue.js 和 Firebase 合做搞出了一个 Vuefire, 可是在这里并不能用它,由于用 Vuex 管理数据的结果就是组件内部只承担基本的View层的职责,而数据基本上都在 store 里面。因此咱们只能把数据的存取放在 store 里面。git
若是熟悉 Firebase 的使用,能够放心地跳过这一段。github
若是你尚未 Firebase 的帐号,能够去注册一个,注册号以后会自动生成一个"MY FIRST APP",这个初始应用给的地址就是用来存数据的地方。web
Firebase 存的数据都是 JSON 对象。咱们向 JSON 树里面加数据的时候,这条数据就变成了 JSON 树里的一个键。比方说,在/user/mchen
下面加上widgets
属性以后,数据就变成了这个样子:vuex
{ "users": { "mchen": { "friends": { "brinchen": true }, "name": "Mary Chen", "widgets": { "one": true, "three": true } }, "brinchen": { ... }, "hmadi": { ... } } }
要读写数据库里的数据,首先要建立一个指向数据的引用,每一个引用对应一条 URL。要获取其子元素,能够用child
API, 也能够直接把子路径加到 URL 上:vue-cli
// 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 数据库不能原生支持数组。若是你存了一个数组,其实是把它存储为一个用数组做为键的对象:
// we send this ['hello', 'world'] // firebase database store this {0: 'hello', 1: 'world'}
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
下原本有数据的话,那么第一种方式就会覆盖掉以前的数据。
上面的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" })
前面已经提到了,因为数组索引不具备独特性,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 事件,其余的事件就不介绍了。
开始写代码以前,我想搞清楚两个问题:
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 } }
若是你是个 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 } }
讲得比较粗糙,具体能够拿源码跑一下。若是有什么问题,欢迎评论。