本文針對那些對Node.js有一定了解的讀者。假設你已經知道如何執行Node程式碼,使用npm安裝依賴模組。但我保證,你並不需要是這方面的專家。本文針對的是Express 3.2.5版本,以介紹相關概念為主。
Express.js這麼描述自己:”輕量靈活的node.js Web應用框架”。它可以幫助你快速搭建web應用。如果你使用過Ruby裡的Sinatra,那麼相信你對這個也會很快就能熟悉。
和其他web框架一樣,Express隱藏了程式碼背後的祕密,然後告訴你:”別擔心,你不用去理解這個部分”。它來幫你解決這些問題,所以你不用去為這個而煩惱,只用將重心集中到程式碼上。換句話說,它有某些魔法!
Express的wiki裡介紹了一些它的使用者,其中就有很多知名的公司: MySpace, Klout.
但是擁有魔力是需要付出代價的,你可能根本就不知道它的工作原理。正如駕駛一輛汽車,我可以很好的駕馭它但是可能不理解為什麼汽車可以正常工作,但是我最好知道這些東西。如果車壞掉怎麼辦?如果你想最大程度的去發揮它的效能?如果你對知識有無限的渴望並想去弄清它?
那麼我首先從理解Express的最底層-Node開始。
底層:Node HTTP伺服器
Node中有HTTP模組, 它將搭建一個web伺服器的過程抽象出來。你可以這樣使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 引入所需模組 var http = require("http"); // 建立伺服器 var app = http.createServer(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello world!\n"); }); // 啟動伺服器 app.listen(1337, "localhost"); console.log("Server running at http://localhost:1337/"); |
執行這個程式(假設檔名為 app.js
,執行 node app.js
),你會得到”Hello world!“ 在瀏覽器訪問localhost:1337
,你會得到同樣的結果。你也可以嘗試訪問其他地址,如 localhost:1337/whatever
,結果仍然會一樣。
分解以上程式碼來看。
第一行使用 require
函式引入Node內建模組 http
。然後存入名為 http
的變數中。如果你要了解更多關於require函式的知識,參考Nodejitsu的文件。
然後我們使用 http.createServer
將伺服器儲存至 app
變數。它將一個函式作為引數監聽請求。稍後將會詳細介紹它。
最後我們要做的就是告訴伺服器監聽來自1337埠的請求,之後輸出結果。然後一切完成。
好的,回到request請求處理函式。這個函式相當重要。
request方法
在開始這個部分之前,我事先宣告這裡所涉及的HTTP相關知識與學習Express本身沒有太大關係。如果你感興趣,可以檢視HTTP模組文件。
任何時候我們向伺服器發起請求,request方法將會被呼叫。如果你不信,你可以 console.log
將結果列印出來。你會發現每次請求一個頁面時它都會出來。
request
是來自客戶端的請求。在很多應用中,你可能會看到它的縮寫 req
。仔細看程式碼。我們修改程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var app = http.createServer(function(request, response) { // 建立answer變數 var answer = ""; answer += "Request URL: " + request.url + "\n"; answer += "Request type: " + request.method + "\n"; answer += "Request headers: " + JSON.stringify(request.headers) + "\n"; // 返回結果 response.writeHead(200, {"Content-Type": "text/plain" }); response.end(answer); }); |
重啟伺服器並重新整理 localhsot:1337
.你會發現,每次訪問一個URL,就會發起一次GET請求,並會得到一堆類似使用者代理或者一些其他的更加複雜的HTTP相關資訊。如果你訪問 localhost:1337/what_is_fraser
, 你會看到request的地址發生了變化。如果你使用不同的瀏覽器訪問,使用者代理也會跟著改變,如果你使用POST請求,request的方法也很改變。
response
是另外一個部分。正如 request
被縮寫為 req
,response
同樣被簡寫為 res
。每次response你都會得到對應的返回結果,之後你便可以通過呼叫 response.end
來結束。實際上最終你還是要執行這個方法的, 甚至在node的文件裡也是這麼描述的。這個方法完成了真正的資料傳輸部分。你可以建立一個伺服器並不呼叫 req.end
方法,它就會永遠存在。
在你返回結果之前,你也可以填寫一下header頭部部分。我們的例子裡是這麼寫的:
1 |
response.writeHead(200, { "Content-Type": "text/plain" }); |
這個步驟主要完成兩件事情。第一,傳送HTTP狀態碼,表示請求成功。其次,它設定了返回的頭部資訊。這裡表示我們要返回的是純文字格式的內容。我們也可以返回類似JSON或者HTML格式的內容。
未完待續。。。
// 接上回
看了上面的之後,你可能會立馬開始利用它來寫api了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var http = require("http"); http.createServer(function(req, res) { // Homepage if(req.url == "/") { res.writeHead(200, { "Content-Type": "text/html" }); res.end("Welcome to the homepage!"); } // About page else if (req.url == "/about") { res.writeHead(200, { "Content-Type": "text/html" }); res.end("Welcome to the about page!"); } // 404'd! else { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("404 error! File not found."); } }).listen(1337, "localhost"); |
你可以選擇優化程式碼,讓它變得更整潔。也可以向npm.org的那幫傢伙一樣用原生的Node來編寫。但是你也可以選擇去建立一個框架。這就是Sencha所做的,並把這個框架稱為 – Connect.
中介軟體: Connect
Connect是Nodejs的中介軟體。可能你現在還並不太理解什麼是中介軟體(middleware),別擔心,我馬上會進行詳細解釋。
一段Connect程式碼
假如我們想要編寫和上面一樣的程式碼,但是這次我們要使用Connect.別忘記安裝Connect模組(npm install
)。完成之後,程式碼看起來非常相似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 引入所需模組 var connect = require("connect"); var http = require("http"); // 建立app var app = connect(); // 新增中介軟體 app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello world!\n"); }); // 啟動應用 http.createServer(app).listen(1337); |
下面分解這段程式碼來看。
首先我們分別引入了Connect和Node HTTP模組。
接下來和之前一樣宣告 app
變數,但是在建立伺服器時,我們呼叫了 connect()
.這有是如何工作的?
我們新增了一箇中介軟體,實際上就是一個函式。傳入 app.use
,幾乎和上面使用request方法寫法一樣。實際上程式碼是從上面貼上過來的。
之後我們建立並啟動伺服器。 http.createServer
接收函式作為引數。沒錯,app
實際上也是一個函式。這是一個Connect提供的函式,它會查詢程式碼並自上而下執行。
(你可能會看見其他人使用 app.listen(1337)
, 這實際上只是將 http.createServer
返回一個promise物件。 再Connect和Express中都是一樣的原理。)
接下來解釋什麼是中介軟體(middleware).
什麼是中介軟體?
首先推薦閱讀Stephen Sugden對於Connect中介軟體的描述,比我講的更好。如果你不喜歡我的解釋,那就去看看。
還記得之前的request方法?每個中介軟體都是一個handler.依次傳入request, response, next三個引數。
一個最基本的中介軟體結構如下:
1 2 3 4 5 |
function myFunMiddleware(request, response, next) { // 對request和response作出相應操作 // 操作完畢後返回next()即可轉入下個中介軟體 next(); } |
當我們啟動一個伺服器,函式開始從頂部一直往下執行。如果你想輸出函式的執行過程,新增一下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var connect = require("connect"); var http = require("http"); var app = connect(); // log中介軟體 app.use(function(request, response, next) { console.log("In comes a " + request.method + " to " + request.url); next(); }); // 返回"hello world" app.use(function(request, response, next) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello World!\n"); }); http.createServer(app).listen(1337); |
如果你啟動應用並訪問 localhost:1337
,你會看到伺服器可以log出相關資訊。
有一點值得注意,任何可以在Node.js下執行的程式碼都可以在中介軟體執行。例如上面我們所使用的req.method
方法。
你當然可以編寫自己的中介軟體,但是也不要錯過Connect的一些很cool的第三方中介軟體。下面我們移除自己的log中介軟體,使用Connect內建方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var connect = require("connect"); var http = require("http"); var app = connect(); app.use(connect.logger()); // 一個有趣的事實:connect.logger返回一個函式 app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello world!\n"); }); http.createServer(app).listen(1337); |
跳轉至瀏覽器並訪問 localhost:1337
你會得到同樣的結果。
很快有人就會想使用上面的中介軟體組合起來建立一個完整應用。程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
var connect = require("connect"); var http = require("http"); var app = connect(); app.use(connect.logger()); // Homepage app.use(function(request, response, next) { if (request.url == "/") { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Welcome to the homepage!\n"); // The middleware stops here. } else { next(); } }); // About page app.use(function(request, response, next) { if (request.url == "/about") { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Welcome to the about page!\n"); // The middleware stops here. } else { next(); } }); // 404'd! app.use(function(request, response) { response.writeHead(404, { "Content-Type": "text/plain" }); response.end("404 error!\n"); }); http.createServer(app).listen(1337); |
“這個看起來不太好看!我要自己寫框架!”
某些人看了Connect的程式碼之後覺得,“這個程式碼可以更簡單”。於是他們創造了Express.(事實上他們好像直接盜用了Sinatra.)
最頂層: Express
文章進入第三部分,我們開始真正進入Express.
正如Connect擴充了Node, Express擴充Connect.程式碼的開始部分看起來和在Connect中非常類似:
1 2 3 |
var express = require("express"); var http = require("http"); var app = express(); |
結尾部分也一樣:
1 |
http.createServer(app).listen(1337); |
中間部分才是不一樣的地方。Connect為我們提供了中介軟體,Express則為我們提供了另外三個優秀的特性: 路由分發,請求處理,檢視渲染。首先從如有開始看。
特性一:路由
路由的功能就是處理不同的請求。在上面的很多例子中,我們分別有首頁,關於和404頁面。我們是通過 if
來判斷並處理不同請求地址。
但是Express卻可以做的更好。Express提供了”routing”這個東西,也就是我們所說的路由。我覺得可讀性甚至比純文字還要好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var express = require("express"); var http = require("http"); var app = express(); app.all("*", function(request, response, next) { response.writeHead(404, { "Content-Type": "text/plain" }); next(); }); app.get("/", function(request, response) { response.end("Welcome to the homepage!"); }); app.get("/about", function(request, response) { response.end("Welcome to the about page!"); }); app.get("*", function(request, response) { response.end("404!"); }); http.createServer(app).listen(1337); |
簡單的引入相關模組之後,我們立即呼叫 app.all
處理所有請求。寫法看起來也非常像中介軟體不是嗎?
程式碼中的 app.get
就是Express提供的路由系統。也可以是 app.post
來處理POST請求,或者是PUT和任何的HTTP請求方式。第一個引數是路徑,例如 /about
或者 /
。第二個引數類似我們之前所見過的請求handler。引用Expess文件的內容:
這些請求handler和中介軟體一樣,唯一的區別是這些回撥函式會呼叫
next('route')
從而能夠繼續執行剩下的路由回撥函式。這種機制
簡單說來,它們和我們之前提過的中介軟體是一樣,只不過是一些函式而已。
這些路由也可以更加靈活,看起來是這樣:
1 2 3 |
app.get("/hello/:who", function(req, res) { res.end("Hello, " + req.params.who + "."); }); |
重啟伺服器並在瀏覽器訪問 localhost:1337/hello/animelover69
你會得到如下資訊:
1 |
<code>Hello, animelover69. </code> |
這些文件演示瞭如何使用正規表示式,可以使得路由更加靈活。如果只是單從概念理解來講,我說的已經足夠了。
但是還有更加值得我們去關注的。
特性二:請求處理 request handling
Express將你傳入請求的handler傳入request和response物件中。原先該有的還在,但是卻加入了更多新的特性。API文件裡有詳細解釋。下面讓我們來看一些例子。
其中一個就是 redirect
方法。程式碼如下:
1 2 3 |
response.redirect("/hello/anime"); response.redirect("http://xvfeng.me"); response.redirect(301, "http://xvfeng.me"); // HTTP 301狀態碼 |
以上程式碼既不屬於原生Node程式碼也不是來自與Connect,而是Express中自身新增的。它加入了一些例如sendFile
,讓你傳輸整個檔案等功能:
1 |
response.sendFile("/path/to/anime.mp4"); |
request物件還有一些很cool的屬性,例如 request.ip
可以獲取IP地址, request.files
上傳檔案等。
理論上來講,我們要知道的東西也不是太多,Express做的只是擴充了request和response物件而已。Express所提供的方法,請參考API文件.
特性三:檢視
Express可以渲染檢視。程式碼如下:
1 2 3 4 5 6 7 8 9 |
// 啟動Express var express = require("express"); var app = express(); // 設定view目錄 app.set("views", __dirname + "/views"); // 設定模板引擎 app.set("view engine", "jade"); |
開頭部分的程式碼和前面基本一樣。之後我們指定檢視檔案所在目錄。然後告訴Express我們要使用 Jade
作為模板引擎。 Jade是一種模板語言。稍後將會詳細介紹。
現在我們已經設定好了view.但是如何來使用它呢?
首先我們建立一個名為 index.jade
的檔案並把它放入 views
目錄。程式碼如下:
1 2 3 4 5 |
doctype 5 html body h1 Hello, world! p= message |
程式碼只是去掉了括號的HTML程式碼。如果你懂HTML那肯定也看得懂上面的程式碼。唯一有趣的是最後一樣。 message
是一個變數。它是從哪裡來的呢?馬上告訴你。
我們需要從Express中渲染這個檢視。程式碼如下:
1 2 3 |
app.get("/", function(request, response) { response.render("index", { message: "I love anime" }); }); |
Express為 response
物件新增了一個 render
方法。這個方法可以處理很多事情,但最主要的還是載入模板引擎和對應的檢視檔案,之後渲染成普通的HTML文件,例如這裡的 index.jade
.
最後一步(我覺得可能算是第一步)就是安裝Jade,因為它本身並不是Express的一部分。新增至package.json
檔案並使用 npm install
進行安裝。
加分特性: 所有程式碼來自於Connect和Node
我需要再次提醒你的是Express建立與Connect和Node之上,這意味著所有的Connect中介軟體均可以在Express中使用。這個對與開發來講幫助很大。例如:
1 2 3 4 5 6 7 8 9 10 |
var express = require("express"); var app = express(); app.use(express.logger()); // 繼承自Connect app.get("/", function(req, res) { res.send("fraser"); }); app.listen(1337); |
如果說你從這篇文章中學到了一點什麼,就是這一點。
實戰
本文的大部分內容都是理論,但是下面我將教你如何使用它來做一點你想做的東西。我不想說的過於具體。
你可以將Express安裝到系統全域性,從而可以在命令列使用它。它可以幫助你迅速的完成程式碼組織並啟動應用。使用npm安裝:
1 |
<code># 安裝時可能需要加 `sudo` npm install -g express </code> |
如果你需要幫助,輸入 express --help
。它加入一些可選引數。例如,如果你想使用EJS模板引擎,LESS作為CSS引擎。應用的名稱為”myApp”.輸入以下命令:
1 |
<code>express --ejs --css less myApp </code> |
這裡會自動生成很多檔案。進入專案目錄,並使用 npm install
安裝依賴包,之後便可以使用 node app
啟動應用!我建議你詳細的檢視專案結構和程式碼。它可能還算不上一個真正的應用,但是我覺得它對於初學者來講還是很有幫助的。
專案Github目錄下也有一些很有幫助的文件。
一些補充
- 如果你也和我一樣喜歡使用CoffeeScript,好訊息是Express完美支援CoffeeScript.你甚至不需要編譯它。這樣你只用
coffee app.coffee
即可啟動應用。我在我的其他專案中也是這麼做的。 - 在我看到
app.use(app.router)
的時候我很疑惑: Express不是一直在使用router嗎?簡單回答是app.router
是Express的路由中介軟體,在你定義路由的時候被直接新增到專案中。如果你需要在載入其他檔案之前應用,也可以直接引入它。關於這麼做的原因,請參考StackOverflow的這個答桉. - 本文是針對Express 3,而在第四版的規劃中又會有很多大的改動。最明顯的是,Experss可能要將會分解成一些小的模組,並吸收Connect的一些特性。這個雖然還在計劃中,但是也值得一看。
如果這個還不能滿足你?你肯定是個變態!你很快就會變成像一個癮君子,半睜著眼,耗盡你最後一點精力,寫著苦逼的程式碼。
正如Rails成為使用Ruby建立網頁應用的王者一樣,我覺得Express也會成為Node中的主流。但是和Rails不一樣,Express更加底層。似乎還沒有一個真正意義上的高階Node庫。我覺得可能會發生改變。(譯者注:這點我不同意,Node的很多思想來自與Unix哲學,強調的是一個Module只解決一個問題,而不是成為一個複雜的庫。很多Rails的開發者轉向Node,就是因為Rails正在逐漸變得臃腫,不易自定義,且效率逐漸降低。)。
這裡我就不再多談。已經又很多很基於Express建立了新的東西,Expess的維基裡有列舉。如果你覺得好可以隨意使用它們,如果你喜歡從底層做起,你也可以只選擇Express。不管是哪一種,好好利用它吧。
原文地址:http://evanhahn.com/understanding-express-js/
時間倉促,翻譯錯誤在所難免,還請指正,轉載還請註明。