Node.js博客搭建

Node.js 博客搭建

一. 學習需求

Node 的安裝運行

會安裝node,搭建node環境javascript

會運行node。css

基礎模塊的使用

Buffer:二進制數據處理模塊html

Event:事件模塊前端

fs:文件系統模塊java

Net:網絡模塊node

Http:http模塊jquery

...git

NPM(node包管理工具)

第三方node模塊(包)的管理工具,可使用該下載工具安裝第三方模塊。,固然也能夠建立上傳本身的模塊。github

參考

假定已經理解並掌握了入門教程的全部內容。在易出錯的地方將進行簡要的說明。web

其它

這是最不起眼,但也是最必不可少的——你得準備一個博客的靜態文件。

博客的後臺界面,登陸註冊界面,文章展現界面,首頁等。


二. 項目需求分析

一個博客應當具有哪些功能?

前臺展現

  • 點擊下一頁,能夠點擊分類導航。
  • 能夠點擊進入到具體博文頁面
  • 下方容許評論。顯示發表時間。容許留言分頁。
  • 右側有登陸註冊界面。

後臺管理

  • 管理員帳號:登錄後看到頁面不同,有後臺頁面。
  • 容許添加新的分類。從後臺添加新的文章。
  • 編輯容許markdown寫法。
  • 評論管理。

三. 項目建立,安裝及初始化

技術框架

本項目採用瞭如下核心技術:

  • Node版本:6.9.1——基礎核心的開發語言

    (安裝後查看版本:cmd窗口:node -v)

(查看方式:cmd窗口:node -v

  • Express

    一個簡潔靈活的node.js WEB應用框架,提供一系列強大的特性幫助咱們建立web應用。

  • Mongodb

    用於保存產生的數據

還有一系列第三方模塊和中間件:

  • bodyParser,解析post請求數據
  • cookies:讀寫cookie
  • swig:模板解析引擎
  • mongoose:操做Mongodb數據
  • markdown:語法解析生成模塊

...

初始化

在W ebStorm建立一個新的空工程,指定文件夾。

打開左下角的Terminal輸入:

npm init

回車。而後讓你輸入name:(code),輸入項目名稱,而後後面均可以不填,最後在Is it OK?處寫上yes。

完成這一步操做以後,系統就會在當前文件夾建立一個package.json的項目文件。

項目文件下面擁有剛纔你所基本的信息。後期須要更改的話可直接在這裏修改。

第三方插件的安裝

  • 以Express爲例

    在命令行輸入:

    npm install --save express

    耐心等待一段時間,安裝完成後,json文件夾追加了一些新的內容:

    {
      //以前內容........
      "author": "",
      "license": "ISC",
      "dependencies": {
        "express": "^4.14.0"
      }

    表示安裝成功。

同理,使用npm install --save xxx的方法安裝下載如下模塊:

  • body-parser
  • cookies
  • markdown
  • mongoose
  • swig

因此安裝完以後的package.json文件是這樣的。

{
  "name": "blog",
  "version": "1.0.0",
  "description": "this is my first blog.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.15.2",
    "cookies": "^0.6.2",
    "express": "^4.14.0",
    "markdown": "^0.5.0",
    "mongoose": "^4.7.5",
    "swig": "^1.4.2"
  }
}

在這個json中,就能經過依賴模塊(dependencies)看到各個第三方模塊的版本信息

切記:依賴模塊安裝,要聯網!

安裝完成以後

第二個文件夾放的是你的第三方模塊。

此外還須要別的文件,完整的結構是這樣的——

接下來就把缺失的文件目錄本身創建起來。

完成着一系列操做以後,就把app.js做爲應用程序的啓動(入口頁面)。

建立應用

如下代碼建立應用,監聽端口

// 加載express
var express=require('express');
//建立app應用,至關於=>Node.js Http.createServer();
var app=express();
//監聽http請求
app.listen(9001);

運行(ctrl+shift+c)以後就能夠經過瀏覽器訪問了。

用戶訪問:

  • 用戶經過URL訪問web應用,好比http://localhost:9001/

這時候會發現瀏覽器呈現的內容是這樣的。

  • web後端根據用戶訪問的url處理不一樣的業務邏輯。

  • 路由綁定——

    在Express框架下,能夠經過app.get()app.post()等方式,把一個url路徑和(1-n)個函數進行綁定。當知足對應的規則時,對應的函數將會被執行,該函數有三個參數——

    app.get('/',function(req,res,next){
      // do sth.
    });
    // req:request對象,保存客戶請求相關的一些數據——http.request
    // res:response對象,服務端輸出對象,停工了一些服務端相關的輸出方法——http.response
    // next:方法,用於執行下一個和路徑相匹配的函數(行爲)。
  • 內容輸出

經過res.send(string)發送內容到客戶端。

app.get('/',function(req,res,next){
    res.send('<h1>歡迎光臨個人博客!</h1>');
});

運行。這時候網頁就打印出了h1標題的內容。

注意,js文件編碼若是不爲UTF-8,網頁文件顯示中文會受到影響。


三. 模板引擎的配置和使用

使用模板

如今,我想向後端發送的內容可不是一個h1標題那麼簡單。還包括整個博客頁面的html內容,若是仍是用上面的方法,麻煩就大了。

怎麼辦呢?關鍵步驟在於html和js頁面相分離(相似結構和行爲層的分離)。

模板的使用在於後端邏輯和前端表現的分離(先後端分離)。

模板配置

基本配置以下

// 定義模板引擎,使用swig.renderFile方法解析後綴爲html的文件
var swig=require('swig');
app.engine('html',swig.renderFile);

// 設置模板存放目錄
app.set('views','./views');
// 註冊模板引擎
app.set('view engine','html');

swig.setDefaults({cache:false});

配置模板的基本流程是:

請求swig模塊=>定義模板引擎=>註冊模板引擎=>設置調試方法

咱們可使用var swig=require('swig');定義了swig方法。

如下進行逐行解析——

定義模板引擎

app.engine('html',swig.renderFile);

第一個參數:模板引擎的名稱,同時也是模板引擎的後綴,你能夠定義打開的是任何文件格式,好比json,甚至tdl等。
第二個參數表示用於解析處理模板內容的方法。
第三個參數:使用swig.renderFile方法解析後綴爲html的文件。

設置模板目錄

如今就用express組件提供的set方法標設置模板目錄:

app.set('views','./views');

定義目錄時也有兩個參數,注意,第一個參數必須views!第二個參數能夠是咱們所給出的路徑。由於以前已經定義了模板文件夾爲views。因此,使用對應的路徑名爲./views

註冊模板引擎

app.set('view engine','html');

仍是使用express提供了set方法。
第一個參數必須是字符串'view engine'
第二個參數和app.engine方法定義的模板引擎名稱(第一個參數)必須是一致的(都是「html」)。

重回app.get

如今咱們回到app.get()方法裏面,使用res.render()方法從新渲染指定內容

app.get('/',function(req,res,next){

    /*
    * 讀取指定目錄下的指定文件,解析並返回給客戶端
    * 第一個參數:模板文件,相對於views目錄,views/index.html
    * */

    res.render('index');
});

這時候,咱們定義了返回值渲染index文件,就須要在views文件夾下新建立一個index.html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h1>歡迎來到個人第一個博客!<h1>
</body>
</html>

render方法還能夠接受第二個參數,用於傳遞模板使用的第二個數據。

好了。這時候再刷新頁面,就出現了index的內容。

調試方法

咱們在不中止服務器的狀況下,從新修改index的文件內容,發現並無刷新。

什麼問題呢?出於性能上考慮,node把第一次讀取的index放到了內容中,下次訪問時,就是緩存中的內容了,而不是真正的index文件。所以須要重啓。

開發過程當中,爲了減小麻煩,須要取消模板緩存。

swig.setDefaults({cache:false});

固然,當項目上線時,能夠把這一段刪除掉。


四. 靜態文件託管

在寫模板文件時,常常引入一些外鏈的css,js和圖片等等。

css怎麼引入?

若是咱們直接在首頁的head區域這麼寫:

<link rel="stylesheet" type="text/css" href="css.css"/>

再刷新,發現對css.css的引用失敗了。

問題不在於css.css是否存在,而在於請求失敗。由於外鏈文件本質也是一個請求,可是在app.js中尚未對應設置。

若是這麼寫:

app.get('/css.css', function (req,res,next) {
    res.send('body {background: red;}');
});

發現沒有效果。

打開http://localhost:9001/css.css發現內容是這樣的:

搞笑了。默認發送的是一個html。所以須要設定一個header

app.get('/css.css', function (req,res,next) {
    res.setHeader('content-type','text/css');
    res.send('body {background: red;}');
});

ctrl+F5,就解析了紅色背景了。

一樣的,靜態文件須要徹底分離,所以這種方法也是不行的。

靜態文件託管目錄

最好的方法是,把全部的靜態文件都放在一個public的目錄下,劃分並存放好。

而後在開頭就經過如下方法,把public目錄下的全部靜態文件都渲染了:

app.use('/public',express.static(__dirname+'/public'));

以上方法表示:當遇到public文件下的文件,都調用第二個參數裏的方法(注意是兩個下劃線)。

當用戶訪問的url以public開始,那麼直接返回對應__dirname+'public'下的文件。所以咱們的css應該放到public下。

引用方式爲:

<link rel="stylesheet" type="text/css" href="../public/css.css"/>

而後到public文件下建立一個css.css,設置body背景爲紅色。原來的app.get方法就不要了。

至此,靜態文件什麼的均可以用到了

小結

在以上的內容中,咱們實現了初始化項目,能夠調用html和css文件。基本過程邏輯是:

用戶發送http請求(url)=>解析路由=>找到匹配的規則=>指定綁定函數,返回對應內容到用戶。

訪問的是public:靜態——直接讀取指定目錄下的文件,返回給用戶。

=>動態=>處理業務邏輯

那麼整個基本雛形就搭建起來了。


五. 分模塊開發與實現

把整個網站放到一個app.js中,是不利於管理和維護的。實際開發中,是按照不一樣的功能,管理代碼。

根據功能劃分路由(routers)

根據本項目的業務邏輯,分爲三個模塊就夠了。

  • 前臺模塊
  • 後臺管理模塊
  • API模塊:經過ajax調用的接口。

或者,使用app.use(路由設置)劃分:

  • app.use('/admin',require('./routers/admin'));

    解釋:當用戶訪問的是admin文件下的內容,這調用router文件夾下admin.js文件。下同。

  • app.use('/api',require('./routers/api'));後臺

  • app.use('/',require('./routers/main'));前臺

好了。重寫下之前的代碼,去掉多餘的部分。

// 加載express
var express=require('express');
//建立app應用,至關於=>Node.js Http.createServer();
var app=express();

// 設置靜態文件託管
app.use('/public',express.static(__dirname+'/public'))

// 定義模板引擎,使用swig.renderFile方法解析後綴爲html的文件
var swig=require('swig');
app.engine('html',swig.renderFile);

// 設置模板存放目錄
app.set('views','./views');
// 註冊模板引擎
app.set('view engine','html');
// 調試優化
swig.setDefaults({cache:false});

//app.use('/admin',require('./routers/admin'));
//app.use('/api',require('./routers/api'));
//app.use('/',require('./routers/main'));


//監聽http請求
app.listen(9001);

routers建立一個admin.js,同理再建立一個api.js,一個main.js

怎麼訪問不一樣文件夾下的文件?

好比,我想訪問一個如http://localhost:9001/admin/user這樣的地址,這樣按理來講就應該調用admin.js(分路由)。

因此編輯admin.js

var express=require('express');

// 建立一個路由對象,此對象將會監聽admin文件下的url
var router=express.Router();

router.get('/user',function(req,res,next){
    res.send('user');
});

module.exports=router;//把router的結果做爲模塊的輸出返回出去!

注意,在分路由中,不須要寫明路徑,就當它是在admin文件下的相對路徑就能夠了。

儲存,而後回到app.js,應用app.use('/admin',require('./routers/admin'));

再打開頁面,就看到結果了。

同理,api.js也如法炮製。

var express=require('express');

// 建立一個路由對象,此對象將會監聽api文件夾下的url
var router=express.Router();

router.get('/user',function(req,res,next){
    res.send('api-user');
});

module.exports=router;//把router的結果做爲模塊的輸出返回出去!

再應用app.use('api/',require('./routers/api'))。重啓服務器,結果以下

首頁也如法炮製

路由的細分

前臺路由涉及了至關多的內容,所以再細化分多若干個路由也是不錯的選擇。

每一個內容包括基本的分類和增刪改

  • main模塊

    /——首頁

    /view——內容頁

  • api模塊

    /——首頁

    /login——用戶登錄

    /register——用戶註冊

    /comment——評論獲取

    /comment/post——評論提交

  • admin模塊

    /——首頁

    • 用戶管理

      /user——用戶列表

    • 分類管理

      /category——分類目錄

      /category/add——分類添加

      /category/edit——分類編輯

      /category/delete——分類刪除

    • 文章管理

      /article——內容列表

      /article/add——添加文章

      /article/edit——文章修改

      /article/delete——文章刪除

    • 評論管理

      /comment——評論列表

      /comment/delete——評論刪除

開發流程

功能開發順序

用戶——欄目——內容——評論

一切操做依賴於用戶,因此先須要用戶。

欄目也分爲先後臺,優先作後臺。

內容和評論相互關聯。

編碼順序

  • 經過Schema定義設計數據儲存結構
  • 功能邏輯
  • 頁面展現

六. 數據庫鏈接,表結構

好比用戶,在SCHEMA文件夾下新建一個users.js

如何定義一個模塊呢?這裏用到mongoose模塊

var mongoose=require('mongoose');//引入模塊

除了在users.js請求mongoose模塊之外,在app.js也須要引入mongoose。

// 加載express
var express=require('express')

//建立app應用,至關於=>Node.js Http.createServer();
var app=express();

// 加載數據庫模塊
var mongoose=require('mongoose');

// 設置靜態文件託管
app.use('/public',express.static(__dirname+'/public'))

// 定義模板引擎,使用swig.renderFile方法解析後綴爲html的文件
var swig=require('swig');
app.engine('html',swig.renderFile);

// 設置模板存放目錄
app.set('views','./views');
// 註冊模板引擎
app.set('view engine','html');
// 調試優化
swig.setDefaults({cache:false});

/*
* 根據不一樣的內容劃分路由器
* */
app.use('/admin',require('./routers/admin'));
app.use('/api',require('./routers/api'));
app.use('/',require('./routers/main'));



//監聽http請求
mongoose.connect();
app.listen(9001);

創建鏈接數據庫(每次運行都須要這樣)

mongoose使用須要安裝mongodb數據庫。

mongodb安裝比較簡單,在官網上下載了,制定好路徑就能夠了。

找到mongodb的bin文件夾。啓動mongod.exe——經過命令行

命令行依次輸入:

f:
cd Program Files\MongoDB\Server\3.2\bin

總之就是根據本身安裝的的路徑名來找到mongod.exe就好了。

開啓數據庫前須要指定參數,好比數據庫的路徑。我以前已經在項目文件夾下建立一個db文件夾,而後做爲數據庫的路徑就能夠了。

除此以外還得指定一個端口。好比27018

mongod --dbpath=G:\node\db --port=27018

而後回車

信息顯示:等待連接27018,證實開啓成功

下次每次關機後開啓服務器,都須要作如上操做。

接下來要開啓mongo.exe。

命令行比較原始,仍是可使用一些可視化的工具進行鏈接。在這裏我用的是robomongo。

直接在國外網站上下載便可,下載不通可能須要科學上下網。

名字隨便寫就好了,端口寫27018

點擊連接。

回到命令行。發現新出現如下信息:

表示正式創建鏈接。

數據保存

連接已經創建起來。但裏面空空如也。

接下來使用mongoose操做數據庫。

能夠上這裏去看看文檔。文檔上首頁就給出了mongoose.connect()方法。

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var Cat = mongoose.model('Cat', { name: String });

var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
  if (err) {
    console.log(err);
  } else {
    console.log('meow');
  }
});

