第五章 Node.js進行Web開發
目錄
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 建立微博網站
- 功能分析 開發中的一個大忌就是沒有想清楚要做什麼就開始動手,因此我們準備在動手實踐之前先規劃一下網站的功能,即使是出於學習目的也不例外。首先,微博應該以使用者為中心,因此需要有使用者的註冊和登入功能。微博網站最核心的功能是資訊的發表,這個功能涉及許多方面,包括資料庫訪問、前端顯示等。一個完整的微博系統應該支援資訊的評論、轉發、圈點使用者等功能,但出於演示目的,我們不能一一實現所有功能,只是實現一個微博社交網站的雛形。
- 路由規劃
- /:首頁
- /u/[user]:使用者的主頁
- /post:發表資訊
- /reg:使用者註冊
- /login:使用者登入
- /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,
});
});
});
});