pig's diary

何でも忘れるので万年初心者ね

node のexpress で Closure Library

node で Closure Library をやる一例です。

やること

  1. express プロジェクトを作る
  2. 使うnodeモジュールのインストール
  3. Closure Library を落とす
  4. Closure Compiler を落とす
  5. 実験用のapp.jsコードを置く
  6. production モードとそうでないモードを分ける
  7. Cakefileにタスクを書く
  8. 配備してみる

1. まず、express プロジェクトをつくります

$ express my_project
$ cd my_project

2. ./package.json を編集して、こんなふうにします。

/* ./package.json */
{
    "name": "application-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.8"
    , "jade": ">= 0.0.1"
    , "q": ">= 0.8.2"      // new
    , "muffin": ">= 0.2.7" // new
    , "underscore": ""     // new
  }
}
追記:underscoreを追加

nodeモジュールをインストールします。

$ npm install
...
q@0.8.2 ./node_modules/q 
└── event-queue@0.2.0
jade@0.20.3 ./node_modules/jade 
├── mkdirp@0.3.0
└── commander@0.5.2
express@2.5.8 ./node_modules/express 
├── mime@1.2.4
├── mkdirp@0.3.0
├── qs@0.4.2
└── connect@1.8.5
muffin@0.2.7 ./node_modules/muffin 
├── temp@0.2.0
├── coffee-script@1.1.3
├── docco@0.3.0
├── uglify-js@1.1.0
├── q-fs@0.1.17 (q-io@0.0.10 qq@0.3.2 fs-boot@0.0.7)
├── q@0.7.1 (event-queue@0.2.0)
├── glob@3.0.1 (graceful-fs@1.1.5 fast-list@1.0.2 inherits@1.0.0 minimatch@0.1.5)
└── prompt@0.1.12

3. Closure Library を落っことします。

$ svn checkout http://closure-library.googlecode.com/svn/trunk/ ./public/closure-library

publicフォルダの下には、置きたくないんですけどね。でも置きます。public/closure-library/closure/goog/base.js をhttpで読まないといけないので、ここに置いているんです。ハードリンクとか貼る人もいるんでしょうか?

4. Closure Compiler を落っことします。

$ mkdir -p library/closure-compiler
$ wget -P library/closure-compiler/ http://closure-compiler.googlecode.com/files/compiler-latest.tar.gz
$ tar -C library/closure-compiler/ -xf library/closure-compiler/compiler-latest.tar.gz
$ ls library/closure-compiler/
COPYING			compiler-latest.tar.gz
README			compiler.jar

ふーっ。ライブラリとかの準備はできました。

5. 次に、自分で書くクライアントサイドのコードを置く場所をつくります。あと、実験用のコードも置いておきます。

$ mkdir -p resources/client
$ touch resources/client/app.js

./resources/client/app.js には試しにこう書いておきます。

/* ./resources/client/app.js */
goog.provide('my.app');
goog.require('goog.dom');

my.app = function () {
  var elm = goog.dom.createDom('h1', {
    style: 'font-size: 600%'
  }, 'yeah..');
  goog.dom.append(document.body, elm);
};

goog.exportSymbol('my.app', my.app);

./resources/client 配下にファイルを並べて、goog.provide/ goog.require していく感じです。

6. production モードと開発モードの区別ができるようにします。
作戦としては、

  • node を「NODE_ENV=production node app.js」で起動したときは本番用(JSを小さくします)。
  • ただの「node app.js」で起動したときは、開発用(ビルドにできるだけ時間をかけないようにします)。

まず、サーバ側のコードの修正です。
./routes/index.js

/* ./routes/index.js */
exports.index = function(req, res){
  res.render('index', { 
    title: 'Express'
  , IS_PRODUCTION: process.env.NODE_ENV === 'production' // new
  })
};

次に、この値をHTMLテンプレート側で拾って、クライアント側のjsファイルの読み分けをします。

// ./view/layout.jade
!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    
    // new code
    - if (IS_PRODUCTION)
      script(src='/javascripts/app-min.js')
    - else
      script(src='/closure-library/closure/goog/base.js')
      script(src='/javascripts/app.js')

  // modified
  body(onload='my.app()')!= body

OK。だと思います。ついでに、onload でスクリプトを実行するように書いておきました。