connect方法接收的第一個參數,就是這個'mongodb://localhost:27018'。第二個參數是回調函數。

數據庫連接失敗的話,是不該該開啓監聽的,因此要把listen放到connect方法裏面。

mongoose.connect('mongodb://localhost:27018/blog',function(err){
    if(err){
        console.log('數據庫鏈接錯誤!');
    }else{
        console.log('數據庫鏈接成功!');
        app.listen(9001);
    }
});

運行,console顯示,數據庫連接成功。

注意,若是出現錯誤,仍是得看看編碼格式,必須爲UTF-8。

回到users.js的編輯上來,繼續看mongoose文檔。

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var blogSchema = new Schema({
  title:  String,
  author: String,
  body:   String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs:  Number
  }
});

經過mongoose.Schema構造函數,生成一個Schema對象。

new出的Schema對象包含不少內容,傳入的對象表明數據庫中的一個表。每一個屬性表明表中的每個字段,每一個值表明該字段存儲的數據類型。

在這裏,users.js須要暴露的內容就是用戶名和密碼。

// 加載數據庫模塊
var mongoose=require('mongoose');

// 返回用戶的表結構
module.exports= new mongoose.Schema({

    // 用戶名
    username: String,
    // 密碼
    password: String

});

而後在經過模型類來操做表結構。在項目的models文件夾下建立一個User.js

var mongoose=require('mongoose');

var usersSchema=require('../schemas/users');

module.exports=mongoose.model('User',usersSchema);

這樣就完成了一個模型類的建立。

模型怎麼用?仍是看看文檔給出的使用方法。

// 建立一個表結構對象
var schema = new mongoose.Schema({ name: 'string', size: 'string' });
// 根據表結構對象建立一個模型類
var Tank = mongoose.model('Tank', schema);

構造函數如何使用:

var Tank = mongoose.model('Tank', yourSchema);

var small = new Tank({ size: 'small' });
small.save(function (err) {
  if (err) return handleError(err);
  // saved!
})

// or

Tank.create({ size: 'small' }, function (err, small) {
  if (err) return handleError(err);
  // saved!
})

七. 用戶註冊的前端邏輯

引入首頁

用戶註冊首先得加載一個首頁。

在views下面新建一個main文件夾,而後把你以前寫好的index.html放進去。

因此回到main.js中。渲染你已經寫好的博客首頁。

var express=require('express');

// 建立一個路由對象,此對象將會監聽前臺文件夾下的url
var router=express.Router();

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

module.exports=router;//把router的結果做爲模塊的輸出返回出去!

保存,而後重啓app.js,就能在localhost:9001看到首頁了。

固然這個首頁很醜,你能夠本身寫一個。

原來的路徑所有按照項目文件夾的結構進行修改。

邏輯

註冊登陸一共有三個狀態。

一開始就是註冊,若是已有帳號就點擊登陸,出現登陸彈窗。

若是已經登陸,則顯示已經登陸狀態。並有註銷按鈕。

<div class="banner-wrap">
                <div class="login" id="register">
                <h3>註冊</h3>
                <span>用戶:<input name="username" type="text"/></span><br/>
                <span>密碼:<input name="password" type="text"/></span><br/>
                <span>確認:<input name="repassword" type="text"/></span><br/>
                <span><input class="submit" type="button" value="提交"/></span>
                <span>已有帳號?立刻<a href="javascript:;">登陸</a></span>
            </div>

            <div class="login" id="login" style="display:none;">
                <h3>登陸</h3>
                <span>用戶:<input type="text"/></span><br/>
                <span>密碼:<input type="text"/></span><br/>
                <span><input type="button" value="提交"/></span>
                <span>沒有帳號?立刻<a href="javascript:;">註冊</a></span>
            </div>

jquery能夠這麼寫:

$(function(){
    // 登陸註冊的切換
    $('#register a').click(function(){
        $('#login').show();
        $('#register').hide();
    });

    $('#login a').click(function(){
        $('#login').hide();
        $('#register').show();
    });
});

當點擊註冊按鈕,應該容許ajax提交數據。地址應該是api下的user文件夾的register,該register文件暫時沒有建立,因此不理他照寫便可。

// 點擊註冊按鈕,經過ajax提交數據
    $('#register .submit').click(function(){
        // 經過ajax提交交
        $.ajax({
            type:'post',
            url:'/api/user/register',
            data:{
                username:$('#register').find('[name="username"]').val(),
                password:$('#register').find('[name="password"]').val(),
                repassword:$('#register').find('[name="repassword"]').val()
            },
            dataType:'json',
            success:function(data){
                console.log(data);
            }
        });
    });

容許網站,輸入用戶名密碼點擊註冊。

雖然報錯,可是在chrome的network下的header能夠看到以前提交的信息。

挺好,挺好。


八. body-paser的使用:後端的基本驗證

後端怎麼響應前臺的ajax請求?

首先,找到API的模塊,增長一個路由,回到api.js——當收到前端ajax的post請求時,路由打印出一個register字符串。

var express=require('express');

// 建立一個路由對象,此對象將會監聽api文件夾下的url
var router=express.Router();

router.post('/user/register',function(req,res,next){
    console.log('register');
});

module.exports=router;//把router的結果做爲模塊的輸出返回出去!

這時候,就不會顯示404了。說明路由處理成功。

如何獲取前端post的數據?

這就須要用到新的第三方模塊——body-parser

相關文檔地址:https://github.com/expressjs/body-parser

bodyParser.urlencoded(options)

Returns middleware that only parses urlencoded bodies. This parser accepts only UTF-8 encoding of the body and supports automatic inflation of gzip and deflate encodings.

A new body object containing the parsed data is populated on the request object after the middleware (i.e. req.body). This object will contain key-value pairs, where the value can be a string or array (when extended is false), or any type (when extended is true).

var bodyParser=require('body-parser');

app.use(bodyParser.urlencoded(extended:true));

在app.js中,加入body-parser。而後經過app.use()方法調用。此時的app.js是這樣的:

// 加載express
var express=require('express');

//建立app應用,至關於=>Node.js Http.createServer();
var app=express();

// 加載數據庫模塊
var mongoose=require('mongoose');

// 加載body-parser,用以處理post提交過來的數據
var bodyParser=require('body-parser');

