從0到1學習node(七)之express搭建簡易論壇

咱們須要搭建的這個簡易的論壇主要的功能有:註冊、登陸、發佈主題、回覆主題。下面咱們來一步步地講解這個系統是如何實現的。javascript

總索引: http://www.xiabingbao.com/node/2017/01/08/node-list.html
本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.htmlcss

1. 應用生成器

使用上節學習到express的知識,咱們也能夠從0開始,一步步把系統搭建起來。不過express中還有一個應用生成器,使用這個應用生成器能夠快速的建立一個應用的框架,而後咱們再在這個框架中完善咱們須要的內容。html

首先安裝應用生成器:java

$npm install -g express-generator

運行express --version若能正常輸出版本號,則安裝成功。node

咱們的論壇名稱能夠爲node_express_forum,而後使用express建立一個框架:mysql

$express node_express_forum

運行後,生成器會在這個目錄下生成幾個目錄和文件:jquery

create : node_express_forum                
 create : node_express_forum/package.json   
 create : node_express_forum/app.js         
 create : node_express_forum/public         
 create : node_express_forum/public/javascri
 create : node_express_forum/public/images  
 create : node_express_forum/public/styleshe
 create : node_express_forum/public/styleshe
 create : node_express_forum/routes         
 create : node_express_forum/routes/index.js
 create : node_express_forum/routes/users.js
 create : node_express_forum/views          
 create : node_express_forum/views/index.jad
 create : node_express_forum/views/layout.ja
 create : node_express_forum/views/error.jad
 create : node_express_forum/bin            
 create : node_express_forum/bin/www        
                                           
 install dependencies:                     
   $ cd node_express_forum && npm install   
                                           
 run the app:                              
   $ DEBUG=node-express-form:* npm start

已經生成成功。進入到這個目錄:git

$cd node_express_forum

咱們來看下生成的這個框架,方便知道後面怎麼進行填充。github

.
├── app.js 
├── package.json  // 依賴的模塊
├── bin
│   └── www
├── public  // 靜態文件目錄
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes  // 路由,至關於控制器
│   ├── index.js
│   └── users.js
└── views  // 視圖
    ├── error.jade
    ├── index.jade
    └── layout.jade

打開package.json後,咱們看到還須要再安裝幾個模塊才能運行:ajax

$npm install --save-dev

好了,到如今框架已搭建完畢,咱們來運行一下:

$npm start

而後在瀏覽器中輸入127.0.0.1:3000,就能夠看到了:Express Welcome to Express。

2. 準備工做

基本框架已經建立好了,如今開始咱們論壇的準備工做。這裏咱們的準備工做有3個:模板引擎,模型,數據庫,路由。

2.1 模板引擎

express裏默認使用的模板引擎是jade,不過咱們也能夠選擇其餘的模板引擎,我這裏選擇了ejs,由於感受ejs更像是個html文件,方便維護,固然,每一個人都有本身的喜愛。

$npm install ejs --save-dev

而後在app.js中修改模板引擎:

app.set('view engine', 'ejs'); // 原爲jade,現改成ejs

這裏我會將views目錄中的.jade文件清空,後續使用.ejs編寫模板。

2.2 模型

這裏咱們說的模型是指MVC中的M,主要是進行數據庫的鏈接和操做。建立models目錄用來存放文件。

2.3 數據庫

咱們使用mysql數據庫來存放數據,數據庫名稱能夠叫作forum。裏面有3張表:user, list, reply。

  • user表(用戶)的字段有: id, username, password, regtime
  • list表(主題)的字段有: id, uid(用戶id), title, content, createtime
  • reply表(回覆)的字段有: id, pid(主題id), uid(用戶id), content, createtime

2.4 路由

上節咱們是使用app.use()app.get()等方式來實現路由,同時,express還提供了express.Router類來建立模塊化。可掛載的路由。Router 實例是一個完整的中間件和路由系統,所以常稱其爲一個 「mini-app」。

routes/user.js中(這裏我將其users.js改成了user.js):

var express = require('express');
var router = express.Router(); // 實例化router

// 定義主頁的路由
router.get('/', function(req, res, next) {
    res.render('index', { title: 'user' }); // 加載index.ejs模板並傳遞數據給模板
});

router.get('/reg', function(req, res, next) {
    res.render('index', { title: 'reg' });
});

module.exports = router;

