2016年7月27日 星期三

JavaScript 好用的 async 異步函數!

Standard

先聲明,async 異步函數是 ECMAScript 第七版(ES7)才被支援的語法和特性,目前 ES7 還沒有被大多數的 JavaScript Engine 所實作,如果你要使用,需要用到 babel 這類工具,先把此程式編譯轉換,讓其可在舊版本 JavaScript Engine 上執行。

如果你覺得以 co 模組來操作 Generator 很好用,你可以想像 async 異步函數就是原生的 co,幾乎是同樣的使用方式,同樣的使用概念,只不過不再需要使用 generator 和 yield 這類語法。如果你是個過不了在函數上有個醜陋「*」符號這一關的人,async 異步函數的使用方式應該會讓你感覺到舒服許多。

什麼是 async 異步函數(async functions)?


異步函數使用方式其實和一般的函數一樣,只不過在這函數之內的程式,可以用 await 的語法去執行並等待異步工作(如:Promise)而不需要使用到骯髒的 callback function。宣告並使用一個 async 異步函數,就是在定義函數時加上「async」,然後直接執行這個函數即可,簡單的範例如下:

async function myAsyncFunc() {
    console.log('Hello async functions!');
}

myAsyncFunc();

搭配 Promise 的使用


Promise 通常被大量用來管理非同步的工作,並讓開發者容易管理錯誤拋出等機制,一個典型的 Promise 使用如下:

var task = new Promise(function(resolve, reject) {

    // 執行一個非同步的工作,完成後呼叫帶入的 callback
    doAsyncTask(function(err) {

        // 有問題呼叫 reject,並帶入錯誤值
        if (err)
            return reject(err);

        // 成功呼叫 resolve 並帶入回傳值
        resolve('VALUE');
    });
});

// 使用 then 去執行並等待工作完成,成功會呼叫 callback,失敗則用 catch 去接收。
task
    .then(function(val) {
        console.log(val);
    })
    .catch(function(err) {
        console.log(err);
    });

如果在「異步函數」中呼叫以 Promise 包裝的工作,可以直接使用 await 語法:

async function myAsyncFunc() {
    var val = await task;
    console.log(val);
}

myAsyncFunc();
你會看到在異步函數中,程式邏輯會以「像是阻塞的方式進行」,await 會等到工作完成後,將回傳值回傳,然後才繼續下一行工作。要注意的是,因為看起來像程式會阻塞,熟悉 JavaScript 的人會不自覺開始害怕事件引擎被鎖死,但實際上 await 是以非同步的方式在進行,並不會卡住或影響事件引擎的運作。

搭配 Thunk 的使用


什麼是 Thunk?簡單來說就是一個處理函數,完成時會呼叫 callback 函數表示完成,實務上最常的用法會在外面包一層函數,創造一個 Closure,一個簡單的 Thunk 如下:

function myThunkFunc(thing) {

    return function(done) {

        setTimeout(function() {
            console.log(thing);
            done(null, 'World');
        }, 1000);
    };
}

異步函數裡面,我們可以這樣使用它:
async function myAsyncFunc() {
    var val = await myThunkFunc('Hello');
    console.log(val);
}

myAsyncFunc();

等待其他異步函數完成工作


當然,await 除了可以吃 Thunk 和 Promise 之外,也可以處理並等待其他的「異步函數」,如下:
async function anotherAsyncFunc(thing) {
    var val = await myThunkFunc(thing);
    return val;
}

async function myAsyncFunc() {
    var val = await anotherAsyncFunc('Hello');
    console.log(val);
}

myAsyncFunc();

錯誤處理


當 Promise 的 reject() 被呼叫,或是 Thunk 的 callback 函數被呼叫時,第一個參數不是 null,就代表這個異步工作是有錯誤發生的,如果要從 await 偵測這些錯誤訊息,需要使用 try-catch 去接這些錯誤訊息。

async function myAsyncFunc() {
    try {
        var val = await myThunkFunc('Hello');
    } catch(e) {
        console.log(e);
    }
}

myAsyncFunc();

舒服!到處使用異步函數


一旦你熟悉如何使用異步函數,你可以到處使用。其實他就像一般的函數一樣,他可以被當成一個 callback 來使用,像是下面這個例子,就把它當成 Promise 的處理函數:

var task = new Promise(async function(resolve, reject) {
    try {
        await doAsyncTask();
    } catch(e) {
        return reject(e);
    }

    resolve();
});

後記


如果你是原本就在使用 co 模組的人,應該會發現 async/await 根本就是一樣的東西,對你來說根本無痛,唯一有點麻煩的是,目前 JavaScript 仍然還沒有原生支援,需要 babel 一類的編譯器才能使用。但有不少人看重程式碼的簡潔和漂亮,已經大量使用了。

另外提到,Koa 2.0 因為完全採用 async/await 的方式,無限期處於不穩定版本。等到 async/await 被原生支援那一天, Koa 2.0 穩定版就會推出了,相信這一天就快要到來。

2016年6月6日 星期一

下一代的框架:Koa 1.0 起手式

Standard


身為 Node.js 使用者的你,還在使用 Express 嗎?快來使用下一代的 Web Framework 吧!Koa 是由 Express 的開發者們出來所開發的新網站框架,嘗試採用了最新的 ECMAScript 6 語法,讓開發者可以用更簡約的方式,開發網站應用程式,讓程式碼更好維護之外,也能受益於最新的語言特性。

穩定版與不穩定版


現在的 Koa 分為 1.0 和 2.0 兩個版本,1.0 使用 ES6 的 Generator 特性,也是目前的 stable 版本,而 2.0 採用 ES7+ 的 async/await,據 Koa 官方說法,2.0 穩定度可以用於實際產品,只是在 ECMAScript 7 規格正式敲定,且 JavaScript V8 Engine 推出原生的實作之前,將無限期處於 unstable 的狀態。也就是說,若你想要在你自己的專案上使用 2.0,你必須使用 babel 這一類的編譯器,因為裡面用到了 ES7 的語法。

