nodeJS

目錄[-]javascript

學習環境

node.js: 0.10.7+

快速開始

安裝Express

npm install -g express

咱們須要用全局模式安裝 express,由於只有這樣咱們才能在命令行中使用它。目前 express 最新版本爲 express 3.2.4。

新建一個工程

從此的學習把 D:\blog 文件夾做爲咱們的工程目錄。windows 下打開 cmd 切換到 D 盤,輸入 express -e ejs blog (注意 express 3.* 中安裝 ejs 再也不是 -t 而是 -e,能夠輸入 express -h 查看),而後輸入 cd blog&npm install 安裝所需模塊,以下圖所示:

安裝完成後輸入 node app,此時命令行中會顯示 Express server listening on port 3000,在瀏覽器裏輸入 localhost:3000,以下所示:

咱們用 express 初始化了一個工程,並指定使用 ejs 模板引擎,下一節咱們講解工程的內部結構。

工程結構

咱們回頭看看生成的工程目錄裏面有什麼,打開 D:\blog,裏面如圖所示:

app.js:啓動文件。 

package.json:存儲着工程的信息及所需的依賴模塊,當在 dependencies 中添加依賴時,運行npm install,會檢查當前目錄下的 package.json,並自動安裝全部指定的依賴模塊。 

node_modules:存放 package.json 中安裝的模塊,當你在 package.json 添加依賴的模塊並安裝後,默認存放在這個文件夾下 

public:存放 image、css、js 等文件 

routes:存放路由文件 

views:存放模版文件

打開 app.js,讓咱們看看裏面究竟有什麼東西:

/**
 * Module dependencies.
 */

var express = require('express')
  , routes = require('./routes')
  , user = require('./routes/user')
  , http = require('http')
  , path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

在 node.js 中模塊分爲核心模塊和文件模塊兩種,核心模塊是經過 require('xxxx') 導入的,文件模塊是以 require('/xxxx') 或 require('./xxxx')、require('../xxxx') 形式導入的;核心模塊是用c/c++編譯的二進制模塊,而文件模塊是後綴爲.js、.json、.node 的文件,在 node.js 中一個文件/文件夾也能夠稱之爲一個模塊。更多關於模塊及模塊加載順序的信息請查閱官網:http://nodejs.org/api/all.html#all_modules 

這裏導入了 express、http、path 核心模塊,routes 文件夾下的 index.js 和 user.js 文件模塊。

由於 express 框架是依賴 connect 框架(Node的一箇中間件框架)建立而成的,可查閱 connect 文檔:http://www.senchalabs.org/connect/和 express 官方文檔:http://expressjs.com/api.html瞭解更多內容。 

app.set(name, value):設置 name 的值爲 value 

app.set('port', process.env.PORT || 3000):設置端口爲 process.env.PORT 或 3000 

app.set('views', __dirname + '/views'):設置 views 文件夾爲視圖文件的目錄,存放模板文件,__dirname 爲全局變量,存儲着當前正在執行腳本所在的目錄名。 

app.set('view engine', 'ejs'):設置視圖模版引擎爲 ejs

app.use([path], function):使用中間件 function,可選參數path默認爲」/」 

app.use(express.favicon()):connect 內建的中間件,使用默認的 favicon 圖標,若是想使用本身的圖標,需改成 app.use(express.favicon(__dirname + '/public/images/favicon.ico')); 這裏咱們把自定義的 favicon.ico 放到了 public/images 文件夾下。 

app.use(express.logger('dev')):connect 內建的中間件,在開發環境下使用,在終端顯示簡單的不一樣顏色的日誌,好比在啓動 app.js 後訪問 localhost:3000,終端會輸出:

Express server listening on port 3000
GET / 200 21ms - 206b
GET /stylesheets/style.css 304 4ms

數字200顯示爲綠色,304顯示爲藍色。假如你去掉這一行代碼,無論你怎麼刷新網頁,終端都只有一行 Express server listening on port 3000。 

