鍍金池/ 教程/ HTML/ 第1章 一個簡單的博客
第9章 增加標(biāo)簽和標(biāo)簽頁面
番外篇之——使用 Mongoose
番外篇之——使用 Async
第4章 實現(xiàn)用戶頁面和文章頁面
第12章 增加友情鏈接
第14章 增加頭像
第7章 實現(xiàn)分頁功能
第5章 增加編輯與刪除功能
第11章 增加文章檢索功能
第3章 增加文件上傳功能
番外篇之——部署到 Heroku
第2章 使用 Markdown
第13章 增加404頁面
第16章 增加日志功能
第1章 一個簡單的博客
番外篇之——使用 Handlebars
第10章 增加pv統(tǒng)計和留言統(tǒng)計
番外篇之——使用 Passport
第15章 增加轉(zhuǎn)載功能和轉(zhuǎn)載統(tǒng)計
第8章 增加存檔頁面
番外篇之——使用 generic pool
番外篇之——使用 _id 查詢
番外篇之——使用 Disqus
番外篇之——使用 KindEditor
第6章 實現(xiàn)留言功能

第1章 一個簡單的博客

學(xué)習(xí)環(huán)境

Node.js : 0.10.32

Express : 4.10.2

MongoDB : 2.6.1

快速開始

安裝 Express

express 是 Node.js 上最流行的 Web 開發(fā)框架,正如他的名字一樣,使用它我們可以快速的開發(fā)一個 Web 應(yīng)用。我們用 express 來搭建我們的博客,打開命令行,輸入:

$ npm install -g express-generator

安裝 express 命令行工具,使用它我們可以初始化一個 express 項目。

新建一個工程

在命令行中輸入:

$ express -e blog
$ cd blog && npm install

初始化一個 express 項目并安裝所需模塊,如下圖所示:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.1.jpg" alt="" />

然后運(yùn)行:

$ DEBUG=blog node ./bin/www

啟動項目,此時命令行中會顯示 blog Express server listening on port 3000 +0ms,在瀏覽器里訪問 localhost:3000,如下圖所示:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.2.jpg" alt="" />

至此,我們用 express 初始化了一個工程項目,并指定使用 ejs 模板引擎,下一節(jié)我們講解工程的內(nèi)部結(jié)構(gòu)。

工程結(jié)構(gòu)

我們回頭看看生成的工程目錄里面都有什么,打開我們的 blog 文件夾,里面如圖所示:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.3.jpg" alt="" />

app.js:啟動文件,或者說入口文件 package.json:存儲著工程的信息及模塊依賴,當(dāng)在 dependencies 中添加依賴的模塊時,運(yùn)行 npm install,npm 會檢查當(dāng)前目錄下的 package.json,并自動安裝所有指定的模塊 node_modules:存放 package.json 中安裝的模塊,當(dāng)你在 package.json 添加依賴的模塊并安裝后,存放在這個文件夾下 public:存放 image、css、js 等文件 routes:存放路由文件 views:存放視圖文件或者說模版文件 bin:存放可執(zhí)行文件

打開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’)):設(shè)置 views 文件夾為存放視圖文件的目錄, 即存放模板文件的地方,dirname 為全局變量,存儲當(dāng)前正在執(zhí)行的腳本所在的目錄。 (3)app.set('view engine', 'ejs’):設(shè)置視圖模板引擎為 ejs。 (4)app.use(favicon(dirname + '/public/favicon.ico’)):設(shè)置/public/favicon.ico為favicon圖標(biāo)。 (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'))):設(shè)置public文件夾為存放靜態(tài)文件的目錄。 (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錯誤,并轉(zhuǎn)發(fā)到錯誤處理器。 (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
        });
    });
}

開發(fā)環(huán)境下的錯誤處理器,將錯誤信息渲染error模版并顯示到瀏覽器中。 (13)

app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

生產(chǎn)環(huán)境下的錯誤處理器,將錯誤信息渲染error模版并顯示到瀏覽器中。 (14)module.exports = app :導(dǎo)出app實例供其他模塊調(diào)用。

我們再看 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 可執(zhí)行文件。 (2)var debug = require('debug')('blog’):引入debug模塊,打印調(diào)試日志。 (3)var app = require('../app’):引入我們上面導(dǎo)出的app實例。 (4)app.set('port', process.env.PORT || 3000):設(shè)置端口號。 (5)

var server = app.listen(app.get('port'), function() {
  debug('Express server listening on port ' + server.address().port);
});

啟動工程并監(jiān)聽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請求,導(dǎo)出這個路由并在app.js中通過app.use('/', routes); 加載。這樣,當(dāng)訪問主頁時,就會調(diào)用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顯示到瀏覽器中,如上圖所示。

在這一小節(jié)我們學(xué)習(xí)了如何創(chuàng)建一個工程并啟動它,了解了工程的大體結(jié)構(gòu)和運(yùn)作流程,下一小節(jié)我們將學(xué)習(xí) express 的基本使用及路由控制。

路由控制

工作原理

routes/index.js 中有以下代碼:

router.get('/', function(req, res){
  res.render('index', { title: 'Express' });
});

這段代碼的意思是當(dāng)訪問主頁時,調(diào)用 ejs 模板引擎,來渲染 index.ejs 模版文件(即將 title 變量全部替換為字符串 Express),生成靜態(tài)頁面并顯示在瀏覽器中。

我們來作一些修改,以上代碼實現(xiàn)了路由的功能,我們當(dāng)然可以不要 routes/index.js 文件,把實現(xiàn)路由功能的代碼都放在 app.js 里,但隨著時間的推移 app.js 會變得臃腫難以維護(hù),這也違背了代碼模塊化的思想,所以我們把實現(xiàn)路由功能的代碼都放在 routes/index.js 里。官方給出的寫法是在 app.js 中實現(xiàn)了簡單的路由分配,然后再去 index.js 中找到對應(yīng)的路由函數(shù),最終實現(xiàn)路由功能。我們不妨把路由控制器和實現(xiàn)路由功能的函數(shù)都放到 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' });
  });
};

現(xiàn)在,再運(yùn)行你的 app,你會發(fā)現(xiàn)主頁毫無二致。這里我們在 routes/index.js 中通過 module.exports 導(dǎo)出了一個函數(shù)接口,在 app.js 中通過 require 加載了 index.js 然后通過 routes(app) 調(diào)用了 index.js 導(dǎo)出的函數(shù)。

路由規(guī)則

express 封裝了多種 http 請求方式,我們主要只使用 get 和 post 兩種,即 app.get() 和 app.post() 。

app.get() 和 app.post() 的第一個參數(shù)都為請求的路徑,第二個參數(shù)為處理請求的回調(diào)函數(shù),回調(diào)函數(shù)有兩個參數(shù)分別是 req 和 res,代表請求信息和響應(yīng)信息 。路徑請求及對應(yīng)的獲取路徑有以下幾種形式:

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 請求參數(shù)
  • req.params: 處理 /:xxx 形式的 get 或 post 請求,獲取請求參數(shù)
  • req.body: 處理 post 請求,獲取 post 請求體
  • req.param(): 處理 get 和 post 請求,但查找優(yōu)先級由高到低為 req.params→req.body→req.query

路徑規(guī)則還支持正則表達(dá)式,更多請查閱 Express 官方文檔

添加路由規(guī)則

當(dāng)我們訪問 localhost:3000 時,會顯示:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.4.jpg" alt="" />

當(dāng)我們訪問 localhost:3000/nswbmw 這種不存在的頁面時就會顯示:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.5.jpg" alt="" />

這是因為不存在 /nswbmw 的路由規(guī)則,而且它也不是一個 public 目錄下的文件,所以 express 返回了 404 Not Found 的錯誤。下面我們來添加這條路由規(guī)則,使得當(dāng)訪問 localhost:3000/nswbmw 時,頁面顯示 hello,world!

注意:以下修改僅用于測試,看到效果后再把代碼還原回來。

修改 index.js,在 app.get('/') 函數(shù)后添加一條路由規(guī)則:

app.get('/nswbmw', function (req, res) {
  res.send('hello,world!');
});

重啟之后,訪問 localhost:3000/nswbmw 頁面顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.6.jpg" alt="" />

很簡單吧?這一節(jié)我們學(xué)習(xí)了基本的路由規(guī)則及如何添加一條路由規(guī)則,下一節(jié)我們將學(xué)習(xí)模板引擎的知識。

模版引擎

什么是模板引擎

模板引擎(Template Engine)是一個將頁面模板和要顯示的數(shù)據(jù)結(jié)合起來生成 HTML 頁面的工具。 如果說上面講到的 express 中的路由控制方法相當(dāng)于 MVC 中的控制器的話,那模板引擎就相當(dāng)于 MVC 中的視圖。

模板引擎的功能是將頁面模板和要顯示的數(shù)據(jù)結(jié)合起來生成 HTML 頁面。它既可以運(yùn) 行在服務(wù)器端又可以運(yùn)行在客戶端,大多數(shù)時候它都在服務(wù)器端直接被解析為 HTML,解析完成后再傳輸給客戶端,因此客戶端甚至無法判斷頁面是否是模板引擎生成的。有時候模板引擎也可以運(yùn)行在客戶端,即瀏覽器中,典型的代表就是 XSLT,它以 XML 為輸入,在客戶端生成 HTML 頁面。但是由于瀏覽器兼容性問題,XSLT 并不是很流行。目前的主流還是由服務(wù)器運(yùn)行模板引擎。 在 MVC 架構(gòu)中,模板引擎包含在服務(wù)器端??刂破鞯玫接脩粽埱蠛螅瑥哪P瞳@取數(shù)據(jù),調(diào)用模板引擎。模板引擎以數(shù)據(jù)和頁面模板為輸入,生成 HTML 頁面,然后返回給控制器,由控制器交回客戶端。 ——《Node.js開發(fā)指南》

什么是 ejs ?

ejs 是模板引擎的一種,也是我們這個教程中使用的模板引擎,因為它使用起來十分簡單,而且與 express 集成良好。

使用模板引擎

前面我們通過以下兩行代碼設(shè)置了模板文件的存儲位置和使用的模板引擎:

app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

注意:我們通過 express -e blog 只是初始化了一個使用 ejs 模板引擎的工程而已,比如 node_modules 下添加了 ejs 模塊,views 文件夾下有 index.ejs 。并不是說強(qiáng)制該工程只能使用 ejs 不能使用其他的模板引擎比如 jade,真正指定使用哪個模板引擎的是 app.set('view engine', 'ejs'); 。

在 routes/index.js 中通過調(diào)用 res.render() 渲染模版,并將其產(chǎn)生的頁面直接返回給客戶端。它接受兩個參數(shù),第一個是模板的名稱,即 views 目錄下的模板文件名,擴(kuò)展名 .ejs 可選。第二個參數(shù)是傳遞給模板的數(shù)據(jù)對象,用于模板翻譯。

打開 views/index.ejs ,內(nèi)容如下:

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>

當(dāng)我們 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'))) 設(shè)置了靜態(tài)文件目錄為 public 文件夾,所以上面代碼中的 href='/stylesheets/style.css' 就相當(dāng)于 href='public/stylesheets/style.css' 。

ejs 的標(biāo)簽系統(tǒng)非常簡單,它只有以下三種標(biāo)簽:

  • <% code %>:JavaScript 代碼。
  • <%= code %>:顯示替換過 HTML 特殊字符的內(nèi)容。
  • <%- code %>:顯示原始 HTML 內(nèi)容。

