用Express 框架創建草地鷚旅行社網站
來自: http://www.ccwebsite.com/create-a-meadowlark-travel-website-with-express-framework/
腳手架
腳手架并不是一個新想法,但很多人(包括我自己)都是通過Ruby才接觸到這個概念的。這個想法很簡單:大多數項目都需要一定數量的“套路化”代碼,誰會想每次開始新項目時都重新寫一次這些代碼呢?對此有個簡單的方法,那就是創建一個通用的項目骨架,每次開始新項目時,只需復制這個骨架,或者說是模板。
RoR把這個概念向前推進了一步,它提供了一個可以自動生成腳手架的程序。相對于從一堆模板中作出選擇,這種方式的優點是可以生成更復雜的框架。
Express借鑒了RoR的這一做法,提供了一個生成腳手架的工具,從而可以讓你開始一個新的Express項目。
盡管Express有可用的腳手架工具,但它目前并不能生成我推薦使用的框架。特別是它不支持我所選擇的模板語言(Handlebars),也沒有遵循我所偏好的命名規則(盡管這很容易解決)。
盡管我們不用這個腳手架工具,但我還是建議你看一下它:到那時,你就能夠充分了解它生成的腳手架是否對你有用了。
套路化對最終發送到客戶端的真正HTML也是有用的。我推薦非常出色的HTML5 Boilerplate( http://html5boilerplate.com/ ),它能生成一個很不錯的空白HTML5網站。最近HTML5 Boilerplate又新增加了可定制的功能,其中一個定制選項包含推ter Bootstrap,這個是我高度推薦的前端框架。
草地鷚旅行社網站
本文以一個可運行的網站為例:假想的草地鷚旅行社網站,該旅行社是一家為到俄勒岡州旅游的人提供服務的公司。如果你對創建REST應用程序更感興趣,不用擔心,因為草地鷚旅行社網站除了作為功能性網站外,也提供REST服務。
初始步驟
先給你的項目創建一個新目錄,這將作為項目的根目錄。本文中,凡提到“項目目錄”“程序目錄”或“項目根路徑”,指的都是這個目錄。
提示:或許你會把Web程序文件跟項目相關的其他文件全都分開存放,比如會議紀要、文檔等。因此,我建議你把項目根路徑作為項目目錄的子目錄。比如,對于草地鷚旅行社網站而言,我會把項目放在~/projects/meadowlark,而項目根路徑放在~/projects/meadowlark/site。
npm在package.json文件中管理項目的依賴項以及項目的元數據。要創建這個文件,最簡單的辦法是運行 npm init :它會問一系列的問題,然后為你生成一個package.json文件幫你起步(對于“入口點”的問題,用meadowlark.js或項目的名字作為答案)。
提示:如果你的package.json文件中沒有指定一個存儲庫的URL,以及一個非空的README.md文件,那么你每次運行npm時都會看到警告信息。package.json文件中的元數據只有在發布到npm存儲庫時才是真正必要的,但為了消除npm的警告信息,做這些工作依然是值得的。
第一步是安裝Express。運行下面這條npm命令:
npm install --save express
運行 npm install 會把指定名稱的包安裝到node_modules目錄下。如果你用了 --save 選項,它還會更新package.json文件。因為node_modules隨時都可以用npm重新生成,所以我們不會把這個目錄保存在我們的代碼庫中。為了確保不把它添加到代碼庫中, 我們可以創建一個.gitignore文件:
# ignore packages installed by npm node_modules # put any other files you don't want to check in here, # such as .DS_Store (OSX), *.bak, etc.
接下來創建meadowlark.js文件,這是我們項目的入口。本文中將這個文件簡單稱為“程序文件”:
var express = require('express'); var app = express(); app.set('port', process.env.PORT || 3000); // 定制404頁面 app.use(function(req, res){ res.type('text/plain'); res.status(404); res.send('404 - Not Found'); }); //定制500頁面 app.use(function(err, req, res, next){ console.error(err.stack); res.type('text/plain'); res.status(500); res.send('500 - Server Error'); }); app.listen(app.get('port'), function(){ console.log( 'Express started on http://localhost:' + app.get('port') + '; press Ctrl-C to terminate.' ); });
提示:很多教程,甚至是Express的腳手架生成器會建議你把主文件命名為app.js(或者有時是index.js或server.js)。除非你用的托管服務或部署系統對程序主文件的名稱有特定的要求,否則我認為這么做是沒有道理的,我更傾向于按照項目命名主文件。凡是曾在編輯器里見過一堆index.html標簽的人都會立刻明白這樣做的好處。 npm init 默認是用index.js,如果要使用其他的主文件名,要記得修改package.json文件中的 main 屬性。
現在你有了一個非常精簡的Express服務器。你可以啟動這個服務器(node meadowlark.js),然后訪問http://localhost:3000。結果可能會讓你失望,因為你還沒給Express任何路由信息,所以它會返回一個404頁面,表示你訪問的頁面不存在。
注釋:注意我們指定程序端口的方式: app.set(port, process.env.PORT || 3000) 。這樣我們可以在啟動服務器前通過設置環境變量覆蓋端口。如果你在運行這個案例時發現它監聽的不是3000端口,檢查一下是否設置了環境變量 PORT 。
提示:我高度推薦你安裝一個能顯示HTTP請求狀態碼和所有重定向的瀏覽器插件。這樣在解決重定向問題或者不正確的狀態碼時會更加容易,它們經常被忽視。對于Chrome來說,Ayima的Redirect Path特別好用。在大多數瀏覽器中, 都能在開發者工具的網絡部分看到狀態碼。
我們來給首頁和關于頁面加上路由。在404處理器之前加上兩個新路由:
app.get('/', function(req, res){ res.type('text/plain'); res.send('Meadowlark Travel'); }); app.get('/about', function(req, res){ res.type('text/plain'); res.send('About Meadowlark Travel'); }); // 定制404頁面 app.use(function(req, res, next){ res.type('text/plain'); res.status(404); res.send('404 - Not Found'); });
app.get 是我們添加路由的方法。在Express文檔中寫的是 app.VERB 。這并不意味著存在一個叫 VERB 的方法,它是用來指代HTTP動詞的(最常見的是“get” 和“post”)。這個方法有兩個參數:一個路徑和一個函數。
路由就是由這個路徑定義的。 app.VERB 幫我們做了很多工作:它默認忽略了大小寫或反斜杠,并且在進行匹配時也不考慮查詢字符串。所以針對關于頁面的路由對于/about、/About、/about/、/about?foo=bar、/about/?foo=bar等路徑都適用。
路由匹配上之后就會調用你提供的函數,并把請求和響應對象作為參數傳給這個函數。現在我們只是返回了狀態碼為200的普通文本(Express默認的狀態碼是200,不用顯式指定)。
我們這次使用的不是Node的 res.end ,而是換成了Express的擴展 res.send 。我們還用 res.set 和 res.status 替換了Node的 res.writeHead 。Express還提供了一個 res.type 方法,可以方便地設置響應頭 Content-Type 。盡管仍然可以使用 res.writeHead 和 res.end ,但沒有必要也不作推薦。
注意,我們對定制的404和500頁面的處理與對普通頁面的處理應有所區別:用的不是 app.get ,而是 app.use 。 app.use 是Express添加 中間件 的一種方法。你可以把它看作處理所有沒有路由匹配路徑的處理器。這里涉及一個非常重要的知識點:在Express中,路由和中間件的添加順序至關重要。如果我們把404處理器放在所有路由上面,那首頁和關于頁面就不能用了,訪問這些URL得到的都是404。現在我們的路由相當簡單,但其實它們還能支持通配符,這會導致順序上的問題。比如說,如果要給關于頁面添加子頁面,比如/about/contact和/about/directions會怎么樣呢?下面這段代碼是達不到預期效果的:
app.get('/about*',function(req,res){ // 發送內容.... }) app.get('/about/contact',function(req,res){ // 發送內容.... }) app.get('/about/directions',function(req,res){ // 發送內容.... })
本例中的 /about/contact 和 /about/directions 處理器永遠無法匹配到這些路徑,因為第一個處理器的路徑中用了通配符: /about* 。
Express能根據回調函數中參數的個數區分404和500處理器。
你可以再次啟動服務器,現在首頁和關于頁面都可以運行了。
截至目前我們所做的事情,即使不用Express也很容易完成,但Express所提供的一些功能并非那么顯而易見。還記得如何規范化 req.url 來確定所請求的資源嗎?我們必須手動剝離查詢字符串和反斜杠,并轉化為小寫。而Express的路由器會自動幫我們處理好這些細節。盡管目前看起來這并非什么大不了的事情,但這只是Express路由器能力的冰山一角。
視圖和布局
如果你熟知“模型-視圖-控制器”模式,那你對視圖這個概念應該不會感到陌生。視圖本質上是要發送給用戶的東西。對網站而言,視圖通常就是HTML,盡管也會發送PNG或PDF,或者其他任何能被客戶端渲染的東西。
視圖與靜態資源(比如圖片或CSS文件)的區別是它不一定是靜態的:HTML可以動態構建,為每個請求提供定制的頁面。
Express支持多種不同的視圖引擎,它們有不同層次的抽象。Express比較偏好的視圖引擎是Jade(因為它也是TJ Holowaychuk開發的) 。Jade所采用的方式非常精簡:你寫的根本不像是HTML,因為沒有尖括號和結束標簽,這樣可以少敲好多次鍵盤。然后,Jade引擎會將其轉換成HTML。
Jade是非常吸引人的,但這種程度的抽象也是有代價的。如果你是一名前端開發人員,即便你實際上是用Jade編寫視圖,也必須理解HTML,并且有足夠深入的認識。我認識的大多數前端開發人員都不喜歡他們主要的標記語言被抽象化處理。因此我推薦使用另外一個抽象程度較低的模板框架Handlebars。Handlebars(基于與語言無關的流行模板語言Mustache)不會試圖對HTML進行抽象:你編寫的是帶特殊標簽的HTML,Handlebars可以借此插入內容。
為了支持Handlebars,我們要用到Eric Ferraiuolo的 express3-handlebars 包(盡管名字中是express3,但這個包在Express 4.0中也可以使用)。在你的項目目錄下執行:
npm install --save express3-handlebars
然后在創建app之后,把下面的代碼加到meadowlark.js中:
var app = express(); // 設置handlebars視圖引擎 var handlebars = require('express3-handlebars') .create({ defaultLayout:'main' }); app.engine('handlebars', handlebars.engine); app.set('view engine', 'handlebars');
這段代碼創建了一個視圖引擎,并對Express進行了配置,將其作為默認的視圖引擎。接下來創建views目錄,在其中創建一個子目錄layouts。如果你是一位經驗豐富的Web開發人員,可能已經熟悉 布局 的概念了(有時也被稱為“母版頁”)。在開發網站時,每個頁面上肯定有一定數量的HTML是相同的,或者非常相近。在每個頁面上重復寫這些代碼不僅非常繁瑣,還會導致潛在的維護困境:如果你想在每個頁面上做一些修改,那就要修改所有文件。布局可以解決這個問題,它為網站上的所有頁面提供了一個通用的框架。
所以我們要給網站創建一個模板。接下來我們創建一個views/layouts/main.handlebars文件:
<!doctype html> <html> <head> <title>Meadowlark Travel</title> </head> <body> {{{body}}} </body> </html>
以上內容你未曾見過的可能只有 {{{body}}} 。這個表達式會被每個視圖自己的HTML取代。在創建Handlebars實例時,我們指明了默認布局( defaultLayout:'main' )。這就意味著除非你特別指明,否則所有視圖用的都是這個布局。
接下來我們給首頁創建視圖頁面,views/home.handlebars:
<h1>Welcome to Meadowlark Travel</h1>
關于頁面,views/about.handlebars:
<h1>About Meadowlark Travel</h1>
未找到頁面,views/404.handlebars:
<h1>404 - Not Found</h1>
最后是服務器錯誤頁面,views/500.handlebars:
<h1>500 - Server Error</h1>
提示:你或許想在編輯器中把.handlebars和.hbs (另外一種常見的Handlebars文件擴展名)跟HTML相關聯,以便啟用語法高亮和其他編輯器特性。如果是vim,你可以在~/.vimrc文件中加上一行 au BufNewFile,BufRead *.handlebars set file type=html 。其他編輯器請參考相關文檔。
現在視圖已經設置好了,接下來我們必須將使用這些視圖的新路由替換舊路由:
app.get('/', function(req, res) { res.render('home'); }); app.get('/about', function(req, res) { res.render('about'); }); // 404 catch-all處理器(中間件) app.use(function(req, res, next){ res.status(404); res.render('404'); }); // 500錯誤處理器(中間件) app.use(function(err, req, res, next){ console.error(err.stack); res.status(500); res.render('500'); });
需要注意,我們已經不再指定內容類型和狀態碼了:視圖引擎默認會返回 text/html 的內容類型和200的狀態碼。在catch-all處理器(提供定制的404頁面)以及500處理器中,我們必須明確設定狀態碼。
如果你再次啟動服務器檢查首頁和關于頁面,將會看到那些視圖已呈現出來。如果你檢查源碼,將會看到views/layouts/main.handlebars中的套路化HTML。
視圖和靜態文件
Express靠中間件處理靜態文件和視圖。只需了解中間件是一種模塊化手段,它使得請求的處理更加容易。
static 中間件可以將一個或多個目錄指派為包含靜態資源的目錄,其中的資源不經過任何特殊處理直接發送到客戶端。你可以在其中放圖片、CSS文件、客戶端JavaScript文件之類的資源。
在項目目錄下創建名為public的子目錄 (因為這個目錄中的所有文件都會直接對外開放,所以我們稱這個目錄為public)。接下來,你應該把 static 中間件加在所有路由之前:
app.use(express.static(__dirname + '/public'));
static 中間件相當于給你想要發送的所有靜態文件創建了一個路由,渲染文件并發送給客戶端。接下來我們在public下面創建一個子目錄img,并把logo.png文件放在其中。
現在我們可以直接指向/img/logo.png (注意:路徑中沒有public,這個目錄對客戶端來說是隱形的), static 中間件會返回這個文件,并正確設定內容類型。接下來我們修改一下布局文件,以便讓我們的logo出現在所有頁面上:
<body> <header> <img src="/img/logo.png" alt="Meadowlark Travel Logo"> </header> {{{body}}} </body>
注釋:`是HTML5中引入的元素,它出現在頁面頂部,提供一些與內容有關的額外語義信息,比如logo、標題文本或導航等。
視圖中的動態內容
視圖并不只是一種傳遞靜態HTML的復雜方式(盡管它們當然能做到)。視圖真正的強大之處在于它可以包含動態信息。
比如在關于頁面上發送“虛擬幸運餅干”。我們在meadowlark.js中定義一個幸運餅干數組:
var fortunes = [ "Conquer your fears or they will conquer you.", "Rivers need springs.", "Do not fear what you don't know.", "You will have a pleasant surprise.", "Whenever possible, keep it simple.", ];
修改視圖(/views/about.handlebars)以顯示幸運餅干:
<h1>About Meadowlark Travel</h1> <p>Your fortune for the day:</p> <blockquote>{{fortune}}</blockquote>
接下來修改路由/about,隨機發送幸運餅干:
app.get('/about', function(req, res){ var randomFortune = fortunes[Math.floor(Math.random() * fortunes.length)]; res.render('about', { fortune: randomFortune }); );
重啟服務器,加載/about頁面,你會看到一個隨機發放的幸運餅干。模板真的是非常有用。
小結
我們剛用Express創建了一個非常基本的網站。盡管簡單,但這個網站包含了功能完備的網站所需的一切。