[expressjs] request proxy

api proxy

最近使用 expressjs 要將 api proxy 到另外一個 domain 的 api server 上。查了一下最簡單的做法是用 request 來處理。

var API_HOST = 'xxx.com';
app.use('/proxy', function(req, res) {
  var request_params, url;
  url = 'http://' + API_HOST + req.url;
  request_params = {
    followAllRedirects: true,  //redirect if 301~400
    body: JSON.stringify(req.body) //POST/PUT/DELETE data 
  };
  return req.pipe(request(url, request_params)).pipe(res);
});

看起來很簡單不過為了要解決 api server 會做 301/302 redirect 的動作,追了sourcecode才知道有這個 followAllRedirects 的設定,特此記錄一下。

reference:

[PhantomJS] Function.prototype.bind

PhantomJS 1.9.7 (Mac OS X) ERROR

使用 reactjs 來寫 test case 時,用 PhantomJS 執行時就遇到這個 error,奇怪的是用其他的 browser 來執行就沒有問題。

TypeError: 'undefined' is not a function (evaluating 'RegExp.prototype.test.bind(
      /^(data|aria)-[a-z_][a-z\d_.\-]*$/
    )')

查了一下追到這個 issue Function.prototype.bind is undefined,看起來是 PhantomJS 的內核沒有支援 Function.prototype.bind。於是再找到 MDN 的 Function.prototype.bind,在載入 reactJS 之前先將這個 polyfill 加進去就可以解決了。

karma.conf.js
files: [
    'test/polyfill/*.js',
    'app/bower_components/react/react-with-addons.js',
    '.tmp/test/spec/*.js'
]
phantom_bind_polyfill.js
if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

[gulp] No need to use karma-browserify

天大的 bug

前幾天才寫了一篇 browserify and karma test 的環境設定,結果今天就踩到天大的 bug:Requiring files that require other files will cause failure.,問題是發生在你的 require dependency 太深的話就會發生 error。我自己遇到是在四層以上就會遇到一模一樣的 error message。

底下有人(gah-boh)留言:"I ended up not using karma-browserify. I tried karma-browserifast and ran into another issue with it. ",然後也提供了他的解法。

var gulp = require('gulp');
var source = require('vinyl-source-stream');
var browserify = require('browserify');
var karma = require('gulp-karma');
var reactify = require('reactify');
var glob = require('glob');

gulp.task('browserify-test', function() {
    var testFiles = glob.sync('./assets/tests/**/*.js');
    var bundleStream = browserify(testFiles).transform(reactify).bundle({debug: true});
    return bundleStream
        .pipe(source('bundle-tests.js'))
        .pipe(gulp.dest('assets'));
});

browserify your test case

我才發現根本就不需要用到 karma-browserify 來處理 (而且也不需要 gulp-karma(https://github.com/karma-runner/gulp-karma)),直接用 browserify 把所有的 test script 產生出來就好了。

以下是我的 task 設定

gulp.task "browserify:test", (callback)->
  bundleApp = map( (contents, filename)->
    fname = get_name(filename)
    browserify(
      entries: [filename]
      extensions: ['.coffee']
    )
    .bundle({debug: true})
    .on('error', $.util.log )
    .pipe(source("#{fname}.js"))
    .pipe gulp.dest(compiledPath + "/test/spec")
  )

  gulp.src('test/spec/**/*.coffee')
  .pipe(bundleApp)

然後 test task 照舊但是要在 browserify:test 做完之後在執行即可

gulp.task "test", ['browserify:test'], (callback)->
  appRoot = "#{__dirname}/../../"
  karmaCommand = appRoot + './node_modules/karma/bin/karma'
  karmaConfig = appRoot + 'test/karma.conf.js'
  exec("#{karmaCommand} start #{karmaConfig}", (err, stdout, stderr)->
    $.util.log stdout
    $.util.log stderr
    callback(err)
  )

結論

我一開始把 browerify 和 karma 的整合想得太複雜了,其實只要把 test case 都經過 browserify compile 之後就可以 run karma 了。我又繞了一大圈,真想殺了我自己。

browserify and karma test

[2014/07/10 update] No need to use karma-browserify

browserify

browserify 真的是個很棒的東西,可以用 require 輕易的把程式模組化,由於可以很輕易的拆成模組,就可以針對模組來寫 test case。之前在玩 AngularJS 時注意到 karma,可以真的run browser engine 來跑 unit test 甚至是 end to end test。原本我以為很簡單可以將 karma test 整進專案,但因為 project 用到 coffee,網路上找到的方式找不太到目前用到的需求(gulp + browserify + coffee + karma test)。所以研究了一下,給有需要的人做參考。

package.json

會用到的 library,比較特別的是 karma-browserify,這個是 karma 的 plugin,用在 karma 的 preprocessors,將 test 的程式做 browserify。

+    "karma": "^0.12.16",
+    "karma-browserify": "^0.2.1",
+    "karma-jasmine": "^0.1.5",
+    "karma-phantomjs-launcher": "^0.1.4",

gulp test task

task 沒什麼特別的,其實只是執行 karma start karma.conf.js,只是把執行的 script 用 gulp task 包起來而已。

gulp = require("gulp")
exec = exec = require('child_process').exec

$ = require("gulp-load-plugins")()

gulp.task "test", (callback)->
  karmaCommand = './node_modules/karma/bin/karma'
  karmaConfig = 'karma.conf.js'
  exec("#{karmaCommand} start #{karmaConfig}", (err, stdout, stderr)->
    $.util.log stdout
    $.util.log stderr
    callback(err)
  )

karma.conf

karma 的設定主要是參考 karma-browserify,也可以直接參考我的設定 karma.conf.js

unit-testing

都設定好之後就可以開始寫測試了,最棒的部分就是可以單獨測試某個 module。例如:

auth = require("../app/scripts/service/auth")
describe "service/auth", ->
  it "should be login", ->
    expect(auth.login()).toEqual('login')
  it "should be logout", ->
    expect(auth.logout()).toEqual('logout')

如此就可以把跟 UI 沒關係的 module 抓出來單獨做 unit-testing,而環境設定好之後我想 end to end testing 應該對 karma 來說都是可以相對簡單的事情了,然後要跑什麼 test report 應該就都可以了,喔耶!

也可以直接參考我的 repository:
https://github.com/blackbing/htccs-webapp/tree/develop

[node-webkit] video mp4 format support

node-webkit 不支援 mp4/mp3 format

最近在玩 node-webkit 遇到一個問題,就是 video 不支援 mp4,原本在瀏覽器開發的好好的,丟到 node-webkit 執行就會死掉。追了一下原來有這個 issue,而且還寫了 wiki Support mp3 and h264 in video and audio tag。大意是說因為 License 的關係,node-webkit 並不會把這些有專利的影音格式支援 build 進去,若您的應用程式需要支援這些有專利的影音格式,可以用以下的解決方式,但風險自負。

詳細看了一下 wiki,最簡單的方式是將 libffmpegsumo.so 取代至 build 出來的專案中。但一定要跟 node-webkit 的 chromium 版本相同,若你自己的 chrome 是最新版的並不保證支援。

於是我在 build 完再把 libffmpegsumo 複製過去取代,重新開啟之後發現影片出現了,但是沒有聲音,WTF。
而在 windows 上的症頭是程式無回應。WTF。
不過這也是 wiki 上寫的並不保證支援最新版本的 chrome。

WebM format

突然想到,這麼麻煩解 mp4 的問題幹嘛,就轉成支援的格式就好了,於是我突然想到可以用 WebM的格式,而且大部分瀏覽器都已經 support 了。所以我就先把 mp4 轉成 webm,沒想到最大的 benefit 就是檔案大小,從原本的 13M 降低成 2.8M,真是嚇死我了,因為當初想要用 node-webkit 包起來有個主要原因就是影片的檔案大小問題,想說可以跑在本機執行的話會比較順暢。不過 anyway,加上一個 source 之後就可以在 node-webkit 上順利執行了,也不用在 build process 中加入奇怪的取代 lib 的動作,非常的好。

<video>
    <source src="video.webm" type="video/webm">
    <source src="video.mp4" type="video/mp4">
</video>

小結

在想解決問題的方法時,不一定要執著在問題的本身,有時候回頭去思考問題或者轉個方向,搞不好就很快的迎刃而解了。想當初我為了要解決播放 mp4 的問題,花了很多時間搜尋 node-webkit mp4 的問題(目光含淚),但其實以我的需求根本不需要直接去解決這個問題。以上。

[javascript] 利用 Deferred 處理順序性任務

Javascript 的世界裡頭充滿了 callback,從其他語言進入 Javascript 的世界常會頭昏,甚至會有點排斥這樣的特性。本篇文章介紹 Deferred 物件的基礎用法,避免程式在做順序任務的處理時難以維護,在模組化的程式設計中,是很重要也很常用到的技巧。

Callback function

假設我們有一個 query 的 function,也許是 ajax,也許是 sql query,總之它接受命令做完回傳 callback

var query = function(task, callback) {
    //do something
  return callback(task + ' finished');
};

因此我們可以很簡單的呼叫 query,例如

query('task1', function(msg) {
  console.log(msg); //task1 finished 
}

但假設我們要依序執行 query 的話,例如 task1 -> task2 -> task3,很自然地就會寫成這樣:

// nested way
query('task1', function(msg) {
  console.log(msg); //task1 finished 
  return query('task2', function(msg) {
    console.log(msg); //task2 finished 
    return query('task3', function(msg) {
      return console.log(msg); //task3 finished 
    });
  });
});

function call function

這樣的問題就是巢狀的 function 裡頭要做的事情越來越複雜,就會將 function 寫成一大包,到最後很難維護。所以很直覺的就將要做的事情拆成 function來處理,例如:

//funciton call function
var task1 = function() {
  query('task1', function(msg) {
    console.log('function ' + msg); //function task1 finished 
    return task2();
  });
};
var task2 = function() {
  query('task2', function(msg) {
    console.log('function ' + msg); //function task2 finished 
    return task3();
  });
};
var task3 = function() {
  query('task3', function(msg) {
    return console.log('function ' + msg);  //function task3 finished 
  });
};
task1();

把 function 拆開看起來就不會有巢狀越寫越深的問題了,但是會遇到問題是:

  1. 若你的 function 散落各處,就會很難 trace 程式碼。
  2. function 與 function 之間的依賴性。越複雜的依賴性會讓程式日後難以維護。

那麼如何利用 Deferred 物件?

首先我們用 Deferred 方式把 query 包起來

var query_dfr = function(task) {
  var _dfr;
  _dfr = $.Deferred();
  query(task, function(msg) {
    return _dfr.resolve(msg);
  });
  return _dfr.promise();
};

接著我們就可以這樣呼叫

query_dfr('task1').done(function(msg){
    console.log('Deferred ' + msg); //Deferred task1 finished 
})

看起來沒什麼了不起,那接著來寫順序的任務吧:

query_dfr('task1').done(function(msg) {
  console.log('Deferred ' + msg);
  query_dfr('task2').done(function(msg) {
    console.log('Deferred ' + msg);
    query_dfr('task3').done(function(msg) {
      console.log('Deferred ' + msg);
    });
  });
})

XDXDXD,翻白眼翻到外太空,當然不是這樣的,這樣還是在寫巢狀的程式。

Deferred and pipe

我們需要用pipe來串聯我們的任務:

query_dfr('task1').pipe(function(msg) {
  console.log('Deferred ' + msg);
  return query_dfr('task2');
}).pipe(function(msg) {
  console.log('Deferred ' + msg);
  return query_dfr('task3');
}).pipe(function(msg) {
  return console.log('Deferred ' + msg);
});

這樣看起來就精簡多了,而且也很容易看懂 task 之間的順序關係。

Library

在瀏覽器環境中,可以直接使用 jQuery Deferred,若你不需要 jQuery,可以用 stand alone Deferred

另外在 node 環境中,可以使用 node-deferred

Reference

本範例程式碼:https://gist.github.com/blackbing/a9dad5c141f51b65b650

不只 pipe,Deferred 還有很多其他的用法,有興趣深入了解的話可以參考:

[ReactJS] Dynamic Selection

ReactJS

ReactJS 是 Facebook 提出的 interface library,專注在 'view' 的更新,避免直接操作 DOM 物件。第一次看到 ReactJS 時,其實我看到 React.DOM.div 我就暈了,以前用document.create('div') ,然後appendChild等等。發現這樣操作 DOM 物件效能很差,後來改用 innerHTML 來更新 DOM,現在又說要用這種方式來寫(眼神死)。

Z>B?

但用了之後才發現驚為天人。以往在做較複雜的 UI 時,會針對不同狀態重新 render 整個畫面,而且 rerender 最簡單,不用去管現在到底是怎樣的狀態。但若資料量龐大的時候,rerender 的成本就非常高,因此在做效能優化時,必須針對「改變」的元素做操作。ReactJS 提出了一個 diff algorithm,將 data 與 DOM element mapping 之後去檢查 data 的改變才更新 DOM element,讓開發者可以專注在 data 的變化,而不需要直接操作 DOM element,也可以將邏輯與 UI 呈現分開。總之是 Z>B。

快速上手

其實 ReactJS 相當簡單,因為這個 library 只專注在 view 的更新,官方文件也蠻清楚的:http://facebook.github.io/react/docs/getting-started.html

階層選單

寫了一個小範例用 ReactJS 實作階層選單,來感覺一下有什麼差別。

49471a6e7bb694044b250100016aad8d.gif
http://jsbin.com/fuziz/19/edit

onChange event

在 select element 的 change evnet 處理 2nd 選單的更新,最大的差別在於只要把資料丟進去就會根據 data 的改變做 refresh。

$component.find('#company').on('change', (event)->
  $target = $(event.target)
  id = $target.find('option:selected').attr('value')
  product = {}
  
  if products[id]
    product = products[id]
  Component.update('product', 'dropdown', product)
)

小結

data binding 的機制其實跟 angularJS 很像,但 ReactJS 只針對 data 與 view 的更新,很適合在專案中抽離 template engine 來提升效能,看起來雖然不會少寫多少程式,但至少不用再針對 DOM element 直接操作,也降低 bug 的產生。現在就很不喜歡寫 $('element').find('a').css('color', '#CCC').trigger('blabla')這樣的程式。

[javascript] 跨瀏覽器的屬性判斷

今天在抓蟲的時候發現一個很容易出錯的地方。當我們在宣告變數時,常常會因為不同的 browser 有不同的 API 定義,然後用以下的寫法來判斷宣告哪一個屬性,例如:

var fullscreenElement = document.mozFullScreenElement || document.webkitFullscreenElement || document.fullscreenElement;

||來檢查要用哪一個 attribute。

不過要小心 javascript 的值當成條件時的判斷。

例如:

var sLeft = window.screenLeft || window.screenX; //firefox use screenX
console.log(sLeft); 

這段程式希望 screenLeft 會回傳 window.screenLeft ,而在 firefox 會回傳 window.screenX。reference: Window screenLeft and screenTop Properties

但是如果 screenLeft 剛好等於 0 的時候,就會進入||之後的條件了,接著就 gg 了。

因此建議在值的判斷還是正統一點用 hasOwnPropertytypeof 來判斷會比較精準。

var sLeft = window.screenLeft;
if( !window.hasOwnProperty('screenLeft')) sLeft = window.screenX; 

Jenkins Notes

Jenkins

最近把 jenkins 架起來,筆記一下遇到的問題。

Install and Upgrade

安裝方式請參考http://pkg.jenkins-ci.org/debian/,可能我在多年前有安裝過,因此安裝時出現這樣的error:

Preparing to replace jenkins 1.480.3+dfsg-1~exp2 (using .../archives/jenkins_1.535_all.deb) ...
Unpacking replacement jenkins ...
dpkg: error processing /var/cache/apt/archives/jenkins_1.535_all.deb (--unpack):
 trying to overwrite '/usr/share/jenkins/jenkins.war', which is also in package jenkins-common 1.480.3+dfsg-1~exp2
dpkg-deb: error: subprocess paste was killed by signal (Broken pipe)
jenkins stop/waiting

要移除掉重新安裝:

sudo apt-get purge jenkins
sudo apt-get update jenkins

Reference:
problems installing latest version of jenkins

Nginx + Jenkins

jenkins 預設跑在8080 port,因此直接連到 http://[JENKINS_SERVER]:8080 就可以連到。不過我不太喜歡打 port,所以想要把它藏在 nginx 後面。

1. Nginx config

nginx 設定檔可以參考 Running Jenkins behind Nginx

2. Disable external 8080 port

另外還是要將對外的8080 port鎖起來,請在 /etc/default/jenkins最底下的JENKINS_ARGS之前加入這幾行

JENKINS_LOCAL_ONLY="yes"
if [ "$JENKINS_LOCAL_ONLY" = "yes" ]; then
PARAMS="$PARAMS --httpListenAddress=127.0.0.1"
fi

Reference:
Starting and Accessing Jenkins

3. Enable Jenkins log

若要看 jenkins 的log,還要加上 log 的參數設定

JENKINS_ENABLE_ACCESS_LOG="yes"
if [ "$JENKINS_ENABLE_ACCESS_LOG" = "yes" ]; then
  PARAMS="$PARAMS --accessLoggerClassName=winstone.accesslog.SimpleAccessLogger --simpleAccessLogger.format=combined --simpleAccessLogger.file=/var/log/jenkins/access"
fi

Reference:
Starting and Accessing Jenkins

4. No valid crumb was included in the request

設定完之後基本上就可以用了,不過打開瀏覽器的 console 一直會出現 No valid crumb was included in the request,後來我將 http://[JENKINS_SERVER]/configureSecurity/ 最底下的 Prevent Cross Site Request Forgery exploits 的打勾取消掉,就沒事了。(PS.我也不知道有什麼影響)

設定你的應用程式執行環境變數

因為我對系統不太熟,比較麻煩的是要確認你在執行的環境變數,像是你安裝 rvm 之後 ruby 的執行位置等等,我主要是拿來跑 grunt 的 test 與 build,可以參考這篇的 PATH 設定:jenkins configuration yeoman based angularjs web application

5. set TimeZone

add this on /etc/default/jenkins

JAVA_ARGS="-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Taipei"

Reference:
https://wiki.jenkins-ci.org/display/JENKINS/Change+time+zone

Bitbucket hook Jenkins

最後最重要的就是要把你的專案 hook 到 Jenkins 啦,我測試的 project 放在 Bitbucket,所以就用 Bitbucket 來舉例。

  1. 安裝 jenkins plugin: 到 manage plugin/available 安裝 Build Authorization Token Root Plugin。
  2. 到你的 job 設定裏頭打開 Build Triggers/ Trigger builds remotely,設定 Authentication Token,填入自己設定的一組 Token(例如 build1234567)
  3. 測試 http://jenkins-server/buildByToken/build?job=myjob&token=build1234567 是否有觸發 job 的執行。
  4. 到 bitbucket 你自己的 repository -> setting -> hooks -> select a hook -> Jenkins -> Add hook

參考資料 Jenkins hook management

接下來很重要!!

馬的我追 log 追半天一直 hook 不成功,總之是因為 jenkins 的 hook 格式跟 bitbucket 送出的資料對不起來(我不曉得是哪邊的 bug),bitbucket POST 出去的 data,Jenkins 不吃。

螢幕快照 2014-03-07 下午1.18.46.png

請在 Endpoint 裏頭填入第三點的 url 並且加上一個 &,讓他忽略之後的變數,例如:
http://jenkins-server/buildByToken/build?job=myjob&token=build1234567&

剩下都留空白。就好了。

大功告成

push 之後看到 jenkins 開始聽話的工作了,喝杯咖啡犒賞一下自己。

[VIM] ternjs, YouCompleteMe

同事 othree 分享了 ternjs,之前看到沒有支援 coffee 就放棄了,沒想到強者我同事就順手加了 coffee 的支援,看起來蠻不錯用的,記錄一下安裝方式。

Install tern_for_vim

這裡比較麻煩的是因為 tern 的原作者失聯,為了要支援 coffee script ,因此他自己 clone 了一個版本出來 support。

  1. install https://github.com/othree/tern_for_vim with bundle
  2. cd ~/.vim/bundle/tern_for_vim/
  3. cd tern
  4. npm install

Install YouCompleteMe

完整的安裝說明請參考https://github.com/Valloric/YouCompleteMe#mac-os-x-super-quick-installation

  1. brew install vim make sure your vim version is up to 4.7
  2. brew install CMake
  3. install https://github.com/Valloric/YouCompleteMe with bundle
  4. cd ~/.vim/bundle/YouCompleteMe
  5. ./install.sh --clang-completer

Add triggers to .vimrc

設定你的觸發鍵.,例如當你打.時就會跳出自動完成視窗

let g:ycm_semantic_triggers =  {
    \\   'coffee': ['.'],
    \\ }

Add .tern-project file to your project

在你的專案加入一個檔案,內容如下。

{
    "libs": [
    ],
    "plugins": {
        "requirejs": {
            "baseURL": "app/assets/javascripts/"
    },
        "coffee": {}
    }
}

plugin 還支援 node, requireJS, Angular, Doc Comment,有空來玩玩看。http://ternjs.net/doc/manual.html#plugins


Done!

接下來就可以在你的專案裡頭的 coffee 使用了,實際使用大概像這樣,可以直接用 require 來讀取底下的 method or attribute。
Screen Shot 2014-02-10 at 下午4.33.34.png

reference:

  1. http://tilvim.com/2013/08/21/js-autocomplete.html
  2. http://ternjs.net/
  3. https://github.com/Valloric/YouCompleteMe
  4. https://blog.othree.net/