1. 程式人生 > >第五章 Node.js進行Web開發

第五章 Node.js進行Web開發

目錄

5.1 準備

5.1 準備

5.2 Express 框架

 路由控制;  模板解析支援;  動態檢視;  使用者會話;  CSRF 保護;  靜態檔案服務;  錯誤控制器;  訪問日誌;  快取;  外掛支援。

  • 安裝 Express 

為了使用這個工具,我們需要用全域性模式安裝Express,因為只有這樣我們才能在命令列中使用它。執行以下命令:

$ npm install -g express 

 express --help 檢視幫助資訊:

Usage: express [options] [path] 
Options: 
 -s, --sessions add session support 
 -t, --template <engine> add template <engine> support (jade|ejs). default=jade 
 -c, --css <engine> add stylesheet <engine> support (stylus). default=plain css 
 -v, --version output framework version 
 -h, --help output help information 

 Express 在初始化一個專案的時候需要指定模板引擎,預設支援Jade和ejs。

  •  建立工程

通過以下命令建立網站基本結構:

express -t ejs microblog 
$ cd microblog && npm install 

其中 dependencies 屬性中有express 和ejs。無引數的 npm install 的功能就是檢查當前目錄下的 package.json,並自動安裝所有指定的依賴。

  • 啟動伺服器

要關閉伺服器的話,在終端中按 Ctrl + C。

  • 工程的結構
app.js 是工程的入口,我們先看看其中有什麼內容:
/** 
 * Module dependencies. 
 */ 
var express = require('express') 
 , routes = require('./routes'); 
var app = module.exports = express.createServer(); 
// Configuration 
app.configure(function(){ 
 app.set('views', __dirname + '/views'); 
 app.set('view engine', 'ejs'); 
 app.use(express.bodyParser()); 
 app.use(express.methodOverride()); 
 app.use(app.router); 
 app.use(express.static(__dirname + '/public')); 
}); 
app.configure('development', function(){ 
 app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
}); 
app.configure('production', function(){ 
 app.use(express.errorHandler()); 
}); 
// Routes 
app.get('/', routes.index); 
app.listen(3000); 
console.log("Express server listening on port %d in %s mode", app.address().port, 
app.settings.env); 

app.set 是 Express 的引數設定工具

basepath:基礎地址,通常用於 res.redirect() 跳轉。  views:檢視檔案的目錄,存放模板檔案。  view engine:檢視模板引擎。  view options:全域性檢視引數物件。  view cache:啟用檢視快取。  case sensitive routes:路徑區分大小寫。  strict routing:嚴格路徑,啟用後不會忽略路徑末尾的“ / ”。  jsonp callback:開啟透明的 JSONP 支援。

/* 
 * GET home page. 
 */ 
exports.index = function(req, res) { 
res.render('index', { title: 'Express' }); 
}; 

這是一個典型的 MVC 架構,瀏覽器發起請求,由路由控制器接受,根據不同的路徑定向到不同的控制器。控制器處理使用者的具體請求,可能會訪問資料庫中的物件,即模型部分。控制器還要訪問模板引擎,生成檢視的 HTML,最後再由控制器返回給瀏覽器,完成一次請求。

  • 建立路由規則
  • 路徑匹配
  • REST 風格的路由規則

Express 支援 REST 風格的請求方式,在介紹之前我們先說明一下什麼是 REST。REST 的意思是 表徵狀態轉移(Representational State Transfer),它是一種基於 HTTP 協議的網路應用的介面風格,充分利用 HTTP 的方法實現統一風格介面的服務。HTTP 協議定義了以下8 種標準的方法。

 GET:請求獲取指定資源。  HEAD:請求指定資源的響應頭。  POST:向指定資源提交資料。  PUT:請求伺服器儲存一個資源。  DELETE:請求伺服器刪除指定資源。  TRACE:回顯伺服器收到的請求,主要用於測試或診斷。  CONNECT:HTTP/1.1 協議中預留給能夠將連線改為管道方式的代理伺服器。  OPTIONS:返回伺服器支援的HTTP請求方法。

 GET:獲取   POST:新增   PUT:更新   DELETE:刪除 

  • 控制權轉移

5.3 模板引擎

模板引擎(Template Engine)是一個從頁面模板根據一定的規則生成 HTML 的工具。它的發軔可以追溯到 1996 年 PHP 2.0 的誕生。PHP 原本是 Personal Home Page Tools(個人主頁工具)的簡稱,用於取代 Perl 和 CGI 的組合,其功能是讓程式碼嵌入在 HTML 中執行,以產生動態的頁面,因此 PHP 堪稱是最早的模板引擎的雛形。隨後的 ASP、JSP 都沿用了這個模式,即建立一個 HTML 頁面模板,插入可執行的程式碼,執行時動態生成 HTML。

 頁面功能邏輯與頁面佈局樣式耦合,網站規模變大以後逐漸難以維護。  語法複雜,對於非技術的網頁設計者來說門檻較高,難以學習。  功能過於全面,頁面設計者可以在頁面上程式設計,不利於功能劃分,也使模板解析效 率降低。

  • 使用模板引擎

ejs 的標籤系統非常簡單,它只有以下3種標籤。  <% code %>:JavaScript 程式碼。  <%= code %>:顯示替換過 HTML 特殊字元的內容。  <%- code %>:顯示原始 HTML 內容。

  • 頁面佈局
  • 片段檢視

Express 的檢視系統還支援片段檢視(partials),它就是一個頁面的片段,通常是重複的內容,用於迭代顯示。通過它你可以將相對獨立的頁面塊分割出去,而且可以避免顯式地使用 for 迴圈。

5.4 建立微博網站

  • 功能分析 開發中的一個大忌就是沒有想清楚要做什麼就開始動手,因此我們準備在動手實踐之前先規劃一下網站的功能,即使是出於學習目的也不例外。首先,微博應該以使用者為中心,因此需要有使用者的註冊和登入功能。微博網站最核心的功能是資訊的發表,這個功能涉及許多方面,包括資料庫訪問、前端顯示等。一個完整的微博系統應該支援資訊的評論、轉發、圈點使用者等功能,但出於演示目的,我們不能一一實現所有功能,只是實現一個微博社交網站的雛形。
  • 路由規劃
  1.  /:首頁 
  2.  /u/[user]:使用者的主頁
  3.  /post:發表資訊 
  4.  /reg:使用者註冊 
  5.  /login:使用者登入 
  6.  /logout:使用者登出 
app.get('/', routes.index); 
app.get('/u/:user', routes.user); 
app.post('/post', routes.post); 
app.get('/reg', routes.reg); 
app.post('/reg', routes.doReg); 
app.get('/login', routes.login); 
app.post('/login', routes.doLogin); 
app.get('/logout', routes.logout); 
exports.index = function(req, res) { 
 res.render('index', { title: 'Express' }); 
}; 
exports.user = function(req, res) { 
}; 
exports.post = function(req, res) { 
}; 
exports.reg = function(req, res) { 
}; 
exports.doReg = function(req, res) { 
}; 
exports.login = function(req, res) { 
}; 
exports.doLogin = function(req, res) { 
}; 
exports.logout = function(req, res) { 
}; 
  • 介面設計
  • 使用 Bootstrap
  • 連線資料庫
  • 會話支援
  •  使用者模型
  • 檢視互動
