給 JavaScript 初心者的 ES2015 實戰
給 JavaScript 初心者的 ES2015 實戰
前言
歷時將近6年的時間來制定的新 ECMAScript 標準 ECMAScript 6(亦稱 ECMAScript Harmony,簡稱 ES6)終于在 2015 年 6 月正式發布。自從上一個標準版本 ES5 在 2009 年發布以后,ES6 就一直以 新語法 、 新特性 的優越性吸引著眾多 JavaScript 開發者,驅使他們積極嘗鮮。
雖然至今各大瀏覽器廠商所開發的 JavaScript 引擎都還沒有完成對 ES2015 中所有特性的完美支持,但這并不能阻擋工程師們對 ES6 的熱情,于是乎如 babel 、 Traceur 等編譯器便出現了。它們能將尚未得到支持的 ES2015 特性轉換為 ES5 標準的代碼,使其得到瀏覽器的支持。其中,babel 因其 模塊化轉換器(Transformer) 的設計特點贏得了絕大部份 JavaScript 開發者的青睞,本文也將以 babel 為基礎工具,向大家展示 ES2015 的神奇魅力。
筆者目前所負責的項目中,已經在前端和后端全方位的使用了 ES2015 標準進行 JavaScript 開發,已有將近兩年的 ES2015 開發經驗。如今 ES2015 以成為 ECMA 國際委員會的首要語言標準,使用 ES2015 標準所進行的工程開發已打好了堅實的基礎,而 ES7(ES2016) 的定制也走上了正軌,所以在這個如此恰當的時機,我覺得應該寫一篇通俗易懂的 ES2015 教程來引導廣大 JavaScript 愛好者和工程師向新時代前進。若您能從本文中有所收獲,便是對我最大的鼓勵。
我希望你在閱讀本文前,已經掌握了 JavaScript 的基本知識,并具有一定的 Web App 開發基礎和 Node.js 基本使用經驗。
目錄
-
一言蔽之 ES2015
-
ES2015 能為 JavaScript 的開發帶來什么
- 語法糖
- 工程優勢
-
ES2015 新語法詳解
-
let、const和塊級作用域
- 塊級作用域
-
箭頭函數(Arrow Function)
- 使用方法
- 箭頭函數與上下文綁定
- 注意事項
-
模板字符串
- 支持變量注入
- 支持換行
-
對象字面量擴展語法
- 方法屬性省略function
- 支持注入__proto__
- 同名方法屬性省略語法
- 可以動態計算的屬性名稱
-
表達式解構
-
函數參數表達、傳參
-
默認參數值
-
后續參數
- 注意事項
-
解構傳參
-
注意事項
-
-
新的數據結構
- Set 和 WeakSet
- Map 和 WeakMap
-
類(Classes)
-
語法
- 定義
- 繼承
- 靜態方法
-
遺憾與期望
-
-
生成器(Generator)
-
來龍
-
基本概念
- Generator Function
- Generator
-
基本使用方法
-
-
Promise
- 概念
- 基本用法
- 弊端
-
原生的模塊化
-
歷史小回顧
-
基本用法
- 全局引入
- 局部引入
- 接口暴露
-
降級兼容
-
-
Symbol
- 黑科技
-
Proxy(代理)
-
-
ES2015 的前端開發實戰
-
構建界面
-
結構定義
-
架構設計
- 模塊化
- 數據支持
- 界面渲染
-
構建應用
-
入口文件
-
數據層:文章
-
路由:首頁
- 準備頁面渲染
- 加載數據
- 設計組件
-
路由:文章頁面
-
路由:發布新文章
-
-
路由綁定
-
合并代碼
-
-
ES2015 的 Node.js 開發實戰
-
架構設計
-
構建應用
-
入口文件
-
數據抽象層
-
Posts 控制器
- API:獲取所有文章
- API:獲取指定文章
- API:發布新文章
-
Comments 控制器
- API:獲取指定文章的評論
- API:發表新評論
-
配置路由
-
-
配置任務文件
-
部署到 DaoCloud
- Dockerfile
- 創建 DaoCloud 上的 MongoDB 服務
- 代碼構建
-
-
一窺 ES7
-
async/await
-
Decorators
- 簡單實例
- 黑科技
-
-
后記
本文的實戰部份將以開發一個動態博客系統為背景,向大家展示如何使用 ES2015 進行項目開發。成品代碼將在 GitHub 上展示。
一言蔽之 ES2015
說到 ES2015,有了解過的同學一定會馬上想到各種新語法,如箭頭函數(=>)、class、模板字符串等。是的,ECMA 委員會吸取了許多來自全球眾多 JavaScript 開發者的意見和來自其他優秀編程語言的經驗,致力于制定出一個更適合現代 JavaScript 開發的標準,以達到“和諧”(Harmony)。一言蔽之:
ES2015 標準提供了許多新的語法和編程特性以提高 JavaScript 的開發效率和體驗
從 ES6 的別名被定為 Harmony 開始,就注定了這個新的語言標準將以一種更優雅的姿態展現出來,以適應日趨復雜的應用開發需求。
ES2015 能為 JavaScript 的開發帶來什么
語法糖
如果您有其他語言(如 Ruby 、 Scala )或是某些 JavaScript 的衍生語言(如 CoffeeScript 、 TypeScript )的開發經驗,就一定會了解一些很有意思的 語法糖 ,如 Ruby 中的Range -> 1..10,Scala 和 CoffeeScript 中的箭頭函數(a, b) => a + b。ECMA 委員會 借鑒 了許多其他編程語言的標準,給 ECMAScript 家族帶來了許多可用性非常高的語法糖,下文將會一一講解。
這些語法糖能讓 JavaScript 開發者更舒心地開發 JavaScript 應用,提高我們的工作效率~~,多一些時間出去浪~~。
工程優勢
ES2015 除了提供了許多語法糖以外,還由官方解決了多年來困擾眾多 JavaScript 開發者的問題:JavaScript 的模塊化構建。從許多年前開始,各大公司、團隊、大牛都相繼給出了他們對于這個問題的不同解決方案,以至于定下了如 CommonJS、AMD、CMD 或是 UMD 等 JavaScript 模塊化標準, RequireJS 、 SeaJS 、 FIS 、 Browserify 、 webpack 等模塊加載庫都以各自不同的優勢占領著一方土地。
然而正正是因為這春秋戰國般的現狀,廣大的前端搬磚工們表示很納悶。
這?究竟哪種好?哪種適合我?求大神帶我飛!
對此,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 有什么關系?~~
可惜的是,目前暫時還沒有任何瀏覽器廠商或是 JavaScript 引擎支持這種模塊化語法。所以我們需要用 babel 進行轉換為 CommonJS、AMD 或是 UMD 等模塊化標準的語法。
ES2015 新語法詳解
經過以上的介(xun)紹(tao),相信你對 ES2015 也有了一定的了解和期待。接下來我將帶大家慢慢看看 ECMA 委員會含辛茹苦制定的新語言特性吧。
let、const和塊級作用域
在 ES2015 的新語法中,影響速度最為直接,范圍最大的,恐怕得數let和const了,它們是繼var之后,新的變量定義方法。與let相比,const更容易被理解:const也就是 constant 的縮寫,跟 C/C++ 等經典語言一樣,用于定義常量,即不可變量。
但由于在 ES6 之前的 ECMAScript 標準中,并沒有原生的實現,所以在降級編譯中,會馬上進行引用檢查,然后使用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 新手解答困惑時,經常會提到一個觀點:
JavaScript 沒有塊級作用域
在 ES6 誕生之前的時代中,JavaScript 確實是沒有塊級作用域的。這個問題之所以為人所熟知,是因為它引發了諸如歷遍監聽事件需要使用閉包解決等問題。
<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
出現這個錯誤的原因是因為buttons[i]不存在,即為undefined。
為什么會出現按鈕不存在結果呢?通過排查,我們可以發現,每次我們點擊按鈕時,事件監聽回調函數中得到的變量i都會等于buttons.length,也就是這里的 4。而buttons[4]恰恰不存在,所以導致了錯誤的發生。
再而導致i得到的值都是buttons.length的原因就是因為 JavaScript 中沒有塊級作用域,而使對i的變量引用(Reference)一直保持在上一層作用域(循環語句所在層)上,而當循環結束時i則正好是buttons.length。
而在 ES6 中,我們只需做出一個小小的改動,便可以解決該問題(假設所使用的瀏覽器已經支持所需要的特性):
// ... for (/* var */ let i = 0; i < buttons.length; i++) { // ... } // ...
通過把for語句中對計數器i的定義語句從var換成let,即可。因為let語句會使該變量處于一個塊級作用域中,從而讓事件監聽回調函數中的變量引用得到保持。我們不妨看看改進后的代碼經過 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) } // ...
實現方法一目了然,通過傳值的方法防止了i的值錯誤。
箭頭函數(Arrow Function)
繼let和const之后,箭頭函數就是使用率最高的新特性了。當然了,如果你了解過 Scala 或者曾經如日中天的 JavaScript 衍生語言 CoffeeScript,就會知道箭頭函數并非 ES6 獨創。
箭頭函數,顧名思義便是使用箭頭(=>)進行定義的函數,屬于匿名函數(Lambda)一類。當然了,也可以作為定義式函數使用,但我們并不推薦這樣做,隨后會詳細解釋。
使用
箭頭函數有好幾種使用語法:
1. foo => foo + ' world' // means return `foo + ' world'` 2. (foo, bar) => foo + bar 3. foo => { return foo + ' world' } 4. (foo, bar) => { return foo + bar }
以上都是被支持的箭頭函數表達方式,其最大的好處便是簡潔明了,省略了function關鍵字,而使用=>代替。
箭頭函數語言簡潔的特點使其特別適合用於單行回調函數的定義:
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 的開發經驗,就一定會覺得這非常親切,因為這跟其中的 RDD 操作幾乎如出一轍:
- 將原本的由名字組成的數組轉換為一個格式為{ id, name }的對象,id則為每個名字在原數組中的位置
- 剔除其中id為奇數的元素,只保留id為偶數的元素
- 將剩下的元素轉換為一個包含當前元素中原名字的單元數組,以方便下一步的處理
- 通過不斷合并相鄰的兩個數組,最后能得到的一個數組,便是我們需要得到的目標值
箭頭函數與上下文綁定
事實上,箭頭函數在 ES2015 標準中,并不只是作為一種新的語法出現。就如同它在 CoffeeScript 中的定義一般,是用于對函數內部的上下文 (this)綁定為定義函數所在的作用域的上下文。
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 } // ...
為什么要為箭頭函數給予這樣的特性呢?我們可以假設出這樣的一個應用場景,我們需要創建一個實例,用于對一些數據進行查詢和篩選。
let DataCenter = { baseUrl: 'http://example.com/api/data', search(query) { fetch(`${this.baseUrl}/search?query=${query}`) .then(res => res.json()) .then(rows => { // TODO }) } }
此時,從服務器獲得數據是一個 JSON 編碼的數組,其中包含的元素是若干元素的 ID,我們需要另外請求服務器的其他 API 以獲得元素本身(當然了,實際上的 API 設計大部份不會這么使用這么蛋疼的設計)。我們就需要在回調函數中再次使用this.baseUrl這個屬性,如果要同時兼顧代碼的可閱讀性和美觀性,ES2015 允許我們這樣做。
let DataCenter = { baseUrl: 'http://example.com/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))
因為在單行匿名函數中,如果this指向的是該函數的上下文,就會不符合直觀的語義表達。
注意事項
另外,要注意的是,箭頭函數對上下文的綁定是強制性的,無法通過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
另外,因為箭頭函數會綁定上下文的特性,故不能隨意在頂層作用域使用箭頭函數,以防出錯:
// 假設當前運行環境為瀏覽器,故頂層作上下文為 `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 */)
模板字符串
模板字符串模板出現簡直對 Node.js 應用的開發和 Node.js 自身的發展起到了相當大的推動作用!我的意思并不是說這個原生的模板字符串能代替現有的模板引擎,而是說它的出現可以讓非常多的字符串使用變得尤為輕松。
模板字符串要求使用 ` 代替原本的單/雙引號來包裹字符串內容。它有兩大特點:
- 支持變量注入
- 支持換行
支持變量注入
模板字符串之所以稱之為“模板”,就是因為它允許我們在字符串中引用外部變量,而不需要像以往需要不斷地相加、相加、相加……
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 應用的實際開發中,除了 SQL 的編寫,還有如 Lua 等嵌入語言的出現(如 Redis 中的 SCRIPT 命令),或是手工的 XML 拼接。模板字符串的出現使這些需求的解決變得不再糾結了~
對象字面量擴展語法
看到這個標題的時候,相信有很多同學會感到奇怪,對象字面量還有什么可以擴展的?
確實,對象字面量的語法在 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()
可以動態計算的屬性名稱
這個特性相當有意思,也是可以用在一些特殊的場景中。
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 } ]
在上面的兩個[...]中,我演示了動態計算的對象屬性名稱的使用,分別為對應的對象定義了當前計數器n和n的 2 次方
表達式解構
來了來了來了,相當有用的一個特性。有啥用?多重復值聽過沒?沒聽過?來看看吧!
// 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
函數參數表達、傳參
這個特性有非常高的使用頻率,一個簡單的語法糖解決了從前需要一兩行代碼才能實現的功能。
默認參數值
這個特性在類庫開發中相當有用,比如實現一些可選參數:
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 => { // ... })
后續參數
我們知道,函數的call和apply在使用上的最大差異便是一個在首參數后傳入各個參數,一個是在首參數后傳入一個包含所有參數的數組。如果我們在實現某些函數或方法時,也希望實現像call一樣的使用方法,在 ES2015 之前,我們可能需要這樣做:
function fetchSomethings() { var args = [].slice.apply(arguments) // ... } function doSomeOthers(name) { var args = [].slice.apply(arguments, 1) // ... }
而在 ES2015 中,我們可以很簡單的使用…語法糖來實現:
function fetchSomethings(...args) { // ... } function doSomeOthers(name, ...args) { // ... }
要注意的是,...args后不可再添加
雖然從語言角度看,arguments和...args是可以同時使用 ,但有一個特殊情況則不可:arguments在箭頭函數中,會跟隨上下文綁定到上層,所以在不確定上下文綁定結果的情況下,盡可能不要再箭頭函數中再使用arguments,而使用...args。
雖然 ECMA 委員會和各類編譯器都無強制性要求用...args代替arguments,但從實踐經驗看來,...args確實可以在絕大部份場景下可以代替arguments使用,除非你有很特殊的場景需要使用到arguments.callee和arguments.caller。所以我推薦都使用...args而非arguments。
PS:在嚴格模式(Strict Mode)中,arguments.callee和arguments.caller是被禁止使用的。
解構傳參
在 ES2015 中,...語法還有另外一個功能:無上下文綁定的apply。什么意思?看看代碼你就知道了。
function sum(...args) { return args.map(Number) .reduce((a, b) => a + b) } console.log(sum(...[1, 2, 3])) //=> 6
有什么卵用?我也不知道(⊙o⊙)... Sorry...
注意事項
默認參數值和 后續參數 需要遵循順序原則,否則會出錯。
function(...args, last = 1) { // This will go wrong }
另外,根據函數調用的原則,無論是默認參數值還是后續參數都需要小心使用。
新的數據結構
在介紹新的數據結構之前,我們先復習一下在 ES2015 之前,JavaScript 中有哪些基本的數據結構。
- String 字符串
- Number 數字(包含整型和浮點型)
- Boolean 布爾值
- Object 對象
- Array 數組
其中又分為 值類型 和 引用類型 ,Array 其實是 Object 的一種子類。
Set 和 WeakSet
我們再來復習下高中數學吧,集不能包含相同的元素,我們可以根據元素畫出多個集的韋恩圖…………
好了跑題了。是的,在 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
在實際開發中,我們有很多需要用到集的場景,如搜索、索引建立等。
咦?怎么還有一個 WeakSet?這是干什么的?我曾經寫過一篇關于 JavaScript 內存優化 的文章,而其中大部份都是在語言上動手腳,而 WeakSet 則是在數據上做文章。
WeakSet 在 JavaScript 底層作出調整(在非降級兼容的情況下),檢查元素的變量引用情況。如果元素的引用已被全部解除,則該元素就會被刪除,以節省內存空間。這意味著無法直接加入數字或者字符串。另外 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
從數據結構的角度來說,映射(Map)跟原本的 Object 非常相似,都是 Key/Value 的鍵值對結構。但是 Object 有一個讓人非常不爽的限制:key 必須是字符串或數字。在一般情況下,我們并不會遇上這一限制,但若我們需要建立一個對象映射表時,這一限制顯得尤為棘手。
而 Map 則解決了這一問題,可以使用任何對象作為其 key,這可以實現從前不能實現或難以實現的功能,如在項目邏輯層實現數據索引等。
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 中得到了官方的妥協, “實現” 了 ECMAScript 中的標準類機制。為什么是帶有雙引號的呢?因為我們不難發現這樣一個現象:
$ node > class Foo {} [Function: Foo]
回想一下在 ES2015 以前的時代中,我們是怎么在 JavaScript 中實現類的?
function Foo() {} var foo = new Foo()
是的,ES6 中的類只是一種語法糖,用于定義 原型(Prototype) 的。當然,餓死的廚師三百斤,有總比沒有強,我們還是很欣然地接受了這一設定。
語法
定義
與大多數人所期待的一樣,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 中則有良好的實現。
繼承
ES2015 的類繼承總算是為 JavaScript 類繼承之爭拋下了一根定海神針了。在此前,有各種 JavaScript 的繼承方法被發明和使用。(詳細請參見《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}即可。
靜態方法
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 的類并不能直接地定義靜態成員變量,但若必須實現此類需求,可以用static加上get語句和set語句實現。
class SyncObject { // ... static get baseUrl() { return 'http://example.com/api/sync' } }
遺憾與期望
就目前來說,ES2015 的類機制依然很雞肋:
- 不支持私有屬性(private)
- 不支持前置屬性定義,但可用get語句和set語句實現
- 不支持多重繼承
- 沒有類似于協議(Protocl)或接口(Interface)等的概念
中肯地說,ES2015 的類機制依然有待加強。但總的來說,是值得嘗試和討論的,我們可以像從前一樣,不斷嘗試新的方法,促進 ECMAScript 標準的發展。
生成器(Generator)
終于到了 ES2015 中我最喜歡的特性了,前方高能反應,所有人立刻進入戰斗準備!
為什么說這是我最喜歡的新特性呢?對于一個純前端的 JavaScript 工程師來說,可能 Generator 并沒有什么卵用,但若你曾使用過 Node.js 或者你的前端工程中有大量的異步操作,Generator 簡直是你的“賢者之石”。(不過,這并不是 Generator 最正統的用法。出于嚴謹,我會從頭開始講述 Generator)
來龍
Generator 的設計初衷是為了提供一種能夠簡便地生成一系列對象的方法,如計算斐波那契數列(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后會有一個*?為什么函數里使用了while (true)卻沒有進入死循環而導致死機?yield又是什么鬼?
不著急,我們一一道來。
基本概念
在學習如何使用 Generator 之前,我們先了解一些必要的概念。
Generator Function
生成器函數用于生成生成器(Generator),它與普通函數的定義方式的區別就在于它需要在function后加一個*。
function* FunctionName() { // ...Generator Body }
生成器函數的聲明形式不是必須的,同樣可以使用匿名函數的形式。
let FunctionName = function*() { /* ... */ }
生成器函數的函數內容將會是對應生成器的運行內容,其中支持一種新的語法yield。它的作用與return有點相似,但并非退出函數,而是 切出生成器運行時 。
你可以把整個生成器運行時看成一條長長的面條(while (true)則就是無限長的),JavaScript 引擎在每一次遇到yield就要切一刀,而切面所成的“紋路”則是yield出來的值。
~~好吧這是瑞士卷~~
Generator
生(rui)成(shi)器(juan)在某種意義上可以看做為與 JavaScript 主線程分離的運行時(詳細可參考我的另外一篇文章:http://lifemap.in/koa-co-and-coroutine/),它可以隨時被yield切回主線程(生成器不影響主線程)。
每一次生成器運行時被yield都可以帶出一個值,使其回到主線程中;此后,也可以從主線程返回一個值回到生成器運行時中:
let inputValue = yield outputValue
生成器切出主線程并帶出outputValue,主函數經過處理后(可以是異步的),把inputValue帶回生成器中;主線程可以通過.next(inputValue)方法返回值到生成器運行時中。
基本使用方法
構建生成器函數
使用 Generator 的第一步自然是要構建生成器函數,理清構建思路,比如我需要做一個生成斐波那契數列(俗稱兔子數列)的生成器們則需要如何構建循環體呢?如果我需要在主線程不斷獲得結果,則需要在生成器 中做無限循環,以保證其不斷地生成。
而根據斐波那契數列的定義,第 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 } }
這樣設計生成器函數,就可以先把預先設定好的首兩項輸出,然后通過無限循環不斷把后一項輸出。
啟動生成器
生成器函數不能直接用來作為生成器使用,需要先使用這個函數得到一個生成器,用于運行生成器內容和接收返回值。
let gen = fibo()
運行生成器內容
得到生成器以后,我們就可以通過它進行數列項生成了。此處演示獲得前 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 在工程化方面上有著良好的優勢,而采用的就是 ES2015 中的原生模塊化機制,足以證明它的重要性。
歷史小回顧
在 JavaScript 的發展歷史上,曾出現過多種模塊加載庫,如 RequireJS、SeaJS、FIS 等,而由它們衍生出來的 JavaScript 模塊化標準有 CommonJS、AMD、CMD 和 UMD 等。
其中最為典型的是 Node.js 所遵循的 CommonJS 和 RequireJS 的 AMD。
本文在此不再詳細說明這些模塊化方案,詳細可以閱讀 What Is AMD, CommonJS, and UMD?
基本用法
正如前文所展示的使用方式一樣,ES2015 中的模塊化機制設計也是相當成熟的。基本上所有的 CommonJS 或是 AMD 代碼都可以很快地轉換為 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 中有很多種模塊引入方式,我們可以根據實際需要選擇一種使用。
全局引入
全局引入是最基本的引入方式,這跟 CommonJS、AMD 等模塊化標準并無兩樣,都是把目標模塊的所有暴露的接口引入到一個命名空間中。
import name from 'module-name' import * as name from 'module-name'
這跟 Node.js 所用的 CommonJS 類似:
var name = require('module-name')
局部引入
與 CommonJS 等標準不同的是,ES2015 的模塊引入機制支持引入模塊的部份暴露接口,這在大型的組件開發中顯得尤為方便,如 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語句可以調用多次,單獨使用可暴露一個對象到該模塊外。
暴露復蓋模塊
若需要實現像 CommonJS 中的module.exports = {}以覆蓋整個模塊的暴露對象,則需要在export語句后加上default。
// module.js export default { method1, method2 } // main.js import M from './module' M.method1()
降級兼容
在實際應用中,我們暫時還需要使用 babel 等工具對代碼進行降級兼容。慶幸的是,babel 支持 CommonJS、AMD、UMD 等模塊化標準的降級兼容,我們可以根據項目的實際情況選擇降級目標。
$ 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 是一種用于解決回調函數無限嵌套的工具(當然,這只是其中一種),其字面意義為“保證”。它的作用便是“免去”異步操作的回調函數,保證能通過后續監聽而得到返回值,或對錯誤處理。它能使異步操作變得井然有序,也更好控制。我們以在瀏覽器中訪問一個 API,解析返回的 JSON 數據。
fetch('http://example.com/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 在設計上具有原子性,即只有兩種狀態:未開始和結束(無論成功與否都算是結束),這讓我們在調用支持 Promise 的異步方法時,邏輯將變得非常簡單,這在大規模的軟件工程開發中具有良好的健壯性。
基本用法
創建 Promise 對象
要為一個函數賦予 Promise 的能力,先要創建一個 Promise 對象,并將其作為函數值返回。Promise 構造函數要求傳入一個函數,并帶有resolve和reject參數。這是兩個用于結束 Promise 等待的函數,對應的 成功 和 失敗 。而我們的邏輯代碼就在這個函數中進行。
此處,因為必須要讓這個函數包裹邏輯代碼,所以如果需要用到this時,則需要使用箭頭函數或者在前面做一個this的別名。
function fetchData() { return new Promise((resolve, reject) => { // ... }) }
進行異步操作
事實上,在異步操作內,并不需要對 Promise 對象進行操作(除非有特殊需求)。
function fetchData() { return new Promise((resolve, reject) => { api.call('fetch_data', (err, data) => { if (err) return reject(err) resolve(data) }) }) }
因為在 Promise 定義的過程中,也會出現數層回調嵌套的情況,如果需要使用this的話,便顯現出了箭頭函數的優勢了。
使用 Promise
讓異步操作函數支持 Promise 后,我們就可以享受 Promise 帶來的優雅和便捷了~
fetchData() .then(data => { // ... return storeInFileSystem(data) }) .then(data => { return renderUIAnimated(data) }) .catch(err => console.error(err))
弊端
雖說 Promise 確實很優雅,但是這是在所有需要用到的異步方法都支持 Promise 且遵循標準。而且鏈式 Promise 強制性要求邏輯必須是線性單向的,一旦出現如并行、回溯等情況,Promise 便顯得十分累贅。
所以在目前的最佳實踐中,Promise 會作為一種接口定義方法,而不是邏輯處理工具。后文將會詳細闡述這種最佳實踐。
Symbol
Symbol 是一種很有意思的概念,它跟 Swift 中的 Selector 有點相像,但也更特別。在 JavaScript 中,對象的屬性名稱可以是字符串或數字。而如今又多了一個 Symbol。那 Symbol 究竟有什么用?
首先,我們要了解的是,Symbol 對象是具有唯一性的,也就是說,每一個 Symbol 對象都是唯一的,即便我們看不到它的區別在哪里。這就意味著,我們可以用它來保證一些數據的安全性。
console.log(Symbol('key') == Symbol('key')) //=> false
如果將一個 Symbol 隱藏于一個封閉的作用域內,并作為一個對象中某屬性的鍵,則外層作用域中便無法取得該屬性的值,有效保障了某些私有庫的代碼安全性。
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 除了帶給我們數據安全性以外,還帶來了一些很神奇的黑科技,簡直了。
Symbol.iterator
除 Symbol 以外,ES2015 還為 JavaScript 帶來了for...of語句,這個跟原本的for...in又有什么區別?
我們還是以前面的斐波那契數列作為例子。Iterator 在 Java 中經常用到中會經常用到,意為“迭代器”,你可以把它理解為用于循環的工具。
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是否有種興奮的感覺?雖然說創建fibo的時候稍微有點麻煩……
不如我們先來看看這個fibo究竟是怎么定義出來了。首先,我們要了解到 JavaScript 引擎(或編譯器)在處理for...of的時候,會從of后的對象取得Symbol.iterator這屬性鍵的值,為一個函數。它要求要返回一個包含next方法的對象,用于不斷迭代。而因為Symbol.iterator所在鍵值對的值是一個函數,這就讓我們有了自由發揮的空間,比如定義局部變量等等。
每當for...of進行了一次循環,都會執行一次該對象的next方法,已得到下一個值,并檢查是否迭代完成。隨著 ES7 的開發,for...of所能發揮的潛能將會越來越強。
還有更多的 Symbol 黑科技等待挖掘,再次本文不作詳細闡述,如有興趣,可以看看 Mozilla Developer Center 上的介紹 。
Proxy(代理)
Proxy 是 ECMAScript 中的一種新概念,它有很多好玩的用途,從基本的作用說就是:Proxy 可以在不入侵目標對象的情況下,對邏輯行為進行攔截和處理。
比如說我想記錄下我代碼中某些接口的使用情況,以供數據分析所用,但是因為目標代碼中是嚴格控制的,所以不能對其進行修改,而另外寫一個對象來對目標對象做代理也很麻煩。那么 Proxy 便可以提供一種比較簡單的方法來實現這一需求。
假設我要對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 的兼容性很差,哪怕是降級兼容也難以實現。
到這里,相信你已經對 ES2015 中的大部份新特性有所了解了。那么現在,就結合我們原有的 JavaScript 技能,開始使用 ES2015 構建一個具有工程化特點的項目吧。
ES2015 的前端開發實戰
事實上,你們都應該有聽說過 React 這個來自 非死book 的前端框架,因為現在它實在太火了。React 與 ES2015 的關系可謂深厚,React 在開發上便要求使用 ES2015 標準,因其 DSL ── JSX 的存在,所以必須要依賴 Babel 將其編譯成 JavaScript。
但同樣是由于 JSX 的存在,本文章并不會采用 React 作為前端框架,以避免讀者對 JSX 和 HTML 的誤解。我們會采用同樣優秀的前端 MVVM 框架 ── Vue 進行開發。
數據部份,將會使用 MinDB 進行存儲和處理。MinDB 是由筆者開發的一個用于 JavaScript 環境的簡易而健壯的數據庫,它默認使用 DOM Storage 作為其存儲容器,在其他環境中可以通過更換 Store Interface 以兼容絕大部份 JavaScript 運行環境。
Vue.js 的使用教程可以參考 Vue.js 的 官方教程 。
構建界面
我們首先簡單地用 LayoutIt 搭建一個用 Bootstrap 構架的頁面,其中包含了 DEMO 的首頁和文章內容頁,此后我們將會使用這個模板搭建我們的 JavaScript 代碼架構。
接下來,我們需要通過對頁面的功能塊進行組件劃分,以便于使用組件化的架構搭建前端頁面。
我們可以大致分為 Index、Post 和 Publish 三個頁面,也可以說三個路由方向;而我們還可以把頁面中的組塊分為:文章列表、文章、側邊欄、評論框等。
以此,我們可以設計出以下結構,以作為這個項目的組織結構:
Routes Components |- Index ----|- Posts | |- Sidebar | |- Post -----|- Post | |- Comments | |- Publish
首頁包含了文章列表、側邊欄兩個組件;文章頁面包含文章內容組件和評論框組件(此處我們使用多說評論框作為我們的組件);而文章發布頁則可以單獨為一個路由器,而不需要分出組件。
代碼結構定義
因我們是以 babel 進行 ES2015 降級兼容的,所以我們最好可以采用分離的結構,這里我們使用src和dist。
我們此處以比較簡單的結構構建我們的DEMO:
app |- src 程序的源文件目錄 | |- controllers 后端的路由處理器 | |- lib 后端需要引用的一些庫 | |- public 前端 JavaScript 源文件 | | |- controllers 前端的路由處理器 | | |- components 前端組件 | | |- models 前端數據層 | | |- config.js 前端的配置文件 | | |- main.js 前端 JavaScript 入口 | |- app.js 后端程序入口 | |- routes.js 后端路由表 |- dist 降級兼容輸出目錄 | |- public | |- css | |- index.html 前端 HTML 入口 |- gulpfile.js Gulp 構建配置文件 |- package.json Node.js 項目配置文件
而我們在這一章節中則專注于public這一目錄即可,Node.js 部份將在下一章節詳細展示。
架構設計
模塊化
因為有了 ES2015 自身的模塊化機制,我們就不必使用 RequireJS 等模塊加載庫了,通過 Browserify 我們我可以將整個前端的 JavaScript 程序打包到一個 .js 文件中,而這一步驟我們使用 Gulp 來完成。
詳細的 Gulp 使用教程可以參考: 了不起的任務運行器Gulp基礎教程
數據支持
我們的數據將從后端的 Node.js 程序中獲取,以 API 的形式獲得。
另外,為了得到更佳的用戶體驗,我們將使用 MinDB 作為這個 DEMO 的前端數據庫,減少網絡請求次數,優化使用體驗。
界面渲染
為了能讓我們的界面定義能夠足夠簡單,我們在這使用了 Vue 作為前端開發框架,將其與 MinDB 對接,負責渲染我們的頁面。
其中,我們會利用 Vue 的組件系統來實現我們制定下的組件設計。另外,我們還會使用 watchman.js 來實現前端的路由器,以便我們對不同的頁面的邏輯進行分離。
構建應用
在開始編寫業務代碼之前,我們需要先安裝好我們所需要的依賴庫。
$ 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 規則中
- 建立在共用模板中存在的需要實例化的組件
因為我們還么有開始動工路由相應器,所以我們先把“建立在共用模板中存在的需要實例化的組件”這一任務完成。
在共用模板中,有唯一一個必須的共用組件就是頁面切換器 ── 一個用于包含所有的頁面的元素。
<div class="row" id="wrapper" v-html="html"></div>
對應的,我們將在入口文件中使用 Vue 建立其對應的 ViewModel,并將其綁定至 DOM 元素。
let layoutVM = new Vue({ el: '#wrapper', data: { html: '' } })
以后可以通過改變layoutVM.$data.html來改變#wrapper的內容,配合 watchman.js 以加載不同的頁面內容。
為了能在路由相應器中改變layoutVM的參數,我們將其作為 watchman.js 給予相應器的上下文參數中的一個屬性。
// ... .use((ctx, next) => { ctx.query = qs.parse(window.location.search.substr(1)) ctx.layoutVM = layoutVM next() })
數據層:文章
我們單獨把文章的查詢、讀取和創建等操作抽象成一個庫,使其與邏輯層分離,讓代碼更美觀。
在此之前,我們先定義好從后端用于取得文章數據的 API:
- URL:/api/posts/list
- 參數:
- page當前頁數,每頁 10 條記錄
我們可以直接用新的 Ajax API 來進行 API 訪問。
import 'whatwg-fetch' import min from 'min' async function listPosts(page = 0) { const count = 10 // 檢查 MinDB 是否存在數據 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) + '...' } } }) // 將數據存儲到 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 讀取數據 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)則會將文章的所有數據存入以post:{id}命名的 Hash 中。
完成對首頁的所有文章列表支持后,我們還需要簡單的定義一個用于讀取單篇文章數據的 API。
如果用戶是從首頁的文章列表進來的,那么我們就可以直接從 MinDB 中讀取文章數據。但如果用戶是直接通過 url 打開網址的話,MinDB 有可能并沒有存儲文章數據,那么我們就通過 API 從后端獲取數據,并存儲在 MinDB 中。
- URL:/api/posts/:id
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):
- title
- content
- author
我們也可以通過fetch來發出 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 數據,為后面的頁面渲染做準備
- 分別從后端加載文章數據和多說加載評論數,加載完以后存入 MinDB 中
- 從 MinDB 中加載已緩存的數據
- 建立對應的 VM,并傳入數據
準備頁面渲染
首先,我們需要為 watchman.js 的路由提供一個函數以作相應器,并包含一個為context的參數。我們則可以將其作為模塊的暴露值。
export default function(ctx) { // 改變 layoutVM ctx.layoutVM.$data.html = ` <h1> ES2015 實戰 - 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="側邊欄" content="{{content}}" list="{{list}}"></panel> </div> ` }
此處將首頁的 HTML 結構賦予layoutVM,并讓其渲染至頁面中。
加載數據
因為我們之前已經將數據層抽象化了,所以我們此處只需通過我們的抽象數據層讀取我們所需要的數據即可。
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}} 發表</small> | <a class="btn" href="/#!/post/{{id}}">查看更多 ?</a> </p> </div> `
我們可以通過 Vue 的雙向綁定機制將數據插入到模板中。
Vue.component('post-in-list', { template: template, replace: true })
根據 Vue 的組件機制,我們可以通過對組件標簽中加入自定義屬性來傳入參數,以供組件中使用。
但此處我們先用 Vue 的v-repeat指令來進行循環使用組件。
<post-in-list v-repeat="posts"></post-in-list>
panel.js同理建立。
路由:文章頁面
相比首頁,文章頁面要簡單得多。因為我們在首頁已經將數據加載到 MinDB 中了,所以我們可以直接從 MinDB 中讀取數據,然后將其渲染到頁面中。
// ... let post = await min.hgetall(`post:${this.id}`) // ...
然后,我們再從之前設計好的頁面模板中,抽出我們需要用來作為文章頁面的內容頁。
<h1 v-text="title"></h1> <small>由 {{author}} 發表</small> <div class="post" v-html="content | marked"></div>
我們同樣是同樣通過對layoutVM的操作,來準備頁面的渲染。在完成渲染準備后,我們就可以開始獲取數據了。
let post = await min.hgetall(`post:${this.id}`)
在獲得相應的文章數據以后,我們就可以通過建立一個組件來將其渲染至頁面中。其中,要注意的是我們需要通過 Vue 的一些 API 來整合數據、渲染等步驟。
在這我不再詳細說明其構建步驟,與上一小節相同。
import Vue from 'vue' import min from 'min' import marked from 'marked' const template = ` <h1 v-text="title"></h1> <small>由 {{author}} 發表</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特性。
路由:發布新文章
在發布新文章的頁面中,我們直接調用我們之前建立好的數據抽象層的接口,將新數據傳向后端,并保存在 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 } })
路由綁定
在完成路由響應器的開發后,我們就可以把他們都綁定到 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 }) // ...
這樣,就可以讓我們之前的路由結構都綁定到入口文件中:
- 首頁綁定到/和#!/
- 文章頁面則綁定到了#!/post/:id,比如#!/post/123則表示 id 為 123 的文章頁面
- #!/new則綁定了新建文章的頁面
合并代碼
在完成三個我們所構建的路由設計后,我們就可以用 Browserify 把我們的代碼打包到一個文件中,以作為整個項目的入口文件。此處,我們再引入 Gulp 作為我們的構建輔助器,而不需要直接使用 Browserify 的命令行進行構建。
在開始編寫 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 發布了版本 6,其 API 與 5 版本有著相當大的區別,且 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 所設計的頁面中,把可以用于包含替換內容的部份去掉,然后引入我們通過 Browserify 生成的 bundle.js。
完成 JavaScript 部份的開發后,我們再將所需要的靜態資源文件加載到 HTML 中,我們就可以看到這個基本脫離后端的前端 Web App 的效果了。
ES2015 的 Node.js 開發實戰
就目前來說,能最痛快地使用 ES2015 中各種新特性進行 JavaScript 開發的環境,無疑就是 Node.js。就 Node.js 本身來說,就跟前端的 JavaScript 環境有著本質上的區別,Node.js 有著完整意義上的異步 IO 機制, 有著無窮無盡的應用領域,而且在語法角度上遇到問題機率比在前端大不少。甚至可以說,Node.js 一直是等著 ES2015 的到來的,Node.js 加上 ES2015 簡直就是如虎添翼了。
從 V8 引擎開始實驗性的開始兼容 ES6 代碼時,Node.js 便開始馬上跟進,在 Node.js 中開放 ES6 的兼容選項,如 Generator、Classes 等等。經過相當長一段時間的測試和磨合后,就以在 Node.js 上使用 ES6 標準進行應用開發這件事來說,已經變得越來越成熟,越來越多的開發者走上了 ES6 這條“不歸路”。
一些針對 Node.js + ES6 的開發模式和第三方庫也如雨后春筍般冒出,其中最為人所熟知的便是以 co 為基礎所建立的 Web 框架 Koa。Koa 由 TJ 等 express 原班人馬打造,目前也有越來越多的中國開發者加入到 Koa 的開發團隊中來,為前沿 Node.js 的開發做貢獻。
co 通過使用 ES2015 中的 Generator 特性來模擬 ES7 中相當誘人的async/await特性,可以讓復雜的異步方法調用及處理變得像同步操作一樣簡單。引用響馬大叔在他所維護的某項目中的一句話:
用同步代碼抒發異步情懷
在本章節中,我將以一個簡單的后端架構體系,來介紹 ES2015 在 Node.js 開發中的基于 Koa 的一種優秀實踐方式。
因為 Node.js 自帶模塊機制,所以用 babel 對 ES2015 的模塊語法做降級兼容的時候,只需降至 Node.js 所使用的 CommonJS 標準即可。
不一樣的是,ES2015 的模塊語法是一種聲明式語法,根據 ES2015 中的規定,模塊引入和暴露都需要在當前文件中的最頂層,而不能像 CommonJS 中的require()那樣可以在任何地方使用;使用import引入的模塊所在命名空間將會是一個常量,在 babel 的降級兼容中會進行代碼檢查,保證模塊命名空間的安全性。
架構設計
因為我們這個 DEMO 的數據結構并不復雜,所以我們可以直接使用 MongoDB 作為我們的后端數據 庫,用于存儲我們的文章數據。我們可以通過monk庫作為我們讀取、操作 MongoDB 的客戶端庫。在架構上,Koa 將作為 Web 開發框架,配合 co 等庫實現全“同步”的代碼編寫方式。
構建應用
這就讓我們一步一步來吧,創建 Node.js 應用并安裝依賴。
$ npm init $ npm install koa koa-middlewares koa-static monk co-monk thunkify --save
在上一個章節中,我們已經建立了整個 DEMO 的文件結構,在此我們再展示一遍:
app |- src 程序的源文件目錄 | |- controllers 后端的路由處理器 | |- models 數據抽象層 | |- lib 后端需要引用的一些庫 | |- public 前端 JavaScript 源文件 | |- app.js 后端程序入口 | |- routes.js 后端路由表 |- dist 降級兼容輸出目錄 |- gulpfile.js Gulp 構建配置文件 |- package.json Node.js 項目配置文件
在src/controllers中,包含我們用來相應請求的控制器文件;src/lib文件夾則包含了我們需要的一些抽象庫;src/public文件則包含了在上一章節中我們建立的前端應用程序;src/app.js是該應用入口文件的源文件;src/routes.js則是應用的路由表文件。
入口文件
在入口文件中,我們需要完成幾件事情:
- 創建 Koa 應用,并監聽指定端口
- 將所需要的 Koa 中間件接入我們所創建的 Koa 應用中,如靜態資源服務
- 引入路由表文件,將路由控制器接入我們所建立的 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}`) })
數據抽象層
為了方便我們在 co 的環境中使用 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 實例時都需要連接一遍數據庫了。
// models/posts.js import mongo from '../lib/mongo' export default mongo.collection('posts')
Posts 控制器
完成了數據層的抽象處理后,我們就可以將其用于我們的控制器了,參考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函數傳入thunkify中,我們需要的是 callback 中的第三個參數body,所以我們需要自行包裝一層函數以取得body并返回到 co 中。
API:獲取所有文章
我們可以通過這個 API 獲取存儲在 MongoDB 中的所有文章,并支持分頁。支持提供當前獲取的頁數,每頁10篇文章,提供每篇文章的標題、作者、文章內容和評論。
// 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.duoshuo.com/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 } }) // 返回結果 this.body = { posts: posts } }
此處我們用到了 co 的一個很有意思的特性,并行處理異步操作。我們通過對從 MongoDB 中取得數據進行#map()方法的操作,返回一組 Generator Functions,并將這個數組傳給yield,co 便可以將這些 Generator Functions 全部執行,并統一返回結果。同樣的我們還可以使用對象來進行類似的操作:
co(function*() { let result = yield { posts: getPostsAsync(), hot: getHotPosts(), latestComments: getComments(10) } result //=> { posts: [...], hot: [...], latestComments: [...] } })
API:獲取指定文章
這個 API 用于通過提供指定文章的 ID,返回文章的數據。
// 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.duoshuo.com/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:發布新文章
相比上面兩個 API,發布新文章的 API 在邏輯上則要簡單得多,只需要向 Collection 內插入新元素,然后將得到的文檔返回至客戶端既可以。
// 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 向移動端擴展和彌補多說評論框在原生移動端上的不足。由于評論的數據并不是存儲在項目數據庫當中,所以我們也不需要為它創建一個數據抽象層文件,而是直接從控制器入手。
API:獲取指定文章的評論
// GET /api/comments/post/:id router.get.fetchCommentsInPost = function*() { let postId = this.params.id let duoshuoReply = JSON.parse(yield requestAsync(`http://api.duoshuo.com/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:發表新評論
同樣是為了擴展系統的 API,我們通過多說的 API,允許使用 API 來向文章發表評論。
// 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.duoshuo.com/posts/create.json`, json: true, body: { short_name: duoshuo.short_name, secret: duoshuo.secret, thread_key: postId, message: message } }) this.body = { comment: reply.response } }
配置路由
完成控制器的開發后,我們是時候把路由器跟控制器都連接起來了,我們在src/routes.js中會將所有控制器都綁定到路由上,成為一個類似于路由表的文件。
import { router as Router } from 'koa-middlewares'
首先,我們要將所有的控制器引入到路由文件中來。
import posts from './controllers/posts' import comments from './controllers/comments'
然后,創建一個路由器實例,并將所有控制器的響應器和 URL 規則一一綁定。
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
配置任務文件
經過對數據抽象層、邏輯控制器、路由器的開發后,我們便可以將所有代碼利用 Gulp 進行代碼構建了。
我們先安裝好所需要的依賴庫。
$ 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')
我們主要需要完成兩個構建任務:
- 編譯并構建前端 JavaScript 文件
- 編譯后端 JavaScript 文件
在構建前端 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 后端處理靜態資源請求的響應地址。
而構建后端代碼則更為簡單,因為我們只需要將其編譯并復制到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 文件的編寫以后,我們就可以進行代碼構建了。
$ 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文件夾中已經編譯好的代碼運行起來了。
$ node dist/app.js Demo is running, port:3000
不出意外,我們就可以看到我們想要的效果了。
前方高能反應
部署到 DaoCloud
完成了代碼的開發 ,我們還需要將我們的項目部署到線上,讓別人看到我們的成果~
Docker 是目前最流行的一種容器化應用搭建工具,我們可以通過 Docker 快速地將我們的應用部署在任何支持 Docker 的地方,哪怕是 Raspberry Pi 還是公司的主機上。
而 DaoCloud 則是目前國內做 Docker 商業化體驗最好的公司,他們提供了一系列幫助開發者和企業快速使用 Docker 進行項目部署的工具。這里我們將介紹如何將我們的 DEMO 部署到 DaoCloud 的簡易容器中。
Dockerfile
在使用 Docker 進行項目部署前,我們需要在項目中新建一個 Dockerfile 以表達我們的鏡像構建任務。
因為我們用的是 Node.js 作為項目基礎運行環境,所以我們需要從 Node.js 的官方 Docker 鏡像中引用過來。
FROM node:onbuild
因為我們已經在package.json中寫好了對gulp的依賴,這樣我們就可以直接在docker build的時候對代碼進行編譯。
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
至此,我們已經完成了 Dockerfile 的編寫,接下來就可以將項目代碼上傳到 GitHub 等地方,供 DaoCloud 使用了。這里我不再說明 Git 和 GitHub 的使用。
創建 DaoCloud 上的 MongoDB 服務
借助于 Docker 的強大擴容性,我們可以在 DaoCloud 上很方便地創建用于項目的 MongoDB 服務。
在“服務集成”標簽頁中,我們可以選擇部署一個 MongoDB 服務。
創建好 MongoDB 服務后,我們就要將我們上傳到 GitHub 的項目代碼利用 DaoCloud 進行鏡像構建了。
代碼構建
DaoCloud 提供了一個十分方便的工具,可以把我們存儲在 GitHub、BitBucket、GitCafe、Coding 等地方的項目代碼,通過其中的 Dockerfile 構建成一個 Docker 鏡像,用于部署到管理在 DaoCloud 上的容器。
通過綁定 GitHub 賬號,我們可以選擇之前發布在 GitHub 上的項目,然后拉取到 DaoCloud 中。
構建完成以后,我們就可以在“鏡像倉庫”中看到我們的項目鏡像了。
我們將其部署到一個 DaoCloud 的容器中,并且把它與之前創建的 MongoDB 服務綁定。
最后的最后,我們點擊“立即部署”,等待部署成功就可以看到我們的項目線上狀態了。
我們可以在這里看到我部署的 DEMO: http://es2015-demo.daoapp.io/
而我們的 DEMO,可以在這里查看詳細代碼: https://github.com/iwillwen/es2015-demo
至此,我們已經完成了 Node.js 端和前端的構建,并且將其部署到了 DaoCloud 上,以供瀏覽。下面,我們再來看看,正在發展中的 ES7 又能給我們帶來什么驚喜吧。
一窺 ES7
async/await
上文中我們提及到 co 是一個利用 Generator 模擬 ES7 中async/await特性的工具,那么,這個async/await究竟又是什么呢?它跟 co 又有什么區別呢?
我們知道,Generator Function 與普通的 Function 在執行方式上有著本質的區別,在某種意義上是無法共同使用的。但是,對于 ES7 的 Async Function 來說,這一點并不存在!它可以以普通函數的執行方式使用,并且有著 Generator Function 的異步優越性,它甚至可以作為事件響應函數使用。
async function fetchData() { let res = await fetch('/api/fetch/data') let reply = await res.json() return reply } var reply = fetchData() //=> DATA...
遺憾的是,async/await所支持的并不如 co 多,如并行執行等都暫時沒有得到支持。
Decorators
對于 JavaScript 開發者來說,Decorators 又是一種新的概念,不過它在 Python 等語言中早已被玩出各種花式。
Decorator 的定義如下:
- 是一個表達式
- Decorator 會調用一個對應的函數
- 調用的函數中可以包含target(裝飾的目標對象)、name(裝飾目標的名稱)和descriptor(描述器)三個參數
- 調用的函數可以返回一個新的描述器以應用到裝飾目標對象上
PS:如果你不記得descriptor是什么的話,請回顧一下 Object.defineProperty()方法 。
簡單實例
我們在實現一個類的時候,有的屬性并不想被for..in或Object.keys()等方法檢索到,那么在 ES5 時代,我們會用到Object.defineProperty()方法來實現:
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 來很簡單地實現這個需求:
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 做點什么神奇的事情。
假如我們要實現一個類似于 Koa 和 PHP 中的 CI 的框架,且利用 Decorator 特性實現 URL 路由,我們可以這樣做。
// 框架內部 // 控制器 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) } } // 路由執行部份 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) }
最重要的是,同一個修飾對象是可以同時使用多個修飾器的,所以說我們還可以用修飾器實現很多很多有意思的功能。
后記
對于一個普通的 JavaScript 開發者來說,ES2015 可能會讓人覺得很模糊和難以學習,因為 ES2015 中帶來了許多我們從前沒有在 JavaScript 中接觸過的概念和特性。但是經過長時間的考察,我們不難發現 ES2015 始終是 JavaScript 的發展方向,這是不可避免的。因此我要在很長一段時間內都向身邊的或是社區中的 JavaScript 開發者推廣 ES2015,推薦他們使用最新的技術。
這篇文章說長不長,說短也不短,我只能在有限的文字篇幅內盡可能把更多的知識展示出來,更深入的細節還需要讀者自行探索。無論如何,若是這篇文章能引起各位 JavaScript 開發者對 ES2015 的興趣和重視,并且中從學會了如何在項目中使用 ES2015 標準進行開發,那么這篇文章的目的就已經達到了。
再次感謝對這篇文章的寫作提供了支持的各位(名次均不分先后):
審讀大牛團: 代碼家 , 樸靈 , 寒冬winter , TooBug , 郭達峰 , 芋頭 , 尤雨溪 , 張云龍 , 民工精髓V
內測讀者: 死月 , 米粽 , 陰明 , 引證 , 老雷 , Jonah , Crzidea , 送送
PS:我廠準備在100offer招人了!聽說技術牛人都上他們網站找工作!
關于作者
甘超陽(小問),LikMoon(離門創造)創始人
GitHub: Will Wen Gunn
微博:端木文_Wen
推ter: @iwillwen
非死book: Will Wen Gunn
博客地址: Life Map
歡迎大家加入我的文章陪審團,以后有新的文章都可以率先閱讀哦~