《nodejs開發指南》express4.x版-微博案例完整實現
本來網上也有了很多相關的教程,寫本文的目的主要是希望梳理對整個程式碼的認識,另一方面,參考的那篇文章某些地方的實現目前也不適用,需要更新。也歡迎大家與我交流^^。
本文嘗試完整實現整個例子,因此將不嘗試區分與《nodejs開發指南》實現的差異。
完整程式碼下載
開發詳細步驟
建立專案:
express -e microblog
按提示輸入
PS E:\code\nodejsExercise\express\3> cd .\microblog\
PS E:\code\nodejsExercise\express\3\microblog> npm i
現在,我們先啟動網站看看
npm start
如果能執行到以上的效果,那麼專案已經建立好了。
功能分析
那麼在正式開始建立網站前,我也試著對接下來的專案進行一個功能分析。
本專案是一個微博專案的簡單實現,需要包括如下功能:使用者的登入、註冊、退出登入,另外還有資訊登入功能。
大致規劃:
- 一個主頁用於顯示微博的主體內容
- 一個登入頁面
- 一個註冊頁面
- 一個使用者頁面,只顯示使用者的微博資訊
接下來開始正式的搭建專案。
可以先寫一個index.html頁面看看效果:
接著將它改為模板:
在views資料夾新建一個header.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%= title %> - Microblog</title>
<link rel='stylesheet' href='/stylesheets/bootstrap.css' />
<style type="text/css">
body {
padding-top: 60px;
padding-bottom: 40px;
}
</style>
<link href ="stylesheets/bootstrap-responsive.css" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">Microblog</a>
<div class="nav-collapse">
<ul class="nav">
<li class="active"><a href="/">首頁</a></li>
<li><a href="/login">登入</a></li>
<li><a href="/reg">註冊</a></li>
</ul>
</div>
</div>
</div>
</div>
在views資料夾新建一個footer.ejs
<hr />
<footer>
<p><a href="http://www.byvoid.com/" target="_blank">BYVoid</a> 2012</p>
</footer>
</div>
</body>
<script src="/javascripts/jquery.js"></script>
<script src="/javascripts/bootstrap.js"></script>
</html>
在views資料夾,修改index.ejs模板如下:
<% include header.ejs %>
<div class="hero-unit">
<h1>歡迎來到 Microblog</h1>
<p>Microblog 是一個基於 Node.js 的微博系統。</p>
<p>
<a class="btn btn-primary btn-large" href="/login">登入</a>
<a class="btn btn-large" href="/reg">立即註冊</a>
</p>
</div>
<% include footer.ejs %>
在public資料夾中放入圖片、js/css檔案
可以從我的原始碼直接拷貝。
修改routes資料夾index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: '首頁' }); //修改了這裡
});
module.exports = router;
我的電腦是win10 64位的,可能與你們的情況不一樣,上面的資料庫的安裝與執行僅供參考。
用到資料庫前都需要啟動:
執行cmd,如果已經按上文的網址配置,去到資料庫的bin資料夾執行,-dbpath後的路徑請按照你的具體配置修改。
請記住以下程式碼,啟動網站前記得都要先啟動了資料庫,建議現在就先啟動避免一會忘了啟動報錯
.\mongod.exe -dbpath "E:\mongodb\data\db"
使用資料庫前,還需要安裝一些依賴,進行一些設定:
首先,給package.json一行程式碼(不知道寫在哪的,具體可參考原始碼):
"mongodb": ">=0.9.9"
cmd
npm i
建立一個settings.js檔案
module.exports = {
cookieSecret:'microblogbyvoid', //用於cookie的加密
db:'microblog', //資料庫的名字
host:'localhost', //資料庫地址
}
建立models資料夾,在此資料夾中建立db.js
var settings = require('../settings.js'),
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});
接下來需要將使用者資料儲存到資料庫中,你覺得需要做些什麼呢?
為了將使用者資料儲存到資料庫中,做出如下配置:
- 新增一個connect-mongo模組:
"express-session": "^1.15.6",
"connect-mongo": ">= 0.1.7"
cmd
npm i
對app.js進行修改,新增:
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
var settings = require('./settings');
以及
app.use(session({
secret:settings.cookieSecret,
store:new MongoStore({
// db:settings.db
url: 'mongodb://localhost/microblog'
})
}));
接下來是註冊頁面以及登入介面
在views頁面新建reg.ejs模版
<% include header.ejs %>
<form class="form-horizontal" method="post" >
<fieldset>
<legend>使用者註冊</legend>
<div class="control-group">
<label class="control-label" for="username">使用者名稱</label>
<div class="controls">
<input type="text" class="input-xlarge" id="username" name="username">
<p class="help-block">你的賬戶的名稱,用於登入和顯示。</p>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">口令</label>
<div class="controls">
<input type="password" class="input-xlarge" id="password" name="password">
</div>
</div>
<div class="control-group">
<label class="control-label" for="password-repeat">重複輸入口令</label>
<div class="controls">
<input type="password" class="input-xlarge" id="password-repeat" name="password-repeat">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">註冊</button>
</div>
</fieldset>
</form>
<% include footer.ejs %>
在views頁面新建login.ejs模版
<% include header.ejs %>
<form class="form-horizontal" method="post">
<fieldset>
<legend>使用者登入</legend>
<div class="control-group">
<label class="control-label" for="username">使用者名稱</label>
<div class="controls">
<input type="text" class="input-xlarge" id="username" name="username">
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">口令</label>
<div class="controls">
<input type="password" class="input-xlarge" id="password" name="password">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">登入</button>
</div>
</fieldset>
</form>
<% include footer.ejs %>
修改index.js,引入模版渲染頁面
router.get('/reg', function(req, res, next) {
res.render('reg', { title: '使用者註冊' });
});
router.get('/login', function(req, res, next) {
res.render('login', { title: '使用者登入' });
});
啟動網頁效果如下:
至此註冊、登陸頁面均能正常顯示了,是時候給頁面增加一些響應功能了。
先做註冊介面的功能:
需要做些什麼呢?
- 驗證使用者名稱是否存在-這個需要讀取資料庫的內容,進行比對
- 驗證密碼是否一致
- 進行必要的密碼保護
- 驗證無誤後將使用者名稱和密碼儲存到資料庫
- 不管驗證結果是正確還是錯誤,提交頁面後要給出一個反饋
這裡主要對涉資料庫的操作進行一下分析:
驗證使用者名稱需要讀取資料庫的使用者資訊,而儲存使用者名稱和密碼到資料庫是要新增資料庫的使用者資訊。這些功能可以抽離出來,
用User這個建構函式實現這些功能。
User.get()用於獲取使用者資訊,
user.save()用於將特定例項儲存到資料庫。
由於程式碼用到crypto、user,先新增模組(其實我是寫完了post才新增的,不過為了避免後面新增時大家都忘了之前的程式碼了,先添加了),在index.js新增:
var crypto = require('crypto');
var User = require('../models/user.js');
然後,程式碼先寫成這樣:
router.post('/reg',function(req,res,next){
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password);
var newUser =new User({
name:req.body.username,
password:password
});
User.get(newUser.name,function(err,user){
if(err){
//反饋錯誤,跳轉到/reg,記得return
}
//判斷使用者是否存在
if(user){
//反饋使用者存在,跳轉到/reg,記得return
}
//判斷密碼是否一致
if(req.body.password !== req.body['password-repeat'] ){
//反饋密碼不一致,跳轉到/reg,記得return
}
//使用者不存在
newUser.save(function(err){
if(err){
//反饋錯誤,跳轉到/reg,記得return
}
//反饋註冊成功,跳轉到/。
});
});
});
程式碼寫到這,還有一些問題沒有解決:
- User建構函式未定義
- 反饋未實現
先解決建構函式的問題:
models資料夾新建user.js
var mongodb = require('./db.js');
function User(user){
this.name = user.name;
this.password = user.password;
}
User.prototype.save = function(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(username,callback){
mongodb.open(function(err,db){
if(err){
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);
}
});
});
});
};
module.exports = User;
寫到這裡,先嚐試簡單驗證一下User函式是否存在問題。
將程式碼修改為:(是修改不是新增)
router.post('/reg',function(req,res,next){
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password);
var newUser =new User({
name:req.body.username,
password:password
});
User.get(newUser.name,function(err,user){
if(err){
//反饋錯誤,跳轉到/reg,記得return
console.log(err);
return res.redirect('/reg');
}
//判斷使用者是否存在
if(user){
console.log('user existed');
return res.redirect('/reg');
}
//判斷密碼是否一致
if(req.body.password !== req.body['password-repeat'] ){
//反饋密碼不一致,跳轉到/reg,記得return
console.log('password not equal');
return res.redirect('/reg');
}
//使用者不存在
newUser.save(function(err){
if(err){
//反饋錯誤,跳轉到/reg,記得return
console.log('save failure');
return res.redirect('/reg');
}
console.log('save success');
res.redirect('/');
});
});
});
這個可以自行測試,就不截圖了。
至此,註冊還有一個反饋的功能未實現。
為此,引入新的模組,
package.json:
"connect-flash": "^0.1.1"
cmd
npm i
app.js
var flash = require('connect-flash');
app.use(flash());
app.use(function(req,res,next){
console.log('app.user local');
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();
});
再次修改index.js
router.post('/reg',function(req,res,next){
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password);
var newUser =new User({
name:req.body.username,
password:password
});
User.get(newUser.name,function(err,user){
if(err){
//反饋錯誤,跳轉到/reg,記得return
req.flash('error',err);
return res.redirect('/reg');
}
//判斷使用者是否存在
if(user){
req.flash('error','使用者已存在');
return res.redirect('/reg');
}
//判斷密碼是否一致
if(req.body.password !== req.body['password-repeat'] ){
//反饋密碼不一致,跳轉到/reg,記得return
req.flash('error','密碼不一致');
return res.redirect('/reg');
}
//使用者不存在
newUser.save(function(err){
if(err){
//反饋錯誤,跳轉到/reg,記得return
req.flash('error','儲存失敗');
return res.redirect('/reg');
}
req.flash('success','儲存成功');
res.redirect('/');
});
});
});
同時,在header.ejs結尾處新增以顯示反饋:
<div id="container" class="container">
<% if (success) { %>
<div class="alert alert-success">
<%= success %>
</div>
<% } %>
<% if (error) { %>
<div class="alert alert-error">
<%= error %>
</div>
<% } %>
現在可以先測試一下,應該已經可以註冊,並且每次註冊均會有反饋。
然後就是登入/登出的頁面
上面,登入介面已經做好了,登出直接點選就登出了,不需要額外製作介面。但是現在頁面沒有登出的介面,需要加上去。登出的按鈕只在登陸後才出現。
為此,可以修改header.ejs
<ul class="nav">
<li class="active"><a href="/">首頁</a></li>
<% if (!user) { %>
<li><a href="/login">登入</a></li>
<li><a href="/reg">註冊</a></li>
<% } else { %>
<li><a href="/logout">登出</a></li>
<% } %>
</ul>
做登入的響應,在index.js新增如下程式碼:
router.post('/login',function(req,res,next){
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','登入成功');
return res.redirect('/');
});
});
router.get('/logout',function(req,res,next){
req.session.user=null;
req.flash('success','登出成功');
res.redirect('/');
});
至此,登入登出功能已經完成。
接下來,對頁面許可權進行控制:
在index.js中新增
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();
}
並將程式碼修改為如下,可參考原始碼:
接著就是微博的介面了
為了方便,先做了微博模型,與User類似的Post。它的功能也是獲取與儲存,只不過資料從使用者資訊變成了發表的微博資訊。
首先,我們來思考一下,Post具體要做什麼呢?
Post建立的物件應該包含微博正文、使用者名稱、時間這三個資訊;
使用者發信息的時候,例項post.save()需要儲存微博正文、使用者名稱、時間這三個資訊,這些資訊都包含在例項中了,因此可以不傳進去,只需設定一個callback(err)即可。
Post.get是獲取微博,這裡的設想是有兩種模式,一種是指定使用者獲取,一種是獲取全部,因此其可以傳入使用者名稱或者null(顯示全部),另外需要一個callback。
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.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);
});
});
});
};
修改index.ejs,用於顯示微博文章。
<% include header.ejs %>
<% if (!user) { %>
<div class="hero-unit">
<h1>歡迎來到 Microblog</h1>
<p>Microblog 是一個基於 Node.js 的微博系統。</p>
<p>
<a class="btn btn-primary btn-large" href="/login">登入</a>
<a class="btn btn-large" href="/reg">立即註冊</a>
</p>
</div>
<% } else { %>
<% include say.ejs %>
<% } %>
<% include posts.ejs %>
<% include footer.ejs %>
這裡用到了say.ejs以及posts.ejs,可以參考原始碼,因為這個模板後面也不需要修改了,就不列出來了。
修改index.js,傳入微博資訊給模板:
var Post = require('../models/post.js');
/* GET home page. */
router.get('/', function(req, res, next) {
Post.get(null,function(err,posts){
if(err){
posts=[];
}
res.render('index', {
title: '首頁',
posts: posts,
});
});
});
//用於發表微博
router.post('/post',checkLogin);
router.post('/post',function(req,res,next){
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);
});
});
還需要加入一個使用者介面:
user.ejs
接著在index.js新增其響應
router.get('/u/:user',function(req,res){
User.get(req.params.user,function(err,user){
if(!user){
req.flash('error','使用者不存在');
res.redirect('/');
}
Post.get(user.name,function(err,posts){
if(err){
req.flash('/');
return redirect('error',err);
}
res.render('user',{
title:user.name,
posts:posts
});
});
});
});
至此,整個微博的案例基本完成了。