給 JavaScript 初心者的 ES2015 實戰(zhàn)
前言
歷時將近6年的時間來制定的新 ECMAScript 標準 ECMAScript 6(亦稱 ECMAScript Harmony,簡稱 ES6)終于在 2015 年 6 月正式發(fā)布。自從上一個標準版本 ES5 在 2009 年發(fā)布以后,ES6 就一直以新語法、新特性的優(yōu)越性吸引著眾多 JavaScript 開發(fā)者,驅(qū)使他們積極嘗鮮。
雖然至今各大瀏覽器廠商所開發(fā)的 JavaScript 引擎都還沒有完成對 ES2015 中所有特性的完美支持,但這并不能阻擋工程師們對 ES6 的熱情,于是乎如 babel、Traceur 等編譯器便出現(xiàn)了。它們能將尚未得到支持的 ES2015 特性轉(zhuǎn)換為 ES5 標準的代碼,使其得到瀏覽器的支持。其中,babel 因其模塊化轉(zhuǎn)換器(Transformer)的設計特點贏得了絕大部份 JavaScript 開發(fā)者的青睞,本文也將以 babel 為基礎工具,向大家展示 ES2015 的神奇魅力。
筆者目前所負責的項目中,已經(jīng)在前端和后端全方位的使用了 ES2015 標準進行 JavaScript 開發(fā),已有將近兩年的 ES2015 開發(fā)經(jīng)驗。如今 ES2015 以成為 ECMA 國際委員會的首要語言標準,使用 ES2015 標準所進行的工程開發(fā)已打好了堅實的基礎,而 ES7(ES2016) 的定制也走上了正軌,所以在這個如此恰當?shù)臅r機,我覺得應該寫一篇通俗易懂的 ES2015 教程來引導廣大 JavaScript 愛好者和工程師向新時代前進。若您能從本文中有所收獲,便是對我最大的鼓勵。
我希望你在閱讀本文前,已經(jīng)掌握了 JavaScript 的基本知識,并具有一定的 Web App 開發(fā)基礎和 Node.js 基本使用經(jīng)驗。
目錄
一言蔽之 ES2015
ES2015 能為 JavaScript 的開發(fā)帶來什么
- 語法糖
- 工程優(yōu)勢
ES2015 新語法詳解
let
、 const
和塊級作用域
箭頭函數(shù)(Arrow Function)
- 使用方法
- 箭頭函數(shù)與上下文綁定
- 注意事項
模板字符串
對象字面量擴展語法
- 方法屬性省略
function
- 支持注入
__proto__
- 同名方法屬性省略語法
- 可以動態(tài)計算的屬性名稱
表達式解構(gòu)
函數(shù)參數(shù)表達、傳參
默認參數(shù)值
后續(xù)參數(shù)
解構(gòu)傳參
注意事項
新的數(shù)據(jù)結(jié)構(gòu)
- Set 和 WeakSet
- Map 和 WeakMap
類(Classes)
生成器(Generator)
來龍
基本概念
- Generator Function
- Generator
基本使用方法
Promise
原生的模塊化
Symbol
Proxy
(代理)
ES2015 的前端開發(fā)實戰(zhàn)
構(gòu)建界面
結(jié)構(gòu)定義
架構(gòu)設計
構(gòu)建應用
入口文件
數(shù)據(jù)層:文章
路由:首頁
- 準備頁面渲染
- 加載數(shù)據(jù)
- 設計組件
路由:文章頁面
路由:發(fā)布新文章
路由綁定
合并代碼
ES2015 的 Node.js 開發(fā)實戰(zhàn)
架構(gòu)設計
構(gòu)建應用
入口文件
數(shù)據(jù)抽象層
Posts 控制器
- API:獲取所有文章
- API:獲取指定文章
- API:發(fā)布新文章
Comments 控制器
- API:獲取指定文章的評論
- API:發(fā)表新評論
配置路由
配置任務文件
部署到 DaoCloud
- Dockerfile
- 創(chuàng)建 DaoCloud 上的 MongoDB 服務
- 代碼構(gòu)建
一窺 ES7
async/await
Decorators
后記
本文的實戰(zhàn)部份將以開發(fā)一個動態(tài)博客系統(tǒng)為背景,向大家展示如何使用 ES2015 進行項目開發(fā)。成品代碼將在 GitHub 上展示。
一言蔽之 ES2015
說到 ES2015,有了解過的同學一定會馬上想到各種新語法,如箭頭函數(shù)(=>
)、class
、模板字符串等。是的,ECMA 委員會吸取了許多來自全球眾多 JavaScript 開發(fā)者的意見和來自其他優(yōu)秀編程語言的經(jīng)驗,致力于制定出一個更適合現(xiàn)代 JavaScript 開發(fā)的標準,以達到“和諧”(Harmony)。一言蔽之:
ES2015 標準提供了許多新的語法和編程特性以提高 JavaScript 的開發(fā)效率和體驗
從 ES6 的別名被定為 Harmony 開始,就注定了這個新的語言標準將以一種更優(yōu)雅的姿態(tài)展現(xiàn)出來,以適應日趨復雜的應用開發(fā)需求。
ES2015 能為 JavaScript 的開發(fā)帶來什么
語法糖
如果您有其他語言(如 Ruby、Scala)或是某些 JavaScript 的衍生語言(如 CoffeeScript、TypeScript)的開發(fā)經(jīng)驗,就一定會了解一些很有意思的語法糖,如 Ruby 中的 Range -> 1..10
,Scala 和 CoffeeScript 中的箭頭函數(shù) (a, b) => a + b
。ECMA 委員會借鑒了許多其他編程語言的標準,給 ECMAScript 家族帶來了許多可用性非常高的語法糖,下文將會一一講解。
這些語法糖能讓 JavaScript 開發(fā)者更舒心地開發(fā) JavaScript 應用,提高我們的工作效率~~,多一些時間出去浪~~。
工程優(yōu)勢
ES2015 除了提供了許多語法糖以外,還由官方解決了多年來困擾眾多 JavaScript 開發(fā)者的問題:JavaScript 的模塊化構(gòu)建。從許多年前開始,各大公司、團隊、大牛都相繼給出了他們對于這個問題的不同解決方案,以至于定下了如 CommonJS、AMD、CMD 或是 UMD 等 JavaScript 模塊化標準,RequireJS、SeaJS、FIS、Browserify、webpack 等模塊加載庫都以各自不同的優(yōu)勢占領著一方土地。
然而正正是因為這春秋戰(zhàn)國般的現(xiàn)狀,廣大的前端搬磚工們表示很納悶。
這?究竟哪種好?哪種適合我?求大神帶我飛!
對此,ECMA 委員會終于是坐不住了,站了起來表示不服,并制訂了 ES2015 的原生模塊加載器標準。
import fs from 'fs'
import readline from 'readline'
import path from 'path'
let Module = {
readLineInFile(filename, callback = noop, complete = noop) {
let rl = readline.createInterface({
input: fs.createReadStream(path.resolve(__dirname, './big_file.txt'))
})
rl.on('line', line => {
//... do something with the current line
callback(line)
})
rl.on('close', complete)
return rl
}
}
function noop() { return false }
export default Module
~~老實說,這套模塊化語法不禁讓我們又得要對那個很 silly 的問題進行重新思考了:JavaScript 和 Java 有什么關(guān)系?~~
可惜的是,目前暫時還沒有任何瀏覽器廠商或是 JavaScript 引擎支持這種模塊化語法。所以我們需要用 babel 進行轉(zhuǎn)換為 CommonJS、AMD 或是 UMD 等模塊化標準的語法。
ES2015 新語法詳解
經(jīng)過以上的介(xun)紹(tao),相信你對 ES2015 也有了一定的了解和期待。接下來我將帶大家慢慢看看 ECMA 委員會含辛茹苦制定的新語言特性吧。
let
、const
和塊級作用域
在 ES2015 的新語法中,影響速度最為直接,范圍最大的,恐怕得數(shù) let
和 const
了,它們是繼 var
之后,新的變量定義方法。與 let
相比,const
更容易被理解:const
也就是 constant 的縮寫,跟 C/C++ 等經(jīng)典語言一樣,用于定義常量,即不可變量。
但由于在 ES6 之前的 ECMAScript 標準中,并沒有原生的實現(xiàn),所以在降級編譯中,會馬上進行引用檢查,然后使用 var
代替。
// foo.js
const foo = 'bar'
foo = 'newvalue'
$ babel foo.js
...
SyntaxError: test.js: Line 3: "foo" is read-only
1 | const foo = 'bar'
2 |
> 3 | foo = 'newvalue'
...
塊級作用域
在 ES6 誕生之前,我們在給 JavaScript 新手解答困惑時,經(jīng)常會提到一個觀點:
JavaScript 沒有塊級作用域
在 ES6 誕生之前的時代中,JavaScript 確實是沒有塊級作用域的。這個問題之所以為人所熟知,是因為它引發(fā)了諸如歷遍監(jiān)聽事件需要使用閉包解決等問題。
<button>一</button>
<button>二</button>
<button>三</button>
<button>四</button>
<div id="output"></div>
<script>
var buttons = document.querySelectorAll('button')
var output = document.querySelector('#output')
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
output.innerText = buttons[i].innerText
})
}
</script>
前端新手非常容易寫出類似的代碼,因為從直觀的角度看這段代碼并沒有語義上的錯誤,但是當我們點擊任意一個按鈕時,就會報出這樣的錯誤信息:
Uncaught TypeError: Cannot read property 'innerText' of undefined
出現(xiàn)這個錯誤的原因是因為 buttons[i]
不存在,即為 undefined
。
為什么會出現(xiàn)按鈕不存在結(jié)果呢?通過排查,我們可以發(fā)現(xiàn),每次我們點擊按鈕時,事件監(jiān)聽回調(diào)函數(shù)中得到的變量 i
都會等于 buttons.length
,也就是這里的 4。而 buttons[4]
恰恰不存在,所以導致了錯誤的發(fā)生。
再而導致 i
得到的值都是 buttons.length
的原因就是因為 JavaScript 中沒有塊級作用域,而使對 i
的變量引用(Reference)一直保持在上一層作用域(循環(huán)語句所在層)上,而當循環(huán)結(jié)束時 i
則正好是 buttons.length
。
而在 ES6 中,我們只需做出一個小小的改動,便可以解決該問題(假設所使用的瀏覽器已經(jīng)支持所需要的特性):
// ...
for (/* var */ let i = 0; i < buttons.length; i++) {
// ...
}
// ...
通過把 for
語句中對計數(shù)器 i
的定義語句從 var
換成 let
,即可。因為 let
語句會使該變量處于一個塊級作用域中,從而讓事件監(jiān)聽回調(diào)函數(shù)中的變量引用得到保持。我們不妨看看改進后的代碼經(jīng)過 babel 的編譯會變成什么樣子:
// ...
var _loop = function (i) {
buttons[i].addEventListener('click', function () {
output.innerText = buttons[i].innerText
})
}
for (var i = 0; i < buttons.length; i++) {
_loop(i)
}
// ...
實現(xiàn)方法一目了然,通過傳值的方法防止了 i
的值錯誤。
箭頭函數(shù)(Arrow Function)
繼 let
和 const
之后,箭頭函數(shù)就是使用率最高的新特性了。當然了,如果你了解過 Scala 或者曾經(jīng)如日中天的 JavaScript 衍生語言 CoffeeScript,就會知道箭頭函數(shù)并非 ES6 獨創(chuàng)。
箭頭函數(shù),顧名思義便是使用箭頭(=>
)進行定義的函數(shù),屬于匿名函數(shù)(Lambda)一類。當然了,也可以作為定義式函數(shù)使用,但我們并不推薦這樣做,隨后會詳細解釋。
使用
箭頭函數(shù)有好幾種使用語法:
1. foo => foo + ' world' // means return `foo + ' world'`
2. (foo, bar) => foo + bar
3.
foo => {
return foo + ' world'
}
4.
(foo, bar) => {
return foo + bar
}
以上都是被支持的箭頭函數(shù)表達方式,其最大的好處便是簡潔明了,省略了 function
關(guān)鍵字,而使用 =>
代替。
箭頭函數(shù)語言簡潔的特點使其特別適合用於單行回調(diào)函數(shù)的定義:
let names = [ 'Will', 'Jack', 'Peter', 'Steve', 'John', 'Hugo', 'Mike' ]
let newSet = names
.map((name, index) => {
return {
id: index,
name: name
}
})
.filter(man => man.id % 2 == 0)
.map(man => [man.name])
.reduce((a, b) => a.concat(b))
console.log(newSet) //=> [ 'Will', 'Peter', 'John', 'Mike' ]
如果你有 Scala + Spark 的開發(fā)經(jīng)驗,就一定會覺得這非常親切,因為這跟其中的 RDD 操作幾乎如出一轍:
- 將原本的由名字組成的數(shù)組轉(zhuǎn)換為一個格式為
{ id, name }
的對象,id
則為每個名字在原數(shù)組中的位置
- 剔除其中
id
為奇數(shù)的元素,只保留 id
為偶數(shù)的元素
- 將剩下的元素轉(zhuǎn)換為一個包含當前元素中原名字的單元數(shù)組,以方便下一步的處理
- 通過不斷合并相鄰的兩個數(shù)組,最后能得到的一個數(shù)組,便是我們需要得到的目標值
箭頭函數(shù)與上下文綁定
事實上,箭頭函數(shù)在 ES2015 標準中,并不只是作為一種新的語法出現(xiàn)。就如同它在 CoffeeScript 中的定義一般,是用于對函數(shù)內(nèi)部的上下文 (this
)綁定為定義函數(shù)所在的作用域的上下文。
let obj = {
hello: 'world',
foo() {
let bar = () => {
return this.hello
}
return bar
}
}
window.hello = 'ES6'
window.bar = obj.foo()
window.bar() //=> 'world'
上面代碼中的 obj.foo
等價于:
// ...
foo() {
let bar = (function() {
return this.hello
}).bind(this)
return bar
}
// ...
為什么要為箭頭函數(shù)給予這樣的特性呢?我們可以假設出這樣的一個應用場景,我們需要創(chuàng)建一個實例,用于對一些數(shù)據(jù)進行查詢和篩選。
let DataCenter = {
baseUrl: 'http:///api/data',
search(query) {
fetch(`${this.baseUrl}/search?query=${query}`)
.then(res => res.json())
.then(rows => {
// TODO
})
}
}
此時,從服務器獲得數(shù)據(jù)是一個 JSON 編碼的數(shù)組,其中包含的元素是若干元素的 ID,我們需要另外請求服務器的其他 API 以獲得元素本身(當然了,實際上的 API 設計大部份不會這么使用這么蛋疼的設計)。我們就需要在回調(diào)函數(shù)中再次使用 this.baseUrl
這個屬性,如果要同時兼顧代碼的可閱讀性和美觀性,ES2015 允許我們這樣做。
let DataCenter = {
baseUrl: 'http:///api/data',
search(query) {
return fetch(`${this.baseUrl}/search?query=${query}`)
.then(res => res.json())
.then(rows => {
return fetch(`${this.baseUrl}/fetch?ids=${rows.join(',')}`)
// 此處的 this 是 DataCenter,而不是 fetch 中的某個實例
})
.then(res => res.json())
}
}
DataCenter.search('iwillwen')
.then(rows => console.log(rows))
因為在單行匿名函數(shù)中,如果 this
指向的是該函數(shù)的上下文,就會不符合直觀的語義表達。
注意事項
另外,要注意的是,箭頭函數(shù)對上下文的綁定是強制性的,無法通過 apply
或 call
方法改變其上下文。
let a = {
init() {
this.bar = () => this.dam
},
dam: 'hei',
foo() {
return this.dam
}
}
let b = {
dam: 'ha'
}
a.init()
console.log(a.foo()) //=> hei
console.log(a.foo.bind(b).call(a)) //=> ha
console.log(a.bar.call(b)) //=> hei
另外,因為箭頭函數(shù)會綁定上下文的特性,故不能隨意在頂層作用域使用箭頭函數(shù),以防出錯:
// 假設當前運行環(huán)境為瀏覽器,故頂層作上下文為 `window`
let obj = {
msg: 'pong',
ping: () => {
return this.msg // Warning!
}
}
obj.ping() //=> undefined
let msg = 'bang!'
obj.ping() //=> bang!
為什么上面這段代碼會如此讓人費解呢?
我們來看看它的等價代碼吧。
let obj = {
// ...
ping: (function() {
return this.msg // Warning!
}).bind(this)
}
// 同樣等價于
let obj = { /* ... */ }
obj.ping = (function() {
return this.msg
}).bind(this /* this -> window */)
模板字符串
模板字符串模板出現(xiàn)簡直對 Node.js 應用的開發(fā)和 Node.js 自身的發(fā)展起到了相當大的推動作用!我的意思并不是說這個原生的模板字符串能代替現(xiàn)有的模板引擎,而是說它的出現(xiàn)可以讓非常多的字符串使用變得尤為輕松。
模板字符串要求使用 ` 代替原本的單/雙引號來包裹字符串內(nèi)容。它有兩大特點:
- 支持變量注入
- 支持換行
支持變量注入
模板字符串之所以稱之為“模板”,就是因為它允許我們在字符串中引用外部變量,而不需要像以往需要不斷地相加、相加、相加……
let name = 'Will Wen Gunn'
let title = 'Founder'
let company = 'LikMoon Creation'
let greet = `Hi, I'm ${name}, I am the ${title} at ${company}`
console.log(greet) //=> Hi, I'm Will Wen Gunn, I am the Founder at LikMoon Creation
支持換行
在 Node.js 中,如果我們沒有支持換行的模板字符串,若需要拼接一條SQL,則很有可能是這樣的:
var sql =
"SELECT * FROM Users " +
"WHERE FirstName='Mike' " +
"LIMIT 5;"
或者是這樣的:
var sql = [
"SELECT * FROM Users",
"WHERE FirstName='Mike'",
"LIMIT 5;"
].join(' ')
無論是上面的哪一種,都會讓我們感到很不爽。但若使用模板字符串,仿佛打開了新世界的大門~
let sql = `
SELECT * FROM Users
WHERE FirstName='Mike'
LIMIT 5;
`
Sweet! 在 Node.js 應用的實際開發(fā)中,除了 SQL 的編寫,還有如 Lua 等嵌入語言的出現(xiàn)(如 Redis 中的 SCRIPT 命令),或是手工的 XML 拼接。模板字符串的出現(xiàn)使這些需求的解決變得不再糾結(jié)了~
對象字面量擴展語法
看到這個標題的時候,相信有很多同學會感到奇怪,對象字面量還有什么可以擴展的?
確實,對象字面量的語法在 ES2015 之前早已挺完善的了。不過,對于聰明的工程師們來說,細微的改變,也能帶來不少的價值。
方法屬性省略 function
這個新特性可以算是比較有用但并不是很顯眼的一個。
let obj = {
// before
foo: function() {
return 'foo'
},
// after
bar() {
return 'bar'
}
}
支持 __proto__
注入
在 ES2015 中,我們可以給一個對象硬生生的賦予其 __proto__
,這樣它就可以成為這個值所屬類的一個實例了。
class Foo {
constructor() {
this.pingMsg = 'pong'
}
ping() {
console.log(this.pingMsg)
}
}
let o = {
__proto__: new Foo()
}
o.ping() //=> pong
什么?有什么卵用?
有一個比較特殊的場景會需要用到:我想擴展或者覆蓋一個類的方法,并生成一個實例,但覺得另外定義一個類就感覺浪費了。那我可以這樣做:
let o = {
__proto__: new Foo(),
constructor() {
this.pingMsg = 'alive'
},
msg: 'bang',
yell() {
console.log(this.msg)
}
}
o.yell() //=> bang
o.ping() //=> alive
同名方法屬性省略語法
也是看上去有點雞肋的新特性,不過在做 JavaScript 模塊化工程的時候則有了用武之地。
// module.js
export default {
someMethod
}
function someMethod() {
// ...
}
// app.js
import Module from './module'
Module.someMethod()
可以動態(tài)計算的屬性名稱
這個特性相當有意思,也是可以用在一些特殊的場景中。
let arr = [1, 2, 3]
let outArr = arr.map(n => {
return {
[ n ]: n,
[ `${n}^2` ]: Math.pow(n, 2)
}
})
console.dir(outArr) //=>
[
{ '1': 1, '1^2': 1 },
{ '2': 2, '2^2': 4 },
{ '3': 3, '3^2': 9 }
]
在上面的兩個 [...]
中,我演示了動態(tài)計算的對象屬性名稱的使用,分別為對應的對象定義了當前計數(shù)器 n
和 n
的 2 次方
表達式解構(gòu)
來了來了來了,相當有用的一個特性。有啥用?多重復值聽過沒?沒聽過?來看看吧!
// Matching with object
function search(query) {
// ...
// let users = [ ... ]
// let posts = [ ... ]
// ...
return {
users: users,
posts: posts
}
}
let { users, posts } = search('iwillwen')
// Matching with array
let [ x, y ] = [ 1, 2 ]
// missing one
[ x, ,y ] = [ 1, 2, 3 ]
function g({name: x}) {
console.log(x)
}
g({name: 5})
還有一些可用性不大,但也是有一點用處的:
// Fail-soft destructuring
var [a] = []
a === undefined //=> true
// Fail-soft destructuring with defaults
var [a = 1] = []
a === 1 //=> true
函數(shù)參數(shù)表達、傳參
這個特性有非常高的使用頻率,一個簡單的語法糖解決了從前需要一兩行代碼才能實現(xiàn)的功能。
默認參數(shù)值
這個特性在類庫開發(fā)中相當有用,比如實現(xiàn)一些可選參數(shù):
import fs from 'fs'
import readline from 'readline'
import path from 'path'
function readLineInFile(filename, callback = noop, complete = noop) {
let rl = readline.createInterface({
input: fs.createReadStream(path.resolve(__dirname, filename))
})
rl.on('line', line => {
//... do something with the current line
callback(line)
})
rl.on('close', complete)
return rl
}
function noop() { return false }
readLineInFile('big_file.txt', line => {
// ...
})
后續(xù)參數(shù)
我們知道,函數(shù)的 call
和 apply
在使用上的最大差異便是一個在首參數(shù)后傳入各個參數(shù),一個是在首參數(shù)后傳入一個包含所有參數(shù)的數(shù)組。如果我們在實現(xiàn)某些函數(shù)或方法時,也希望實現(xiàn)像 call
一樣的使用方法,在 ES2015 之前,我們可能需要這樣做:
function fetchSomethings() {
var args = [].slice.apply(arguments)
// ...
}
function doSomeOthers(name) {
var args = [].slice.apply(arguments, 1)
// ...
}
而在 ES2015 中,我們可以很簡單的使用 …
語法糖來實現(xiàn):
function fetchSomethings(...args) {
// ...
}
function doSomeOthers(name, ...args) {
// ...
}
要注意的是,...args
后不可再添加
雖然從語言角度看,arguments
和 ...args
是可以同時使用 ,但有一個特殊情況則不可:arguments
在箭頭函數(shù)中,會跟隨上下文綁定到上層,所以在不確定上下文綁定結(jié)果的情況下,盡可能不要再箭頭函數(shù)中再使用 arguments
,而使用 ...args
。
雖然 ECMA 委員會和各類編譯器都無強制性要求用 ...args
代替 arguments
,但從實踐經(jīng)驗看來,...args
確實可以在絕大部份場景下可以代替 arguments
使用,除非你有很特殊的場景需要使用到 arguments.callee
和 arguments.caller
。所以我推薦都使用 ...args
而非 arguments
。
PS:在嚴格模式(Strict Mode)中,arguments.callee
和 arguments.caller
是被禁止使用的。
解構(gòu)傳參
在 ES2015 中,...
語法還有另外一個功能:無上下文綁定的 apply
。什么意思?看看代碼你就知道了。
function sum(...args) {
return args.map(Number)
.reduce((a, b) => a + b)
}
console.log(sum(...[1, 2, 3])) //=> 6
有什么卵用?我也不知道(⊙o⊙)... Sorry...
注意事項
默認參數(shù)值和后續(xù)參數(shù)需要遵循順序原則,否則會出錯。
function(...args, last = 1) {
// This will go wrong
}
另外,根據(jù)函數(shù)調(diào)用的原則,無論是默認參數(shù)值還是后續(xù)參數(shù)都需要小心使用。
新的數(shù)據(jù)結(jié)構(gòu)
在介紹新的數(shù)據(jù)結(jié)構(gòu)之前,我們先復習一下在 ES2015 之前,JavaScript 中有哪些基本的數(shù)據(jù)結(jié)構(gòu)。
- String 字符串
- Number 數(shù)字(包含整型和浮點型)
- Boolean 布爾值
- Object 對象
- Array 數(shù)組
其中又分為值類型和引用類型,Array 其實是 Object 的一種子類。
Set 和 WeakSet
我們再來復習下高中數(shù)學吧,集不能包含相同的元素,我們可以根據(jù)元素畫出多個集的韋恩圖…………
好了跑題了。是的,在 ES2015 中,ECMA 委員會為 ECMAScript 增添了集(Set)和“弱”集(WeakSet)。它們都具有元素唯一性,若添加了已存在的元素,會被自動忽略。
let s = new Set()
s.add('hello').add('world').add('hello')
console.log(s.size) //=> 2
console.log(s.has('hello')) //=> true
在實際開發(fā)中,我們有很多需要用到集的場景,如搜索、索引建立等。
咦?怎么還有一個 WeakSet?這是干什么的?我曾經(jīng)寫過一篇關(guān)于 JavaScript 內(nèi)存優(yōu)化 的文章,而其中大部份都是在語言上動手腳,而 WeakSet 則是在數(shù)據(jù)上做文章。
WeakSet 在 JavaScript 底層作出調(diào)整(在非降級兼容的情況下),檢查元素的變量引用情況。如果元素的引用已被全部解除,則該元素就會被刪除,以節(jié)省內(nèi)存空間。這意味著無法直接加入數(shù)字或者字符串。另外 WeakSet 對元素有嚴格要求,必須是 Object,當然了,你也可以用 new String('...')
等形式處理元素。
let weaks = new WeakSet()
weaks.add("hello") //=> Error
weaks.add(3.1415) //=> Error
let foo = new String("bar")
let pi = new Number(3.1415)
weaks.add(foo)
weaks.add(pi)
weaks.has(foo) //=> true
foo = null
weaks.has(foo) //=> false
Map 和 WeakMap
從數(shù)據(jù)結(jié)構(gòu)的角度來說,映射(Map)跟原本的 Object 非常相似,都是 Key/Value 的鍵值對結(jié)構(gòu)。但是 Object 有一個讓人非常不爽的限制:key 必須是字符串或數(shù)字。在一般情況下,我們并不會遇上這一限制,但若我們需要建立一個對象映射表時,這一限制顯得尤為棘手。
而 Map 則解決了這一問題,可以使用任何對象作為其 key,這可以實現(xiàn)從前不能實現(xiàn)或難以實現(xiàn)的功能,如在項目邏輯層實現(xiàn)數(shù)據(jù)索引等。
let map = new Map()
let object = { id: 1 }
map.set(object, 'hello')
map.set('hello', 'world')
map.has(object) //=> true
map.get(object) //=> hello
而 WeakMap 和 WeakSet 很類似,只不過 WeakMap 的鍵和值都會檢查變量引用,只要其一的引用全被解除,該鍵值對就會被刪除。
let weakm = new WeakMap()
let keyObject = { id: 1 }
let valObject = { score: 100 }
weakm.set(keyObject, valObject)
weakm.get(keyObject) //=> { score: 100 }
keyObject = null
weakm.has(keyObject) //=> false
類(Classes)
類,作為自 JavaScript 誕生以來最大的痛點之一,終于在 ES2015 中得到了官方的妥協(xié),“實現(xiàn)”了 ECMAScript 中的標準類機制。為什么是帶有雙引號的呢?因為我們不難發(fā)現(xiàn)這樣一個現(xiàn)象:
$ node
> class Foo {}
[Function: Foo]
回想一下在 ES2015 以前的時代中,我們是怎么在 JavaScript 中實現(xiàn)類的?
function Foo() {}
var foo = new Foo()
是的,ES6 中的類只是一種語法糖,用于定義原型(Prototype)的。當然,餓死的廚師三百斤,有總比沒有強,我們還是很欣然地接受了這一設定。
語法
定義
與大多數(shù)人所期待的一樣,ES2015 所帶來的類語法確實與很多 C 語言家族的語法相似。
class Person {
constructor(name, gender, age) {
this.name = name
this.gender = gender
this.age = age
}
isAdult() {
return this.age >= 18
}
}
let me = new Person('iwillwen', 'man', 19)
console.log(me.isAdult()) //=> true
與 JavaScript 中的對象字面量不一樣的是,類的屬性后不能加逗號,而對象字面量則必須要加逗號。
然而,讓人很不爽的是,ES2015 中對類的定義依然不支持默認屬性的語法:
// 理想型
class Person {
name: String
gender = 'man'
// ...
}
而在 TypeScript 中則有良好的實現(xiàn)。
繼承
ES2015 的類繼承總算是為 JavaScript 類繼承之爭拋下了一根定海神針了。在此前,有各種 JavaScript 的繼承方法被發(fā)明和使用。(詳細請參見《JavaScript 高級程序設計》)
class Animal {
yell() {
console.log('yell')
}
}
class Person extends Animal {
constructor(name, gender, age) {
super() // must call `super` before using `this` if this class has a superclass
this.name = name
this.gender = gender
this.age = age
}
isAdult() {
return this.age >= 18
}
}
class Man extends Person {
constructor(name, age) {
super(name, 'man', age)
}
}
let me = new Man('iwillwen', 19)
console.log(me.isAdult()) //=> true
me.yell()
同樣的,繼承的語法跟許多語言中的很類似,ES2015 中若要是一個類繼承于另外一個類而作為其子類,只需要在子類的名字后面加上 extends {SuperClass}
即可。
靜態(tài)方法
ES2015 中的類機制支持 static
類型的方法定義,比如說 Man
是一個類,而我希望為其定義一個 Man.isMan()
方法以用于類型檢查,我們可以這樣做:
class Man {
// ...
static isMan(obj) {
return obj instanceof Man
}
}
let me = new Man()
console.log(Man.isMan(me)) //=> true
遺憾的是,ES2015 的類并不能直接地定義靜態(tài)成員變量,但若必須實現(xiàn)此類需求,可以用static
加上 get
語句和 set
語句實現(xiàn)。
class SyncObject {
// ...
static get baseUrl() {
return 'http:///api/sync'
}
}
遺憾與期望
就目前來說,ES2015 的類機制依然很雞肋:
- 不支持私有屬性(
private
)
- 不支持前置屬性定義,但可用
get
語句和 set
語句實現(xiàn)
- 不支持多重繼承
- 沒有類似于協(xié)議(
Protocl
)或接口(Interface
)等的概念
中肯地說,ES2015 的類機制依然有待加強。但總的來說,是值得嘗試和討論的,我們可以像從前一樣,不斷嘗試新的方法,促進 ECMAScript 標準的發(fā)展。
生成器(Generator)
終于到了 ES2015 中我最喜歡的特性了,前方高能反應,所有人立刻進入戰(zhàn)斗準備!
為什么說這是我最喜歡的新特性呢?對于一個純前端的 JavaScript 工程師來說,可能 Generator 并沒有什么卵用,但若你曾使用過 Node.js 或者你的前端工程中有大量的異步操作,Generator 簡直是你的“賢者之石”。(不過,這并不是 Generator 最正統(tǒng)的用法。出于嚴謹,我會從頭開始講述 Generator)
來龍
Generator 的設計初衷是為了提供一種能夠簡便地生成一系列對象的方法,如計算斐波那契數(shù)列(Fibonacci Sequence):
function* fibo() {
let a = 1
let b = 1
yield a
yield b
while (true) {
let next = a + b
a = b
b = next
yield next
}
}
let generator = fibo()
for (var i = 0; i < 10; i++)
console.log(generator.next().value) //=> 1 1 2 3 5 8 13 21 34 55
如果你沒有接觸過 Generator,你一定會對這段代碼感到很奇怪:為什么 function
后會有一個 *
?為什么函數(shù)里使用了 while (true)
卻沒有進入死循環(huán)而導致死機?yield
又是什么鬼?
不著急,我們一一道來。
基本概念
在學習如何使用 Generator 之前,我們先了解一些必要的概念。
Generator Function
生成器函數(shù)用于生成生成器(Generator),它與普通函數(shù)的定義方式的區(qū)別就在于它需要在 function
后加一個 *
。
function* FunctionName() {
// ...Generator Body
}
生成器函數(shù)的聲明形式不是必須的,同樣可以使用匿名函數(shù)的形式。
let FunctionName = function*() { /* ... */ }
生成器函數(shù)的函數(shù)內(nèi)容將會是對應生成器的運行內(nèi)容,其中支持一種新的語法 yield
。它的作用與 return
有點相似,但并非退出函數(shù),而是切出生成器運行時。
你可以把整個生成器運行時看成一條長長的面條(while (true)
則就是無限長的),JavaScript 引擎在每一次遇到 yield
就要切一刀,而切面所成的“紋路”則是 yield
出來的值。

