Express 是一個簡潔而靈活的 node.js Web應用框架, 提供了一系列強大特性幫助你建立各類 Web 應用,和豐富的 HTTP 工具。javascript
使用 Express 能夠快速地搭建一個完整功能的網站,它有一套健壯的特性,可用於開發單頁、多頁和混合Web應用。php
此文介紹如何使用Express搭建多人博客。css
做者: nswbmw 項目地址: N-bloghtml
Node.js: 0.10.32java
Express: 4.10.2node
MongoDB: 2.6.1jquery
express 是 Node.js 上最流行的 Web 開發框架,正如他的名字同樣,使用它咱們能夠快速的開發一個 Web 應用。咱們用 express 來搭建咱們的博客,打開命令行,輸入:git
$ npm install -g express-generator
安裝 express 命令行工具,使用它咱們能夠初始化一個 express 項目。github
在命令行中輸入:web
$ 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,表明請求信息和響應信息 。路徑請求及對應的獲取路徑有如下幾種形式:
// 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"
// 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"
// GET /user/tj req.params.name // => "tj" // GET /file/javascripts/jquery.js req.params[0] // => "javascripts/jquery.js"
// ?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 是模板引擎的一種,也是咱們這個教程中使用的模板引擎,由於它使用起來十分簡單,並且與 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 ,內容以下:
<!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 %>
和 <%- code %>
的區別,當變量 code 爲普通字符串時,二者沒有區別。當 code 好比爲 <h1>hello</h1>
這種字符串時, <%= code %>
會原樣輸出 <h1>hello</h1>
,而 <%- code %>
則會顯示 H1 大的 hello 字符串。
咱們能夠在 <% %>
內使用 JavaScript 代碼。下面是 ejs 的官方示例:
supplies: ['mop', 'broom', 'duster']
<ul> <% for(var i=0; i<supplies.length; i++) {%> <li><%= supplies[i] %></li> <% } %> </ul>
<ul> <li>mop</li> <li>broom</li> <li>duster</li> </ul>
咱們能夠用上述三種標籤實現頁面模板系統能實現的任何內容。
這裏咱們不使用layout進行頁面佈局,而是使用更爲簡單靈活的include。include 的簡單使用以下:
<%- include a %> hello,world! <%- include b %>
this is a.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 是一個基於分佈式文件存儲的 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 文件夾裏新建 blog 文件夾做爲咱們博客內容的存儲目錄。進入到 bin 目錄下:運行:
./mongod --dbpath ../blog/
以上命令的意思是:設置 blog 文件夾做爲咱們工程的存儲目錄並啓動數據庫。
數據庫雖然安裝並啓動成功了,但咱們須要鏈接數據庫後才能使用數據庫。怎麼才能在 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 即 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 讀取用戶信息。
如今,啓動應用,在瀏覽器輸入 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 就鏈接到數據庫了。如圖所示: