Node.js 博客搭建
一. 學習需求
Node 的安裝運行
會安裝node,搭建node環境
會運行node。
基礎模塊的使用
Buffer:二進制數據處理模塊
Event:事件模塊
fs:文件系統模塊
Net:網絡模塊
Http:http模塊
...
NPM(node包管理工具)
第三方node模塊(包)的管理工具,可以使用該下載工具安裝第三方模塊。,當然也可以創建上傳自己的模塊。
參考
假定已經理解並掌握了入門教程的所有內容。在易出錯的地方將進行簡要的說明。
其它
這是最不起眼,但也是最必不可少的——你得准備一個博客的靜態文件。
博客的后台界面,登錄注冊界面,文章展示界面,首頁等。
二. 項目需求分析
一個博客應當具備哪些功能?
前台展示
- 點擊下一頁,可以點擊分類導航。
- 可以點擊進入到具體博文頁面
- 下方允許評論。顯示發表時間。允許留言分頁。
- 右側有登錄注冊界面。
后台管理
- 管理員賬號:登陸后看到頁面不一樣,有后台頁面。
- 允許添加新的分類。從后台添加新的文章。
- 編輯允許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 ofgzip
anddeflate
encodings.A new
body
object containing the parsed data is populated on therequest
object after the middleware (i.e.req.body
). This object will contain key-value pairs, where the value can be a string or array (whenextended
isfalse
), or any type (whenextended
istrue
).
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部分代碼。再改改行距,自適應圖片等等。讓文章好看些。
十七. 收尾
到目前為止,這個博客就基本實現了。
前端需要一些后端的邏輯,才能對產品有較為深刻的理解。