~~好吧這是瑞士卷~~
Generator
生(rui)成(shi)器(juan)在某種意義上可以看做為與 JavaScript 主線程分離的運行時(詳細可參考我的另外一篇文章:http:///koa-co-and-coroutine/),它可以隨時被 yield
切回主線程(生成器不影響主線程)。
每一次生成器運行時被 yield
都可以帶出一個值,使其回到主線程中;此后,也可以從主線程返回一個值回到生成器運行時中:
let inputValue = yield outputValue
生成器切出主線程并帶出 outputValue
,主函數(shù)經(jīng)過處理后(可以是異步的),把 inputValue
帶回生成器中;主線程可以通過 .next(inputValue)
方法返回值到生成器運行時中。
基本使用方法
構(gòu)建生成器函數(shù)
使用 Generator 的第一步自然是要構(gòu)建生成器函數(shù),理清構(gòu)建思路,比如我需要做一個生成斐波那契數(shù)列(俗稱兔子數(shù)列)的生成器們則需要如何構(gòu)建循環(huán)體呢?如果我需要在主線程不斷獲得結(jié)果,則需要在生成器 中做無限循環(huán),以保證其不斷地生成。
而根據(jù)斐波那契數(shù)列的定義,第 n (n ≥ 3) 項是第 n - 1 項和第 n - 2 之和,而第 1 項和第 2 項都是 1。
function* fibo() {
let [a, b] = [1, 1]
yield a
yield b
while (true) {
[a, b] = [b, a + b]
yield b
}
}
這樣設計生成器函數(shù),就可以先把預先設定好的首兩項輸出,然后通過無限循環(huán)不斷把后一項輸出。
啟動生成器
生成器函數(shù)不能直接用來作為生成器使用,需要先使用這個函數(shù)得到一個生成器,用于運行生成器內(nèi)容和接收返回值。
let gen = fibo()
運行生成器內(nèi)容
得到生成器以后,我們就可以通過它進行數(shù)列項生成了。此處演示獲得前 10 項。
let arr = []
for (let i = 0; i < 10; i++)
arr.push(gen.next().value)
console.log(arr) //=> [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]
你也可以通過圖示理解 Generator 的運行原理

事實上,Generator 的用法還是很多種,其中最為著名的一種便是使用 Generator 的特性模擬出 ES7 中的 async/await 特性。而其中最為著名的就是 co 和 koa(基于 co 的 Web Framework) 了。詳細可以看我的另外一篇文章:Koa, co and coroutine。
原生的模塊化
在前文中,我提到了 ES2015 在工程化方面上有著良好的優(yōu)勢,而采用的就是 ES2015 中的原生模塊化機制,足以證明它的重要性。
歷史小回顧
在 JavaScript 的發(fā)展歷史上,曾出現(xiàn)過多種模塊加載庫,如 RequireJS、SeaJS、FIS 等,而由它們衍生出來的 JavaScript 模塊化標準有 CommonJS、AMD、CMD 和 UMD 等。
其中最為典型的是 Node.js 所遵循的 CommonJS 和 RequireJS 的 AMD。
本文在此不再詳細說明這些模塊化方案,詳細可以閱讀 What Is AMD, CommonJS, and UMD?
基本用法
正如前文所展示的使用方式一樣,ES2015 中的模塊化機制設計也是相當成熟的?;旧纤械?CommonJS 或是 AMD 代碼都可以很快地轉(zhuǎn)換為 ES2015 標準的加載器代碼。
import name from "module-name"
import * as name from "module-name"
import { member } from "module-name"
import { member as alias } from "module-name"
import { member1 , member2 } from "module-name"
import { member1 , member2 as alias2 , [...] } from "module-name"
import defaultMember, { member [ , [...] ] } from "module-name"
import defaultMember, * as alias from "module-name"
import defaultMember from "module-name"
import "module-name"
// Copy from Mozilla Developer Center
如上所示,ES2015 中有很多種模塊引入方式,我們可以根據(jù)實際需要選擇一種使用。
全局引入
全局引入是最基本的引入方式,這跟 CommonJS、AMD 等模塊化標準并無兩樣,都是把目標模塊的所有暴露的接口引入到一個命名空間中。
import name from 'module-name'
import * as name from 'module-name'
這跟 Node.js 所用的 CommonJS 類似:
var name = require('module-name')
局部引入
與 CommonJS 等標準不同的是,ES2015 的模塊引入機制支持引入模塊的部份暴露接口,這在大型的組件開發(fā)中顯得尤為方便,如 React 的組件引入便是使用了該特性。
import { A, B, C } from 'module-name'
A()
B()
C()
接口暴露
ES2015 的接口暴露方式比 CommonJS 等標準都要豐富和健壯,可見 ECMA 委員會對這一部份的重視程度之高。
ES2015 的接口暴露有幾種用法:
暴露單獨接口
// module.js
export function method() { /* ... */ }
// main.js
import M from './module'
M.method()
基本的 export
語句可以調(diào)用多次,單獨使用可暴露一個對象到該模塊外。
暴露復蓋模塊
若需要實現(xiàn)像 CommonJS 中的 module.exports = {}
以覆蓋整個模塊的暴露對象,則需要在 export
語句后加上 default
。
// module.js
export default {
method1,
method2
}
// main.js
import M from './module'
M.method1()
降級兼容
在實際應用中,我們暫時還需要使用 babel 等工具對代碼進行降級兼容。慶幸的是,babel 支持 CommonJS、AMD、UMD 等模塊化標準的降級兼容,我們可以根據(jù)項目的實際情況選擇降級目標。
$ babel -m common -d dist/common/ src/
$ babel -m amd -d dist/amd/ src/
$ babel -m umd -d dist/umd/ src/
Promise
Promise,作為一個老生常談的話題,早已被聰明的工程師們“玩壞”了。
光是 Promise 自身,目前就有多種標準,而目前最為流行的是 Promises/A+。而 ES2015 中的 Promise 便是基于 Promises/A+ 制定的。
概念
Promise 是一種用于解決回調(diào)函數(shù)無限嵌套的工具(當然,這只是其中一種),其字面意義為“保證”。它的作用便是“免去”異步操作的回調(diào)函數(shù),保證能通過后續(xù)監(jiān)聽而得到返回值,或?qū)﹀e誤處理。它能使異步操作變得井然有序,也更好控制。我們以在瀏覽器中訪問一個 API,解析返回的 JSON 數(shù)據(jù)。
fetch('http:///api/users/top')
.then(res => res.json())
.then(data => {
vm.data.topUsers = data
})
// Handle the error crash in the chaining processes
.catch(err => console.error(err))
Promise 在設計上具有原子性,即只有兩種狀態(tài):未開始和結(jié)束(無論成功與否都算是結(jié)束),這讓我們在調(diào)用支持 Promise 的異步方法時,邏輯將變得非常簡單,這在大規(guī)模的軟件工程開發(fā)中具有良好的健壯性。
基本用法
創(chuàng)建 Promise 對象
要為一個函數(shù)賦予 Promise 的能力,先要創(chuàng)建一個 Promise 對象,并將其作為函數(shù)值返回。Promise 構(gòu)造函數(shù)要求傳入一個函數(shù),并帶有 resolve
和 reject
參數(shù)。這是兩個用于結(jié)束 Promise 等待的函數(shù),對應的成功和失敗。而我們的邏輯代碼就在這個函數(shù)中進行。
此處,因為必須要讓這個函數(shù)包裹邏輯代碼,所以如果需要用到 this
時,則需要使用箭頭函數(shù)或者在前面做一個 this
的別名。
function fetchData() {
return new Promise((resolve, reject) => {
// ...
})
}
進行異步操作
事實上,在異步操作內(nèi),并不需要對 Promise 對象進行操作(除非有特殊需求)。
function fetchData() {
return new Promise((resolve, reject) => {
api.call('fetch_data', (err, data) => {
if (err) return reject(err)
resolve(data)
})
})
}
因為在 Promise 定義的過程中,也會出現(xiàn)數(shù)層回調(diào)嵌套的情況,如果需要使用 this
的話,便顯現(xiàn)出了箭頭函數(shù)的優(yōu)勢了。
使用 Promise
讓異步操作函數(shù)支持 Promise 后,我們就可以享受 Promise 帶來的優(yōu)雅和便捷了~
fetchData()
.then(data => {
// ...
return storeInFileSystem(data)
})
.then(data => {
return renderUIAnimated(data)
})
.catch(err => console.error(err))
弊端
雖說 Promise 確實很優(yōu)雅,但是這是在所有需要用到的異步方法都支持 Promise 且遵循標準。而且鏈式 Promise 強制性要求邏輯必須是線性單向的,一旦出現(xiàn)如并行、回溯等情況,Promise 便顯得十分累贅。
所以在目前的最佳實踐中,Promise 會作為一種接口定義方法,而不是邏輯處理工具。后文將會詳細闡述這種最佳實踐。
Symbol
Symbol 是一種很有意思的概念,它跟 Swift 中的 Selector 有點相像,但也更特別。在 JavaScript 中,對象的屬性名稱可以是字符串或數(shù)字。而如今又多了一個 Symbol。那 Symbol 究竟有什么用?
首先,我們要了解的是,Symbol 對象是具有唯一性的,也就是說,每一個 Symbol 對象都是唯一的,即便我們看不到它的區(qū)別在哪里。這就意味著,我們可以用它來保證一些數(shù)據(jù)的安全性。
console.log(Symbol('key') == Symbol('key')) //=> false
如果將一個 Symbol 隱藏于一個封閉的作用域內(nèi),并作為一個對象中某屬性的鍵,則外層作用域中便無法取得該屬性的值,有效保障了某些私有庫的代碼安全性。
let privateDataStore = {
set(val) {
let key = Symbol(Math.random().toString(32).substr(2))
this[key] = val
return key
},
get(key) {
return this[key]
}
}
let key = privateDateStore('hello world')
privateDataStore[key] //=> undefined
privateDataStore.get(key) //=> hello world
如果你想通過某些辦法取得被隱藏的 key 的話,我只能說:理論上,不可能。
let obj = {}
let key = Symbol('key')
obj[key] = 1
JSON.stringify(obj) //=> {}
Object.keys(obj) //=> []
obj[key] //=> 1
黑科技
Symbol 除了帶給我們數(shù)據(jù)安全性以外,還帶來了一些很神奇的黑科技,簡直了。
Symbol.iterator
除 Symbol 以外,ES2015 還為 JavaScript 帶來了 for...of
語句,這個跟原本的 for...in
又有什么區(qū)別?
我們還是以前面的斐波那契數(shù)列作為例子。Iterator 在 Java 中經(jīng)常用到中會經(jīng)常用到,意為“迭代器”,你可以把它理解為用于循環(huán)的工具。
let fibo = {
[ Symbol.iterator ]() {
let a = 0
let b = 1
return {
next() {
[a, b] = [b, a + b]
return { done: false, value: b }
}
}
}
}
for (let n of fibo) {
if (n > 100) break
console.log(n)
}
Wow! 看到這個 for…of
是否有種興奮的感覺?雖然說創(chuàng)建 fibo
的時候稍微有點麻煩……
不如我們先來看看這個 fibo
究竟是怎么定義出來了。首先,我們要了解到 JavaScript 引擎(或編譯器)在處理 for...of
的時候,會從 of
后的對象取得 Symbol.iterator
這屬性鍵的值,為一個函數(shù)。它要求要返回一個包含 next
方法的對象,用于不斷迭代。而因為 Symbol.iterator
所在鍵值對的值是一個函數(shù),這就讓我們有了自由發(fā)揮的空間,比如定義局部變量等等。
每當 for...of
進行了一次循環(huán),都會執(zhí)行一次該對象的 next
方法,已得到下一個值,并檢查是否迭代完成。隨著 ES7 的開發(fā),for...of
所能發(fā)揮的潛能將會越來越強。
還有更多的 Symbol 黑科技等待挖掘,再次本文不作詳細闡述,如有興趣,可以看看 Mozilla Developer Center 上的介紹。
Proxy(代理)
Proxy 是 ECMAScript 中的一種新概念,它有很多好玩的用途,從基本的作用說就是:Proxy 可以在不入侵目標對象的情況下,對邏輯行為進行攔截和處理。
比如說我想記錄下我代碼中某些接口的使用情況,以供數(shù)據(jù)分析所用,但是因為目標代碼中是嚴格控制的,所以不能對其進行修改,而另外寫一個對象來對目標對象做代理也很麻煩。那么 Proxy 便可以提供一種比較簡單的方法來實現(xiàn)這一需求。
假設我要對 api
這一對象進行攔截并記錄下代碼行為,我就可以這樣做:
let apiProxy = new Proxy(api, {
get(receiver, name) {
return (function(...args) {
min.sadd(`log:${name}`, args)
return receiver[name].apply(receiver, args)
}).bind(receiver)
}
})
api.getComments(artical.id)
.then(comments => {
// ...
})
可惜的是,目前 Proxy 的兼容性很差,哪怕是降級兼容也難以實現(xiàn)。
到這里,相信你已經(jīng)對 ES2015 中的大部份新特性有所了解了。那么現(xiàn)在,就結(jié)合我們原有的 JavaScript 技能,開始使用 ES2015 構(gòu)建一個具有工程化特點的項目吧。
ES2015 的前端開發(fā)實戰(zhàn)
事實上,你們都應該有聽說過 React 這個來自 Facebook 的前端框架,因為現(xiàn)在它實在太火了。React 與 ES2015 的關(guān)系可謂深厚,React 在開發(fā)上便要求使用 ES2015 標準,因其 DSL ── JSX 的存在,所以必須要依賴 Babel 將其編譯成 JavaScript。
但同樣是由于 JSX 的存在,本文章并不會采用 React 作為前端框架,以避免讀者對 JSX 和 HTML 的誤解。我們會采用同樣優(yōu)秀的前端 MVVM 框架 ── Vue 進行開發(fā)。
數(shù)據(jù)部份,將會使用 MinDB 進行存儲和處理。MinDB 是由筆者開發(fā)的一個用于 JavaScript 環(huán)境的簡易而健壯的數(shù)據(jù)庫,它默認使用 DOM Storage 作為其存儲容器,在其他環(huán)境中可以通過更換 Store Interface 以兼容絕大部份 JavaScript 運行環(huán)境。
Vue.js 的使用教程可以參考 Vue.js 的官方教程。
構(gòu)建界面
我們首先簡單地用 LayoutIt 搭建一個用 Bootstrap 構(gòu)架的頁面,其中包含了 DEMO 的首頁和文章內(nèi)容頁,此后我們將會使用這個模板搭建我們的 JavaScript 代碼架構(gòu)。


