Express教程(入門簡單版)
Express 簡介
Express 是一個簡潔而靈活的 node.js Web應用框架, 提供了一系列強大特性幫助你建立各種 Web 應用,和豐富的 HTTP 工具。
使用 Express 可以快速地搭建一個完整功能的網站,它有一套健壯的特性,可用於開發單頁、多頁和混合Web應用。
此文介紹如何使用Express搭建多人部落格。
學習環境
Node.js: 0.10.32
Express: 4.10.2
MongoDB: 2.6.1
快速開始
安裝 Express
express 是 Node.js 上最流行的 Web 開發框架,正如他的名字一樣,使用它我們可以快速的開發一個 Web 應用。我們用 express 來搭建我們的部落格,開啟命令列,輸入:
$ npm install -g express-generator
安裝 express 命令列工具,使用它我們可以初始化一個 express 專案。
新建一個工程
在命令列中輸入:
$ express -e blog
$ cd blog && npm install
初始化一個 express 專案並安裝所需模組,如下圖所示:
然後執行:
$ DEBUG=blog node ./bin/www(windows 下:DEBUG=blog:* npm start )
(上面的程式碼報錯的話,可以這樣執行啟動專案:npm start) 啟動專案,此時命令列中會顯示 blog Express server listening on port 3000 +0ms
localhost:3000
,如下圖所示:
至此,我們用 express 初始化了一個工程專案,並指定使用 ejs 模板引擎,下一節我們講解工程的內部結構。
工程結構
我們回頭看看生成的工程目錄裡面都有什麼,開啟我們的 blog 資料夾,裡面如圖所示:
app.js:啟動檔案,或者說入口檔案
package.json:儲存著工程的資訊及模組依賴,當在 dependencies 中新增依賴的模組時,執行 npm install
,npm 會檢查當前目錄下的 package.json,並自動安裝所有指定的模組
node_modules:存放 package.json 中安裝的模組,當你在 package.json 新增依賴的模組並安裝後,存放在這個資料夾下
public:存放 image、css、js 等檔案
routes:存放路由檔案
views:存放檢視檔案或者說模版檔案
bin:存放可執行檔案
開啟app.js,讓我們看看裡面究竟有什麼:
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var users = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next){
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next){
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next){
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;
這裡我們通過require()載入了express、path 等模組,以及 routes 資料夾下的index. js和 users.js 路由檔案。 下面來講解每行程式碼的含義。
(1) var app = express():生成一個express例項 app。
(2)app.set(‘views’, path.join(__dirname, ‘views’)):設定 views 資料夾為存放檢視檔案的目錄, 即存放模板檔案的地方,__dirname 為全域性變數,儲存當前正在執行的指令碼所在的目錄。
(3)app.set(‘view engine’, ‘ejs’):設定檢視模板引擎為 ejs。
(4)app.use(favicon(__dirname + ‘/public/favicon.ico’)):設定/public/favicon.ico為favicon圖示。
(5)app.use(logger(‘dev’)):載入日誌中介軟體。
(6)app.use(bodyParser.json()):載入解析json的中介軟體。
(7)app.use(bodyParser.urlencoded({ extended: false })):載入解析urlencoded請求體的中介軟體。
(8)app.use(cookieParser()):載入解析cookie的中介軟體。
(9)app.use(express.static(path.join(__dirname, ‘public’))):設定public資料夾為存放靜態檔案的目錄。
(10)app.use(‘/’, routes);和app.use(‘/users’, users):路由控制器。
(11)
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
捕獲404錯誤,並轉發到錯誤處理器。(12)
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
開發環境下的錯誤處理器,將錯誤資訊渲染error模版並顯示到瀏覽器中。(13)
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
生產環境下的錯誤處理器,將錯誤資訊渲染error模版並顯示到瀏覽器中。(14)module.exports = app :匯出app例項供其他模組呼叫。
我們再看 bin/www 檔案:
#!/usr/bin/env node
var debug = require('debug')('blog');
var app = require('../app');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function(){
debug('Express server listening on port ' + server.address().port);
});
(1)#!/usr/bin/env node:表明是 node 可執行檔案。
(2)var debug = require(‘debug’)(‘blog’):引入debug模組,列印除錯日誌。
(3)var app = require(‘../app’):引入我們上面匯出的app例項。
(4)app.set(‘port’, process.env.PORT || 3000):設定埠號。
(5)
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});
啟動工程並監聽3000埠,成功後列印 Express server listening on port 3000。
我們再看 routes/index.js 檔案:
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});
module.exports = router;
生成一個路由例項用來捕獲訪問主頁的GET請求,匯出這個路由並在app.js中通過app.use(‘/’, routes); 載入。這樣,當訪問主頁時,就會呼叫res.render(‘index’, { title: ‘Express’ });渲染views/index.ejs模版並顯示到瀏覽器中。
我們再看看 views/index.ejs 檔案:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<linkrel='stylesheet'href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
在渲染模板時我們傳入了一個變數 title 值為 express 字串,模板引擎會將所有 <%= title %> 替換為 express ,然後將渲染後生成的html顯示到瀏覽器中,如上圖所示。
在這一小節我們學習瞭如何建立一個工程並啟動它,瞭解了工程的大體結構和運作流程,下一小節我們將學習 express 的基本使用及路由控制。
路由控制
工作原理
routes/index.js 中有以下程式碼:
router.get('/', function(req, res){
res.render('index', { title: 'Express' });
});
這段程式碼的意思是當訪問主頁時,呼叫 ejs 模板引擎,來渲染 index.ejs 模版檔案(即將 title 變數全部替換為字串 Express),生成靜態頁面並顯示在瀏覽器中。
我們來作一些修改,以上程式碼實現了路由的功能,我們當然可以不要 routes/index.js 檔案,把實現路由功能的程式碼都放在 app.js 裡,但隨著時間的推移 app.js 會變得臃腫難以維護,這也違背了程式碼模組化的思想,所以我們把實現路由功能的程式碼都放在 routes/index.js 裡。官方給出的寫法是在 app.js 中實現了簡單的路由分配,然後再去 index.js 中找到對應的路由函式,最終實現路由功能。我們不妨把路由控制器和實現路由功能的函式都放到 index.js 裡,app.js 中只有一個總的路由介面。
最終將 app.js 修改為:
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var app = express();
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
routes(app);
app.listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
修改 index.js 如下:
module.exports = function(app) {
app.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});
};
現在,再執行你的 app,你會發現主頁毫無二致。這裡我們在 routes/index.js 中通過 module.exports
匯出了一個函式介面,在 app.js 中通過 require
載入了 index.js 然後通過 routes(app)
呼叫了 index.js 匯出的函式。
路由規則
express 封裝了多種 http 請求方式,我們主要只使用 get
和 post
兩種,即 app.get()
和 app.post()
。
app.get()
和 app.post()
的第一個引數都為請求的路徑,第二個引數為處理請求的回撥函式,回撥函式有兩個引數分別是 req 和 res,代表請求資訊和響應資訊 。路徑請求及對應的獲取路徑有以下幾種形式:
req.query
// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"
req.query.shoe.color
// => "blue"
req.query.shoe.type
// => "converse"
req.body
// POST user[name]=tobi&user[email][email protected].com
req.body.user.name
// => "tobi"
req.body.user.email
// => "[email protected]"
// POST { "name": "tobi" }
req.body.name
// => "tobi"
req.params
// GET /user/tj
req.params.name
// => "tj"
// GET /file/javascripts/jquery.js
req.params[0]
// => "javascripts/jquery.js"
req.param(name)
// ?name=tobi
req.param('name') // => "tobi"
// POST name=tobi
req.param('name') // => "tobi"
///user/tobi for/user/:name
req.param('name') // => "tobi"
不難看出:
-
req.query
: 處理 get 請求,獲取 get 請求引數 -
req.params
: 處理 /:xxx 形式的 get 或 post 請求,獲取請求引數 -
req.body
: 處理 post 請求,獲取 post 請求體 -
req.param()
: 處理 get 和 post 請求,但查詢優先順序由高到低為 req.params→req.body→req.query
路徑規則還支援正則表示式,更多請查閱 Express 官方文件 。
新增路由規則
當我們訪問 localhost:3000 時,會顯示:
當我們訪問 localhost:3000/nswbmw 這種不存在的頁面時就會顯示:
這是因為不存在 /nswbmw
的路由規則,而且它也不是一個 public 目錄下的檔案,所以 express 返回了 404 Not Found 的錯誤。下面我們來新增這條路由規則,使得當訪問 localhost:3000/nswbmw 時,頁面顯示 hello,world!
注意:以下修改僅用於測試,看到效果後再把程式碼還原回來。
修改 index.js,在 app.get('/')
函式後新增一條路由規則:
app.get('/nswbmw', function(req, res) {
res.send('hello,world!');
});
重啟之後,訪問 localhost:3000/nswbmw 頁面顯示如下:
很簡單吧?這一節我們學習了基本的路由規則及如何新增一條路由規則,下一節我們將學習模板引擎的知識。
模版引擎
什麼是模板引擎
模板引擎(Template Engine)是一個將頁面模板和要顯示的資料結合起來生成 HTML 頁面的工具。如果說上面講到的 express 中的路由控制方法相當於 MVC 中的控制器的話,那模板引擎就相當於 MVC 中的檢視。
模板引擎的功能是將頁面模板和要顯示的資料結合起來生成 HTML 頁面。它既可以運 行在伺服器端又可以執行在客戶端,大多數時候它都在伺服器端直接被解析為 HTML,解析完成後再傳輸給客戶端,因此客戶端甚至無法判斷頁面是否是模板引擎生成的。有時候模板引擎也可以執行在客戶端,即瀏覽器中,典型的代表就是 XSLT,它以 XML 為輸入,在客戶端生成 HTML 頁面。但是由於瀏覽器相容性問題,XSLT 並不是很流行。目前的主流還是由伺服器執行模板引擎。
在 MVC 架構中,模板引擎包含在伺服器端。控制器得到使用者請求後,從模型獲取資料,呼叫模板引擎。模板引擎以資料和頁面模板為輸入,生成 HTML 頁面,然後返回給控制器,由控制器交回客戶端。
——《Node.js開發指南》
什麼是 ejs ?
ejs 是模板引擎的一種,也是我們這個教程中使用的模板引擎,因為它使用起來十分簡單,而且與 express 整合良好。
使用模板引擎
前面我們通過以下兩行程式碼設定了模板檔案的儲存位置和使用的模板引擎:
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
注意:我們通過 express -e blog
只是初始化了一個使用 ejs 模板引擎的工程而已,比如 node_modules 下添加了 ejs 模組,views 資料夾下有 index.ejs 。並不是說強制該工程只能使用 ejs 不能使用其他的模板引擎比如 jade,真正指定使用哪個模板引擎的是 app.set('view engine', 'ejs');
。
在 routes/index.js 中通過呼叫 res.render()
渲染模版,並將其產生的頁面直接返回給客戶端。它接受兩個引數,第一個是模板的名稱,即 views 目錄下的模板檔名,副檔名 .ejs 可選。第二個引數是傳遞給模板的資料物件,用於模板翻譯。
開啟 views/index.ejs ,內容如下:
index.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<linkrel='stylesheet'href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
當我們 res.render('index', { title: 'Express' });
時,模板引擎會把 <%= title %> 替換成 Express,然後把替換後的頁面顯示給使用者。
渲染後生成的頁面程式碼為:
<!DOCTYPE html>
<html>
<head>
<title>Express</title>
<linkrel='stylesheet'href='/stylesheets/style.css' />
</head>
<body>
<h1>Express</h1>
<p>Welcome to Express</p>
</body>
</html>
注意:我們通過 app.use(express.static(path.join(__dirname, 'public')))
設定了靜態檔案目錄為 public 資料夾,所以上面程式碼中的 href='/stylesheets/style.css'
就相當於 href='public/stylesheets/style.css'
。
ejs 的標籤系統非常簡單,它只有以下三種標籤:
- <% code %>:JavaScript 程式碼。
- <%= code %>:顯示替換過 HTML 特殊字元的內容。
- <%- code %>:顯示原始 HTML 內容。
注意: <%= code %>
和 <%- code %>
的區別,當變數 code 為普通字串時,兩者沒有區別。當 code 比如為 <h1>hello</h1>
這種字串時, <%= code %>
會原樣輸出 <h1>hello</h1>
,而 <%- code %>
則會顯示 H1 大的 hello 字串。
我們可以在 <% %>
內使用 JavaScript 程式碼。下面是 ejs 的官方示例:
The Data
supplies: ['mop', 'broom', 'duster']
The Template
<ul>
<%for(var i=0; i<supplies.length; i++) {%>
<li><%= supplies[i] %></li>
<% } %>
</ul>
The Result
<ul>
<li>mop</li>
<li>broom</li>
<li>duster</li>
</ul>
我們可以用上述三種標籤實現頁面模板系統能實現的任何內容。
頁面佈局
這裡我們不使用layout進行頁面佈局,而是使用更為簡單靈活的include。include 的簡單使用如下:
index.ejs
<%-include a %>
hello,world!
<%- include b %>
a.ejs
this is a.ejs
b.ejs
this is b.ejs
最終 index.ejs 會顯示:
this is a.ejs
hello,world!
this is b.ejs
這一節我們學習了模版引擎的相關知識,下一節我們正式開始學習如何從頭開始搭建一個多人部落格。
搭建多人部落格
功能分析
搭建一個簡單的具有多人註冊、登入、發表文章、登出功能的部落格。
設計目標
未登入:主頁左側導航顯示 home、login、register,右側顯示已發表的文章、發表日期及作者。
登陸後:主頁左側導航顯示 home、post、logout,右側顯示已發表的文章、發表日期及作者。
使用者登入、註冊、發表成功以及登出後都返回到主頁。
未登入:
主頁:
登入頁:
註冊頁:
登入後:
主頁:
發表頁:
注意:沒有登出頁,當點選 LOGOUT 後,退出登陸並返回到主頁。
路由規劃
我們已經把設計的構想圖貼出來了,接下來的任務就是完成路由規劃了。路由規劃,或者說控制器規劃是整個網站的骨架部分,因為它處於整個架構的樞紐位置,相當於各個介面之間的粘合劑,所以應該優先考慮。
根據構思的設計圖,我們作以下路由規劃:
/ :首頁
/login :使用者登入
/reg :使用者註冊
/post :發表文章
/logout :登出
我們要求 /login
和 /reg
只能是未登入的使用者訪問,而 /post
和 /logout
只能是已登入的使用者訪問。左側導航列表則針對已登入和未登入的使用者顯示不同的內容。
修改 index.js 如下:
module.exports = function(app) {
app.get('/', function(req, res) {
res.render('index', { title: '主頁' });
});
app.get('/reg', function(req, res) {
res.render('reg', { title: '註冊' });
});
app.post('/reg', function(req, res) {
});
app.get('/login', function(req, res) {
res.render('login', { title: '登入' });
});
app.post('/login', function(req, res) {
});
app.get('/post', function(req, res) {
res.render('post', { title: '發表' });
});
app.post('/post', function(req, res) {
});
app.get('/logout', function(req, res) {
});
};
如何針對已登入和未登入的使用者顯示不同的內容呢?或者說如何判斷使用者是否已經登陸了呢?進一步說如何記住使用者的登入狀態呢?我們通過引入會話(session)機制記錄使用者登入狀態,還要訪問資料庫來儲存和讀取使用者資訊。下一節我們將學習如何使用資料庫。
使用資料庫
MongoDB簡介
MongoDB 是一個基於分散式檔案儲存的 NoSQL(非關係型資料庫)的一種,由 C++ 語言編寫,旨在為 WEB 應用提供可擴充套件的高效能資料儲存解決方案。MongoDB 支援的資料結構非常鬆散,是類似 json 的 bjson 格式,因此可以儲存比較複雜的資料型別。MongoDB 最大的特點是他支援的查詢語言非常強大,其語法有點類似於面向物件的查詢語言,幾乎可以實現類似關係資料庫單表查詢的絕大部分功能,而且還支援對資料建立索引。
MongoDB 沒有關係型資料庫中行和表的概念,不過有類似的文件和集合的概念。文件是 MongoDB 最基本的單位,每個文件都會以唯一的 _id 標識,文件的屬性為 key/value 的鍵值對形式,文件內可以巢狀另一個文件,因此可以儲存比較複雜的資料型別。集合是許多文件的總和,一個數據庫可以有多個集合,一個集合可以有多個文件。
下面是一個 MongoDB 文件的示例:
{
"_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),
"name" : "nswbmw",
"age" : 22,
"email" : [ "[email protected]", "[email protected]" ],
"family" : {
"mother" : { ... },
"father" : { ... },
"sister : {
"name" : "miaomiao",
"