注意: <%= code %> 和 <%- code %> 的區(qū)別,當(dāng)變量 code 為普通字符串時,兩者沒有區(qū)別。當(dāng) code 比如為

hello

這種字符串時,<%= code %> 會原樣輸出

hello

,而 <%- code %> 則會顯示 H1 大的 hello 字符串。

我們可以在 <% %> 內(nèi)使用 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>

我們可以用上述三種標(biāo)簽實現(xiàn)頁面模板系統(tǒng)能實現(xiàn)的任何內(nèi)容。

頁面布局

這里我們不使用layout進(jìn)行頁面布局,而是使用更為簡單靈活的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

這一節(jié)我們學(xué)習(xí)了模版引擎的相關(guān)知識,下一節(jié)我們正式開始學(xué)習(xí)如何從頭開始搭建一個多人博客。

搭建多人博客

功能分析

搭建一個簡單的具有多人注冊、登錄、發(fā)表文章、登出功能的博客。

設(shè)計目標(biāo)

未登錄:主頁左側(cè)導(dǎo)航顯示 home、login、register,右側(cè)顯示已發(fā)表的文章、發(fā)表日期及作者。 登陸后:主頁左側(cè)導(dǎo)航顯示 home、post、logout,右側(cè)顯示已發(fā)表的文章、發(fā)表日期及作者。 用戶登錄、注冊、發(fā)表成功以及登出后都返回到主頁。

未登錄:

主頁:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.7.jpg" alt="" />

登錄頁:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.8.jpg" alt="" />

注冊頁:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.9.jpg" alt="" />

登錄后:

主頁:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.10.jpg" alt="" />

發(fā)表頁:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.11.jpg" alt="" />

注意:沒有登出頁,當(dāng)點(diǎn)擊 LOGOUT 后,退出登陸并返回到主頁。

路由規(guī)劃

我們已經(jīng)把設(shè)計的構(gòu)想圖貼出來了,接下來的任務(wù)就是完成路由規(guī)劃了。路由規(guī)劃,或者說控制器規(guī)劃是整個網(wǎng)站的骨架部分,因為它處于整個架構(gòu)的樞紐位置,相當(dāng)于各個接口之間的粘合劑,所以應(yīng)該優(yōu)先考慮。

根據(jù)構(gòu)思的設(shè)計圖,我們作以下路由規(guī)劃:

/ :首頁
/login :用戶登錄
/reg :用戶注冊
/post :發(fā)表文章
/logout :登出

我們要求 /login 和 /reg 只能是未登錄的用戶訪問,而 /post 和 /logout 只能是已登錄的用戶訪問。左側(cè)導(dǎo)航列表則針對已登錄和未登錄的用戶顯示不同的內(nèi)容。

修改 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: '發(fā)表' });
  });
  app.post('/post', function (req, res) {
  });
  app.get('/logout', function (req, res) {
  });
};

如何針對已登錄和未登錄的用戶顯示不同的內(nèi)容呢?或者說如何判斷用戶是否已經(jīng)登陸了呢?進(jìn)一步說如何記住用戶的登錄狀態(tài)呢?我們通過引入會話(session)機(jī)制記錄用戶登錄狀態(tài),還要訪問數(shù)據(jù)庫來保存和讀取用戶信息。下一節(jié)我們將學(xué)習(xí)如何使用數(shù)據(jù)庫。

使用數(shù)據(jù)庫

MongoDB簡介

MongoDB 是一個基于分布式文件存儲的 NoSQL(非關(guān)系型數(shù)據(jù)庫)的一種,由 C++ 語言編寫,旨在為 WEB 應(yīng)用提供可擴(kuò)展的高性能數(shù)據(jù)存儲解決方案。MongoDB 支持的數(shù)據(jù)結(jié)構(gòu)非常松散,是類似 json 的 bjson 格式,因此可以存儲比較復(fù)雜的數(shù)據(jù)類型。MongoDB 最大的特點(diǎn)是他支持的查詢語言非常強(qiáng)大,其語法有點(diǎn)類似于面向?qū)ο蟮牟樵冋Z言,幾乎可以實現(xiàn)類似關(guān)系數(shù)據(jù)庫單表查詢的絕大部分功能,而且還支持對數(shù)據(jù)建立索引。

MongoDB 沒有關(guān)系型數(shù)據(jù)庫中行和表的概念,不過有類似的文檔和集合的概念。文檔是 MongoDB 最基本的單位,每個文檔都會以唯一的 _id 標(biāo)識,文檔的屬性為 key/value 的鍵值對形式,文檔內(nèi)可以嵌套另一個文檔,因此可以存儲比較復(fù)雜的數(shù)據(jù)類型。集合是許多文檔的總和,一個數(shù)據(jù)庫可以有多個集合,一個集合可以有多個文檔。

下面是一個 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" : { ... }
      }
    }
  }
}

更多有關(guān) MongoDB 的知識請參閱 《mongodb權(quán)威指南》或查閱:http://www.mongodb.org/

安裝MongoDB

安裝 MongoDB 很簡單,去官網(wǎng)下載對應(yīng)系統(tǒng)的 MongoDB 壓縮包即可。解壓后將文件夾重命名為 mongodb,并在 mongodb 文件夾里新建 blog 文件夾作為我們博客內(nèi)容的存儲目錄。進(jìn)入到 bin 目錄下:運(yùn)行:

mongod --dbpath ../blog/

以上命令的意思是:設(shè)置 blog 文件夾作為我們工程的存儲目錄并啟動數(shù)據(jù)庫。

連接MongoDB

數(shù)據(jù)庫雖然安裝并啟動成功了,但我們需要連接數(shù)據(jù)庫后才能使用數(shù)據(jù)庫。怎么才能在 Node.js 中使用 MongoDB 呢?我們使用官方提供的 node-mongodb-native 驅(qū)動模塊,打開 package.json,在 dependencies 中添加一行:

"mongodb": "1.4.15"

然后運(yùn)行 npm install 更新依賴的模塊,稍等片刻后 mongodb 模塊就下載并安裝完成了。

接下來在工程的根目錄中創(chuàng)建 settings.js 文件,用于保存該博客工程的配置信息,比如數(shù)據(jù)庫的連接信息。我們將數(shù)據(jù)庫命名為 blog,因為數(shù)據(jù)庫服務(wù)器在本地,所以 settings.js 文件的內(nèi)容如下:

module.exports = { 
  cookieSecret: 'myblog', 
  db: 'blog', 
  host: 'localhost',
  port: 27017
}; 

其中 db 是數(shù)據(jù)庫的名稱,host 是數(shù)據(jù)庫的地址,port是數(shù)據(jù)庫的端口號,cookieSecret 用于 Cookie 加密與數(shù)據(jù)庫無關(guān),我們留作后用。

接下來在根目錄下新建 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}); 設(shè)置數(shù)據(jù)庫名、數(shù)據(jù)庫地址和數(shù)據(jù)庫端口創(chuàng)建了一個數(shù)據(jù)庫連接實例,并通過 module.exports 導(dǎo)出該實例。這樣,我們就可以通過 require 這個文件來對數(shù)據(jù)庫進(jìn)行讀寫了。

打開 app.js,在 var routes = require('./routes/index'); 下添加:

var settings = require('./settings');

會話支持

會話是一種持久的網(wǎng)絡(luò)協(xié)議,用于完成服務(wù)器和客戶端之間的一些交互行為。會話是一個比連接粒度更大的概念, 一次會話可能包含多次連接,每次連接都被認(rèn)為是會話的一次操作。在網(wǎng)絡(luò)應(yīng)用開發(fā)中,有必要實現(xiàn)會話以幫助用戶交互。例如網(wǎng)上購物的場景,用戶瀏覽了多個頁面,購買了一些物品,這些請求在多次連接中完成。許多應(yīng)用層網(wǎng)絡(luò)協(xié)議都是由會話支持的,如 FTP、Telnet 等,而 HTTP 協(xié)議是無狀態(tài)的,本身不支持會話,因此在沒有額外手段的幫助下,前面場景中服務(wù)器不知道用戶購買了什么。

為了在無狀態(tài)的 HTTP 協(xié)議之上實現(xiàn)會話,Cookie 誕生了。Cookie 是一些存儲在客戶端的信息,每次連接的時候由瀏覽器向服務(wù)器遞交,服務(wù)器也向瀏覽器發(fā)起存儲 Cookie 的請求,依靠這樣的手段服務(wù)器可以識別客戶端。我們通常意義上的 HTTP 會話功能就是這樣實現(xiàn)的。具體來說,瀏覽器首次向服務(wù)器發(fā)起請求時,服務(wù)器生成一個唯一標(biāo)識符并發(fā)送給客戶端瀏覽器,瀏覽器將這個唯一標(biāo)識符存儲在 Cookie 中,以后每次再發(fā)起請求,客戶端瀏覽器都會向服務(wù)器傳送這個唯一標(biāo)識符,服務(wù)器通過這個唯一標(biāo)識符來識別用戶。 對于開發(fā)者來說,我們無須關(guān)心瀏覽器端的存儲,需要關(guān)注的僅僅是如何通過這個唯一標(biāo)識符來識別用戶。很多服務(wù)端腳本語言都有會話功能,如 PHP,把每個唯一標(biāo)識符存儲到文件中。

——《Node.js開發(fā)指南》

express 也提供了會話中間件,默認(rèn)情況下是把用戶信息存儲在內(nèi)存中,但我們既然已經(jīng)有了 MongoDB,不妨把會話信息存儲在數(shù)據(jù)庫中,便于持久維護(hù)。為了使用這一功能,我們需要借助 express-session 和 connect-mongo 這兩個第三方中間件,在 package.json 中添加:

"express-session": "1.9.1",
"connect-mongo": "0.4.1"

運(yùn)行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 模塊實現(xiàn)了將會化信息存儲到mongoldb中。secret 用來防止篡改 cookie,key 的值為 cookie 的名字,通過設(shè)置 cookie 的 maxAge 值設(shè)定 cookie 的生存期,這里我們設(shè)置 cookie 的生存期為 30 天,設(shè)置它的 store 參數(shù)為 MongoStore 實例,把會話信息存儲到數(shù)據(jù)庫中,以避免丟失。在后面的小節(jié)中,我們可以通過 req.session 獲取當(dāng)前用戶的會話對象,獲取用戶的相關(guān)信息。

注冊和登陸

我們已經(jīng)準(zhǔn)備好了數(shù)據(jù)庫訪問和會話的相關(guān)信息,接下來我們完成用戶注冊和登錄功能。

頁面設(shè)計

首先我們來完成主頁、登錄頁和注冊頁的頁面設(shè)計。

修改 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;}

運(yùn)行 app ,主頁顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.12.jpg" alt="" />

接下來在 views 文件夾下新建 login.ejs,內(nèi)容如下:

<%- include header %>
<form method="post">
  用戶名:<input type="text" name="name"/><br />
  密碼:  <input type="password" name="password"/><br />
         <input type="submit" value="登錄"/>
</form>
<%- include footer %>

登錄頁面顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.13.jpg" alt="" />

在 views 文件夾下新建 reg.ejs,內(nèi)容如下:

<%- include header %>
<form method="post">
  用戶名:  <input type="text" name="name"/><br />
  密碼:    <input type="password" name="password"/><br />
  確認(rèn)密碼:<input type="password" name="password-repeat"/><br />
  郵箱:    <input type="email" name="email"/><br />
           <input type="submit" value="注冊"/>