// 設置靜態文件託管
app.use('/public',express.static(__dirname+'/public'))

// 定義模板引擎,使用swig.renderFile方法解析後綴爲html的文件
var swig=require('swig');


app.engine('html',swig.renderFile);

// 設置模板存放目錄
app.set('views','./views');
// 註冊模板引擎
app.set('view engine','html');
// 調試優化
swig.setDefaults({cache:false});

// bodyParser設置
app.use(bodyParser.urlencoded({extended:true}));


/*
 * 根據不一樣的內容劃分路由器
 * */
app.use('/admin',require('./routers/admin'));
app.use('/api',require('./routers/api'));
app.use('/',require('./routers/main'));



//監聽http請求
mongoose.connect('mongodb://localhost:27018/blog',function(err){
    if(err){
        console.log('數據庫鏈接錯誤!');
    }else{
        console.log('數據庫鏈接成功!');
        app.listen(9001);
    }
});

配置好以後,回到api.js,就能在router.post方法中,經過req.body獲得提交過來的數據。

router.post('/user/register',function(req,res,next){
    console.log(req.body);
});

重啓app.js,而後網頁再次提交數據。

出現console信息:

後端的表單驗證

拿到數據以後,就是進行基本的表單驗證。好比

  • 用戶名是否符合規範(空?)
  • 是否被註冊
  • 密碼是否符合規範
  • 重複密碼是否一致

其中,檢測用戶名是否被註冊須要用到數據庫查詢。

因此按照這個邏輯,從新歸下類:

// 基本驗證=>用戶不得爲空(錯誤代碼1),密碼不得爲空(錯誤代碼2),兩次輸入必須一致(錯誤代碼3)
// 數據庫查詢=>用戶是否被註冊。

返回格式的初始化

咱們要對用戶的請求進行響應。對於返回的內容,應該作一個初始化,指定返回信息和錯誤代碼

// 統一返回格式
var responseData=null;

router.use(function(req,res,next){
    responseData={
        code:0,
        message:''
    }
    
    next();
});

寫出判斷邏輯,經過res.json返回給前端

res.json方法就是把響應的數據轉化爲一個json字符串。再直接return出去。後面代碼再也不執行。

router.post('/user/register',function(req,res,next){
    var username=req.body.username;
    var password=req.body.password;
    var repassword=req.body.repassword;

    //用戶名是否爲空
    if(username==''){
        responseData.code=1;
        responseData.message='用戶名不得爲空!';
        res.json(responseData);
        return;
    }

    if(password==''){
        responseData.code=2;
        responseData.message='密碼不得爲空!';
        res.json(responseData);
        return;
    }

    if(repassword!==password){
        responseData.code=3;
        responseData.message='兩次密碼不一致!';
        res.json(responseData);
        return;
    }
    
    responseData.message='註冊成功!';
    res.json(responseData);
});

基本運行就成功了。

基於數據庫的查重驗證

以前已經完成了簡單的驗證,基於數據庫怎麼驗證呢?

首先得請求模型中的user.js。

var User=require('../model/User');

這個對象有很是多的方法,再看看mongoose文檔:http://mongoosejs.com/docs/api.html#model-js

其中

// #方法表示必須new出一個具體對象才能使用
Model#save([options], [options.safe], [options.validateBeforeSave], [fn])

在這裏,咱們實際上就使用這個方法就夠了。

Model.findOne([conditions], [projection], [options], [callback])

在router.post方法內追加:

// 用戶名是否被註冊?
    User.findOne({
        username:username
    }).then(function(userInfo){
        console.log(userInfo);
    });

重啓運行發現返回的是一個null——若是存在,表示數據庫有該記錄。若是爲null,則保存到數據庫中。

因此完整的驗證方法是:

router.post('/user/register',function(req,res,next){
    var username=req.body.username;
    var password=req.body.password;
    var repassword=req.body.repassword;

    //基本驗證
    if(username==''){
        responseData.code=1;
        responseData.message='用戶名不得爲空!';
        res.json(responseData);
        return;
    }

    if(password==''){
        responseData.code=2;
        responseData.message='密碼不得爲空!';
        res.json(responseData);
        return;
    }

    if(repassword!==password){
        responseData.code=3;
        responseData.message='兩次密碼不一致!';
        res.json(responseData);
        return;
    }

    // 用戶名是否被註冊?
    User.findOne({
        username:username
    }).then(function(userInfo){
        if(userInfo){
            responseData.code=4;
            responseData.message='該用戶名已被註冊!';
            res.json(responseData);
            return;
        }else{//保存用戶名信息到數據庫中
            var user=new User({
                username:username,
                password:password,
            });
            return user.save();
        }
    }).then(function(newUserInfo){
        console.log(newUserInfo);
        responseData.message='註冊成功!';
        res.json(responseData);
    });

});

再查看console內容

若是你再次輸入該用戶名。會發現後臺console信息爲undefined,網頁控制檯顯示該用戶名已被註冊。

回到久違的Robomongo,能夠看到數據庫中多了一條註冊用戶的內容。

裏面確確實實存在了一條記錄。

在實際工做中,應該以加密的形式存儲內容。在這裏就不加密了。

前端對後臺返回數據的處理

如今後端的基本驗證就結束了。前端收到數據後應當如何使用?

回到index.js

我要作兩件事:

  • 把信息經過alert的形式展示出來。
  • 若是註冊成功,在用戶名處(#loginInfo)展示用戶名信息。這裏我把它加到導航欄最右邊。

暫時就這樣寫吧:

$(function(){
    // 登陸註冊的切換
    $('#register a').click(function(){
        $('#login').show();
        $('#register').hide();
    });

    $('#login a').click(function(){
        $('#login').hide();
        $('#register').show();
    });

    // 點擊註冊按鈕,經過ajax提交數據
    $('#register .submit').click(function(){
        // 經過ajax移交
        $.ajax({
            type:'post',
            url:'/api/user/register',
            data:{
                username:$('#register').find('[name="username"]').val(),
                password:$('#register').find('[name="password"]').val(),
                repassword:$('#register').find('[name="repassword"]').val()
            },
            dataType:'json',
            success:function(data){
                alert(data.message);
                if(!data.code){
                    // 註冊成功
                    $('#register').hide();
                    $('#login').show();
                }
            }
        });
    });
});

九. 用戶登陸邏輯

用戶登陸的邏輯相似,當用戶點擊登陸按鈕,一樣發送ajax請求到後端。後端再進行驗證。

基本設置

因此在index.js中,ajax方法也如法炮製:

// 點擊登陸按鈕,經過ajax提交數據
    $('#login .submit').click(function(){
        // 經過ajax提交
        $.ajax({
            type:'post',
            url:'/api/user/login',
            data:{
                username:$('#login').find('[name="username"]').val(),
                password:$('#login').find('[name="password"]').val(),
            },
            dataType:'json',
            success:function(data){
                console.log(data);
            }
        });
    });

回到後端api.js,新增一個路由:

// 登陸驗證
router.post('/user/login',function(res,req,next){
    var username=req.body.username;
    var password=req.body.password;

    if(username==''||password==''){
        responseData.code=1;
        responseData.message='用戶名和密碼不得爲空!';
        res.json(responseData);
        return;
    }


});

數據庫查詢:用戶名是否存在

一樣也是用到findOne方法。

router.post('/user/login',function(req,res,next){
    //console.log(req.body);
    var username=req.body.username;
    var password=req.body.password;
    
    if(username==''||password==''){
        responseData.code=1;
        responseData.message='用戶名和密碼不得爲空!';
        res.json(responseData);
        return;
    }

    // 查詢用戶名和對應密碼是否存在,若是存在則登陸成功
    User.findOne({
        username:username,
        password:password
    }).then(function(userInfo){
        if(!userInfo){
            responseData.code=2;
            responseData.message='用戶名或密碼錯誤!';
            res.json(responseData);
            return;
        }else{
            responseData.message='登陸成功!';
            res.json(responseData);
            return;
        }
    });

});

獲取登陸信息

以前登錄之後在#userInfo裏面顯示內容。

如今咱們來從新設置如下前端應該提示的東西:

  • 提示用戶名,若是是admin,則提示管理員,並增長管理按鈕
  • 註銷按鈕

這一切都是在導航欄面板上完成。

後端須要把用戶名返回出來。在後端的userInfo參數裏,已經包含了username的信息。因此把它也加到responseData中去。

<nav class="navbar">
                <ul>
                    <li><a href="index.html">首頁</a></li>
                    <li><a href="article.html">文章</a></li>
                    <li><a href="portfolio.html">做品</a></li>
                    <li><a href="about.html">關於</a></li>
                    <li>
                        <a id="loginInfo">
                            <span>未登陸</span>
                        </a>
                    </li>
                    <li><a  id="logout" href="javascript:;">
                        註銷
                    </a></li>
                </ul>
            </nav>

導航的結構大體如是,而後有一個註銷按鈕,display爲none。

因而index.js能夠這麼寫:

// 點擊登陸按鈕,經過ajax提交數據
    $('#login .submit').click(function(){
        // 經過ajax提交
        $.ajax({
            type:'post',
            url:'/api/user/login',
            data:{
                username:$('#login').find('[name="username"]').val(),
                password:$('#login').find('[name="password"]').val(),
            },
            dataType:'json',
            success:function(data){
                alert(data.message);
                if(!data.code){
                    $('#login').slideUp(1000,function(){
                        $('#loginInfo span').text('你好,'+data.userInfo)
                        $('#logout').show();
                    });
                }
            }
        });
    });

這一套簡單的邏輯也完成了。


十. cookie設置

當你登錄成功以後再刷新頁面,發現並非登陸狀態。這很蛋疼。

記錄登陸狀態應該反饋給瀏覽器。

cookie模塊的調用

在app.js中引入cookie模塊——

var Cookies=require('cookies');

app.use(function(req,res){
  req.cookies=new Cookies(req,res);
  next();
});

回到api.js,在登錄成功以後,還得作一件事情,就是把cookies發送給前端。

}else{
            responseData.message='登陸成功!';
            responseData.userInfo=userInfo.username;
          
            //每當用戶訪問站點,將保存用戶信息。
            req.cookies.set('userInfo',JSON.stringify({
                    _id:userInfo._id,
                    username:userInfo.username
                });
            );//把id和用戶名做爲一個對象存到一個名字爲「userInfo」的對象裏面。
          
            res.json(responseData);
            return;
        }

重啓服務器,登陸。在network上看cookie信息

再刷新瀏覽器,查看headers

也多了一個userInfo,證實可用。

處理cookies信息

//設置cookie
app.use(function(req,res,next){
    req.cookies=new Cookies(req,res);

    // 解析cookie信息把它由字符串轉化爲對象
    if(req.cookies.get('userInfo')){
        try {
            req.userInfo=JSON.parse(req.cookies.get('userInfo'));;
        }catch(e){}
    }
    next();
});

調用模板去使用這些數據。

回到main.js

var express=require('express');

var router=express.Router();

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

module.exports=router;

而後就在index.html中寫模板。

模板語法

模板語法是根據從後端返回的信息在html裏寫邏輯的方法。

全部邏輯內容都在{%%}裏面

簡單的應用就是if else

{% if userInfo._id %}
<div id="div1"></div>
{% else %}
<div id="div2"></div>
{% endif %}

若是後端返回的內容存在,則渲染div1,不然渲染div2,這個語句到div2就結束。

因此,如今咱們的渲染邏輯是:

  • 如userInfo._id存在,則直接渲染導航欄裏的我的信息
  • 不然,渲染登陸註冊頁面。
  • 博客下面的內容也是如此。最好讓登陸的人才看得見。

若是我須要顯示userInfo裏的username,須要雙大括號{{userInfo.username}}

登陸後的邏輯

這樣一來,登錄後的效果就不必了。直接重載頁面。

if(!data.code){
   window.location.reload();
}

而後順便把註銷按鈕也作了。

註銷無非是把cookie設置爲空,而後前端所作的事情就是一個一個ajax請求,一個跳轉。

index.js

// 註銷模塊
    $('#logout').click(function(){
        $.ajax({
            type:'get',
            url:'/api/user/logout',
            success:function(data){
                if(!data.code){
                    window.location.reload();
                }
            }
        });
    });

在api.js寫一個退出的方法

// 退出方法
router.get('/user/logout',function(req,res){
    req.cookies.set('userInfo',JSON.stringify({
        _id:null,
        username:null
    }));
    res.json(responseData);
    return;
});

十一. 區分管理員和普通用戶

建立管理員

管理員用戶表面上看起來也是用戶,可是在數據庫結構是獨立的一個字段,

打開users.js,新增一個字段

var mongoose=require('mongoose');

// 用戶的表結構
module.exports= new mongoose.Schema({

    username: String,
    password: String,

    // 是否管理員
    isAdmin:{
        type:Boolean,
        default:false
    }

});

爲了記錄方便,我直接在RoboMongo中設置。

添加的帳號這麼寫:

保存。

那麼這個管理員權限的帳戶就建立成功了。

cookie設置

注意,管理員的帳戶最好不要記錄在cookie中。

回到app.js,重寫cookie代碼

//請求User模型
var User=require('./models/User');

 //設置cookie
app.use(function(req,res,next){
    req.cookies=new Cookies(req,res);

    // 解析cookie信息
    if(req.cookies.get('userInfo')){
        try {
            req.userInfo=JSON.parse(req.cookies.get('userInfo'));

            // 獲取當前用戶登陸的類型,是否管理員
            User.findById(req.userInfo._id).then(function(userInfo){
                req.userInfo.isAdmin=Boolean(userInfo.isAdmin);

                next();
            });
        }catch(e){
            next();
        }
    }else{
        next();
    }

});

整體思路是,根據isAdmin判斷是否爲真,

管理員顯示判斷

以前html顯示的的判斷是:{{userInfo.username}}

如今把歡迎信息改寫成「管理員」,並提示「進入後臺按鈕」

<li>
    <a  id="loginInfo">
    {% if userInfo.isAdmin %}
    <span id="admin" style="cursor:pointer;">管理員你好,進入管理</span>
    {% else %}
    <span>{{userInfo.username}}</span>
    {% endif %}
     </a>
</li>

很棒吧!


十二. 後臺管理功能及界面

打開網站,登陸管理員用戶,以前已經作出了進入管理連接。

基本邏輯

咱們要求打開的網址是:http://localhost:9001/admin。後臺管理是基於admin.js上進行的。

先對admin.js作以下測試:

var express=require('express');


var router=express.Router();

router.use(function(req,res,next){
    if(!req.userInfo.isAdmin){
        // 若是當前用戶不是管理員
        res.send('不是管理員!');
        return;
    }else{
        next();
    }
});

router.get('/',function(res,req,next){
   res.send('管理首頁');
});

module.exports=router;

當登陸用戶不是管理員。直接顯示「不是管理員」

後臺界面的前端實現

後臺意味着你要寫一個後臺界面。這個index頁面放在view>admin文件夾下。因此router應該是:

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

因此你還得在admin文件夾寫一個index.html

後臺管理基於如下結構:

  • 首頁
  • 設置
  • 分類管理
  • 文章管理
  • 評論管理

由於是臨時寫的,湊合着看大概是這樣。

<header>
    <h1>後臺管理系統</h1>

</header>
<span class="userInfo">你好,{{userInfo.username}}! <a href="javascript:;">退出</a></span>
<aside>
    <ul>
        <li><a href="javascript:;">首頁</a></li>
        <li><a href="javascript:;">設置</a></li>
        <li><a href="/admin/user">用戶管理</a></li>
        <li><a href="javascript:;">分類管理</a></li>
        <li><a href="javascript:;">文章管理</a></li>
        <li><a href="javascript:;">評論管理</a></li>
    </ul>
</aside>
<section>
    {% block main %}{% endblock %}
</section>
<footer></footer>

父類模板

這個代碼應該是可複用的。所以可使用父類模板的功能。

繼承

在同文件夾下新建一個layout.html。把前端代碼所有剪切進去。這時候admin/index.html一個字符也不剩了。

怎麼訪問呢?

在index下面,輸入:

{% extends 'layout.html' %}

再刷新localhost:9001/admin,發現頁面又回來了。

有了父類模板的功能,咱們能夠作不少事情了。

非公用的模板元素

相似面向對象的繼承,右下方區域是不一樣的內容,不該該寫進layout中,所以能夠寫爲

<section>
{% block 佔位區塊名稱 %}{% endblock %}
</section>

而後回到index.html,定義這個區塊的內容

{% block main %}
<!-- 你的html內容 -->
{% endblock %}

十三. 用戶管理

需求:點擊「用戶管理」,右下方的主體頁面顯示博客的註冊用戶數量。

因此連接應該是:

<li><a href="/admin/user">用戶管理</a></li>

其實作到這塊,應該都熟悉流程了。每增長一個新的頁面,意味着寫一個新的路由。在路由裏渲染一個新的模板。在渲染的第二個參數裏,以對象的方式寫好你準備用於渲染的信息。

回到admin.js

router.get('/user/',function(req,res,next){
    res.render('admin/user_index',{
        userInfo:req.userInfo
    })
});

爲了和index區分,新的頁面定義爲user_index。所以在view/admin文件夾下建立一個user_index.html

先作個簡單的測試吧

{% extends 'layout.html' %}

{% block main %}
用戶列表
{% endblock %}

點擊就出現了列表。

接下來就是要從數據庫中讀取全部的用戶數據。而後傳進模板中。

讀取用戶數據

model下的User.js輸出的對象含有咱們須要的方法。

咱們的User.js是這樣的

var mongoose=require('mongoose');

// 用戶的表結構
var usersSchema=require('../schemas/users');

module.exports=mongoose.model('User',usersSchema);

回到admin.js

var User=reuire('/model/User.js');

User有一個方法是find方法,返回的是一個promise對象

試着打印出來:

User.find().then(function(user){
        console.log(user);
    });

結果一看,厲害了:

當前博客的兩個用戶都打印出來了。

接下來就是把這個對象傳進去了,就跟傳ajax同樣:

var User=require('../models/User');
//用戶管理

User.find().then(function(user){
    router.get('/user/', function (req,res,next) {
        res.render('admin/user_index',{
            userInfo:req.userInfo,
            users:user
        })
    })
});

模板就能使用用戶數據了。

模板如何使用後臺傳進來的用戶對象數據

main的展現區中,應該是一個標題。下面是一串表格數據。

大體效果如圖

這須要模板中的循環語法

{% extends 'layout.html' %}

{% block main %}
<h3>用戶列表</h3>

<table class="users-list">
    <thead>
        <tr>
            <th>id</th>
            <th>用戶名</th>
            <th>密碼</th>
            <th>是否管理員</th>
        </tr>
    </thead>
    <tbody>
        {% for user in users %}
        <tr>
            <td>{{user._id.toString()}}</td>
            <td>{{user.username}}</td>
            <td>{{user.password}}</td>
            <td>
                {% if user.isAdmin %}
                是
                {% else %}
                不是
                {% endif %}
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

顯示結果如圖

分頁顯示(limit方法)

實際上用戶多了,就須要分頁

假設咱們分頁只須要對User對象執行一個limit方法。好比我想每頁只展現1條用戶數據:

router.get('/user/', function (req,res,next) {
    User.find().limit(1).then(function(user){
        res.render('admin/user_index',{
            userInfo:req.userInfo,
            users:user
        });
    });
});

分頁展現設置(skip)

User的skip方法用於設置截取位置。好比skip(2),表示從第3條開始取。

好比我想每頁設置兩條數據:

  • 第一頁:1=> skip(0)
  • 第二頁:2=>skip(1)
  • 所以,當我要在第page頁展現limit條數據時,skip方法裏的數字參數爲:(page-1)*limit

好比我要展現第二頁數據:

router.get('/user/', function (req,res,next) {
    var page=2;
    var limit=1;
    var skip=(page-1)*limit;

    User.find().limit(limit).skip(skip).then(function(user){
        res.render('admin/user_index',{
            userInfo:req.userInfo,
            users:user
        });
    });
});

可是究竟有多少頁不是咱們所能決定的。

有多少頁?(req.query.page)

首先要解決怎麼用戶怎麼訪問下一頁的問題,通常來講,在網頁中輸入http://localhost:9001/admin/user?pages=數字

就能夠經過頁面訪問到。

既然page不能定死,那就把page寫活。

var page=req.query.page||1;

這樣就解決了

分頁按鈕

又回到了前端。

分頁按鈕是直接作在表格的後面。

到目前爲止,寫一個「上一頁」和「下一頁」的邏輯就行了——當在第一頁時,上一頁不顯示,當在第最後一頁時,下一頁不顯示

首先,把page傳到前端去:

router.get('/user/', function (req,res,next) {
    var page=req.query.page||1;
    var limit=1;
    var skip=(page-1)*limit;

    User.find().limit(limit).skip(skip).then(function(user){
        res.render('admin/user_index',{
            userInfo:req.userInfo,
            users:user,
            page:page
        });
    });
});

注意,傳到前端的page是個字符串形式的數字,因此使用時必須轉化爲數字。

查詢總頁數(User.count)

user.count是一個promise對象,

User.count().then(function(count){
  console.log(count);
})

這個count就是總記錄條數。把這個count獲取到以後,計算出須要多少頁(向上取整),傳進渲染的對象中。注意,這些操做都是異步的。因此不能用變量儲存count。而應該把以前的渲染代碼寫到then的函數中

還有一個問題是頁面取值。不該當出現page=200這樣不合理的數字。因此用min方法取值。

router.get('/user/', function (req,res,next) {
    var page=req.query.page||1;
    var limit=1;
    var count=0;

    User.count().then(function(_count){
        count=_count;
        var pages=Math.ceil(count/limit);
        console.log(count);

        page=Math.min(page,pages);
        page=Math.max(page,1);

        var skip=(page-1)*limit;

        User.find().limit(limit).skip(skip).then(function(user){
            res.render('admin/user_index',{
                userInfo:req.userInfo,
                users:user,
                page:page,
                pages:pages
            });
        });
    });//獲取總頁數
});

添加表格信息

