nodejs 8開始, nodejs可以不再需要一定跟V8綁在一起了, 之後底層的Engine可以換成微軟的查克拉(node-chakracore), 甚至是Mozilla的SpiderMonkey(spidernode)

node-chakracore的官方文件用的version managent是nvs, 而我習慣用的nvm則是尚未支援查克拉版本的安裝, 如果用nvm想嚐鮮的話, 要用下面的方式:

NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/chakracore-nightly/ nvm i node

這樣就會幫你裝最新版本的nodejs nightly (with Chakra Core), 以我剛剛執行的結果, 它就幫我裝了v9.0.0-nightly20170617021fbca6bc, 當然如果想用8而不是9一樣可以用:

NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/chakracore-nightly/ nvm list-remote

這樣就可以看有哪些版本可以安裝

裝完之後可以用nvm aliasv9.0.0-nightly20170617021fbca6bc取一個代號, 如nvm alias v9.0.0-nightly20170617021fbca6bc chakra9, 這樣之後就可以用nvm use chakra9來切換到這個版本, 這樣就搞定了! 歡迎來到木葉忍者村!!

另外, 補充一點, 怎知道用的版本是查克拉版的呢?

執行: node -e "console.log('Hello from Node.js ' + process.jsEngine)"

如果是查克拉版的會顯示: Hello from Node.js chakracore

如果是v8的則是: Hello from Node.js undefined

Vue的component裡有一個還蠻好用的東西叫做Slot(文件在此), 尤其適用在開發複雜或巢狀的元件

那Slot是用在什麼樣的地方, 舉個例子, 假設我們有個元件叫做panel:

<panel>
  <div>Inside panel</div>
  <div>Panel content</div>
</panel>

Panel的定義可能會是這樣:

Vue.component('panel', {
  template: '<div><slot></slot></div>'
})

執行範例

簡單的說, 這邊template裡的slot會被拿來放前面例子<panel></panel>裡面的<div>

當然, 這也可以讓我們這個component裡面再放其他的component, 像是這樣的例子:

<b-menu>
 <b-menu-item>menu 1</b-menu-item>
 <b-menu-item>menu 2</b-menu-item>
 <b-menu-item>menu 3</b-menu-item>
 <b-menu-item>menu 4</b-menu-item>
</b-menu>

實作上可以寫成這樣

Vue.component('b-menu', {
  template: '<div><ul><slot></slot></ul></div>'
})

Vue.component('b-menu-item', {
  template: '<li><slot></slot></li>'
})

執行範例

其實這範例看起來好像也沒啥必要寫成component(不過就ul/li), 當然, 實際上的應用可以再更複雜, 再來看一個更複雜的範例:

<b-menu>
	<b-menu-item title="menu 1">Content 1</b-menu-item>
	<b-menu-item title="menu 2">Content 2</b-menu-item>
	<b-menu-item title="menu 3">Content 3</b-menu-item>
	<b-menu-item title="menu 4">Content 4</b-menu-item>
</b-menu>

跟上面不一樣的是, 這次想render成的結果是像這樣:

<div>
  <ul>
    <li><a href="#a1">menu 1</a></li>
    <li><a href="#a2">menu 2</a></li>
    <li><a href="#a3">menu 3</a></li>
    <li><a href="#a4">menu 4</a></li>
  </ul>
  <div id="a1">Content1</div>
  <div id="a2">Content2</div>
  <div id="a3">Content3</div>
  <div id="a4">Content4</div>
</div>

這明顯被切成兩區了, title的部分顯示在<ul>內, 而content卻在另一區, 那這該怎麼做呢?

取得Slot內的子元件(Child component)

上面的例子可以寫成這樣:

Vue.component('b-menu', {
  data () {
  	return {
      items: []
    }
  },
  template: '<div><ul><li v-for="(item,i) in items"><a :href="`#a${i+1}`"></a></li></ul><div><slot></slot></div></div>',
  mounted () {
     this.items= this.$slots.default.filter(item => item.componentInstance || false).map(item => item.componentInstance)
     for (let [index, item] of this.items.entries()) {
       this.$set(item, 'id', 'a' + (index + 1))
     }
  }
})