7. Cakefile にタスクを書いて、./public/javascript/app.js を作ってもらいます。
以下、muffin を使いたいのでcoffeescriptが必要です。coffeescriptをインストールしておく必要があります(npm install -g coffee-script)。

プロジェクト直下に Cakefile というファイルを作って、こんなファイルにします。
※修正しました。muffinだとoutputするやりかたが分からなかったのでネイティブのプロセス使った。
Cakefile


# Include required libraries.

muffin = require 'muffin'
child  = require 'child_process'
Q      = require 'q'
_      = require 'underscore'



# Options.

option '-w', '--watch', 'Keep watching file modifying.'
option '-c', '--compile', 'Compile client scripts as ADVANCED_OPTIMIZATIONS.'



# Constants.

commonArgs = "
  public/closure-library/closure/bin/build/closurebuilder.py
  --root=./public/closure-library/
  --root=./resources/client/
  --namespace=\"my.app\" "

COMMAND = 
  BUILD: commonArgs + "
  --output_mode=script
  --output_file=./public/javascripts/app.js
  "
  COMPILE: commonArgs + "
  --output_mode=compiled 
  --output_file=./public/javascripts/app-min.js
  --compiler_jar=./library/closure-compiler/compiler.jar
  --compiler_flags=\"--compilation_level=ADVANCED_OPTIMIZATIONS\""



# Tasks.

stamp = ( ->
  startTime = -1
  return {
    start: ->
      startTime = Date.now()  if startTime is -1
    end: ->
      t = if startTime isnt -1 then Date.now() - startTime else 0
      startTime = -1
      return t
  }
)()

execCommand = (command) ->
  stamp.start()
  child.exec command, (error, stdout, stderr) ->
    console.log """
    \n\n======
    #{stderr}
    --IT TOOK #{stamp.end() / 1000} sec.--\n
    """

task 'build', 'Make closure builder build scripts.', (options) ->
  command = if options.compile then COMMAND.COMPILE else COMMAND.BUILD
  if !options.watch
    execCommand(command)
  else
    isFirst = true
    muffin.run
      files: './**/*'
      options: options
      map:
        'client/.*?\.js': (matches) ->
          return  if isFirst
          execCommand(command)
      after: ->
        execCommand(command)  if isFirst
        isFirst = false

これで、cakeタスクが使えるようになりました。

  • 開発用に、更新監視しながらビルドするとき: cake -w build
  • 本番用にコンパイルするとき。:cake -c build

8. ビルドしてみましょう。

$ cake build
The "sys" module is now called "util". It should have a similar interface.
public/closure-library/closure/bin/build/closurebuilder.py: Scanning paths...
public/closure-library/closure/bin/build/closurebuilder.py: 797 sources scanned.
public/closure-library/closure/bin/build/closurebuilder.py: Building dependency tree..

$ node app.js


だ 大丈夫そうですね。

本番環境用はどうでしょうか?

$ cake -c build
The "sys" module is now called "util". It should have a similar interface.
public/closure-library/closure/bin/build/closurebuilder.py: Scanning paths...
public/closure-library/closure/bin/build/closurebuilder.py: 797 sources scanned.
public/closure-library/closure/bin/build/closurebuilder.py: Building dependency tree..
public/closure-library/closure/bin/build/closurebuilder.py: Compiling with the following command: java -jar ./library/closure-compiler/compiler.jar --js public/closure-library/closure/goog/base.js --js public/closure-library/closure/goog/debug/error.js --js public/closure-library/closure/goog/string/string.js --js public/closure-library/closure/goog/asserts/asserts.js --js public/closure-library/closure/goog/array/array.js --js public/closure-library/closure/goog/dom/classes.js --js public/closure-library/closure/goog/object/object.js --js public/closure-library/closure/goog/dom/tagname.js --js public/closure-library/closure/goog/useragent/useragent.js --js public/closure-library/closure/goog/math/size.js --js public/closure-library/closure/goog/dom/browserfeature.js --js public/closure-library/closure/goog/math/coordinate.js --js public/closure-library/closure/goog/dom/dom.js --js resources/client/app.js --compilation_level=ADVANCED_OPTIMIZATIONS
public/closure-library/closure/bin/build/closurebuilder.py: JavaScript compilation succeeded.

$ NODE_ENV=production node app.js


できました。
これで、さて、何作ろう。

      • -

追記:Cakefileをちょっと修正しました。深い階層のjsを更新したら、階層の数だけ無駄にcompileしてた。