</form>
<%- include footer %>

注冊頁面顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.14.jpg" alt="" />

至此,未登錄時的主頁、注冊頁、登錄頁都已經(jīng)完成。

現(xiàn)在,啟動我們的博客看看吧。

注意:每次我們更新代碼后,都需要手動停止并重啟應(yīng)用,使用 supervisor 模塊可以解決這個問題,每當(dāng)我們保存修改的文件時,supervisor 都會自動幫我們重啟應(yīng)用。通過:

$ npm install -g supervisor

安裝 supervisor 。使用 supervisor 命令啟動 app.js:

$ supervisor app

頁面通知

接下來我們實現(xiàn)用戶的注冊和登陸,在這之前我們需要引入 flash 模塊來實現(xiàn)頁面通知(即成功與錯誤信息的顯示)的功能。

什么是 flash?

我們所說的 flash 即 connect-flash 模塊https://github.com/jaredhanson/connect-flash,flash 是一個在 session 中用于存儲信息的特定區(qū)域。信息寫入 flash ,下一次顯示完畢后即被清除。典型的應(yīng)用是結(jié)合重定向的功能,確保信息是提供給下一個被渲染的頁面。

在 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());

現(xiàn)在我們就可以使用 flash 功能了。

注冊響應(yīng)

前面我們已經(jīng)完成了注冊頁,當(dāng)然現(xiàn)在點(diǎn)擊注冊是沒有效果的,因為我們還沒有實現(xiàn)處理 POST 請求的功能,下面就來實現(xiàn)它。

在 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) {
  //要存入數(shù)據(jù)庫的用戶文檔
  var user = {
      name: this.name,
      password: this.password,
      email: this.email
  };
  //打開數(shù)據(jù)庫
  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 信息
      }
      //將用戶數(shù)據(jù)插入 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) {
  //打開數(shù)據(jù)庫
  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 實現(xiàn)了用戶信息的存儲,通過 User.get 實現(xiàn)了用戶信息的讀取。

打開 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
  });
  //檢查用戶名是否已經(jīng)存在 
  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;//用戶信息存入 session
      req.flash('success', '注冊成功!');
      res.redirect('/');//注冊成功后返回主頁
    });
  });
});

注意:我們把用戶信息存儲在了 session 里,以后就可以通過 req.session.user 讀取用戶信息。

  • req.body: 就是 POST 請求信息解析過后的對象,例如我們要訪問 POST 來的表單內(nèi)的 name="password" 域的值,只需訪問 req.body['password'] 或 req.body.password 即可。
  • res.redirect: 重定向功能,實現(xiàn)了頁面的跳轉(zhuǎn),更多關(guān)于 res.redirect 的信息請查閱:http://expressjs.com/api.html#res.redirect 。
  • User:在前面的代碼中,我們直接使用了 User 對象。User 是一個描述數(shù)據(jù)的對象,即 MVC 架構(gòu)中的模型。前面我們使用了許多視圖和控制器,這是第一次接觸到模型。與視圖和控制器不同,模型是真正與數(shù)據(jù)打交道的工具,沒有模型,網(wǎng)站就只是一個外殼,不能發(fā)揮真實的作用,因此它是框架中最根本的部分。 現(xiàn)在,啟動應(yīng)用,在瀏覽器輸入 localhost:3000 注冊試試吧!注冊成功后顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.15.jpg" alt="" />

這樣我們并不知道是否注冊成功,我們查看數(shù)據(jù)庫中是否存入了用戶的信息,打開一個命令行切換到 mongodb/bin/ (保證數(shù)據(jù)庫已打開的前提下),輸入:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.16.jpg" alt="" />

可以看到,用戶信息已經(jīng)成功存入數(shù)據(jù)庫。