須要在表頭作一個簡單的統計,包括以下信息

  • 一共有多少條用戶記錄
  • 每頁顯示:多少條
  • 共多少頁
  • 當前是第多少頁

所以應該這麼寫:

router.get('/user/', function (req,res,next) {
    var page=req.query.page||1;
    var limit=1;
    var count=0;

    User.count().then(function(_count){
        count=_count;
        var pages=Math.ceil(count/limit);


        page=Math.min(page,pages);
        page=Math.max(page,1);

        var skip=(page-1)*limit;

        User.find().limit(limit).skip(skip).then(function(user){
            res.render('admin/user_index',{
                userInfo:req.userInfo,
                users:user,
                page:page,
                pages:pages,
                limit:limit,
                count:count
            });
        });
    });//獲取總頁數
});

前端模板能夠這樣寫:

{% extends 'layout.html' %}

{% block main %}
<h3>用戶列表    <small>(第{{page}}頁)</small></h3>

<table class="users-list">
    <thead>
        <tr>
            <th>id</th>
            <th>用戶名</th>
            <th>密碼</th>
            <th>是否管理員</th>
        </tr>
    </thead>
    <tbody>
        {% for user in users %}
        <tr>
            <td>{{user._id.toString()}}</td>
            <td>{{user.username}}</td>
            <td>{{user.password}}</td>
            <td>
                {% if user.isAdmin %}
                是
                {% else %}
                不是
                {% endif %}
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<p class="table-info">一共有{{count}}個用戶,每頁顯示{{limit}}個。</p>

<ul class="page-btn">
    {% if Number(page)-1!==0 %}
    <li><a href="/admin/user?page={{Number(page)-1}}">上一頁</a></li>
    {% else %}
    <li>再往前..沒有了</li>
    {% endif %}
    {% if Number(page)+1<=pages %}
    <li><a href="/admin/user?page={{Number(page)+1}}">下一頁</a></li>
    {% else %}
    <li>已經是最後一頁</li>
    {% endif %}
</ul>

{% endblock %}

效果如圖

封裝

分頁是一個極其經常使用的形式,能夠考慮把它封裝一下。

同目錄下新建一個page.html

把按鈕組件放進去。

{%include 'page.html'%}

結果有個問題,裏面有一條寫死的url(admin/xxx),爲了解決,能夠設置爲...admin/{{type}}?page=yyy,而後把回到admin.js,把type做爲一個屬性傳進去。

那麼用戶管理部分就到此結束了。


十四. 博客分類管理

前面已經實現了那麼多頁面,如今嘗試實現博客內容的分類管理。

基本設置

首先把分類管理的連接修改成/category/,在admin.js中增長一個對應的路由。渲染的模板爲admin/category_inndex.html

路由器基本寫法:

router.get('/category/',function(req,res,next){

    res.render('admin/category_index',{
        userInfo:req.userInfo
    });
});

模板基本結構:

{% extends 'layout.html' %}

{% block main %}

{% endblock %}

點擊「分類管理」,請求的頁面就出來了。固然仍是一個空模板。

分類管理的特殊之處在於,它下面有兩個子菜單(分類首頁,管理分類)。對此咱們能夠用jQuery實現基本動效。

html結構

<li id="category">
            <a href="/admin/category">分類管理</a>
            <ul class="dropdown">
                <li><a href="javascript:;">管理首頁</a></li>
                <li><a href="/admin/category/add">添加分類</a></li>
            </ul>
        </li>

jq

$('#category').hover(function(){
        $(this).find('.dropdown').stop().slideDown(400);
    },function(){
        $(this).find('.dropdown').stop().slideUp(400);
    });

仍是得佈局。

佈局的基本設置仍是遵循用戶的列表——一個大標題,一個表格。

添加分類頁面

分類頁面下面單獨有個頁面,叫作「添加分類「。

基本實現

根據上面的邏輯再寫一個添加分類的路由

admin.js:

// 添加分類
router.get('/category/add',function(req,res,next){
    res.render('admin/category_add',{
        userInfo:req.userInfo
    });
});

同理,再添加一個category_add模板,大體這樣:

{% extends 'layout.html' %}

{% block main %}

<h3>添加分類    <small>>表單</small></h3>

<form>
    <span>分類名</span><br/>
    <input type="text" name="name"/>
    <button type="submit">提交</button>
</form>

{%include 'page.html'%}
{% endblock %}

目前還很是簡陋可是先實現功能再說。

添加邏輯

添加提交方式爲post。

<form method="post">
  <!--balabala-->
</form>

因此路由器還得寫個post形式的函數。

// 添加分類及保存方法:post
router.post('/category/add',function(req,res,next){

});

post提交的結果,仍是返回當前的頁面。

post提交到哪裏?固然仍是數據庫。因此在schemas中新建一個提交數據庫。categories.js

var mongoose=require('mongoose');

// 博客分類的表結構
module.exports= new mongoose.Schema({
    // 分類名稱
    name: String,

});

好了。跟用戶註冊同樣,再到model文件夾下面添加一個model添加一個Categories.js:

var mongoose=require('mongoose');

// 博客分類的表結構
var categoriessSchema=require('../schemas/categories');

module.exports=mongoose.model('Category',categoriessSchema);

文件看起來不少,但思路清晰以後至關簡單。

完成這一步,就能夠在admin.js添加Category對象了。

admin.js的路由操做:處理前端數據

還記得bodyparser麼?前端提交過來的數據都由它進行預處理:

// app.js
app.use(bodyParser.urlencoded({extended:true}));

有了它,就能夠經過req.body來進行獲取數據了。

刷新,提交內容。

在post方法函數中打印req.body:

在這裏我點擊了兩次,其中第一次沒有提交數據。記錄爲空字符串。這在規則中是不容許的。因此應該返回一個錯誤頁面。

// 添加分類及保存方法:post
var Category=require('../models/Categories');
router.post('/category/add',function(req,res,next){

    //處理前端數據
    var name=req.body.name||'';
    if(name===''){
        res.render('admin/error',{
          userInfo:req.userInfo
        });
    }
});

錯誤頁面,最好寫一個返回上一步(javascript:window.history.back())。

<!--error.html-->
{% extends 'layout.html' %}}

{% block main %}
<h3>出錯了</h3>
<h4>你必定有東西忘了填寫!</h4>
<a href="javascript:window.history.back()">返回上一步</a>
{% endblock %}

錯誤頁面應該是可複用的。但的渲染須要傳遞哪些數據?

  • 錯誤信息(message)
  • 操做,返回上一步仍是跳轉其它頁面?
  • url,跳轉到哪裏?

就當前項目來講,大概這樣就好了。

res.render('admin/error',{
            userInfo:req.userInfo,
            message:'提交的內容不得爲空!',
            operation:{
                url:'javascript:window.history.back()',
                operation:'返回上一步'
            }
        });

模板頁面:

{% extends 'layout.html' %}}

{% block main %}
<h3>出錯了</h3>
<h4>{{message}}</h4>
<a href={{operation.url}}>{{operation.operation}}</a>
{% endblock %}

若是名稱不爲空(save方法)

顯然,這個和用戶名的驗證是同樣的。用findOne方法,在返回的promise對象執行then。返回一個新的目錄,再執行then。進行渲染。

其次,須要一個成功頁面。基本結構和錯誤界面同樣。只是h3標題不一樣

// 查詢數據是否爲空
    Category.findOne({
        name:name
    }).then(function(rs){
        if(rs){//數據庫已經有分類
            res.render('admin/error',{
                userInfo:req.userInfo,
                message:'數據庫已經有該分類了哦。',
                operation:{
                    url:'javascript:window.history.back()',
                    operation:'返回上一步'
                }
            });
            return Promise.reject();
        }else{//不然表示數據庫不存在該記錄,能夠保存。
            return  new Category({
                name:name
            }).save();
        }
    }).then(function(newCategory){
        res.render('admin/success',{
            userInfo:req.userInfo,
            message:'分類保存成功!',
            operation:{
                url:'javascript:window.history.back()',
                operation:'返回上一步'
            }
        })
    });
});

接下來的事就又交給前端了。

數據可視化

顯然,渲染的分類管理頁面應該還有一個表格。如今順便把它完成了。其實基本邏輯和以前的用戶分類顯示是同樣的。並且代碼極度重複:

// 添加分類及保存方法
var Category=require('../models/Categories');


router.get('/category/', function (req,res,next) {
    var page=req.query.page||1;
    var limit=2;
    var count=0;

    Category.count().then(function(_count){
        count=_count;
        var pages=Math.ceil(count/limit);

        page=Math.min(page,pages);
        page=Math.max(page,1);

        var skip=(page-1)*limit;

        Category.find().limit(limit).skip(skip).then(function(categories){

            res.render('admin/category_index',{
                type:'category',
                userInfo:req.userInfo,
                categories:categories,
                page:page,
                pages:pages,
                limit:limit,
                count:count
            });
        });
    });//獲取總頁數
});

能夠封裝成函數了——一下就少了三分之二的代碼量。

function renderAdminTable(obj,type,limit){
    router.get('/'+type+'/', function (req,res,next) {
        var page=req.query.page||1;

        var count=0;

        obj.count().then(function(_count){
            count=_count;
            var pages=Math.ceil(count/limit);

            page=Math.min(page,pages);
            page=Math.max(page,1);

            var skip=(page-1)*limit;

            obj.find().limit(limit).skip(skip).then(function(data){

                res.render('admin/'+type+'_index',{
                    type:type,
                    userInfo:req.userInfo,
                    data:data,
                    page:page,
                    pages:pages,
                    limit:limit,
                    count:count
                });
            });
        });//獲取總頁數
    });
}
//調用時,
//用戶管理首頁
var User=require('../models/User');
renderAdminTable(User,'user',1);
//分類管理首頁
// 添加分類及保存方法
var Category=require('../models/Categories');
renderAdminTable(Category,'category',2);

模板

{% extends 'layout.html' %}

{% block main %}

<h3>分類列表</h3>

<table class="users-list">
    <thead>
    <tr>
        <th>id</th>
        <th>分類名</th>
        <th>備註</th>
        <th>操做</th>
    </tr>
    </thead>
    <tbody>
    {% for category in data %}
    <tr>
        <td>{{category._id.toString()}}</td>
        <td>{{category.name}}</td>
        <td>
          <a href="/admin/category/edit">修改 </a>
            |<a href="/admin/category/edit"> 刪除</a>
        </td>
        <td></td>
    </tr>
    {% endfor %}
    </tbody>
</table>

{%include 'page.html'%}
{% endblock %}

博客分類的修改與刪除

基本邏輯

刪除的按鈕是/admin/category/delete?id={{category._id.toString()}},同理修改的按鈕是/admin/category/edit?id={{category._id.toDtring()}}(帶id的請求)。

這意味着兩個新的頁面和路由:

分類修改,分類刪除。

刪除和修改都遵循一套比較嚴謹的邏輯。其中修改的各類判斷至關麻煩,可是,修改和刪除的邏輯基本是同樣的。

當一個管理員在進行修改時,另外一個管理員也可能修改(刪除)了數據。所以須要嚴格判斷。

修改(update)

