[NodeJS] 使用 Coke 快速開發 Part 2

上次介紹的 Coke 所使用的樣板引擎 thunder,這次稍微簡單的介紹一下這一套樣板引擎。雖然說是 Ben 自行開發,但是我不得不說,這速度真的相當的快!

雖然我也寫過 jade 或是 EJS,相較之下 thunder 就沒那麼多花招。


Template Engine

樣板引擎流行好一陣子了,雖然說不能跟義大利麵比執行速度,但是對於可維護性上卻是相當加分的東西。thunder 這一套並不是一定得搭配 Coke 來使用,只是 Coke 他內建了這一套樣板引擎而已。

當然,如果你是 ExpressJS 的愛用者,你也可以使用這一套樣板引擎,這是沒有問題的。然後呢,你也可以把他放在 client 端去執行,很可愛吧(疑

#Installation

如果你使用 Coke 就不必額外安裝了。不過,你倘若是要給 ExpressJS 或是要用在 client 端的話,就得額外安裝,client 端必須得 -g 安裝才行。

$ npm install thunder [-g]

#Startup

用法相當簡單,我底下皆以搭配 Coke 為主來作示範,

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><?- it.title ?></title>
    <?= it.css( it.styles ); ?>
  </head>
  <body>
    <h1><?= it.title ?></h1>
    <? if( it.is_nav ) { ?>
      <?= it.partial( 'common/_nav' ) ?>
    <? } ?>

    <?= it.body ?>

    <?= it.js( it.scripts ); ?>
  </body>
</html>

是的,這個樣板內建功能都以 it 開頭,裡面有好多符號,我簡單說明一下,

  • <? ... ?> 直接使用 JavaScript 運算(Evaluation)
  • <?- ... ?> 內容跳脫(Escaping)
  • <?= ... ?> 內容不做跳脫(No escape)

如果你是習慣寫 PHP 的人,應該很眼熟。至於你問我效能?這些都是在 Coke 啟動之前,就會將之編譯成一般的 html 檔案,所以嚴格來說他並不是動態產生的,所以你不必擔心前端的效能問題。

#Functional

在 Coke 裡面,有一些自帶與沿用自 ExpressJS 的方法,

  • it.styles 這是一個陣列,裡面是放你所需要的 css 檔案
  • it.scripts 也是一個陣列,裡面是放你所需要的 js 檔案
  • it.css( ... ) 這是個函式,用來壓縮跟打包你的 css 檔案
  • it.js( ... ) 同上,但是他是用來打包 js 檔案
  • it.body 這是在 layout.html 中使用,他會把你的 Controller/Action 對應的 View 放進來
  • it.partial 這是沿用自 ExpressJS,幫你編譯單獨的 View

如果你要自己寫一些方法呢?在 Coke 裡面,你只要編輯 app/helpers/application.js 就好了,

var moment = require( 'moment' );

module.exports = function ( app ){
  app.helpers({

    selected : function ( target, current ){
      return target === current ? 'selected' : '';
    },

    val : function ( obj, prop ){
      return obj === undefined ? '' : obj[ prop ];
    },

    date : function ( date, format ){
      return moment( date ).format( format || 'MMM Do YYYY, h:m:s' );
    }
  });

  app.dynamicHelpers({
    messages : require( 'express-messages' )
  });
};

Controller

我們再次回到 Controller 這個地方,輸出嘛,所以我們還是得回到這裡來作一些事情。但是我這次不只會講輸出,另外順便提一下運作模式。

這是一個 Coke 剛產生出來的 Controller,

var Application = require( CONTROLLER_DIR + 'application' );

module.exports = Application.extend({
    index : function ( req, res, next ){
        res.render( 'hellos/index' );
    }
});

如果我們要傳一些東西到 View 裡面去,那很簡單,

var Application = require( CONTROLLER_DIR + 'application' );

module.exports = Application.extend({
    index : function ( req, res, next ){
        res.render( 'hellos/index', { title: 'Hello World' } );
    }
});

然後你就可以在 View 裡面使用 it.title 來拿到你從 Controller 傳過來的東西。當然,如果你傳的是 Object 或是 Array 的話,也可以,但是 View 就得用 forEach 來拿東西囉。

var Application = require( CONTROLLER_DIR + 'application' );

module.exports = Application.extend({
    index : function ( req, res, next ){
        res.render( 'hellos/index', { packages: [1,2,3,4,5] } );
    }
});
<? it.packages.forEach( function( package ) { ?>
    <p><?= package ?></p>
<? } ?>

這樣應該相當容易理解吧!所以 Ben 才說,跟寫 PHP 很像啊(笑

Middleware

再來聊一下這個功能很強大的東西,由於一套 Framework 不可能盡善盡美,所以有些時候我們總是需要一些洋人其他的東西來輔助。像是 OAuth 這件事情,自己做很煩,所以用別人做好的套件是相當合理的。

然後 Ben 推薦這一套 Passport,自從我用了他之後考試都考一百分呢,這裡我們就使用 Facebook 來作說明。

首先你需要安裝在你的 Project 裡面,

$ npm install passport 
$ npm install passport-facebook 

喔,為什麼要裝兩個?第二個是 Passport 所提供的 Providers 套件,相當多元!

接著我們要在 app/middleware 底下建立一個 passport.js 這個檔案,

var passport = require( 'passport' );
var Strategy = require( 'passport-facebook' ).Strategy;
var User = require( 'mongoose' ).model( 'User' );
var config = CONF.passport;

passport.serializeUser( function( user, next ) {
    var id = user.id ?
    user.id : user.facebook_id;

    next( null, id );
});

passport.deserializeUser( function( id, next ) {
    User.findOne({
        facebook_id : id
    }, function ( err, user ) {
        if ( user ) return next( null, user );

        next( null, id );
    });
});

passport.use( new Strategy({
    clientID        : config.facebook_api_id,
    clientSecret    : config.facebook_api_secret,
    callbackURL     : config.facebook_callback_url
    }, function( accessToken, refreshToken, profile, next ) {
        process.nextTick( function() {
            return next( null, profile );
        });
    }
));

module.exports = function() {
    return function( req, res, next ) {
        passport.initialize()( req, res, function() {
            passport.session()( req, res, next );
        });
    };
};

喔,裡面有用到了 ModelCONF,這兩個東西暫時先跳過沒關係(喂)。接著我們要在 Application 的地方做設定,讓他可以使用。

app.use( middleware.passport());

加在哪?放在 app.use( app.router ); 之前就可以了。然後,關於 CONF 的部份,由於我們設定了三組數值,我又不想寫在 Middleware 裡面,所以,放在 config.yml 也是很合理的,

passport:
  facebook_api_id: '361188863952843'
  facebook_api_secret: 'bc44c095050c902ef7565bcd3c2a94be'
  facebook_callback_url: 'http://localhost:4000/passport/callback'

那剛剛用到的 Model 呢?這個我們最後再說,先來看看 Controller 這個部份,

var Application = require( CONTROLLER_DIR + 'application' );
var passport = require ( 'passport' );
var mongoose = require ( 'mongoose' );
var User = mongoose.model ( 'User' );

module.exports = Application.extend({
    referer : function ( req, res, next ) {
        var referer = req.headers.referer ?
            req.headers.referer : '/';

        res.cookie( 'referer', referer );
        next();
    },

    facebook : passport.authenticate( 'facebook', {
        scope: [ 'email', 'read_friendlists', 'read_stream', 'publish_stream' ]
    }),

    failure_redirect : passport.authenticate( 'facebook', {
        failureRedirect : '/'
    }),

    callback : function ( req, res, next ) {
        var referer = req.cookies.referer ?
            req.cookies.referer : '/';

        User.create( 'facebook', req.user, 
            function( err ) {
                LOG.error( 500, res, err );
                res.redirect( '/logout' );
            },
            function() {
                res.redirect( referer );
            }
        );
    },

    logout : function ( req, res ) {
        req.logout();
        res.render( 'passport/logout' );
    }
});

這個 Controller 幫我們做幾件事情,

  • 使用者連入 passport/facebook 做 Facebook 的登入驗證
  • 然後 Facebook 導回我們所設定的 callback 位址
  • 我們把 Facebook 帶回來的資訊儲存起來
  • 再次導回到真正的 referer,如果沒有就回到首頁
  • 登出功能

然後這裡又用到了 Model 了!關於 Model 建立的方式我想我之前有講過,我們就直接建立一個 User 的 Model 來用吧。

$ coke g model user 

然後我們就開始編輯 schema.js 囉,

/**
 * Module dependencies.
 * @private
 */
var mongoose = require( 'mongoose' );

var Schema   = mongoose.Schema;
var ObjectId = Schema.ObjectId;

var Model = {
};

Model.User = new Schema({
    facebook_id     : { type : String, required : true, index : true },
    name            : { type : String, required : true },
    email           : { type : String },
    created_at      : { type : Number, 'default' : Date.now },
    updated_at      : { type : Number }
});

// auto update `updated_at` on save
Object.keys( Model ).forEach( function ( model ){
  if( Model[ model ].tree.updated_at !== undefined ){
    Model[ model ].pre( 'save', function ( next ){
      this.updated_at = this.isNew?
        this.created_at :
        Date.now();

      next();
    });
  }
});

/**
 * Exports module.
 */
module.exports = Model;

然後就可以用了嗎?當然不是囉,我們要再去 models/User.js 建立一下我們剛剛用到的方法,

var User = require( BASE_DIR + 'db/schema' ).User;

User.statics = {

    create : function( provider, src, error, success ) {
        var self = this;

        this.findOne({
            facebook_id : src.id
        }, function( err, user ) {
            if( err ) return error( err );
            if( user ) return success();

            new self({
                facebook_id     : src.id,
                name            : src._json.name,
                email           : src._json.email
            }).save( function( err, user, count ) {
                if( err ) return error( err );

                return success();
            });
        });
    }
};

require( 'mongoose' ).model( 'User', User );

嗯,裡面詳細的情況我就不解釋了,在這個 Model 裡面就是建立一個靜態方法,讓我們剛剛在 Controller 裡面可以呼叫,由這個靜態方法來對資料庫做操作動作。

小結

由於辦公室冷氣壞掉,然後筆者早上七點半就到辦公室,現在熱到靠北,所以不想寫小結惹~

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei