tag:blogger.com,1999:blog-43894612346074182032024-03-14T16:13:24.529+08:00Fred's blogNever Stop Researching New GoodsFred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.comBlogger405125tag:blogger.com,1999:blog-4389461234607418203.post-43122057743148290662019-10-31T21:12:00.002+08:002019-10-31T21:15:41.859+08:00從 IT 轉型看微服務<p>從電腦、資訊科技開始普及後,每過一段時間,企業界就會開始出現新的 IT 轉型計畫,以整頓整個資訊系統、商業運行模式,然後試圖找出下一個世代的新方向。每一個具規模的企業,都生怕與新世代的技術和商業模式脫鉤,於是砸重金、拚人力,說什麼也要追上世界的腳步。當然,中間的技術供應鏈也不斷在改變,上一世代的王者或是贏家,在下一個世代可能不一定跟得上,因此身為終端用戶的企業,每當面臨新的 IT 轉型計畫,往往也跟無頭蒼蠅般, 不知所措甚至不小心會做錯了決定。</p>
<p>回到十幾二十年前,你也許可以有機會一次性把一個大系統整個替換掉。但在今天資訊系統高度介入管理的時代,版本更迭無數次的平台,沒有一個系統能在一夕之間被整個換掉,哪怕你的品管和測試做得非常足夠,甚至是超標達成,也哪怕你事先的需求訪談談到祖宗十八代都瞭然於心。</p>
<p>這常常是系統設計者和規劃者的誤區,看見許多表面問題,於是設計了一套全新、看起來完美的解決方案。而剩下沒照顧到的部分,都覺得只要能配合測試就可以邊走邊補足。事實上,現代資訊系統的維度和複雜度,已經超出人類的想像,太多的例外、不可控、溝通執行問題,遠遠超過理論上的執行方法。</p>
<p>所以從系統規劃設計的角度來看,我們總是要慢慢來,一段段改善、替換,最終達成全面換血。而且越是龐大複雜且重要的系統,越要切割得越細,透過降維度的方法,來慢慢替換。所以我們在討論IT轉型,從一個肥大的系統換到另一個肥大的系統,已經不會是選項,系統架構的去耦合、高擴展性甚至是服務不中斷才是考量的重點。</p>
<p>這也是為什麼「微服務架構」在近年來特別熱門,微服務真正在討論的不是容器化,而是在討論的是怎麼讓一個龐大的系統,更容易受到管控、維護和擴充,除了可以更容易實現災難隔離、例外狀況、整體高可用性,也可以更有彈性做各種變化。</p>
<p>更重要的是,微服務架構下,其系統風險管理上,從在開發階段開始,每個部分就已經受到隔離和管控。也由於每個被拆解後的系統都非常小,遵循 SRP 原則,在開發維護上相對容易,也可以保證每個單元有更低的問題發生率。至少,你不會因為一顆螺絲或換一顆螺絲,導致整個塔倒塌。</p>
<p>不過微服務架構卻也不是說要做就能做,對於規劃設計者來說,他必須很了解各種新舊技術議題、有能力做實務執行面的評估,也需要耐心做各種服務拆解梳理和引入正確的 Pattern 及技術,甚至很多時候要有走一步是一步且可以快速反應決策的能力。這些都不是隨便讓工程師去上個 DDD(Domain-driven Design)或是各種微服務設計課程,回來就能掌握的技能。</p>
<p>以目前業界情況來說,由於微服務帶來的衝擊範疇,涉及了系統設計、開發管理甚至是業務規劃,將橫跨基礎設施(Infrastructure)、應用開發(Application Development)到終端使用者(End-user)的負責團隊。由於要相互配合的人太多,以致導入會是一個長期計劃,所以對於多數有意導入微服務的企業而言,到目前為止,最多也只能做到容器化而已。畢竟,微服務架構說白了,就是去中心化的分散式架構,這對傳統的系統整合設計人員、開發人員來說,都是相對陌生的領域。</p>
<p>真正要能普及微服務,必須等到從企業內部到外部廠商,都能充分理解和懂得微服務架構方法,才能真正的落實,而這段路才剛開始而已。</p>
<p>對了,別忘了微服務後,下一段還有邊緣運算(Edge Computing)等著大家。</p>Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com3tag:blogger.com,1999:blog-4389461234607418203.post-29908811970780405122019-09-04T01:46:00.000+08:002019-09-04T01:46:15.214+08:00GitLab DevOps:Kubernetes 整合的 Applications 裝不起來怎麼辦?<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-osXbPeuJC5oLIaxZUAQHM2bTfK8NCMnhaM1ofPGbArKlkM_2Do0u2wQhntFTBlaQNhTLcXzYkpiYtFVE8FO3rK7JjX159CR-hlOQOc4Z_RGFuBimj6PU14XtBjo96CS0zstkzziwa-A/s1600/gitlab-oops.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-osXbPeuJC5oLIaxZUAQHM2bTfK8NCMnhaM1ofPGbArKlkM_2Do0u2wQhntFTBlaQNhTLcXzYkpiYtFVE8FO3rK7JjX159CR-hlOQOc4Z_RGFuBimj6PU14XtBjo96CS0zstkzziwa-A/s1600/gitlab-oops.png" data-original-width="1024" data-original-height="768" /></a></div>
<p>我們的平民化旅遊黑卡秘書服務 LiMaGo,系統已經開發了兩年,有一定的複雜度,一直以來在維護上其實都有一定困難。此外,不知道是好消息還是壞消息,由於我們開始拓展日本等其他國家的業務,也加強了行銷通路,所以業務數字持續增加,導致各種功能和後勤機制也持續在擴張。還有,許多隱藏尚未公開的新計畫,整個系統複雜度不斷的再增加,慢慢的一切都開始混亂。</p>
<p>所以,為了重整系統,以及重建開發團隊的各種資源和管理流程,開始試圖引入了 Kubernetes 和 GitLab,並試著將 DevOps 流程給搭建起來。畢竟,自己的另外一家公司,是微服務和容器化的解決方案供應商,自己手上的東西要是沒有導入,也太講不過去。還好轉換的問題不太大,LiMaGo 的系統從第一天設計,就是採用微服務架構(Micro-service Architecture),甚至當時還設計了一套框架「Engined」,可以在 Node.js 上更容易開發微服務架構,只要專心在容器化和 DevOps 的工作上即可。</p>
<p>於是動手把 Kubernetes 架好,也安裝好 GitLab CE。下一步,就是把 Kubernetes 的設定加入 GitLab,讓 GitLab 可以把 GitLab Runner 和 DevOps 相關機制跟手上的 Kubernetes 整合起來。</p>
<h2 id="toc_1">糟糕!Applictions 裝不起來!</h2>
<p>原本一切很順利,但在 GitLab 設定頁裡面的最後一個步驟「在 Kubernetes 上安裝 Applications」遭遇到莫名失敗,一開始是 Ingress 裝不起來,後來發現 Prometheus 也裝不起來,如下圖:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgq9LabcJqNx3uKmQUs-HZ1WPzGIuvyJ_tJyAXpNdv6HjryVJFZI355gfPgCSA9_C1Br920DG5L9uzKSMnFT0_mDQX_BC1i6sA05u-DlFlf2vkpjLCVpNW5mYbcjotajaIhY_UqhW6ABC4/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2019-09-04+%25E4%25B8%258A%25E5%258D%258812.20.31.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgq9LabcJqNx3uKmQUs-HZ1WPzGIuvyJ_tJyAXpNdv6HjryVJFZI355gfPgCSA9_C1Br920DG5L9uzKSMnFT0_mDQX_BC1i6sA05u-DlFlf2vkpjLCVpNW5mYbcjotajaIhY_UqhW6ABC4/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2019-09-04+%25E4%25B8%258A%25E5%258D%258812.20.31.png" width="640" height="230" data-original-width="1600" data-original-height="575" /></a></div>
<p>透過 <code>kubectl</code> 直接去 Kubernetes 裡面挖 logs ,得到了錯誤訊息:</p>
<div><pre><code class="language-shell">$ kubectl logs install-prometheus -n gitlab-managed-apps -f
+ helm init --upgrade
Creating /root/.helm
Creating /root/.helm/repository
Creating /root/.helm/repository/cache
Creating /root/.helm/repository/local
Creating /root/.helm/plugins
Creating /root/.helm/starters
Creating /root/.helm/cache/archive
Creating /root/.helm/repository/repositories.yaml
Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com
Adding local repo with URL: http://127.0.0.1:8879/charts
$HELM_HOME has been configured at /root/.helm.
Tiller (the Helm server-side component) has been upgraded to the current version.
+ seq 1 30
+ helm version --tls --tls-ca-cert /data/helm/prometheus/config/ca.pem --tls-cert /data/helm/prometheus/config/cert.pem --tls-key /data/helm/prometheus/config/key.pem
Client: &version.Version{SemVer:"v2.14.3", GitCommit:"0e7f3b6637f7af8fcfddb3d2941fcc7cbebb0085", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.14.3", GitCommit:"0e7f3b6637f7af8fcfddb3d2941fcc7cbebb0085", GitTreeState:"clean"}
+ break
+ helm upgrade prometheus stable/prometheus --install --reset-values --tls --tls-ca-cert /data/helm/prometheus/config/ca.pem --tls-cert /data/helm/prometheus/config/cert.pem --tls-key /data/helm/prometheus/config/key.pem --version 6.7.3 --set 'rbac.create=true,rbac.enabled=true' --namespace gitlab-managed-apps -f /data/helm/prometheus/config/values.yaml
E0902 10:50:18.229952 28 portforward.go:372] error copying from remote stream to local connection: readfrom tcp4 127.0.0.1:42953->127.0.0.1:44820: write tcp4 127.0.0.1:42953->127.0.0.1:44820: write: broken pipe
Error: UPGRADE FAILED: "prometheus" has no deployed releases
UPGRADE FAILED
Error: "prometheus" has no deployed releases</code></pre></div>
<p>雖然乍看起來是連線的問題,但我確定網路應該沒什麼問題,而且我手動操作 helm 都是正常的。而且在 GitLab 頁面上,不管我怎麼重試,問題依舊。</p>
<h2 id="toc_2">Workaround: 直接連線到 Tiller 解決</h2>
<p>上網試圖找了一些解決方法,卻都找不太到(大概是我關鍵字下的不對),最後決定試圖去直接操作 gitlab-managed-apps 底下的 Tiller 看看。於是找到了一則 GitLab 的 Issue,討論怎麼連線到 GitLab 所自動安裝出來的 Tiller:</p>
<p><a href="https://gitlab.com/gitlab-org/gitlab-ce/issues/56591#workaround-for-now">https://gitlab.com/gitlab-org/gitlab-ce/issues/56591#workaround-for-now</a></p>
<p>文中所說的連線方法如下:</p>
<div><pre><code class="language-shell">export TILLER_NAMESPACE="gitlab-managed-apps"
# 取得 gitlab-managed-apps namespace 底下的 Tiller 憑證和金鑰
kubectl get secrets/tiller-secret -n "$TILLER_NAMESPACE" -o "jsonpath={.data['ca\.crt']}" | base64 --decode > tiller-ca.crt
kubectl get secrets/tiller-secret -n "$TILLER_NAMESPACE" -o "jsonpath={.data['tls\.crt']}" | base64 --decode > tiller.crt
kubectl get secrets/tiller-secret -n "$TILLER_NAMESPACE" -o "jsonpath={.data['tls\.key']}" | base64 --decode > tiller.key
# 連線到 Tiller 並取得已安裝的套件清單
helm list --tiller-connection-timeout 30 --tls --tls-ca-cert tiller-ca.crt --tls-cert tiller.crt --tls-key tiller.key --all --tiller-namespace gitlab-managed-apps
</code></pre></div>
<p>幸運的是,用憑證和金鑰可以順利連線並取出 helm 的資訊,然後看到 ingress 和 prometheus 都出了問題:</p>
<div><pre><code class="language-shell">$ helm list --tiller-connection-timeout 30 --tls --tls-ca-cert tiller-ca.crt --tls-cert tiller.crt --tls-key tiller.key --all --tiller-namespace gitlab-managed-apps
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
ingress 1 Mon Sep 2 16:51:47 2019 FAILED nginx-ingress-1.1.2 0.21.0 gitlab-managed-apps
prometheus 1 Mon Sep 2 17:42:41 2019 FAILED prometheus-6.7.3 2.2.1 gitlab-managed-apps
runner 1 Mon Sep 2 17:33:55 2019 DEPLOYED gitlab-runner-0.7.0 12.1.0 gitlab-managed-apps</code></pre></div>
<p>就在我打算手動砍掉失敗的項目,然後重新安裝時,突然發現其實有人在討論類似的問題,雖然碰到的問題跟我不一樣:</p>
<p><a href="https://gitlab.com/gitlab-org/gitlab-ce/issues/65326#note_198244170">https://gitlab.com/gitlab-org/gitlab-ce/issues/65326#note_198244170</a></p>
<p>下面剛好有篇原發問者自己的回文,和我的思路一樣,說他砍掉後重新安裝就解決了問題:</p>
<div><pre><code class="language-shell">helm delete --purge prometheus --tls --tls-ca-cert tiller-ca.crt --tls-cert tiller.crt --tls-key tiller.key --tiller-namespace gitlab-managed-apps</code></pre></div>
<p>很好!我也搞定了!</p>
Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com1tag:blogger.com,1999:blog-4389461234607418203.post-66048425680196818412018-07-22T17:21:00.000+08:002018-07-22T17:28:27.026+08:00有趣的洗牌演算法<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMwj2VdAxPbULTzoltJq7t96TUNDLY1L75Yu80yiOt4yn5HpnPHPS12HI7baMCARqmV1YIJFWODJYJcynpSc2hv9aeQYq2wG7-97obKJIhoDGvBuDw4-fUyJvUrZFHDAzhCnmcXBT2dXc/s1600/shuffle.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="768" data-original-width="1024" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMwj2VdAxPbULTzoltJq7t96TUNDLY1L75Yu80yiOt4yn5HpnPHPS12HI7baMCARqmV1YIJFWODJYJcynpSc2hv9aeQYq2wG7-97obKJIhoDGvBuDw4-fUyJvUrZFHDAzhCnmcXBT2dXc/s1600/shuffle.png" /></a></div>
最近因為一些專案,所以需要實做一些撲克牌的洗牌機制。雖然這個動作看起來簡單,但其實對於開發者來說相當有趣,因為真的除了做這種牌類遊戲之外,平常很少用到這樣演算法,也由於有太多種做法,不免著迷於其中。<br />
<br />
洗牌目的就是讓結果隨機、不能預期,只不過雖然很多遊戲同樣都是圍繞在亂數產生上面,但撲克牌遊戲(或麻將遊戲)最大的不同,就是同一排組每次發出來的牌,一但發過了就不會再出現一次。這一點,和每次都可以出一到六點數的骰子遊戲,就完全不一樣,不是隨機出一個亂數就可以搞定。<br />
<h2 id="toc_1">
準備工作:先準備個牌組</h2>
開始前,先準備四種花色、A 到 K 的牌組,使我們可以以 0 至 51 的號碼去取得任意一張牌。<br />
<div>
<pre><code class="language-javascript">const suits = [ 'S', 'H', 'D', 'C' ];
const points = [ 'A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K' ];
const cards = [];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 13; j++) {
cards.push(points[j] + suits[i]);
}
}
console.log(cards[10]); // 10S(黑桃10)</code></pre>
</div>
<h2 id="toc_2">
方法一:硬幹</h2>
最直覺的方法,不外乎就是不斷產生 52 張牌的亂數(0 ~ 51),然後檢查這張牌發過沒,如果牌發過了就重新產生一個新的亂數,持續這個步驟。<br />
<div>
<pre><code class="language-javascript">let shuffledCards = [];
while(shuffledCards.length != 52) {
// 取得 0 ~ 51 的亂數
let idx = Math.floor(Math.random() * 51);
let card = cards[idx];
// 檢查這張牌是否已經出現過
if (shuffledCards.indexOf(card) !== -1)
continue;
// 沒出現過則放入陣列
shuffledCards.push(card);
}
</code></pre>
</div>
雖然這樣的做法可以達成目的,但太不優雅,每次都要去掃一次過往的發牌紀錄,雖然 CPU 很快,這些繁瑣的檢查工作很快能完成,但這對開發者來說,做法太過噁心。<br />
<h2 id="toc_3">
方法二:隨機抽牌</h2>
這種方法也是種直覺會想到的方法,隨機把牌從牌組內抽出,使原本牌組的牌越來越少,直到原本的牌組被抽光。簡單來說,就是第一次抽 0 到 51 之間的一張牌,第二次抽 0 到 50 之間的一張牌,以此類推。因為牌越抽越少,我們每次產生出來的亂數範圍就越來越小。<br />
<div>
<pre><code class="language-javascript">let shuffledCards = [];
for (let left = 51; left >= 0; left--) {
// 取得 0 至剩餘撲克牌數量之間的亂數
let idx = Math.floor(Math.random() * left);
let card = cards[idx];
// 將該張牌從原牌組移除
cards.splice(idx, 1);
// 放入陣列
shuffledCards.push(card);
}</code></pre>
</div>
這種方法似乎感覺比第一種方法好多了,但一直在破壞原有的牌組陣列,這又不太妥當。對於一個慣 C 來說,乍看下尤為不舒服,因為一般來說,普通陣列是不容許這樣一直變更大小的,記憶體的重配置總是會讓程式效率大幅下降。當然,如果採用的是鏈結(Linked-list)的做法,當然就沒什麼問題,而且這個例子也不是用 C 語言的陣列實作。<br />
<h2 id="toc_4">
方法三:隨機交換</h2>
隨機交換的方法,就是幫 0 到 51 位置上的每張牌,隨機選一個排組上的位置進行兩張牌的交換,換言之就是幫每張牌隨機選一個新位置。<br />
<div>
<pre><code class="language-javascript">// 複製一份牌組陣列
let shuffledCards = Object.assign({}, cards);
for (let i = 0; i < 52; i++) {
// 隨機取一個位置
let idx = Math.floor(Math.random() * 51);
// 交換兩張牌
let temp = shuffledCards[idx];
shuffledCards[idx] = shuffledCards[i];
shuffledCards[i] = temp;
}</code></pre>
</div>
如果選到了自己怎麼辦?不換位置也是種隨機的結果。<br />
<h2 id="toc_5">
後記</h2>
對開發者來說,琢磨這樣的演算法應用,相當有樂趣,可以暫時從工作的苦悶中跳出。追求達成同樣目的,但各種不同做法,有時候其實也是在磨練自己對各種邏輯思維,以及程式技法的掌握度。Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-67315157615768179622017-09-12T18:55:00.000+08:002017-09-13T03:34:44.005+08:00打造自己的 Node.js Transform Stream<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwvLD46JMINtdPkctqsLNQuVxnPbI_ru8LNtp1pa7iyC988YZt49O0zmPy-NWg7zH2odPLJ8Mq9UpakDW7j1jdBuj-Qlv3Xp4Q71wUBlanpKRpFwliAtrlqN3QTK2xzAoYZodBk_ldky8/s1600/TransformStream.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="768" data-original-width="1024" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwvLD46JMINtdPkctqsLNQuVxnPbI_ru8LNtp1pa7iyC988YZt49O0zmPy-NWg7zH2odPLJ8Mq9UpakDW7j1jdBuj-Qlv3Xp4Q71wUBlanpKRpFwliAtrlqN3QTK2xzAoYZodBk_ldky8/s1600/TransformStream.png" /></a></div>
<p>熟悉並學習實作 Node.js Stream,在 Node.js 開發者生涯裡是一件很重要的事,尤其在資料處理的工作上更是需要運用 Stream。在這些應該用的情境下,若不懂得使用 Stream,我們所開發出來的程式其執行效能及穩定性會相當令人擔心。</p>
<p>而如果你從未自己實作過 Stream,從 Transform Stream 開始入手是一個好選擇,也是一個非常實用的開發技巧。</p>
<p>更多關於 Stream 的說明,可以參閱 Node.js 官網上的文件:<a href="https://nodejs.org/api/stream.html">https://nodejs.org/api/stream.html</a></p>
<h2 id="toc_1">什麼是 Transform Stream?</h2>
<p>你可能知道 Node.js 裡有多種 Stream 的機制,但其實主要是 ReadableStream 和 WritableStream 兩種基本 Stream 的組成和變化。而對一般開發者來說,最常自己實作的是 Transform Stream,你可以想像這是一個產品生產線上的加工器,進入 Transform Stream 的資料會被加工後輸出。</p>
<p>而以一個 Stream 而言,Transform Stream 同時具有 ReadableStream(讀入)和 WritableStream(輸出)的特性,俗話說「左耳進右耳出」就是其最佳的寫照。</p>
<p>舉一個 Node.js 官方的例子,利用 Gzip 的 Transform Stream 將通過的資料流進行壓縮:</p>
<div><pre><code class="language-javascript">const zlib = require('zlib');
const gzip = zlib.createGzip();
const fs = require('fs');
const inp = fs.createReadStream('input.txt');
const out = fs.createWriteStream('input.txt.gz');
inp.pipe(gzip).pipe(out);</code></pre></div>
<h2 id="toc_2">實作第一個 Transform Stream</h2>
<p>先不必暸解太多 Stream 的專有名詞和機制,若想要實作一個標準的「耳邊風」Stream,程式碼如下:</p>
<div><pre><code class="language-javascript">const Transform = require('stream').Transform;
const util = require('util');
const MyTransform = module.exports = function(options) {
// 直接呼叫時建立一個實例
if (!(this instanceof MyTransform))
return new Parser(options);
// 呼叫原始 Transform 的 constructor
Transform.call(this, options);
};
// 繼承 Transform
util.inherits(MyTransform, Transform);
// 當每一筆資料進來時
MyTransform.prototype._transform = function(data, encoding, callback) {
// 將輸入進來的資料直接推送出去
this.push(data);
// 完成這筆資料的處理工作
callback();
};</code></pre></div>
<p>實際結合檔案讀取寫入,以使用這個 Transform Stream:</p>
<div><pre><code class="language-javascript">const fs = require('fs');
const myStream = new MyTransform();
// 讀取檔案
const input = fs.createReadStream('/my/file');
const output = fs.createWriteStream('/my/file.out');
// 導入 myStream,輸出後寫入 file.out
input.pipe(myStream).pipe(output);</code></pre></div>
<h2 id="toc_3">範例:累積並打包一批資料輸出</h2>
<p>有些實際應用中,我們需要累積一定量的資料,然後打包成一包輸出,尤其像是我們在做開放資料的處理時,總是會將整理好的資料一批批的批次寫入資料庫。會這樣做的原因,是因為一筆寫一次太耗時(與資料庫來回的時間),一次 50、100 或更多筆資料寫入資料庫,會有更好的效率。</p>
<p>這時我們可以寫一個自己的 Transform Stream 來做到這件事,如每輸入 10 筆資料後,打包成一個陣列輸出:</p>
<div><pre><code class="language-javascript">const Transform = require('stream').Transform;
const util = require('util');
const BatchStream = module.exports = function(options) {
// 直接呼叫時建立一個實例
if (!(this instanceof MyTransform))
return new Parser(options);
// 啟用 object mode,讓此 Stream 不是用文字或 Binary 格式,而是以物件形式輸入、輸出資料
let opts = Object.assign(options, {
objectMode: true
});
// 呼叫原始 Transform 的 constructor
Transform.call(this, options);
// 建立一個暫存區陣列
this.batch = [];
};
// 繼承 Transform
util.inherits(BatchStream, Transform);
// 當每一筆資料進來時
BatchStream.prototype._transform = function(data, encoding, callback) {
// 放入暫存區
this.batch.push(data);
// 每 10 筆推送出去一次
if (this.batch.length === 10) {
this.push(this.batch);
// 清空暫存區
this.batch = [];
}
// 完成這筆資料
callback();
};
// 當前一個 Stream 的資料輸入已經全部完成時
BatchStream.prototype._flush = function(callback) {
// 將尚未推送出去的資料送出去
if (this.batch.length > 0) {
this.push(this.batch);
// 清空暫存區
this.batch = [];
}
// 完成
callback();
};</code></pre></div>
<h2 id="toc_4">實用的 Transform Stream 簡易用法</h2>
<p>如果每一個 Transform Stream 都要先設計定義一個原型物件後才能使用,那也太煩人,在實際開發上會相當不便。這時可以運用簡單的方法,建立一個客製化的 Transform Stream:</p>
<div><pre><code class="language-javascript">const fs = require('fs');
const Transform = require('stream').Transform;
const myStream = new MyTransform();
// 讀取檔案
const input = fs.createReadStream('/my/file');
const output = fs.createWriteStream('/my/file.out');
// 導入 myStream,輸出後寫入 file.out
input
.pipe(new Transform({
transform(data, encoding, callback) {
// 將輸入進來的資料直接推送出去
this.push(data);
// 完成這筆資料的處理工作
callback();
}
})
.pipe(output);</code></pre></div>
<h2 id="toc_5">精簡!使用 ECMAScript 新支援的 class 關鍵字</h2>
<p>在各種新一代的 JavaScript 引擎上,已經可以使用 <code>class</code> 關鍵字來定義物件了,我們也可以使用 class 來定義自己的 Transform Stream,程式碼會看起來精簡許多:</p>
<div><pre><code class="language-javascript">const Transform = require('stream').Transform;
class MyTransform extends Transform {
constructor(options) {
super(options)
}
// 當每一筆資料進來時
_transform(data, encoding, callback) {
// 將輸入進來的資料直接推送出去
this.push(data);
// 完成這筆資料的處理工作
callback();
}
}</code></pre></div>
<h2 id="toc_6">再精簡一點的技巧:用 callback 推送資料</h2>
<p>用 <code>this.push()</code> 來推送一筆資料出去,有時確實還太囉唆,我們可以用 <code>callback()</code> 一次搞定:</p>
<div><pre><code class="language-javascript">const Transform = require('stream').Transform;
class MyTransform extends Transform {
constructor(options) {
super(options)
}
// 當每一筆資料進來時
_transform(data, encoding, callback) {
// 完成這筆資料的處理工作,同時將輸入進來的資料直接推送出去
callback(null, data);
}
}</code></pre></div>
<h2 id="toc_7">疑難排解:怪 Bug?為什麼程式會提前結束、資料有漏?</h2>
<p>很多人在玩弄 Stream 時,實作自己的 Transform Stream 時會發現,時常掉資料,或是資料還沒跑完,應用程式就提前結束,感覺相當不穩定。</p>
<p>通常,這得提到 Stream 本身的機制才能夠很完善的說明原因,但簡單來說,Stream 本身會在資料滿載處理不過來時暫停運作(可參考 Node.js Stream 的 highWaterMark 設定),所以如果 Stream 後面沒有下一家 Stream 接手消化資料,這條資料流就會堵塞卡死。</p>
<p>所以,通常發生這樣的情況,肯定是因為你沒有幫自己的 Transform Stream 設定下一家該往哪去,例如:</p>
<div><pre><code class="language-javascript">input.pipe(myStream); // 下面沒有了</code></pre></div>
<p>尤其是,當 Stream 暫停運作後,事件引擎就沒有新的事件在跑。眾所皆知,當 JavaScript 的事件引擎沒有事件時,Node.js 整個應用程式自然就會結束。</p>
<p>此外,Node.js Stream 的設計上,除了用 <code>.pipe()</code> 來設定下一家是誰之外,還有另一種辦法可排泄資料,那就是使用 <code>.on()</code> 監聽 <code>data</code> 事件:</p>
<div><pre><code class="language-javascript">myStream.on('data', function(data) {
// ...
});</code></pre></div>
<p>但通常不建議這樣使用,尤其是當你接到資料時,需要做許多非同步(Asynchronous)的複雜工作時。當資料量大時,這樣監聽事件的做法並無法做資料節流,會導致你一瞬間觸發許多非同步工作,進而將你的系統資源耗盡。若在分秒算錢的雲端系統上,你會得到爆量的結果,不是伺服器負荷不過來,就是把你的錢燒盡。</p>
<p><em>註:如果你想實作一個沒有後面又可以運作的 Stream,你必須要參考 WritableStream 的實作方式,理論上做法大同小異。</em></p>
<h2 id="toc_8">後記</h2>
<p>之前有人抱怨,搞不太懂 Stream 在做什麼事,或到底怎麼實際開發使用。</p>
<p>其實 Stream 是一個看似單純,但細節很多的機制,而且開發過程中會在觀察者與非觀察者切換,對許多初學者來說更是一大挑戰,更別說會碰上一些掉資料等看似奇怪的行為。所以,很能理解不少人為什麼看不太懂網路上各種說明 Stream 機制的文章,也看不到太多人使用 Stream 機制在實際的開發上。</p>
<p>所以本文以較通俗簡單的範例和方法來說明 Stream 的使用,至於比較嚴格的定義或原理性的說明,就留給讀者自己去翻閱官方相關文件了。:-)</p>
Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-91153960218743068682017-05-01T17:29:00.000+08:002017-05-01T17:42:16.720+08:00救火奇兵之 Android USB Host API 反應遲緩<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifog1skGzqMl_5EkZUybW4X582HdxD-O6Af_yTSL1p4WdTvuFV7aqE2PAr_YK4DIX4JhFHW5b_ZJe7i_vDnHKg9Xr0xgonld9chbrjwIvYU8MhCTp21suvUjj0TGpiqgJ8qcEsmssuStA/s1600/AndroidFire.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifog1skGzqMl_5EkZUybW4X582HdxD-O6Af_yTSL1p4WdTvuFV7aqE2PAr_YK4DIX4JhFHW5b_ZJe7i_vDnHKg9Xr0xgonld9chbrjwIvYU8MhCTp21suvUjj0TGpiqgJ8qcEsmssuStA/s1600/AndroidFire.png" /></a></div>
<p>話說,Android 在某個版本後,開始提供了 USB Host API,這代表開發者可以不必再用 NDK 和硬梆梆的 C 語言去開發 USB 裝置的驅動程式,而可以完全用 Java 來開發。但是,現實往往沒有這麼美好。</p>
<p>日前,就協助了一個案子,解決了一個 USB 裝置驅動程式的問題,起因就是客戶用了 Android USB Host API 去控制 USB 裝置,但發現 USB 裝置的回應一直不如預期,有時像是掉資料,有時像是沒反應。而同樣的控制邏輯,用純 C 開發的驅動程式配上 libusb 就完全正常,所以我們相信肯定不是控制邏輯上的問題。</p>
<p>剛開始,大家都懷疑是 Java 本身的問題,懷疑是不是 JVM 執行驅動程式太慢,而造成接收 USB 裝置的資料時來不及。但我一直保持著懷疑,因為 USB 裝置回傳的資料並不多,如果 JVM 本身的效能連處理這幾 KB 的資料量都如此差,就實在是太可笑了,我無論如何不相信。</p>
<p>還好最後還是解決了,雖然過程曲折。</p>
<h2 id="toc_1">USB Request Block 的 16KB 限制</h2>
<p>事實上,每次最多傳送 16KB 資料,是一個 bulk transfer 的 URB 限制,使用 Android USB Host API 就會直接遭遇到這個問題,所以不管用什麼方法,怎麼收資料,只要資料太大,你最多一次就只能收到 16KB。</p>
<h2 id="toc_2">多次收資料所發現的延遲問題</h2>
<p>當然,既然一次最多只能收 16KB,我們可以分多次向 USB 裝置要求收資料,但就會發現會莫名掉資料。從 USB 的分析器上來看,該有的命令都有,但就是有掉,後續的資料不管怎麼取都是 0。</p>
<p>後續資料為 0,在這個案子的 USB 裝置設計上是可以理解的狀況,因為該 USB 裝置只會保留資料一小段時間,然後就會清空,所以若之後跟它要任何資訊,他都會回傳空的東西回來。這很明顯,就是我們要資料的過程時間,已經超過了該 USB 裝置正常的情況。</p>
<p>而從收到的資料來看,有收到的資料,經驗證過後發現是斷斷續續的,中間有漏資料。經過測試,發現是每個命令之間的間距時間太長,因為該 USB 裝置會不斷復寫一段緩衝區,如果我們太慢去要資料,那段緩衝區就會被新的資料蓋掉,理所當然的,我們就會漏掉一些資料。</p>
<p>經過各種測試紀錄,很明顯的,Android USB Host API 並沒有這麼聽我們的話,每當我們下命令或進行控制時,他並沒有馬上送到 USB 裝置,會有一些延遲,這才導致這樣的後果。</p>
<h2 id="toc_3">硬幹 usbfs 的系統程式</h2>
<p>如果在這件事上, Android USB Host API 的遲緩導致沒辦法滿足我們的需要,我們只好繞過去自幹了。</p>
<p>但其實並不困難,不管怎麼說,Android 其實就是 Linux,底層肯定是透過 usbfs 去控制 USB 裝置,我們甚至可以不需要 libusb 和其他 framework,而直接去跟 usbfs 要資料。更何況我們只是要收資料而已,用 C 寫一小段程式去直接處理 URB 就可以解決,然後用 NDK 包裝成 JNI 即可。</p>
<p>於是有下面的實作,一個與 libusb 內部實作原理相同,但更為簡化的版本:</p>
<div><pre><code class="language-c">#include <stdio.h>
#include <stdlib.h>
#include <linux/usbdevice_fs.h>
#include <sys/ioctl.h>
// We have 32 URBs
#define NUM_URBS 32
#define BUFFER_SIZE 16384
char *getURBs(int fd, int ep)
{
struct usbdevfs_urb urbs[NUM_URBS];
struct usbdevfs_bulktransfer bt;
int len = 307200;
int sizeCount = len;
unsigned int urb_num = 0;
// Allocate buffer for image
char *buf = (char *)malloc(len * sizeof(char));
/* Send out initial URBs */
memset(urbs, 0, sizeof urbs);
for (unsigned int i = 0; i < NUM_URBS; i++) {
urbs[i].type = USBDEVFS_URB_TYPE_BULK;
urbs[i].endpoint = ep;
urbs[i].buffer = buf + (i * BUFFER_SIZE);
urbs[i].buffer_length = (sizeCount < BUFFER_SIZE) ? sizeCount : BUFFER_SIZE;
urbs[i].actual_length = (sizeCount < BUFFER_SIZE) ? sizeCount : BUFFER_SIZE;
if (sizeCount > BUFFER_SIZE)
sizeCount -= BUFFER_SIZE;
if (ioctl(fd, USBDEVFS_SUBMITURB, &urbs[i]) < 0) {
free(buf);
return NULL;
}
}
/* Wait for completions */
while(urb_num < NUM_URBS) {
struct usbdevfs_urb *urb;
if (ioctl(fd, USBDEVFS_REAPURB, &urb) < 0) {
free(buf);
return NULL;
}
// Completed early
if (urb->actual_length < BUFFER_SIZE)
break;
urb_num++;
}
return buf;
}</code></pre></div>
<h2 id="toc_4">後記</h2>
<p>很久沒當救火隊長了,偶爾當當救火奇兵,也算是練練腦袋,還好腦袋還算靈活。</p>
Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com3tag:blogger.com,1999:blog-4389461234607418203.post-43394499787718974462017-04-12T21:38:00.000+08:002017-04-13T01:10:26.495+08:00JavaScript async/await 的奇淫技巧<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjB1F-dmJZrdwAT7eO34QTDgo35A_baTpa41khLdwgF4LmwOIEDEHggd44BBBjcai_QZz-S4S1j6P5ZDsUxoXK7aziUqzSnm29d4DlGoWaWy-u7qfAGwgwoNM1u9CXfqSAOiScKm19cUXU/s1600/JS-AsyncAwaitSkill.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjB1F-dmJZrdwAT7eO34QTDgo35A_baTpa41khLdwgF4LmwOIEDEHggd44BBBjcai_QZz-S4S1j6P5ZDsUxoXK7aziUqzSnm29d4DlGoWaWy-u7qfAGwgwoNM1u9CXfqSAOiScKm19cUXU/s1600/JS-AsyncAwaitSkill.png" /></a></div>
<br />
<h1 id="toc_0">
JavaScript async/await 的奇淫技巧</h1>
<br />
話說,最新的 ECMAScript 已經引入了 async/await 語法,讓開發者可以更容易控制非同步的程式邏輯,換言之,我們可以減少許多 callback 的使用,讓 JavaScript 這種單線程、事件驅動的程式語言更易讀、好寫。<br />
<br />
關於 async/await 的基礎使用,有興趣的人可以參考舊文「<a href="http://fred-zone.blogspot.tw/2016/07/javascript-async.html">JavaScript 好用的 async 異步函數!</a>」,而本文將探討更多實際使用上的小技巧。<br />
<br />
另外,瀏覽器不一定有支援 async/await,你可以在新版的 Node.js 上面測試本文的內容。<br />
<br />
<h2 id="toc_1">
呼叫 async 函數與一般的函數沒有差別</h2>
<br />
想像一下,async 函數就是一個在執行後會回傳 Promise 物件的「普通函數」,和一般常見的函數的使用差異,僅僅只是 async 函數在執行後「不是回傳函數執行結果」。這代表我們可以把 async 函數當作一般函數來呼叫使用,用法一模一樣。<br />
<br />
<h2 id="toc_2">
async/await 與 Promise 是可以共通的</h2>
<br />
非常有趣,async 函數與 Promise 其實能夠共通,這代表我們可以玩一些特別的組合技。所以,若要把 async/await 玩得通透,建議你盡量熟悉 Promise 的各種用法。<br />
<br />
<h2 id="toc_3">
實現 delay 函數</h2>
<br />
過去因為單線程和事件驅動的關係,JavaScript 不可能實現一個沒有嚴重副作用的 delay 函數,所以取而代之的是使用 <code>setTimeout()</code> 加上 callback 來實現「一定時間後執行什麼工作」的需要。<br />
<br />
不過來到 async/await 的世界後,我們可以一行行描述程式邏輯,無論是不是同步(Synchronous)的程式碼,所以我們可以用 Promise 來包裝 <code>setTimeout()</code>,以實現一個在 async 函數裡可以跑的 delay 函數:<br />
<br />
<div>
<pre><code class="language-javascript">// 實現一個等待函數
const delay = (interval) => {
return new Promise((resolve) => {
setTimeout(resolve, interval);
});
};
const main = async () => {
console.log('Starting...');
// 等待五秒
await delay(5000);
console.log('Done after five seconds')
};
main();</code></pre>
</div>
<br />
<h2 id="toc_4">
與 .map() 的組合技</h2>
<br />
JavaScript 陣列裡常使用的 <code>.map()</code> 方法,但是 <code>.map()</code> 方法內的處理函數是同步的(synchronous),也就是如果我們想在裡面跑非同步的邏輯,是沒辦法等到我們非同步的工作完成的。<br />
<br />
假設我們有一個陣列,然後使用 <code>.map()</code> 方法操作它:<br />
<br />
<div>
<pre><code class="language-javascript">const arr = [ 1, 2, 3, 4, 5 ];
let results = arr.map((num) => {
return num + 1;
});
// [ 2, 3, 4, 5, 6 ]
console.log(results);</code></pre>
</div>
<br />
通常,如果我們想引入非同步邏輯,我們可以這樣做,直接代換 <code>.map()</code> 內的處理函數就可以:<br />
<br />
<div>
<pre><code class="language-javascript">const asyncWorker = async (num) => {
// 非同步的工作,會做一段時間
};
let results = arr.map(async (num) => {
// 等待非同步工作完成
await asyncWorker(num);
return num + 1;
});</code></pre>
</div>
<br />
特別注意,引入 async 以後,results 會是一堆的 Promise 物件,而不是一個數值陣列。而且 <code>.map</code> 並不會等 <code>asyncWorker()</code> 這個非同步的工作做完才回傳,你可以想像這是一種「射後不理」的機制。<br />
<br />
<h2 id="toc_5">
等 .map() 裡的所有工作處理完</h2>
<br />
既然 async 函數被執行後,會回傳一個 Promise,這代表我們可以藉由 Promise 物件來得知工作什麼時候完成。所以我們可以這樣做:<br />
<br />
<div>
<pre><code class="language-javascript">const asyncWorker = async (num) => {
// 非同步的工作,會做一段時間
};
let jobs = arr.map(async (num) => {
// 等待非同步工作完成
await asyncWorker(num);
return num + 1;
});
// 當所有工作完成後,顯示執行內容
Promise.all(jobs).then((results) => {
// [ 2, 3, 4, 5, 6 ]
console.log(results);
});</code></pre>
</div>
<br />
<h2 id="toc_6">
用 await 取代 promise.then() 的使用方式</h2>
<br />
前面說到可以運用 <code>Promise.all()</code> 方法來等待所有的非同步工作完成,但最終還是回到了 callback 的模式進行等待。而且,總是有懶惰鬼開發者會把這些程式碼寫成一行,非常不好讀:<br />
<br />
<div>
<pre><code class="language-javascript">Promise.all(arr.map(async (num) => {
// 等待非同步工作完成
await asyncWorker(num);
return num + 1;
})).then((results) => {
// [ 2, 3, 4, 5, 6 ]
console.log(results);
});</code></pre>
</div>
<br />
既然已經有 async/await 的環境,很多人會盡量讓自己的 <code>context</code> 處於 async 函數的環境之下,這時我們就可以用 await 來取代 Promise 的 <code>.then()</code> 方法:<br />
<br />
<div>
<pre><code class="language-javascript">const main = async () => {
// 改用 await 等待 Promise 內的工作全部完成
let results = await Promise.all(arr.map(async (num) => {
// 等待非同步工作完成
await asyncWorker(num);
return num + 1;
}));
// [ 2, 3, 4, 5, 6 ]
console.log(results);
};
main();</code></pre>
</div>
<br />
<h2 id="toc_7">
與 .reduce() 的組合技</h2>
<br />
如同 <code>.map()</code> 方法,<code>.reduce()</code> 是另一個常見的陣列處理方法之一,它也同樣不是一個非同步的方法。若引入 async/await,可以讓 <code>.reduce()</code> 擴展為一個依序處理非同步工作的工具,讓非同步工作一個處理完後下一個才接著做。<br />
<br />
一個原始的 <code>.reduce()</code> 使用大概如下:<br />
<br />
<div>
<pre><code class="language-javascript">const arr = [ 1, 2, 3, 4, 5 ];
// 將陣列所有數值一一加總
let result = arr.reduce((accumulation, num) => {
return accumulation + num;
}, 0);
// 15
console.log(result);</code></pre>
</div>
<br />
若引入 async/await,會變成這樣的形式:<br />
<br />
<div>
<pre><code class="language-javascript">const arr = [ 1, 2, 3, 4, 5 ];
const main = async () => {
// 將陣列所有數值一一加總
let result = await arr.reduce(async (prev, num) => {
// 等待前一個工作完成,並得到前個工作的結果
let accumulation = await prev;
return accumulation + num;
}, Promise.resolve(0));
// 15
console.log(result);
};
main();</code></pre>
</div>
<br />
這時裡面可以跑各式各樣非同步工作,如前面所提到的 delay 函數:<br />
<br />
<div>
<pre><code class="language-javascript">const arr = [ 1, 2, 3, 4, 5 ];
const main = async () => {
// 將陣列所有數值一一加總
let result = await arr.reduce(async (prev, num) => {
// 等待前一個工作完成,並得到前個工作的結果
let accumulation = await prev;
// 非同步工作:等一秒
await delay(1000);
return accumulation + num;
}, Promise.resolve(0));
// 15
console.log(result);
};
main();</code></pre>
</div>
<br />
<h2 id="toc_8">
後記</h2>
<br />
懂得使用 async/await 和 Promise 之後,其實有很多的玩法,邏輯的描述也更為多元和簡單,強烈建議一定要熟悉他。:-)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com2tag:blogger.com,1999:blog-4389461234607418203.post-60716447676242171122017-04-08T15:22:00.000+08:002017-04-08T15:24:40.793+08:00自幹 JavaScript 的 Tail Call Optimization<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9NUEa9Nli3IONgmyWc0THE8cHsZm_MtmXsTEqxs2B4RBB5WRUzJ-RYdZN9rQOpSX8veEVch_75xr1gL-PZphjwYJnwJJuIp3-Qpxe7l8zQgf-EahhLJqpdSytqTwdKy7sOQmjTtWysss/s1600/JS-TailCallOptimization.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9NUEa9Nli3IONgmyWc0THE8cHsZm_MtmXsTEqxs2B4RBB5WRUzJ-RYdZN9rQOpSX8veEVch_75xr1gL-PZphjwYJnwJJuIp3-Qpxe7l8zQgf-EahhLJqpdSytqTwdKy7sOQmjTtWysss/s1600/JS-TailCallOptimization.png" /></a></div><br />
ECMAScript 6 開始,規範中出現了一項被稱為「尾呼叫優化(Tail Call Optimization, TCO)」的優化技術,這讓開發者可以在函數的執行過程中,減少 Stack Frame 的數量,進而提升效能。TCO 尤其是在遞迴這種不停呼叫自己或新函數的工作上,能得到最大的優化效益,能提升遞迴的執行效能如同迴圈一樣。<br />
<br />
只不過很可惜的是,截至本文發稿前,大多數瀏覽器及 JavaScript 引擎尚未支援這項技術。但我們還是可以自幹並模擬一個 TCO 的行為,雖然比起語言本身、編譯器(Compiler)及虛擬機(VM)層面的實現,效果差了些,但仍然可以減少 Stack Frame 的數量避免達到 Stack Frame 的數量上限。<br />
<br />
<h2 id="toc_1">什麼時候會啟用尾呼叫優化機制?</h2><br />
如果 JavaScript 引擎有支援,通常一個函數執行到最後一行 <code>return</code> 時,是回傳另一個函數的執行結果,就會啟用 TCO 機制,如:<br />
<br />
<div><pre><code class="language-javascript">const f = () => {
return 999;
};
const g = () => {
// 執行並直接回傳 f 函數的執行結果:會啟用尾呼叫優化機制
return f();
};
g();</code></pre></div><br />
但要注意的是,回傳的「必定為函數的直接回傳值」,所以下面這些寫法不會啟用 TCO 機制:<br />
<br />
<div><pre><code class="language-javascript">// 不會啟用 TCO 機制的設計
const g = () => {
return f() + 1;
};
// 不會啟用 TCO 機制的設計
const g = () => {
let ret = f();
return ret;
};</code></pre></div><br />
<h2 id="toc_2">創造一個跑不完的函數</h2><br />
首先我們先創造一個肯定跑不完的遞迴,然後改善它:<br />
<br />
<div><pre><code class="language-javascript">const func = (x) => {
// 讓他跑 10000000 次
if (x === 10000000)
return x;
return func(x + 1);
};
let ret = func(0);</code></pre></div><br />
理論上,如果你直接執行上述程式碼,會得到 stack size 超過上限的錯誤訊息:<br />
<br />
<div><pre><code class="language-none">RangeError: Maximum call stack size exceeded</code></pre></div><br />
<h2 id="toc_3">簡單模擬實現自己的 TCO 效果</h2><br />
簡單來說,尾呼叫優化對開發者最主要的效果,就是避免每次呼叫函數時,就產生一個新的 Stack Frame,所以這是我們實現的主要訴求。而我們可以實現的作法,是讓函數的遞迴呼叫轉換成迴圈形式執行,避免不斷增加 Stack Frame,使遞迴可以無窮的跑下去。<br />
<br />
我們可以創造一個閉包,來包裝我們的函數:<br />
<br />
<div><pre><code class="language-javascript">const tco = (fn) => {
return (...args) => {
let f = fn.bind(this, ...args);
// 每次執行函數後,若得到的回傳值是函數物件,則繼續執行下去
while(f instanceof Function) {
f = f();
}
// 沒有需要繼續執行的函數,回傳結果
return f;
}
};</code></pre></div><br />
然後,我們用此閉包把待優化的函數包裝起來,並做點修改:<br />
<br />
<div><pre><code class="language-javascript">const func = (x) => {
// 讓他跑 10000000 次
if (x === 10000000)
return x;
// 不直接執行函數,改為綁定參數後產生並回傳一個函數物件
return func.bind(this, x + 1);
};
// 包裝我們的遞迴函數
const improvedFunc = tco(func);
// 執行方法和舊函數一樣
let ret = improvedFunc(0);</code></pre></div><br />
執行優化後的函數,就不會再出現 stack size 的錯誤了。<br />
<br />
<h2 id="toc_4">自己自幹的限制</h2><br />
需要一提的是,在我們自己實現的版本中,我們無法做到合併所有的 Stack,也無法減少函數的來回跳轉數量,這些是屬於虛擬機和語言層面才能做到的設計。<br />
<br />
<h2 id="toc_5">使用遞迴時要注意事件引擎鎖死的問題</h2><br />
要記得,雖然 TCO 可以解決 stack size 上限的問題,但 JavaScript 仍然依賴著事件引擎,而密集運算會造成事件引擎鎖死,所以在一般的應用中,我們應該避免運用太深的遞迴,除非你確定你的應用程式沒有其他需要事件機制的地方,或是真的要拿 JavaScript 做密集運算的工作。<br />
<br />
<h2 id="toc_6">後記</h2><br />
一如程式語言的發展趨勢,所有現代化的程式語言都向 Functional Programming 靠攏。考量到為了讓函數可以無窮的遞迴執行下去,尾呼叫優化(Tail Call Optimization, TCO)就是一個重要的機制。Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com1tag:blogger.com,1999:blog-4389461234607418203.post-55876835388716584252017-04-04T22:21:00.000+08:002017-04-05T00:19:11.955+08:00實現 JavaScript 的 Memoization<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNsDm5Nu14_DXJm6HrTrOrH0v5dHsVjBLQ5KKPSr44Z4lzAIEI41nwFtR9URIrBQfF7qafZ4pcPyeiGKLCCwbgnqt0XUjiuUEEnAIQykFINS9AcmS0RgOdX9-5kHK92ZJXIPMPNQHiRkk/s1600/JS-memorization.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNsDm5Nu14_DXJm6HrTrOrH0v5dHsVjBLQ5KKPSr44Z4lzAIEI41nwFtR9URIrBQfF7qafZ4pcPyeiGKLCCwbgnqt0XUjiuUEEnAIQykFINS9AcmS0RgOdX9-5kHK92ZJXIPMPNQHiRkk/s1600/JS-memorization.png" /></a></div>函數式程式設計(Functional Programming)是近年來越來越被軟體開發者常提及的話題,許多人討論它時,不外乎說其就是在程式設計中引入了數學方法,彷彿有神奇又高深的理論加持一般。事實上,對於一般開發者而言,函數式程式設計比較通俗且直接的好處,就是讓開發者可以在「函數」的層面和維度,進行邏輯或是效能上的優化。所以說,比起命令化的執行程式、管理物件,怎麼去設計和管理函數這件事,就是函數式程式設計所關心的重點。<br />
<br />
JavaScript 這語言在設計上,天生就支援 first-class function,這代表函數在 JavaScript 是一種資料型態,可以被當成普通物件傳遞、處理,這讓開發者在使用 JavaScript 時可以時不時引入 Functional Programming 的技巧和概念。<br />
<br />
本文將介紹 Functional Programming 中大量被使用的 Memoization 機制,然後我們如何在 JavaScript 中引入並實地使用它,無論你會不會 Functional Programming,這都是一個可以常用於日常開發中的優化技巧。<br />
<br />
<h2 id="toc_1">Memoization 是什麼?</h2><br />
用一般程式開發的說法就是快取(Cache)機制,只不過 Memoization 是針對「函數」進行快取。快取的好處在於我們只要執行過一次工作後,之後在執行相同工作前,就能提前知道執行結果為何,所以我們可以不用「真正的」去執行工作,而直接取用執行結果就好,可大量提升程式執行的效能。<br />
<br />
同樣快取概念套用在函數上,若我們給予特定「輸入(Input)」到一個函數中,而該函數會回傳一個特定的「輸出(Output)」,理論上函數執行一次後,下次再使用這個函數時,只要「輸入」和過去一樣,我們就能提前知道結果。<br />
<br />
而這樣對函數進行的快取機制,就是所謂的 Memoization。<br />
<br />
<h2 id="toc_2">只能使用在純函數</h2><br />
你可能會聽一些人說,只有「純函數」才能引入快取機制,然後開始討論數學上所謂函數的定義,然後你就聽到昏了,後面在講什麼你就都聽不進去了。<br />
<br />
但如果撇除函數的數學定義,若白話來說,能被快取的東西,就是能被預測的東西,這代表函數的執行結果也要能被預測,也就是一樣的輸入值,就會有一樣的輸出結果。<br />
<br />
所以,如果一個函數每次執行,代入的輸入值一樣,但回傳結果卻是可能不一樣,這就不是一個純函數,像是取亂數的函數就屬此類,我們無法對其引入快取機制,而且也沒有任何引入的意義。<br />
<br />
<h2 id="toc_3">從一個簡單的函數開始</h2><br />
要實現 Memoization 之前,我們得先創造一個有用的函數,然後才能討論怎麼快取它。現在假設我們有一個函數,專門用來查詢一個資料陣列,我們會這樣設計:<br />
<br />
<div><pre><code class="language-javascript">const getData = (records, id) => {
// 找到指定的 element 並回傳
return records.find((data) => {
// 找到指定的 ID 就回傳 true
return (data.id === id);
});
};
// 想像這是一個有上萬筆資料的陣列
const list = [ ... ];
// 取得 list 裡 id 為 helloId 的資料
let data = getData(list, 'helloId');</code></pre></div><br />
<h2 id="toc_4">創造一個函數,專門查詢特定陣列</h2><br />
如果我們想要創造一個函數,專門查詢 list,則可以運用 JavaScript 中的 <code>.bind()</code> ,來綁定 list 並創造出一個新的函數來使用:<br />
<br />
<div><pre><code class="language-javascript">// 以舊的 getData 為基礎創造一個新的函數,綁定 caller 和第一個代入參數
const newGetData = getData.bind(this, list);
// 執行新的函數時只需要代入尚未被綁定的參數即可
let data = newGetData('helloId');</code></pre></div><br />
<em>註:有興趣者可以延伸搜尋 Functional Programming 的「柯里化(Currying)」相關資料。</em><br />
<br />
<h2 id="toc_5">設計函數的快取機制</h2><br />
我們可以自己設計一個閉包(Closure)來實現函數的快取機制,簡單來說就是利用 JavaScript 的 Object 來建立一個 key-value pair 的快取。而 key 就使用函數的「輸入(Input)」,value 則是函數的執行「輸出(Output)」。<br />
<br />
<div><pre><code class="language-javascript">const memoize = (fn) => {
// 將快取資料存放在閉包裡的記憶體
const cache = {};
return (arg) => {
// 先檢查是否有快取,如果沒有就執行原始函數並快取其結果
if (cache[arg] === undefined)
cache[arg] = fn(arg);
// 回傳執行結果
return cache[arg];
};
};
// 對函數套用 Memoization 機制,會得到一個新的函數
const improvedGetData = memoize(newGetData);
// 這個新的函數使用方法和舊的一樣
let data1 = improvedGetData('helloId');
// 第二次執行同樣函數,並帶同樣參數時,就是直接從快取記憶體中取得結果
let data2 = improvedGetData('helloId');</code></pre></div><br />
被我們設計出來的 <code>memoize()</code> 是通用的方法,可以應用在其他函數中,只要其他函數也有這樣的需求,又符合純函數的條件,是可以直接以 <code>memoize()</code> 包裝,受惠於快取機制。<br />
<br />
<h2 id="toc_6">你應該要注意的問題</h2><br />
Memoization 這樣的快取機制,雖對執行效能上有所有好處,但最直接的問題就是會佔用記憶體,如果我們的程式處理的資料量很大,就可能會佔用非常大量的記憶體而不會釋放。<br />
<br />
所以在某些函式程式語言中, Memoization 甚至支援快取資料數量的限制,如果你有需求,可以自己試著增加這樣的設計。<br />
<br />
<h2 id="toc_7">效能評估?</h2><br />
至於增加快取後的效能如何呢?這邊準備了一個簡單的測試「<a href="https://jsperf.com/memoization-testing/1">Memoization Testing</a>」,分別為原始函數執行的效能,以及引入 Memoization 機制後的效能,你可以試著跑跑看。<br />
<br />
第一次的執行上效能差不多,但若「同個函數+同樣輸入值」執行兩次後,原始函數的執行效能就會開始明顯與有快取機制的函數有所差距。<br />
<br />
<h2 id="toc_8">後記</h2><br />
物件導向大家都很會玩了,可以開始玩玩不同維度的優化和技巧,其實相當有趣。:-PFred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com1tag:blogger.com,1999:blog-4389461234607418203.post-5583681387196419062017-02-26T03:33:00.001+08:002017-02-26T15:21:51.386+08:00Koa 2 起手式!<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhopE5DgXVNpEm-TGej4cKycrd3jboVFYnjf614YNDQe-_nINaoDFLgUO8c0zoOwlaT1TLEX6ocF8V3Rnxwp56LSnqJsrOYuv4vTrMzmQdefECUe9fiVE3LTxJFdr332lN_O3RjHBzL24Q/s1600/koa2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhopE5DgXVNpEm-TGej4cKycrd3jboVFYnjf614YNDQe-_nINaoDFLgUO8c0zoOwlaT1TLEX6ocF8V3Rnxwp56LSnqJsrOYuv4vTrMzmQdefECUe9fiVE3LTxJFdr332lN_O3RjHBzL24Q/s1600/koa2.png" /></a></div>
<br />
<br />
在 Node.js 的世界裡,說到今天最潮的 Web Framework,肯定就是 Koa!其用了最新的 JavaScript 語法和特性,來改善 Web Framework 的設計。只不過,Koa 雖然相對於其他舊的 Web Framework 來說有相當多的進步,但很多人卻相當討厭 Koa 的 Generator 設計,尤其是那些「*」符號,那不知所謂的 yield 也讓很多人不舒服。所以至今仍然有許多人在使用 express 來當作自己的 Web Framework,寧可繼續使用那老派的 callback 設計,而不肯嘗試 Koa。<br />
<br />
隨著 ECMAScript 標準的進步,Koa 才剛被開發出來沒多久,原本的開發團隊就立即著手打造 Koa 2 ,開始更進一步採用更新的 JavaScript 特性,以 async/await 語法重新打造了全新且更簡潔的框架。可惜的是,由於 async/await 語法一直遲遲沒有被 JavaScript 引擎原生支援,因此總需要靠 babel 編譯打包程式碼後,才能正常跑在 Node.js 之上。這讓 Koa 2 一直無限期處於非穩定版,讓原開發者從開發的一開始,就打算等到 V8 和 Node.js 開始原生支援 async/await 後,才會被以穩定版(stable)的姿態釋出。<br />
<br />
所以,即使 Koa 2 到了今天已經相當穩定,也開始有不少人使用在自己的線上服務,卻一直無限期處於非穩定版的狀態。<br />
<br />
另外,由於 Koa 2 大量使用 Async/Await,如果你還對 Async/Await 的使用還不熟悉,建議在閱讀本文之前,先閱讀舊文「<a href="http://fred-zone.blogspot.tw/2016/07/javascript-async.html">JavaScript 好用的 async 異步函數!</a>」來學習如何使用。<br />
<br />
<h2 id="toc_1">
學習 Koa 的好時機來囉</h2>
<br />
總算,日前 Node.js v7.6.0 釋出後已經正式宣布原生支援了 async/await 語法,而且不需要額外的參數選項。伴隨著這個消息,Koa 2.0 也隨即正式釋出了!<br />
<br />
Node.js 內建支援 ES7 的 async/await 真的是非常棒的消息!過去我們使用 async/await,都還需要 babel 的協助才能正常跑在舊版的 Node.js,不但開發上相當麻煩,非原生的各種 ES7 特性也浪費不少額外的記憶體和效能,這樣的問題在斤斤計較效能的 Server 環境下,更是讓人頭痛。<br />
<br />
如今 Node.js 的原生支援,讓我們已經不需要再擔心種種問題,讓我們可以得到簡潔的程式碼和兼顧效能,現在就是準備轉換到 Koa 2 的最好時機!:-)<br />
<br />
<h2 id="toc_2">
安裝 Koa 2</h2>
<br />
現在,我們終於可以直接使用 NPM 命令安裝 Koa 2:<br />
<br />
<div>
<pre><code class="language-bash">npm install koa</code></pre>
</div>
<br />
<h2 id="toc_3">
開發第一個應用程式</h2>
<br />
如果你有開發過 Koa 或 Express 的網站應用程式,Koa 2 的寫法其實相當雷同,差別是 Express 使用的是普通函數當 callback、Koa 是使用 Generator Function,而 Koa 2 是使用 Async Function。<br />
<br />
一個簡單的範例如下:<br />
<br />
<div>
<pre><code class="language-javascript">const Koa = require('koa');
const app = new Koa();
app.use(async function(ctx) {
ctx.body = 'Hello World';
});
app.listen(3001);</code></pre>
</div>
<br />
當 ctx.body 被設定一個內容後,連線就會回傳該內容回瀏覽器。在這範例中,無論發什麼要求給 Server ,都會得到「Hello World」的回傳。<br />
<br />
註:如果你使用過 Koa,會發現 Koa 2 已經不再使用 this 關鍵字,而是改成一個 context 物件代入到函數之中。<br />
<br />
<h2 id="toc_4">
使用異步函數打造的 Middleware</h2>
<br />
koa.use() 將用來載入 Middleware,所有連線工作都會經過 Middleware 處理。這也是為什麼,前一個例子裡,我們使用 koa.use() 設定了一個處理函數後,所有連線透會通過該函數進行處理並回傳同樣的值。<br />
<br />
要注意的是,該函數是一個「異步函數(Async Function)」,要用到 async 關鍵字來宣告:<br />
<br />
<div>
<pre><code class="language-javascript">app.use(async function() {
// ...
});</code></pre>
</div>
<br />
註:如果你有過 express 開發經驗,對於 koa.use() 會相當熟悉,Koa 同樣支援了 Middleware 的架構,你可以將過去的程式輕易移植到這新的框架上。<br />
<br />
<h2 id="toc_5">
自訂 Router 和路徑管理</h2>
<br />
之前的範例直接使用 koa.use(),會將所有的連線都導入同一個處理函數,輸出同一個結果。若我們想要自訂不同的路徑,讓不同路徑用不同的處理函數,將需要額外安裝「koa-router」模組:<br />
<br />
<div>
<pre><code class="language-bash">npm install koa-router</code></pre>
</div>
<br />
然後可以用 koa-router 來管理對外的連線路徑:<br />
<br />
<div>
<pre><code class="language-javascript">const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = Router();
// 設定根路徑的處理函數
router.get('/', async function(ctx) {
ctx.body = 'Hello World';
});
app.use(router.routes());
app.listen(3001);</code></pre>
</div>
<br />
<h2 id="toc_6">
接收 QueryString 資料</h2>
<br />
QueryString 可說是歷史悠久且非常常見的傳值方法,藉由一個網址後面加上一個「?」字元後,就可以使用鍵值(Key/Value)來進行資料傳遞,並用「&」區隔多組資料。一個簡單的實際應用如下:<br />
<br />
<div>
<pre><code class="language-none">http://my_server/send?name=fred&msg=Hello</code></pre>
</div>
<br />
取得資料的方法如下:<br />
<br />
<div>
<pre><code class="language-javascript">ctx.query.name
ctx.query.msg</code></pre>
</div>
<br />
<h2 id="toc_7">
接收 body 資料</h2>
<br />
當我們使用「POST」或「PUT」方法,我們就可以利用 body 傳送一些資料到伺服器,像是網頁表單時常使用這樣的傳值方法。若想要取得 body 的資料,必須先安裝「koa-bodyparser」模組。<br />
<br />
截至本文釋出為止,該模組還沒有隨著 Koa 2 推出正式支援的版本,所以預設下載回來的版本還是支援舊的 Koa,所以必須指定版本號「next」:<br />
<br />
<div>
<pre><code class="language-none">npm install koa-bodyparser@next</code></pre>
</div>
<br />
當然,你也可以用「koa-convert」模組將舊的 Koa Middleare 直接轉換給 Koa 2 使用:<br />
<br />
<div>
<pre><code class="language-none">npm install koa-convert</code></pre>
</div>
<br />
使用 koa.use() 載入 koa-bodyparser,koa 就會自動在處理連線時使用它解析 body:<br />
<br />
<div>
<pre><code class="language-javascript">var bodyParser = require('koa-bodyparser');
// 若想使用 koa-convert 進行轉換,要先載入模組:
// const convert = require('koa-convert');
// 再以 convert(bodyParser()) 包裝
app.use(bodyParser());</code></pre>
</div>
<br />
然後可以在路徑處理函數中,正常取得 body 內的資訊:<br />
<br />
<div>
<pre><code class="language-javascript">ctx.request.body.name
ctx.request.body.msg</code></pre>
</div>
<br />
<h2 id="toc_8">
錯誤處理</h2>
<br />
在 Koa 2 中,可以透過 ctx.throw() 進行錯誤處理,並回傳狀態值和內容給客戶端,他會中斷目前的處理函數,實際使用情境如下:<br />
<br />
<div>
<pre><code class="language-javascript">
router.get('/api/v1/user', async function(ctx) {
// 檢查 Token,若有問題回傳 400 HTTP StatusCode
if (ctx.query.token == '123')
ctx.throw(400);
// 若已經拋出 400 的狀態,接下來的程式不會被執行
ctx.body = 'Hello World';
});
</code></pre>
</div>
<br />
<h2 id="toc_9">
加入多個 Middleware</h2>
<br />
所有的連線要求可以透過一系列、不只一個 Middleware 來進行處理,我們可以利用多次 koa.use() 來加入多個 Middleware,多個 Middleware 可以用來做到很多功能,例如記錄和顯示每個連線的狀態。<br />
<br />
加入多個 Middleware 的範例如下:<br />
<br />
<div>
<pre><code class="language-javascript">const Koa = require('koa');
const app = new Koa();
app.use(async function(ctx, next) {
// 略過這個 Middleware,讓下一個 Middleware 來接著處理
await next();
});
app.use(async function(ctx) {
ctx.body = 'Hello World';
});
app.listen(3001);
</code></pre>
</div>
<br />
<h2 id="toc_10">
加入 Logger 來記錄連線要求</h2>
<br />
koa-logger 是一個能顯示連線要求狀態的第三方 Middleware,可以先透過 NPM 安裝它:<br />
<br />
<div>
<pre><code class="language-bash">npm install koa-logger</code></pre>
</div>
<br />
然後可以直接以 app.use() 引用:<br />
<br />
<div>
<pre><code class="language-javascript">const Koa = require('koa');
const logger = require('koa-logger');
const app = new Koa();
// 加入 logger 在其他的 Middleware 之前
app.use(logger());
app.use(async function(ctx) {
ctx.body = 'Hello World';
});
app.listen(3001);</code></pre>
</div>
<br />
然後,你的應用程式就會輸出漂亮的連線要求訊息:<br />
<br />
<div>
<pre><code class="language-none"> <-- GET /api/v1/hello
--> GET /api/v1/hello 200 8,257ms 2b</code></pre>
</div>
<br />
<h2 id="toc_11">
在 Koa 2 裡使用 Mongoose</h2>
<br />
異步函數使用 await 關鍵字對 Promise、Thunk 進行等待,使開發者不再需要用到大量的 callback function,讓程式碼比較不會「橫著長大」。所以,只要 Mongoose 可以在做任何工作時,回傳一個 Promise 物件,我們就可以在 Koa 2 中使用 await 等它完成。<br />
<br />
還好,Mongoose 有支援這個功能,但我們得使用 .exec() 這個方法來取得 Promise:<br />
<br />
<div>
<pre><code class="language-javascript">router.get('/api/v1/user', async function(ctx) {
// 利用 exec() 取得 Promise,然後以 await 等待完成
ctx.body = await Users.find({}).exec();
});</code></pre>
</div>
<br />
<h2 id="toc_12">
函數宣告的習慣改變</h2>
<br />
在本文的範例中,仍然還是使用「function()」這樣的函數宣告方式,但很多開發者為了減少程式碼,大量改用 Arrow Function(箭頭函數)來宣告函數,所以你會大量看到這樣的情況:<br />
<br />
<div>
<pre><code class="language-javascript">router.get('/api/v1/user', async (ctx) => {
// ...
});</code></pre>
</div>
<br />
因為多數情況下,改用 Arrow Function 來宣告是沒有問題的,所以很多懶惰的開發者都會這樣使用。但建議你,如果有空時,還是要了解這種函數與普通函數宣告的差別。<br />
<br />
<h2 id="toc_13">
後記</h2>
<br />
這篇文章其實卡很久了,一直遲疑著要不要在正式穩定版之前公開,剛好趁著這幾天 Node.js v7.6.0 和 Koa 2 正式版釋出,所有的顧慮就沒有啦。:-DFred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-58398431529283279432017-02-03T14:36:00.000+08:002017-02-03T14:36:06.181+08:00Node.js 也可以使用 Protocol Buffers!<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgu8N2eEIW2RGPiTHaANo0q-hBVisqcqCwW3is2x0twJwgUUsDI_sJpN-lYvEM3B-OoUXep3asG5kDgzUIQPw_IDwftbLRXc2OiSH0siyr252yg9CumTxGAMjC1nrvep2FnEklOmSYxJqQ/s1600/protobuf.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgu8N2eEIW2RGPiTHaANo0q-hBVisqcqCwW3is2x0twJwgUUsDI_sJpN-lYvEM3B-OoUXep3asG5kDgzUIQPw_IDwftbLRXc2OiSH0siyr252yg9CumTxGAMjC1nrvep2FnEklOmSYxJqQ/s1600/protobuf.png" /></a></div>
<br />
「<a href="https://developers.google.com/protocol-buffers/docs/overview">Protocol Buffers (protobuf)</a>」是一套 Google 所提出的結構化資料的包裝技術,讓資料便於網路傳輸或交換,如同常見的 JSON 和 XML 等技術一般。但相對於其他常見技術,protobuf 設計上更易於用來包裝二進位資料,應用在串流(Streaming)技術上,在資料包裝上也更為節省空間,在包裝或解析上也更有效率。<br />
<br />
<em>註一:若採用 JSON,由於原本的設計上並無法處理二進位資料,所以如果要包裝二進位資料,傳統做法會將資料轉換成 base64 的格式,再以字串(String)的格式儲存。因為這等於二次包裝資料,導致處理上非常沒有效率。</em><br />
<br />
<em>註二:與 Google Protocol Buffers 類似的技術還有 MessagePack 及 Facebook 採用的 Apache Thrift,有興趣的人可以自行參考比較。</em><br />
<br />
<h2 id="toc_1">
跨語言的優點</h2>
<br />
另外,Protocol Buffers 最大的優點,就是擁有跨程式語言的設計,提供了一個標準通用的 <code>.proto</code> 定義方法,讓我們定義資料結構和格式。只需要載入這些我們事先準備好的資料定義,就可以輕易生成給不同語言(如:C++、C#、Go、Java、Objective-C 或 Python)用的資料解析器、包裝方法,讓我們可以在不同的語言之間,解析或包裝相同的結構資料。<br />
<br />
<h2 id="toc_2">
Protocol Buffers 的使用場景?</h2>
<br />
若在純粹的 Web 應用下,大多數情況,我們不需要處理二進位資料,或是需要非常精準的資料格式,也不會進行單筆高流量的資料交換,所以使用 JSON 或 XML 已經足以。但若你的應用有串流、二進位資料的需求,Protocol Buffers 就是你可以考慮的選擇。<br />
<br />
像是筆者在一些公司專案中,會運用 Message Queuing 進行各種訊息資料傳遞,以達成各種資料處理需求。但由於訊息資料內可能有大大小小等各種資料形式和資料型態需求,導致 JSON 包裝已經完全不敷使用,甚至有效能上的疑慮,這時就會採用 Prorocol Buffers 來打包資料。<br />
<br />
<h2 id="toc_3">
安裝 ProtoBuf.js</h2>
<br />
Google 官方其實並沒有實作 JavaScript 版本的 Protocol Buffers 支援,但還好廣大的社群已經有高手開發出 JavaScript 的模組「ProtoBuf.js」,除了在 Node.js 上可以使用以外,<strong>甚至可以在瀏覽器中使用</strong>。<br />
<br />
所以,如果想在 Node.js 裡使用,可以直接透過 NPM 安裝模組:<br />
<br />
<div>
<pre><code class="language-bash">npm install protobufjs</code></pre>
</div>
<br />
補註:Protocol Buffers v3.0.0 beta 2 開始官方支援 JavaScript,未來有機會轉用官方的版本。<br />
<br />
<h2 id="toc_4">
使用 .proto 定義自己的資料格式</h2>
<br />
開始使用 Protocol Buffers 的第一個步驟,就是建立一個 <code>.proto</code> 檔來描述定義一個自己的資料格式相當簡單,一個簡單的定義如下。<br />
<br />
<strong>Product.proto</strong> 內容:<br />
<br />
<div>
<pre><code class="language-none">package Ecommerce;
message Product {
bool available = 1; // 是否上架(布林值)
string name = 2; // 產品名稱(字串)
string desc = 3; // 產品說明(字串)
float price = 4; // 價格(浮點數)
}</code></pre>
</div>
<br />
實際上 Protocol Buffers 支援了更多資料格式,有興趣的人可以自行參考官方所整理的表格:「<a href="https://developers.google.com/protocol-buffers/docs/proto3#scalar">Scalar Value Types</a>」。<br />
<br />
<h2 id="toc_5">
使用我們定義的 .proto 來包裝資料</h2>
<br />
若要包裝資料,要先載入 .proto 檔案裡的資料定義,然後使用此定義去進行接下來的工作,而 ProtoBuf.js 提供了一個 encode 方法來進行資料包裝。<br />
<br />
由於經過 Protocol Buffers 包裝後的資料是二進位格式,所以 ProtoBuf.js 提供了 finish 方法輸出成 Node.js 的 Buffer 格式:<br />
<br />
<div>
<pre><code class="language-javascript">var ProtoBuf = require('protobufjs');
// 載入 Product.proto 檔案
ProtoBuf.load('Product.proto', function(err, root) {
if (err)
throw err;
// 並取得 Product 資料定義
var Product = root.lookup('Ecommerce.Product');
// 準備包裝的資料
var data = {
available: true,
name: 'ApplePen',
desc: 'The combination of Apple and Pen',
price: 100.0
};
// 包裝資料後回傳 Buffer 格式(二進位形態)
var msgBuffer = Product.encode(data).finish();
});</code></pre>
</div>
<br />
<h2 id="toc_6">
解開已包裝的資料</h2>
<br />
若我們有一個已包裝過的資料(無論是從哪裡收到的資料),可以直接使用 decode 方法去解開它:<br />
<br />
<div>
<pre><code class="language-javascript">var ProtoBuf = require('protobufjs');
// 載入 Product.proto 檔案
ProtoBuf.load('Product.proto', function(err, root) {
if (err)
throw err;
// 並取得 Product 資料定義
var Product = root.lookup('Ecommerce.Product');
// 解開
var data = Product.decode(msgBuffer);
});
</code></pre>
</div>
<br />
<h2 id="toc_7">
二進位資料形態的欄位</h2>
<br />
前面提到,Protocol Buffers 可以包裝二進位資料,若我們想要設定某個欄位為二進位的資料,可以將其資料型態設為「bytes」:<br />
<br />
<div>
<pre><code class="language-none">package MyTest;
message Example {
bytes binData = 1;
}</code></pre>
</div>
<br />
然後,當我們在包裝資料時,該欄位應該是一個 Buffer 的物件:<br />
<br />
<div>
<pre><code class="language-javascript">var msgBuffer = Example
.encode({
binData: new Buffer('This is binary data')
})
.finish();</code></pre>
</div>
<br />
解開時,該欄位會是一個 Buffer 物件:<br />
<br />
<div>
<pre><code class="language-javascript">var data = Example.decode(msgBuffer);
// 將 Buffer 內容轉成字串形式輸出
console.log(data.binData.toString());</code></pre>
</div>
<br />
<h2 id="toc_8">
ProtoBuf.js 的效能表現</h2>
<br />
Protocol Buffers 這類的技術,不外乎就是把一個執行期的 JavaScript 物件,轉換包裝成二進位、字串等資料格式,使資料訊息便於透過網路和其他媒介傳送。實務上,與 JavaScript 物件轉成 JSON 字串是同樣的意思。<br />
<br />
所以若要評估這樣技術的效能,最實際的方式就是測試、比較他們的「轉換」的效率,ProtoBuf.js 官方提供了一些「<a href="https://github.com/dcodeIO/ProtoBuf.js#performance">效能測試</a>」,方便我們在自己機器上進行 Protocol Buffers 與原生 JSON 處理的效能比較。<br />
<br />
從官方的測試結果來看,從資料包裝的速度,ProtoBuf.js 的效能快過於「JSON.stringify」將近一倍,如果是轉成二進位形式(to Buffer)更是快三倍左右;從解開包裝的速度來看,ProtoBuf.js 效能則是「JSON.parse」的三至四倍效能以上。<br />
<br />
<strong>整體比較起來,ProtoBuf.js 則是比純 JSON 的處理快上一倍以上。</strong><br />
<br />
節錄官方 Github 上的測試結果(機器:i7-2600K。Node.js 版本:6.9.1):<br />
<br />
<div>
<pre><code class="language-none">benchmarking encoding performance ...
Type.encode to buffer x 547,361 ops/sec ±0.27% (94 runs sampled)
JSON.stringify to string x 310,848 ops/sec ±0.73% (92 runs sampled)
JSON.stringify to buffer x 173,608 ops/sec ±1.51% (86 runs sampled)
Type.encode to buffer was fastest
JSON.stringify to string was 43.5% slower
JSON.stringify to buffer was 68.7% slower
benchmarking decoding performance ...
Type.decode from buffer x 1,294,378 ops/sec ±0.86% (90 runs sampled)
JSON.parse from string x 291,944 ops/sec ±0.72% (92 runs sampled)
JSON.parse from buffer x 256,325 ops/sec ±1.50% (90 runs sampled)
Type.decode from buffer was fastest
JSON.parse from string was 77.4% slower
JSON.parse from buffer was 80.3% slower
benchmarking combined performance ...
Type to/from buffer x 254,126 ops/sec ±1.13% (91 runs sampled)
JSON to/from string x 122,896 ops/sec ±1.29% (90 runs sampled)
JSON to/from buffer x 88,005 ops/sec ±0.87% (89 runs sampled)
Type to/from buffer was fastest
JSON to/from string was 51.7% slower
JSON to/from buffer was 65.3% slower
benchmarking verifying performance ...
Type.verify x 6,246,765 ops/sec ±2.00% (87 runs sampled)
benchmarking message from object performance ...
Type.fromObject x 2,892,973 ops/sec ±0.70% (92 runs sampled)
benchmarking message to object performance ...
Type.toObject x 3,601,738 ops/sec ±0.72% (93 runs sampled)</code></pre>
</div>
<br />
<h2 id="toc_9">
其他使用場景?</h2>
<br />
只要你有需要跟其他系統、服務、外部程式進行資料交換,Protocol Buffers 就有他適用的地方。<br />
<br />
舉例來說,現在很多人開始採用 WebSocket 取代傳統的 Socket,使得 WebSocket 不再只是應用在瀏覽器之中,甚至可能是各種機器與機器之間的溝通。在這種情況下,其中交換、傳遞的資訊可能不是普通純文字這麼簡單,也很有可能是二進位類型、串流形式的資料,導致 JSON 可能因此不適合用於當作其中的資料交換格式。這時,就可以 Protocol Buffers 與 WebSocket 搭配使用。<br />
<br />
不只如此,在這 IoT 當道的年代,在這訊息技術滿天飛的年代 AMQP、MQTT 等各種通訊技術下,以及需要許多爆量資料收集分析的場景,Protocol Buffers 也很有發揮的空間。<br />
<br />
<h2 id="toc_10">
後記</h2>
<br />
要注意的是,Protocol Buffers 雖然是個好東西,但並非是個用來完全取代 JSON 的解決方案,JSON 仍有其可讀性高、易操作及通用性高等優點。在多數 API 設計的場景之下,JSON 仍然是最好的選擇。Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-35382850999779478792017-01-25T20:53:00.001+08:002017-02-02T16:25:11.890+08:00上手使用 JavaScript 的 Map、Reduce 吧!<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhne1CvzfY8rLtOOZU50G4m6lXB1O8Y2Ys1vxpl6rp0HbTNpFAAVbnY3MNvqrF262EBCoiDyvxGkqXzP2F_WH0AzIikUzYIq5s7lBNYN998fTu9amD_xrh0wP7QfrAzAzQInTLAfRvxqv4/s1600/js-mapreduce.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhne1CvzfY8rLtOOZU50G4m6lXB1O8Y2Ys1vxpl6rp0HbTNpFAAVbnY3MNvqrF262EBCoiDyvxGkqXzP2F_WH0AzIikUzYIq5s7lBNYN998fTu9amD_xrh0wP7QfrAzAzQInTLAfRvxqv4/s1600/js-mapreduce.png" /></a></div><span id="goog_1061361673"></span><span id="goog_1061361674"></span><br />
雖然有些概念類似甚至可以相通,但這裡並不是指常聽到的「MapReduce」,本文目的不是要討論如何運用 MapReduce 這樣的架構去處理大資料庫。這裡真正要討論的是,如何使用 JavaScript 裡陣列(Array)中的 .map() 和 .reduce() 方法,並把一些常見的使用方法和情境描述出來大家進行參考。<br />
<br />
很多人對這兩個方法不習慣,原因不外乎是這兩種方法本來就不是一個非常直覺的東西,在大多數 JavaScript 語言的開發情境中,其實也沒有非得使用的理由。但不得不說,習慣了這兩個對陣列操作的方法,程式碼會變得簡潔,也更容易能處理一整批的資料。有時也能順便學習到一些「Functional Programming」會用到的概念,無論是在改善程式品質,還是投資自己的角度上,都有相當好處。<br />
<br />
<h2 id="toc_1">從最簡單的遍歷陣列開始</h2><br />
面對一個陣列裡的一堆資料,我們一定是從遍歷開始,一一處理裡面的每一筆資料。你也許已經非常熟悉如何遍歷陣列,最常見的不外乎就是兩種做法。<br />
<br />
使用 for-loop:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3 ];
for (var index in myArr) {
console.log(myArr[index]);
}</code></pre></div><br />
使用陣列內建的 forEach 方法:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3 ];
myArr.forEach(function(element) {
console.log(element);
});</code></pre></div><br />
<h2 id="toc_2">使用 .map() 對每個陣列元素加工</h2><br />
有些時候,我們想對每個陣列元素(Element)進行加工處理,於是最土法煉鋼的方法大概就是這樣:<br />
<br />
幫每個元素加一:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3 ];
for (var index in myArr) {
myArr[index] = myArr[index] + 1;
}
// [ 2, 3, 4 ]
console.log(myArr);</code></pre></div><br />
這時你可以使用 .map() 方法來達成同樣目的:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3 ];
var newArr = myArr.map(function(element) {
return element + 1;
});
// [ 2, 3, 4 ]
console.log(newArr);</code></pre></div><br />
.map() 會將每一個元素代入處理函數,而處理函數回傳的值,會被收集組成一個新的陣列,這個新的陣列元素數量會和原本陣列的一樣。換句話說,同樣是對陣列加工後得到結果,它會回傳一個新的、加工過後的陣列,而不會修改原本的陣列內容。<br />
<br />
<h2 id="toc_3">使用 .map() 進行資料校正處理</h2><br />
當我們了解 .map() 的運作原理後,可以使用它做到更多資料處理的事,例如資料的校正或過濾。<br />
<br />
舉例來說,若是我們得到一個包含許多數值的陣列,而我們想限定這些數值不得超過我們設定的上限值,這時我們可以這樣處理,來得到一個經過檢查校正過後的資料結果:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
var newArr = myArr.map(function(element) {
// 數值大於五的數值視為五
if (element > 5)
return 5;
return element;
});
// [ 1, 2, 3, 4, 5, 5, 5, 5, 5, 5 ]
console.log(newArr);</code></pre></div><br />
<h2 id="toc_4">使用 .reduce() 進行數值加總</h2><br />
處理陣列資料的工作中,其中一項最常見的就是數值加總,或是進行統計運算。同樣的,若你使用土法煉鋼的做法,大致上如下:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3 ];
var result = 0;
for (var index in myArr) {
result += myArr[index];
}
// 6
console.log(result);</code></pre></div><br />
若使用 .reduce(),可以這樣寫:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3 ];
// 處理每個元素後等待回傳結果,第一次處理時代入初始值 0
var result = myArr.reduce(function(prev, element) {
// 與之前的數值加總,回傳後代入下一輪的處理
return prev + element;
}, 0);
// 6
console.log(result);</code></pre></div><br />
我們可以看到,改用 .reduce() 之後,陣列元素的加總計算,不會再一直存取到外部的 result 變數,而是算完結果後才將結果統計結果回傳。這樣做的好處,是不會再跨 Scope 去存取外部的變數,這對 JavaScript 這種有複雜 Scope 設計的語言來說,程式碼不會到處去污染。<br />
<br />
<h2 id="toc_5">把 .map() 和 .reduce() 串接起來吧!</h2><br />
這兩種方法都是用來處理陣列,所以我們可以輕易地串接兩者,以前面的例子來說,可以先對陣列資料進行校正和加工,然後對資料進行收斂和加總:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
var result = myArr
.map(function(element) {
// 數值大於五的數值視為五
if (element > 5)
return 5;
return element;
})
.reduce(function(prev, element) {
// 與之前的數值加總,回傳後代入下一輪的處理
return prev + element;
}, 0);
// 40
console.log(result);</code></pre></div><br />
<h2 id="toc_6">利用 .reduce() 進行陣列扁平化</h2><br />
如果你開始查 .reduce() 的資料,應該會看到一些 MDN 文件,會提到一些相當實用的功能,其中一個就是扁平化陣列的應用。簡單來說,就是將一個複雜的陣列,扁平化成一維,這在很多資料處理或數值計算上相當有用。<br />
<br />
<div><pre><code class="language-javascript">var myArr = [
[ 1, 2 ],
[ 3, 4, 5 ],
[ 6, 7, 8 ]
];
// 將所有元素都與之前代入的陣列相接起來,第一次處理時代入初始值空陣列
var newArr = myArr.reduce(function(arr, element) {
// ex: [ 1, 2 ].concat([ 3, 4, 5 ])
return arr.concat(element);
}, []);
// [ 1, 2, 3, 4, 5, 6, 7, 8 ]
console.log(newArr);</code></pre></div><br />
所以這個處理函數將會被執行三次:<br />
<br />
<ol><li>將空陣列與 [ 1, 2 ] 相接起來後回傳</li>
<li>將被代入的 [ 1, 2 ] 與 [ 3, 4, 5 ] 相接起來後回傳</li>
<li>將被代入的 [ 1, 2, 3, 4, 5 ] 與 [ 6, 7, 8 ] 相接起來後回傳</li>
</ol><br />
<h2 id="toc_7">利用 .reduce() 進行資料歸納和統計吧!</h2><br />
我們也可以利用 .reduce() 配合上物件操作,對陣列的內容進行統計工作:<br />
<br />
<div><pre><code class="language-javascript">var myArr = [
'C/C++',
'JavaScript',
'Ruby',
'Java',
'Objective-C',
'JavaScript',
'PHP'
];
// 計算出每種語言出現過幾次
var langStatistics = myArr.reduce(function(langs, langName) {
if (langs.hasOwnProperty(langName)) {
langs[langName]++
} else {
langs[langName] = 1;
}
return langs;
}, {});
// { 'C/C++': 1, 'JavaScript': 2, 'Ruby': 1, 'Java': 1, 'Objective-C': 1, 'PHP': 1 }
console.log(langStatistics);</code></pre></div><br />
<h2 id="toc_8">如果想要處理的資料是 Object 的形式怎麼辦?</h2><br />
運用 Object.keys() 這樣的技巧,我們可以把 .map() 或 .reduce() 結合使用到 Object 的資料上使用,這樣就可以對 Object 資料進行相同的統計運算或數值計算。<br />
<br />
<div><pre><code class="language-javascript">var data = {
'Fred': 1,
'Leon': 2,
'Wesley': 3,
'Chuck': 4,
'Denny': 5
};
// 使用 Object.keys() 取得包含所有 key 的陣列
var result = Object.keys(data).reduce(function(prev, name) {
// 利用 key 取得原始物件中的值,然後加總
return data[name] + prev;
}, 0);
// 15
console.log(result);</code></pre></div><br />
<h2 id="toc_9">你在寫啥?結合 ECMAScript 6 後,世界都不一樣了。</h2><br />
ES6 已經上了實際的戰場,當 .map()/.reduce() 方法加上箭頭函數(Arrow Function<br />
),然後又配合上 JavaScript 語言的特性,整個程式碼將變得更為簡短乾淨。<br />
<br />
<div><pre><code class="language-javascript">let newArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map((value) => value + 1);</code></pre></div><br />
當箭頭函數只有一個參數時,可以省去括號「()」:<br />
<br />
<div><pre><code class="language-javascript">let newArr = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map(value => value + 1);</code></pre></div><br />
註:不過,對於不習慣的人來說,更難閱讀了。但在開放原始碼和社群的圈子裡,因為已經被大量使用,所以最好趕快習慣它,會方便你更容易看懂坊間的各種「新」程式碼。<br />
<br />
<h2 id="toc_10">後記</h2><br />
當然,濫用 map/reduce 也可能會造成程式碼難以閱讀,無論是哪一種程式的技巧,這肯定都是一個問題。但至於什麼時候該用,什麼時候不該用,並不在本文範疇,個人認為,我們得先熟練使用這兩種方法,用熟了,再接著探討「好的使用情境」才有意義。因為很多人不熟悉,又不敢亂用,就更沒有機會習慣它了。<br />
<br />
所以,先別想太多,嘗試習慣使用它們吧!Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-30343694845320467712016-12-31T17:55:00.001+08:002016-12-31T18:01:08.236+08:00Node.js 小密技:以 Readline 核心模組一行行讀取檔案內容<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgl5BnehSpoAOTzC8a6Ip1rKlEAEepGvq25f_p31r3_ksDOHPVJ3AWQ5uw2Q7z95F1ll8b80s9kO86Ja0mC_52CyeX7sjYaHHeqwDgqvH95c_P_8zGMFTRdk60V70lplrilsZkXZUHjxBw/s1600/bv_7qb5kxc8-jens-johnsson.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgl5BnehSpoAOTzC8a6Ip1rKlEAEepGvq25f_p31r3_ksDOHPVJ3AWQ5uw2Q7z95F1ll8b80s9kO86Ja0mC_52CyeX7sjYaHHeqwDgqvH95c_P_8zGMFTRdk60V70lplrilsZkXZUHjxBw/s1600/bv_7qb5kxc8-jens-johnsson.jpg" /></a></div><br />
最近參與了一些關於資料處理的專案,處理了很多各式各樣的原始資料(Raw Data)或各種不同格式的資料,於是使用到了 Node.js 上的一些小技巧。像是一行行讀取檔案內容這件事,就隱藏了一些技巧。<br />
<br />
對很多人來說,處理的檔案內容都不大,如果用 Node.js 來一行行讀取檔案內容,不外乎就是將整個檔案讀出後再進行切割,做法大致上如下:<br />
<div><pre>var fs = require('fs');
fs.readFile('example.txt', function(err, data) {
// 以換行字元作為切割點,將內容切成一個大陣列
var lines = data.split('\n');
lines.forEach(function(line) {
// 一行行處理
});
});</pre></div>但有些時候,由於檔案並不小,若又牽涉到運算,不可能整個檔案都讀出到記憶體上才進行切割,這時就得用到 Stream(資料流)機制,將檔案一段段讀出來進行處理。然後,為了進行一行行的切割,我們會自己做這樣的機制,先將一段段讀取出來的檔案內容放到緩衝區(Buffer),然後找到換行字元進行切斷取出,然後再繼續讀取檔案,重複這樣的過程直到檔案結尾。<br />
<br />
的確,實做這樣的機制有點麻煩,所以其實能利用 Node.js 現成內建的核心模組 Readline 來做到切割資料流中一行字串的工作。因為常見的 Readline 用法都是拿來做終端機字元模式下的命令列操作,所以許多人沒有想到可以這樣使用 Readline。作法其實很簡單,就把 Readline 的 input 從標準輸入(Standard Input)換成我們的檔案讀取資料流就可以。<br />
<br />
完整做法如下:<br />
<div><pre>var fs = require('fs');
var readline = require('readline');
// 建立檔案讀取資料流
var inputStream = fs.createReadStream('example.txt');
// 將讀取資料流導入 Readline 進行處理
var lineReader = readline.createInterface({ input: inputStream });
lineReader.on('line', function(line) {
// 取得一行行結果
console.log('NEW LINE', line);
});</pre></div><br />
<h2>後記</h2>其實這樣的 Readline 用法,在 Node.js 官方 API 文件上可以看到,只不過是不久前才被加進去的,在文件的最後面。:-P<br />
<br />
參考連結:<a href="https://nodejs.org/api/readline.html">https://nodejs.org/api/readline.html</a>Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-38205190642006694132016-12-28T00:29:00.001+08:002016-12-28T00:30:06.687+08:00產品開發玩技術很過癮!實作 QML 動畫背景!<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhveakbPHbty2Gmv99twqxyU28KsbZIgEyE7AUFfSETcz3OP87SE81WNXdPfDWxcT8rS3G8BswADw4rdtFgs8EDtTqCEuWGZ1Dorl1s36IXcpHI9u9OpJmcD5SA_G4kP3pYWGaZIS0C7lY/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2016-12-27+%25E4%25B8%258B%25E5%258D%25889.29.39.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhveakbPHbty2Gmv99twqxyU28KsbZIgEyE7AUFfSETcz3OP87SE81WNXdPfDWxcT8rS3G8BswADw4rdtFgs8EDtTqCEuWGZ1Dorl1s36IXcpHI9u9OpJmcD5SA_G4kP3pYWGaZIS0C7lY/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2016-12-27+%25E4%25B8%258B%25E5%258D%25889.29.39.png" /></a></div>
<br />
由於最近在開發自己的產品,又開始重操舊業,開發起 Linux 系統的相關應用和嵌入式技術。為了這個產品,精心開發了一個使用者介面,除了動手把驅動程式搞定、圖形化介面搞定,也調教效能、改善系統架構。<br />
<br />
開發自己的產品很過癮,愛怎麼搞就怎麼搞!於是,看到死板的背景覺得很不舒服,就在思考是否可以跑個動畫背景呢?<br />
<br />
因為使用的是 QML 技術來開發 UI,最直接的想法,就是用 QtMultimedia 的 MediaPlayer 無限循環播放一個影片,當作動畫背景:<br />
<div>
<pre>MediaPlayer {
id: bg;
source: 'bg.mov';
loops: MediaPlayer.Infinite;
autoPlay: true;
}
VideoOutput {
anchors.fill: parent;
source: bg;
}</pre>
</div>
<br />
當然,我們選擇的背景影片,是一個開頭跟結尾一樣的影片,如果正確循環播放,會無縫接軌的變成一個順暢的動畫背景。<br />
<br />
然而,結果不如預期,碰到了一個問題,那就是每當背景影片播放到最後時,會畫面變成全黑,然後才再一次重新開始播放,沒辦法「無縫接軌」。仔細暸解以後,發現 MediaPlayer 元件是 QMediaPlayer 的 QML Type 實作,所有秘密都藏在 QMediaPlayer 之中。因為 QMediaPlayer 預設所有的通知事件,都是固定以 1000ms(1秒)的頻率來觸發,這代表,當 QML 元件發現影片播完時,通常已經是播完以後的事了,所以畫面一定會因為影片結束而變黑,然後 QML 元件才發現影片結束,重新進行播放。<br />
<br />
知道緣由後,我們可以從事件更新頻率下手,讓 QML 元件發現影片播完的時間更接近實際影片結束的時間,但這必須動用到 C/C++ 的實作,因為 QMediaPlayer 的事件更新頻率無法以純 QML 的方法修改。<br />
<br />
C/C++ 完整應用程式的實作如下,我們把更新頻率調高為每 100ms 一次:<br />
<div>
<pre>#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
#include <QMediaObject>
#include <QMediaPlayer>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/resources/App.qml")));
// Getting background component by using background objectName
QObject *obj = static_cast<QObject *>(engine.rootObjects().first());
QObject *background = obj->findChild<QObject *>("background");
// Set NotifyInterval to 100ms
QMediaPlayer *player = qvariant_cast<QMediaPlayer *>(background->property("mediaObject"));
player->setNotifyInterval(100);
return app.exec();
}</pre>
</div>
<br />
除此之外,為了可以順利取得 QML 中的 MediaPlayer 元件,我們需要幫其設定一個「objectName」作為識別,讓 C/C++ 這的原生程式可以搜尋的到該元件:<br />
<div>
<pre>MediaPlayer {
id: bg;
objectName: 'background';
source: 'bg.mov';
loops: MediaPlayer.Infinite;
autoPlay: true;
}</pre>
</div>
<br />
雖然我們縮短了每次更新的間隔時間,調到了 100ms,已經非常接近了影片結束時間,但仍然可能會發生問題。所以保險起見,我們可以多做些檢查工作,在影片結束前 100ms 左右時,就讓他重頭開始播放一次影片。<br />
<div>
<pre>MediaPlayer {
id: bg;
objectName: 'background';
source: 'bg.mov';
loops: MediaPlayer.Infinite;
autoPlay: true;
onPositionChanged: {
if (position >= duration - 100) {
bg.seek(0);
}
}
}</pre>
</div>
<br />
理論上,這樣做會損失不到 100ms 長度的動畫,但人通常感覺不出來這麼短的損失,而且是前方還有 UI 介面的情況之下。但如果你仍然有感,可以考慮把頻率改為 50ms 或更少。<br />
<br />
最後就可以享受會動的背景啦!Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-37262093431733948022016-07-27T12:05:00.000+08:002016-07-27T12:05:22.452+08:00JavaScript 好用的 async 異步函數!<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_5_iIalF5fT2E4ZwGJgmc2nTod4Pz6GzWb9A64r55Q7QD3f0-xZDs8cuhlafAdX6ksNg-bwh_tXop1hdl5dpfLC2KQKqa79Si-EOY-HGaYB1xggAvVirtNAURB6hn_IZ6XLGfWFrfbLI/s1600/ES7-async.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_5_iIalF5fT2E4ZwGJgmc2nTod4Pz6GzWb9A64r55Q7QD3f0-xZDs8cuhlafAdX6ksNg-bwh_tXop1hdl5dpfLC2KQKqa79Si-EOY-HGaYB1xggAvVirtNAURB6hn_IZ6XLGfWFrfbLI/s1600/ES7-async.png" /></a></div><br />
先聲明,async 異步函數是 ECMAScript 第七版(ES7)才被支援的語法和特性,目前 ES7 還沒有被大多數的 JavaScript Engine 所實作,如果你要使用,需要用到 babel 這類工具,先把此程式編譯轉換,讓其可在舊版本 JavaScript Engine 上執行。<br />
<br />
如果你覺得以 co 模組來操作 Generator 很好用,你可以想像 async 異步函數就是原生的 co,幾乎是同樣的使用方式,同樣的使用概念,只不過不再需要使用 generator 和 yield 這類語法。如果你是個過不了在函數上有個醜陋「*」符號這一關的人,async 異步函數的使用方式應該會讓你感覺到舒服許多。<br />
<br />
<h2>什麼是 async 異步函數(async functions)?</h2><br />
異步函數使用方式其實和一般的函數一樣,只不過在這函數之內的程式,可以用 await 的語法去執行並等待異步工作(如:Promise)而不需要使用到骯髒的 callback function。宣告並使用一個 async 異步函數,就是在定義函數時加上「async」,然後直接執行這個函數即可,簡單的範例如下:<br />
<br />
<div><pre>async function myAsyncFunc() {
console.log('Hello async functions!');
}
myAsyncFunc();
</pre></div><br />
<h2>搭配 Promise 的使用</h2><br />
Promise 通常被大量用來管理非同步的工作,並讓開發者容易管理錯誤拋出等機制,一個典型的 Promise 使用如下:<br />
<br />
<div><pre>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);
});
</pre></div><br />
如果在「異步函數」中呼叫以 Promise 包裝的工作,可以直接使用 await 語法:<br />
<br />
<div><pre>async function myAsyncFunc() {
var val = await task;
console.log(val);
}
myAsyncFunc();
</pre></div>你會看到在異步函數中,程式邏輯會以「像是阻塞的方式進行」,await 會等到工作完成後,將回傳值回傳,然後才繼續下一行工作。要注意的是,因為看起來像程式會阻塞,熟悉 JavaScript 的人會不自覺開始害怕事件引擎被鎖死,但實際上 await 是以非同步的方式在進行,並不會卡住或影響事件引擎的運作。<br />
<br />
<h2>搭配 Thunk 的使用</h2><br />
什麼是 Thunk?簡單來說就是一個處理函數,完成時會呼叫 callback 函數表示完成,實務上最常的用法會在外面包一層函數,創造一個 Closure,一個簡單的 Thunk 如下:<br />
<br />
<div><pre>function myThunkFunc(thing) {
return function(done) {
setTimeout(function() {
console.log(thing);
done(null, 'World');
}, 1000);
};
}
</pre></div><br />
異步函數裡面,我們可以這樣使用它:<br />
<div><pre>async function myAsyncFunc() {
var val = await myThunkFunc('Hello');
console.log(val);
}
myAsyncFunc();
</pre></div><br />
<h2>等待其他異步函數完成工作</h2><br />
當然,await 除了可以吃 Thunk 和 Promise 之外,也可以處理並等待其他的「異步函數」,如下:<br />
<div><pre>async function anotherAsyncFunc(thing) {
var val = await myThunkFunc(thing);
return val;
}
async function myAsyncFunc() {
var val = await anotherAsyncFunc('Hello');
console.log(val);
}
myAsyncFunc();
</pre></div><br />
<h2>錯誤處理</h2><br />
當 Promise 的 reject() 被呼叫,或是 Thunk 的 callback 函數被呼叫時,第一個參數不是 null,就代表這個異步工作是有錯誤發生的,如果要從 await 偵測這些錯誤訊息,需要使用 try-catch 去接這些錯誤訊息。<br />
<br />
<div><pre>async function myAsyncFunc() {
try {
var val = await myThunkFunc('Hello');
} catch(e) {
console.log(e);
}
}
myAsyncFunc();
</pre></div><br />
<h2>舒服!到處使用異步函數</h2><br />
一旦你熟悉如何使用異步函數,你可以到處使用。其實他就像一般的函數一樣,他可以被當成一個 callback 來使用,像是下面這個例子,就把它當成 Promise 的處理函數:<br />
<br />
<div><pre>var task = new Promise(async function(resolve, reject) {
try {
await doAsyncTask();
} catch(e) {
return reject(e);
}
resolve();
});
</pre></div><br />
<h2>後記</h2><br />
如果你是原本就在使用 co 模組的人,應該會發現 async/await 根本就是一樣的東西,對你來說根本無痛,唯一有點麻煩的是,目前 JavaScript 仍然還沒有原生支援,需要 babel 一類的編譯器才能使用。但有不少人看重程式碼的簡潔和漂亮,已經大量使用了。<br />
<br />
另外提到,Koa 2.0 因為完全採用 async/await 的方式,無限期處於不穩定版本。等到 async/await 被原生支援那一天, Koa 2.0 穩定版就會推出了,相信這一天就快要到來。Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com1tag:blogger.com,1999:blog-4389461234607418203.post-26229791901068534652016-06-06T11:03:00.001+08:002016-06-06T16:33:42.395+08:00下一代的框架:Koa 1.0 起手式<a href="https://camo.githubusercontent.com/674563115c4e0d4e5d99440b916952ad795c498e/68747470733a2f2f646c2e64726f70626f7875736572636f6e74656e742e636f6d2f752f363339363931332f6b6f612f6c6f676f2e706e67" imageanchor="1"><img border="0" src="https://camo.githubusercontent.com/674563115c4e0d4e5d99440b916952ad795c498e/68747470733a2f2f646c2e64726f70626f7875736572636f6e74656e742e636f6d2f752f363339363931332f6b6f612f6c6f676f2e706e67" /></a><br />
<br />
身為 Node.js 使用者的你,還在使用 Express 嗎?快來使用下一代的 Web Framework 吧!Koa 是由 Express 的開發者們出來所開發的新網站框架,嘗試採用了最新的 ECMAScript 6 語法,讓開發者可以用更簡約的方式,開發網站應用程式,讓程式碼更好維護之外,也能受益於最新的語言特性。<br />
<br />
<h2>
穩定版與不穩定版</h2>
<br />
現在的 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 的語法。<br />
<br />
本篇文章的重點將放在 Koa 1.0 之上,畢竟在 ECMAScript 7 還處於草案階段的現在,很難說未來會不會有什麼改變。<br />
<br />
<h2>
安裝 Koa</h2>
<br />
由於 Koa 需要用到 ECMAScript 6 的語言特性,請先檢查你的 Node.js 版本,最少為 0.12 以上,如果你已經使用了 Node.js 4.0 或更高版本,請不用擔心這個問題。<br />
<br />
然後透過 NPM 即可安裝模組:<br />
<pre>npm install koa</pre>
<br />
<h2>
開發第一個應用程式</h2>
<br />
開發 Koa 應用程式非常容易,下面是程式碼範例:<br />
<pre>var koa = require('koa');
var app = koa();
app.use(function *() {
this.body = 'Hello World';
});
app.listen(3001);
</pre>
跑起來後,用瀏覽器連入 3001 埠即可看到「Hello World」的字樣,因為 this.body 的內容,將會被輸出到前端瀏覽器上。<br />
<br />
<h2>
使用 Generator 打造的 Middleware</h2>
<br />
koa.use() 將用來載入 Middleware,所有連線工作都會經過 Middleware 處理。所以前一個例子裡,我們使用 koa.use() 設定了一個處理函數,該函數會用來處理所有連線工作。<br />
<br />
要注意的是,在 function 後面有一個「*」的符號,這代表這個函數是一個 Generator 函數,所以這函數裡面的程式將可以使用 Generator 的語言特性。若您不知道 Generator 是什麼,可以參考過去的舊文「<a href="http://fred-zone.blogspot.tw/2015/07/es6-generator-co.html">快樂玩 ES6 Generator,從 co 起手式開始</a>」。<br />
<br />
【註一】如果你有過 express 開發經驗,對於 koa.use() 會相當熟悉,Koa 同樣支援了 Middleware 的架構,你可以將過去的程式輕易移植到這新的框架上。<br />
【註二】Koa 底層使用 co 來操作 Generator,若你覺得 Generator 太過艱澀,只需要了解 co 的使用即可。<br />
<br />
<h2>
加入多個 Middleware</h2>
<br />
所有的連線要求可以透過一系列、不只一個 Middleware 來處理,我們可以利用多次 koa.use() 來使用它,範例如下:<br />
<pre>var koa = require('koa');
var app = koa();
app.use(function *(next) {
yield next;
});
app.use(function *() {
this.body = 'Hello World';
});
app.listen(3001);
</pre>
<br />
一個 Middleware 可以透過 yield 傳入 next 參數,讓連線要求進入到下一個 Middleware 被處理。<br />
<br />
<h2>
自訂 Router 和路徑管理</h2>
<br />
之前的範例直接使用 koa.use(),會將所有的連線都導入同一個處理函數,輸出同一個結果。若我們想要自訂不同的路徑,讓不同路徑用不同的處理函數,將需要額外安裝「koa-router」模組:<br />
<pre>npm install koa-router</pre>
<br />
然後可以直接修改我們的程式碼如下:<br />
<pre>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);
</pre>
範例中只有使用到「GET」方法,如果要用來開發 Restful API 或是處理一些表單上傳的工作,可以依樣畫葫蘆使用 router.post、router.put 或 router.del 方法。<br />
<br />
<h2>
接收 QueryString 的資料</h2>
<br />
QueryString 可說是歷史悠久且非常常見的傳值方法,藉由一個網址後面加上一個「?」字元後,就可以使用鍵值(Key/Value)來進行資料傳遞,並用「&」區隔多組資料。一個簡單的實際應用如下:<br />
<pre>http://my_server/send?name=fred&msg=Hello
</pre>
<br />
取得資料的方法如下:<br />
<pre>console.log(this.request.query.name);
console.log(this.request.query.msg);
</pre>
<br />
<h2>
接收 body 的資料</h2>
<br />
當我們使用「POST」或「PUT」方法,我們就可以利用 body 傳送一些資料到伺服器,像是網頁表單時常使用這樣的傳值方法。若想要取得 body 的資料,必須先安裝一個「koa-bodyparser」模組:<br />
<pre>npm install koa-bodyparser</pre>
<br />
使用 koa.use() 載入 koa-bodyparser,koa 就會自動在處理連線時使用它解析 body:<br />
<pre>var bodyParser = require('koa-bodyparser');
app.use(bodyParser());
</pre>
<br />
然後可以在路徑處理函數中,正常取得 body 內的資訊:<br />
<pre>console.log(this.request.body.name);
console.log(this.request.body.msg);
</pre>
<br />
<h2>
靜態文件支援</h2>
<br />
除了一般動態網頁外,我們也會在網頁中嵌入 CSS、前端的 JavaScript 和圖片等靜態檔案,這些檔案在瀏覽器載入頁面時,同時間也要提供瀏覽器能取得。為了達成這功能,可以使用「koa-static」來達成:<br />
<pre>npm install koa-static</pre>
<br />
然後可以直接加入這個 middleware:<br />
<pre>var serve = require('koa-static');
app.use(serve(__dirname + '/public'));
</pre>
<br />
其中要帶入路徑參數,告訴 koa-static 去哪個目錄尋找對應的靜態檔案,範例中是設定為此程式同一個目錄下的 public 目錄。<br />
<br />
<h2>
Session 支援</h2>
<br />
要使用 Session 要先安裝 koa-session:<br />
<pre>npm install koa-session</pre>
<br />
然後就可以在處理函數中使用 this.session 這個物件來存放資料:<br />
<br />
<pre>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);
</pre>
<br />
這範例會在瀏覽器每次連線時,把 session 內的計數器加一,所以若是我們重複整理這個頁面,會看到數字不斷增長。<br />
<br />
要注意的是,使用 session 前,我們要為 app.keys 設一組金鑰(Key), koa-session 會使用這組 Key 加密我們的 session 資料。<br />
<br />
<h2>
後記</h2>
<br />
還在使用 express 嗎?別老土了。(笑)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-35070450172228003852016-05-11T23:02:00.001+08:002016-05-11T23:04:11.313+08:00MakerBoard: 自幹 MT7688 模擬器!簡報釋出!<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgClw8bosTOAUupms_bX0ixle6ah42RzaEA0wghH4-Zdic4AKcXt47ZDO8znLt9lISf_hyphenhyphen4PJlU4D5vo-VmXy4zW5b0J8hyphenhyphendMM7NL99xF1x4GXHYxaPoXezmFR7Guc5xNP-bvjN0SJcL-k/s1600/1*AeTTPrAvjUbvlu5NeR6nwg.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgClw8bosTOAUupms_bX0ixle6ah42RzaEA0wghH4-Zdic4AKcXt47ZDO8znLt9lISf_hyphenhyphen4PJlU4D5vo-VmXy4zW5b0J8hyphenhyphendMM7NL99xF1x4GXHYxaPoXezmFR7Guc5xNP-bvjN0SJcL-k/s640/1*AeTTPrAvjUbvlu5NeR6nwg.png" width="640" /></a></div>
<br />
使用 MTK LinkIt Smart 7688 這類開發板時,總是很痛苦,由於儲存空間不大,記憶體也不大,常常在開發的過程中飽受折磨。於是我們開始思考如何可以在自己的電腦上,模擬一個 MT7688 的環境,在有充沛資源的機器上進行開發。就這樣,前陣子開發了一個小小的 Open Source 工具專案「MakerBoard」,並在 MakerCup 的共筆網站發表「<a href="https://medium.com/maker-cup/%E6%B2%92%E6%9C%89%E6%9D%BF%E5%AD%90%E4%B9%9F%E5%8F%AF%E4%BB%A5%E7%8E%A9-7688-%E6%A8%A1%E6%93%AC%E5%99%A8-25cb7d082dd5">沒有板子也可以玩的 7688 模擬器!</a>」。<br />
<br />
雖然這樣一個小小的模擬器運用了 VM 和 Container 相關技術。但其實主要概念並不難,這次 5/10 在台大的開放原始碼課程中,就簡單從 MakerBoard 這專案出發,然後說明了一下怎麼樣自己打造一個簡單的 Container,並利用 QEMU 來進行 Binary Translation 的工作。<br />
<br />
簡報釋出,請自行服用:<br />
<br />
<div style="text-align: center;">
<iframe allowfullscreen="" frameborder="0" height="485" marginheight="0" marginwidth="0" scrolling="no" src="//www.slideshare.net/slideshow/embed_code/key/r4lBoh76FRmM6d" style="border-width: 1px; border: 1px solid #ccc; margin-bottom: 5px; max-width: 100%;" width="595"> </iframe></div>
Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-45733802974885072532015-12-13T00:10:00.000+08:002015-12-13T00:10:43.733+08:00從 Maker 出發並反思:於是我們成立了 MakerCup!<div style="text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHNk7ZCmOFyyU6mPfSya7IVyQ_UFHoueggb3XMs51mgM8Asn5_pq5_t5-UoDPk9cEZdCp3MYYC33sbTzmQal6MAjTQ41ljv2FBpfc_7AhDwyZuUCpJ-rvO4PCBx-bqChBQ7vvkxmRxWEk/s1600/12345465_1013862918633878_7821304416010543559_n.jpg" imageanchor="1"><img border="0" height="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHNk7ZCmOFyyU6mPfSya7IVyQ_UFHoueggb3XMs51mgM8Asn5_pq5_t5-UoDPk9cEZdCp3MYYC33sbTzmQal6MAjTQ41ljv2FBpfc_7AhDwyZuUCpJ-rvO4PCBx-bqChBQ7vvkxmRxWEk/s640/12345465_1013862918633878_7821304416010543559_n.jpg" width="640" /></a></div>
<br />
Maker 一詞近年來翻紅,有人稱「自造者」,有人稱「創客」,以代工起家的國內產業,覺得 Maker 風潮是一個維持舊有工業地位的方法和機會,更將其引伸成軟硬整合、創業模式,無一不紛紛出來插手,想佔一塊地,分一杯羹。有些媒體將 Maker 塑造成有專業技術能力的人們,彷彿與一般人有很大的鴻溝。種種因素,自然越來越多人不了解 Maker 是什麼了。<br />
<br />
但我們認為真正的 Maker 並不是擁有厲害能力的人,而是願意動手落實的人。<br />
<h2>
為什麼我們要成立 MakerCup?</h2>
我們想聚集純粹想動手、交流的朋友,並讓更多人參與並體驗 Maker 的世界。<br />
<br />
事實上,Maker 的定義很簡單,凡是能打造、做東西的人,都能稱為 Maker。做菜的廚師,是個 Maker;編織衣服的人,是個 Maker;畫家,也是個 Maker。當然,寫軟體、做電子電路的人,以及各種設計師,通通都算是 Maker。無論在什麼領域,Maker 精神強調的是動手去實現、完成,去參與過程、瞭解過程,進而讓自己更有能力去打造出更多創意十足的東西。更重要的是,在這種不設限的旅程,能讓我們都具備著跨領域思考的能力。<br />
<br />
既然過程才是最重要的,我們便開始思考怎麼樣讓更多人交流,交流技術、能力,共同發展和探討更多的知識。我們不應該只是追求一時且短暫的成果,滿足政府或代工產業想要立即成果的 KPI,更或是不應該鑽牛角尖盲目追求頂尖技能,而是讓更多人參與、動手,普遍瞭解更多不同的事物和技能。<br />
<br />
於是, MakerCup 這個社群出現了,每週四都會舉辦一場分享交流活動或是小聚會,讓 Maker 平日下班或閒暇時,可以來走走坐坐,輕鬆喝點小飲料,或是現場做點東西:<br />
<a href="https://www.facebook.com/groups/MakerCup/">https://www.facebook.com/groups/MakerCup/</a><br />
<br />
我們希望,這個社群將如一碗太古時代的生命濃湯一般,熬煮出真正的 Maker 生命。<br />
<h2>
延續黑客松台灣的精神</h2>
還記得這一年,我們籌辦了整個年度的「<a href="https://hackathon.tw/">黑客松台灣(Hackathon Taiwan)</a>」,每個月都有 300 至 500 人的大型創作活動,讓不敢踏出來的年輕學子、上班感到無聊的人、及很少離開自己專業領域的朋友,走出來到活動上以「能力會友」。這一年的過程,讓大家的成果,從簡陋成長到真正的創意或產品,從簡單的技術到複雜的應用,從小設計到跨領域的整合。<br />
<br />
想當初,很多人剛開始嘲笑我們的成果都像玩具,勸我們不要再辦下去,請大家白吃白喝沒有意義。但一年以後的今天,事實證明我們是對的,黑客松台灣的參加者們,有最堅實的創意、能力和執行力,能解決各式各樣的問題,就算去號稱 Maker 聖地的中國深圳,也絲毫不遜色。<br />
<br />
更重要的是所有人都樂在其中,並把這份能力和喜悅,帶回原本的工作崗位上。<br />
<br />
同樣的精神,同樣的想法,我們一樣將在 MakerCup 落實。我們希望讓更多人來交流,讓更多人來學習動手,共同成長,而不將只是各式各樣的發表會而已。<br />
<h2>
我們所見、期待的未來?</h2>
<div>
從商業角度,許多傳統代工廠,在初面對 Maker 時,都誤將 Maker 當作了新的客人,期望 Maker 能產出點子、找到客人,然後下單。事實上,Maker 並不應該是代工廠下的消費者,而是橫向整合者,將不同領域、需求及客群,重新整頓和安排設計,然後創造出各種新的產業型態。而對於代工廠,精緻化並不再是唯一選擇,成為各行各業的技術供應者亦是一種出路。</div>
<div>
<div>
<br /></div>
<div>
所以我們相信,新的世代和市場潮流,不是築一道高牆,將 Maker 拒於專業的工廠門外,而是讓 Maker 視野做廣、扎根,讓大家愛上來台灣當一個 Maker,做出許多前所未有的成果或產品。未來,肯定會有更多的企業投入、民間組織投入,技術上也會有更多模組化解決方案,或是各類的知識交流,甚至是文化交流,來支撐這樣的整合性變革。</div>
<div>
<br /></div>
<div>
不可否認,在 Maker 的年代,什麼產業都將會是科技業,也都會是混血兒產業,誰能迎合這樣多族群共榮,誰就能在這時代中發光發熱。<br />
<h2>
歡迎加入我們!</h2>
MakerCup 社群是由黑客松台灣(Hackathon Taiwan)的部分成員共同推動的,感謝背後有更多合作單位或是朋友的陸續協助和資助,如黑客松台灣講師發起的創作學校「<a href="http://letschool.com/">LetSchool</a>」、「聯發科 MediaTek」、「Seeed Studio」、「緯創 Wistron」、「台灣品牌協會」、「台灣土地開發」及「卡市達創業加油站」。不久的將來,還有「Node.js Party」、「IoT Taiwan 社群」、「MakerBot」或是「品酒社群」在這一望無際的場地裡當鄰居。<br />
<br />
更多需要感謝的朋友們,不勝枚舉,也歡迎更多人共襄盛舉這樣具有台灣風味的「圓山社群觀光夜市」。</div>
<div>
<br /></div>
<div>
不多說了,先來一杯 Maker 吧!</div>
</div>
Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-46099442512539911222015-11-01T07:03:00.001+08:002015-11-01T10:00:28.976+08:00Lantern 專案:快速打造屬於自己的 Isomorphic 網站服務<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-KgrATwrCHmk3QdnVlV5Bs6X2dzGVbo-qN4baq0u8YHKqePol-SRm7kEJJqT64xLIT4jO2mfPl8YLgKFszyOeSVUU0Qtgu6zFQx-Ge6DbnA6fRWCZwsALfqhRs35d1aHIK_lJVqwNpzs/s1600/lamp-halloween-lantern-pumpkin.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-KgrATwrCHmk3QdnVlV5Bs6X2dzGVbo-qN4baq0u8YHKqePol-SRm7kEJJqT64xLIT4jO2mfPl8YLgKFszyOeSVUU0Qtgu6zFQx-Ge6DbnA6fRWCZwsALfqhRs35d1aHIK_lJVqwNpzs/s1600/lamp-halloween-lantern-pumpkin.jpg" /></a></div><br />
話說,Isomorphic 一直是 Node.js 開發者的夢想,期望同一套程式碼前後端都可以使用,大幅簡化程式碼和加速開發。此外,動態網頁的 SEO 問題也可以同時獲得解決,許多效能問題也可以得到改善。但是,要實現 Isomorphic 的架構,有很多的問題得先解決,會花大量時間在前期工作上,往往讓許多開發者頭痛。<br />
<br />
儘管頭痛,仍然阻止不了大家往 Isomorphic 的世界前進,我也因此建立了一個專案「<a href="https://github.com/cfsghost/lantern">Lantern</a>」,希望能讓更多人能以 Isomorphic 架構,快速建構出自己的網站服務,省去許多前期工作的時間。該專案是一個網站服務的樣板,實作了會員系統、權限管理、第三方登入、多國語系和送信機制等功能,在使用者介面上也做了一個還算美觀的介面。基本上,開發者只要 clone 下來,然後修改設定檔或改改介面、增加點功能,就可以快速完成一個屬於自己的全新網站服務。<br />
<br />
最特別的是,「<a href="https://github.com/cfsghost/lantern">Lantern</a>」整合了現今所有最新的技術和概念,包括了 Koa、React、FLUX、ES6/7+、Webpack 以及 Semantic UI,大量運用了 Generator、class 及 decorator 等最新 JavaScript 語言特性來簡化設計。所以,如果你想要接觸最新的技術,完全可以透過修改「<a href="https://github.com/cfsghost/lantern">Lantern</a>」專案來學習和熟悉。<br />
<br />
目前「<a href="https://github.com/cfsghost/lantern">Lantern</a>」支援 Facebook 剛發佈的最新 React v0.14+ 和 react-router 1.0.0+,也避免使用像 redux 這類反 FLUX 原始設計的框架,讓原本熟悉 React 和 FLUX 架構的開發者,可以快速上手。也提供一些常見的 Extension,方便開發者寫出前後端通用的程式碼,大多數情況下,開發者不需思考程式碼運行在前端還是後端。<br />
<br />
<h2>快速安裝使用</h2>若想要使用「Lantern」,方式很簡單,先從 Github 取得程式碼:<br />
<div><pre>git clone git@github.com:cfsghost/lantern.git</pre></div><br />
安裝必要之 NPM 模組:<br />
<div><pre>npm install</pre></div><br />
使用 webpack 編譯專案(若要正式上線,可加上 -p 選項來編譯):<br />
<div><pre>webpack</pre></div><br />
運行網站服務:<br />
<div><pre>node app.js</pre></div><br />
最後可以使用瀏覽器開啟網址,確認是否成功:<br />
<div><pre>http://localhost:3001/</pre></div><br />
<h2>修改設定檔</h2>一般情況,你無需做任何設定就可以把服務跑起來,但如果你需要修改網站名稱、使用自己的第三方登入設定以及電子郵件伺服器,可以修改 Lantern 的設定檔。設定檔是 JSON 的格式,相當容易修改。<br />
<br />
<br />
<ol><li>只要進入到「configs」目錄</li>
<li>把「general.json.default」複製一份並更名為「general.json」</li>
<li>修改「general.json」內的設定</li>
<li>重啟服務</li>
</ol><br />
<h2>目錄架構</h2>如果你想要開始客製化網站服務,需要先簡單理解「Lantern」的目錄架構。<br />
<div><ul><li>src - 主要程式</li>
<ul><li>js - 頁面部分的程式</li>
<li>img - 存放圖片</li>
<li>less - CSS 原始碼</li>
<li>translations - 存放多國語言的對應表</li>
</ul><li>routes - 主要為 Restful API</li>
<li>lib - 後端的相關函式庫(資料庫、第三方認證、發送電子郵件等功能)</li>
<li>models - 資料庫 Schema</li>
</ul></div><br />
<h2>快速上手開發</h2>首先記得,只要你修改了「src」底下的任何檔案,你必須重新執行「webpack」來進行編譯。或是可以跑一個「webpack -w」在背景,讓 webpack 在檔案有變更的時候自動重新編譯程式碼:<br />
<div><pre>webpack -w
</pre></div><br />
一般來說,我們會從頁面修改和增減開始進行客製化工作。由於「Lantern」是採用 React 來繪製頁面,所有的頁面程式都將放在「src/js/components」底下,只要看到副檔名為「.jsx」的檔案,就分別是各種畫面上的元件。<br />
<br />
<h2>建立新的頁面</h2>建立頁面需要修改「src/js/routes.js」,加入一個網址及對應的頁面元件(以 Chatroom.jsx 為例):<br />
<div><pre>module.exports = [
// 省略 ...
{
path: '/chatroom',
handler: require('./components/Chatroom.jsx')
}
];
</pre></div><br />
接著可以建立「src/js/components/Chatroom.jsx」檔案,開始設計你的頁面。如果需要使用 FLUX 的機制,可以載入並引入「Lantern」所提供之 decorator 到你的 React 元件上:<br />
<div><pre>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;
</pre></div><br />
<h2>開發自己的 Actions 和 Stores</h2>假設你已經很了解 FLUX 的開發模式,你可以直接開始設計 Action 和 Store。對「Lantern」而言,無論是 Action 和 Store 都是一樣的東西,只不過執行的順序不一樣。<br />
<br />
建立 Action(放在 src/js/actions/chatroom.js):<br />
<div><pre>export default function *() {
this.on('action.Chatroom.say', function *(name, message) {
this.dispatch('store.Chatroom.addMessage', name + ':' + message);
});
};
</pre></div><br />
建立 Store(放在 src/js/stores/chatroom.js):<br />
<div><pre>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');
});
};
</pre></div><br />
最後在「actions/index.js」和「stores/index.js」分別載入新建立的 Action 和 Store:<br />
<div><pre>export default {
// ...省略
chatroom: require('./chatroom')
};
</pre></div><br />
<h2>存取 Restful API</h2>「Lantern」提供了統一的方法呼叫 Restful API,無論前端還是後端都可以使用(在 Store 或 Action 中),此外,如果在後端使用呼叫,該方法會自動接續使用者的 Session (登入)狀態,進行 Restful API 存取。使某些使用者登入後才可存取的 API,更為容易被存取。<br />
<div><pre>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;
}
}
});
};
</pre></div><br />
<h2>在畫 React 元件前先預載資料</h2>後端要把畫面送到瀏覽器前,有時需要先資料庫的資料載入,預先植入畫面之中,前端有時也需要預先載入一些資料,以便畫面宣染時有實質內容。我們可以透過載入「@preAction」這個 decorator 來達成這個需求。「@preAction」會在元件初始化前,先去執行一些工作。<br />
<br />
底下範例是利用「@preAction」去跑 FLUX 裡的 Action - 「Chatroom.fetchMessages」:<br />
<div><pre>import { preAction } from 'Decorator';
// 相當於 this.flux.dispatch('action.Chatroom.fetchMessages')
@preAction('Chatroom.fetchMessages')
class MyComponent extends React.Component {
// ...
}
</pre></div><br />
當然可能要預先做的工作不只一項,而且可能要帶入 React 元件的 props 或更多資訊到 Action 中。「@preAction」可以被帶入函數,作更複雜的設計:<br />
<div><pre>@preAction((handle) => {
handle.doAction('Chatroom.fetchMessages');
handle.doAction('Chatroom.doSomething', handle.props.blah, 123);
})
</pre></div><br />
因為 Store 會因為「@preAction」而被更新、有資料,這時就可以理所當然地在元件初始化時直接取用 State(Store)的內容。<br />
<div><pre>class MyComponent extends React.Component {
constructor(props, context) {
super();
this.state = {
messages: context.flux.getState('Chatroom').messages;
};
}
// 省略 ...
}
</pre></div><br />
<h2>動態載入 JavaScript 或 CSS 檔案</h2><br />
很多 JavaScript 或 CSS 檔案是隨著 React Component 的載入,才會被動態載入,有時甚至需要照順序載入。此外,通常這樣的機制比較多會被使用在前端瀏覽器的頁面上,同樣的載入程式碼工作,在後端 Rendering 時往往會壞掉而無法通用,這在 Isomorphic 的架構中往往需要特別處理,像是判斷執行期是在前端還是後端,相當麻煩。<br />
<br />
為此,「Lantern」提供了「@loader」這個 Decorator,使開發者可以容易引入動態載入的機制,而且不用思考前後端的問題,也可以控制載入順序,或是等待檔案載入完成。<br />
<br />
以下範例就是一個載入地圖 API 的範例,載入工作只會在前端執行,不會在後端執行:<br />
<div><pre>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 <div></div>;
}
}
</pre></div><br />
<h2>取得和監聽視窗資訊</h2><br />
為了更方便前端排版,尤其是需要滿版的設計時,我們往往需要得知或監控瀏覽器視窗的大小,通常做法是存取瀏覽器中的「window」物件,並監聽事件來達成。但「window」物件只在瀏覽器上存在,在後端如果存取該物件,會失敗而且有錯誤發生,在以往 Isomorphic 架構中,每次都要特別處理,相當麻煩。因此「Lantern」預設提供了一個名為「Window」的 Store,將這類資訊包裝起來,使 React Component 能輕易存取又不會因在後端或前端而出現問題。<br />
<br />
下面範例就是存取 Window 的例子,以及監聽視窗大小改變的事件。<br />
<div><pre>@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 <div>{this.state.winWidth}x{this.state.winHeight}</div>;
}
}
</pre></div><h2>看不懂很多 ES6 和 ES7 的東西?</h2>這邊已經整理了一些常用的對應表「<a href="https://github.com/cfsghost/lantern/wiki/ES6-and-ES7">ES6 and ES7</a>」,方便開發者理解其中的語法。<br />
<br />
<h2>更多文件和說明</h2><div>更多資訊可以參考 Github 上的 Wiki:</div><div><a href="https://github.com/cfsghost/lantern/wiki">https://github.com/cfsghost/lantern/wiki</a></div><h2>後記</h2>其實「Lantern」已經改版了幾次,因為之前在好幾個要上線的專案上,每次都發現有些許不足之處,所以就不斷翻新架構和改進,甚至是優化效能。到目前為止,大致已經算是穩定的狀態,未來的開發方向不外乎是繼續寫 Isomorphic 的 Extension,以及效能優化。<br />
<br />
如果你有興趣,歡迎加入並共同改善這個專案。:-)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-54832775025104410212015-09-30T11:54:00.000+08:002015-09-30T11:54:07.781+08:00Git 大哉問:如何為 Fork 出來的專案,同步上游的更新?<div class="separator" style="clear: both; text-align: center;">
<a href="https://git-scm.com/images/logos/downloads/Git-Logo-2Color.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="165" src="https://git-scm.com/images/logos/downloads/Git-Logo-2Color.png" width="400" /></a></div>
<br />
<br />
搭配使用 Git 進行開發工作,時常會碰到一個狀況,就是我們 fork 一個專案出來修改,但在我們在修改的同時上游有了更新,這時我們會想要把上游的更新同步下來。這是一個常見的問題,許多人不時會提出來詢問,事實上如果你去 Google ,多半能找到這樣一篇名為「<a href="https://help.github.com/articles/syncing-a-fork/">Syncing a fork</a>」的 Github 文件。雖然這篇文章已經把程序詳細列出來了,但還是有人看不太懂,原因是要搭配「<a href="https://help.github.com/articles/configuring-a-remote-for-a-fork/">Configuring a remote for a fork</a>」這一篇文件一起看才知道來龍去脈。<br />
<br />
簡單來說,我們要先把「上游(upstream)」的 repository 加入我們眼前正在修改的專案,然後把上游更新拉回來,最後再與我們現有程式碼合併。<br />
<br />
首先,加入上游的 repository 並命名為「upstream」:<br />
<div>
<pre>git remote add upstream https://github.com/YOUR_USERNAME/YOUR_FORK.git</pre>
</div>
<br />
未來想要更新前,可以使用 fetch 去拉上游的更新回來:<br />
<div>
<pre>git fetch upstream</pre>
</div>
<br />
最後再把 upstream 的內容,與現有的正在修改的進行「合併」:<br />
<div>
<pre>git merge upstream/master</pre>
</div>
Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-88297066430377776922015-09-20T04:13:00.000+08:002015-09-20T04:13:41.537+08:00Fluky - 打造 Isomorphic App 的副產品:一個基於事件驅動的 Flux 框架<div class="separator" style="clear: both; text-align: center;"><a href="https://images.unsplash.com/photo-1436874555419-bb64221c5c1d?q=80&fm=jpg&s=4593f78e972ee577c98775d55b66a344" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="480" src="https://images.unsplash.com/photo-1436874555419-bb64221c5c1d?q=80&fm=jpg&s=4593f78e972ee577c98775d55b66a344" width="640" /></a></div><br />
要講到 Fluky 這一個 Flux 框架,這要從我的一個新計畫說起。因為最近又重新興起了一波 Isomorphic App 的熱潮,許多人開始打造了自己的 Isomorphic 網站,自己也做了一個。而什麼是 Isomorphic 呢?簡單來說,就是寫一次程式,然後前後端都可以使用的機制,也是一個網站服務工程師的夢想。還記得過去自己曾實作了 frex.js 試圖達成 API 層面的 Isomorphic,現在 React 這樣的前端框架,更提供了一個打造前後端 Rendering 的 Isomorphic,使得原本在前端動態產生的畫面,可以在後端產生,更一舉解決了 SEO 的問題。<br />
<br />
話說,搭上了這波熱潮,最近開始土炮自己的 Isomorphic App,Github 上開啟了一個「<a href="https://github.com/cfsghost/lantern">Lantern 燈籠</a>」專案,希望做一個標準的專案架構,讓自己以後開發新專案時,可以不需要重新再來一遍。痛苦的是,與很多人一樣,踩到了很多地雷,在專案架構設計上,也一直有很多許要調整的地方,這也難怪,畢竟這是一個興新的開發概念。<br />
<br />
於是從零到有的過程中,也有許多副產品,其中包括了一個新的 Flux 框架「Fluky」。很多人問我為什麼不採用當今紅遍半天邊的「redux」,原因其實很簡單,我不想脫離傳統 Flux 模式和 React 開發的概念太遠,然後同時想要用試著更精簡的方式描述這些流程。另外一點是,受到過去 X11 這世界最先進的網路圖形化視窗系統的設計所啟發,打算試著全面使用「事件」來管理資料流和程式上任何的溝通。<br />
<br />
如果你有興趣,可以直接以 NPM 來安裝這個 Flux 框架:<br />
<div><pre>npm install fluky</pre></div><br />
<h2>Fluky 的設計</h2><br />
基本上, Fluky 本身的概念很簡單,幾乎所有的行為都是透過 Fluky.dispatch() 這個 API 來進行,包括呼叫 Action、Store,然後所有的行為都可以使用 Fluky.on() 所監聽。也就是說,只需要這兩個 API,幾乎就已經足夠。對於 View 的工作而言,永遠就是呼叫 Fluky.dispatch('action.*') 和監聽 Fluky.on('store.*') 。<br />
<br />
這樣設計有什麼好處呢?因為所有的訊息和命令傳遞,都有統一的事件機制和命名規則,理論上來說,事件可以很容易被提到前端或是放在遠端被處理,這就有點像 X11 的設計,可遠端也可本地端進行圖形繪製處理。不過對 Fluky 來說,這目前還算太遠了,目前 Fluky 還沒有真正處理太過複雜的狀況,純粹就是以完全的事件化來處理資料流。<br />
<br />
也因為一切都事件化,就可以良好支援 Isomorphic 的設計,例如很多的 Action、Store 工作,可以有個前後端統一的命名和呼叫方法,在 Server 預處理,便於 Server Rendering 的使用,甚至是一部份在前端做,一部份在後端做都有可能。最重要的是,在 Isomorphic 上會碰到的前後端 state 不一致的狀況,也可以很容易使用事件、或是在事件分配中的空擋,進行 state 同步而獲得解決。<br />
<br />
此外,為了嘗試新技術,Fluky 也在前端引入了 Generator ,所以如果你想要使用 Fluky,要確保前端瀏覽器能使用 ECMAScript 6+ 最新的標準,或是你必須安裝 babel 模組來打包並轉換你的程式碼為 ES5。<br />
<br />
講了這麼多,怎麼使用 Fluky 呢?下面將以實作一個簡單的 TODO 清單為例。<br />
<br />
<h2>建立 Action</h2><div><pre>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);
});</pre></div><br />
<h2>建立 Store</h2><div><pre>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;
}
}
});</pre></div><br />
<h2>在 React 元件內的使用</h2><div><pre>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(<div onclick="{this.toggle.bind(this," todo="">{todo.text}</div>);
});
return (
<div> {todoList}
</div> );
}
}</pre></div><br />
<h2>State 資料暫存區的設計</h2>傳統的 Flux 做法,不外乎是載入所要的 Store 檔案,來取得 Store 資料,這樣做不但麻煩且囉唆。既然事件分配器(Event Dispatcher)是 Singleton(只存在一個實例,所有人共用),將 Store 的資料共同管理顯然是比較簡單的做法,然後只需要統一使用 Fluky.getState() 就可以取得所需要的 Store 資料。 <br />
<br />
如果從前述範例來看,Fluky.getState() 可以帶兩個參數,第一個是 State 的名稱,第二個是當 State 不存在時,其預設值。<br />
<br />
當然,這個暫存區是可以整個取出來的,也可以使用 Fluky.setInitialState() 或是藉由 window.Fluky.state 在第一時間載入時整個放回去,這可以應用在解決 Isomorphic App 的前後端 Store 不同步的問題。<br />
<br />
<h2>後記</h2><br />
新專案「<a href="https://github.com/cfsghost/lantern">Lantern 燈籠</a>」目標就是嘗試開發一個 Isomorphic 的網站服務,並使用最新的技術,此外,也希望開發一些基本功能(如:會員系統、第三方登入、權限管理等),方便日後開發新網站服務時,可以避免早期的苦工和踩地雷。這是一個 Open Source 專案,如果你有興趣,可以一同開發。:-)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-21377463011046264272015-08-28T12:15:00.000+08:002015-08-28T12:20:59.851+08:00Node.js 的單執行緒(Single Thread)設計,到底有什麼優點?<div class="separator" style="clear: both; text-align: center;">
<a href="https://images.unsplash.com/uploads/1412198485051133af17f/5049dacb?q=80&fm=jpg&s=b8d7cc2e2818b3fde9aad29dfff63f02" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="425" src="https://images.unsplash.com/uploads/1412198485051133af17f/5049dacb?q=80&fm=jpg&s=b8d7cc2e2818b3fde9aad29dfff63f02" width="640" /></a></div>
<br />
這是個總有人一問再問的問題,到底 Node.js 這樣單執行緒(Single Thread)的設計,到底有什麼優點?為什麼總是有人說,它比傳統多執行緒的設計來得有效率?以往,一旦開始討論這個問題,總是會有人開始提到 Context Switch、Asynchronous 等機制,越講越玄也越講越複雜化。<br />
<br />
其實我們可以用簡單的餐廳比喻,就能理解 Single Thread 加上事件驅動(Event-driven)的機制,如何與傳統設計不一樣。<br />
<h2>
場景想像</h2>
試想一個場景:一間餐廳有 100 個座位,然後來了100個客人。<br />
<h2>
處理方法</h2>
身為老闆的你,你會選擇哪種方式服務這些客人:<br />
<br />
<ol>
<li>請100個服務生一對一服務這些客人。</li>
<li>請 25 個服務生,看當下狀況服務這些客人。</li>
</ol>
一般來說,傳統的多 Multi-thread 的設計就類似方法 1,而 Single-thread 且 Event-driven 的設計就是方法 2。 <br />
<br />
通常大多數情況我們會選擇方法 2,因為客人通常都是處於等待(看菜單、等上菜、吃自己)的情況,並不需要服務生貼身服務。所以即便請 100 個服務生,這些服務生大多數時間也只是等在那也佔地方,而服務生眾多也導致服務生之間的溝通和互動其實不易,更不容易交換資源,回報和協同工作難以進行。反而安排一個小型的外場班,讓裡面的人合作見機行事,會比派出 100 個各自獨立的人來得好。<br />
<h2>
併發數高的原因</h2>
併發數(concurrent request)指的是單位時間內可以處理的要求量,一般用來評估一個網路應用程式的效能。而通常在網路服務裡,併發數也相當於單一時間內能服務的連線數量。<br />
<br />
所以,以前面餐廳外場班的模型來說,如果你有 100 個服務生,就可以服務 400 個客人。換句話說,同樣的資源,能處理的併發數(concurrent requests)也就比較高。<br />
<br />
<h2>
後記</h2>
不過如果你開的是酒店或按摩店,那可能就要請一百位服務生了。:-)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-27288677367421647972015-07-13T12:12:00.002+08:002015-07-13T12:12:54.111+08:00Geek?技客?是什麼?我不宅,我用動手代替說話!<div class="separator" style="clear: both; text-align: center;">
<a href="https://download.unsplash.com/reserve/9Fx2auQ4yoHM8OpWA8qw__MG_5265.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="426" src="https://download.unsplash.com/reserve/9Fx2auQ4yoHM8OpWA8qw__MG_5265.jpg" width="640" /></a></div>
<br />
因為我對外都自稱一個 Geek,所以時常有人問我,Geek 是什麼?一直以來,「技客(Geek)」個名詞讓許多人感到陌生,甚至對這詞彙一知半解的人,都以為 Geek 與普通宅宅無異,更甚至是覺得這只是另一種宅宅的說法。的確,Geek 某些地方與普通宅宅很相近,指的是醉心於特定專業或領域的人,但這些人擁有絕佳才能,只是為了鑽研知識或研究新事物可以幾乎荒廢其他事,廢寢忘食對他們來說只是小意思,甚至可以犧牲人際關係等一般正常社會交際行為。由此可以看出 Geek 對於自有興趣的事物,將會多努力去鑽研。<br />
<br />
不可否認,「技客(Geek)」這個詞在早期帶有貶意,但在現今社會中,人們交流的手段和方法已經透過科技有很大的改變,Geek 可以透過網路或各種新方法,找到能分享交流的同好朋友。透過網路串連,他們不再只是群體中被傳統主流排除在外的人,反而搖身一變,變成強力促使世界進步的主要群體。如今,大家眼中的 Geek ,代表著才能與努力,更代表能「動手完成任務」的傑出人們。開玩笑的說,專心致志(宅宅)加上真正動手實做的能力,就是「技客(Geek)」。<br />
<br />
想要成為一個 Geek 嗎?只要你符合這樣的條件,無論你原本是個技術 Hacker、Maker 還是個設計師,更甚至是任何領域的人,都可以稱自己是一個 Geek,甚至可以以此為榮!也許,你也有自己沒發現,但是能堪稱 Geek 的一面也說不定!<br />
<br />
<h2>
後記</h2>
為了讓更多 Geek 能聚集在一起交流,共同迸出創新的火花,我們 <a href="https://hackathon.tw/">Hackathon Taiwan</a> 開始了一個新計畫 GeekBar I/O,將嘗試透過各種活動和方式,讓 Geek 們共同發光發熱!歡迎大家共襄盛舉!<br />
<br />Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-83476869036794896082015-07-08T20:28:00.001+08:002015-07-08T20:31:11.450+08:00快樂玩 ES6 Generator,從 co 起手式開始<div class="separator" style="clear: both; text-align: center;">
<a href="https://c2.staticflickr.com/8/7306/16407404782_8b9c57eab3.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="400" src="https://c2.staticflickr.com/8/7306/16407404782_8b9c57eab3.jpg" width="400" /></a></div>
<br />
自從 Node.js 0.12 版和 io.js 之後,大量的開發者開始了各自的 ECMAScript 6 大冒險,許多人對 Generator 的使用仍跌跌撞撞,對於這種看似「同步(Synchronous)」的「異步(Asynchronous)」機制,有許多人腦袋遲遲無法轉過來。雖然在小弟的書(參閱連結:<a href="http://fred-zone.blogspot.tw/2015/04/nodejs.html">新書報到!Node.js 模組參考手冊!</a>)已經有清楚的說明 Generator 使用方法,但就許多讀者回函來看,對於 JavaScript 越是熟悉的人,越無法直觀理解 Generator 的思維,甚至是老是抓不准使用的時機點。<br />
<br />
尤其是過去我們已經有 Promise、async、Q 和 bluebird 等處理非同步程式流程的模組和工具,很多人就是覺得沒有使用 Generator 的必要。不過,如果你會使用 co 模組,你會突然發現若是將過去的流程機制與 Generator 相搭配,程式開發將變得更為流暢。<br />
<br />
若想要安裝 co 模組,可以直接以 NPM 下載:<br />
<div>
<pre>npm install co</pre>
</div>
<br />
本文接下來會說明一些 co 的基本使用,破除一些 Generator 難以使用的地方,讓開發者們更容易開始 Generator 的旅程。<br />
<h2>
讓原生 Generator 更好用</h2>
這是很多人不喜歡使用 Generator 的主因,以往為了使用 Generator,我們還要先建立一個 Generator 的函數,然後不時的去處理 Generator 所返回的資訊,一遍又一遍進入 Generator 之中。因此,無論 Generator 再好用,這些麻煩的動作也完全抵銷他的優勢,大多數人還不如回到舊的流程控制方法,以免徒增自己的麻煩。<br />
<br />
而如果使用 co,可以直接將 Generator 函數當作「立即函數使用」,其餘的部份我們可以不需要擔心:<br />
<div>
<pre>var co = require('co');
co(function *() {
console.log('Inside');
}) </pre>
</div>
<h2>
再也不用煩惱 yield!</h2>
以前光是 Promise,就已經讓很多人詬病,覺得每次使用 Promise 都要花許多時間對函數進行包裝,而 Generator 也有類似的問題,若是要使用 yield,更是一件大工程。於是,co 幫開發者做了些設計,讓 Generator 可以直接利用 yield 去跑 Promise 類型的函數、陣列等物件,因為是 yield,其執行模式是「異步」,不會阻塞事件引擎。<br />
<br />
co 支援 Trunks,也就是回傳一個簡單函數進行異步工作,範例如下:<br />
<div>
<pre>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);
});</pre>
</div>
<br />
<h2>
支援 Promise 的 yield</h2>
如果你熟悉 Promise,這是個好消息, co 讓 yield 可以直接吃 Promise:<br />
<div>
<pre>co(function *() {
var ret = yield Promise.resolve(true);
});</pre>
</div>
<h2>
直接使用 yield 跑一整個陣列的工作</h2>
前面提到 co 讓 yield 也支援陣列,所以我們可以準備一系列的工作,放在陣列中讓 yield 去處理:<br />
<div>
<pre>co(function *() {
var a = Promise.resolve(true);
var b = Promise.resolve(true);
var c = Promise.resolve(true);
var ret = yield [ a, b, c ];
});</pre>
</div>
<h2>
異步處理一個陣列裡的所有元素</h2>
這大概是以往 JavaScript 最頭痛的工作,後來有了 async 這類模組後,才比較容易處理,不然光使用 forEach 或是 for 迴圈來做,碰到較大的陣列,往往使程式阻塞卡死。如果使用 co 前面的起手式,我們可以試著這樣做(如果不使用 Promise):<br />
<div>
<pre>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);
}
});</pre>
</div>
<h2>
後記</h2>
經過 co 包裝後的 Generator 非常好用,無論你過去習慣用哪一種流程控制的方式,都可以很好的轉換並整合過來。試試看吧!:-)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-25975790113068799312015-05-24T23:59:00.001+08:002015-05-25T01:27:58.634+08:00黑客松式的學習活動:NodeSchool International Day 精彩紀錄<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgS3QaSpqjKSIJfPsoM7HUee2ljyLfYmR620DD90SzEngUIDNfQ27twrK_7fBCR6nEhcxxoKgDnp8BeVQlr7Jp3GLtjWsPPTah7LI0KcRNTKY47NtNybT-DwxRjos-bQoK0q8xB_iH4ps/s1600/DSC_0006.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="360" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgS3QaSpqjKSIJfPsoM7HUee2ljyLfYmR620DD90SzEngUIDNfQ27twrK_7fBCR6nEhcxxoKgDnp8BeVQlr7Jp3GLtjWsPPTah7LI0KcRNTKY47NtNybT-DwxRjos-bQoK0q8xB_iH4ps/s640/DSC_0006.JPG" width="640" /></a></div>
『是不是要很厲害才能參加黑客松呢?』這是一個永遠都會有人問的問題。<br />
<br />
事實上,黑客松從一開始的出發點,就是三五好友聚在一起進行研究及開發的活動,帶有很純粹的動機。參加這樣的活動,你不必真的很厲害到極點,而最重要的是動手和夥伴合作,過程中可以順便認識朋友與他人交流,讓投入的過程中更加豐富有趣。<br />
<br />
從這樣的角度來看,『黑客松式的學習』是不是有可能的呢?如今看來是肯定的,5/23 於台北剛結束的 NodeSchool 聚會就是一場黑客松式學習活動,吸引了近兩百位參加者,共同參與了這場具有黑客精神的學習活動!<br />
<br />
順帶一提,這此使用的場地,也是 <a href="http://hackathon.tw/">黑客松台灣(Hackathon Taiwan)</a> 每個月辦活動的主場地,無論如何,也記得來報名下次 6/13 - 6/14 (六、日)的黑客松活動!<br />
<h2>
什麼是 NodeSchool?</h2>
NodeSchool 是一個線上學校,目標是運用線上工作坊課程,讓所有人都可以在這學到各種技術。而恰逢 NodeSchool International Day 國際日活動,全球各地都紛紛各自舉行一場 NodeSchool 盛會,因此身在台灣的我們也理所當然響應了這樣的活動。 <br />
<br />
這個活動的形式相當特別,沒有講師在台上講課,而是採用線上工作坊教材與題目的形式,讓參加者在自己的電腦上挑戰通關。NodeScool 擁有許多工作坊教材,從基礎課程到進階的選修課程都有,除了 JavaScript、Node.js 之外,也有 ES6、WebGL、Three.js、Functional Programing 和 React.js 等課程。詳細的課程資訊,可以參考 <a href="http://nodeschool.io/">NodeSchool 官方網站</a>。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://github.com/sethvincent/javascripting/raw/master/screenshot.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="272" src="https://github.com/sethvincent/javascripting/raw/master/screenshot.png" width="400" /></a></div>
<br />
由於每一份教材都是經過設計,而且每一個主題和關卡都擁有提示和說明,參加者在一題題解答的過程中,可以靠自己慢慢學會一門技術。如果真的遭遇到了自己難以突破的困難,可以跟左右鄰居討論研究,若真的也沒有辦法了,可以找在現場自願的指導員(Mentor)幫忙排解問題。<br />
<h2>
黑客松式的學習活動 </h2>
回想起過去從小到大,我們的學習模式,總是有個老師在台上教學,枯燥而乏味,而為了保證教學進度,不免伴隨著填鴨式的教學方法。這樣的模式我們早就習慣,雖不見得喜歡,但也無可奈何,逐漸的,我們的思考和學習,慢慢僵化了。<br />
<br />
具有一定寫程式經驗的人都知道,如果想要把程式技術練好,動手研究是不可缺少的工作,而持續鑽研的熱情更是不可以缺少,學習並習慣自己動手解決一個個問題更是必要的訓練。此外,比起學習到一門技術,我們如何在學習和訓練的過程,培養自己自我學習和與其他人間的溝通交流,更為重要。與他人交換想法,或是藉由分享知識將自己所學內化,更是一種提昇自己的方式。<br />
<br />
因此,NodeSchool 活動與過去的學習方法最大不同,就是提供一個讓大家共同參與的形式,目標是要大家能在此透過自己動手學習到技術,也參與交流討論,當大家在活動結束後走出會場時,能帶著貨真價值的學習成果回家。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibmZSrfh9AlFJc6wxEIsDEp5Rz-b5V_Lky7dph79t8v2vk0AIOq3h_66TkejrmOWNKHuUqaMOeyrBLVvMc6AktCDDGDoFbwkmyHumBsba2WJhIY4VcFQEaVbabLBgKGSJ97r7t8G7CRCQ/s1600/DSC_0001.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="360" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibmZSrfh9AlFJc6wxEIsDEp5Rz-b5V_Lky7dph79t8v2vk0AIOq3h_66TkejrmOWNKHuUqaMOeyrBLVvMc6AktCDDGDoFbwkmyHumBsba2WJhIY4VcFQEaVbabLBgKGSJ97r7t8G7CRCQ/s640/DSC_0001.JPG" width="640" /></a></div>
<h2>
沒有大神,只有迫切突破自我的歡樂參加者</h2>
別以為這樣標榜學習的活動只有初學者前來,其實會眾裡臥虎藏龍,不乏有大神級別的高手參與。只不過,在這樣的活動中,即便是高手,也正煩惱自己手上的問題和課程,也需要更多高手一同討論和解決問題。<br />
<br />
由於 NodeScool 工作坊的主題相當廣泛,無論是初學者還是專家,都可以前來,然後挑戰一些自己感興趣的技術,然後號招在現場的高手們,一起討論並學習。我們可以說,NodeSchool 的活動,是一個兼具動手、討論交流和突破自我的學習活動。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjxWJ8gJE5_aVla_GS2rvvszHgV7PhPMvlI0XqJ3izVZpVX-ut2RJBSD_pit0GD9bLTKZ0iplyv3Jz_k7s8UyaIGzdoq-9taOlpqfOK984Wa6C6s_3S7wa74Bnt7iGson7c-4SCvE8cZs/s1600/DSC_0012.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="360" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjxWJ8gJE5_aVla_GS2rvvszHgV7PhPMvlI0XqJ3izVZpVX-ut2RJBSD_pit0GD9bLTKZ0iplyv3Jz_k7s8UyaIGzdoq-9taOlpqfOK984Wa6C6s_3S7wa74Bnt7iGson7c-4SCvE8cZs/s640/DSC_0012.JPG" width="640" /></a></div>
<h2>
後記</h2>
如果有人完成了課程,日後可以在參加 NodeSchool 時,自願擔任該課程的指導員(Mentor),如果還沒完成的人,可以回家繼續完成,若碰到問題,可以至 <a href="https://github.com/nodeschool/taiwan/issues">Github Issues</a> 直接發問,或到每週四的 Node.js Party 及讀書會討論。日後,社群應該也會不定期的舉辦中小型的 NodeSchool 活動,讓大家共同參與。<br />
<br />
另外有趣的是,這次活動也有碰到一些企業派人來參與或獵才,像我就碰到 Foxconn 等公司的主管級人物前來參加活動,也一起下去研究程式。誰說主管只會動一張嘴呢?至少會來黑客松的主管也會動手!:-DFred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0tag:blogger.com,1999:blog-4389461234607418203.post-91957355190405805122015-05-21T23:57:00.003+08:002015-05-21T23:57:36.667+08:00簡報釋出!超酷炫科幻 UI:QML 入門<div class="separator" style="clear: both; text-align: center;">
<a href="https://cloud.githubusercontent.com/assets/252072/7604018/44697ee8-f96f-11e4-9690-db826fccbc22.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="478" src="https://cloud.githubusercontent.com/assets/252072/7604018/44697ee8-f96f-11e4-9690-db826fccbc22.png" width="640" /></a></div>
<br />
這次五月的 <a href="https://hackathon.tw/">Hackathon Taiwan</a>, 因為原本的講師沒空,所以我就來救援了一場 QML 工作坊,這堂課以活動的倒數計時器為開頭,討論怎麼使用 QML 進行超酷炫科幻 UI 的設計,是一堂初學入門課程。<br />
<br />
有鑑於過去聽到不少人反應,要使用 QML 必須與 C/C++ 打交道,門檻相當高,於是,這個工作坊的目標,就是讓學員可以在純 QML 的環境下,以 QML 來開發自己的 UI 界面。即便是不熟悉程式語言的人,都可以輕易使用 QML 做出絢麗的 UI。<br />
<h2>
活動簡報</h2>
<br />
<div style="text-align: center;">
<iframe allowfullscreen="" frameborder="0" height="355" marginheight="0" marginwidth="0" scrolling="no" src="//www.slideshare.net/slideshow/embed_code/key/MttB7jBhJqxktH" style="border-width: 1px; border: 1px solid #CCC; margin-bottom: 5px; max-width: 100%;" width="425"></iframe></div>
<h2>
後記</h2>
這次 QML 工作坊事後反應還算不錯,甚至有人以 QML 來開發黑客松活動的作品,未來如果有機會可以再次開課。:-)Fred Chienhttp://www.blogger.com/profile/16383759688690536670noreply@blogger.com0