修改首先作的是邏輯,根據發送請求的id值進行修改。若是id不存在則返回錯誤頁面,若是存在,則切換到新的提交頁面

// 分類修改
router.get('/category/edit',function(req,res,next){

    // 獲取修改的分類信息,並以表單的形式呈現,注意不能用body,_id是個對象,不是字符串
    var id=req.query.id||'';

    // 獲取要修改的分類信息
    Category.findOne({
        _id:id
    }).then(function(category){
        if(!category){
            res.render('admin/error',{
                userInfo:req.userInfo,
                message:'分類信息不存在!'
            });
            return Promise.reject();
        }else{
            res.render('admin/edit',{
                userInfo:req.userInfo,
                category:category
            });
        }
    });
});

而後是一個提交頁,post返回的是當前頁面

{% extends 'layout.html' %}

{% block main %}

<h3>分類管理    <small>>編輯分類</small></h3>

<form method="post">
    <span>分類名</span><br/>
    <input type="text" value="{{category.name}}" name="name"/>
    <button type="submit">提交</button>
</form>

仍是以post請求保存數據。

  • 提交數據一樣也須要判斷id,當id不存在時,跳轉到錯誤頁面。

  • 當id存在,並且用戶沒有作任何修改,就提交,直接跳轉到「修改爲功」頁面。實際上不作任何修改。

  • 當id存在,並且用戶提交過來的名字和非原id({$ne: id})下的名字不一樣時,作兩點判斷:

    • 數據庫是否存在同名數據?是則跳轉到錯誤頁面。

    • 若是數據庫不存在同名數據,則更新同id下的name數據值,並跳轉「保存成功」。

      更新的方法是

      Category.update({
        _id:你的id
      },{
        要修改的key:要修改的value
      })

根據此邏輯能夠寫出這樣的代碼。

//分類保存
router.post('/category/edit/',function(req,res,next){
    var id=req.query.id||'';

    var name=req.body.name||name;
    Category.findOne({
        _id:id
    }).then(function(category){

        if(!category){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'分類信息不存在!'
            });
            return Promise.reject();
        }else{
            // 若是用戶不作任何修改就提交
            if(name==category.name){
                res.render('admin/success',{
                    userInfo:req.body.userInfo,
                    message:'修改爲功!',
                    operation:{
                        url:'/admin/category',
                        operation:'返回分類管理'
                    }
                });
                return Promise.reject();
            }else{
                // id不變,名稱是否相同
                Category.findOne({
                    _id: {$ne: id},
                    name:name
                }).then(function(same){

                    if(same){
                        res.render('admin/error',{
                            userInfo:req.body.userInfo,
                            message:'已經存在同名數據!'
                        });
                        return Promise.reject();
                    }else{

                        Category.update({
                            _id:id
                        },{
                            name:name
                        }).then(function(){
                            res.render('admin/success',{
                                userInfo:req.body.userInfo,
                                message:'修改爲功!',
                                operation:{
                                    url:'/admin/category',
                                    operation:'返回分類管理'
                                }
                            });
                        });


                    }
                });
            }
        }
    });
});

爲了防止異步問題,能夠寫得更加保險一點。讓它每一步都返回一個promise對象,

//分類保存
router.post('/category/edit/',function(req,res,next){
    var id=req.query.id||'';

    var name=req.body.name||name;
    Category.findOne({
        _id:id
    }).then(function(category){

        if(!category){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'分類信息不存在!'
            });
            return Promise.reject();
        }else{
            // 若是用戶不作任何修改就提交
            if(name==category.name){
                res.render('admin/success',{
                    userInfo:req.body.userInfo,
                    message:'修改爲功!',
                    operation:{
                        url:'/admin/category',
                        operation:'返回分類管理'
                    }
                });
                return Promise.reject();
            }else{
                // 再查詢id:不等於當前id
                return Category.findOne({
                    _id: {$ne: id},
                    name:name
                });
            }
        }
    }).then(function(same){
        if(same){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'已經存在同名數據!'
            });
            return Promise.reject();
        }else{
            return Category.update({
                _id:id
            },{
                name:name
            });
        }
    }).then(function(resb){
        res.render('admin/success',{
            userInfo:req.body.userInfo,
            message:'修改爲功!',
            operation:{
                url:'/admin/category',
                operation:'返回分類管理'
            }
        });
    });
});

這樣就能實現修改了。

刪除(remove)

刪除的邏輯相似。可是要簡單一些,判斷頁面是否還存在該id,是就刪除,也不須要專門去寫刪除界面。,只須要一個成功或失敗的界面就OK了。

刪除用的是remove方法——把_id屬性爲id的條目刪除就行啦

// 分類的刪除
router.get('/category/delete',function(req,res){
    var id=req.query.id;

    Category.findOne({
        _id:id
    }).then(function(category){
        if(!category){
            res.render('/admin/error',{
                userInfo:req.body.userInfo,
                message:'該內容不存在於數據庫中!',
                operation:{
                    url:'/admin/category',
                    operation:'返回分類管理'
                }
            });
            return  Promise.reject();
        }else{
            return Category.remove({
             _id:id
             })
        }
    }).then(function(){
        res.render('admin/success',{
            userInfo:req.body.userInfo,
            message:'刪除分類成功!',
            operation:{
                url:'/admin/category',
                operation:'返回分類管理'
            }
        });
    });
});

前臺分類導航展現與排序

前臺的導航分類是寫死的,如今是時候把它換成咱們須要的內容了。

由於我我的項目的關係,我一級導航是固定的。因此就在文章分類下實現下拉菜單。

從數據庫讀取前臺首頁內容,基於main.js

爲此還得引入Category

var Category=require('../models/Categories');
router.get('/',function(req,res,next){
    // 讀取分類信息
    Category.find().then(function(rs){
        console.log(rs)
    });

    res.render('main/index',{
        userInfo:req.userInfo
    });
});

運行後打印出來的信息是:

就成功拿到了後臺數據。

接下來就是把數據加到模板裏面去啦

var Category=require('../models/Categories');

router.get('/',function(req,res,next){

    // 讀取分類信息
    Category.find().then(function(categories){
        console.log(categories);
        res.render('main/index',{
            userInfo:req.userInfo,
            categories:categories
        });
    });

});

前端模板這麼寫:

<ul class="nav-article">
                            {% if !userInfo._id %}
                            <li><a href="javascript:;">僅限註冊用戶查看!</a></li>
                            {% else %}
                            {% for category in categories %}
                            <li><a href="javascript:;">{{category.name}}</a></li>
                            {% endfor %}
                            {% endif %}
                        </ul>

你在後臺修改分類,

結果就出來了。挺好,挺好。

然而有一個小問題,就是咱們拿到的數據是倒序的。

思路1:在後端把這個數組reverse一下。就符合正常的判斷邏輯了。

res.render('main/index',{
            userInfo:req.userInfo,
            categories:categories.reverse()
        });

但這不是惟一的思路,從展現後端功能的考慮,最新添加的理應在最後面,因此有了思路2

思路2:回到admin.js對Category進行排序。

id表面上看是一串毫無規律的字符串,然而它確實是按照時間排列的。

那就好了,根據id用sort方法排序

obj.find().sort({_id:-1})......
//-1表示降序,1表示升序

博客分類管理這部分到此結束了。


十五. 文章管理(1):後臺

文章管理仍是基於admin.js

<!--layout.html-->
<li><a href="/admin/content">文章管理</a></li>

增長一個管理首頁

<!--content.html-->
{% extends 'layout.html' %}

{% block main %}

<h3>文章管理 </h3>
<a href="content/add">添加新的文章!</a>

<!--表格-->

{% endblock %}

再增長一個編輯文章的界面,其中,要獲取分類信息

{% extends 'layout.html' %}

{% block main %}

<h3>文章管理    <small>>添加文章</small></h3>

<form method="post">

    <span>標題</span>
    <input type="text" name="title"/>
    <span>分類</span>
    <select name="categories">
        {% for category in categories %}
        <option value="{{category._id.toString()}}">{{category.name}}</option>
        {% endfor %}
    </select>
    <button type="submit">提交</button><br>
    <span style="line-height: 30px;">內容摘要</span><br>
    <textarea id="description" cols="150" rows="3" placeholder="請輸入簡介" name="description">

    </textarea>
    <br>
    <span style="line-height: 20px;">文章正文</span><br>
    <textarea id="article-content">

    </textarea>

</form>

{% endblock %}

效果以下

再寫兩個路由。

// admin.js
// 內容管理
router.get('/content',function(req,res,next){

    res.render('admin/content_index',{
        userInfo:req.userInfo
    });
});

// 添加文章
router.get('/content/add',function(req,res,next){
    Category.find().then(function(categories){
        console.log(categories)
        res.render('admin/content_add',{
            userInfo:req.userInfo,
            categories:categories
        });
    })
});

獲取數據

仍是用到了schema設計應該存儲的內容。

最主要的固然是文章相關——標題,簡介,內容,發表時間。

還有一個不可忽視的問題,就是文章隸屬分類。咱們是根據分類id進行區分的

// schemas文件夾下的content.js

var mongoose=require('mongoose');

module.exports=new mongoose.Schema({
    // 關聯字段 -分類的id
    category:{
        // 類型
        type:mongoose.Schema.Tpyes.ObjectId,
        // 引用,其實是說,存儲時根據關聯進行索引出分類目錄下的值。而不是存進去的值。
        ref:'Category'
    },

    // 標題
    title:String,

    // 簡介
    description:{
        type:String,
        default:''
    },

    // 文章內容
    content:{
        type:String,
        default:''
    },

    // 當前時間
    date:String

});

接下來就是建立一個在models下面建立一個Content模型

// model文件夾下的Content.js

var mongoose=require('mongoose');
var contentsSchema=require('../schemas/contents');

module.exports=mongoose.model('Content',contentsSchema);

內容保存是用post方式提交的。

所以再寫一個post路由

//admin.js
// 內容保存
router.post('/content/add',function(req,res,next){

    console.log(req.body);
});

在後臺輸入內容,提交,就看到提交上來的數據了。

不錯。

表單驗證

簡單的驗證規則:不能爲空

驗證不能爲空的時候,應該調用trim方法處理以後再進行驗證。

// 內容保存
router.post('/content/add',function(req,res,next){
    console.log(req.body)
    if(req.body.category.trim()==''){
        res.render('admin/error',{
            userInfo:req.userInfo,
            message:'分類信息不存在!'
        });
        return Promise.reject();
    }

    if(req.body.title.trim()==''){
        res.render('admin/error',{
            userInfo:req.userInfo,
            message:'標題不能爲空!'
        });
        return Promise.reject();
    }

    if(req.body.content.trim()==''){
        res.render('admin/error',{
            userInfo:req.userInfo,
            message:'內容忘了填!'
        });
        return Promise.reject();
    }


});

還有個問題。就是簡介(摘要)

保存數據庫數據

保存和渲染相關的方法都是經過引入模塊來進行的。

var Content=require('../models/Contents');
····

new Content({
        category:req.body.category,
        title:req.body.title,
        description:req.body.description,
        content:req.body.content,
        date:new Date().toDateString()
    }).save().then(function(){
            res.render('admin/success',{
                userInfo:req.userInfo,
                message:'文章發佈成功!'
            });
        });