app.use(express.bodyParser()):connect 內建的中間件,用來解析請求體,支持 application/json, 

application/x-www-form-urlencoded, 和 multipart/form-data。 

app.use(express.methodOverride()):connect 內建的中間件,能夠協助處理 POST 請求,假裝 PUT、DELETE 和其餘 HTTP 方法。 

app.use(app.router):設置應用的路由(可選),詳細請參考:http://stackoverflow.com/questions/12695591/node-js-express-js-how-does-app-router-work 

app.use(express.static(path.join(__dirname, 'public'))):connect 內建的中間件,設置根目錄下的 public 文件夾爲靜態文件服務器,存放 image、css、js 文件於此。 

if ('development' == app.get('env')) {app.use(express.errorHandler());}:開發環境下的錯誤處理,輸出錯誤信息。

app.get('/', routes.index):路由控制器,若是用戶訪問」 / 「路徑,則由 routes.index 來控制,routes/index.js 內容以下:

exports.index = function(req, res){
  res.render('index', { title: 'Express' });
};

經過 exports.index 導出 index 函數接口,app.get('/', routes.index) 至關於:

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

res.render('index', { title: 'Express' }):調用 ejs 模板引擎解析 views/index.ejs(咱們以前經過 app.set('views', __dirname + '/views') 設置了模版文件默認存儲在 views 下),並傳入一個對象做爲參數,這個對象只有一個屬性 title: 'Express',即用字符串 Express 替換 views/index.ejs 中全部 title 變量,後面咱們將會了解更多關於模板引的內容。

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

這段代碼的意思是建立服務器並監聽3000端口,成功後在命令行中顯示 Express server listening on port 3000,而後咱們就能夠經過在瀏覽器輸入 localhost:3000 來訪問了。

這一小節咱們學習瞭如何建立一個工程並啓動它,瞭解了工程的大致結構,下一節咱們將學習 Express 的基本使用及路由控制。

路由控制

工做原理

上面提到過 app.js 中 app.get('/', routes.index) 能夠用如下代碼取代:

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

這段代碼的意思是當訪問主頁時,調用 ejs 模板引擎,傳入 title 變量的值爲字符串 Express,來渲染 index.ejs 模版文件,生成靜態頁面並顯示在瀏覽器裏。

咱們來做一些修改,以上代碼實現了路由的功能,咱們固然能夠不要 routes/index.js 文件,把實現路由功能的代碼都放在 app.js 裏,但隨着時間的推移 app.js 會變得難以維護,這也違背了代碼模塊化的思想,因此咱們把實現路由功能的代碼都放在 routes/index.js 裏。官方給出的寫法是在 app.js 中實現了簡單的路由分配,而後再去 index.js 中找到對應的路由函數,最終實現路由功能。咱們不妨把路由控制器和實現路由功能的函數都放到 index.js 裏,app.js 中只有一個總的路由接口。 

打開 app.js,刪除 , user = require('./routes/user') (咱們這裏用不到 routes/user.js,同時刪除這個文件)和

app.get('/', routes.index);
app.get('/users', user.list);

在 app.js 最後添加:

routes(app);

修改 index.js 以下:

module.exports = function(app){
  app.get('/',function(req,res){
    res.render('index', { title: 'Express' });
  });
};

如今,再運行你的 app,你會發現主頁毫無二致。這兩種寫法的區別就比如: 

你的朋友結婚了,你收到請帖要赴宴。到了酒店門口被總管給攔住了。官方的寫法是總管看了看請帖而後給你指了朋友團的地方,而後你過去坐下。咱的寫法是總管看了看請帖簡單確認了下你是被邀請的人,而後你進去本身找到朋友團的地方坐下。

路由規則

express 封裝了多種 http 請求方式,但咱們這裏主要只使用 get 和 post 兩種。 

get 和 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 請求 

req.params 處理 /:xxx 形式的 get 請求 

req.body 處理 post 請求 

req.param(name) 能夠處理 get 和 post 請求,但查找優先級由高到低爲 req.params→req.body→req.query 

路徑規則還支持正則表達式,更多請查閱:http://expressjs.com/api.html

添加路由規則

當咱們訪問 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');

在 routes/index.js 中經過調用 res.render 渲染模版,並將其產生的頁面直接返回給客戶端。它接受兩個參數,第一個是模板的名稱,即 views 目錄下的模板文件名,擴展名 .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>

注意:咱們設置了靜態文靜目錄爲 public(app.use(express.static(path.join(__dirname, 'public')))),因此上面代碼中的 href='/stylesheets/style.css' 就至關於 href='public/stylesheets/style.css' 。

ejs 的標籤系統很是簡單,它只有如下3種標籤。

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

注意: <%= code %> 和 <%- code %> 的區別,當變量 code 爲字符串時,二者沒有區別;當 code 爲好比<h1>hello</h1> 時,<%= code %> 會原樣輸出 <h1>hello</h1>,而<%- code %> 則會輸出H1大的 hello。

EJS 的官方示例:

The Data

{ title:    'Cleaning Supplies',
  supplies: ['mop', 'broom', 'duster'] }

The Template

<ul>
<% for(var i=0; i<supplies.length; i++) {%>
   <li><%= supplies[i] %></li>
<% } %>
</ul>

The Result

  • mop

  • broom

  • duster

咱們能夠用上述三種方式實現頁面模板系統能實現的任何內容。

頁面佈局

Express 3.* 中咱們再也不使用 layout.ejs 進行頁面佈局,轉而使用 include 來替代。include 的簡單使用以下:

a.ejs

<%- include b %>
hello,world!
<%- include c %>

b.ejs

this is b

c.ejs

this is c

最終 a.ejs 會顯示:

this is b
hello,world!
this is c

這一節咱們學習了模版引擎的相關知識,下一節咱們正式開始學習如何從頭開始搭建一個多人博客。

搭建多人博客

功能分析

做爲入門教程,咱們要搭建的博客具備簡單的容許多人註冊、登陸、發表文章、登出的功能。

設計目標

未登陸:主頁左側導航顯示 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){
  });
};

如何針對已登陸和未登陸的用戶顯示不一樣的內容呢?或者說如何判斷用戶是否已經登錄了呢?進一步說如何記住用戶的登陸狀態呢?咱們經過引入會話機制,來記錄用戶登陸狀態,還要訪問數據庫來保存和讀取用戶信息。下一節咱們將學習如何使用數據庫。

使用數據庫

MongoDB簡介

MongoDB 是一個對象數據庫,它沒有表、行等概念,也沒有固定的模式和結構,全部的數據以文檔的形式存儲。所謂文檔就是一個關聯數組式的對象,它的內部由屬性組成,一個屬性對應的值多是一個數、字符串、日期、數組,甚至是一個嵌套的文檔。

——《Node.js開發指南》

下面是一個 MongoDB 文檔的示例:

{ 
  "_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),   
  "name" : "nswbmw",  
  "age" : 22,
  "email" : [ "xxx@126.com", "xxx@gmail.com" ],  
  "family" : {  
    "mother" : { ... },  
    "father" : { ... },  
    "sister : { ... },  
    "address" : "earth"  
  }
}

更多有關 MongoDB 的知識請參考 《mongodb權威指南》或查閱:http://www.mongodb.org/

安裝MongoDB