本篇文章的重點將放在 Koa 1.0 之上,畢竟在 ECMAScript 7 還處於草案階段的現在,很難說未來會不會有什麼改變。

安裝 Koa


由於 Koa 需要用到 ECMAScript 6 的語言特性,請先檢查你的 Node.js 版本,最少為 0.12 以上,如果你已經使用了 Node.js 4.0 或更高版本,請不用擔心這個問題。

然後透過 NPM 即可安裝模組:
npm install koa

開發第一個應用程式


開發 Koa 應用程式非常容易,下面是程式碼範例:
var koa = require('koa');

var app = koa();

app.use(function *() {
    this.body = 'Hello World';
});

app.listen(3001);
跑起來後,用瀏覽器連入 3001 埠即可看到「Hello World」的字樣,因為 this.body 的內容,將會被輸出到前端瀏覽器上。

使用 Generator 打造的 Middleware


koa.use() 將用來載入 Middleware,所有連線工作都會經過 Middleware 處理。所以前一個例子裡,我們使用 koa.use() 設定了一個處理函數,該函數會用來處理所有連線工作。

要注意的是,在 function 後面有一個「*」的符號,這代表這個函數是一個 Generator 函數,所以這函數裡面的程式將可以使用 Generator 的語言特性。若您不知道 Generator 是什麼,可以參考過去的舊文「快樂玩 ES6 Generator,從 co 起手式開始」。

【註一】如果你有過 express 開發經驗,對於 koa.use() 會相當熟悉,Koa 同樣支援了 Middleware 的架構,你可以將過去的程式輕易移植到這新的框架上。
【註二】Koa 底層使用 co 來操作 Generator,若你覺得 Generator 太過艱澀,只需要了解 co 的使用即可。

加入多個 Middleware


所有的連線要求可以透過一系列、不只一個 Middleware 來處理,我們可以利用多次 koa.use() 來使用它,範例如下:
var koa = require('koa');

var app = koa();

app.use(function *(next) {
    yield next;
});

app.use(function *() {
    this.body = 'Hello World';
});

app.listen(3001);

一個 Middleware 可以透過 yield 傳入 next 參數,讓連線要求進入到下一個 Middleware 被處理。

自訂 Router 和路徑管理


之前的範例直接使用 koa.use(),會將所有的連線都導入同一個處理函數,輸出同一個結果。若我們想要自訂不同的路徑,讓不同路徑用不同的處理函數,將需要額外安裝「koa-router」模組:
npm install koa-router

然後可以直接修改我們的程式碼如下:
var koa = require('koa');
var Router = require('koa-router');

var app = koa();
var router = new Router();

// 針對不同路徑套用不同處理函數
router.get('/', function *() {
    this.body = 'HOME';
});

router.get('/myapi', function *() {
    this.body = 'API';
});

// 載入自訂的 router
app.use(router.middleware());
app.listen(3001);
範例中只有使用到「GET」方法,如果要用來開發 Restful API 或是處理一些表單上傳的工作,可以依樣畫葫蘆使用 router.post、router.put 或 router.del 方法。

接收 QueryString 的資料


QueryString 可說是歷史悠久且非常常見的傳值方法,藉由一個網址後面加上一個「?」字元後,就可以使用鍵值(Key/Value)來進行資料傳遞,並用「&」區隔多組資料。一個簡單的實際應用如下:
http://my_server/send?name=fred&msg=Hello

取得資料的方法如下:
console.log(this.request.query.name);
console.log(this.request.query.msg);

接收 body 的資料


當我們使用「POST」或「PUT」方法,我們就可以利用 body 傳送一些資料到伺服器,像是網頁表單時常使用這樣的傳值方法。若想要取得 body 的資料,必須先安裝一個「koa-bodyparser」模組:
npm install koa-bodyparser

使用 koa.use() 載入 koa-bodyparser,koa 就會自動在處理連線時使用它解析 body:
var bodyParser = require('koa-bodyparser');

app.use(bodyParser());

然後可以在路徑處理函數中,正常取得 body 內的資訊:
console.log(this.request.body.name);
console.log(this.request.body.msg);

靜態文件支援


除了一般動態網頁外,我們也會在網頁中嵌入 CSS、前端的 JavaScript 和圖片等靜態檔案,這些檔案在瀏覽器載入頁面時,同時間也要提供瀏覽器能取得。為了達成這功能,可以使用「koa-static」來達成:
npm install koa-static

然後可以直接加入這個 middleware:
var serve = require('koa-static');

app.use(serve(__dirname + '/public'));

其中要帶入路徑參數,告訴 koa-static 去哪個目錄尋找對應的靜態檔案,範例中是設定為此程式同一個目錄下的 public 目錄。

Session 支援


要使用 Session 要先安裝 koa-session:
npm install koa-session

然後就可以在處理函數中使用 this.session 這個物件來存放資料:

var koa = require('koa');
var Router = require('koa-router');
var session = require('koa-session');

var app = koa();
var router = new Router();

// 設定一組金鑰,用來加密 session
app.keys = [ '$*&!@#$^)*(DSIJCH(*&@#' ];

// 載入 session middleware
app.use(session(app));

// 每次連線就將計數器加一
app.use(function *(next) {
    if (this.session.counter)
        this.session.counter = 0;

    this.session.counter++;

    yield next;
});

router.get('/', function *() {
    // 回傳顯示計數器的值
    this.body = this.session.counter;
});

// 載入自訂的 router
app.use(router.middleware());
app.listen(3001);