····

而後你發佈一篇文章,驗證無誤後,就會出現「發佈成功」的頁面。

而後你就能夠在數據庫查詢到想要的內容了

這個對象有當前文章相關的內容,也有欄目所屬的id,也有內容本身的id。還有日期

爲了顯示內容,能夠用以前封裝的renderAdminTable函數

{% extends 'layout.html' %}

{% block main %}

<h3>文章管理 </h3>
<a href="content/add">添加新的文章!</a>

<table class="users-list">
    <thead>
    <tr>
        <th>標題</th>
        <th>所屬分類</th>
        <th>發佈時間</th>
        <th>操做</th>
    </tr>
    </thead>
    <tbody>
    {% for content in data %}

    <tr>
        <td>{{content.title}}</td>
        <td>{{content.category}}</td>
        <td>
            {{content.date}}
        </td>
        <td>
            <a href="/admin/content/edit?id={{content._id.toString()}}">修改 </a>
            |<a href="/admincontent/delete?id={{content._id.toString()}}"> 刪除</a>
        </td>
    </tr>
    {% endfor %}
    </tbody>
</table>

{%include 'page.html'%}
{% endblock %}

分類名顯示出來的是個object

分類名用的是data.category

但若是換成data.category.id就能獲取到一個buffer對象,這個buffer對象轉換後,應該就是分類信息。

可是直接用的話,又顯示亂碼。

這就有點小麻煩了。

回看schema中的數據庫,當存儲後,會自動關聯Category模對象(注意:這裏的Category固然是admin.js的Category)進行查詢。查詢意味着有一個新的方法populate。populate方法的參數是執行查詢的屬性。在這裏咱們要操做的屬性是category

// 這是一個功能函數
function renderAdminTable(obj,type,limit,_query){
    router.get('/'+type+'/', function (req,res,next) {
        var page=req.query.page||1;
        var count=0;

        obj.count().then(function(_count){
            count=_count;
            var pages=Math.ceil(count/limit);
            page=Math.min(page,pages);
            page=Math.max(page,1);

            var skip=(page-1)*limit;
            /*
            * sort方法排序,根據id,
            * */
            var newObj=_query?obj.find().sort({_id:-1}).limit(limit).skip(skip).populate(_query):obj.find().sort({_id:-1}).limit(limit).skip(skip);
            newObj.then(function(data){
                console.log(data);

                res.render('admin/'+type+'_index',{
                    type:type,
                    userInfo:req.userInfo,
                    data:data,
                    page:page,
                    pages:pages,
                    limit:limit,
                    count:count
                });
            });
        });//獲取總頁數
    });
}

diao調用時寫法爲:renderAdminTable(Content,'content',2,'category');

打印出來的data數據爲:

發現Category的查詢結果就返回給data的category屬性了

很棒吧!那就把模板改了

不錯不錯。

修改和刪除

修改和刪除基本上遵守同一個邏輯。

修改

請求的文章id若是在數據庫查詢不到,那就返回錯誤頁面。不然渲染一個編輯頁面(content_edit)——注意,這裏得事先獲取分類。

// 修改
router.get('/content/edit',function(req,res,next){
    var id=req.query.id||'';

    Content.findOne({
        _id:id
    }).then(function(content){
       if(!content){
           res.render('admin/error',{
               userInfo:req.userInfo,
               message:'該文章id事先已被刪除了。'
           });
           return Promise.reject();
       }else{
           Category.find().then(function(categories){
               // console.log(content);
               res.render('admin/content_edit',{
                   userInfo:req.userInfo,
                   categories:categories,
                   data:content
               });
           });
       }
    });
});

把前端頁面顯示出來以後就是保存。

保存的post邏輯差很少,但實際上能夠簡化。

// 保存文章修改
router.post('/content/edit',function(req,res,next){
    var id=req.query.id||'';

    Content.findOne({
        _id:id
    }).then(function(content){
        if(!content){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'文章id事先被刪除了!'
            });
            return Promise.reject();
        }else{
            return Content.update({
                _id:id
            },{
                category:req.body.category,
                title:req.body.title,
                description:req.body.description,
                content:req.body.content
            });
        }
    }).then(function(){
        res.render('admin/success',{
            userInfo:req.body.userInfo,
            message:'修改爲功!',
            operation:{
                url:'/admin/content',
                operation:'返回分類管理'
            }
        });
    });
});

刪除

基本差很少。

router.get('/content/delete',function(req,res,next){
   var id=req.query.id||'';

    Content.remove({
        _id:id
    }).then(function(){
        res.render('admin/success',{
            userInfo:req.userInfo,
            message:'刪除文章成功!',
            operation:{
                url:'/admin/content',
                operation:'返回分類管理'
            }
        });
    });
});

信息擴展(發佈者,點擊量)

能夠在數據表結構中再添加兩個屬性

user: {
        //類型
        type:mongoose.Schema.Types.objectId,
        //引用
        ref:'User'
    },
    
    views:{
      type:Number,
      default:0
    }

而後在文章添加時,增添一個user屬性,把req.userInfo._id傳進去。

顯示呢?實際上populate方法接受一個字符串或者有字符串組成的數組。因此數組應該是xxx.populate(['category','user'])。這樣模板就能拿到user的屬性了。

而後修改模板,讓它展示出來:


十六. 文章管理(2):前臺

先給博客寫點東西吧。當前的文章確實太少了。

當咱們寫好了文章,內容就已經存放在服務器上了。前臺怎麼渲染是一個值得考慮的問題。顯然,這些事情都是main.js完成的。

這時候注意了,入門一個領域,知道本身在幹什麼是很是重要的。

獲取數據集

因爲業務邏輯,個人博客內容設置爲不在首頁展現,須要在/article頁專門展現本身的文章,除了所有文章,分類連接渲染的是:/article?id=xxx

先看所有文章下的/article怎麼渲染吧。

文章頁效果預期是這樣的:

  • 文章頁須要接收文章的信息。
  • 文章須要接收分頁相關的信息。

文章頁須要接收的信息比較多,因此寫一個data對象,把這些信息放進去,到渲染時直接用這個data就好了。

//main.js
var express=require('express');

var router=express.Router();

var Category=require('../models/Categories');
var Content=require('../models/Content');
/*
*省略首頁路由
*
*/
router.get('/article',function(req,res,next){
    var data={
        userInfo:req.userInfo,
        categories:[],
        count:0,
        page:Number(req.query.page||1),
        limit:3,
        pages:0
    };

    // 讀取分類信息
    Category.find().then(function(categories){
        data.categories=categories;

        return Content.count();
    }).then(function(count){
        data.count=count;
        //計算總頁數
        data.pages=Math.ceil(data.count/data.limit);
        // 取值不超過pages
        data.page=Math.min(data.page,data.pages);
        // 取值不小於1
        data.page=Math.max(data.page,1);
        // skip不須要分配到模板中,因此忽略。
        var skip=(data.page-1)*data.limit;

        return Content.find().limit(data.limit).skip(skip).populate(['category','user']).sort(_id:-1);


    }).then(function(contents){
        data.contents=contents;
        console.log(data);//這裏有你想要的全部數據
        res.render('main/article',data);
    })
});

該程序反映了data一步步獲取內容的過程。

前臺應用數據

  • 我只須要對文章展現作個for循環,而後把數據傳進模板中就能夠了。

    {% for content in contents %}
                    <div class="cell">
                        <div class="label">
                            <time>{{content.date.slice(5,11)}}</time>
                            <div>{{content.category.name.slice(0,3)+'..'}}</div>
                        </div>
                        <hgroup>
                            <h3>{{content.title}}</h3>
                            <h4>{{content.user.username}}</h4>
                        </hgroup>
    
                        <p>{{content.description}}</p>
                        <address>推送於{{content.date}}</address>
                    </div>
                    {% endfor %}
  • 側邊欄有一個文章內容分類區,把數據傳進去就好了。

  • 分頁按鈕能夠這樣寫

    <div class="pages-num">
                        <ul>
                            <li><a href="/article?page=1">第一頁</a></li>
                            {% if page-1!==0 %}
                            <li><a href="/article?page={{page-1}}">上一頁</a></li>
                            {%endif%}
    
                            {% if page+1<=pages %}
                            <li><a href="/article?page={{page+1}}">下一頁</a></li>
                            {% endif %}
                            <li><a href="/article?page={{pages}}">最後頁</a></li>
                        </ul>
                    </div>

效果:

你會發現,模板的代碼越寫越簡單。

獲取分類下的頁面(where方法)

如今來解決分類的問題。

以前咱們寫好的分類頁面地址爲/article?category={{category._id.toString()}}

因此要對當前的id進行響應。若是請求的category值爲不空,則調用where顯示。

