Web框架express

海興發表於2013-09-03

Express 是一個簡潔而靈活的 node.js Web應用框架,提供了豐富的HTTP工具以及來自Connect框架的中介軟體。還附帶一個叫做express的命令列工具,可以用它生成一個kickstart程式。可以用npm的全域性安裝命令(L系需要root許可權,加sudo):

npm install -g express

安裝完在任何目錄下都可以執行express命令,express --help可以檢視其幫助:

Usage: express [options] [dir]

  Options:

    -h, --help          output usage information
    -V, --version       output the version number
    -s, --sessions      add session support
    -e, --ejs           add ejs engine support (defaults to jade)
    -J, --jshtml        add jshtml engine support (defaults to jade)
    -H, --hogan         add hogan.js engine support
    -c, --css <engine>  add stylesheet <engine> support (less|stylus) (defaults to plain css)
    -f, --force         force on non-empty directory

在用express生成kickstart程式之前,先express -V看看我的版本:
3.3.8

一行命令生成kickstart程式:

express -s -c less nodecoffee

於是:

   create : nodecoffee
   create : nodecoffee/package.json
   create : nodecoffee/app.js
   create : nodecoffee/public
   create : nodecoffee/public/javascripts
   create : nodecoffee/public/images
   create : nodecoffee/public/stylesheets
   create : nodecoffee/public/stylesheets/style.less
   create : nodecoffee/routes
   create : nodecoffee/routes/index.js
   create : nodecoffee/routes/user.js
   create : nodecoffee/views
   create : nodecoffee/views/layout.jade
   create : nodecoffee/views/index.jade

   install dependencies:
     $ cd nodecoffee && npm install

   run the app:
     $ node app

很nice,連後續命令都告訴我們了。不過這個目錄結構不太MVC,以後再改,先執行第一個命令看看什麼效果。一切順利,接下來用npm ls看看我們都install了什麼東西:

npm WARN package.json application-name@0.0.1 No README.md file found!
application-name@0.0.1 /Users/wukaixing/GitHub/nodecoffee
├─┬ express@3.3.8
│ ├── buffer-crc32@0.2.1
│ ├─┬ commander@1.2.0
│ │ └── keypress@0.1.0
│ ├─┬ connect@2.8.8
│ │ ├── bytes@0.2.0
│ │ ├── formidable@1.0.14
│ │ ├── pause@0.0.1
│ │ ├── qs@0.6.5
│ │ └── uid2@0.0.2
│ ├── cookie@0.1.0
│ ├── cookie-signature@1.0.1
│ ├── debug@0.7.2
│ ├── fresh@0.2.0
│ ├── methods@0.0.1
│ ├── mkdirp@0.3.5
│ ├── range-parser@0.0.4
│ └─┬ send@0.1.4
│   └── mime@1.2.11
├─┬ jade@0.35.0
│ ├── character-parser@1.2.0
│ ├── commander@2.0.0
│ ├─┬ constantinople@1.0.2
│ │ └─┬ uglify-js@2.4.0
│ │   ├── async@0.2.9
│ │   ├─┬ optimist@0.3.7
│ │   │ └── wordwrap@0.0.2
│ │   ├─┬ source-map@0.1.29
│ │   │ └── amdefine@0.0.8
│ │   └── uglify-to-browserify@1.0.1
│ ├── mkdirp@0.3.5
│ ├─┬ monocle@1.1.50
│ │ └─┬ readdirp@0.2.5
│ │   └─┬ minimatch@0.2.12
│ │     ├── lru-cache@2.3.1
│ │     └── sigmund@1.0.0
│ ├─┬ transformers@2.1.0
│ │ ├─┬ css@1.0.8
│ │ │ ├── css-parse@1.0.4
│ │ │ └── css-stringify@1.0.5
│ │ ├─┬ promise@2.0.0
│ │ │ └── is-promise@1.0.0
│ │ └─┬ uglify-js@2.2.5
│ │   ├─┬ optimist@0.3.7
│ │   │ └── wordwrap@0.0.2
│ │   └─┬ source-map@0.1.29
│ │     └── amdefine@0.0.8
│ └─┬ with@1.1.1
│   └─┬ uglify-js@2.4.0
│     ├── async@0.2.9
│     ├─┬ optimist@0.3.7
│     │ └── wordwrap@0.0.2
│     ├─┬ source-map@0.1.29
│     │ └── amdefine@0.0.8
│     └── uglify-to-browserify@1.0.1
└─┬ less-middleware@0.1.12
  ├─┬ less@1.4.2
  │ ├── mime@1.2.11
  │ ├─┬ request@2.21.0
  │ │ ├── aws-sign@0.3.0
  │ │ ├── cookie-jar@0.3.0
  │ │ ├── forever-agent@0.5.0
  │ │ ├─┬ form-data@0.0.8
  │ │ │ ├── async@0.2.9
  │ │ │ └─┬ combined-stream@0.0.4
  │ │ │   └── delayed-stream@0.0.5
  │ │ ├─┬ hawk@0.13.1
  │ │ │ ├─┬ boom@0.4.2
  │ │ │ │ └── hoek@0.9.1
  │ │ │ ├── cryptiles@0.2.2
  │ │ │ ├── hoek@0.8.5
  │ │ │ └─┬ sntp@0.2.4
  │ │ │   └── hoek@0.9.1
  │ │ ├─┬ http-signature@0.9.11
  │ │ │ ├── asn1@0.1.11
  │ │ │ ├── assert-plus@0.1.2
  │ │ │ └── ctype@0.5.2
  │ │ ├── json-stringify-safe@4.0.0
  │ │ ├── node-uuid@1.4.1
  │ │ ├── oauth-sign@0.3.0
  │ │ ├── qs@0.6.5
  │ │ └── tunnel-agent@0.3.0
  │ └── ycssmin@1.0.1
  └── mkdirp@0.3.5