這範例會在瀏覽器每次連線時,把 session 內的計數器加一,所以若是我們重複整理這個頁面,會看到數字不斷增長。

要注意的是,使用 session 前,我們要為 app.keys 設一組金鑰(Key), koa-session 會使用這組 Key 加密我們的 session 資料。

後記


還在使用 express 嗎?別老土了。(笑)

2016年5月11日 星期三

MakerBoard: 自幹 MT7688 模擬器!簡報釋出!

Standard

使用 MTK LinkIt Smart 7688 這類開發板時,總是很痛苦,由於儲存空間不大,記憶體也不大,常常在開發的過程中飽受折磨。於是我們開始思考如何可以在自己的電腦上,模擬一個 MT7688 的環境,在有充沛資源的機器上進行開發。就這樣,前陣子開發了一個小小的 Open Source 工具專案「MakerBoard」,並在 MakerCup 的共筆網站發表「沒有板子也可以玩的 7688 模擬器!」。

雖然這樣一個小小的模擬器運用了 VM 和 Container 相關技術。但其實主要概念並不難,這次 5/10 在台大的開放原始碼課程中,就簡單從 MakerBoard 這專案出發,然後說明了一下怎麼樣自己打造一個簡單的 Container,並利用 QEMU 來進行 Binary Translation 的工作。

簡報釋出,請自行服用:

2015年12月13日 星期日

從 Maker 出發並反思:於是我們成立了 MakerCup!

Standard

Maker 一詞近年來翻紅,有人稱「自造者」,有人稱「創客」,以代工起家的國內產業,覺得 Maker 風潮是一個維持舊有工業地位的方法和機會,更將其引伸成軟硬整合、創業模式,無一不紛紛出來插手,想佔一塊地,分一杯羹。有些媒體將 Maker 塑造成有專業技術能力的人們,彷彿與一般人有很大的鴻溝。種種因素,自然越來越多人不了解 Maker 是什麼了。

但我們認為真正的 Maker 並不是擁有厲害能力的人,而是願意動手落實的人。

為什麼我們要成立 MakerCup?

我們想聚集純粹想動手、交流的朋友,並讓更多人參與並體驗 Maker 的世界。

事實上,Maker 的定義很簡單,凡是能打造、做東西的人,都能稱為 Maker。做菜的廚師,是個 Maker;編織衣服的人,是個 Maker;畫家,也是個 Maker。當然,寫軟體、做電子電路的人,以及各種設計師,通通都算是 Maker。無論在什麼領域,Maker 精神強調的是動手去實現、完成,去參與過程、瞭解過程,進而讓自己更有能力去打造出更多創意十足的東西。更重要的是,在這種不設限的旅程,能讓我們都具備著跨領域思考的能力。

既然過程才是最重要的,我們便開始思考怎麼樣讓更多人交流,交流技術、能力,共同發展和探討更多的知識。我們不應該只是追求一時且短暫的成果,滿足政府或代工產業想要立即成果的 KPI,更或是不應該鑽牛角尖盲目追求頂尖技能,而是讓更多人參與、動手,普遍瞭解更多不同的事物和技能。

於是, MakerCup 這個社群出現了,每週四都會舉辦一場分享交流活動或是小聚會,讓 Maker 平日下班或閒暇時,可以來走走坐坐,輕鬆喝點小飲料,或是現場做點東西:
https://www.facebook.com/groups/MakerCup/

我們希望,這個社群將如一碗太古時代的生命濃湯一般,熬煮出真正的 Maker 生命。

延續黑客松台灣的精神

還記得這一年,我們籌辦了整個年度的「黑客松台灣(Hackathon Taiwan)」,每個月都有 300 至 500 人的大型創作活動,讓不敢踏出來的年輕學子、上班感到無聊的人、及很少離開自己專業領域的朋友,走出來到活動上以「能力會友」。這一年的過程,讓大家的成果,從簡陋成長到真正的創意或產品,從簡單的技術到複雜的應用,從小設計到跨領域的整合。

想當初,很多人剛開始嘲笑我們的成果都像玩具,勸我們不要再辦下去,請大家白吃白喝沒有意義。但一年以後的今天,事實證明我們是對的,黑客松台灣的參加者們,有最堅實的創意、能力和執行力,能解決各式各樣的問題,就算去號稱 Maker 聖地的中國深圳,也絲毫不遜色。

更重要的是所有人都樂在其中,並把這份能力和喜悅,帶回原本的工作崗位上。

同樣的精神,同樣的想法,我們一樣將在 MakerCup 落實。我們希望讓更多人來交流,讓更多人來學習動手,共同成長,而不將只是各式各樣的發表會而已。

我們所見、期待的未來?

從商業角度,許多傳統代工廠,在初面對 Maker 時,都誤將 Maker 當作了新的客人,期望 Maker 能產出點子、找到客人,然後下單。事實上,Maker 並不應該是代工廠下的消費者,而是橫向整合者,將不同領域、需求及客群,重新整頓和安排設計,然後創造出各種新的產業型態。而對於代工廠,精緻化並不再是唯一選擇,成為各行各業的技術供應者亦是一種出路。

所以我們相信,新的世代和市場潮流,不是築一道高牆,將 Maker 拒於專業的工廠門外,而是讓 Maker 視野做廣、扎根,讓大家愛上來台灣當一個 Maker,做出許多前所未有的成果或產品。未來,肯定會有更多的企業投入、民間組織投入,技術上也會有更多模組化解決方案,或是各類的知識交流,甚至是文化交流,來支撐這樣的整合性變革。

不可否認,在 Maker 的年代,什麼產業都將會是科技業,也都會是混血兒產業,誰能迎合這樣多族群共榮,誰就能在這時代中發光發熱。

歡迎加入我們!