router.get('/article',function(req,res,next){
    var data={
        userInfo:req.userInfo,
        category:req.query.category||'',
        categories:[],
        count:0,
        page:Number(req.query.page||1),
        limit:3,
        pages:0
    };
    var where={};
    if(data.category){
        where.category=data.category
    }
  //...
  return Content.where(where).find().limit(data.limit).skip(skip).sort({_id:-1}).populate(['category','user']);

這樣點擊相應的分類,就能獲取到相應的資料了。

可是頁碼仍是有問題。緣由在於count的獲取,也應該根據where進行查詢。

return Content.where(where).count();

另一個頁碼問題是,頁碼的連接寫死了。

只要帶上category就好了。

因此比較完整的頁碼判斷是:

<ul>
                        {% if pages>0 %}
                        <li><a href="/article?category={{category.toString()}}&page=1">第一頁</a></li>
                        {% if page-1!==0 %}
                        <li><a href="/article?category={{category.toString()}}&page={{page-1}}">上一頁</a></li>
                        {%endif%}

                        <li style="background:rgb(166,96,183);"><a style="color:#fff;"  href="javascript:;">{{page}}/{{pages}}</a></li>

                        {% if page+1<=pages %}
                        <li><a href="/article?category={{category.toString()}}&page={{page+1}}">下一頁</a></li>
                        {% endif %}
                        <li><a href="/article?category={{category.toString()}}&page={{pages}}">最後頁</a></li>
                        {% else %}
                        <li style="width: 100%;text-align: center;">當前分類沒有任何文章!</li>
                        {% endif %}
                    </ul>

而後作一個當前分類高亮顯示的判斷

<ul>
                        {% if category=='' %}
                        <li><a style="border-left: 6px solid #522a5c;" href="/article">所有文章</a></li>
                        {%else%}
                        <li><a href="/article">所有文章</a></li>
                        {% endif %}
                        {% for _category in categories %}
                        {% if category.toString()==_category._id.toString() %}
                        <li><a style="border-left: 6px solid #522a5c;" href="/article?category={{_category._id.toString()}}">{{_category.name}}</a></li>
                        {% else %}
                        <li><a href="/article?category={{_category._id.toString()}}">{{_category.name}}</a></li>
                        {% endif %}
                        {% endfor %}
                    </ul>

展現文章詳細信息

同理內容詳情頁須要給個連接,而後就再寫一個路由。在這裏我用的是/view?contentid={{content._id}}

基本邏輯

須要哪些數據?

  • userInfo
  • 所有分類信息
  • 文章內容(content)——包括當前文章所屬的分類信息

查詢方式:contentId

router.get('/view/',function(req,res,next){
    var contentId=req.query.contentId||'';
    var data={
        userInfo:req.userInfo,
        categories:[],
        content:null
    };

    Category.find().then(function(categories){
        data.categories=categories;
        return Content.findOne({_id:contentId});
    }).then(function(content){
        data.content=content;
        console.log(data);
        res.render('main/view',data);
    });
});

發現能夠打印出文章的主要內容了。

接下來就是寫模板。

新建一個article_layout.html模板,把article.html的全部內容剪切進去。

博客展現頁的主要區域在於以前的內容列表。因此把它抽離出來。

把一個個內容按照邏輯加上去,大概就是這樣。

閱讀數的實現

很簡單,每當用戶點擊文章,閱讀數就加1.

router.get('/view/',function(req,res,next){
    var contentId=req.query.contentId||'';
    var data={
        userInfo:req.userInfo,
        categories:[],
        content:null
    };

    Category.find().then(function(categories){
        data.categories=categories;
        return Content.findOne({_id:contentId});
    }).then(function(content){
        data.content=content;
        content.views++;//保存閱讀數
        content.save();
        console.log(data);
        res.render('main/view',data);
    });
});

內容評論

先把評論的樣式寫出來吧!大概是這樣

評論是經過ajax提交的。是在ajax模塊——api.js

評論的post提交到數據庫,應該放到數據庫的contents.js中。

// 評論
    comments: {
        type:Array,
        default:[]
    }

每條評論包括以下內容:

評論者,評論時間,還有評論的內容。

在api.js中寫一個post提交的路由

// 評論提交
router.post('/comment/post',function(req,res,next){
    // 文章的id是須要前端提交的。
    var contentId=req.body.contentId||'';
    var postData={
        username:req.userInfo.username,
        postTime: new ConvertDate().getDate(),
        content: req.body.content
    };

    // 查詢當前內容信息
    Content.findOne({
        _id:contentId
    }).then(function(content){
        content.comments.push(postData);
        return content.save()
    }).then(function(newContent){//最新的內容在newContent!
        responseData.message='評論成功!';
        res.json(responseData);
    })

});

而後在你的view頁面相關的文件中寫一個ajax方法,咱們要傳送文章的id

可是文章的id最初並無發送過去。能夠在view頁面寫一個隱藏的input#contentId,把當前文章的id存進去。而後經過jQuery拿到數據。

// 評論提交
    $('#messageComment').click(function(){
        $.ajax({
            type:'POST',
            url:'/api/comment/post',
            data:{
                contentId:$('#contentId').val(),
                content:$('#commentValue').val(),
            },
            success:function(responseData){
                console.log(responseData);
            }
        });
        return false;

    });

很簡單吧!

評論提交後,清空輸入框,而後下方出現新增長的內容。

最新的內容從哪來呢?在newContent處。因此咱們只須要讓responseData存進newContent,就能實現內容添加。

// api.js
//...
// 查詢當前內容信息
    Content.findOne({
        _id:contentId
    }).then(function(content){
        content.comments.push(postData);
        return content.save()
    }).then(function(newContent){
        responseData.message='評論成功!';
        responseData.data=newContent;
        res.json(responseData);
    })

//...

看,這樣就拿到數據了。

接下來就在前端渲染頁面:

用這個獲取內容。

function renderComment(arr){
    var innerHtml='';
    for(var i=0;i<arr.length;i++){
        innerHtml='<li><span class="comments-user">'+arr[i].username+' </span><span class="comments-date">'+arr[i].postTime+'</span><p>'+arr[i].content+'</p></li>'+innerHtml;
    }
    return innerHtml;
}
// 評論提交
    $('#messageComment').click(function(){
        $.ajax({
            type:'POST',
            url:'/api/comment/post',
            data:{
                contentId:$('#contentId').val(),
                content:$('#commentValue').val(),
            },
            success:function(responseData){
                console.log(responseData);
                alert(responseData.message);
                var arr= responseData.data.comments;
                //console.log(renderComment(arr));
                $('.comments').html(renderComment(arr));
            }
        });
        return false;

    });

這樣就能夠顯示出來了。可是發現頁面一刷新,內容就又沒有了——加載時就調用ajax方法。

api是提供一個虛擬地址,ajax可以從這個地址獲取數據。

重新寫一個路由:

//api.js
// 獲取指定文章的全部評論
router.get('/comment',function(req,res,next){
    var contentId=req.query.contentId||'';
    Content.findOne({
       _id:contentId
    }).then(function(content){
        responseData.data=content;
        res.json(responseData);
    })
});

注意這裏是get方式

//每次文章重載時獲取該文章的全部評論
    $.ajax({
        type:'GET',
        url:'/api/comment',
        data:{
            contentId:$('#contentId').val(),
            content:$('#commentValue').val(),
        },
        success:function(responseData){
            console.log(responseData);
            var arr= responseData.data.comments;
            //console.log(renderComment(arr));
            $('.comments').html(renderComment(arr));
            $('#commentValue').val('');
            $('#commentsNum').html(arr.length)
        }
    });

評論分頁

分由於是ajax請求到的數據,因此徹底能夠在前端完成。

評論分頁太老舊了。不如作個僞瀑布流吧!

預期效果:點擊加載更多按鈕,出現三條評論。

之因此說是僞,由於評論一早就拿到手了。只是分段展現而已。固然你也能夠寫真的。每點擊一次都觸發新的ajax請求。只請求三條新的數據。

評論部分徹底能夠寫一個對象。重置方法,加載方法,獲取數據方法。

寫下來又是一大篇文章。

// 加載評論的基本邏輯
function Comments(){
    this.count=1;
    this.comments=0;
}

在ajax請求評論內容是時,給每條評論的li加一個data-index值。

// 獲取評論內容
Comments.prototype.getComment=function(arr){
    var innerHtml='';
    this.comments=arr.length;//獲取評論總數
    for(var i=0;i<arr.length;i++){
        innerHtml=
            '<li data-index='+(arr.length-i)+'><span class="comments-user">'+
            arr[i].username+
            ' </span><span class="comments-date">'+
            arr[i].postTime+
            '</span><p>'+
            arr[i].content+
            '</p></li>'+innerHtml;
    }
    
    return innerHtml;
};

在每次加載頁面,每次發完評論的時候,都初始化評論頁面。首先要作的是解綁加載按鈕可能的事件。當評論數少於三條,加載按鈕變成「沒有更多了」。超過三條時,數據自動隱藏。

Comments.prototype.resetComment=function (limit){
    this.count=1;
    this.comments=$('.comments').children().length;//獲取評論總數
    $('#load-more').unbind("click");

    if(this.comments<limit){
        $('#load-more').text('..沒有了');
    }else{
        $('#load-more').text('加載更多');
    }

    for(var i=1;i<=this.comments;i++){
        if(i>limit){
            $('.comments').find('[data-index='+ i.toString()+']').css('display','none');
        }
    }
};

點擊加載按鈕,根據點擊計數加載評論

Comments.prototype. loadComments=function(limit){
    var _this=this;
    $('#load-more').click(function(){
        //console.log([_this.comments,_this.count]);
        if((_this.count+1)*limit>=_this.comments){
            $(this).text('..沒有了');

        }
        _this.count++;

        for(var i=1;i<=_this.comments;i++){
            if(_this.count<i*_this.count&&i<=(_this.count)*limit){
                $('.comments').find('[data-index='+ i.toString()+']').slideDown(300);
            }
        }
    });
};

而後就是在網頁中應用這些方法:

$(function(){
    //每次文章重載時獲取該文章的全部評論
    $.ajax({
        type:'GET',
        url:'/api/comment',
        data:{
            contentId:$('#contentId').val(),
            content:$('#commentValue').val(),
        },
        success:function(responseData){

            var arr= responseData.data.comments;
            //渲染評論的必要方法
            var renderComments=new Comments();

            //獲取評論內容
            $('.comments').html(renderComments.getComment(arr));

            //清空評論框
            $('#commentValue').val('');

            //展現評論條數
            $('#commentsNum').html(arr.length);

            //首次加載展現三條,每點擊一次加載3條
            renderComments.resetComment(3);
            renderComments.loadComments(3);


            // 評論提交
            $('#messageComment').click(function(){
                $.ajax({
                    type:'POST',
                    url:'/api/comment/post',
                    data:{
                        contentId:$('#contentId').val(),
                        content:$('#commentValue').val(),
                    },
                    success:function(responseData){

                        alert(responseData.message);
                        var arr= responseData.data.comments;
                        $('.comments').html(renderComments.getComment(arr));
                        $('#commentValue').val('');
                        $('#commentsNum').html(arr.length);

                        renderComments.resetComment(3);
                        renderComments.loadComments(3);
                    }
                });
                return false;
            });
            
            
            
        }
    });

});

發佈者信息和文章分類展現

get方式獲取的內容中雖然有了文章做者id,可是沒有做者名。也缺失當前文章的內容。因此在get獲取以後,須要發送發佈者的信息。

另外一方面,因爲view.html繼承的是article的模板。而article是須要在在發送的一級目錄下存放一個category屬性,才能在模板判斷顯示。

所以須要把data.content.category移到上層數性來。

}).then(function(content){
        //console.log(content);
        data.content=content;
        content.views++;
        content.save();

       return User.find({
            _id:data.content.user
        });

    }).then(function(rs){
        data.content.user=rs[0];
        data.category=data.content.category;
        res.render('main/view',data);
    });

markdown模塊的使用

如今的博客內容是混亂無序的。

那就用到最後一個模塊——markdown

按照邏輯來講,內容渲染不該該在後端進行。儘管你也能夠這麼作。可是渲染以後,編輯文章會發生很大的問題。

因此我仍是採用熟悉的marked.js,由於它能比較好的兼容hightlight.js的代碼高亮。

<script type="text/javascript" src="../../public/js/marked.js"></script>
<script type="text/javascript" src="../../public/js/highlight.pack.js"></script>
<script >hljs.initHighlightingOnLoad();</script>
// ajax方法
success:function(responseData){
           // console.log(responseData);
            var a=responseData.data.content;

            var rendererMD = new marked.Renderer();
            marked.setOptions({
                renderer: rendererMD,
                gfm: true,
                tables: true,
                breaks: false,
                pedantic: false,
                sanitize: false,
                smartLists: true,
                smartypants: false
            });


            marked.setOptions({
                highlight: function (code,a,c) {
                    return hljs.highlightAuto(code).value;
                }
            });
  //後文略...

在經過ajax請求到數據集以後,對內容進行渲染。而後插入到內容中去。

那麼模板裏的文章內容就不要了。

可是,瀏覽器自帶的html標籤樣式實在太醜了!在引入樣式庫吧

highlight.js附帶的樣式庫提供了多種基本的語法高亮設置。

而後你能夠參考bootstrap的code部分代碼。再改改行距,自適應圖片等等。讓文章好看些。


十七. 收尾

到目前爲止,這個博客就基本實現了。

前端須要一些後端的邏輯,才能對產品有較爲深入的理解。

相關文章
相關標籤/搜索