Vuex + Firebase 构建 Notes App
HectorManns
8年前
<p> </p> <p>前几天翻译了基于 <a href="/misc/goto?guid=4959671981288342918" rel="nofollow,noindex">这篇博客</a> 的文章: <a href="http://www.open-open.com/lib/view/open1462026812188.html">用 Vuex 构建一个笔记应用</a> 。在此基础上我对它做了一些更新:</p> <ul> <li> <p>把数据同步到 Firebase 上,不会每次关掉浏览器就丢失数据。</p> </li> <li> <p>加了笔记检索功能</p> </li> <li> <p>为保证代码整洁,加上了 eslint</p> </li> </ul> <p>你可以从 <a href="/misc/goto?guid=4959671981389868218" rel="nofollow,noindex">Github Repo</a> 下载源码,和 Firebase 的同步效果看下面这个 gif:</p> <p><img src="https://simg.open-open.com/show/ec761efc1c193122f721c895849f6c11.gif"></p> <h2>一、把数据同步到 Firebase</h2> <p>可能你也知道 Vue.js 和 Firebase 合作搞出了一个 <a href="/misc/goto?guid=4959671981650450963" rel="nofollow,noindex">Vuefire</a> , 但是在这里并不能用它,因为用 Vuex 管理数据的结果就是组件内部只承担基本的View层的职责,而数据基本上都在 store 里面。所以我们只能把数据的存取放在 store 里面。</p> <h3>1.1 Firebase 概述</h3> <p>如果熟悉 Firebase 的使用,可以放心地跳过这一段。</p> <p>如果你还没有 <a href="/misc/goto?guid=4958851967735651154" rel="nofollow,noindex">Firebase</a> 的账号,可以去注册一个,注册号之后会自动生成一个"MY FIRST APP",这个初始应用给的地址就是用来存数据的地方。</p> <p>Firebase 存的数据都是 JSON 对象。我们向 JSON 树里面加数据的时候,这条数据就变成了 JSON 树里的一个键。比方说,在 /user/mchen 下面加上 widgets 属性之后,数据就变成了这个样子:</p> <pre> <code class="language-javascript">{ "users": { "mchen": { "friends": { "brinchen": true }, "name": "Mary Chen", "widgets": { "one": true, "three": true } }, "brinchen": { ... }, "hmadi": { ... } } }</code></pre> <p>创建数据引用</p> <p>要读写数据库里的数据,首先要创建一个指向数据的引用,每个引用对应一条 URL。要获取其子元素,可以用 child API, 也可以直接把子路径加到 URL 上:</p> <pre> <code class="language-javascript">// 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')</code></pre> <p>Firebase 数据库中的数组</p> <p>Firebase 数据库不能原生支持数组。如果你存了一个数组,实际上是把它存储为一个用数组作为键的对象:</p> <pre> <code class="language-javascript">// we send this ['hello', 'world'] // firebase database store this {0: 'hello', 1: 'world'}</code></pre> <p>存储数据</p> <p>set()</p> <p>set() 方法把新数据放到指定的引用的路径下,代替那个路径下原有的数据。它可以接收各种数据类型,如果参数是 null 的话就意味着删掉这个路径下的数据。</p> <p>举个例子:</p> <pre> <code class="language-javascript">// 新建一个博客的引用 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" } })</code></pre> <p>当然,也可以直接在子路径下存储数据:</p> <pre> <code class="language-javascript">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" })</code></pre> <p>不同之处在于,由于分成了两次操作,这种方式会触发两个事件。另外,如果 usersRef 下本来有数据的话,那么第一种方式就会覆盖掉之前的数据。</p> <p>update()</p> <p>上面的 set() 对数据具有"破坏性",如果我们并不想改动原来的数据的话,可能 update() 是更合适的选择:</p> <pre> <code class="language-javascript">var hopperRef = userRef.child('gracehop') hopperRef.update({ 'nickname': 'Amazing Grace' })</code></pre> <p>这段代码会在 Grace 的资料下面加上 nickname 这一项,如果我们用的是 set() 的话,那么 full_name 和 date_of_birth 就会被删掉。</p> <p>另外,我们还可以在多个路径下同时做 update 操作:</p> <pre> <code class="language-javascript">usersRef.update({ "alanisawesome/nickname": "Alan The Machine", "gracehop/nickname": "Amazing Grace" })</code></pre> <p>push()</p> <p>前面已经提到了,由于数组索引不具有独特性,Firebase 不提供对数组的支持,我们因此不得不转而用对象来处理。</p> <p>在 Firebase 里面, push 方法会为每一个子元素根据时间戳生成一个唯一的 ID,这样就能保证每个子元素的独特性:</p> <pre> <code class="language-javascript">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' })</code></pre> <p>最后生成的数据就是这样的:</p> <pre> <code class="language-javascript">{ "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "-JRHTHaKuITFIhnj02kE": { "author": "alanisawesome", "title": "The Turing Machine" } } }</code></pre> <p><a href="/misc/goto?guid=4959671981766224556" rel="nofollow,noindex">这篇博客</a> 聊到了这个 ID 是怎么回事以及怎么生成的。</p> <p>获取数据</p> <p>获取 Firebase 数据库里的数据是通过对数据引用添加一个异步的监听器来完成的。在数据初始化和每次数据变化的时候监听器就会触发。 value 事件用来读取在此时数据库内容的快照,在初始时触发一次,然后每次变化的时候也会触发:</p> <pre> <code class="language-javascript">// 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); });</code></pre> <p>简单起见,我们只用了 value 事件,其他的事件就不介绍了。</p> <h3>1.2 Firebase 的数据处理方式对代码的影响</h3> <p>开始写代码之前,我想搞清楚两个问题:</p> <ul> <li> <p>Firebase 是怎么管理数据的,它对组件的 View 有什么影响</p> </li> <li> <p>用户交互过程中是怎么和 Firebase 同步数据的</p> </li> </ul> <p>先看第一个问题,这是我在 Firebase 上保存的 JSON 数据:</p> <pre> <code class="language-javascript">{ "notes" : { "-KGXQN4JVdopZO9SWDBw" : { "favorite" : true, "text" : "change" }, "-KGXQN6oWiXcBe0a54cT" : { "favorite" : false, "text" : "a" }, "-KGZgZBoJJQ-hl1i78aa" : { "favorite" : true, "text" : "little" }, "-KGZhcfS2RD4W1eKuhAY" : { "favorite" : true, "text" : "bit" } } }</code></pre> <p>这个乱码一样的东西是 Firebase 为了保证数据的独特性而加上的。我们发现一个问题,在此之前 notes 实际上是一个包含对象的数组:</p> <pre> <code class="language-javascript">[ { favorite: true, text: 'change' }, { favorite: false, text: 'a' }, { favorite: true, text: 'little' }, { favorite: true, text: 'bit' }, ]</code></pre> <p>显然,对数据的处理方式的变化使得渲染 notes 列表的组件,也就是 NotesList.vue 需要大幅修改。修改的逻辑简单来说就是在思路上要完成从数组到对象的转换。</p> <p>举个例子,之前 filteredNotes 是这么写的:</p> <pre> <code class="language-javascript">filteredNotes () { if (this.show === 'all'){ return this.notes } else if (this.show === 'favorites') { return this.notes.filter(note => note.favorite) } }</code></pre> <p>现在的问题就是,notes 不再是一个数组,而是一个对象,而对象是没有 filter 方法的:</p> <pre> <code class="language-javascript">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 } }</code></pre> <p>另外由于每个对象都对应一个自己的 ID,所以我也在 state 里面加了一个 activeKey 用来表示当前笔记的 ID,实际上现在我们在 TOGGLE_FAVORITE , SET_ACTIVE 这些方法里面都需要对相应的 activeKey 赋值。</p> <p>再看第二个问题,要怎么和 Firebase 交互:</p> <pre> <code class="language-javascript">// 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 } }</code></pre> <h2>二、笔记检索功能</h2> <p>效果图:</p> <p><img src="https://simg.open-open.com/show/7781b1a10856dc02b7d94138f2df95dd.gif"></p> <p>这个功能比较常见,思路就是列表渲染 + 过滤器:</p> <pre> <code class="language-javascript">// 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></code></pre> <pre> <code class="language-javascript">// 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 } }</code></pre> <h2>三、在项目中用 eslint</h2> <p>如果你是个 Vue 重度用户,你应该已经用上 eslint-standard 了吧。</p> <pre> <code class="language-javascript">"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"</code></pre> <p>把以上各条添加到 devDependencies 里面。如果用了 vue-cli 的话, 那就不需要手动配置 eslint 了。</p> <pre> <code class="language-javascript">// webpack.config.js module: { preLoaders: [ { test: /\.vue$/, loader: 'eslint' }, { test: /\.js$/, loader: 'eslint' } ], loaders: [ ... ], eslint: { formatter: require('eslint-friendly-formatter') } }</code></pre> <p>如果需要自定义规则的话,就在根目录下新建 .eslintrc ,这是我的配置:</p> <pre> <code class="language-javascript">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 } }</code></pre> <h2>四、结语</h2> <p>讲得比较粗糙,具体可以拿 <a href="/misc/goto?guid=4959671981389868218" rel="nofollow,noindex">源码</a> 跑一下。如果有什么问题,欢迎评论。</p> <p>来自: <a href="/misc/goto?guid=4959671981858481166" rel="nofollow">https://segmentfault.com/a/1190000005038509</a></p>