下面開始用 Node.js 進行 Web 開發。css
我是經過《Node.js開發指南》這本書來學習 Node.js Web 開發的,書中使用的 Express 框架是 2.5.8,而個人是 4.14.1,因此遇到了許多問題,在文章中我都有提到並講解。html
☞GitHub 地址前端
《Node.js開發指南》中創建項目的方式是:express -t ejs microblog,可是這種方式對於高版本的 Express 新建的標籤替換引擎並非 .ejs,而是 .jade,若是要使用 .ejs 咱們能夠經過下面命令創建網站基本結構。node
express -e NodeJSBlog
複製代碼
執行命令後在當前目錄下出現了一些文件,而且下邊提示咱們經過 npm install 安裝依賴。jquery
在 npm install 以後,打開 NodeJSBlog 目錄下的 package.json,能夠看到已安裝的包及對應的版本號。git
注意,咱們以前開啓 Node.js 服務器,都是執行 node xxx.js,而後去瀏覽器訪問便可,可是 Express 4.x 以上就不是這種方式了,應該是 npm start,端口配置在 bin/www 中。github
啓動成功訪問 localhost:3000/。正則表達式
咱們看一下 express 在 NodeJSBlog 這個目錄下都生成了哪些文件。mongodb
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 index = 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(path.join(__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('/', index);
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 handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
複製代碼
app.js 是項目的入口,首先引入了一系列咱們所須要的模塊,而後引入了 routes 目錄下的兩個本地模塊,它的功能是爲指定路徑組織返回內容,至關於 MVC 架構中的控制器。數據庫
接下來是視圖引擎設置, app.set() 是 Express 的參數設置工具,接受一個鍵(key)和一個值(value),可用的參數以下所示:
Express 依賴於 connect,提供了大量的中間件,能夠經過 app.use() 啓用
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
複製代碼
routes/index.js 是路由文件,至關於控制器,用於組織展現的內容,app.js 中經過 app.get('/', routes.index); 將「 / 」路徑映射到 exports.index 函數下,其中只有一個語句 res.render('index', { title: 'Express' }),功能是調用模板解析引擎,翻譯名爲 index 的模板,並傳入一個對象做爲參數,這個對象只有一個屬性,即 title: 'Express'。
<!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>
複製代碼
index.ejs 是模板文件,即 routes/index.js 中調用的模板,內容是:
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
複製代碼
它的基礎是 HTML 語言,其中包含了形如 <%= title %> 的標籤,功能是顯示引用的變量,即 res.render 函數第二個參數傳入的對象的屬性。
在書中 views 目錄下是有 layout.ejs 的,它可讓全部模板去繼承它,<%- body %> 中是獨特的內容,其餘部分是共有的,能夠看做是頁面框架。
書中 layout.ejs:
<head>
<title>
<%= title %>
</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<%- body %>
</body>
</html>
複製代碼
可是 Express 4.x 就沒有 layout.ejs 了,解決方法:
官方推薦了 include 方式,它不只能實現 layout 的功能,仍是將 view 的那些可複用的 html 片斷提取成模塊,在須要使用的地方直接用 <% include xxx %>。
例如先在 views 目錄下新建一個 public_file.ejs ,在裏面添加須要引用的公共文件:
<link rel='stylesheet' href='/stylesheets/style.css' />
複製代碼
而後修改一下 index.ejs,使用 <% include listitem %> 方式引用上邊公共文件:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<% include public_file %>
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
複製代碼
重啓服務,訪問 localhost:3000/,能夠看到 style.css 文件正常引用。
每一次修改咱們都須要重啓服務才能看到修改後的結果,您能夠看個人《Node.js 應用程序自動重啓》這篇文章去安裝 supervisor 或 nodemon 兩個插件來實現應用程序自動重啓。
簡單說一下新增一個頁面的流程。
首先在 views 目錄下新建一個模板,例如 hello.ejs:
而後打開 index.js 文件,添加頁面路由信息:
訪問 localhost:3000/hello。
補充:在《Node.js開發指南》這本書中,還須要向 app.js 文件中添加頁面路由信息,但在 Express 4.x 中是不須要的。
上面的例子是爲固定的路徑設置路由規則,Express 還支持更高級的路徑匹配模式,例如咱們想要展現一個用戶的我的頁面,路徑爲 /user/[username],能夠用下面的方法定義路由 規則:
router.get('/user/:username', function(req, res, next) {
res.send('user: ' + req.params.username);
});
複製代碼
重啓項目,訪問 localhost:3000/user/LiuZhenghe。
注意:調用模板解析引擎,用 res.render(),只是向頁面發送數據,用 res.send()。
路徑規則 /user/:username 會被自動編譯爲正則表達式,相似於 /user/([^/]+)/? 這樣的形式,路徑參數能夠在響應函數中經過 req.params 的屬性訪問。
路徑規則一樣支持 JavaScript 正則表達式,例如 app.get(/user/([^/]+)/?,callback),這樣的好處在於能夠定義更加複雜的路徑規則,而不一樣之處是匹配的參數是匿名的,所以須要經過 req.params[0]、req.params[1] 這樣的形式訪問。
Express 支持 REST 風格的請求方式,在介紹以前咱們先說明一下什麼是 REST。
REST 的意思是表徵狀態轉移(Representational State Transfer),它是一種基於 HTTP 協議的網絡應用的接口風格,充分利用 HTTP 的方法實現統一風格接口的服務。
HTTP 協議定義瞭如下 8 種標準的方法:
其中咱們常常用到的是 GET、POST、PUT 和 DELETE 方法,根據 REST 設計模式,這4種方法一般分別用於實現如下功能。
這是由於這 4 種方法有不一樣的特色,按照定義,它們的特色以下表所示:
請求方式 | 安全 | 冪等 |
---|---|---|
GET | 是 | 是 |
POST | 否 | 否 |
PUT | 否 | 是 |
DELETE | 否 | 是 |
所謂安全是指沒有反作用,即請求不會對資源產生變更,連續訪問屢次所得到的結果不受訪問者的影響,而冪等指的是重複請求屢次與一次請求的效果是同樣的,好比獲取和更新操做是冪等的,這與新增不一樣,刪除也是冪等的,即重複刪除一個資源,和刪除一次是同樣的。
Express 對每種 HTTP 請求方法都設計了不一樣的路由綁定函數,例如前面例子所有是 app.get,表示爲該路徑綁定了 GET 請求,向這個路徑發起其餘方式的請求不會被響應。
下表是 Express 支持的全部 HTTP 請求的綁定函數。
請求方式 | 綁定函數 |
---|---|
GET | app.get(path, callback) |
POST | app.post(path, callback) |
PUT | app.put(path, callback) |
DELETE | app.delete(path, callback) |
PATCH | app.patch(path, callback) |
TRACE | app.trace(path, callback) |
CONNECT | app.connect(path, callback) |
OPTIONS | app.options(path, callback) |
全部方法 | app.all(path, callback) |
例如咱們要綁定某個路徑的 POST 請求,則能夠用 app.post(path, callback) 的 方法,須要注意的是 app.all 函數,它支持把全部的請求方式綁定到同一個響應函數,是一個很是靈活的函數,在後面咱們能夠看到許多功能均可以經過它來實現。
Express 支持同一路徑綁定多個路由響應函數,例如:
index.js
// ...
/* 路徑匹配模式 */
router.all('/user/:username', function(req, res, next) {
res.send('all methods captured');
});
router.get('/user/:username', function(req, res, next) {
res.send('user: ' + req.params.username);
});
// ...
複製代碼
當再次訪問 localhost:3000/user/LiuZhenghe 時,發現頁面被第一條路由規則捕獲。
緣由是 Express 在處理路由規則時,會優先匹配先定義的路由規則,所以後面相同的規則被屏蔽。
Express 提供了路由控制權轉移的方法,即回調函數的第三個參數 next,經過調用 next(),會將路由控制權轉移給後面的規則,例如:
// ...
/* 路徑匹配模式 */
router.all('/user/:username', function(req, res, next) {
console.log('all methods captured');
next();
});
router.get('/user/:username', function(req, res, next) {
res.send('user: ' + req.params.username);
});
// ...
複製代碼
此時刷新頁面,在控制檯能夠看到「all methods captured」,瀏覽器顯示了 user: LiuZhenghe。
這是一個很是有用的工具,可讓咱們輕易地實現中間件,並且還能提升代碼的複用程度,例如咱們針對一個用戶查詢信息和修改信息的操做,分別對應了 GET 和 PUT 操做,而二者共有的一個步驟是檢查用戶名是否合法,所以能夠經過 next() 方法實現。
模板引擎也就是視圖,視圖決定了用戶最終能看到什麼,這裏咱們用 ejs 爲例介紹模板引擎的使用方法。
在 app.js 中,如下兩句設置了模板引擎和頁面模板的位置:
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
複製代碼
在 index.js 中經過 res.render() 調用模板,res.render() 的功能是調用模板引擎,並將其產生的頁面直接返回給客戶端,它接受兩個參數,第一個是模板的名稱,即 views 目錄下的模板文件名,不包含文件的擴展名;第二個參數是傳遞給模板的數據,用於模板翻譯。
ejs 的標籤系統很是簡單,它只有如下 3 種標籤:
咱們能夠用它們實現頁面模板系統能實現的任何內容。
《Node.js開發指南》中所講的片斷視圖(partials)在 Express 4.x 中已經不支持了,在上面項目結構分析那一節中我曾補充過 include,這裏再次介紹一下它的用法。
官方推薦了 include 方式,它不只能實現 layout 的功能,仍是將 view 的那些可複用的 html 片斷提取成模塊,在須要使用的地方直接用 <% include xxx %>,看下面這個例子:
首先在 index.js 中新增如下內容:
// 片段視圖
router.get('/list', function(reg, res) {
res.render('list', {
title: "List",
items: [2019, 'Node.js', 'NodeJSBlog', 'Express']
});
});
複製代碼
而後新建 list.ejs 文件並添加如下內容:
<ul>
<% items.forEach(function(listitem){ %>
<% include listitem %>
<% }) %>
</ul>
複製代碼
同時新建 listitem.ejs 文件並添加:
<li><%= listitem %></li>
複製代碼
訪問 localhost:3000/list,能夠看到如下內容:
博客網站首先應該有登陸註冊功能,而後是最核心的功能——信息發表,這個功能涉及到許多方面,包括數據庫訪問,前端顯示等。
一個完整的博客系統,應該有評論,收藏,轉發等功能,處於本人目前的能力水平還不能都實現,先作一個博客網站的雛形吧。
根據功能設計,咱們把路由按照如下方案規劃:
以上頁面還能夠根據用戶狀態細分,發表信息以及用戶登出頁面必須是已登陸用戶才能操做的功能,而用戶註冊和用戶登入所面向的對象必須是未登入的用戶,首頁和用戶主頁則針對已登入和未登入的用戶顯示不一樣的內容。
在 index.js 中添加如下內容:
router.get('/', function(req, res) {
res.render('index', {
title: 'Express'
});
});
router.get('/u/:user', function(req, res) {});
router.post('/post', function(req, res) {});
router.get('/reg', function(req, res) {});
router.post('/reg', function(req, res) {});
router.get('/login', function(req, res) {});
router.post('/login', function(req, res) {});
router.get('/logout', function(req, res) {});
複製代碼
其中 /post、/login 和 /reg 因爲要接受表單信息,所以使用 router.post 註冊路由,/login 和 /reg 還要顯示用戶註冊時要填寫的表單,因此要以 router.get 註冊。
下載 jquery.js,bootstrap.css 和 bootstrap.js,放到 public 下對應的目錄中。
在 public_file.ejs 中引用,可使用 include 方法給模板添加公共文件。
去 Bootstrap官網 查看並使用所需的模板或組件。
下圖是我從 Bootstrap 官網找的一個模板,並放到了 index.ejs 目錄下並進行了簡單地修改,並將頁面公共部分(頭尾部分)取出,用 include 方式來複用。
咱們選用 MongoDB 做爲網站的數據庫系統,它是一個開源的 NoSQL 數據庫,相比 MySQL 那樣的關係型數據庫,它更爲輕巧、靈活,很是適合在數據規模很大、事務性不強的場合下使用。
經過 npm 安裝 mongodb。
npm install mongodb --save
複製代碼
補充:經過 --save 安裝,包名和版本號將會出如今 package.json 中。
接下來在項目主目錄中建立 settings.js 文件,這個文件用於保存數據庫的鏈接信息,咱們將用到的數據庫命名爲 NodeJSBlog,數據庫服務器在本地,所以 settings.js 文件的內容以下:
settings.js
module.exports = {
cookieSecret: 'NodeJSBlogbyvoid',
db: 'NodeJSBlog',
host: 'localhost',
};
複製代碼
其中,db 是數據庫的名稱,host 是數據庫的地址,cookieSecret 用於 Cookie 加密與數據庫無關,咱們留做後用。
接下來新建 models 目錄,並在目錄中建立 db.js:
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, 27017, {}), {
safe: true
});
複製代碼
以上代碼經過 module.exports 輸出了建立的數據庫鏈接,在後面的小節中咱們會用到這個模塊,因爲模塊只會被加載一次,之後咱們在其餘文件中使用時均爲這一個實例。
《Node.js開發指南》中對會話支持是這樣描述的:
會話是一種持久的網絡協議,用於完成服務器和客戶端之間的一些交互行爲。會話是一個比鏈接粒度更大的概念,一次會話可能包含屢次鏈接,每次鏈接都被認爲是會話的一次操做。在網絡應用開發中,有必要實現會話以幫助用戶交互。例如網上購物的場景,用戶瀏覽了多個頁面,購買了一些物品,這些請求在屢次鏈接中完成。許多應用層網絡協議都是由會話支持的,如 FTP、Telnet 等,而 HTTP 協議是無狀態的,自己不支持會話,所以在沒有額外手段的幫助下,前面場景中服務器不知道用戶購買了什麼。
爲了在無狀態的 HTTP 協議之上實現會話,Cookie 誕生了。Cookie 是一些存儲在客戶端的信息,每次鏈接的時候由瀏覽器向服務器遞交,服務器也向瀏覽器發起存儲 Cookie 的請求,依靠這樣的手段服務器能夠識別客戶端。咱們一般意義上的 HTTP 會話功能就是這樣實現的。具體來講,瀏覽器首次向服務器發起請求時,服務器生成一個惟一標識符併發送給客戶端瀏覽器,瀏覽器將這個惟一標識符存儲在 Cookie 中,之後每次再發起請求,客戶端瀏覽器都會向服務器傳送這個惟一標識符,服務器經過這個惟一標識符來識別用戶。
對於開發者來講,咱們無須關心瀏覽器端的存儲,須要關注的僅僅是如何經過這個惟一標識符來識別用戶。不少服務端腳本語言都有會話功能,如 PHP,把每一個惟一標識符存儲到文件中。Express 也提供了會話中間件,默認狀況下是把用戶信息存儲在內存中,但咱們既然已經有了 MongoDB,不妨把會話信息存儲在數據庫中,便於持久維護。
可是若是你的 Express 版本是 4.x,按照書中接下來的內容編寫代碼,就會出各類問題,下面我來講一下 Express 4.x 中的會話支持。
在 Express 4.x 中咱們須要本身安裝 express-session 包,而後添加引用:
var session = require('express-session');
複製代碼
而後再引用 connect-mongo 包:
var MongoStore = require('connect-mongo')(session);
複製代碼
最終在 app.js 中新增的內容就是:
var settings = require('./settings');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
app.use(session({
secret: settings.cookieSecret,
store: new MongoStore({
db: settings.db,
})
}));
複製代碼
可是此時程序會報錯:'Connection strategy not found':
從網上查找到該問題以後找到了解決辦法,打開 package.json 文件,將 connect-mongo 的版本改成 0.8.2,而後執行 npm install 便可解決。
若是程序沒有報錯,那麼就成功了,你可使用 mongo.exe 或可視化工具來查看數據庫是否新建成功。
經過可視化工具或 mongodb 終端新建一個用戶表 users。
註冊頁面:
首先添加註冊頁面的模板 views/reg.ejs,並添加表單結構:
<div class="container">
<div class="bs-example" data-example-id="basic-forms">
<form>
<div class="form-group">
<label for="username">用戶名</label>
<input type="text" class="form-control" id="username" placeholder="請輸入用戶名">
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" class="form-control" id="password" placeholder="請輸入密碼">
</div>
<div class="form-group">
<label for="password_repeat">再次輸入密碼</label>
<input type="password" class="form-control" id="password_repeat" placeholder="再次輸入密碼">
</div>
<button type="submit" class="btn btn-default">註冊</button>
</form>
</div>
</div>
複製代碼
而後打開 index.js 添加註冊頁面的路由信息:
index.js
router.get('/reg', function(req, res) {
res.render('reg', {
title: '用戶註冊'
});
});
複製代碼
而後訪問 localhost:3000/reg。
註冊響應:
在書中使用了 flash,可是最新版本的 Express 已經不支持 flash 了,你須要先經過 npm 安裝 connect-flash。
而後在 app.js 中添加以下代碼:
var flash = require('connect-flash');
複製代碼
在 routes/index.js 中添加 /reg 的 POST 響應函數:
routes/index.js
// 註冊響應
var crypto = require('crypto');
var User = require('../models/user.js');
var MongoClient = require('mongodb').MongoClient;
const DB_CONN_STR='mongodb://localhost:27017/users';
router.post('/reg', function(req, res) {
let newUser = {
username: req.body.username,
password: req.body.password,
password_repeat: req.body.password_repeat
};
let addStr = [{
username: newUser.username,
password: newUser.password
}];
MongoClient.connect(DB_CONN_STR, function(err, db) {
db.collection('users').findOne({
username: newUser.username
}, function(err, result) {
if (!result) {
if (newUser.password === newUser.password_repeat) {
MongoClient.connect(DB_CONN_STR, function(err, db) {
req.session.error = '註冊成功,請登陸!';
db.collection('users').insert(addStr);
db.close();
return res.redirect('/login');
});
} else {
req.session.error = '兩次密碼不一致!';
return res.redirect('/register');
}
} else {
req.session.error = '用戶名已存在!';
return res.redirect('/register');
}
})
db.close();
});
});
複製代碼
用戶模型
在前面的代碼中,咱們直接使用了 User 對象,User 是一個描述數據的對象,即 MVC 架構中的模型,前面咱們使用了許多視圖和控制器,這是第一次接觸到模型。與視圖和控制器不一樣,模型是真正與數據打交道的工具,沒有模型,網站就只是一個外殼,不能發揮真實的做用,所以它是框架中最根本的部分。如今就讓咱們來實現 User 模型吧。
在 models 目錄中建立 user.js 的文件,內容以下:
models/user.js
var mongodb = require('./db');
function User(user) {
this.name = user.name;
this.password = user.password;
};
module.exports = User;
User.prototype.save = function save(callback) {
// 存入 Mongodb 的文檔
var user = {
name: this.name,
password: this.password,
};
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 屬性添加索引
collection.ensureIndex('name', {
unique: true
});
// 寫入 user 文檔
collection.insert(user, {
safe: true
}, function(err, user) {
mongodb.close();
callback(err, user);
});
});
});
};
User.get = function get(username, 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 屬性爲 username 的文檔
collection.findOne({
name: username
}, function(err, doc) {
mongodb.close();
if (doc) {
// 封裝文檔爲 User 對象
var user = new User(doc);
callback(err, user);
} else {
callback(err, null);
}
});
});
});
};
複製代碼
以上代碼實現了兩個接口,User.prototype.save 和 User.get,前者是對象實例的方法,用於將用戶對象的數據保存到數據庫中,後者是對象構造函數的方法,用於從數據庫中查找指定的用戶。
視圖交互
如今幾乎已經萬事俱備,只差視圖的支持了。爲了實現不一樣登陸狀態下頁面呈現不一樣內容的功能,咱們須要建立動態視圖助手,經過它咱們才能在視圖中訪問會話中的用戶數據,同時爲了顯示錯誤和成功的信息,也要在動態視圖助手中增長響應的函數。
在書中,在 app.js 中添加的視圖交互代碼是:
app.dynamicHelpers({
user: function(req, res) {
return req.session.user;
},
error: function(req, res) {
var err = req.flash('error');
if (err.length)
return err;
else
return null;
},
success: function(req, res) {
var succ = req.flash('success');
if (succ.length)
return succ;
else
return null;
},
});
複製代碼
可是在 Express 4.x 中會報錯「app.dynamicHelpers is not a function 」,此處應該添加:
app.js
app.use(flash());
app.use(function(req, res, next) {
res.locals.user = req.session.user;
res.locals.post = req.session.post;
var error = req.flash('error');
res.locals.error = error.length ? error : null;
var success = req.flash('success');
res.locals.success = success.length ? success : null;
next();
});
複製代碼
注意,這段代碼不要放的太靠後,應該放到路由控制代碼以前。
接下來修改公共導航部分。
header.ejs
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">NodeJS Blog</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<% if (!user) { %>
<a href="/login" type="submit" class="btn btn-success">登陸</a>
<a href="/reg" type="submit" class="btn btn-success">註冊</a>
<% } else { %>
<a href="" type="submit" class="btn">註銷</a>
<% } %>
</form>
</div>
</div>
</nav>
複製代碼
而後打開註冊頁,輸入用戶名、密碼,點擊註冊按鈕,發現又遇到坑了...「db.collection is not a function」。
引發這個錯誤的緣由是你經過 npm 安裝的 mongodb 的版本和你 Node.js 操做數據的 api 版本不一致,查看了一下 package.json,發現 mongodb 的版本是 3.1.13。
解決方法,下載低版本 mongodb,例:
"mongodb": "^2.2.33",
複製代碼
npm install 安裝。
未完待續......