接下來,我們需要通過對頁面的功能塊進行組件劃分,以便于使用組件化的架構(gòu)搭建前端頁面。
我們可以大致分為 Index、Post 和 Publish 三個頁面,也可以說三個路由方向;而我們還可以把頁面中的組塊分為:文章列表、文章、側(cè)邊欄、評論框等。
以此,我們可以設計出以下結(jié)構(gòu),以作為這個項目的組織結(jié)構(gòu):
Routes Components
|- Index ----|- Posts
| |- Sidebar
|
|- Post -----|- Post
| |- Comments
|
|- Publish
首頁包含了文章列表、側(cè)邊欄兩個組件;文章頁面包含文章內(nèi)容組件和評論框組件(此處我們使用多說評論框作為我們的組件);而文章發(fā)布頁則可以單獨為一個路由器,而不需要分出組件。
代碼結(jié)構(gòu)定義
因我們是以 babel 進行 ES2015 降級兼容的,所以我們最好可以采用分離的結(jié)構(gòu),這里我們使用 src
和 dist
。
我們此處以比較簡單的結(jié)構(gòu)構(gòu)建我們的DEMO:
app
|- src 程序的源文件目錄
| |- controllers 后端的路由處理器
| |- lib 后端需要引用的一些庫
| |- public 前端 JavaScript 源文件
| | |- controllers 前端的路由處理器
| | |- components 前端組件
| | |- models 前端數(shù)據(jù)層
| | |- config.js 前端的配置文件
| | |- main.js 前端 JavaScript 入口
| |- app.js 后端程序入口
| |- routes.js 后端路由表
|- dist 降級兼容輸出目錄
| |- public
| |- css
| |- index.html 前端 HTML 入口
|- gulpfile.js Gulp 構(gòu)建配置文件
|- package.json Node.js 項目配置文件
而我們在這一章節(jié)中則專注于 public
這一目錄即可,Node.js 部份將在下一章節(jié)詳細展示。
架構(gòu)設計
模塊化
因為有了 ES2015 自身的模塊化機制,我們就不必使用 RequireJS 等模塊加載庫了,通過 Browserify 我們我可以將整個前端的 JavaScript 程序打包到一個 .js 文件中,而這一步驟我們使用 Gulp 來完成。
詳細的 Gulp 使用教程可以參考:了不起的任務運行器Gulp基礎教程
數(shù)據(jù)支持
我們的數(shù)據(jù)將從后端的 Node.js 程序中獲取,以 API 的形式獲得。
另外,為了得到更佳的用戶體驗,我們將使用 MinDB 作為這個 DEMO 的前端數(shù)據(jù)庫,減少網(wǎng)絡請求次數(shù),優(yōu)化使用體驗。
界面渲染
為了能讓我們的界面定義能夠足夠簡單,我們在這使用了 Vue 作為前端開發(fā)框架,將其與 MinDB 對接,負責渲染我們的頁面。
其中,我們會利用 Vue 的組件系統(tǒng)來實現(xiàn)我們制定下的組件設計。另外,我們還會使用 watchman.js 來實現(xiàn)前端的路由器,以便我們對不同的頁面的邏輯進行分離。
構(gòu)建應用
在開始編寫業(yè)務代碼之前,我們需要先安裝好我們所需要的依賴庫。
$ npm install vue min watchman-router marked --save
安裝好依賴庫以后,我們就開始編寫入口文件吧!
入口文件
// main.js
import Vue from 'vue'
import watch from 'watchman-router'
import qs from 'querystring' // From Node.js
watch({
// TODO
})
.use((ctx, next) => {
ctx.query = qs.parse(window.location.search.substr(1))
next()
})
.run()
在入口中,我們將做如下任務:
- 引入所有的路由相應器
- 將路由相應器通過 watchman.js 綁定到對應的 url 規(guī)則中
- 建立在共用模板中存在的需要實例化的組件
因為我們還么有開始動工路由相應器,所以我們先把“建立在共用模板中存在的需要實例化的組件”這一任務完成。
在共用模板中,有唯一一個必須的共用組件就是頁面切換器 ── 一個用于包含所有的頁面的元素。
<div class="row" id="wrapper" v-html="html"></div>
對應的,我們將在入口文件中使用 Vue 建立其對應的 ViewModel,并將其綁定至 DOM 元素。
let layoutVM = new Vue({
el: '#wrapper',
data: {
html: ''
}
})
以后可以通過改變 layoutVM.$data.html
來改變 #wrapper
的內(nèi)容,配合 watchman.js 以加載不同的頁面內(nèi)容。
為了能在路由相應器中改變 layoutVM
的參數(shù),我們將其作為 watchman.js 給予相應器的上下文參數(shù)中的一個屬性。
// ...
.use((ctx, next) => {
ctx.query = qs.parse(window.location.search.substr(1))
ctx.layoutVM = layoutVM
next()
})
數(shù)據(jù)層:文章
我們單獨把文章的查詢、讀取和創(chuàng)建等操作抽象成一個庫,使其與邏輯層分離,讓代碼更美觀。
在此之前,我們先定義好從后端用于取得文章數(shù)據(jù)的 API:
- URL:
/api/posts/list
- 參數(shù):
我們可以直接用新的 Ajax API 來進行 API 訪問。
import 'whatwg-fetch'
import min from 'min'
async function listPosts(page = 0) {
const count = 10
// 檢查 MinDB 是否存在數(shù)據(jù)
let existsInMinDB = await min.exists('posts:id')
if (!existsInMinDB) {
var posts = (await _fetchPost(page))
.map(post => {
return {
id: post._id,
title: post.title,
content: post.content,
author: post.author,
comments: post.comments.length,
get summary() {
return post.content.substr(0, 20) + '...'
}
}
})
// 將數(shù)據(jù)存儲到 MinDB 中
for (let i = 0; i < posts.length; i++) {
let post = posts[i]
await min.sadd('posts:id', post.id)
await min.hmset(`post:${post.id}`, post)
}
} else {
// 從 MinDB 讀取數(shù)據(jù)
let ids = await min.smembers('posts:id')
ids = ids.slice(page * count, (page + 1) * count)
var posts = await min.mget(ids.map(id => `post:${id}`))
}
return posts
}
async function _fetchPost(page) {
// 通過 `fetch` 訪問 API
let res = await fetch(`/api/posts/list?page=${page}`)
let reply = await res.json()
return reply.posts
}
其中,min.sadd('posts:id', post.id)
會將文章的 ID 存入 MinDB 中名為 posts:id
的集中,這個集用于保存所有文章的 ID;min.hmset(`post:${post.id}`, post)
則會將文章的所有數(shù)據(jù)存入以 post:{id}
命名的 Hash 中。
完成對首頁的所有文章列表支持后,我們還需要簡單的定義一個用于讀取單篇文章數(shù)據(jù)的 API。
如果用戶是從首頁的文章列表進來的,那么我們就可以直接從 MinDB 中讀取文章數(shù)據(jù)。但如果用戶是直接通過 url 打開網(wǎng)址的話,MinDB 有可能并沒有存儲文章數(shù)據(jù),那么我們就通過 API 從后端獲取數(shù)據(jù),并存儲在 MinDB 中。
async function getPost(id) {
let existsInMinDB = await min.exists(`post:${id}`)
if (existsInMinDB) {
return await min.hgetall(`post:${id}`)
} else {
let res = await fetch(`/api/posts/${id}`)
let post = (await res.json()).post
await min.hmset(`post:${id}`, {
id: post._id,
title: post.title,
content: post.content,
author: post.author,
comments: post.comments.length,
get summary() {
return post.content.substr(0, 20) + '...'
}
})
return post
}
}
完成用于讀取的接口后,我們也該做做用于寫入的接口了。同樣的,我們也先來把 API 定義一下。
- URL:
/api/posts/new
- Body(JSON):
我們也可以通過 fetch
來發(fā)出 POST 請求。
async function publishPost(post) {
let res = await fetch('/api/posts/new', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(post)
})
var _post = await res.json()
await min.sadd('posts:id', _post._id)
await min.hmset(`post:${_post._id}`, {
id: _post._id,
title: _post.title,
content: _post.content,
author: _post.author,
comments: 0,
get summary() {
return _post.title.substr(0, 20) + '...'
}
})
_post.id = _post._id
return _post
}
最后我們就可以暴露出這幾個接口了。
export default {
listPosts,
getPost,
publishPost
}
路由:首頁
首先我們確定一下首頁中我們需要做些什么:
- 改變
layoutVM
的 HTML 數(shù)據(jù),為后面的頁面渲染做準備
- 分別從后端加載文章數(shù)據(jù)和多說加載評論數(shù),加載完以后存入 MinDB 中
- 從 MinDB 中加載已緩存的數(shù)據(jù)
- 建立對應的 VM,并傳入數(shù)據(jù)
準備頁面渲染
首先,我們需要為 watchman.js 的路由提供一個函數(shù)以作相應器,并包含一個為 context
的參數(shù)。我們則可以將其作為模塊的暴露值。
export default function(ctx) {
// 改變 layoutVM
ctx.layoutVM.$data.html = `
<h1>
ES2015 實戰(zhàn) - DEMO
</h1>
<hr>
<div id="posts" class="col-md-9">
<post-in-list v-repeat="posts"></post-in-list>
</div>
<div id="sidebar" class="col-md-3">
<panel title="側(cè)邊欄" content="{{content}}" list="{{list}}"></panel>
</div>
`
}
此處將首頁的 HTML 結(jié)構(gòu)賦予 layoutVM
,并讓其渲染至頁面中。
加載數(shù)據(jù)
因為我們之前已經(jīng)將數(shù)據(jù)層抽象化了,所以我們此處只需通過我們的抽象數(shù)據(jù)層讀取我們所需要的數(shù)據(jù)即可。
import Vue from 'vue'
import Posts from '../models/posts'
// ...
export default async function(ctx) {
let refresh = 'undefined' != typeof ctx.query.refresh
let page = ctx.query.page || 0
let posts = await Posts.listPosts(page)
}
設計組件
在首頁的 HTML 中,我們用到了兩個 Component,分別為 post-in-list
和 panel
,我們分別在 components
文件夾中分別建立 post-in-list.js
和 panel.js
。我們從我們之前通過 LayoutIt 簡單建立的 HTML 模板中,抽出對應的部份,并將其作為 Vue Component 的模板。
// post-in-list.js
import Vue from 'vue'
import marked from 'marked'
// 模板
const template = `
<div class="post" v-attr="id: id">
<h2><a href="/#!/post/{{id}}" v-text="title"></a></h2>
<p v-text="summary"></p>
<p>
<small>由 {{author}} 發(fā)表</small> | <a class="btn" href="/#!/post/{{id}}">查看更多 ?</a>
</p>
</div>
`
我們可以通過 Vue 的雙向綁定機制將數(shù)據(jù)插入到模板中。
Vue.component('post-in-list', {
template: template,
replace: true
})
根據(jù) Vue 的組件機制,我們可以通過對組件標簽中加入自定義屬性來傳入?yún)?shù),以供組件中使用。
但此處我們先用 Vue 的 v-repeat
指令來進行循環(huán)使用組件。
<post-in-list v-repeat="posts"></post-in-list>
panel.js
同理建立。
路由:文章頁面
相比首頁,文章頁面要簡單得多。因為我們在首頁已經(jīng)將數(shù)據(jù)加載到 MinDB 中了,所以我們可以直接從 MinDB 中讀取數(shù)據(jù),然后將其渲染到頁面中。
// ...
let post = await min.hgetall(`post:${this.id}`)
// ...
然后,我們再從之前設計好的頁面模板中,抽出我們需要用來作為文章頁面的內(nèi)容頁。
<h1 v-text="title"></h1>
<small>由 {{author}} 發(fā)表</small>
<div class="post" v-html="content | marked"></div>
我們同樣是同樣通過對 layoutVM
的操作,來準備頁面的渲染。在完成渲染準備后,我們就可以開始獲取數(shù)據(jù)了。
let post = await min.hgetall(`post:${this.id}`)
在獲得相應的文章數(shù)據(jù)以后,我們就可以通過建立一個組件來將其渲染至頁面中。其中,要注意的是我們需要通過 Vue 的一些 API 來整合數(shù)據(jù)、渲染等步驟。
在這我不再詳細說明其構(gòu)建步驟,與上一小節(jié)相同。
import Vue from 'vue'
import min from 'min'
import marked from 'marked'
const template = `
<h1 v-text="title"></h1>
<small>由 {{author}} 發(fā)表</small>
<div class="post" v-html="content | marked"></div>
`
let postVm = Vue.component('post', {
template: template,
replace: true,
props: [ 'id' ],
data() {
return {
id: this.id,
content: '',
title: ''
}
},
async created() {
this.$data = await min.hgetall(`post:${this.id}`)
},
filters: {
marked
}
})
export default postVm
此處我們除了 ES2015 的特性外,我們還更超前地使用了正在制定中的 ES7 的特性,比如 async/await
,這是一種用于對異步操作進行“打扁”的特性,它可以把異步操作以同步的語法編寫。如上文所說,在 ES2015 中,我們可以用 co 來模擬 async/await
特性。
路由:發(fā)布新文章
在發(fā)布新文章的頁面中,我們直接調(diào)用我們之前建立好的數(shù)據(jù)抽象層的接口,將新數(shù)據(jù)傳向后端,并保存在 MinDB 中。
import marked from 'marked'
import Posts from '../models/posts'
// ...
new Vue({
el: '#new-post',
data: {
title: '',
content: '',
author: ''
},
methods: {
async submit(e) {
e.preventDefault()
var post = await Posts.publishPost({
title: this.$data.title,
content: this.$data.content,
author: this.$data.author
})
window.location.hash = `#!/post/${post.id}`
}
},
filters: {
marked
}
})
路由綁定
在完成路由響應器的開發(fā)后,我們就可以把他們都綁定到 watchman.js 上了。
// ...
import Index from './controllers/index'
import Post from './controllers/post'
import Publish from './controllers/publish'
watch({
'/': Index,
'#!/': Index,
'#!/post/:id': Post,
'#!/new': Publish
})
// ...
這樣,就可以讓我們之前的路由結(jié)構(gòu)都綁定到入口文件中:
- 首頁綁定到
/
和 #!/
- 文章頁面則綁定到了
#!/post/:id
,比如 #!/post/123
則表示 id 為 123 的文章頁面
#!/new
則綁定了新建文章的頁面
合并代碼
在完成三個我們所構(gòu)建的路由設計后,我們就可以用 Browserify 把我們的代碼打包到一個文件中,以作為整個項目的入口文件。此處,我們再引入 Gulp 作為我們的構(gòu)建輔助器,而不需要直接使用 Browserify 的命令行進行構(gòu)建。
在開始編寫 Gulpfile 之前,我們先安裝我們所需要的依賴庫:
$ npm install gulp browserify babelify vinyl-source-stream vinyl-buffer babel-preset-es2015-without-regenerator babel-plugin-transform-async-to-generator --save
將依賴庫安裝好以后,我們就可以開始編寫 Gulp 的配置文件了。
在本文將近完成的時候,Babel 發(fā)布了版本 6,其 API 與 5 版本有著相當大的區(qū)別,且 Babel 6 并不向前兼容,詳細更改此處不作介紹。
var gulp = require('gulp')
var browserify = require('browserify')
var babelify = require('babelify')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
gulp.task('browserify', function() {
return browserify({
entries: ['./src/public/main.js']
})
.transform(babelify.configure({
presets: [ 'es2015-without-regenerator' ],
plugins: [ 'transform-async-to-generator' ]
}))
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(gulp.dest('dist/public'))
})
gulp.task('default', [ 'browserify' ])
在這個配置文件中,我們把前端的代碼中的入口文件傳入 babel 中,然后將其打包成 bundle.js。
最后,我們可以在我們最開始通過 Layoutit 所設計的頁面中,把可以用于包含替換內(nèi)容的部份去掉,然后引入我們通過 Browserify 生成的 bundle.js。
完成 JavaScript 部份的開發(fā)后,我們再將所需要的靜態(tài)資源文件加載到 HTML 中,我們就可以看到這個基本脫離后端的前端 Web App 的效果了。