很多,也不知道都是幹嘛用的。為了保證以後換了環境重新install不會出現相容性問題,先把 package.json 改一下,明確jade和less-middleware的版本號,改成這樣:

{
  "name": "node-express-coffee",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.3.8",
    "jade": "0.35.0",
    "less-middleware": "0.1.12"
  }
}

執行node app,執行app.js檔案,在瀏覽器裡訪問app.js中設定的兩個route '/' 和 '/users' :

app.get('/', routes.index);
app.get('/users', user.list);

頁面正確顯示了routes目錄下index、user的響應結果。

工作環境

用node-dev,不再重啟

node-dev是用來做程式碼熱載入的node.js 開發工具,還支援coffeescript和livescript。可以通過npm做全域性安裝:

npm install -g node-dev

這樣在任何目錄下都可以執行node-dev,開發時就可以用node-dev代替node執行程式。

也可以作為開發依賴項加到package.json中:

  "devDependencies": {
    "node-dev": "latest"
  }

為了方便,可以建立一個指令碼檔案來執行開發環境下的程式啟動,bin/dev:

#!/bin/bash

NODE_ENV=development node-dev app

如果是放在了專案的modules中,要把node-dev 改成 ./node_modules/node-dev/bin/node-dev 以指明實際路徑。以後每次啟動程式,就只需要執行bin/dev了。

把程式碼放到github上

在Github上新建一個repository放程式碼 : nodecoffee。因為本地目錄已經有了,所以要麻煩一點:

##其實就是把.git拽下來放到原來那個目錄裡,然後恢復下狀態
git clone --no-checkout nodecoffee-url nodecoffee/nodecoffee.tmp 
mv nodecoffee/nodecoffee.tmp/.git nodecoffee/ 
rmdir nodecoffee/nodecoffee.tmp
cd nodecoffee
git reset --hard HEAD 

建立.gitignore檔案,把node-modules排除掉,然後git commitgit push到github上去。

把應用部署到heroku

按照heroku的文件沒有成功,git push 時失敗了,看到有人在fuck GFW,並給出瞭解決方案。要設定配置檔案,用沒被牆掉的IP地址,vi ~/.ssh/config,內容如下:

Host heroku.com
User freemember007
Hostname 107.21.95.3 
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa
port 22

如果遇到Permission denied (publickey)的錯誤,找到自己的public key,上傳到heroku上。

heroku keys:add ~/.ssh/id_rsa.pub

在heroku上配置域名,然後設定域名解析為CNAME,就可以訪問www.nodecoffee.com了。

解構app.js

解讀app.js

express 生成的web程式主檔案是app.js,所以我們從這個檔案入手,瞭解如何用express構建web程式,並順手把它拆了,做成適合更大應用的結構。到處都有的require就不提了。

