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>