MakerCup 社群是由黑客松台灣(Hackathon Taiwan)的部分成員共同推動的,感謝背後有更多合作單位或是朋友的陸續協助和資助,如黑客松台灣講師發起的創作學校「LetSchool」、「聯發科 MediaTek」、「Seeed Studio」、「緯創 Wistron」、「台灣品牌協會」、「台灣土地開發」及「卡市達創業加油站」。不久的將來,還有「Node.js Party」、「IoT Taiwan 社群」、「MakerBot」或是「品酒社群」在這一望無際的場地裡當鄰居。

更多需要感謝的朋友們,不勝枚舉,也歡迎更多人共襄盛舉這樣具有台灣風味的「圓山社群觀光夜市」。

不多說了,先來一杯 Maker 吧!

2015年11月1日 星期日

Lantern 專案:快速打造屬於自己的 Isomorphic 網站服務

Standard

話說,Isomorphic 一直是 Node.js 開發者的夢想,期望同一套程式碼前後端都可以使用,大幅簡化程式碼和加速開發。此外,動態網頁的 SEO 問題也可以同時獲得解決,許多效能問題也可以得到改善。但是,要實現 Isomorphic 的架構,有很多的問題得先解決,會花大量時間在前期工作上,往往讓許多開發者頭痛。

儘管頭痛,仍然阻止不了大家往 Isomorphic 的世界前進,我也因此建立了一個專案「Lantern」,希望能讓更多人能以 Isomorphic 架構,快速建構出自己的網站服務,省去許多前期工作的時間。該專案是一個網站服務的樣板,實作了會員系統、權限管理、第三方登入、多國語系和送信機制等功能,在使用者介面上也做了一個還算美觀的介面。基本上,開發者只要 clone 下來,然後修改設定檔或改改介面、增加點功能,就可以快速完成一個屬於自己的全新網站服務。

最特別的是,「Lantern」整合了現今所有最新的技術和概念,包括了 Koa、React、FLUX、ES6/7+、Webpack 以及 Semantic UI,大量運用了 Generator、class 及 decorator 等最新 JavaScript 語言特性來簡化設計。所以,如果你想要接觸最新的技術,完全可以透過修改「Lantern」專案來學習和熟悉。

目前「Lantern」支援 Facebook 剛發佈的最新 React v0.14+ 和 react-router 1.0.0+,也避免使用像 redux 這類反 FLUX 原始設計的框架,讓原本熟悉 React 和 FLUX 架構的開發者,可以快速上手。也提供一些常見的 Extension,方便開發者寫出前後端通用的程式碼,大多數情況下,開發者不需思考程式碼運行在前端還是後端。

快速安裝使用

若想要使用「Lantern」,方式很簡單,先從 Github 取得程式碼:
git clone git@github.com:cfsghost/lantern.git

安裝必要之 NPM 模組:
npm install

使用 webpack 編譯專案(若要正式上線,可加上 -p 選項來編譯):
webpack

運行網站服務:
node app.js

最後可以使用瀏覽器開啟網址,確認是否成功:
http://localhost:3001/

修改設定檔

一般情況,你無需做任何設定就可以把服務跑起來,但如果你需要修改網站名稱、使用自己的第三方登入設定以及電子郵件伺服器,可以修改 Lantern 的設定檔。設定檔是 JSON 的格式,相當容易修改。


  1. 只要進入到「configs」目錄
  2. 把「general.json.default」複製一份並更名為「general.json」
  3. 修改「general.json」內的設定
  4. 重啟服務

目錄架構

如果你想要開始客製化網站服務,需要先簡單理解「Lantern」的目錄架構。
  • src - 主要程式
    • js - 頁面部分的程式
    • img - 存放圖片
    • less - CSS 原始碼
    • translations - 存放多國語言的對應表
  • routes - 主要為 Restful API
  • lib - 後端的相關函式庫(資料庫、第三方認證、發送電子郵件等功能)
  • models - 資料庫 Schema

快速上手開發

首先記得,只要你修改了「src」底下的任何檔案,你必須重新執行「webpack」來進行編譯。或是可以跑一個「webpack -w」在背景,讓 webpack 在檔案有變更的時候自動重新編譯程式碼:
webpack -w

一般來說,我們會從頁面修改和增減開始進行客製化工作。由於「Lantern」是採用 React 來繪製頁面,所有的頁面程式都將放在「src/js/components」底下,只要看到副檔名為「.jsx」的檔案,就分別是各種畫面上的元件。

建立新的頁面

建立頁面需要修改「src/js/routes.js」,加入一個網址及對應的頁面元件(以 Chatroom.jsx 為例):
module.exports = [
    // 省略 ...
    {
        path: '/chatroom',
        handler: require('./components/Chatroom.jsx')
    }
];

接著可以建立「src/js/components/Chatroom.jsx」檔案,開始設計你的頁面。如果需要使用 FLUX 的機制,可以載入並引入「Lantern」所提供之 decorator 到你的 React 元件上:
import React from 'react';
import { flux } from 'Decorator';

@flux
class MyComponent extends React.Component {
    constructor() {
        super();

        this.state = {
            messages: []
        };
    }

    componentWillMount() {
        this.flux.on('state.Chatroom', this.flux.bindListener(this.onChange));
    }

    componentWillUnmount() {
        this.flux.off('state.Chatroom', this.onChange);
    }

    onChange = () => {
        var store = this.flux.getState('Chatroom');

        this.setState({
            messages: store.messages
        });
    }

    render() {
        return <div>{this.state.messages}</div>;
    }
}

export default MyComponent;

開發自己的 Actions 和 Stores

假設你已經很了解 FLUX 的開發模式,你可以直接開始設計 Action 和 Store。對「Lantern」而言,無論是 Action 和 Store 都是一樣的東西,只不過執行的順序不一樣。