建立

require之後建立一個express應用程式:

var app = express();

這個可以留在app.js中。

配置

接下來是一大段的app.setapp.use:

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser('your secret here'));
app.use(express.session());
app.use(app.router);
app.use(require('less-middleware')({ src: __dirname + '/public' }));
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.set用來設定環境變數,第一個引數是環境變數的name,第二個引數是值;在其他需要訪問環境變數的地方,可以用app.get獲取,比如上面的app.get('env')。下面是一些內建Express環境變數:

  • env 執行時環境,預設為 process.env.NODE_ENV 或者 "development"
  • trust proxy 啟用反向代理,預設未啟用狀態
  • jsonp callback name 修改預設?callback=的jsonp回撥的名字
  • json replacer JSON replacer 替換時的回撥, 預設為null
  • json spaces JSON 響應的空格數量,開發環境下是2 , 生產環境是0
  • case sensitive routing 路由的大小寫敏感, 預設是關閉狀態, "/Foo" 和"/foo" 是一樣的
  • strict routing 路由的嚴格格式, 預設情況下 "/foo" 和 "/foo/" 是被同樣對待的
  • view cache 模板快取,在生產環境中是預設開啟的
  • view engine 模板引擎
  • views 模板的目錄

我們用到了最後兩個,設定了頁面顯示的模板引擎和儲存模板的目錄。

設定app.use([path], function)是指在訪問字首為path的路徑執行時中間函式function,上面的程式碼中沒有指定path,則表示在訪問預設字首/的路徑時執行中間函式function。比如上面程式碼中的

app.use(require('less-middleware')({ src: __dirname + '/public' }));
app.use(express.static(path.join(__dirname, 'public')));

在訪問請求靜態檔案/stylesheets/style.css時,就會先執行less-middleware,再執行express.static,返回public/stylesheets/style.css給瀏覽器端。

app.use() 的出場順序非常重要,use的先後順序決定了中間函式的優先順序。 比如 express.logger() 通常是第一個,可以記錄全部請求。如果不想記錄靜態檔案的請求,可以把less-middleware和 app.use(express.static)放到logger前面。

在express以前的版本中有個app.configure()方法,該方法雖然仍得以保留,但推薦使用if代替:

if ('development' == app.get('env')) 

因為以後還會有很多配置,所以我們要把這部分內容提取出來放到單獨的檔案中,建立/config/express.coffee做這些配置,而在app.js中用require代替這部分程式碼:

require('./config/express')(app);

至於express.coffee的內容,以及如何支援coffeescript,後面再講。

路由

express的路由是用方法app.VERB(path, [callback...], callback)設定的,在上面的程式碼中體現就是:

app.get('/', routes.index);
app.get('/users', user.list);

app.VERB() 中的 VERB 是指某一個HTTP 動作, 比如 app.get()app.post()。 每個path都可以對應多個callbacks,這些callbacks跟中間函式一樣,按順序逐一執行,但也有例外,如果某個callback執行了next('route'),它後面的callback就被忽略。

前面的路徑字串path是當做正規表示式處理的,在遇到符合規則的http請求時執行callbacks。 path中不考慮請求引數,比如 "GET /" 會匹配下面的這個路由, 而"GET /?name=tobi"也會匹配。

app.get('/', routes.index);

因為程式要處理的路徑肯定不止這兩條,所以這些也要從app.js裡拿出來,放到另外的檔案裡。在app.js中用

require('./config/routes')(app);

代替。/config/routes.coffee檔案也要在下一節給出了。

啟動

檔案最後的程式碼是啟動伺服器,並輸出一段日誌:

http.createServer(app).listen(app.get('port'), function(){
  console.log('NodeCoffee server listening on port ' + app.get('port'));
});

這個也要保留在app.js中

拆分app.js

因為express生成的app.js不適用比較大的程式,所以我們要把它拆開,最終的檔案如下所示:

var express = require('express');
var http = require('http');
var path = require('path');
var app = express();

require('./config/express')(app);

require('./config/routes')(app);

http.createServer(app).listen(app.get('port'), function(){
  console.log('NodeCoffee server listening on port ' + app.get('port'));
});