Vue.component('b-menu-item', {
  template: '<div :id="id"><slot></slot></div>',
  props: ['title'],
  data () {
    return {
      id: ''
    }
  }
})

要取得slot裡面的child nodes可以用this.$slots.default, 但這個包含所有的child nodes, 如果我們要的只是child components, 那可以檢查這個node是否包含componentInstance

因此, 透過filter和map, 我們可以以this.$slots.default.filter(item => item.componentInstance || false).map(item => item.componentInstance)來取得child components, 在這個例子就包含所有的b-menu-item

這段程式的作法就是取得所有child components放入items這個資料欄位中, 而在template中有<li v-for="(item,i) in items">利用items內的值來渲染<li>的部分

這邊有一點需要注意的是, 這段必須要跑在 mounted()不能在created(), 因為在created()裡面雖然可以用this.$slots.default來取得child nodes, 但這時候child nodes的componentInstace全部都是undefined, 因為這時候child components其實都還沒準備好

處理動態內容

b-menu-item當然也可以用v-for來動態渲染, 像是:

<b-menu>
   <b-menu-item v-for="mItem in menuItems" :title="mItem.title"></b-menu-item>
</b-menu>

這邊的menuItems如果是一個靜態的陣列下面例子, 不會有問題

let vue = new Vue({
  el: '#app',
  data () {
     return {
       menuItems: [
          {
             title: 'menu 1',
             content: 'content 1'
          },
          {
             title: 'menu 2',
             content: 'content 2'
          },
          {
             title: 'menu 3',
             content: 'content 3'
          }
       ]
     }
  }
})

但如果它的內容是由一個async function所產生, 像是

let vue = new Vue({
  el: '#app',
  data () {
     return {
       menuItems: []
     }
  },
  mounted () {
	let vm = this
	doGetSomething(data => {
		vm.menuItems = data
	})
  }
})

你可能會發現畫面完全沒變化, 那是因為我們在b-menumouted()的時候去掃所有的child components, 而menuItems可能在mouted很之後才會被更新, 所以不會被重掃一次, items並不會被更新, 所以畫面也不會有變化, 因此必須要在menuItems資料被更新後再掃一次slot的child components

可以把b-menu改成這樣

Vue.component('b-menu', {
  data () {
  	return {
      items: []
    }
  },
  template: '<div><ul><li v-for="(item,i) in items"><a :href="`#a${i+1}`"></a></li></ul><div><slot></slot></div></div>',
  methods: {
	updateItems () {
	  	this.items= this.$slots.default.filter(item => item.componentInstance || false).map(item => item.componentInstance)
		for (let [index, item] of this.items.entries()) {
		this.$set(item, 'id', 'a' + (index + 1))
		}
	}
  },
  mounted () {
     updateItems()
  }
})

這樣亦即是, 我們在更新完資料後必須要再呼叫一次updateItems()

為了直接呼叫到b-menu的updateItems, 可以先替他加一個ref="menu", 方便後面存取

<b-menu ref="menu">
   <b-menu-item v-for="mItem in menuItems" :title="mItem.title"></b-menu-item>
</b-menu>

前面更新menuItems的程式可以改寫成這樣:

let vue = new Vue({
  el: '#app',
  data () {
     return {
       menuItems: []
     }
  },
  mounted () {
	let vm = this
	doGetSomething(data => {
		vm.menuItems = data
		vm.$refs.menu.updateItems()
	})
  }
})

這邊透過vm.$refs.menu.updateItems()來更新items

但….還是沒動靜呀…怎麼回事? 因為這時候menuItems才剛被更新, 它先去更新b-munu-item, 如果讓items更新後, 畫面要跟著更新, 就必須要在下一個DOM的更新週期, 也就是使用$nextTick, 如下:

let vue = new Vue({
  el: '#app',
  data () {
     return {
       menuItems: []
     }
  },
  mounted () {
	let vm = this
	doGetSomething(data => {
		vm.menuItems = data
		vm.$nextTick(() => {
			vm.$refs.menu.updateItems()
		})
	})
  }
})

這樣就沒問題了!

但對於一個元件來說, 這樣的設計並不是很好, 變成這個元件必須相依於使用它的程式, 還有沒更好的寫法?

在子原件更新時呼叫父元件呢?

b-menu-item這樣改寫:

Vue.component('b-menu-item', {
  template: '<div :id="id"><slot></slot></div>',
  props: ['title'],
  data () {
    return {
      id: ''
    }
  },
  mounted () {
     this.$parent.updateItems()
  }
})

這樣也是可行的, 當新的b-menu-item被加入slot中時, 就會呼叫一次updateItems

但這是有缺點的:

  1. 每個child component會呼叫一次, 但實際上不需要被呼叫這麼多次, 有點浪費
  2. 這個子原件的設計變成會依賴父元件, 不易與用在其他元件內

所以還是需要一個更好的方式

MutationObserver

這時候就要借用HTML5的MutationObserver, 這個在Vuejs內部也是大量地被使用

使用MutationObserver, 我們可以把b-menu改成這樣:

Vue.component('b-menu', {
  data () {
  	return {
      items: [],
	  domObserver: null
    }
  },
  template: '<div><ul><li v-for="(item,i) in items"><a :href="`#a${i+1}`"></a></li></ul><div ref="content"><slot></slot></div></div>',
  methods: {
	updateItems () {
	  	this.items= this.$slots.default.filter(item => item.componentInstance || false).map(item => item.componentInstance)
		for (let [index, item] of this.items.entries()) {
		this.$set(item, 'id', 'a' + (index + 1))
		}
	}
  },
  mounted () {
     updateItems()
	 let vm = this
	 vm.domObserver = new MutationObserver((mr, el) => {
		 let shouldUpdate = false
		 for (let m of mr) {
			 if (m.addedNodes.length > 0 || m.removedNodes.length > 0) {
				 shouldUpdate = true
				 break
			 }
		 }

		 if (shouldUpdate) {
			 vm.updateItems()
		 }
	 })

	 vm.domObserver.observer(vm.$refs.content, {childList: true, subtree: true})
  }
})

藉由監控slot的父節點(parent node)的變化來確定是否要去更新items, 這樣一來就不用依賴其他人也可以做到自動更新了

axios是蠻好用的javascript http client, 不僅可以在browser上跑, 也可以在node.js上用, 而且Promise形態的API寫起來就比較好看, 如果搭配async/await的寫法, 看起來就更加漂亮了

function loadUser(uid) {
  axios.get('/user?ID=12345')
    .then(response => {
      console.log(response)
    })
    .catch(error => {
      console.log(error)
    })
}

或是(async/await)

async function loadUser(uid) {
  try {
    data = await axios.get('/user?ID=12345')
	console.log(data)
  } catch(e) {
    console.log(e)
  }
}

但如果開發時期或是要做Unit testing需要用假資料來取代server api直接回傳呢? 目前我看到兩套方案, 一個是axios作者做的moxios另外一個是axios-mock-adapter, moxios看起來好像比較適合在Unit testing時, 而我是想在開發過程中使用, 所以我選的是axios-mock-adapter

使用axios-mock-adapter還蠻簡單的:

import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'

let mock = new MockAdapter(axios)

mock.onGet('/users').reply(200, {
  users: [
    { id: 1, name: 'John Smith' },
    { id: 2, name: 'John Doe' }
  ]
})

axios.get('/users')
  .then(response => {
    console.log(response.data)
  })

創建uri跟假資料的對應很簡單, 基本上也就是’on’‘Method’, 比如說onGet, onPost, 另外還有一個onAny可以處理所有的HTTP methods