建立 Action(放在 src/js/actions/chatroom.js):
export default function *() {
    this.on('action.Chatroom.say', function *(name, message) {
        this.dispatch('store.Chatroom.addMessage', name + ':' + message);
    });
}; 

建立 Store(放在 src/js/stores/chatroom.js):
export default function *() {
    // 初始化一個 state 用來存放 store 的資料
    var store = this.getState('Chatroom', {
        messages: []
    });

    this.on('store.Chatroom.say', function *(msg) {

        // 加入新訊息到 store
        store.messages.push(msg);

        // State(Store) 已經更新,React 元件會被觸發更新
        this.dispatch('state.Chatroom');
    });
}; 

最後在「actions/index.js」和「stores/index.js」分別載入新建立的 Action 和 Store:
export default {
    // ...省略
    chatroom: require('./chatroom')
}; 

存取 Restful API

「Lantern」提供了統一的方法呼叫 Restful API,無論前端還是後端都可以使用(在 Store 或 Action 中),此外,如果在後端使用呼叫,該方法會自動接續使用者的 Session (登入)狀態,進行 Restful API 存取。使某些使用者登入後才可存取的 API,更為容易被存取。
export default function *() {
    this.on('store.Chatroom.getMessages', function *() {
        var store = this.getState('Chatroom');

        try {
            var res = yield this.request
                .get('/apis/messages')
                .query();

            // 取得聊天室訊息,並更新到 store
            store.messages = res.body;

            // State(Store) 已經更新,React 元件會被觸發更新
            this.dispatch('state.Chatroom');
        } catch(e) {
            switch(e.status) {
            case 500:
            case 400:
                console.log('Something\' wrong');
                break;
            }
        }
    });
};

在畫 React 元件前先預載資料

後端要把畫面送到瀏覽器前,有時需要先資料庫的資料載入,預先植入畫面之中,前端有時也需要預先載入一些資料,以便畫面宣染時有實質內容。我們可以透過載入「@preAction」這個 decorator 來達成這個需求。「@preAction」會在元件初始化前,先去執行一些工作。

底下範例是利用「@preAction」去跑 FLUX 裡的 Action - 「Chatroom.fetchMessages」:
import { preAction } from 'Decorator';

// 相當於 this.flux.dispatch('action.Chatroom.fetchMessages')
@preAction('Chatroom.fetchMessages')
class MyComponent extends React.Component {
    // ...
}

當然可能要預先做的工作不只一項,而且可能要帶入 React 元件的 props 或更多資訊到 Action 中。「@preAction」可以被帶入函數,作更複雜的設計:
@preAction((handle) => {
    handle.doAction('Chatroom.fetchMessages');
    handle.doAction('Chatroom.doSomething', handle.props.blah, 123);
})

因為 Store 會因為「@preAction」而被更新、有資料,這時就可以理所當然地在元件初始化時直接取用 State(Store)的內容。
class MyComponent extends React.Component {
    constructor(props, context) {
        super();

        this.state = {
            messages: context.flux.getState('Chatroom').messages;
        };
    }
    // 省略 ...
}

動態載入 JavaScript 或 CSS 檔案


很多 JavaScript 或 CSS 檔案是隨著 React Component 的載入,才會被動態載入,有時甚至需要照順序載入。此外,通常這樣的機制比較多會被使用在前端瀏覽器的頁面上,同樣的載入程式碼工作,在後端 Rendering 時往往會壞掉而無法通用,這在 Isomorphic 的架構中往往需要特別處理,像是判斷執行期是在前端還是後端,相當麻煩。

為此,「Lantern」提供了「@loader」這個 Decorator,使開發者可以容易引入動態載入的機制,而且不用思考前後端的問題,也可以控制載入順序,或是等待檔案載入完成。

以下範例就是一個載入地圖 API 的範例,載入工作只會在前端執行,不會在後端執行:
import { loader } from 'Decorator';

@loader
class MyMap extends React.Component {

    componentWillMount() {
        // Loader 在後端不會有任何作用
        this.loader.css('https://example.com/css/test.css');
    }

    // componentDidMount 只會在前端觸發
    componentDidMount() {
        this.loader.css('https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.5/leaflet.css');
        this.loader.css('https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-minimap/v1.0.0/Control.MiniMap.css');

        this.loader.script([
            'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.5/leaflet.js',
            'https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-minimap/v1.0.0/Control.MiniMap.js'
        ], function() {
            // 初始化地圖 ...
        });
    }

    render() {
        return 
; } }

取得和監聽視窗資訊


為了更方便前端排版,尤其是需要滿版的設計時,我們往往需要得知或監控瀏覽器視窗的大小,通常做法是存取瀏覽器中的「window」物件,並監聽事件來達成。但「window」物件只在瀏覽器上存在,在後端如果存取該物件,會失敗而且有錯誤發生,在以往 Isomorphic 架構中,每次都要特別處理,相當麻煩。因此「Lantern」預設提供了一個名為「Window」的 Store,將這類資訊包裝起來,使 React Component 能輕易存取又不會因在後端或前端而出現問題。

下面範例就是存取 Window 的例子,以及監聽視窗大小改變的事件。
@flux
class MyPage extends React.Component {
    constructor(props, context) {
        super();

        var win = context.flux.getState('Window');
        this.state = {
            winWidth: win.width,
            winHeight: win.height
        };
    }

    componentWillMount() {
        this.flux.on('state.Window', this.flux.bindListener(this.updateDimensions));
    }

    componentWillUnmount() {
        this.flux.off('state.Window', this.updateDimensions);
    }

    updateDimensions = () => {
        var win = this.flux.getState('Window');
        this.setState({
            winWidth: win.width,
            winHeight: win.height
        });
    }

    render() {
        return 
{this.state.winWidth}x{this.state.winHeight}
; } }

