1. 程式人生 > >急速JavaScript全棧教程

急速JavaScript全棧教程

自從一年前釋出了Vuejs小書的電子書,也有些日子沒有碰過它們了,現在因為專案的緣故,需要使用JavaScript全棧開發。所以,我得把這個全棧環境搭建起來。

整個系列,是會採用我的一貫風格,就是不疾不徐,娓娓道來,學習完畢,你可以掌握我提到的全系列的知識,並且得到一個可以直接拷貝的程式碼模板,並把它用到你的專案中。

完成操練下來,得半小時到一個小時吧。騰出你的時間再來學習。

前端的複雜度

很多人是看不起JavaScript開發的。這玩意不就是玩具嘛,一些指令碼和標籤而已。說這話的時候,他們可能是就翹起二郎腿的,或者抱著膀子的。

然而,前端因為還在快速發展,因此很多東西在變,構造環境的選擇比較多,技術種類也不少,很多事情得自己做。因此它其實並不比那麼簡單的。這篇文章的圖,可以窺視到前端複雜的一角了。

Modern Frontend Developer in 2018

我看了不少資料,很多都是講解這張圖中的一個技術,講解全棧的肯定是有的,但是往往過於複雜。本文試圖通過一組文章,把JavaScript的全棧開發做一個全景的展示,力圖使用一個案例,全須全尾的貫穿整個系列,便於初學者對技術的急速理解。

大綱

所以,文章會包括這些:

  1. 使用Vuejs腳手架,快速搭建一個CRD使用者介面。會使用vuex管理狀態,使用vue-router管理路由。
  2. 使用Mongodb儲存和提供後端CRD服務。
  3. 使用Nodejs搭建後端CRD服務。
  4. 使用Fecth|Axios訪問後端CRD服務
  5. 使用bulfy的美化元件的方法
  6. 整合全棧服務

其中的CRD指的是Create、Read、Delete。針對的資料物件,就是一個Todo物件,看起來是這樣的:

{id:1,subject:"Loving"}

如果是多個數據物件,看起來是這樣的:


[
  {id:1,subject:"Loving"},
  {id:1,subject:"Writing"},
  {id:1,subject:"Preying"}
]

這個看起來很簡單平實的JS物件,會在一組組函式、模組和物件之間流動,甚至跨越網路邊界,從記憶體到硬碟。它會被儲存在Mongodb內,也會從Mongodb提取出來,在使用者介面、HTTP客戶端,HTTP伺服器傳遞。

整個App看起來就是一臺機器,可以說程式碼在運轉這個機器,但是也不妨說是資料在驅動這個它。

使用Vuejs腳手架,快速搭建Todo App介面

我們給自己命題,做一個TODO應用,它看起來是這樣的:

使用者可以看到一個編輯框,和一個清單。

  1. 在編輯框內加入新的todo專案,按回車即可加入到列表
  2. 列表內每個專案有一個按鈕,點選按鈕,可以刪除當前專案

環境要求

說起來搭建JS全棧開發環境,涉及到的東西真的不少。大的選擇是這樣的:

  1. 前端採用Vuejs
  2. 後端採用Nodejs
  3. 儲存使用Mongodb。

大的選擇定了,小的配套也就跟著來,前端配套的話需要一系列的技術,特別是前端,對應著Vuejs,配套管理路由、狀態、元件的都有相應的技術手段。自己搭配的話,還是非常麻煩的。

幸好Vuejs還有一個前端腳手架工具vue-cli,它可以把以上的元件整合起來到一個工程內。一個最為基礎的vue-cli工程腳手架的建立,現在得需要160M左右的空間佔用。在我的電腦和網路情況下,需要2分半的時間才會完成。

前端 Vuejs

  1. vue-router,前端路由管理
  2. vuex,前端資料管理,專業一點的說法,就是狀態管理,這些資料,可能是屬性,陣列,物件等等,可以跨元件訪問,而不像是data函式那樣提供的資料只能被本元件訪問,可以想到,稍微大一點的前端工程都必須前端狀態管理的。
  3. axios,前端HTTP訪問,以promise的形式,封裝了類似fetch,AJAX的能力
  4. buefy,前端微型框架,可以使用自定義標籤使用自定義元件,並且CSS框架為Bulma
  5. Bulma,儘管使用了微框架,只是讓對CSS framework的瞭解降到最低,但是不是說就不需要了解了。還是得學習的。Bulma相對於老牌的Bootstrap,是不需要依賴於JS框架,也沒有任何JS程式碼,因此可以和任何一框架很好的結合,比如這裡的Vuejs。這就是我選擇它的原因

