前幾天翻譯了基於這篇博客的文章:用 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 } }
講得比較粗糙,具體能夠拿源碼跑一下。若是有什麼問題,歡迎評論。