做過mock後, axios呼叫這個uri所拿回來的資料通通就都會是假資料了, 這樣也不用為了塞假資料開發測試而去改動自己的程式

在開發的時候總會有一個需求是想在開發階段做跟生產環境不一樣的事, 像我自己的習慣是在做畫面時, 不見得後端資料和API都已經準備好了, 所以我會先以假的資料(mock data)來取代, 放上線後才是真正去抓server api

因此就會需要一個方法來判斷現在到底是不是在開發階段, 如果使用webpack來開發, 這件事就會變得很簡單, 最近一直在寫Vue.js, 這邊就有Vue.js (反正我也還不懂react.js, 哈)

用vue-cli建立一個以webpack為工作流程的專案很簡單:

vue init webpack my-project

跑完後相關的檔案都幫你產生好了

要在你的程式裡面判斷目前是否是開發環境的話, 只要加入這樣的判斷:

if (process.env.NODE_ENV === 'development') {
  console.log('Hi!You are in dev env')
}

生產環境就把”development”換成”production”就好了

不過實際跑到瀏覽器上時, 如果你在developement console內直接下console.log(process.env.NODE_ENV), 你會發現完全沒這東西, 這是因為process.env.NODE_ENV並不是活在client端, 而是webpack在建置過程(跑在node.js)中動了手腳做了轉換了

那這值是在哪邊被定義呢? 打開”build/webpack.dev.conf.js”這個檔案看, 你會發現:

plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),
	...
]

以及”config/dev.env.js”:

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})

在標準的go package中除了已經內建了http相關的實作外, 還有一個net/http/httptest, 這的package是用來給寫http相關測試用的, 可分為測試http server (http handler)和http client的(提供mock server給client)

如果要測試http handler, 所需要的是ResponseRecorder, 基本上相關的也只需要兩個方法NewRequest, NewRecorder, 參照下面範例:

package main

import (
        "fmt"
        "github.com/stretchr/testify/assert"
        "io"
        "io/ioutil"
        "net/http"
        "net/http/httptest"
        "testing"
)

func TestHttpServer(t *testing.T) {
        assert := assert.New(t)
        handler := func(w http.ResponseWriter, r *http.Request) {
                io.WriteString(w, "<html><body>Hello World!</body></html>")
        }

        req := httptest.NewRequest("GET", "http://example.com/foo", nil)
        w := httptest.NewRecorder()
        handler(w, req)

        resp := w.Result()
        body, _ := ioutil.ReadAll(resp.Body)

        assert.Equal(200, resp.StatusCode)
        assert.Equal("text/html; charset=utf-8", resp.Header.Get("Content-Type"))
        fmt.Println(string(body))
}

範例中先用NewRequest假造出一個連接到http://example.com/foo的request, 而對於一個http handler來說的話, 所需要的參數一個是*Request, 另一個則是ResponseWriter了, ResponseWriter便可以由NewRecorder假冒, 再將這兩者傳給handler去處理, 並可以由ResponseRecorder.Result取得回傳內容來驗證

那如果是要測試http client呢?測client通常我們會需要mock server來偽造成真著server餵給client適當的假資料來測試, 以下是一個測試的範例:

func TestServer(t *testing.T) {
        assert := assert.New(t)
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "Hello, client")
        }))
        defer ts.Close()

        res, err := http.Get(ts.URL)
        if err != nil {
                t.Fatal(err)
        }
        greeting, err := ioutil.ReadAll(res.Body)
        res.Body.Close()
        if err != nil {
                t.Fatal(err)
        }

        assert.Equal("Hello, client\n", string(greeting))
}

利用httptest.NewServer創建一個test server, 這邊的handler就看你需要利用什麼樣的假資料測試來做, 上面這例子只是用單純的http client來測, 回傳總是是”Hello, client”, 但假設你是測試restful API, 那也可以準備一系列的JSON回傳, 你只要把ts.URL當作你的API endpoint給你的REST client的實作使用即可