後端 cli-service

為了給前端開發提供工具鏈和開發便利性,我們常常需要webpack&babel。有了它們,就可以使用ES6的語法,以及程式碼更新後自動重新整理等。這些都是非常便利的特性,用了就離不開的。有了vue-cli,對webpack&babel的瞭解可以降到最低,但是也不能不學,稍微需要一些定製的配置,也是必須要掌握的,起碼得知道如何啟動一個開發伺服器,已經發布build,還有把前端服務經過proxyChain跳轉到後端服務去等等。所幸是在這個教程內,你不需要學習太多就可以把案例跑起來。

App Server + Mongodb

接下來看後端,一般習慣就是使用Nodejs+Express.js的搭配。這個沒有多少說的,都是老東西了。為了訪問Mongodb,也需要一套框架,基於Callback的,或者基於Promise+Await+Async的,也是需要選擇的。

為了便於理解,我會用一個最小的案例完成整個開發過程,就是案例在現實中並不存在,但是也是有用的,就是你可以當它們是模板,直接拷貝程式碼,然後填充你的內容。天下程式碼一大抄嘛,沒有什麼不對的,畢竟這些寫程式碼是最快的。這個案例的資料模型就是對一個{id,name}的物件進行CRD(建立刪除列表)。

安裝執行環境

安裝環境相對簡單,特別是如果使用Mac OS X的話。有一些工具鏈可以幫助快速搭建環境。當然Windows也並不多麻煩就是了,它常常提供的是一個安裝程式,大部分時間,你需要的就是點選下一步。

這裡以MAC為例,講解安裝。

mongodb

安裝和執行Mongodb Daemon:


brew install mongodb
mongodb

訪問驗證,首先執行Mongodb Shell:


mongo

輸入命令,查詢資料庫清單:


> show dbs
local           0.000GB

能夠看到這些資訊,說明mongodb安裝成功。

Node.js

安裝並驗證:


$brew install nodejs
$node -v
10.7.0

能夠看到這些資訊,說明Node.js安裝成功。

開始前端編碼

安裝編碼環境

首先安裝vue-cli,方法和一般的NPM模組一樣的,我們安裝的版本是3.0:


npm i @vue/cli 

檢視版本:


vue -V
3.0.0

看到如下資訊,說明成功。然後建立App的腳手架程式碼:


vue create todoapp

注意,在此建立過程中,命令列會指示你做出選擇,我們會選擇Manual select feature,然後選擇router和vuex,其他不選。然後並執行此程式碼:


cd todoapp
npm run serve

可以在瀏覽器中訪問localhost:8080看到Vue的啟動畫面。說明建立腳手架成功。

此時,vue-cli已經幫助安裝了vuex和router的模組依賴。本節一次性的安裝全部剩餘的全部NPM依賴,免得以後用一個安裝一個,麻煩而且囉嗦。


  npm install buefy --save
npm install axios --save 

buefy是一個基於Bulma的Vuejs使用者介面元件庫。這次的UI的CSS方案,使用Bulma,Vuejs如果想要以定製元件的形式去使用它的話,那麼需要安裝Buefy模組。實際上,我研究過Bulma和Bootstrap,還寫了一本免費的電子書,我覺得Bulma相對於Bootstrap的優勢在於1.不依賴任何JS框架2.使用者介面設計更加簡明。這就是我現在選擇使用Bulma的原因。

Axios是一個封裝了HTTPClient的庫,提供了promise介面。我們使用它訪問後端的HTTP Server的資料。之前提到的資料物件,就是由Axios提取到客戶端,也會是通過Axios把資料物件提交到伺服器的。

前端編碼

首先,我們從狀態開始。我們之前提到的Vuex,是Vuejs管理狀態的官方外掛。所謂的狀態,就是應用程式的資料物件們。也就是我們提到的Todo物件和Todo物件集合。我們在App使用者介面上看到的很多資料都是來自於狀態物件。狀態物件在src/store.js。不僅僅是的應用狀態資訊,還有和對這些的操作函式。既然需要一個todo專案清單,因此應該加入如下程式碼:


import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const defaultTodo = [
      {id:1,subject:'Eating'},
      {id:2,subject:'Loving'},
      {id:3,subject:'Preying'},
    ]