安裝 mongodb 很簡單,去官網(http://www.mongodb.org/downloads)下載最新版的 mongodb,解壓到D盤並把文件夾重命名爲 mongodb,並在 mongodb 文件夾下新建 blog 文件夾做爲咱們的存儲目錄。打開 cmd,切換到 d:\mongodb\bin 目錄下,在命令行中輸入 mongod -dbpath 「d:\mongodb\blog」 設置 blog 文件夾做爲咱們工程的存儲目錄,這樣數據庫就成功啓動了。爲了方便之後使用數據庫,咱們在桌面上新建 「啓動mongodb.bat」 ,並寫入 d:\mongodb\bin\mongod.exe -dbpath d:\mongodb\blog ,這樣咱們之後只需運行桌面上的 「啓動mongodb.bat」 就可啓動數據庫了。

鏈接MongoDB

數據庫雖然安裝並啓動成功了,但咱們須要鏈接數據庫後才能使用數據庫。怎麼才能在 Node.js 中使用 MongoDb 呢?咱們須要使用 node-mongodb-native 模塊,打開 package.json,在 dependencies 中添加一行代碼:

{
  "name": "blog",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.4",
    "ejs": "*",
    "mongodb": "*"
  }
}

而後運行 npm install 更新依賴的模塊,稍等片刻後模塊就下載並安裝完成。

接下來在工程的目錄中建立 settings.js 文件,這個文件用於保存數據庫的鏈接信息。咱們將數據庫命名爲 blog,數據庫服務器在本地,所以 settings.js 文件的內容以下:

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

其中 db 是數據庫的名稱,host 是數據庫的地址。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, Connection.DEFAULT_PORT, {}));

咱們先不用弄清楚代碼爲何這麼寫,咱們如今只需知道以上代碼經過 module.exports 輸出了建立的數據庫鏈接,在後面的小節中咱們會用到這個模塊。

會話支持

會話是一種持久的網絡協議,用於完成服務器和客戶端之間的一些交互行爲。會話是一個比鏈接粒度更大的概念, 一次會話可能包含屢次鏈接,每次鏈接都被認爲是會話的一次操做。在網絡應用開發中,有必要實現會話以幫助用戶交互。例如網上購物的場景,用戶瀏覽了多個頁面,購買了一些物品,這些請求在屢次鏈接中完成。許多應用層網絡協議都是由會話支持的,如 FTP、Telnet 等,而 HTTP 協議是無狀態的,自己不支持會話,所以在沒有額外手段的幫助下,前面場景中服務器不知道用戶購買了什麼。

爲了在無狀態的 HTTP 協議之上實現會話,Cookie 誕生了。Cookie 是一些存儲在客戶端的信息,每次鏈接的時候由瀏覽器向服務器遞交,服務器也向瀏覽器發起存儲 Cookie 的請求,依靠這樣的手段服務器能夠識別客戶端。咱們一般意義上的 HTTP 會話功能就是這樣實現的。具體來講,瀏覽器首次向服務器發起請求時,服務器生成一個惟一標識符併發送給客戶端瀏覽器,瀏覽器將這個惟一標識符存儲在 Cookie 中,之後每次再發起請求,客戶端

瀏覽器都會向服務器傳送這個惟一標識符,服務器經過這個惟一標識符來識別用戶。 對於開發者來講,咱們無須關心瀏覽器端的存儲,須要關注的僅僅是如何經過這個惟一標識符來識別用戶。不少服務端腳本語言都有會話功能,如 PHP,把每一個惟一標識符存儲到文件中。Express 也提供了會話中間件,默認狀況下是把用戶信息存儲在內存中,但咱們既然已經有了 MongoDB,不妨把會話信息存儲在數據庫中,便於持久維護。

——《Node.js開發指南》

爲了使用這一功能,咱們首先要獲取一個叫作 connect-mongo 的模塊,在 package.json 中添加一行代碼:

{
  "name": "blog",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.4",
    "ejs": "*",
    "mongodb": "*",
    "connect-mongo": "*"
  }
}

運行 npm install 安裝模塊。而後打開 app.js,在 , path = require('path') 後添加如下代碼:

, MongoStore = require('connect-mongo')(express)
, settings = require('./settings');