ES2015 的 Node.js 開發(fā)實戰(zhàn)
就目前來說,能最痛快地使用 ES2015 中各種新特性進行 JavaScript 開發(fā)的環(huán)境,無疑就是 Node.js。就 Node.js 本身來說,就跟前端的 JavaScript 環(huán)境有著本質(zhì)上的區(qū)別,Node.js 有著完整意義上的異步 IO 機制, 有著無窮無盡的應用領域,而且在語法角度上遇到問題機率比在前端大不少。甚至可以說,Node.js 一直是等著 ES2015 的到來的,Node.js 加上 ES2015 簡直就是如虎添翼了。
從 V8 引擎開始實驗性的開始兼容 ES6 代碼時,Node.js 便開始馬上跟進,在 Node.js 中開放 ES6 的兼容選項,如 Generator、Classes 等等。經(jīng)過相當長一段時間的測試和磨合后,就以在 Node.js 上使用 ES6 標準進行應用開發(fā)這件事來說,已經(jīng)變得越來越成熟,越來越多的開發(fā)者走上了 ES6 這條“不歸路”。
一些針對 Node.js + ES6 的開發(fā)模式和第三方庫也如雨后春筍般冒出,其中最為人所熟知的便是以 co 為基礎所建立的 Web 框架 Koa。Koa 由 TJ 等 express 原班人馬打造,目前也有越來越多的中國開發(fā)者加入到 Koa 的開發(fā)團隊中來,為前沿 Node.js 的開發(fā)做貢獻。
co 通過使用 ES2015 中的 Generator 特性來模擬 ES7 中相當誘人的 async/await
特性,可以讓復雜的異步方法調(diào)用及處理變得像同步操作一樣簡單。引用響馬大叔在他所維護的某項目中的一句話:
用同步代碼抒發(fā)異步情懷
在本章節(jié)中,我將以一個簡單的后端架構(gòu)體系,來介紹 ES2015 在 Node.js 開發(fā)中的基于 Koa 的一種優(yōu)秀實踐方式。
因為 Node.js 自帶模塊機制,所以用 babel 對 ES2015 的模塊語法做降級兼容的時候,只需降至 Node.js 所使用的 CommonJS 標準即可。
不一樣的是,ES2015 的模塊語法是一種聲明式語法,根據(jù) ES2015 中的規(guī)定,模塊引入和暴露都需要在當前文件中的最頂層,而不能像 CommonJS 中的 require()
那樣可以在任何地方使用;使用 import
引入的模塊所在命名空間將會是一個常量,在 babel 的降級兼容中會進行代碼檢查,保證模塊命名空間的安全性。
架構(gòu)設計
因為我們這個 DEMO 的數(shù)據(jù)結(jié)構(gòu)并不復雜,所以我們可以直接使用 MongoDB 作為我們的后端數(shù)據(jù) 庫,用于存儲我們的文章數(shù)據(jù)。我們可以通過 monk
庫作為我們讀取、操作 MongoDB 的客戶端庫。在架構(gòu)上,Koa 將作為 Web 開發(fā)框架,配合 co 等庫實現(xiàn)全“同步”的代碼編寫方式。
構(gòu)建應用
這就讓我們一步一步來吧,創(chuàng)建 Node.js 應用并安裝依賴。
$ npm init
$ npm install koa koa-middlewares koa-static monk co-monk thunkify --save
在上一個章節(jié)中,我們已經(jīng)建立了整個 DEMO 的文件結(jié)構(gòu),在此我們再展示一遍:
app
|- src 程序的源文件目錄
| |- controllers 后端的路由處理器
| |- models 數(shù)據(jù)抽象層
| |- lib 后端需要引用的一些庫
| |- public 前端 JavaScript 源文件
| |- app.js 后端程序入口
| |- routes.js 后端路由表
|- dist 降級兼容輸出目錄
|- gulpfile.js Gulp 構(gòu)建配置文件
|- package.json Node.js 項目配置文件
在 src/controllers
中,包含我們用來相應請求的控制器文件;src/lib
文件夾則包含了我們需要的一些抽象庫;src/public
文件則包含了在上一章節(jié)中我們建立的前端應用程序;src/app.js
是該應用入口文件的源文件;src/routes.js
則是應用的路由表文件。
入口文件
在入口文件中,我們需要完成幾件事情:
- 創(chuàng)建 Koa 應用,并監(jiān)聽指定端口
- 將所需要的 Koa 中間件接入我們所創(chuàng)建的 Koa 應用中,如靜態(tài)資源服務
- 引入路由表文件,將路由控制器接入我們所建立的 Koa 文件中
import koa from 'koa'
import path from 'path'
import { bodyParser } from 'koa-middlewares'
import Static from 'koa-static'
import router from './routes'
let app = koa()
// Static
app.use(Static(path.resolve(__dirname, './public')))
// Parse the body in POST requests
app.use(bodyParser())
// Router
app.use(router.routes())
let PORT = parseInt(process.env.PORT || 3000)
app.listen(PORT, () => {
console.log(`Demo is running, port:${PORT}`)
})
數(shù)據(jù)抽象層
為了方便我們在 co 的環(huán)境中使用 MongoDB,我們選擇了 monk
和 co-monk
兩個庫進行組合,并抽象出鏈接庫。
// lib/mongo.js
import monk from 'monk'
import wrap from 'co-monk'
import config from '../../config.json'
const db = monk(config.dbs.mongo)
/**
* 返回 MongoDB 中的 Collection 實例
*
* @param {String} name collection name
* @return {Object} Collection
*/
function collection(name) {
return wrap(db.get(name))
}
export default {
collection
}
通過這個抽象庫,我們就可以避免每次獲取 MongoDB 中的 Collection 實例時都需要連接一遍數(shù)據(jù)庫了。
// models/posts.js
import mongo from '../lib/mongo'
export default mongo.collection('posts')
Posts 控制器
完成了數(shù)據(jù)層的抽象處理后,我們就可以將其用于我們的控制器了,參考 monk
的文檔,我們可以對這個 Posts Collection 進行我們所需要的操作。
import thunkify from 'thunkify'
import request from 'request'
import Posts from '../models/posts'
const requestAsync = thunkify((opts, callback) => {
request(opts, (err, res, body) => callback(err, body))
})
此處我們使用 thunkify
對 request
做了一點小小的封裝工作,而因為 request
庫自身的 callback 并不是標準的 callback 形式,所以我們并不能直接把 request
函數(shù)傳入 thunkify
中,我們需要的是 callback 中的第三個參數(shù) body
,所以我們需要自行包裝一層函數(shù)以取得 body
并返回到 co 中。
API:獲取所有文章
我們可以通過這個 API 獲取存儲在 MongoDB 中的所有文章,并支持分頁。支持提供當前獲取的頁數(shù),每頁10篇文章,提供每篇文章的標題、作者、文章內(nèi)容和評論。
// GET /api/posts/list?page=0
router.get.listPosts = function*() {
let page = parseInt(this.query.page || 0)
const count = 10
let posts = yield Posts.find({}, {
skip: page * count,
limit: count
})
// 從多說獲取評論
posts = yield posts.map(post => {
return function*() {
let duoshuoReply = JSON.parse(yield requestAsync(`http://api./threads/listPosts.json?short_name=es2015-in-action&thread_key=${post._id}&page=0&limit=1000`))
var commentsId = Object.keys(duoshuoReply.parentPosts)
post.comments = commentsId.map(id => duoshuoReply.parentPosts[id])
return post
}
})
// 返回結(jié)果
this.body = {
posts: posts
}
}
此處我們用到了 co 的一個很有意思的特性,并行處理異步操作。我們通過對從 MongoDB 中取得數(shù)據(jù)進行 #map()
方法的操作,返回一組 Generator Functions,并將這個數(shù)組傳給 yield
,co 便可以將這些 Generator Functions 全部執(zhí)行,并統(tǒng)一返回結(jié)果。同樣的我們還可以使用對象來進行類似的操作:
co(function*() {
let result = yield {
posts: getPostsAsync(),
hot: getHotPosts(),
latestComments: getComments(10)
}
result //=> { posts: [...], hot: [...], latestComments: [...] }
})
API:獲取指定文章
這個 API 用于通過提供指定文章的 ID,返回文章的數(shù)據(jù)。
// GET /api/posts/:id
router.get.getPost = function*() {
let id = this.params.id
let post = yield Posts.findById(id)
let duoshuoReply = JSON.parse(yield requestAsync(`http://api./threads/listPosts.json?short_name=es2015-in-action&thread_key=${id}&page=0&limit=1000`))
var commentsId = Object.keys(duoshuoReply.parentPosts)
post.comments = commentsId.map(id => duoshuoReply.parentPosts[id])
this.body = {
post: post
}
}
API:發(fā)布新文章
相比上面兩個 API,發(fā)布新文章的 API 在邏輯上則要簡單得多,只需要向 Collection 內(nèi)插入新元素,然后將得到的文檔返回至客戶端既可以。
// POST /api/posts/new
router.post.newPost = function*() {
let data = this.request.body
let post = yield posts.insert(data)
this.body = {
post: post
}
}
Comments 控制器
除了文章的 API 以外,我們還需要提供文章評論的 API,以方便日后該 DEMO 向移動端擴展和彌補多說評論框在原生移動端上的不足。由于評論的數(shù)據(jù)并不是存儲在項目數(shù)據(jù)庫當中,所以我們也不需要為它創(chuàng)建一個數(shù)據(jù)抽象層文件,而是直接從控制器入手。
API:獲取指定文章的評論
// GET /api/comments/post/:id
router.get.fetchCommentsInPost = function*() {
let postId = this.params.id
let duoshuoReply = JSON.parse(yield requestAsync(`http://api./threads/listPosts.json?short_name=es2015-in-action&thread_key=${postId}&page=0&limit=1000`))
let commentsId = Object.keys(duoshuoReply.parentPosts)
let comments = commentsId.map(id => duoshuoReply.parentPosts[id])
this.body = {
comments: comments
}
}
API:發(fā)表新評論
同樣是為了擴展系統(tǒng)的 API,我們通過多說的 API,允許使用 API 來向文章發(fā)表評論。
// POST /api/comments/post
router.post.postComment = function*() {
let postId = this.request.body.post_id
let message = this.request.body.message
let reply = yield requestAsync({
method: 'POST',
url: `http://api./posts/create.json`,
json: true,
body: {
short_name: duoshuo.short_name,
secret: duoshuo.secret,
thread_key: postId,
message: message
}
})
this.body = {
comment: reply.response
}
}
配置路由
完成控制器的開發(fā)后,我們是時候把路由器跟控制器都連接起來了,我們在 src/routes.js
中會將所有控制器都綁定到路由上,成為一個類似于路由表的文件。
import { router as Router } from 'koa-middlewares'
首先,我們要將所有的控制器引入到路由文件中來。
import posts from './controllers/posts'
import comments from './controllers/comments'
然后,創(chuàng)建一個路由器實例,并將所有控制器的響應器和 URL 規(guī)則一一綁定。
let router = new Router()
// Posts
router.get('/api/posts/list', posts.get.listPosts)
router.get('/api/posts/:id', posts.get.getPost)
router.post('/api/posts/new', posts.post.newPost)
// Comments
router.get('/api/comments/post/:id', comments.get.fetchCommentsInPost)
router.post('/api/comments/post', comments.post.postComment)
export default router
配置任務文件
經(jīng)過對數(shù)據(jù)抽象層、邏輯控制器、路由器的開發(fā)后,我們便可以將所有代碼利用 Gulp 進行代碼構(gòu)建了。
我們先安裝好所需要的依賴庫。
$ npm install gulp gulp-babel vinyl-buffer vinyl-source-stream babelify browserify --save-dev
$ touch gulpfile.js
很可惜的是,Gulp 原生并不支持 ES2015 標準的代碼,所以在此我們也只能通過 ES5 標準的代碼編寫任務文件了。
'use strict'
var gulp = require('gulp')
var browserify = require('browserify')
var babel = require('gulp-babel')
var babelify = require('babelify')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
var babel = require('gulp-babel')
我們主要需要完成兩個構(gòu)建任務:
- 編譯并構(gòu)建前端 JavaScript 文件
- 編譯后端 JavaScript 文件
在構(gòu)建前端 JavaScript 文件的過程中,我們需要利用 Browserify 配合 babelify 進行代碼編譯和合并。
gulp.task('browserify', function() {
return browserify({
cache: {},
packageCache: {},
entries: ['./src/public/main.js']
})
.transform(babelify.configure({
presets: [ 'es2015-without-regenerator' ],
plugins: [ 'transform-async-to-generator' ]
}))
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(gulp.dest('dist/public'))
})
我們將代碼編譯好以后,便將其復制到 dist/public
文件夾中,這也是我們 Node.js 后端處理靜態(tài)資源請求的響應地址。
而構(gòu)建后端代碼則更為簡單,因為我們只需要將其編譯并復制到 dist
文件夾即可。
gulp.task('babel-complie', function() {
return gulp.src('src/**/*.js')
.pipe(babel({
presets: [ 'es2015-without-regenerator' ],
plugins: [ 'transform-async-to-generator' ]
}))
.pipe(gulp.dest('dist/'))
})
好了,在完成 gulpfile.js 文件的編寫以后,我們就可以進行代碼構(gòu)建了。
$ gulp
[07:30:27] Using gulpfile ~/path/to/app/gulpfile.js
[07:30:27] Starting 'babel-complie'...
[07:30:27] Starting 'browserify'...
[07:30:29] Finished 'babel-complie' after 2.35 s
[07:30:30] Finished 'browserify' after 3.28 s
[07:30:30] Starting 'default'...
[07:30:30] Finished 'default' after 29 μs
最后,我們就可以利用 dist
文件夾中已經(jīng)編譯好的代碼運行起來了。
$ node dist/app.js
Demo is running, port:3000
不出意外,我們就可以看到我們想要的效果了。
前方高能反應
部署到 DaoCloud
完成了代碼的開發(fā) ,我們還需要將我們的項目部署到線上,讓別人看到我們的成果~
Docker 是目前最流行的一種容器化應用搭建工具,我們可以通過 Docker 快速地將我們的應用部署在任何支持 Docker 的地方,哪怕是 Raspberry Pi 還是公司的主機上。
而 DaoCloud 則是目前國內(nèi)做 Docker 商業(yè)化體驗最好的公司,他們提供了一系列幫助開發(fā)者和企業(yè)快速使用 Docker 進行項目部署的工具。這里我們將介紹如何將我們的 DEMO 部署到 DaoCloud 的簡易容器中。
Dockerfile
在使用 Docker 進行項目部署前,我們需要在項目中新建一個 Dockerfile 以表達我們的鏡像構(gòu)建任務。
因為我們用的是 Node.js 作為項目基礎運行環(huán)境,所以我們需要從 Node.js 的官方 Docker 鏡像中引用過來。
FROM node:onbuild
因為我們已經(jīng)在 package.json
中寫好了對 gulp
的依賴,這樣我們就可以直接在 docker build
的時候?qū)Υa進行編譯。
RUN ./node_modules/.bin/gulp
此外,我們還需要安裝另外一個依賴庫 pm2
,我們需要使用 pm2
作為我們項目的守護程序。
$ npm install pm2 --save-dev
然后,我們簡單地向 Docker 的宿主機器申請暴露 80 端口,并利用 pm2
啟動 Node.js 程序。
EXPOSE 80
CMD ./node_modules/.bin/pm2 start dist/app.js --name ES2015-In-Action --no-daemon
至此,我們已經(jīng)完成了 Dockerfile 的編寫,接下來就可以將項目代碼上傳到 GitHub 等地方,供 DaoCloud 使用了。這里我不再說明 Git 和 GitHub 的使用。