而後在app.js中加載路由模塊:

var user = require('./routes/user');
//...
app.use('/user', user);

這樣就能夠訪問/user/user/reg頁面了。若是須要增長其餘的路由,則依照此方式建立添加便可。

3. 註冊與登陸

咱們論壇的功能:註冊、登陸、發佈主題和回覆主題。這4個功能的實現都須要鏈接數據庫。

3.1 數據庫鏈接

引入mysql模塊,而後使用mysql.createPool()建立鏈接:

$npm install mysql --save-dev

在models目錄下建立db.js,其餘須要操做數據庫的,首先引入db.js:

var mysql = require('mysql');

var pool = mysql.createPool({
    host : '127.0.0.1',
    user : 'root',
    password : '123',
    database : 'forum'
});

module.exports = pool;

3.2 註冊功能

註冊功能的流程咱們很是熟悉了:

  1. 加載註冊頁面;
  2. 用戶輸入數據後提交;
  3. 處理表單數據而後進行數據庫操做

咱們在routes/user.js中建立一個reg的get方式的路由用來加載註冊頁面:

// routes/user.js

// get方式
router.get('/reg', function(req, res, next) {
    res.render('reg', { errmsg:'' }); // 加載reg.ejs模板
});

views目錄下建立reg.ejs

<!DOCTYPE html>
<html>
  <head>
    <title>註冊</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <style type="text/css">
      .tip{color: #f00;}
    </style>
  </head>
  <body>
  <div class="container">
    <p><a href="/">回到首頁</a></p>
    <h1>註冊</h1>
    <form action="/user/reg/" method="post">
      <% if(errmsg){ %>
      <p class="tip">*<%= errmsg %></p>
      <% } %>
      <p>用&nbsp;&nbsp;戶&nbsp;&nbsp;名: <input type="text" name="username" required="required"></p>
      <p>密&emsp;&emsp;碼: <input type="password" name="password" required="required"></p>
      <p>重複密碼: <input type="password" name="password2" required="required"></p>
      <p><input type="submit" name="submit" value="submit"></p>
    </form>
    <p>已有賬號? <a href="/user/login">點擊登陸</a></p>
  </div>
  </body>
</html>

運行程序,並用瀏覽器訪問127.0.0.1:3000/user/reg,註冊頁面就出來了。

而後再在routes/user.js中建立一個reg的post方式的路由用來處理提交過來的數據,post方式過來的數據並不能使用req.query變量獲取,而應該使用req.body

// routes/user.js

// post方式
router.post('/reg', function(req, res, next) {
    var username = req.body.username || '',
        password = req.body.password || '',
        password2 = req.body.password2 || '';

    if(password!=password2){
        res.render('reg', {errmsg:'密碼不一致'});
        return;
    }
    var password_hash = user_m.hash(password), // 對密碼進行加密
        regtime = parseInt(Date.now()/1000);

    // 數據庫處理
});

凡是設計到數據庫處理的,咱們都將其放到models中。這裏,咱們在models中建立一個user.js

// models/user.js

var pool = require('./db'), // 鏈接數據庫
    crypto = require('crypto'); // 對密碼進行加密

module.exports = {
    // 對字符串進行sha1加密
    hash : function(str){
        return crypto.createHmac('sha1', str).update('love').digest('hex');
    },

    // 註冊
    // 因數據庫操做是異步操做,則須要傳入回調函數來對結果進行處理,而不能使用return的方式
    reg : function(username, password, regtime, cb){
        pool.getConnection(function(err, connection){
            if(err) throw err;

            // 首先檢測用戶名是否存在
            connection.query('SELECT `id` FROM `user` WHERE `username`=?', [username], function(err, sele_res){
                if(err) throw err;

                // 若用戶名已存在,則直接回調
                if(sele_res.length){
                    cb({isExisted:true});
                    connection.release();
                }else{
                    // 不然將信息插入到數據庫中
                    var params = {username:username, password:password, regtime:regtime};
                    connection.query('INSERT INTO `user` SET ?', params, function(err, insert_res){
                        if(err) throw err;

                        cb(insert_res);
                        connection.release();
                        // 接下來connection已經沒法使用,它已經被返回到鏈接池中 
                    })
                }
            })
        });
    }
}

咱們將檢測用戶名和插入數據兩個功能放到一塊兒處理了,實際應用中,最好是在用戶提交數據以前就對用戶名進行檢測。註冊功能的model寫好以後,就能夠調用了,承接上面的代碼,從數據庫處理接着編寫。

// routes/user.js

var user_m = require('../models/user'); // 引入model

// post方式
router.post('/reg', function(req, res, next) {
    // 與上面的代碼同樣

    // 數據庫處理
    user_m.reg(username, password_hash, regtime, function(result){
        if(result.isExisted){
            res.render('reg', {errmsg:'用戶名已存在'}); // 從新加載註冊模板,並提示用戶名已存在
        }else if(result.affectedRows){
            // 註冊成功
            res.redirect('/');
        }else{
            // console.log('登陸失敗');
            res.render('reg', {errmsg:'註冊失敗,請從新嘗試'});
        }
    });
});

頁面若跳轉到首頁,則說明註冊成功了,查看下數據庫是否將數據正確的插入了。

到這裏,註冊功能完成了,完成了嗎?還沒呢,咱們這裏註冊完成後僅僅是跳轉到了首頁,還缺乏的操做是:

  • 若直接跳轉到首頁,則默認是已經登陸了,這裏就須要記錄用戶的登陸狀態;
  • 若不跳轉到首頁,則註冊成功後要跳轉到登陸頁面讓用戶登陸

咱們這裏使用第1種方式,稍後講解如何記錄用戶的登陸狀態。

3.2 登陸功能

登陸過程與註冊是很是相似的,並且比註冊還要簡單,只須要查詢數據庫中是否存在對應的用戶名和密碼便可。

首先編寫一個登陸頁面:

// views/login.ejs

<!DOCTYPE html>
<html>
  <head>
    <title>登陸</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <style type="text/css">
        .tip{color: #f00;}
    </style>
  </head>
  <body>
  <div class="container">
    <p><a href="/">回到首頁</a></p>
    <h1>登陸</h1>
    <form action="/user/login/" method="post">
        <% if(errmsg){ %>
        <p class="tip">*<%= errmsg %></p>
        <% } %>
        <p>用戶名: <input type="text" name="username" required="required"></p>
        <p>密&emsp;碼: <input type="password" name="password" required="required"></p>
        <p><input type="submit" name="submit" value="submit"></p>
    </form>
    <p>還沒賬號?<a href="/user/reg">點擊註冊</a></p>
  </div>
  </body>
</html>

而後在model/user.js中添加上對數據庫的登陸操做:

module.exports = {
    // ...

    // 登陸
    login : function(username, password, cb){
        pool.getConnection(function(err, connection){
            if(err) throw err;

            connection.query('SELECT `id` FROM `user` WHERE `username`=? AND `password`=?', [username, password], function(err, result){
                if(err) throw err;

                cb(result);
                connection.release();
                // 接下來connection已經沒法使用,它已經被返回到鏈接池中 
            })
        });
    }
}

最後在routes/user.js中添加上登陸的路由:

// routes/user.js 

// 進入到登陸頁面
router.get('/login', function(req, res, next) {
  res.render('login', {errmsg:''});
});

// 處理登陸請求
router.post('/login', function(req, res, next) {
    var username = req.body.username || '',
            password = req.body.password || '';

    var password_hash = user_m.hash(password);

    user_m.login(username, password_hash, function(result){
        if(result.length){
            // console.log( req.session );
            // req.session.user = {
            //  uid : result[0].id,
            //  username : username
            // }
            // res.redirect('/');
            res.send('登陸成功');
        }else{
            // console.log('登陸失敗');
            res.render('login', {errmsg:'用戶名或密碼錯誤'});
        }
    });
});

登陸功能也編寫好了。

3.3 保存登陸狀態

咱們一般是使用session來保存用戶的登陸狀態,express框架沒有對session處理的功能,須要咱們引入額外的模塊express-session

$npm install express-session --save-dev

而後在app.js中引用:

var session = require('express-session')

app.use(session({
  secret: 'wenzi', // 建議使用 128 個字符的隨機字符串
  cookie: { maxAge: 60*60*1000 }, // 設置時間
  resave : false,
  saveUninitialized : true
}));

設置完成後,就可使用session保存數據了。以登陸成功後保存數據爲例:

user_m.login(username, password_hash, function(result){
    if(result.length){
        // 將數據保存到名爲user的session中
        req.session.user = {
            uid : result[0].id,
            username : username
        }
        res.redirect('/');
    }else{
        // console.log('登陸失敗');
        res.render('login', {errmsg:'用戶名或密碼錯誤'});
    }
});

還有一個問題:如何把session中的數據傳遞給模板呢,好比沒有登陸時,顯示「註冊,登陸」鏈接,登陸後顯示「用戶名,登陸」信息。

app.js中添加:

app.use(function(req, res, next){
    // 若是session中存在,則說明已經登陸
    if( req.session.user ){
        res.locals.user = {
            uid : req.session.user.uid,
            username : req.session.user.username
        }
    }else{
        res.locals.user = {};
    }
    next();
})

而後在模板中就可使用user變量了:

<% if(user.uid){ %>
    <!-- 登陸狀態下 -->

<% }else{ %>
    <!-- 非登陸狀態下 -->

<% } %>

4. 首頁及詳情頁

咱們在首頁可以展現主題列表並能發表主題,點擊連接進入詳情頁後能該主題進行回覆。固然發表主題和對主題進行回覆都是在已經登陸的狀態進行的。

4.1 首頁

models目錄建立list.js,從數據庫中獲取主題列表:

// models/list.js

var pool = require('./db');

module.exports = {
    // 獲取首頁的主題
    getIndexList : function(cb){
        pool.getConnection(function(err, connection){
            if(err) throw err;

            // 連表查詢,獲取到做者的用戶名
            connection.query('SELECT `list`.*, username FROM `list`, `user` WHERE `list`.`uid`=`user`.`id`', function(err, result){
                if(err) throw err;

                cb(result);
                connection.release();
                // 接下來connection已經沒法使用,它已經被返回到鏈接池中 
            })
        });
    }
}

routes中的index.js中調用getIndexList獲取數據,並調用index.ejs模板:

// routes/index.js

var list_m = require('../models/list');

router.get('/', function(req, res, next) {
    list_m.getIndexList(function(result){
        res.render('index', { data:result }); // 選擇index模板並傳遞數據
    })
});

views/index.ejs建立首頁模板:

<h1>列表</h1>
<table>
  <tr>
    <td>標題</td>
    <td>做者</td>
    <td>建立時間</td>
  </tr>
  <% for(var i=0, len=data.length; i<len; i++) { %>
    <tr>
      <td><a href="/list/<%=data[i].id %>.html" title="<%=data[i].title %>"><%=data[i].title %></a></td>
      <td><%=data[i].username %></td>
      <td><%=data[i].createtime %></td>
    </tr>
  <% } %>
</table>
<% if(user.username){ %>
<!-- 在登陸狀態展現輸入框 -->
<div class="add">
    <p><input type="text" class="title"></p>
    <textarea class="content" cols="100" rows="10"></textarea>
    <p><input type="button" class="submit" value="提交"><span class="tip"></span></p>
</div>
<% }%>

展現數據完畢。

4.2 發表主題

咱們在首頁上添加了能夠輸入標題和內容的兩個輸入窗口,可使用ajax的方式提交數據。

<script type="text/javascript" src="http://mat1.gtimg.com/libs/jquery/1.12.0/jquery.min.js"></script>
<script type="text/javascript">
  var running = false;
  $('.submit').on('click', function(){
    if(running) return;
    running = true;
    $('.tip').text('');

    var title = $('.add .title').val();
        content = $('.add .content').val();
    if(!title || !content){
      $('.tip').text('*輸入不能爲空');
      return;
    }
    $('.tip').text('數據正在提交中...');

    $.ajax({
      url : '/list/addtopic', // 提交接口
      data : {title:title, content:content},
      dataType : 'json',
      type : 'get'
    }).done(function(result){
      if(result.code==0){
        var html = '<tr><td><a href="'+result.data.url+'" title="'+result.data.title+'">'+result.data.title+'</a></td><td>'+result.data.author+'</td><td>'+result.data.createtime+'</td></tr>';
        $('table').append(html);
        $('.tip').text('');
        $('.title, .content').val('');
      }else{
        $('.tip').text('添加失敗');
      }
      running = false;
    })
  })
</script>

這裏的提交接口是/list/addtopic,所以咱們須要再建立一個這樣的路由:在routes目錄下建立list.js:

// routes/list.js

router.get('/addtopic', function(req, res){
    // 在登陸狀態下能夠添加主題
    if(req.session.user){
        var title = req.query.title,
            content = req.query.content,
            uid = req.session.user.uid,
            createtime = parseInt(Date.now()/1000);

        var params = {uid:uid, title:title, content:content, createtime:createtime};

        list_m.addTopic(params, function(result){
            // console.log(result);
            if(result.affectedRows){
                res.json({code:0, msg:'添加成功', data:{url:'/list/'+result.insertId+'.html', title:title, author:req.session.user.username, createtime:createtime}});
            }else{
                res.json({code:2, msg:'添加失敗,請從新嘗試'})
            }
        });
        
    }else{
        res.json({code:1, msg:'您還未登陸'})
    }
})

這裏用到了list_m.addTopic,所以須要在models/list.js中添加 addTopic 方法:

// models/list.js

/*
    添加主題
    uid, title, content, createtime
*/
addTopic : function(params, cb){
    pool.getConnection(function(err, connection){
        if(err) throw err;

        connection.query('INSERT INTO `list` SET ?', params, function(err, result){
            if(err) throw err;

            cb(result);
            connection.release();
            // 接下來connection已經沒法使用,它已經被返回到鏈接池中 
        })
    });
}

4.3 詳情頁

在首頁列表中,能夠看到,咱們將詳情頁的連接設置爲了/list/1.html的方式,也能夠設置成其餘的方式(好比 /list?pid=1 等),只要設置好路由就行。

// routes/list.js

// http://127.0.0.1:3000/list/1.html
router.get('/:pid.html', function(req, res) {
    var pid = req.params.pid || 1;

    console.log(pid);
});

在詳情頁中須要獲取到這個主題的詳細信息和對這個主題的回覆,所以在list_m中:

// models/list.js

// 根據id查詢主題的詳情信息
getListById : function(id, cb){
    pool.getConnection(function(err, connection){
        if(err) throw err;

        connection.query('SELECT * FROM `list` WHERE `id`=?', [id], function(err, result){
            if(err) throw err;

            cb(result);
            connection.release();
            // 接下來connection已經沒法使用,它已經被返回到鏈接池中 
        })
    });
},

// 某主題的回覆
getReplyById : function(pid, cb){
    pool.getConnection(function(err, connection){
        if(err) throw err;

        connection.query('SELECT * FROM `reply` WHERE `pid`=?', [pid], function(err, result){
            if(err) throw err;

            cb(result);
            connection.release();
            // 接下來connection已經沒法使用,它已經被返回到鏈接池中 
        })
    });
}

而後在路由中進行調用,這裏使用async簡單的控制了下兩個異步的流程問題:

// http://127.0.0.1:3000/list/1.html
router.get('/:pid.html', function(req, res) {
    var pid = req.params.pid || 1;

    async.parallel([
        function(callback){
            list_m.getListById(pid, function(result){
                callback(null, result[0]);
            })
        },
        function(callback){
            list_m.getReplyById(pid, function(result){
                callback(null, result);
            })
        },
    ], function(err, results){
        // console.log( results );
        // res.json(results);
        res.render('list', { data:results });
    })
    
});

稍微解釋下async.parallel的功能,下節咱們會詳細的講解。 async.parallel([f1, f2, f3,..., fn], fb); 是f1到fn全部的異步都執行完了就會執行fb函數。這裏咱們是主題的詳情和對主題的回覆都請求完成了,就能夠調用模板渲染。

// views/list.ejs

<p><a href="/">返回到首頁</a></p>
<h1>詳情</h1>
<p>標題: <%=data[0].title %></p>
<div><%=data[0].content %></div>
<div class="reply">
  <h2>評論</h2>
  <div class="reply_con">
    <table>
      <% for(var i=0, len=data[1].length; i<len; i++) { %>
        <tr>
          <td><%=(i+1) %></td>
          <td><%=data[1][i].content %></td>
          <td><%=data[1][i].createtime %></td>
        </tr>
       <% } %>
    </table>
  </div>
</div>

對主題的回覆功能能夠本身實現一下。

總索引: http://www.xiabingbao.com/node/2017/01/08/node-list.html
本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html

5. 總結

寫着寫着就發現篇幅這麼大了,並且充斥了大量的代碼,須要咱們細心的理解。不少人可能從剛開始就想着,能不能在哪裏下載到完成的代碼呀,能夠的。完整的程序能夠個人github上進行下載:【node_express_form

相關文章
相關標籤/搜索