接下來我們實現(xiàn)當(dāng)注冊成功返回主頁時,左側(cè)導(dǎo)航顯示 HOME 、POST 、LOGOUT ,右側(cè)顯示 注冊成功! 字樣,即添加 flash 的頁面通知功能。

修改 header.ejs,將

修改如下:

<nav>
<span><a title="主頁" href="/">home</a></span>
<% if (user) { %>
  <span><a title="發(fā)表" 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>

后添加如下代碼:

<% 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()
  });
});

現(xiàn)在運(yùn)行我們的博客,注冊成功后顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.17.jpg" alt="" />

我們通過對 session 的使用實現(xiàn)了對用戶狀態(tài)的檢測,再根據(jù)不同的用戶狀態(tài)顯示不同的導(dǎo)航信息。 簡單解釋一下流程:用戶在注冊成功后,把用戶信息存入 session ,頁面跳轉(zhuǎn)到主頁顯示 注冊成功! 的字樣。同時把 session 中的用戶信息賦給變量 user ,在渲染 index.ejs 文件時通過檢測 user 判斷用戶是否在線,根據(jù)用戶狀態(tài)的不同顯示不同的導(dǎo)航信息。

success: req.flash('success').toString() 的意思是將成功的信息賦值給變量 success, error: req.flash('error').toString() 的意思是將錯誤的信息賦值給變量 error ,然后我們在渲染 ejs 模版文件時傳遞這兩個變量來進(jìn)行檢測并顯示通知。

登錄與登出響應(yīng)

現(xiàn)在我們來實現(xiàn)用戶登錄的功能。

打開 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');//用戶不存在則跳轉(zhuǎn)到登錄頁
    }
    //檢查密碼是否一致
    if (user.password != password) {
      req.flash('error', '密碼錯誤!'); 
      return res.redirect('/login');//密碼錯誤則跳轉(zhuǎn)到登錄頁
    }
    //用戶名密碼都匹配后,將用戶信息存入 session
    req.session.user = user;
    req.flash('success', '登陸成功!');
    res.redirect('/');//登陸成功后跳轉(zhuǎn)到主頁
  });
});

將 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()});
});

(這樣就不會出現(xiàn) 'user is not defined' 的錯誤了)

接下來我們實現(xiàn)登出響應(yīng)。修改 app.get('/logout') 如下:

app.get('/logout', function (req, res) {
  req.session.user = null;
  req.flash('success', '登出成功!');
  res.redirect('/');//登出成功后跳轉(zhuǎn)到主頁
});

注意:通過把 req.session.user 賦值 null 丟掉 session 中用戶的信息,實現(xiàn)用戶的退出。

登錄后頁面顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.18.jpg" alt="" />

登出后頁面顯示如下:

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/images/1.19.jpg" alt="" />

至此,我們實現(xiàn)了用戶注冊與登陸的功能,并且根據(jù)用戶登錄狀態(tài)顯示不同的導(dǎo)航。

頁面權(quán)限控制

我們雖然已經(jīng)完成了用戶注冊與登陸的功能,但并不能阻止比如已經(jīng)登陸的用戶訪問 localhost:3000/reg 頁面,讀者可親自嘗試下。為此,我們需要為頁面設(shè)置訪問權(quán)限。即注冊和登陸頁面應(yīng)該阻止已登陸的用戶訪問,登出及后面我們將要實現(xiàn)的發(fā)表頁只對已登錄的用戶開放。如何實現(xiàn)頁面權(quán)限的控制呢?我們可以把用戶登錄狀態(tài)的檢查放到路由中間件中,在每個路徑前增加路由中間件,即可實現(xiàn)頁面權(quán)限控制。我們添加 checkNotLogin 和 checkLogin 函數(shù)來實現(xiàn)這個功能。

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() 轉(zhuǎn)移控制權(quán),檢測到未登錄則跳轉(zhuǎn)到登錄頁,檢測到已登錄則跳轉(zhuǎn)到前一個頁面。

最終 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('/');