function indexById(todos,id){
  for (var i = 0; i < todos.length; i++) {
    if (id == todos[i].id)
      return i
  }
  return -1
}
import axios from 'axios'
export default new Vuex.Store({
  state: {
    msg:'Todo App',
    todos:defaultTodo
  },
  mutations: {
    add(state,subject){
      var todo = {id:subject,subject:subject}
      state.todos.push(todo)
    },
    remove(state,id){
      state.todos.splice(indexById(state.todos,id),1)
    },
    reload(state){
      state.todos = defaultTodo
    }
  },
  actions: {
  add: (context, link) => {
      context.commit("add", link)
    },
    remove: (context, link) => {
      context.commit("remove", link)
    },
    reload: (context) => {
      context.commit("reload")
    }
  }
})


其中的state.todos屬性,就是我們的主要的資料物件了。state.msg這是提供了一個App的標題字串。mutations屬性內是對資料修改提供的方法,比如

  1. 我們需要新增一個todo,使用add()方法,相應的
  2. 刪除一個todo,使用remove()方法
  3. 重新整理一個todo列表,就會使用load()方法

有時候,對資料的修改可能是比較消耗時間的,因此為了避免阻塞客戶端的主執行緒,這個物件也提供了非同步的方法,actions物件內就是對應修改操作的非同步方法,這裡的方法功能上和mutations一致,但是是非同步的。Vuex提供了類似:


context.commit()

的語法,提供和actions和mutations方法的對接。第一個引數是mutations的方法名稱,之後的引數最為mutations方法的引數傳遞給mutations方法。

特別說下,mutations內的add()方法,其中使用者介面會提供一個Todo.subject屬性,而ID是需要自動生成的,我們這裡臨時使用subject的值作為id,就是一個偷懶,只要subject不要輸入重複,也暫時可以矇混過關。因為知道本專案內的後臺儲存會採用Mongodb,在Mongodb內插入一個新的物件後,會自動生成一個ID,我們的Todo物件的id會採用這個ID。這裡就沒有必要自己生成了。

在src/views/home.vue內,貼上為如下程式碼:


<template>
  <div class="home">
    <h1>{{msg}}</h1>
    <NewTodo></NewTodo>
    <TodoList></TodoList>
  </div>
</template>
<script>
import NewTodo from '@/components/NewTodo.vue'
import TodoList from '@/components/TodoList.vue'
import {mapState,mapActions} from 'vuex'
export default {
  name: 'home',
  computed:mapState(['todos','msg']),
  components: {
    TodoList,NewTodo
  },
  data(){
      return{newtodo:''}
  },
  methods:{
      ...mapActions([
      'remove',
      'add'
    ]),
      add1:function(){
          this.add(this.newtodo)
          this.newtodo = ''
      }
  }
}
</script>

...mapState,mapActions的解說。

就是說,我們這個Todo App劃分為為兩個元件,其中一個元件負責顯示編輯框,並接受回車事件,把新的Todo專案加入到應用狀態內。另外一個元件負責顯示全部Todo專案,並接受刪除事件,刪除指定的Todo專案。它們分別是NewTodo元件和TodoList元件:


<NewTodo></NewTodo>
<TodoList></TodoList>

這兩個元件的程式碼實現,分別在檔案src/components/NewTodo.vuesrc/components/TodoList.vue內。NewTodo程式碼:


<template>
  <div class="home">
    <form @submit.prevent="add1">
        <input type="text" name="newTodo" placeholder="new todo" v-model="newtodo">
    </form>
  </div>
</template>
<script>
import {mapState,mapActions} from 'vuex'
export default {
  name: 'newtodo',
  computed:mapState(['todos','msg']),
  data(){
      return{newtodo:''}
  },
  methods:{
      ...mapActions([
      'add'
    ]),
      add1:function(){
          this.add(this.newtodo)
          this.newtodo = ''
      }
  }
}
</script>

TodoList程式碼:


<template>
  <div class="hello">
    <ul>
      <li v-for="(todo,index) in todos" v-bind:key="todo.id">
        {{todo.subject}}<button @click="remove(todo.id)" class="rm">remove</button>
      </li>
    </ul>
  </div>
</template>
<script>
import {mapState,mapActions} from 'vuex'
export default {
  name: 'todolist',
  computed:mapState(['todos','msg']),
  components: {
  },
  methods:{
    ...mapActions([
      'remove','reload'
    ])
  },
  mounted(){
    this.reload()
  }
}
</script>
<style scoped>
</style>


在src/main.js檔案內,新增如下程式碼,引入Buefy:


import Buefy from 'buefy'
import 'buefy/lib/buefy.css'
Vue.use(Buefy)

現在可以使用Buefy元件了。我們可以把NewTodo元件內的標準的input變成元件化的input,把標籤換成b-input即可。程式碼如下:


<b-input type="text" name="newTodo" placeholder="new todo" v-model="newtodo"></b-input>

現在看瀏覽器,input變成了比較有吸引力的Bulma風格的控制元件了。
訪問網路使用axios。需要首先找到src/home.vue在程式碼的開頭部分引用此庫:


import axios from 'axios'

在Vue單頁元件內使用此庫了。比如在src/home.vue內程式碼物件中加入新方法:


mounted(){
  var url  = 'https://api.coindesk.com/v1/bpi/currentprice.json'
  axios ({
        url:url,
        method: 'get',
    })
    .then( res => {console.log(res.data.chartName)} )
    .catch( err => cosole.error(err))
}


我們來看看適應效果。啟動cli-service:


npm run serve

然後開啟瀏覽器,輸入地址localhost:8080,如果可以在瀏覽器內看到我們期望的使用者介面,並且都可以看到console列印了Bitcoin,那麼就說明使用者介面程式碼和初步的訪問HTTP網路的axios程式碼以及狀態管理功能都是成功了的。

後端編碼

現在,我們已經可以看到UI了,但是使用者介面內的資料來自於客戶端,而不是來自於伺服器。我們的資料當然應該來源於伺服器的了。因此我們需要啟動給一個自己的伺服器,這個伺服器可以接受客戶在介面上錄入的新的Todo物件,也可以提供後端資料庫內的Todo清單。

為了測試的目的,常常需要準備一個todo應用的後臺JSON服務,可以通過HTTP方式,提供todo專案的增加刪除修改和查詢。

這樣的伺服器,使用了nodejs作為伺服器端,並且使用了兩個node模組,可以使用npm安裝它們:

npm install express body-parser

body-parser是一箇中間件,可以解析請求內容並把解析結果放到req.body屬性內。最常見的做法就是解析json內容。

程式碼如下(檔名為:jsonserver.js):


  var express = require('express');
  var app = express();
  var path = require('path')
  var bodyParser = require('body-parser')
  app.use(bodyParser.json())
  var todos = []
  var public = path.join(__dirname, '/')
  app.use('/',express.static(public))
  const defaultTodo = [
    {id:1,subject:'Eating'},
    {id:2,subject:'Loving'},
    {id:3,subject:'Preying'},
  ]
  function rs(){
    todos = defaultTodo
  }
  function indexById(id){
    for (var i = 0; i < todos.length; i++) {
      if (id ==todos[i].id)return i
    }
    return -1
  }
  rs()
  app.delete('/api/todo/:id', function (req, res) {
    var userkey = +req.params.id
    todos.splice(indexById(userkey),1)
    res.end( JSON.stringify(todos));
    rs()
  })
  app.get('/api/todos', function (req, res) {
    res.end( JSON.stringify(todos));
  })
  app.post('/api/todo', function (req, res) {
    todos.push(req.body)
    res.end(JSON.stringify(todos))
    rs()
  })
  var server = app.listen(8081, function () {
    var host = server.address().address
    var port = server.address().port
    console.log("listening at http://%s:%s", host, port)
  })

可以使用命令執行:

node jsonserver.js

Curl命令驗證

可以通過curl命令驗證服務的有效性:

  1. GET操作


 $curl http://localhost:8081/todo/1
 $curl http://localhost:8081/todos

  • DELETE操作

  • 
      $ curl -X "DELETE" http://localhost:8081/api/todo/1
    
    
  • POST操作

  • 
    $curl -X POST  -H "Content-Type: application/json" -d '{"subject":"s4"}' http://localhost:8081/api/todo
    
    

    前端HTML驗證

    建立一個index.html檔案,並放置到和jsonserver.js程式碼同一目錄,程式碼如下:

    
    <a href='/todos'>todos</a>
    <a href='/todo/1'>todo/1</a>
    <button onclick='remove()'>remove 1</button>
    <button onclick='create()'>create</button>
    <script>
      function remove(){
        fetch (
          '/api/todo/1',
          {
            method: 'DELETE',
          }
        )
        .then( res => console.log(res.json()))
        .catch( err => cosole.error(err))
      }
      function create(){
        fetch (
          '/api/todo',
          {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({id: "4", subject: "s4"})
          }
        )
        .then( res => console.log(res.json()))
        .catch( err => cosole.error(err))
      }
    </script>
    
    

    可以提供建立,刪除,列表的測試,其中部分結果在console內顯示。

    說起來,JS訪問HTTP的庫真的是不少,這裡 提到的庫都有9種。其中的fetch api使用起來非常的簡潔趁手,可是它不支援IE。如果你需要支援IE的話,使用Axios更好。這就是為什麼Vuejs官方推薦Axios的原因吧:

    
    The Fetch API is a powerful native API for these types of requests. You may have heard that one of the benefits of the Fetch API is that you don’t need to load an external resource in order to use it, which is true! Except… that it’s not fully supported yet, so you will still need to use a polyfill. There are also some gotchas when working with this API, which is why many prefer to use axios for now. This may very well change in the future though.
    
    

    axios訪問方法

    相比fetch,使用axios必須依賴於外部檔案。為了方便,我們直接使用unpkg網站提供的庫檔案。

    axios的語法和fetch的大同小異,看著也是比較簡潔美觀的。以下程式碼,把create和remove函式的內部實現換掉,其他不變。

    
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <a href='/todos'>todos</a>
    <a href='/todo/1'>todo/1</a>
    <button onclick='remove()'>remove 1</button>
    <button onclick='create()'>create</button>
    <script>
      function remove(){
        axios ({
            url:'/api/todo/1',
            method: 'DELETE',
        })
        .then( res => console.log(res.json()))
        .catch( err => cosole.error(err))
      }
      function create(){
        axios ({
            method: 'POST',
            url:'/api/todo',
            headers: {
              'Content-Type': 'application/json'
            },
            data: JSON.stringify({subject: "s4"})
        })
        .then( res => console.log(res.json()))
        .catch( err => cosole.error(err))
      }
    </script>
    
    

    現在,後端也是可以跑起來了的。

    整合:前端後端

    建立cli-service到App Server的通道

    每次執行命令:npm run serve,都會啟動vue定製腳手架的伺服器程式碼,它會提供不少方便開發的特性。但是我希望一部分URL可以轉發到我自己的伺服器內。比如把所有的/api打頭的URL全部轉過來。只要在工程根目錄內加入此檔案vue.config.js,錄入如下內容:

    
    module.exports = {
      devServer: {
        proxy: {
          "/api": {
            target: "http://localhost:8181",
            secure: false
          }
        }
      }
    };
    
    

    我們自己的測試伺服器在這裡:

    
    var http = require('http');
    http.createServer(function (req, res) {
      res.write('Hello World!'); 
      res.end(); 
    }).listen(8181); 
    
    

    我們的定製伺服器,就可以監聽8181的本地機器埠,等待客戶端的匹配的URL轉發過來,並轉發我們伺服器的響應程式碼到客戶端。

    但是正常開發過程中,是需要自己的伺服器端程式碼的,如何在利用Vue腳手架伺服器的方便性的基礎上,加入自己的程式碼呢。做法是另外做一個定製的伺服器,然後讓vue定製腳手架的伺服器轉發URL到此伺服器。

    為了測試的目的,我們把函式mounted修改為:

    
    mounted(){
      var url  = '/api/1'
      axios ({
            url:url,
            method: 'get',
        })
        .then( res => {console.log(res.data)} )
        .catch( err => console.error(err))
    }
    
    

    即可看到瀏覽器console內列印Hello World!

    整合前端到後端

    我們已經通過配置,要求cli-service轉移全部api打頭的URL到App Server。只要在工程根目錄內加入此檔案vue.config.js,錄入如下內容:

    
    module.exports = {
      devServer: {
        proxy: {
          "/api/*": {
            target: "http://localhost:8181/api",
            secure: false
          }
        }
      }
    };
    
    

    現在,我們可以修改前端的Axios使用程式碼,分別替代前端程式碼的資料裝入、資料刪除、資料新增的程式碼,讓這些程式碼可以支援網路操作。為了避免網路操作程式碼和業務邏輯程式碼混合在一起,我決定包裝三個網路操作函式,並把它們放置到src/store.js檔案內:

    
    
    import axios from 'axios'
    function httpadd(subject,cb){
      axios ({
            method: 'POST',
            url:'/api/todo',
            headers:[{'Content-Type':'application/json'}],
            data: {subject:subject}
          })
          .then( res => cb(res.data))
          .catch( err => console.error(err))
    }
    function httpremove(id,cb){
      axios ({
            url:'/api/todo/'+id,
            method: 'delete',
          })
          .then( res => {
              cb()
          })
          .catch( err => console.error(err))
    }
    function httpreload(cb){
      axios ({
            url:'/api/todos',
            method: 'get',
          })
          .then( res => {
              cb(res.data)
          })
          .catch( err => console.error(err))
    }
    
    

    分別完成新增、刪除、查詢的任務,當完成工作後,都會呼叫一個callback函式,在此函式內,可以消費訪問網路後得到的響應資料。

    然後把檔案內src/store.js的mutations物件改成如下程式碼:

    
     mutations: {
      add(state,subject){
        httpadd(subject,function(todo){
          state.todos.push(todo)
        })
      },
      remove(state,id){
        httpremove(id,function(){
          state.todos.splice(indexById(state.todos,id),1)  
        })
      },
      reload(state){
        httpreload(function(todos){
          // console.log(todos)
          state.todos = todos
        })
        // state.todos = defaultTodo
      }
    },
    
    

    最後,在TodoList內加入一個新函式,並通過mapActions引入src/store.js的load()函式到當前物件內:

    
      methods:{
        ...mapActions([
          'remove','load'
        ])
      },
      mounted(){
        this.load()
      }
    
    

    以便在啟動後呼叫this.load()裝入它。

    整合:後端和資料庫

    要完成後端到資料庫的整合,需要做如下的修改:

    1. 原本在後端App Server內Todo資料陣列,現在應該從Mongodb獲得
    2. 原本在新增Todo物件的時候只是新增到AppServer物件內,現在需要同時寫入Mongodb
    3. 原本在刪除時只是從陣列刪除,現在需要同時在Mongodb內刪除

    因此,現在我們需要新增三個函式,分別做針對Mongodb的獲取清單、新增和刪除的工作:

    
    var mongo = require('mongodb')
    function insertDoc(subject,callback){
      const connectionString = 'mongodb://localhost:27017';
      (async () => {
          const client = await MongoClient.connect(connectionString,
              { useNewUrlParser: true });
          const dbo = client.db('todos');
          try {
             var res = await dbo.collection('todo').insertOne(
              {subject:subject})
             callback(undefined,res.insertedId)
          }
          catch(err){
            callback(err)
          }
          finally {
              client.close();
          }
      })().catch(err => console.error(err));
    }
    function deleteDoc(_id,callback){
      const MongoClient = mongo.MongoClient;
      const connectionString = 'mongodb://localhost:27017';
      (async () => {
          const client = await MongoClient.connect(connectionString,
              { useNewUrlParser: true });
          const dbo = client.db('todos');
          try {
                  var myquery = {_id:new mongo.ObjectID(_id)}
                  var r = await dbo.collection("todo").deleteMany(myquery)
            }
            catch(err){
            callback(err)
          }
          finally {
                client.close();
                callback()
            }
      })().catch(err => console.error(err));
    }
    function allDoc(callback){
      const MongoClient = mongo.MongoClient;
      const connectionString = 'mongodb://localhost:27017';
      (async () => {
          const client = await MongoClient.connect(connectionString,
              { useNewUrlParser: true });
          const dbo = client.db('todos');
          try {
                 var r = await dbo.collection("todo").find().toArray()
                 var ts = []
                 for (var i = 0; i < r.length; i++) {
                   ts.push({id:r[i]._id,subject:r[i].subject})
                 }
                 callback(undefined,ts)
            }
            catch(err){
            callback(err)
          }
          finally {
                client.close();
            }
      })().catch(err => console.error(err));
    }
    
    

    這三個函式的功能和使用方法如下:

    1. 函式allDoc會從Mongodb內獲取全部todo集合,並通過callback傳遞這個集合給呼叫者函式。
    2. 函式deleteDoc會從Mongodb內刪除指定id的todo條目,完成刪除後,通過callback通知呼叫者。
    3. 函式deleteDoc會向Mongodb內新增一個todo條目,完成新增後,通過callback通知呼叫者,並傳遞新的todo物件給呼叫者。

    這裡的程式碼本身並不複雜,但是因為涉及到如何訪問Mongodb,因此涉及到比較多的概念,這裡不做具體的解釋,你可以先把它們用起來。如果完成了本教程後,希望對Mongodb的訪問做具體瞭解的話,可以檢視後文附錄的“Mongodb快速參考”。

    並且和App Server對應的程式碼接駁,把原來的路由程式碼替換如下:

    
    app.delete('/api/todo/:id', function (req, res) {
      var userkey = req.params.id
      deleteDoc(userkey,function(){
        todos.splice(indexById(userkey),1)
        res.end( JSON.stringify(todos));
      })
    })
    app.get('/api/todos', function (req, res) {
      allDoc(function(err,todos){
        res.end( JSON.stringify(todos));  
      })
    })
    app.post('/api/todo', function (req, res) {
      insertDoc(req.body.subject,function(err,_id){
        var obj ={id:_id,subject:req.body.subject}
      todos.push(obj)
      res.end(JSON.stringify(obj))
        rs()
      })
    })
    
     
    

    Mongodb快速參考

    本文會把一個物件todo物件(有屬性{id,name})儲存到Mongodb,做查詢刪除的測試(Create Remove Delete = CRD)。這個測試包括使用Mongodb Shell,使用CallBack古典風格的訪問程式碼,以及使用Await/Async的現代風格的程式碼。完成這個這個驗證後,就可以掌握最初步的Mongodb了。

    我使用的Nodejs是10.7 。作業系統環境為Mac OS X High Sierra。

    準備環境

    安裝和執行Mongodb Daemon

    
    brew install mongodb
    mongodb
    
    
    

    訪問驗證

    首先執行Mongodb Shell:

    
    mongo
    
    

    輸入命令,查詢資料庫清單:

    
    > show dbs
    local           0.000GB
    
    

    建立一個數據庫

    
    use todos
    
    

    (若database不存在,則會建立一個,此時若不做任何操作直接退出,則MongoDB會刪除該資料庫)

    
    db.todos.insert({id:1,name:"reco"})
    db.todos.insert({id:2,name:"rita"})
    
    

    查詢 :

    
    db.todos.find()
    
    { "_id" : ObjectId("5b727c0846b6c71a98d3af52"), "id" : 1, "name" : "reco" }
    { "_id" : ObjectId("5b727c7046b6c71a98d3af53"), "id" : 2, "name" : "reta" }
    
    

    刪除記錄:

    
    db.todo.remove({id:1})
    
    

    刪除資料庫

    
    db.todo.drop()
    
    

    使用nodejs方式訪問Mongodb

    使用nodejs執行類似Shell對物件的CRD,程式碼如下:

    
    var MongoClient = require('mongodb').MongoClient;
    var url = "mongodb://localhost:27017/todos";
    MongoClient.connect(url, function(err, db) {
      if (err) throw err;
      console.log("Database created!");
      var dbo = db.db("todos");
      // var myobj = { id: 1, name: "reco" };
      // dbo.collection("todo").insertOne(myobj, function(err, res) {
      //   if (err) throw err;
      //   console.log("1 document inserted");
      //   db.close();
      // });
       var myobj = [
        { id: 1, name: 'reco'},
        { id: 2, name: 'rita'},
      ];
      dbo.collection("todo").insertMany(myobj, function(err, res) {
        if (err) throw err;
        console.log("Number of documents inserted: " + res.insertedCount);
        dbo.collection("todo").find({}).toArray(function(err, result) {
          if (err) throw err;
          console.log(result);
            var myquery = { id: 1 };
          dbo.collection("todo").deleteMany(myquery, function(err, obj) {
            if (err) throw err;
            console.log("document deleted");
            db.close();
          });
        });    
      });
    })
    
    

    程式碼非常簡單,無需更多解釋。此程式碼使用了mongodb模組,需要首先安裝:

    
    
    npm init -y
    npm i mongodb --save
    
    

    然後使用node index.js執行即可看到效果:

    
    Database created!
    Number of documents inserted: 2
    [ { _id: 5b72ab9e3245f169ef5f43d2, id: 1, name: 'reco' },
      { _id: 5b72ab9e3245f169ef5f43d3, id: 2, name: 'rita' } ]
    document deleted
    
    

    利用高階非同步特性

    使用Await/Async特性,可以有效的減少程式碼中的回撥地獄現象。同樣的功能,可以使用這樣的程式碼:

    
    const MongoClient = require('mongodb').MongoClient;
    const connectionString = 'mongodb://localhost:27017';
    (async () => {
        const client = await MongoClient.connect(connectionString,
            { useNewUrlParser: true });
        const dbo = client.db('todos');
        try {
           var res = await dbo.collection('todo').insertMany(
            [{id:1,name:"reco"}, {id:2,name:"rita"}]);
           console.log("Number of documents inserted: " + res.insertedCount);
           var r = await dbo.collection("todo").find().toArray()
           console.log(r);
           var myquery = { id: 1 };
         var r = await dbo.collection("todo").deleteMany(myquery)
         console.log("document deleted");
        }
        finally {
            client.close();
        }
    })().catch(err => console.error(err));
    
    
    

    執行此程式碼,輸出如下:

    
    Number of documents inserted: 2
    [ { _id: 5b72ae8a1c674a6ac1c5aa6e, id: 1, name: 'reco' },
      { _id: 5b72ae8a1c674a6ac1c5aa6f, id: 2, name: 'rita' } ]
    document deleted
    
    

    Vuejs快速參考

    Vuejs本身要學的還真不少,這也是我會編寫一本書來介紹它的原因。但是說到入門的話,我倒是寫過一篇簡單的介紹文章。不妨去閱讀看看。

    上面的文章,也就對Vuejs瞭解個大概,提起一個興趣。如果真的想要學習Vuejs的話,還是得看書的。這裡也放一個我的書的廣告,歡迎參閱。

    購買

    問題索引

    麻雀雖小五臟俱全,雖然是一個小小的示例的應用,但是每樣技術也都需要用到,遇到的技術問題也是要一個個的解決的。這裡列出我遇到的問題,作為索引,也算記錄我在寫作過程中解決的問題,也可以作為你學習完畢後的一個查漏補缺的索引,在這裡重新看到問題,然後使用程式碼驗證自己對問題的理解和學習。

    1. 使用Nodejs如何搭建一個RESTFUL的服務?
    • 如何把cli-service中來自客戶端但是需要去應用伺服器的URL訪問轉移到應用伺服器?
    • 如何解析前端提交的Json?
    • 如何打包Json到響應內?
    • 如何建立一個唯一的ID?本來是用於建立一個唯一ID,這樣就不必使用MongoDB生成的ObjectID了,因此插入效率會更高。但是,最後還是按慣例採用了Mongo的了。
    1. 如何加入新的單頁元件?
    • 在哪裡插入獲取服務端資料的程式碼?
    1. 如何使用Buefy?
    • 在Vuejs應用中內使用Buefy?
    • 在Vuejs單頁元件內使用Buefy?
    • 如何關閉input的自動完成特性?這個自動完成每次當你輸入的時候,都會顯示一個曾經的輸入的列表,真的很煩。
    1. 如何在元件之間共享狀態?
    • 為何需要共享前端狀態?
    • 如何區別使用同步和非同步狀態操作?
    • 如何對映狀態操作到元件內,以方便元件對方法的呼叫?
    1. 如何訪問Mongo?
    • 如何安裝Mongo,並快速驗證Mongo?
    • 如何命令列插入、刪除、列表一個Collection?
    • 有哪些訪問Mongo的Nodejs庫,差別在哪裡?
    • 如何使用Await風格的程式碼做插入刪除和列表?
    • 如何獲取插入專案後的專案ID?
    • 如何使用ObjectId查詢對應的專案?
    1. 如何訪問網路,包括curl、axios、fetch等幾種方法?
    • 如何提交Get請求?
    • 如何提交Put請求?
    • 如何在Body內提交json?
    • 如何指定請求頭內容型別為json?
    • 如何獲取響應中的Json資料?

    參考文章

    這個App雖然很小,但是並非一次寫成的,我之前已經完成了若干個更加細小的、特定方面的文章,也寫了一些程式碼。當然外部參考肯定也是不少,特別是Medium和Stack Overflow網站,對我支援很多。這裡列出關鍵的一些參考文章。

    1. 這裡會使用vuex管理狀態,之前有寫過文章,到底Vuex是什麼,閱讀量和評價還不少呢。當然,我舉例的案例,有些勉強,為了用而用,實際案例並不需要Vuex,使用元件內狀態即可。
    2. 使用Mongodb儲存和提供後端CRD服務
    3. 使用Nodejs搭建{id,name}的物件的後端CRD服務。計劃參考之前我自己的2篇文章和一個SO關於如何在AJAX內建立PUT請求的問答
    1. 使用Fecth|Axios訪問後端CRD服務。此文章列舉了幾種訪問網路庫的方法,給了我一個全域性的視野,因此我很受益於它。[Fetch vs. Axios.js for making http requests

    ](https://medium.com/@thejasonf...

    1. 使用buefy的美化元件的方法。這個很小眾了,因此就只能看看官方文件了。

    來源:https://segmentfault.com/a/1190000016101940