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部分代码。再改改行距,自适应图片等等。让文章好看些。
十七. 收尾
到目前为止,这个博客就基本实现了。
前端需要一些后端的逻辑,才能对产品有较为深刻的理解。