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文件夾追加了一些新的內容:
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)個函數進行綁定。當滿足對應的規則時,對應的函數將會被執行,該函數有三個參數——
javascript 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
});
});
});//獲取總頁數
});
可以封裝成函數了——一下就少了三分之二的代碼量。
functionrenderAdminTable(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。
// 這是一個功能函數
functionrenderAdminTable(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循環,然后把數據傳進模板中就可以了。
```javascript
{% for content in contents %}
{{content.date.slice(5,11)}}
{{content.category.name.slice(0,3)+'..'}}
<p>{{content.description}}</p>
<address>推送於{{content.date}}</address>
</div>
{% endfor %}
```
•側邊欄有一個文章內容分類區,把數據傳進去就行了。
•分頁按鈕可以這樣寫
```html
◦第一頁
{% if page-1!==0 %}
◦上一頁
{%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);
})
//...
看,這樣就拿到數據了。
接下來就在前端渲染頁面:
用這個獲取內容。
functionrenderComment(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請求。只請求三條新的數據。
評論部分完全可以寫一個對象。重置方法,加載方法,獲取數據方法。
寫下來又是一大篇文章。
// 加載評論的基本邏輯
functionComments(){
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部分代碼。再改改行距,自適應圖片等等。讓文章好看些。
十七. 收尾
到目前為止,這個博客就基本實現了。
前端需要一些后端的邏輯,才能對產品有較為深刻的理解
本篇文章來源於 Linux公社網站(www.linuxidc.com) 原文鏈接:https://www.linuxidc.com/Linux/2017-02/140115.htm