看不懂很多 ES6 和 ES7 的東西?

這邊已經整理了一些常用的對應表「ES6 and ES7」,方便開發者理解其中的語法。

更多文件和說明

更多資訊可以參考 Github 上的 Wiki:

後記

其實「Lantern」已經改版了幾次,因為之前在好幾個要上線的專案上,每次都發現有些許不足之處,所以就不斷翻新架構和改進,甚至是優化效能。到目前為止,大致已經算是穩定的狀態,未來的開發方向不外乎是繼續寫 Isomorphic 的 Extension,以及效能優化。

如果你有興趣,歡迎加入並共同改善這個專案。:-)

2015年9月30日 星期三

Git 大哉問:如何為 Fork 出來的專案,同步上游的更新?

Standard


搭配使用 Git 進行開發工作,時常會碰到一個狀況,就是我們 fork 一個專案出來修改,但在我們在修改的同時上游有了更新,這時我們會想要把上游的更新同步下來。這是一個常見的問題,許多人不時會提出來詢問,事實上如果你去 Google ,多半能找到這樣一篇名為「Syncing a fork」的 Github 文件。雖然這篇文章已經把程序詳細列出來了,但還是有人看不太懂,原因是要搭配「Configuring a remote for a fork」這一篇文件一起看才知道來龍去脈。

簡單來說,我們要先把「上游(upstream)」的 repository 加入我們眼前正在修改的專案,然後把上游更新拉回來,最後再與我們現有程式碼合併。

首先,加入上游的 repository 並命名為「upstream」:
git remote add upstream https://github.com/YOUR_USERNAME/YOUR_FORK.git

未來想要更新前,可以使用 fetch 去拉上游的更新回來:
git fetch upstream

最後再把 upstream 的內容,與現有的正在修改的進行「合併」:
git merge upstream/master

2015年9月20日 星期日

Fluky - 打造 Isomorphic App 的副產品:一個基於事件驅動的 Flux 框架

Standard

要講到 Fluky 這一個 Flux 框架,這要從我的一個新計畫說起。因為最近又重新興起了一波 Isomorphic App 的熱潮,許多人開始打造了自己的 Isomorphic 網站,自己也做了一個。而什麼是 Isomorphic 呢?簡單來說,就是寫一次程式,然後前後端都可以使用的機制,也是一個網站服務工程師的夢想。還記得過去自己曾實作了 frex.js 試圖達成 API 層面的 Isomorphic,現在 React 這樣的前端框架,更提供了一個打造前後端 Rendering 的 Isomorphic,使得原本在前端動態產生的畫面,可以在後端產生,更一舉解決了 SEO 的問題。

話說,搭上了這波熱潮,最近開始土炮自己的 Isomorphic App,Github 上開啟了一個「Lantern 燈籠」專案,希望做一個標準的專案架構,讓自己以後開發新專案時,可以不需要重新再來一遍。痛苦的是,與很多人一樣,踩到了很多地雷,在專案架構設計上,也一直有很多許要調整的地方,這也難怪,畢竟這是一個興新的開發概念。

於是從零到有的過程中,也有許多副產品,其中包括了一個新的 Flux 框架「Fluky」。很多人問我為什麼不採用當今紅遍半天邊的「redux」,原因其實很簡單,我不想脫離傳統 Flux 模式和 React 開發的概念太遠,然後同時想要用試著更精簡的方式描述這些流程。另外一點是,受到過去 X11 這世界最先進的網路圖形化視窗系統的設計所啟發,打算試著全面使用「事件」來管理資料流和程式上任何的溝通。

如果你有興趣,可以直接以 NPM 來安裝這個 Flux 框架:
npm install fluky

Fluky 的設計


基本上, Fluky 本身的概念很簡單,幾乎所有的行為都是透過 Fluky.dispatch() 這個 API 來進行,包括呼叫 Action、Store,然後所有的行為都可以使用 Fluky.on() 所監聽。也就是說,只需要這兩個 API,幾乎就已經足夠。對於 View 的工作而言,永遠就是呼叫 Fluky.dispatch('action.*') 和監聽 Fluky.on('store.*') 。

這樣設計有什麼好處呢?因為所有的訊息和命令傳遞,都有統一的事件機制和命名規則,理論上來說,事件可以很容易被提到前端或是放在遠端被處理,這就有點像 X11 的設計,可遠端也可本地端進行圖形繪製處理。不過對 Fluky 來說,這目前還算太遠了,目前 Fluky 還沒有真正處理太過複雜的狀況,純粹就是以完全的事件化來處理資料流。

也因為一切都事件化,就可以良好支援 Isomorphic 的設計,例如很多的 Action、Store 工作,可以有個前後端統一的命名和呼叫方法,在 Server 預處理,便於 Server Rendering 的使用,甚至是一部份在前端做,一部份在後端做都有可能。最重要的是,在 Isomorphic 上會碰到的前後端 state 不一致的狀況,也可以很容易使用事件、或是在事件分配中的空擋,進行 state 同步而獲得解決。

此外,為了嘗試新技術,Fluky 也在前端引入了 Generator ,所以如果你想要使用 Fluky,要確保前端瀏覽器能使用 ECMAScript 6+ 最新的標準,或是你必須安裝 babel 模組來打包並轉換你的程式碼為 ES5。

講了這麼多,怎麼使用 Fluky 呢?下面將以實作一個簡單的 TODO 清單為例。

建立 Action

import Fluky from 'fluky';

Fluky.on('action.Todo.toggle', function *(todo) {
  // 用不同的 store 方法處理
  if (todo.completed)
    Fluky.dispatch('store.Todo.unmark', todo.id);
  else
    Fluky.dispatch('store.Todo.mark', todo.id);
});

建立 Store

var todoStore = Fluky.getState('Todo', {
  todos: [
    {
      id: 1,
      name: '寫一篇文章',
      completed: false
    }
  ]
});

Fluky.on('store.Todo.unmark', function *(id) {

  // 找到指定的 TODO 項目
  for (var index in todoStore.todos) {
    var todo = todoStore.todos[index];

    if (todo.id == id) {
      // 改為未完成
      todo.completed = false;

      // 發出 store 已改變的事件
      Fluky.dispatch('store.Todo', 'change');
      break;
    }
  }
});

Fluky.on('store.Todo.mark', function *(id) {

  // 找到指定的 TODO 項目
  for (var index in todoStore.todos) {
    var todo = todoStore.todos[index];

    if (todo.id == id) {
      // 改為完成
      todo.completed = true;

      // 發出 store 已改變的事件
      Fluky.dispatch('store.Todo', 'change');
      break;
    }
  }
});

在 React 元件內的使用

import React from 'react';
import Fluky from 'fluky';

class TodoList extends React.Component {

  constructor() {

    // 取得 Todo 的 Store,從 Fluky 的 state 資料暫存區
    this.state = {
        todos: Fluky.getState('Todo').todos;
    };
  }

  componentDidMount() {
    // 監聽 store 的改變事件
    Fluky.on('store.Todo', Fluky.bindListener(this.onChange));
  }

  componentWillUnmount() {
    // 停止監聽 store
    Fluky.off('store.Todo', this.onChange);
  }

  onChange = () => {

    // 當 store 有改變時更新元件的 state
    this.setState({
      todos: Fluky.getState('Todo').todos;
    });
  }

  toggle = (todo) => {
    // 呼叫 Action 去切換工作項目狀態
    Fluky.dispatch('action.Todo.toggle', todo);
  }

  render: function() {
    var todoList = [];

    // 印出所有的工作項目
    this.state.todos.forEach((todo) => {
      todoList.push(
{todo.text}
); }); return (
{todoList}
); } }

State 資料暫存區的設計

傳統的 Flux 做法,不外乎是載入所要的 Store 檔案,來取得 Store 資料,這樣做不但麻煩且囉唆。既然事件分配器(Event Dispatcher)是 Singleton(只存在一個實例,所有人共用),將 Store 的資料共同管理顯然是比較簡單的做法,然後只需要統一使用 Fluky.getState() 就可以取得所需要的 Store 資料。

如果從前述範例來看,Fluky.getState() 可以帶兩個參數,第一個是 State 的名稱,第二個是當 State 不存在時,其預設值。

當然,這個暫存區是可以整個取出來的,也可以使用 Fluky.setInitialState() 或是藉由 window.Fluky.state 在第一時間載入時整個放回去,這可以應用在解決 Isomorphic App 的前後端 Store 不同步的問題。

後記


新專案「Lantern 燈籠」目標就是嘗試開發一個 Isomorphic 的網站服務,並使用最新的技術,此外,也希望開發一些基本功能(如:會員系統、第三方登入、權限管理等),方便日後開發新網站服務時,可以避免早期的苦工和踩地雷。這是一個 Open Source 專案,如果你有興趣,可以一同開發。:-)

2015年8月28日 星期五

Node.js 的單執行緒(Single Thread)設計,到底有什麼優點?

Standard

這是個總有人一問再問的問題,到底 Node.js 這樣單執行緒(Single Thread)的設計,到底有什麼優點?為什麼總是有人說,它比傳統多執行緒的設計來得有效率?以往,一旦開始討論這個問題,總是會有人開始提到 Context Switch、Asynchronous 等機制,越講越玄也越講越複雜化。

其實我們可以用簡單的餐廳比喻,就能理解 Single Thread 加上事件驅動(Event-driven)的機制,如何與傳統設計不一樣。

場景想像

試想一個場景:一間餐廳有 100 個座位,然後來了100個客人。

處理方法

身為老闆的你,你會選擇哪種方式服務這些客人:

  1. 請100個服務生一對一服務這些客人。
  2. 請 25 個服務生,看當下狀況服務這些客人。
一般來說,傳統的多 Multi-thread 的設計就類似方法 1,而 Single-thread 且 Event-driven 的設計就是方法 2。

通常大多數情況我們會選擇方法 2,因為客人通常都是處於等待(看菜單、等上菜、吃自己)的情況,並不需要服務生貼身服務。所以即便請 100 個服務生,這些服務生大多數時間也只是等在那也佔地方,而服務生眾多也導致服務生之間的溝通和互動其實不易,更不容易交換資源,回報和協同工作難以進行。反而安排一個小型的外場班,讓裡面的人合作見機行事,會比派出 100 個各自獨立的人來得好。

併發數高的原因

併發數(concurrent request)指的是單位時間內可以處理的要求量,一般用來評估一個網路應用程式的效能。而通常在網路服務裡,併發數也相當於單一時間內能服務的連線數量。

所以,以前面餐廳外場班的模型來說,如果你有 100 個服務生,就可以服務 400 個客人。換句話說,同樣的資源,能處理的併發數(concurrent requests)也就比較高。

後記

不過如果你開的是酒店或按摩店,那可能就要請一百位服務生了。:-)

2015年7月13日 星期一

Geek?技客?是什麼?我不宅,我用動手代替說話!

Standard

因為我對外都自稱一個 Geek,所以時常有人問我,Geek 是什麼?一直以來,「技客(Geek)」個名詞讓許多人感到陌生,甚至對這詞彙一知半解的人,都以為 Geek 與普通宅宅無異,更甚至是覺得這只是另一種宅宅的說法。的確,Geek 某些地方與普通宅宅很相近,指的是醉心於特定專業或領域的人,但這些人擁有絕佳才能,只是為了鑽研知識或研究新事物可以幾乎荒廢其他事,廢寢忘食對他們來說只是小意思,甚至可以犧牲人際關係等一般正常社會交際行為。由此可以看出 Geek 對於自有興趣的事物,將會多努力去鑽研。

