Node.js 博客搭建javascript
一. 学习需求css
Node 的安装运行html
会安装node,搭建node环境前端
会运行node。java
基础模块的使用node
Buffer:二进制数据处理模块jquery
Event:事件模块linux
fs:文件系统模块git
Net:网络模块github
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