在 app.use(express.methodOverride()); 後添加:

app.use(express.cookieParser());
app.use(express.session({
  secret: settings.cookieSecret,
  key: settings.db,
  cookie: {maxAge: 1000 * 60 * 60 * 24 * 30},//30 days
  store: new MongoStore({
    db: settings.db
  })
}));

其中 express.cookieParser() 是 Cookie 解析的中間件。express.session() 則提供會話支持,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>

在 views 下新建 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.js ,主頁顯示以下:

接下來在 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 %>

註冊頁面顯示以下:

至此,未登陸時的主頁、註冊頁、登陸頁都已經完成。

頁面通知

接下來咱們將實現用戶註冊和登錄,在這以前咱們須要引入 flash 模塊來實現頁面的通知和錯誤信息顯示的功能。

什麼是 flash?

req.flash 是 Express 提供的一個奇妙的工具,經過它保存的變量只會在用戶當前和下一次的請求中被訪問( req.flash 是存放在session裏的),以後會被清除,經過它咱們能夠很方便地實現頁面的通知和錯誤信息顯示功能。

——《Node.js開發指南》

在 package.json 添加一行代碼:

{
  "name": "blog",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.4",
    "ejs": "*",
    "mongodb": "*",
    "connect-mongo": "*",
    "connect-flash": "*"
  }
}

而後 npm install 安裝 flash 模塊,修改 app.js 在 , settings = require('./settings') 後添加:

, 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);
    }
    //讀取 users 集合
    db.collection('users', function(err, collection){
      if(err){
        mongodb.close();
        return callback(err);
      }
      //將用戶數據插入 users 集合
      collection.insert(user,{safe: true}, function(err, user){
        mongodb.close();
        callback(err, user);//成功!返回插入的用戶信息
      });
    });
  });
};

User.get = function(name, callback){//讀取用戶信息
  //打開數據庫
  mongodb.open(function(err, db){
    if(err){
      return callback(err);
    }
    //讀取 users 集合
    db.collection('users', function(err, collection){
      if(err){
        mongodb.close();
        return callback(err);
      }
      //查找用戶名 name 值爲 name文檔
      collection.findOne({
        name: name
      },function(err, doc){
        mongodb.close();
        if(doc){
          var user = new User(doc);
          callback(err, user);//成功!返回查詢的用戶信息
        } else {
          callback(err, null);//失敗!返回null
        }
      });
    });
  });
};

咱們暫時先不去研究這些代碼,咱們只需知道經過 User.prototype.save 實現了用戶信息的存儲,經過 User.get 實現了用戶信息的讀取。

打開 routes/index.js ,在最前面添加以下代碼:

var crypto = require('crypto'),
    User = require('../models/user.js');

經過 require 引入 user.js 用戶模型和 crypto 模塊,crypto 是 Node.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');
  }
  //生成密碼的散列值
  var md5 = crypto.createHash('md5'),
      password = md5.update(req.body.password).digest('hex');
  var newUser = new User({
      name: req.body.name,
      password: password,
      email: req.body.email
  });
  //檢查用戶名是否已經存在 
  User.get(newUser.name, function(err, user){
    if(user){
      err = '用戶已存在!';
    }
    if(err){
      req.flash('error', err);
      return res.redirect('/reg');
    }
    //若是不存在則新增用戶
    newUser.save(function(err){
      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 請求信息解析事後的對象,例如咱們要訪問用戶傳遞的 name=「password」 域的值,只需訪問 req.body['password'] 或 req.body.password 便可。

  • res.redirect: 重定向功能,實現了頁面的跳轉,更多關於 res.redirect 的信息請查閱:http://expressjs.com/api.html#res.redirect

  • User:在前面的代碼中,咱們直接使用了 User 對象。User 是一個描述數據的對象,即 MVC 

    架構中的模型。前面咱們使用了許多視圖和控制器,這是第一次接觸到模型。與視圖和控制器不一樣,模型是真正與數據打交道的工具,沒有模型,網站就只是一個外殼,不能發揮真實的做用,所以它是框架中最根本的部分。

在桌面新建 「啓動app.bat」 並寫入 :

supervisor d:\blog\app
//node d:\blog\app

之後咱們能夠經過打開 「啓動app.bat」 來啓動咱們的博客,而沒必要每次都要在命令行中啓動。

如今前後運行你的 「啓動mongodb.bat」 和 「啓動app.bat」 ,打開瀏覽器輸入 localhost:3000/reg 註冊試試吧!註冊成功後顯示以下:

咱們查看數據庫中是否存入了用戶的信息,運行 d:\mongodb\bin\mongo ,輸入:

用戶信息已經成功存入數據庫。但這還不是咱們想要的效果,咱們想要的效果是當註冊成功返回主頁時,左側導航顯示 HOME 、POST 、LOGOUT ,右側顯示 「註冊成功!」字樣。下面咱們來實現它。

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

修改 header.ejs,將<nav></nav>修改以下:

<nav>
<span><a title="主頁" href="/">home</a></span>
<% if(locals.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 (locals.success) { %>
  <div ><%= locals.success %></div>
<% } %>
<% if (locals.error) { %>
  <div><%= locals.error %> </div>
<% } %>

如今註冊成功後顯示以下:

咱們經過對 session 的使用實現了對用戶狀態的檢測,再根據不一樣的用戶狀態顯示不一樣的導航信息。 

簡單解釋一下流程:用戶在註冊成功後,把用戶信息存入 session ,同時頁面跳轉到主頁顯示 「註冊成功!」 。而後把 session 中的用戶信息賦給全局變量 res.locals.user ,在渲染 ejs 文件時經過檢測 locals.user 判斷用戶是否在線,根據用戶狀態的不一樣顯示不一樣的導航信息。

登陸與登出響應

如今咱們來實現登錄的響應。 

打開 index.js 將 app.post('/login') 修改以下:

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');
    }
    //用戶名密碼都匹配後,將用戶信息存入 session
    req.session.user = user;
    req.flash('success','登錄成功!');
    res.redirect('/');
  });
});

接下來咱們實現登出響應。修改 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 函數。 

最終 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: req.body.name,
        password: password,
        email: req.body.email
    });
    //檢查用戶名是否已經存在 
    User.get(newUser.name, function(err, user){
      if(user){
        err = '用戶已存在!';
      }
      if(err){
        req.flash('error', err);
        return res.redirect('/reg');
      }
      //若是不存在則新增用戶
      newUser.save(function(err){
        if(err){
          req.flash('error',err);
          return res.redirect('/reg');
        }
        req.session.user = newUser;//用戶信息存入session
        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');
      }
      //用戶名密碼都匹配後,將用戶信息存入 session
      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','未登陸!'); 
    return res.redirect('/login');
  }
  next();
}

function checkNotLogin(req,res,next){
  if(req.session.user){
    req.flash('error','已登陸!'); 
    return res.redirect('/');
  }
  next();
}

注意:爲了維護用戶狀態和 flash 通知功能的使用,咱們在 app.get('/reg)' 和 app.get('/login) 和 app.get('/post') 裏添加了如下代碼:

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()
  }
  //要存入數據庫的文檔
  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,post) {
        mongodb.close();
        callback(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) {
          callback(err, null);//失敗!返回 null
        }
        callback(null, docs);//成功!以數組形式返回查詢的結果
      });
    });
  });
};

發表響應

接下來咱們給發表文章註冊響應,打開 index.js ,在 User = require('../models/user.js') 後添加一行代碼:

Post = require('../models/post.js');

修改 app.post('/post') 以下:

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

至此,咱們的博客就建成了。

相關文章
相關標籤/搜索