不可否認,「技客(Geek)」這個詞在早期帶有貶意,但在現今社會中,人們交流的手段和方法已經透過科技有很大的改變,Geek 可以透過網路或各種新方法,找到能分享交流的同好朋友。透過網路串連,他們不再只是群體中被傳統主流排除在外的人,反而搖身一變,變成強力促使世界進步的主要群體。如今,大家眼中的 Geek ,代表著才能與努力,更代表能「動手完成任務」的傑出人們。開玩笑的說,專心致志(宅宅)加上真正動手實做的能力,就是「技客(Geek)」。

想要成為一個 Geek 嗎?只要你符合這樣的條件,無論你原本是個技術 Hacker、Maker 還是個設計師,更甚至是任何領域的人,都可以稱自己是一個 Geek,甚至可以以此為榮!也許,你也有自己沒發現,但是能堪稱 Geek 的一面也說不定!

後記

為了讓更多 Geek 能聚集在一起交流,共同迸出創新的火花,我們 Hackathon Taiwan 開始了一個新計畫 GeekBar I/O,將嘗試透過各種活動和方式,讓 Geek 們共同發光發熱!歡迎大家共襄盛舉!

2015年7月8日 星期三

快樂玩 ES6 Generator,從 co 起手式開始

Standard

自從 Node.js 0.12 版和 io.js 之後,大量的開發者開始了各自的 ECMAScript 6 大冒險,許多人對 Generator 的使用仍跌跌撞撞,對於這種看似「同步(Synchronous)」的「異步(Asynchronous)」機制,有許多人腦袋遲遲無法轉過來。雖然在小弟的書(參閱連結:新書報到!Node.js 模組參考手冊!)已經有清楚的說明 Generator 使用方法,但就許多讀者回函來看,對於 JavaScript 越是熟悉的人,越無法直觀理解 Generator 的思維,甚至是老是抓不准使用的時機點。

尤其是過去我們已經有 Promise、async、Q 和 bluebird 等處理非同步程式流程的模組和工具,很多人就是覺得沒有使用 Generator 的必要。不過,如果你會使用 co 模組,你會突然發現若是將過去的流程機制與 Generator 相搭配,程式開發將變得更為流暢。

若想要安裝 co 模組,可以直接以 NPM 下載:
npm install co

本文接下來會說明一些 co 的基本使用,破除一些 Generator 難以使用的地方,讓開發者們更容易開始 Generator 的旅程。

讓原生 Generator 更好用

這是很多人不喜歡使用 Generator 的主因,以往為了使用 Generator,我們還要先建立一個 Generator 的函數,然後不時的去處理 Generator 所返回的資訊,一遍又一遍進入 Generator 之中。因此,無論 Generator 再好用,這些麻煩的動作也完全抵銷他的優勢,大多數人還不如回到舊的流程控制方法,以免徒增自己的麻煩。

而如果使用 co,可以直接將 Generator 函數當作「立即函數使用」,其餘的部份我們可以不需要擔心:
var co = require('co');

co(function *() {
    console.log('Inside'); 
}) 

再也不用煩惱 yield!

以前光是 Promise,就已經讓很多人詬病,覺得每次使用 Promise 都要花許多時間對函數進行包裝,而 Generator 也有類似的問題,若是要使用 yield,更是一件大工程。於是,co 幫開發者做了些設計,讓 Generator 可以直接利用 yield 去跑 Promise 類型的函數、陣列等物件,因為是 yield,其執行模式是「異步」,不會阻塞事件引擎。

co 支援 Trunks,也就是回傳一個簡單函數進行異步工作,範例如下:
var co = require('co');

function updateInfo() {

    // 返回一個將被執行的函數 
    return function(done) { 

        // 可以執行各種異步工作
        setTimeout(function() {

            // 完成工作後執行 callback,參數一為錯誤訊息,參數二為回傳值
            done(null, 'Done'); 
        }, 1000);
    };
}

co(function *() {
    // 執行並等待回傳值 
    var ret = yield updateInfo();

    // 一秒後收到回傳值並印出來
    console.log(ret); 
});

支援 Promise 的 yield

如果你熟悉 Promise,這是個好消息, co 讓 yield 可以直接吃 Promise:
co(function *() {
    var ret = yield Promise.resolve(true);
});

直接使用 yield 跑一整個陣列的工作

前面提到 co 讓 yield 也支援陣列,所以我們可以準備一系列的工作,放在陣列中讓 yield 去處理:
co(function *() {
    var a = Promise.resolve(true);
    var b = Promise.resolve(true);
    var c = Promise.resolve(true); 
    var ret = yield [ a, b, c ];
});

異步處理一個陣列裡的所有元素

這大概是以往 JavaScript 最頭痛的工作,後來有了 async 這類模組後,才比較容易處理,不然光使用 forEach 或是 for 迴圈來做,碰到較大的陣列,往往使程式阻塞卡死。如果使用 co 前面的起手式,我們可以試著這樣做(如果不使用 Promise):
var arr = [ 1, 2, 3, 4, 5 ];

function handle(el) {

    return function() {
        // 處理傳入值 
        console.log(el); 
    };
} 

co(function *() {

    // 在 Generator 裡,使用 yield 不會使 JavaScript 事件引擎阻塞,
    // 但因為程式流程會等 yield 結束,for 迴圈也不會造成阻塞
    for (var index in arr) {
        var el = arr[index];
         yield handle(el);
    }
});

後記

經過 co 包裝後的 Generator 非常好用,無論你過去習慣用哪一種流程控制的方式,都可以很好的轉換並整合過來。試試看吧!:-)