創(chuàng)建 DaoCloud 上的 MongoDB 服務
借助于 Docker 的強大擴容性,我們可以在 DaoCloud 上很方便地創(chuàng)建用于項目的 MongoDB 服務。
在“服務集成”標簽頁中,我們可以選擇部署一個 MongoDB 服務。



創(chuàng)建好 MongoDB 服務后,我們就要將我們上傳到 GitHub 的項目代碼利用 DaoCloud 進行鏡像構(gòu)建了。
代碼構(gòu)建
DaoCloud 提供了一個十分方便的工具,可以把我們存儲在 GitHub、BitBucket、GitCafe、Coding 等地方的項目代碼,通過其中的 Dockerfile 構(gòu)建成一個 Docker 鏡像,用于部署到管理在 DaoCloud 上的容器。

通過綁定 GitHub 賬號,我們可以選擇之前發(fā)布在 GitHub 上的項目,然后拉取到 DaoCloud 中。


構(gòu)建完成以后,我們就可以在“鏡像倉庫”中看到我們的項目鏡像了。

我們將其部署到一個 DaoCloud 的容器中,并且把它與之前創(chuàng)建的 MongoDB 服務綁定。


最后的最后,我們點擊“立即部署”,等待部署成功就可以看到我們的項目線上狀態(tài)了。

我們可以在這里看到我部署的 DEMO:http://es2015-demo./
而我們的 DEMO,可以在這里查看詳細代碼:https://github.com/iwillwen/es2015-demo
至此,我們已經(jīng)完成了 Node.js 端和前端的構(gòu)建,并且將其部署到了 DaoCloud 上,以供瀏覽。下面,我們再來看看,正在發(fā)展中的 ES7 又能給我們帶來什么驚喜吧。
一窺 ES7
async/await
上文中我們提及到 co 是一個利用 Generator 模擬 ES7 中 async/await
特性的工具,那么,這個 async/await
究竟又是什么呢?它跟 co 又有什么區(qū)別呢?
我們知道,Generator Function 與普通的 Function 在執(zhí)行方式上有著本質(zhì)的區(qū)別,在某種意義上是無法共同使用的。但是,對于 ES7 的 Async Function 來說,這一點并不存在!它可以以普通函數(shù)的執(zhí)行方式使用,并且有著 Generator Function 的異步優(yōu)越性,它甚至可以作為事件響應函數(shù)使用。
async function fetchData() {
let res = await fetch('/api/fetch/data')
let reply = await res.json()
return reply
}
var reply = fetchData() //=> DATA...
遺憾的是,async/await
所支持的并不如 co 多,如并行執(zhí)行等都暫時沒有得到支持。
Decorators
對于 JavaScript 開發(fā)者來說,Decorators 又是一種新的概念,不過它在 Python 等語言中早已被玩出各種花式。
Decorator 的定義如下:
- 是一個表達式
- Decorator 會調(diào)用一個對應的函數(shù)
- 調(diào)用的函數(shù)中可以包含
target
(裝飾的目標對象)、name
(裝飾目標的名稱)和 descriptor
(描述器)三個參數(shù)
- 調(diào)用的函數(shù)可以返回一個新的描述器以應用到裝飾目標對象上
PS:如果你不記得 descriptor
是什么的話,請回顧一下 Object.defineProperty()
方法。
簡單實例
我們在實現(xiàn)一個類的時候,有的屬性并不想被 for..in
或 Object.keys()
等方法檢索到,那么在 ES5 時代,我們會用到 Object.defineProperty()
方法來實現(xiàn):
var obj = {
foo: 1
}
Object.defineProperty(obj, 'bar', {
enumerable: false,
value: 2
})
console.log(obj.bar) //=> 2
var keys = []
for (var key in obj)
keys.push(key)
console.log(keys) //=> [ 'foo' ]
console.log(Object.keys(obj)) //=> [ 'foo' ]
那么在 ES7 中,我們可以用 Decorator 來很簡單地實現(xiàn)這個需求:
class Obj {
constructor() {
this.foo = 1
}
@nonenumerable
get bar() { return 2 }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false
return descriptor
}
var obj = new Obj()
console.log(obj.foo) //=> 1
console.log(obj.bar) //=> 2
console.log(Object.keys(obj)) //=> [ 'foo' ]
黑科技
正如上面所說,Decorator 在編程中早已不是什么新東西,特別是在 Python 中早已被玩出各種花樣。聰明的工程師們看到 ES7 的支持當然不會就此收手,就讓我們看看我們還能用 Decorator 做點什么神奇的事情。
假如我們要實現(xiàn)一個類似于 Koa 和 PHP 中的 CI 的框架,且利用 Decorator 特性實現(xiàn) URL 路由,我們可以這樣做。
// 框架內(nèi)部
// 控制器
class Controller {
// ...
}
var handlers = new WeakMap()
var urls = {}
// 定義控制器
@route('/')
class HelloController extends Controller {
constructor() {
super()
this.msg = 'World'
}
async GET(ctx) {
ctx.body = `Hello ${this.msg}`
}
}
// Router Decorator
function route(url) {
return target => {
target.url = url
let urlObject = new String(url)
urls[url] = urlObject
handlers.set(urlObject, target)
}
}
// 路由執(zhí)行部份
function router(url) {
if (urls[url]) {
var handlerClass = handlers.get(urls[url])
return new handlerClass()
}
}
var handler = router('/')
if (handler) {
let context = {}
handler.GET(context)
console.log(context.body)
}
最重要的是,同一個修飾對象是可以同時使用多個修飾器的,所以說我們還可以用修飾器實現(xiàn)很多很多有意思的功能。
后記
對于一個普通的 JavaScript 開發(fā)者來說,ES2015 可能會讓人覺得很模糊和難以學習,因為 ES2015 中帶來了許多我們從前沒有在 JavaScript 中接觸過的概念和特性。但是經(jīng)過長時間的考察,我們不難發(fā)現(xiàn) ES2015 始終是 JavaScript 的發(fā)展方向,這是不可避免的。因此我要在很長一段時間內(nèi)都向身邊的或是社區(qū)中的 JavaScript 開發(fā)者推廣 ES2015,推薦他們使用最新的技術(shù)。
這篇文章說長不長,說短也不短,我只能在有限的文字篇幅內(nèi)盡可能把更多的知識展示出來,更深入的細節(jié)還需要讀者自行探索。無論如何,若是這篇文章能引起各位 JavaScript 開發(fā)者對 ES2015 的興趣和重視,并且中從學會了如何在項目中使用 ES2015 標準進行開發(fā),那么這篇文章的目的就已經(jīng)達到了。
再次感謝對這篇文章的寫作提供了支持的各位(名次均不分先后):
審讀大牛團:代碼家, 樸靈, 寒冬winter, TooBug, 郭達峰, 芋頭, 尤雨溪, 張云龍, 民工精髓V
內(nèi)測讀者:死月, 米粽, 陰明, 引證, 老雷, Jonah, Crzidea, 送送
贊助方:DaoCloud, 100Offer
PS:我廠準備在100offer招人了!聽說技術(shù)牛人都上他們網(wǎng)站找工作!
關(guān)于作者
甘超陽(小問),LikMoon(離門創(chuàng)造)創(chuàng)始人
GitHub: Will Wen Gunn
微博:端木文_Wen
Twitter:@iwillwen
Facebook:Will Wen Gunn
博客地址:Life Map
歡迎大家加入我的文章陪審團,以后有新的文章都可以率先閱讀哦~