Express 簡介
Express 是一個簡潔而靈活的 node.js Web應用框架, 提供了一系列強大特性幫助你創建各種 Web 應用,和豐富的 HTTP 工具。
使用 Express 可以快速地搭建一個完整功能的網站,它有一套健壯的特性,可用於開發單頁、多頁和混合Web應用。
學習環境
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> <link rel='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]=tobi@learnboost.com req.body.user.name // => "tobi" req.body.user.email // => "tobi@learnboost.com" // 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> <link rel='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> <link rel='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" : [ "xxx@126.com", "xxx@gmail.com" ], "family" : { "mother" : { ... }, "father" : { ... }, "sister : { "name" : "miaomiao", "age" : 27, "email" : "xxx@163.com", "family" : { "mother" : { ... }, "father" : { ... }, "brother : { ... }, "husband" : { ... }, "son" : { ... } } } } }
更多有關 MongoDB 的知識請參閱 《mongodb權威指南》或查閱: http://www.mongodb.org/
安裝MongoDB
安裝 MongoDB 很簡單,去 官網 下載對應系統的 MongoDB 壓縮包即可。解壓后將文件夾重命名為 mongodb,並在 mongodb 文件夾里新建 blog 文件夾作為我們博客內容的存儲目錄。進入到 bin 目錄下:運行:
./mongod --dbpath ../blog/
以上命令的意思是:設置 blog 文件夾作為我們工程的存儲目錄並啟動數據庫。
連接MongoDB
數據庫雖然安裝並啟動成功了,但我們需要連接數據庫后才能使用數據庫。怎么才能在 Node.js 中使用 MongoDB 呢?我們使用官方提供的 node-mongodb-native 驅動模塊,打開 package.json,在 dependencies 中添加一行:
"mongodb": "1.4.15"
然后運行 npm install
更新依賴的模塊,稍等片刻后 mongodb 模塊就下載並安裝完成了。
接下來在工程的根目錄中創建 settings.js 文件,用於保存該博客工程的配置信息,比如數據庫的連接信息。我們將數據庫命名為 blog,因為數據庫服務器在本地,所以 settings.js 文件的內容如下:
module.exports = { cookieSecret: 'myblog', db: 'blog', host: 'localhost', port: 27017 };
其中 db 是數據庫的名稱,host 是數據庫的地址,port是數據庫的端口號,cookieSecret 用於 Cookie 加密與數據庫無關,我們留作后用。
接下來在根目錄下新建 models 文件夾,並在 models 文件夾下新建 db.js ,添加如下代碼:
var settings = require('../settings'), Db = require('mongodb').Db, Connection = require('mongodb').Connection, Server = require('mongodb').Server; module.exports = new Db(settings.db, new Server(settings.host, settings.port), {safe: true});
其中通過 new Db(settings.db, new Server(settings.host, settings.port), {safe: true});
設置數據庫名、數據庫地址和數據庫端口創建了一個數據庫連接實例,並通過 module.exports
導出該實例。這樣,我們就可以通過 require 這個文件來對數據庫進行讀寫了。
打開 app.js,在 var routes = require('./routes/index');
下添加:
var settings = require('./settings');
會話支持
會話是一種持久的網絡協議,用於完成服務器和客戶端之間的一些交互行為。會話是一個比連接粒度更大的概念, 一次會話可能包含多次連接,每次連接都被認為是會話的一次操作。在網絡應用開發中,有必要實現會話以幫助用戶交互。例如網上購物的場景,用戶瀏覽了多個頁面,購買了一些物品,這些請求在多次連接中完成。許多應用層網絡協議都是由會話支持的,如 FTP、Telnet 等,而 HTTP 協議是無狀態的,本身不支持會話,因此在沒有額外手段的幫助下,前面場景中服務器不知道用戶購買了什么。
為了在無狀態的 HTTP 協議之上實現會話,Cookie 誕生了。Cookie 是一些存儲在客戶端的信息,每次連接的時候由瀏覽器向服務器遞交,服務器也向瀏覽器發起存儲 Cookie 的請求,依靠這樣的手段服務器可以識別客戶端。我們通常意義上的 HTTP 會話功能就是這樣實現的。具體來說,瀏覽器首次向服務器發起請求時,服務器生成一個唯一標識符並發送給客戶端瀏覽器,瀏覽器將這個唯一標識符存儲在 Cookie 中,以后每次再發起請求,客戶端瀏覽器都會向服務器傳送這個唯一標識符,服務器通過這個唯一標識符來識別用戶。 對於開發者來說,我們無須關心瀏覽器端的存儲,需要關注的僅僅是如何通過這個唯一標識符來識別用戶。很多服務端腳本語言都有會話功能,如 PHP,把每個唯一標識符存儲到文件中。
——《Node.js開發指南》
express 也提供了會話中間件,默認情況下是把用戶信息存儲在內存中,但我們既然已經有了 MongoDB,不妨把會話信息存儲在數據庫中,便於持久維護。為了使用這一功能,我們需要借助 express-session 和 connect-mongo 這兩個第三方中間件,在 package.json 中添加:
"express-session": "1.9.1", "connect-mongo": "0.4.1"
注意:如報"error setting ttl index on collection : sessions"錯誤,把"mongodb"&"connect-mongo"版本號更到最新。
運行npm install安裝模塊,打開app.js,添加以下代碼:
var session = require('express-session'); var MongoStore = require('connect-mongo')(session); app.use(session({ secret: settings.cookieSecret, key: settings.db,//cookie name cookie: {maxAge: 1000 * 60 * 60 * 24 * 30},//30 days store: new MongoStore({ db: settings.db, host: settings.host, port: settings.port }) }));
使用 express-session 和 connect-mongo 模塊實現了將會化信息存儲到mongoldb中。secret 用來防止篡改 cookie,key 的值為 cookie 的名字,通過設置 cookie 的 maxAge 值設定 cookie 的生存期,這里我們設置 cookie 的生存期為 30 天,設置它的 store 參數為 MongoStore 實例,把會話信息存儲到數據庫中,以避免丟失。在后面的小節中,我們可以通過 req.session 獲取當前用戶的會話對象,獲取用戶的相關信息。
注冊和登陸
我們已經准備好了數據庫訪問和會話的相關信息,接下來我們完成用戶注冊和登錄功能。
頁面設計
首先我們來完成主頁、登錄頁和注冊頁的頁面設計。
修改 views/index.ejs 如下:
<%- include header %> 這是主頁 <%- include footer %>
在 views 文件夾下新建 header.ejs,添加如下代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Blog</title> <link rel="stylesheet" href="/stylesheets/style.css"> </head> <body> <header> <h1><%= title %></h1> </header> <nav> <span><a title="主頁" href="/">home</a></span> <span><a title="登錄" href="/login">login</a></span> <span><a title="注冊" href="/reg">register</a></span> </nav> <article>
新建 footer.ejs,添加如下代碼:
</article> </body> </html>
修改 public/stylesheets/style.css 如下:
/* inspired by http://yihui.name/cn/ */ *{padding:0;margin:0;} body{width:600px;margin:2em auto;padding:0 2em;font-size:14px;font-family:"Microsoft YaHei";} p{line-height:24px;margin:1em 0;} header{padding:.5em 0;border-bottom:1px solid #cccccc;} nav{float:left;font-family:"Microsoft YaHei";font-size:1.1em;text-transform:uppercase;margin-left:-12em;width:9em;text-align:right;} nav a{display:block;text-decoration:none;padding:.7em 1em;color:#000000;} nav a:hover{background-color:#ff0000;color:#f9f9f9;-webkit-transition:color .2s linear;} article{font-size:16px;padding-top:.5em;} article a{color:#dd0000;text-decoration:none;} article a:hover{color:#333333;text-decoration:underline;} .info{font-size:14px;}
運行 app ,主頁顯示如下:
接下來在 views 文件夾下新建 login.ejs,內容如下:
<%- include header %> <form method="post"> 用戶名:<input type="text" name="name"/><br /> 密碼: <input type="password" name="password"/><br /> <input type="submit" value="登錄"/> </form> <%- include footer %>
登錄頁面顯示如下:
在 views 文件夾下新建 reg.ejs,內容如下:
<%- include header %> <form method="post"> 用戶名: <input type="text" name="name"/><br /> 密碼: <input type="password" name="password"/><br /> 確認密碼:<input type="password" name="password-repeat"/><br /> 郵箱: <input type="email" name="email"/><br /> <input type="submit" value="注冊"/> </form> <%- include footer %>
注冊頁面顯示如下:
至此,未登錄時的主頁、注冊頁、登錄頁都已經完成。
現在,啟動我們的博客看看吧。
注意:每次我們更新代碼后,都需要手動停止並重啟應用,使用 supervisor 模塊可以解決這個問題,每當我們保存修改的文件時,supervisor 都會自動幫我們重啟應用。通過:
$ npm install -g supervisor
安裝 supervisor 。使用 supervisor 命令啟動 app.js:
$ supervisor app.js
頁面通知
接下來我們實現用戶的注冊和登陸,在這之前我們需要引入 flash 模塊來實現頁面通知(即成功與錯誤信息的顯示)的功能。
什么是 flash?
我們所說的 flash 即 connect-flash 模塊( https://github.com/jaredhanson/connect-flash ),flash 是一個在 session 中用於存儲信息的特定區域。信息寫入 flash ,下一次顯示完畢后即被清除。典型的應用是結合重定向的功能,確保信息是提供給下一個被渲染的頁面。
在 package.json 添加一行代碼:
"connect-flash": "0.1.1"
然后 npm install
安裝 connect-flash 模塊。修改 app.js ,在 var settings = require('./settings');
后添加:
var flash = require('connect-flash');
在 app.set('view engine', 'ejs');
后添加:
app.use(flash());
現在我們就可以使用 flash 功能了。
注冊響應
前面我們已經完成了注冊頁,當然現在點擊注冊是沒有效果的,因為我們還沒有實現處理 POST 請求的功能,下面就來實現它。
在 models 文件夾下新建 user.js,添加如下代碼:
var mongodb = require('./db'); function User(user) { this.name = user.name; this.password = user.password; this.email = user.email; }; module.exports = User; //存儲用戶信息 User.prototype.save = function(callback) { //要存入數據庫的用戶文檔 var user = { name: this.name, password: this.password, email: this.email }; //打開數據庫 mongodb.open(function (err, db) { if (err) { return callback(err);//錯誤,返回 err 信息 } //讀取 users 集合 db.collection('users', function (err, collection) { if (err) { mongodb.close(); return callback(err);//錯誤,返回 err 信息 } //將用戶數據插入 users 集合 collection.insert(user, { safe: true }, function (err, user) { mongodb.close(); if (err) { return callback(err);//錯誤,返回 err 信息 } callback(null, user[0]);//成功!err 為 null,並返回存儲后的用戶文檔 }); }); }); }; //讀取用戶信息 User.get = function(name, callback) { //打開數據庫 mongodb.open(function (err, db) { if (err) { return callback(err);//錯誤,返回 err 信息 } //讀取 users 集合 db.collection('users', function (err, collection) { if (err) { mongodb.close(); return callback(err);//錯誤,返回 err 信息 } //查找用戶名(name鍵)值為 name 一個文檔 collection.findOne({ name: name }, function (err, user) { mongodb.close(); if (err) { return callback(err);//失敗!返回 err 信息 } callback(null, user);//成功!返回查詢的用戶信息 }); }); }); };
我們通過 User.prototype.save
實現了用戶信息的存儲,通過 User.get
實現了用戶信息的讀取。
打開 index.js ,在最前面添加如下代碼:
var crypto = require('crypto'), User = require('../models/user.js');
通過 require()
引入 crypto 模塊和 user.js 用戶模型文件,crypto 是 Node.js 的一個核心模塊,我們用它生成散列值來加密密碼。
修改 index.js 中 app.post('/reg')
如下:
app.post('/reg', function (req, res) { var name = req.body.name, password = req.body.password, password_re = req.body['password-repeat']; //檢驗用戶兩次輸入的密碼是否一致 if (password_re != password) { req.flash('error', '兩次輸入的密碼不一致!'); return res.redirect('/reg');//返回注冊頁 } //生成密碼的 md5 值 var md5 = crypto.createHash('md5'), password = md5.update(req.body.password).digest('hex'); var newUser = new User({ name: name, password: password, email: req.body.email }); //檢查用戶名是否已經存在 User.get(newUser.name, function (err, user) { if (err) { req.flash('error', err); return res.redirect('/'); } if (user) { req.flash('error', '用戶已存在!'); return res.redirect('/reg');//返回注冊頁 } //如果不存在則新增用戶 newUser.save(function (err, user) { if (err) { req.flash('error', err); return res.redirect('/reg');//注冊失敗返回主冊頁 } req.session.user = newUser;//用戶信息存入 session req.flash('success', '注冊成功!'); res.redirect('/');//注冊成功后返回主頁 }); }); });
注意:我們把用戶信息存儲在了 session 里,以后就可以通過 req.session.user 讀取用戶信息。
- req.body : 就是 POST 請求信息解析過后的對象,例如我們要訪問 POST 來的表單內的 name="password" 域的值,只需訪問 req.body['password'] 或 req.body.password 即可。
- res.redirect : 重定向功能,實現了頁面的跳轉,更多關於 res.redirect 的信息請查閱: http://expressjs.com/api.html#res.redirect 。
- User :在前面的代碼中,我們直接使用了 User 對象。User 是一個描述數據的對象,即 MVC 架構中的模型。前面我們使用了許多視圖和控制器,這是第一次接觸到模型。與視圖和控制器不同,模型是真正與數據打交道的工具,沒有模型,網站就只是一個外殼,不能發揮真實的作用,因此它是框架中最根本的部分。
現在,啟動應用,在瀏覽器輸入 localhost:3000 注冊試試吧!注冊成功后顯示如下:
這樣我們並不知道是否注冊成功,我們查看數據庫中是否存入了用戶的信息,打開一個命令行切換到 mongodb/bin/ (保證數據庫已打開的前提下),輸入:
可以看到,用戶信息已經成功存入數據庫。
接下來我們實現當注冊成功返回主頁時,左側導航顯示 HOME 、POST 、LOGOUT ,右側顯示注冊成功! 字樣,即添加 flash 的頁面通知功能。
修改 header.ejs,將 <nav></nav>
修改如下:
<nav> <span><a title="主頁" href="/">home</a></span> <% if (user) { %> <span><a title="發表" href="/post">post</a></span> <span><a title="登出" href="/logout">logout</a></span> <% } else { %> <span><a title="登錄" href="/login">login</a></span> <span><a title="注冊" href="/reg">register</a></span> <% } %> </nav>
在 <article>
后添加如下代碼:
<% if (success) { %> <div><%= success %></div> <% } %> <% if (error) { %> <div><%= error %> </div> <% } %>
修改 index.js ,將 app.get('/')
修改如下:
app.get('/', function (req, res) { res.render('index', { title: '主頁', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString() }); });
將 app.get('reg')
修改如下:
app.get('/reg', function (req, res) { res.render('reg', { title: '注冊', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString() }); });
現在運行我們的博客,注冊成功后顯示如下:
我們通過對 session 的使用實現了對用戶狀態的檢測,再根據不同的用戶狀態顯示不同的導航信息。
簡單解釋一下流程:用戶在注冊成功后,把用戶信息存入 session ,頁面跳轉到主頁顯示 注冊成功! 的字樣。同時把 session 中的用戶信息賦給變量 user ,在渲染 index.ejs 文件時通過檢測 user 判斷用戶是否在線,根據用戶狀態的不同顯示不同的導航信息。
success: req.flash('success').toString()
的意思是將成功的信息賦值給變量 success
, error: req.flash('error').toString()
的意思是將錯誤的信息賦值給變量 error
,然后我們在渲染 ejs 模版文件時傳遞這兩個變量來進行檢測並顯示通知。
登錄與登出響應
現在我們來實現用戶登錄的功能。
打開 index.js ,將 app.post('/login')
修改如下:
app.post('/login', function (req, res) { //生成密碼的 md5 值 var md5 = crypto.createHash('md5'), password = md5.update(req.body.password).digest('hex'); //檢查用戶是否存在 User.get(req.body.name, function (err, user) { if (!user) { req.flash('error', '用戶不存在!'); return res.redirect('/login');//用戶不存在則跳轉到登錄頁 } //檢查密碼是否一致 if (user.password != password) { req.flash('error', '密碼錯誤!'); return res.redirect('/login');//密碼錯誤則跳轉到登錄頁 } //用戶名密碼都匹配后,將用戶信息存入 session req.session.user = user; req.flash('success', '登陸成功!'); res.redirect('/');//登陸成功后跳轉到主頁 }); });
將 app.get('/login')
修改如下:
app.get('/login', function (req, res) { res.render('login', { title: '登錄', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString()}); });
(這樣就不會出現 'user is not defined' 的錯誤了)
接下來我們實現登出響應。修改 app.get('/logout')
如下:
app.get('/logout', function (req, res) { req.session.user = null; req.flash('success', '登出成功!'); res.redirect('/');//登出成功后跳轉到主頁 });
注意:通過把 req.session.user 賦值 null 丟掉 session 中用戶的信息,實現用戶的退出。
登錄后頁面顯示如下:
登出后頁面顯示如下:
至此,我們實現了用戶注冊與登陸的功能,並且根據用戶登錄狀態顯示不同的導航。
頁面權限控制
我們雖然已經完成了用戶注冊與登陸的功能,但並不能阻止比如已經登陸的用戶訪問 localhost:3000/reg 頁面,讀者可親自嘗試下。為此,我們需要為頁面設置訪問權限。即注冊和登陸頁面應該阻止已登陸的用戶訪問,登出及后面我們將要實現的發表頁只對已登錄的用戶開放。如何實現頁面權限的控制呢?我們可以把用戶登錄狀態的檢查放到路由中間件中,在每個路徑前增加路由中間件,即可實現頁面權限控制。我們添加 checkNotLogin
和 checkLogin
函數來實現這個功能。
function checkLogin(req, res, next) { if (!req.session.user) { req.flash('error', '未登錄!'); res.redirect('/login'); } next(); } function checkNotLogin(req, res, next) { if (req.session.user) { req.flash('error', '已登錄!'); res.redirect('back');//返回之前的頁面 } next(); }
checkNotLogin
和 checkLogin
用來檢測是否登陸,並通過 next()
轉移控制權,檢測到未登錄則跳轉到登錄頁,檢測到已登錄則跳轉到前一個頁面。
最終 index.js 代碼如下:
var crypto = require('crypto'), User = require('../models/user.js'); module.exports = function(app) { app.get('/', function (req, res) { res.render('index', { title: '主頁', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString() }); }); app.get('/reg', checkNotLogin); app.get('/reg', function (req, res) { res.render('reg', { title: '注冊', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString() }); }); app.post('/reg', checkNotLogin); app.post('/reg', function (req, res) { var name = req.body.name, password = req.body.password, password_re = req.body['password-repeat']; if (password_re != password) { req.flash('error', '兩次輸入的密碼不一致!'); return res.redirect('/reg'); } var md5 = crypto.createHash('md5'), password = md5.update(req.body.password).digest('hex'); var newUser = new User({ name: name, password: password, email: req.body.email }); User.get(newUser.name, function (err, user) { if (err) { req.flash('error', err); return res.redirect('/'); } if (user) { req.flash('error', '用戶已存在!'); return res.redirect('/reg'); } newUser.save(function (err, user) { if (err) { req.flash('error', err); return res.redirect('/reg'); } req.session.user = user; req.flash('success', '注冊成功!'); res.redirect('/'); }); }); }); app.get('/login', checkNotLogin); app.get('/login', function (req, res) { res.render('login', { title: '登錄', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString() }); }); app.post('/login', checkNotLogin); app.post('/login', function (req, res) { var md5 = crypto.createHash('md5'), password = md5.update(req.body.password).digest('hex'); User.get(req.body.name, function (err, user) { if (!user) { req.flash('error', '用戶不存在!'); return res.redirect('/login'); } if (user.password != password) { req.flash('error', '密碼錯誤!'); return res.redirect('/login'); } req.session.user = user; req.flash('success', '登陸成功!'); res.redirect('/'); }); }); app.get('/post', checkLogin); app.get('/post', function (req, res) { res.render('post', { title: '發表', user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString() }); }); app.post('/post', checkLogin); app.post('/post', function (req, res) { }); app.get('/logout', checkLogin); app.get('/logout', function (req, res) { req.session.user = null; req.flash('success', '登出成功!'); res.redirect('/'); }); function checkLogin(req, res, next) { if (!req.session.user) { req.flash('error', '未登錄!'); res.redirect('/login'); } next(); } function checkNotLogin(req, res, next) { if (req.session.user) { req.flash('error', '已登錄!'); res.redirect('back'); } next(); } };
注意:為了維護用戶狀態和 flash 的通知功能,我們給每個 ejs 模版文件傳入了以下三個值:
user: req.session.user, success: req.flash('success').toString(), error: req.flash('error').toString()
發表文章
現在我們的博客已經具備了用戶注冊、登陸、頁面權限控制的功能,接下來我們完成博客最核心的部分——發表文章。在這一節,我們將會實現發表文章的功能,完成整個博客的設計。
頁面設計
我們先來完成發表頁的頁面設計。在 views 文件夾下新建 post.ejs ,添加如下代碼:
<%- include header %> <form method="post"> 標題:<br /> <input type="text" name="title" /><br /> 正文:<br /> <textarea name="post" rows="20" cols="100"></textarea><br /> <input type="submit" value="發表" /> </form> <%- include footer %>
文章模型
仿照用戶模型,我們將文章模型命名為 Post 對象,它擁有與 User 相似的接口,分別是 Post.get
和 Post.prototype.save
。 Post.get
的功能是從數據庫中獲取文章,可以按指定用戶獲取,也可以獲取全部的內容。 Post.prototype.save
是 Post 對象原型的方法,用來將文章保存到數據庫。
在 models 文件夾下新建 post.js ,添加如下代碼:
var mongodb = require('./db'); function Post(name, title, post) { this.name = name; this.title = title; this.post = post; } module.exports = Post; //存儲一篇文章及其相關信息 Post.prototype.save = function(callback) { var date = new Date(); //存儲各種時間格式,方便以后擴展 var time = { date: date, year : date.getFullYear(), month : date.getFullYear() + "-" + (date.getMonth() + 1), day : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(), minute : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + " " + date.getHours() + ":" + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) } //要存入數據庫的文檔 var post = { name: this.name, time: time, title: this.title, post: this.post }; //打開數據庫 mongodb.open(function (err, db) { if (err) { return callback(err); } //讀取 posts 集合 db.collection('posts', function (err, collection) { if (err) { mongodb.close(); return callback(err); } //將文檔插入 posts 集合 collection.insert(post, { safe: true }, function (err) { mongodb.close(); if (err) { return callback(err);//失敗!返回 err } callback(null);//返回 err 為 null }); }); }); }; //讀取文章及其相關信息 Post.get = function(name, callback) { //打開數據庫 mongodb.open(function (err, db) { if (err) { return callback(err); } //讀取 posts 集合 db.collection('posts', function(err, collection) { if (err) { mongodb.close(); return callback(err); } var query = {}; if (name) { query.name = name; } //根據 query 對象查詢文章 collection.find(query).sort({ time: -1 }).toArray(function (err, docs) { mongodb.close(); if (err) { return callback(err);//失敗!返回 err } callback(null, docs);//成功!以數組形式返回查詢的結果 }); }); }); };
發表響應
接下來我們給發表文章注冊響應,打開 index.js ,在 User = require('../models/user.js')
后添加一行代碼:
,Post = require('../models/post.js');
修改 app.post('/post')
如下:
app.post('/post', checkLogin); app.post('/post', function (req, res) { var currentUser = req.session.user, post = new Post(currentUser.name, req.body.title, req.body.post); post.save(function (err) { if (err) { req.flash('error', err); return res.redirect('/'); } req.flash('success', '發布成功!'); res.redirect('/');//發表成功跳轉到主頁 }); });
最后,我們修改 index.ejs ,讓主頁右側顯示發表過的文章及其相關信息。
打開 index.ejs ,修改如下:
<%- include header %> <% posts.forEach(function (post, index) { %> <p><h2><a href="#"><%= post.title %></a></h2></p> <p class="info"> 作者:<a href="#"><%= post.name %></a> | 日期:<%= post.time.minute %> </p> <p><%- post.post %></p> <% }) %> <%- include footer %>
打開 index.js ,修改 app.get('/')
如下:
app.get('/', function (req, res) { Post.get(null, function (err, posts) { if (err) { posts = []; } res.render('index', { title: '主頁', user: req.session.user, posts: posts, success: req.flash('success').toString(), error: req.flash('error').toString() }); }); });
至此,我們的博客就建成了。
啟動我們的博客,發表一篇博文,如圖所示:
此時,查看一下數據庫,如圖所示:
Tips:Robomongo 是一個基於 Shell 的跨平台開源 MongoDB 管理工具。嵌入了 JavaScript 引擎和 MongoDB mongo 。只要你會使用 mongo shell ,你就會使用 Robomongo,它提供語法高亮、自動完成、差別視圖等。
下載安裝 Robomongo后,運行我們的博客,注冊一個用戶並發表幾篇文章,初次打開 Robomongo ,點擊 Create 創建一個名為 blog (名字自定)的數據庫鏈接(默認監聽 localhost:27017),點擊 Connect 就連接到數據庫了。如圖所示: