1. 程式人生 > >《nodejs開發指南》express4.x版-微博案例完整實現

《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頁面看看效果:
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
      });
    });
  });
});

至此,整個微博的案例基本完成了。