app.get('/login', function(req, res) { 
 res.render('login', { 
 title: '使用者登入', 
 }); 
}); 
app.post('/login', function(req, res) { 
 //生成口令的雜湊值
 var md5 = crypto.createHash('md5'); 
 var password = md5.update(req.body.password).digest('base64'); 
 
 User.get(req.body.username, 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('/logout', function(req, res) { 
 req.session.user = null; 
 req.flash('success', '登出成功'); 
 res.redirect('/'); 
}); 
  • 頁面許可權控制
var crypto = require('crypto'); 
var User = require('../models/user.js'); 
module.exports = function(app) { 
 app.get('/', function(req, res) { 
 res.render('index', { 
 title: '首頁' 
 }); 
 }); 
 
 app.get('/reg', checkNotLogin); 
 app.get('/reg', function(req, res) { 
 res.render('reg', { 
 title: '使用者註冊', 
 }); 
 }); 
 
 app.post('/reg', checkNotLogin); 
 app.post('/reg', function(req, res) { 
 //檢驗使用者兩次輸入的口令是否一致
 if (req.body['password-repeat'] != req.body['password']) { 
 req.flash('error', '兩次輸入的口令不一致'); 
 return res.redirect('/reg'); 
 } 
 
 //生成口令的雜湊值
 var md5 = crypto.createHash('md5'); 
 var password = md5.update(req.body.password).digest('base64'); 
 
 var newUser = new User({ 
 name: req.body.username, 
 password: password, 
 });
//檢查使用者名稱是否已經存在
 User.get(newUser.name, function(err, user) { 
 if (user) 
 err = 'Username already exists.'; 
 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; 
 req.flash('success', '註冊成功'); 
 res.redirect('/'); 
 }); 
 }); 
 }); 
 
 app.get('/login', checkNotLogin); 
 app.get('/login', function(req, res) { 
 res.render('login', { 
 title: '使用者登入', 
 }); 
 }); 
 
 app.post('/login', checkNotLogin); 
 app.post('/login', function(req, res) { 
 //生成口令的雜湊值
 var md5 = crypto.createHash('md5'); 
 var password = md5.update(req.body.password).digest('base64'); 
 
 User.get(req.body.username, 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('/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(); 
} 
  • 發表微博
  •  微博模型
// models/post.js
var mongodb = require('./db'); 
function Post(username, post, time) { 
 this.user = username; 
 this.post = post; 
if (time) { 
 this.time = time; 
 } else { 
 this.time = new Date(); 
 } 
}; 
module.exports = Post; 
Post.prototype.save = function save(callback) { 
 // 存入 Mongodb 的文件
 var post = { 
 user: this.user, 
 post: this.post, 
 time: this.time, 
 }; 
 mongodb.open(function(err, db) { 
 if (err) { 
 return callback(err); 
 } 
 // 讀取 posts 集合
 db.collection('posts', function(err, collection) { 
 if (err) { 
 mongodb.close(); 
 return callback(err); 
 } 
 // 為 user 屬性新增索引
 collection.ensureIndex('user'); 
 // 寫入 post 文件
 collection.insert(post, {safe: true}, function(err, post) { 
 mongodb.close(); 
 callback(err, post); 
 }); 
 }); 
 }); 
}; 
Post.get = function get(username, 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); 
 }
// 查詢 user 屬性為 username 的文件,如果 username 是 null 則匹配全部
 var query = {}; 
 if (username) { 
 query.user = username; 
 } 
 collection.find(query).sort({time: -1}).toArray(function(err, docs) { 
 mongodb.close(); 
 if (err) { 
 callback(err, null); 
 } 
 // 封裝 posts 為 Post 物件
 var posts = []; 
 docs.forEach(function(doc, index) { 
 var post = new Post(doc.user, doc.post, doc.time); 
 posts.push(post); 
 }); 
 callback(null, posts); 
 }); 
 }); 
 }); 
};  
  • 發表微博
app.post('/post', checkLogin); 
app.post('/post', function(req, res) { 
 var currentUser = req.session.user; 
 var post = new Post(currentUser.name, req.body.post); 
 post.save(function(err) { 
 if (err) { 
 req.flash('error', err); 
 return res.redirect('/'); 
 } 
 req.flash('success', '發表成功'); 
 res.redirect('/u/' + currentUser.name); 
 }); 
}); 
  • 使用者頁面
app.get('/u/:user', function(req, res) { 
 User.get(req.params.user, function(err, user) { 
 if (!user) { 
 req.flash('error', '使用者不存在'); 
 return res.redirect('/'); 
 } 
 Post.get(user.name, function(err, posts) { 
 if (err) { 
 req.flash('error', err); 
 return res.redirect('/'); 
 } 
 res.render('user', { 
 title: user.name, 
 posts: posts, 
 }); 
 }); 
 }); 
});