從上面的程式碼可以看出來,我們把配置資訊都放到了config目錄下,又建立了兩個檔案。此外,原來routes下的檔案也被挪走了,因為它們其實不是routes,是controller來的。

所以我們又遵循MVC模式新建了三個目錄app/controllers、app/models、app/views,原來routes目錄下的兩個檔案挪到controllers,views目錄也挪到了app下。

因為我們以後要用coffeescript寫程式碼,所以要先把coffeescript引入專案,在package.json中的dependencies里加一條:

"coffee-script": "1.6.3"

此外還要在app.js裡引入 require('coffee-script');,然後就可以開始用coffeescript改程式碼了。上兩段程式碼: config/express.coffee:

express = require 'express'
path = require 'path'

module.exports = (app,config) ->
    #all environments
    app.set "port", process.env.PORT or 3000

    app.set('showStackError', true)

    app.set('views', config.root + '/app/views')
    app.set "view engine", "jade"
    app.use express.favicon()
    app.use express.logger("dev")
    app.use express.bodyParser()
    app.use express.methodOverride()
    app.use express.cookieParser("p8zztgch48rehu79jskhm6aj3")
    app.use express.session()
    app.use app.router
    app.use require("less-middleware")(src: __dirname + "/public")
    app.use express.static(path.join(__dirname, "public"))
    # development only
    app.use express.errorHandler()  if "development" is app.get("env")

config/routes.coffee:

module.exports = (app) ->
    home = require '../app/controllers/home'
    app.get '/', home.index
    user = require '../app/controllers/user'
    app.get '/users', user.list

原來routes目錄下的index.js改成了/app/controllers/home.coffee,user.js改成了user.coffee。

express的基本配置項

上一節講到app.js中有一段程式碼用app.setapp.use對express進行配置,但這些配置都是什麼意思,以及都能做哪些配置並沒有展開。這一節就專門來講express的配置。上節已經介紹了幾個設定,接下來先介紹剩下的幾個:

app.use(express.favicon());
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser('your secret here'));
app.use(express.session());
app.use(app.router);

express.favicon(iconPath):用來設定網站的圖示,引數為圖示的路徑。如果不指明,則用預設的express圖示。可以修改為:

app.use(express.favicon(path.join(__dirname, '../public/img/favicon.ico')))

express.bodyParser(): 對請求內容進行解析,支援json、 application/x-www-form-urlencoded、multipart/form-data 格式資料的解析。也就是說ajax和form傳送請求時,都會經過它的處理,方便在req中獲取相應的請求值。在express中處理檔案上傳也是用它,可以給出引數指明上傳檔案存放的路徑,比如:

app.use(express.bodyParser({uploadDir:'./uploads'}))

express.methodOverride():為了支援put、delete等HTTP方法,不過要客戶端配合,包含相應的_method引數,比如:

<form action='/users/1'> ...
  <input type="hidden" name="_method" value="put" />
</form>

可以對應到:

app.put('/users/:id', users.put)

app.use(express.cookieParser('your secret here'));app.use(express.session());:是為了支援session,在這樣的設定中,session會被加密儲存在客戶端的cookie,但這樣程式重啟後session就不起作用了,不過express.session支援session的持久化儲存,因為express用的最多的資料庫就是mongo,所以下面給出用mongo儲存session的配置。首先要在package.json里加上依賴項connect-mongo

"connect-mongo": "0.3.3"

然後在config/express.coffee中引入connect-mongo,並修改express.session的設定:

mongoStore = require('connect-mongo')(express)

app.use express.session
      secret: '1234567890'
      store: new mongoStore
        url: config.db,
        collection : 'sessions'

app.routerconnect router的加強版,用來處理app.getapp.post等請求處理設定,在瀏覽器訪問這些設定中對應的url時,express.router會呼叫相應的function。如果不顯式呼叫app.use(app.router),express會在第一次碰到app.get(...)之類的設定時隱含呼叫,所以這個可以不用出現在配置項裡,但.use的順序很關鍵,所以顯式呼叫比較好。比如在use中出現app.use(express.static(path.join(__dirname, 'public')));時,如果router沒有出現,或被放在了它後面,那伺服器每次遇到請求就會到硬碟上找有沒有對應的靜態檔案,會造成效能下降。

這些是express最基本的配置項,其他常用的配置項,比如壓縮、資料校驗、認證和授權等都可以放在這裡,下節繼續介紹。

express配置項more

前面對express配置的介紹都是基於它所生成的app.js檔案,現在去翻翻express的API文件,看看還有什麼。

express把API分成了四部分:Applicationrequestresponsemiddleware。這四部分是express的關注點,其中最重要的是requestresponse兩部分,和大多數web框架一樣。

在application部分中,跟配置相關的set,getuse前面介紹過了,還有一個很重要的:

app.locals:這是一個函式物件,可以給它增加新的屬性。程式內所有頁面模板都能訪問這個物件,所以可以用它儲存全域性配置變數供頁面模板使用。

預設情況下express只有一個應用級變數,即可以由set()設定的settings,比如:

app.set('title', 'NodeCoffee');
#在view裡使用 settings.title

app.locals可以有幾種不同的用法。可以直接增加屬性賦值:

app.locals.title = 'NodeCoffee'

也可以呼叫方法直接傳入一個物件:

app.locals(
  title: 'NodeCoffee'
  phone: '1150-858-9990'
  email: 'me@nodecoffee.com'
)

因為在javascript的函數語言程式設計特性,所以也可以把函式傳到模板中使用,比如:

app.locals._ = require "underscore"

在request部分,沒有配置項,真沒有。

在response部分,有一個跟app.locals對應的res.localsres.locals用於儲存請求級變數,它的用法跟app.locals一樣,比如剛才的title等變數也可以放在res.locals中,但不能直接在配置中賦值,要用app.use以middleware的方式設定。比如middleware view-helpers就是用res.locals儲存請求級變數,其中部分程式碼如下:

res.locals.appName = name || 'App'
res.locals.title = name || 'App'
res.locals.req = req
res.locals.isActive = function (link) {
  return req.url.indexOf(link) !== -1 ? 'active' : ''
}
res.locals.formatDate = formatDate
res.locals.stripScript = stripScript
res.locals.createPagination = createPagination(req)

其中的formatDatestripScriptcreatePagination都是它定義的函式,要在view中使用。

middleware是express重要的請求處理機制,在express的配置中可以通過.use()讓多個middleware構成請求的處理鏈,除了express提供的middleware,還有很多第三方middleware,比如上文提到的view-helpers,還有用於使用者驗證的passport等。我們先來介紹兩個express的middleware:

express.compress() :通過gzip / deflate壓縮響應資料. 這個中介軟體應該放置在所有的中介軟體最前面以保證所有的返回都是被壓縮的。不過也可以只對文字內容進行壓縮,比如:

app.use express.compress
    filter: (req, res) ->
      return /json|text|javascript|css/.test(res.getHeader('Content-Type'))
    level: 9

express.csrf():這是為防護CSRF攻擊的middleware,伺服器會為每個使用者產生一個唯一的"_csrf"標識,儲存在使用者的session中,對於那些需要伺服器更改的請求,伺服器會對req.session._csrf屬性進行校驗,如果客戶端發過來的這個屬性與儲存在session中的值一致,則通過,否則返回304響應。

它需要session支援,因此應該放在.use(express.session())之後。

以上是express自帶的middleware,此外我們還要再介紹兩個常用的第三方middleware。第一個是用於輸入資料校驗的express-validator,另一個是用於使用者驗證的passport

express-validator是第三方middleware,所以要使用它需要先在package.json中加入依賴項:

"express-validator":"0.8.0"

然後在config/express.coffee中引入,配置如下:

expressValidator = require('express-validator')

app.use(expressValidator(
      errorFormatter: (param, msg, value)->
          namespace = param.split('.')
          root = namespace.shift()
          formParam = root

          while(namespace.length) 
            formParam += '[' + namespace.shift() + ']'

          param : formParam
          msg   : msg
          value : value
      ))

express-validator的配置要放在express.bodyParser之後,至於這個validator如何使用,將在controller中介紹。

passport的配置稍微複雜一點,這裡不展開講它的全部配置,只介紹作為middleware在express中的設定。只需要兩行程式碼:

app.use passport.initialize()
app.use passport.session()

第一行是passport的初始化,第二行是為了配合在session中儲存驗證侯的使用者資訊。下一節會以使用者註冊及登入為例介紹如何編寫express web程式,還會詳細講解passport的使用。

相關文章