共计 54532 个字符,预计需要花费 137 分钟才能阅读完成。
Node.js 博客搭建
一. 学习需求
Node 的安装运行
会安装 node,搭建 node 环境
会运行 node。
基础模块的使用
Buffer:二进制数据处理模块
Event:事件模块
fs:文件系统模块
Net:网络模块
Http:http 模块
…
NPM(node 包管理工具)
第三方 node 模块(包)的管理工具,可以使用该下载工具安装第三方模块。,当然也可以创建上传自己的模块。
参考
假定已经理解并掌握了入门教程的所有内容。在易出错的地方将进行简要的说明。
其它
这是最不起眼,但也是最必不可少的——你得准备一个博客的静态文件。
博客的后台界面,登录注册界面,文章展示界面,首页等。
二. 项目需求分析
一个博客应当具备哪些功能?
前台展示
- 点击下一页,可以点击分类导航。
- 可以点击进入到具体博文页面
- 下方允许评论。显示发表时间。允许留言分页。
- 右侧有登录注册界面。
后台管理
- 管理员账号:登陆后看到页面不一样,有后台页面。
- 允许添加新的分类。从后台添加新的文章。
- 编辑允许 markdown 写法。
- 评论管理。
三. 项目创建,安装及初始化
技术框架
本项目采用了以下核心技术:
- Node 版本:6.9.1——基础核心的开发语言
(安装后查看版本:cmd 窗口:node -v)
(查看方式:cmd 窗口:node -v
)
- Express
一个简洁灵活的 node.js WEB 应用框架,提供一系列强大的特性帮助我们创建 web 应用。
- Mongodb
用于保存产生的数据
还有一系列第三方模块和中间件:
- bodyParser,解析 post 请求数据
- cookies:读写 cookie
- swig:模板解析引擎
- mongoose:操作 Mongodb 数据
- markdown:语法解析生成模块
…
初始化
在 W ebStorm 创建一个新的空工程,指定文件夹。
打开左下角的 Terminal 输入:
npm init
回车。然后让你输入 name:(code),输入项目名称,然后后面都可以不填,最后在 Is it OK?
处写上 yes。
完成这一步操作之后,系统就会在当前文件夹创建一个 package.json
的项目文件。
项目文件下面拥有刚才你所基本的信息。后期需要更改的话可直接在这里修改。
第三方插件的安装
- 以 Express 为例
在命令行输入:
npm install --save express
耐心等待一段时间,安装完成后,json 文件夹追加了一些新的内容:
json {// 之前内容........ "author": "","license":"ISC","dependencies": {"express":"^4.14.0"}
表示安装成功。
同理,使用 npm install --save xxx
的方法安装下载以下模块:
- body-parser
- cookies
- markdown
- mongoose
- swig
所以安装完之后的 package.json 文件是这样的。
{
"name": "blog",
"version": "1.0.0",
"description": "this is my first blog.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.15.2",
"cookies": "^0.6.2",
"express": "^4.14.0",
"markdown": "^0.5.0",
"mongoose": "^4.7.5",
"swig": "^1.4.2"
}
}
在这个 json 中,就能通过依赖模块(dependencies
)看到各个第三方模块的版本信息
切记:依赖模块安装,要联网!
安装完成之后
第二个文件夹放的是你的第三方模块。
此外还需要别的文件,完整的结构是这样的——
接下来就把缺失的文件目录自己建立起来。
完成着一系列操作之后,就把 app.js 作为应用程序的启动(入口页面)。
创建应用
以下代码创建应用,监听端口
// 加载 express
var express=require('express');
// 创建 app 应用,相当于 =>Node.js Http.createServer();
var app=express();
// 监听 http 请求
app.listen(9001);
运行(ctrl
+shift
+c
)之后就可以通过浏览器访问了。
用户访问:
- 用户通过 URL 访问 web 应用,比如
http://localhost:9001/
这时候会发现浏览器呈现的内容是这样的。
-
web 后端根据用户访问的 url 处理不同的业务逻辑。
-
路由绑定——
在 Express 框架下,可以通过 app.get()
或app.post()
等方式,把一个 url 路径和(1-n)个函数进行绑定。当满足对应的规则时,对应的函数将会被执行,该函数有三个参数——
Javascript app.get('/',function(req,res,next){// do sth.}); // req:request 对象,保存客户请求相关的一些数据——http.request // res:response 对象,服务端输出对象,停工了一些服务端相关的输出方法——http.response // next:方法,用于执行下一个和路径相匹配的函数(行为)。
- 内容输出
通过 res.send(string)
发送内容到客户端。
app.get('/',function(req,res,next){
res.send('<h1> 欢迎光临我的博客!</h1>');
});
运行。这时候网页就打印出了 h1 标题的内容。
注意,js 文件编码如果不为 UTF-8,网页文件显示中文会受到影响。
三. 模板引擎的配置和使用
使用模板
现在,我想向后端发送的内容可不是一个 h1 标题那么简单。还包括整个博客页面的 html 内容,如果还是用上面的方法,麻烦就大了。
怎么办呢?关键步骤在于 html 和 js 页面相分离(类似结构和行为层的分离)。
模板的使用在于后端逻辑和前端表现的分离(前后端分离)。
模板配置
基本配置如下
// 定义模板引擎,使用 swig.renderFile 方法解析后缀为 html 的文件
var swig=require('swig');
app.engine('html',swig.renderFile);
// 设置模板存放目录
app.set('views','./views');
// 注册模板引擎
app.set('view engine','html');
swig.setDefaults({cache:false});
配置模板的基本流程是:
请求 swig 模块
=> 定义模板引擎
=> 注册模板引擎
=> 设置调试方法
我们可以使用 var swig=require('swig');
定义了 swig 方法。
以下进行逐行解析——
定义模板引擎
app.engine('html',swig.renderFile);
第一个参数:模板引擎的名称,同时也是模板引擎的后缀,你可以定义打开的是任何文件格式,比如 json,甚至 tdl 等。
第二个参数表示用于解析处理模板内容的方法。
第三个参数:使用 swig.renderFile 方法解析后缀为 html 的文件。
设置模板目录
现在就用 express 组件提供的 set 方法标设置模板目录:
app.set('views','./views');
定义目录时也有两个参数,注意,第一个参数 必须 为views
!第二个参数可以是我们所给出的路径。因为之前已经定义了模板文件夹为views
。所以,使用对应的路径名为./views
。
注册模板引擎
app.set('view engine','html');
还是使用 express 提供了 set 方法。
第一个参数必须是字符串 'view engine'
。
第二个参数和 app.engine
方法定义的模板引擎名称(第一个参数)必须是一致的(都是“html”)。
重回 app.get
现在我们回到 app.get()方法里面,使用 res.render()
方法重新渲染指定内容
app.get('/',function(req,res,next){
/* * 读取指定目录下的指定文件,解析并返回给客户端 * 第一个参数:模板文件,相对于 views 目录,views/index.html * */
res.render('index');
});
这时候,我们定义了返回值渲染 index 文件,就需要在 views 文件夹下新创建一个index.html
。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>欢迎来到我的第一个博客!<h1>
</body>
</html>
render 方法还可以接受第二个参数,用于传递模板使用的第二个数据。
好了。这时候再刷新页面,就出现了 index 的内容。
调试方法
我们在不停止服务器的情况下,重新修改 index 的文件内容,发现并没有刷新。
什么问题呢?出于性能上考虑,node 把第一次读取的 index 放到了内容中,下次访问时,就是缓存中的内容了,而不是真正的 index 文件。因此需要重启。
开发过程中,为了减少麻烦,需要取消模板缓存。
swig.setDefaults({cache:false});
当然,当项目上线时,可以把这一段删除掉。
四. 静态文件托管
在写模板文件时,经常引入一些外链的 css,js 和图片等等。
css 怎么引入?
如果我们直接在首页的 head 区域这么写:
<link rel="stylesheet" type="text/css" href="css.css"/>
再刷新,发现对 css.css 的引用失败了。
问题不在于 css.css 是否存在,而在于请求失败。因为外链文件本质也是一个请求,但是在 app.js 中还没有对应设置。
如果这么写:
app.get('/css.css', function (req,res,next) {
res.send('body {background: red;}');
});
发现没有效果。
打开 http://localhost:9001/css.css
发现内容是这样的:
搞笑了。默认发送的是一个 html。因此需要设定一个 header
app.get('/css.css', function (req,res,next) {
res.setHeader('content-type','text/css');
res.send('body {background: red;}');
});
ctrl+F5,就解析了红色背景了。
同样的,静态文件需要完全分离,因此这种方法也是不行的。
静态文件托管目录
最好的方法是,把所有的静态文件都放在一个 public 的目录下,划分并存放好。
然后在开头就通过以下方法,把 public 目录下的所有静态文件都渲染了:
app.use('/public',express.static(__dirname+'/public'));
以上方法表示:当遇到 public 文件下的文件,都调用第二个参数里的方法(注意是两个下划线)。
当用户访问的 url 以 public 开始,那么直接返回对应 __dirname+'public'
下的文件。因此我们的 css 应该放到 public 下。
引用方式为:
<link rel="stylesheet" type="text/css" href="../public/css.css"/>
然后到 public 文件下创建一个 css.css,设置 body 背景为红色。原来的 app.get 方法就不要了。
至此,静态文件什么的都可以用到了
小结
在以上的内容中,我们实现了初始化项目,可以调用 html 和 css 文件。基本过程逻辑是:
用户发送 http 请求(url)=> 解析路由 => 找到匹配的规则 => 指定绑定函数,返回对应内容到用户。
访问的是 public:静态——直接读取指定目录下的文件,返回给用户。
=> 动态 => 处理业务逻辑
那么整个基本雏形就搭建起来了。
五. 分模块开发与实现
把整个网站放到一个 app.js 中,是不利于管理和维护的。实际开发中,是按照不同的功能,管理代码。
根据功能划分路由(routers)
根据本项目的业务逻辑,分为三个模块就够了。
- 前台模块
- 后台管理模块
- API 模块: 通过 ajax 调用的接口。
或者,使用app.use
(路由设置)划分:
app.use('/admin',require('./routers/admin'));
解释:当用户访问的是 admin 文件下的内容,这调用 router 文件夹下 admin.js 文件。下同。
-
app.use('/api',require('./routers/api'));
后台 -
app.use('/',require('./routers/main'));
前台
好了。重写下以前的代码,去掉多余的部分。
// 加载 express
var express=require('express');
// 创建 app 应用,相当于 =>Node.js Http.createServer();
var app=express();
// 设置静态文件托管
app.use('/public',express.static(__dirname+'/public'))
// 定义模板引擎,使用 swig.renderFile 方法解析后缀为 html 的文件
var swig=require('swig');
app.engine('html',swig.renderFile);
// 设置模板存放目录
app.set('views','./views');
// 注册模板引擎
app.set('view engine','html');
// 调试优化
swig.setDefaults({cache:false});
//app.use('/admin',require('./routers/admin'));
//app.use('/api',require('./routers/api'));
//app.use('/',require('./routers/main'));
// 监听 http 请求
app.listen(9001);
在 routers
创建一个 admin.js,同理再创建一个 api.js,一个 main.js
怎么访问不同文件夹下的文件?
比如,我想访问一个如 http://localhost:9001/admin/user
这样的地址,这样按理来说就应该调用 admin.js(分路由)。
所以编辑 admin.js
var express=require('express');
// 创建一个路由对象,此对象将会监听 admin 文件下的 url
var router=express.Router();
router.get('/user',function(req,res,next){
res.send('user');
});
module.exports=router;// 把 router 的结果作为模块的输出返回出去!
注意,在分路由中,不需要写明路径,就当它是在 admin 文件下的相对路径就可以了。
储存,然后回到 app.js,应用app.use('/admin',require('./routers/admin'));
再打开页面,就看到结果了。
同理,api.js 也如法炮制。
var express=require('express');
// 创建一个路由对象,此对象将会监听 api 文件夹下的 url
var router=express.Router();
router.get('/user',function(req,res,next){
res.send('api-user');
});
module.exports=router;// 把 router 的结果作为模块的输出返回出去!
再应用app.use('api/',require('./routers/api'))
。重启服务器,结果如下
首页也如法炮制
路由的细分
前台路由涉及了相当多的内容,因此再细化分多若干个路由也是不错的选择。
每个内容包括基本的分类和增删改
- main 模块
/
——首页
/view
——内容页
- api 模块
/
——首页
/login
——用户登陆
/register
——用户注册
/comment
——评论获取
/comment/post
——评论提交
- admin 模块
/
——首页
-
用户管理
/user
——用户列表 -
分类管理
/category
——分类目录/category/add
——分类添加/category/edit
——分类编辑/category/delete
——分类删除 -
文章管理
/article
——内容列表/article/add
——添加文章/article/edit
——文章修改/article/delete
——文章删除 -
评论管理
/comment
——评论列表/comment/delete
——评论删除
开发流程
功能开发顺序
用户——栏目——内容——评论
一切操作依赖于用户,所以先需要用户。
栏目也分为前后台,优先做后台。
内容和评论相互关联。
编码顺序
- 通过 Schema 定义设计数据储存结构
- 功能逻辑
- 页面展示
六. 数据库连接,表结构
比如用户,在 SCHEMA 文件夹下新建一个 users.js
如何定义一个模块呢?这里用到 mongoose 模块
var mongoose=require('mongoose');// 引入模块
除了在 users.js 请求 mongoose 模块以外,在 app.js 也需要引入 mongoose。
// 加载 express
var express=require('express')
// 创建 app 应用,相当于 =>Node.js Http.createServer();
var app=express();
// 加载数据库模块
var mongoose=require('mongoose');
// 设置静态文件托管
app.use('/public',express.static(__dirname+'/public'))
// 定义模板引擎,使用 swig.renderFile 方法解析后缀为 html 的文件
var swig=require('swig');
app.engine('html',swig.renderFile);
// 设置模板存放目录
app.set('views','./views');
// 注册模板引擎
app.set('view engine','html');
// 调试优化
swig.setDefaults({cache:false});
/** 根据不同的内容划分路由器* */
app.use('/admin',require('./routers/admin'));
app.use('/api',require('./routers/api'));
app.use('/',require('./routers/main'));
// 监听 http 请求
mongoose.connect();
app.listen(9001);
建立连接数据库(每次运行都需要这样)
mongoose 使用需要安装 mongodb 数据库。
mongodb 安装比较简单,在官网上下载了,制定好路径就可以了。
找到 mongodb 的 bin 文件夹。启动 mongod.exe——通过命令行
命令行依次输入:
f:
cd Program Files\MongoDB\Server\3.2\bin
总之就是根据自己安装的的路径名来找到 mongod.exe 就行了。
开启数据库前需要指定参数,比如数据库的路径。我之前已经在项目文件夹下创建一个 db 文件夹,然后作为数据库的路径就可以了。
除此之外还得指定一个端口。比如 27018
mongod --dbpath=G:\node\db --port=27018
然后回车
信息显示:等待链接 27018, 证明开启成功
下次每次关机后开启服务器,都需要做如上操作。
接下来要开启 mongo.exe。
命令行比较原始,还是可以使用一些可视化的工具进行连接。在这里我用的是 robomongo。
直接在国外网站上下载即可,下载不通可能需要科学上下网。
名字随便写就行了,端口写 27018
点击链接。
回到命令行。发现新出现以下信息:
表示正式建立连接。
数据保存
链接已经建立起来。但里面空空如也。
接下来使用 mongoose 操作数据库。
可以上这里去看看文档。文档上首页就给出了 mongoose.connect()
方法。
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
var Cat = mongoose.model('Cat', { name: String });
var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
if (err) {
console.log(err);
} else {
console.log('meow');
}
});
connect 方法接收的第一个参数,就是这个'mongodb://localhost:27018'
。第二个参数是回调函数。
数据库链接失败的话,是不应该开启监听的,所以要把 listen 放到 connect 方法里面。
mongoose.connect('mongodb://localhost:27018/blog',function(err){
if(err){
console.log('数据库连接错误!');
}else{
console.log('数据库连接成功!');
app.listen(9001);
}
});
运行,console 显示,数据库链接成功。
注意,如果出现错误,还是得看看编码格式,必须为 UTF-8。
回到 users.js 的编辑上来,继续看 mongoose 文档。
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
通过 mongoose.Schema 构造函数,生成一个 Schema 对象。
new 出的 Schema 对象包含很多内容,传入的对象代表数据库中的一个表。每个属性代表表中的每一个字段,每个值代表该字段存储的数据类型。
在这里,users.js 需要暴露的内容就是用户名和密码。
// 加载数据库模块
var mongoose=require('mongoose');
// 返回用户的表结构
module.exports= new mongoose.Schema({
// 用户名
username: String,
// 密码
password: String
});
然后在通过模型类来操作表结构。在项目的 models 文件夹下创建一个 User.js
var mongoose=require('mongoose');
var usersSchema=require('../schemas/users');
module.exports=mongoose.model('User',usersSchema);
这样就完成了一个模型类的创建。
模型怎么用?还是看看文档给出的使用方法。
// 创建一个表结构对象
var schema = new mongoose.Schema({ name: 'string', size: 'string' });
// 根据表结构对象创建一个模型类
var Tank = mongoose.model('Tank', schema);
构造函数如何使用:
var Tank = mongoose.model('Tank', yourSchema);
var small = new Tank({ size: 'small' });
small.save(function (err) {
if (err) return handleError(err);
// saved!
})
// or
Tank.create({ size: 'small' }, function (err, small) {
if (err) return handleError(err);
// saved!
})
七. 用户注册的前端逻辑
引入首页
用户注册首先得加载一个首页。
在 views 下面新建一个 main 文件夹,然后把你之前写好的 index.html 放进去。
所以回到 main.js 中。渲染你已经写好的博客首页。
var express=require('express');
// 创建一个路由对象,此对象将会监听前台文件夹下的 url
var router=express.Router();
router.get('/',function(req,res,next){
res.render('main/index');
});
module.exports=router;// 把 router 的结果作为模块的输出返回出去!
保存,然后重启 app.js,就能在 localhost:9001 看到首页了。
当然这个首页很丑,你可以自己写一个。
原来的路径全部按照项目文件夹的结构进行修改。
逻辑
注册登录一共有三个状态。
一开始就是注册,如果已有账号就点击登录,出现登录弹窗。
如果已经登录,则显示已经登录状态。并有注销按钮。
<div class="banner-wrap">
<div class="login" id="register">
<h3>注册</h3>
<span>用户:<input name="username" type="text"/></span><br/>
<span>密码:<input name="password" type="text"/></span><br/>
<span>确认:<input name="repassword" type="text"/></span><br/>
<span><input class="submit" type="button" value="提交"/></span>
<span>已有账号?马上 <a href="javascript:;"> 登录</a></span>
</div>
<div class="login" id="login" style="display:none;">
<h3>登录</h3>
<span>用户:<input type="text"/></span><br/>
<span>密码:<input type="text"/></span><br/>
<span><input type="button" value="提交"/></span>
<span>没有账号?马上 <a href="javascript:;"> 注册</a></span>
</div>
jquery 可以这么写:
$(function(){
// 登录注册的切换
$('#register a').click(function(){
$('#login').show();
$('#register').hide();
});
$('#login a').click(function(){
$('#login').hide();
$('#register').show();
});
});
当点击注册按钮,应该允许 ajax 提交数据。地址应该是 api 下的 user 文件夹的 register,该 register 文件暂时没有创建,所以不理他照写即可。
// 点击注册按钮,通过 ajax 提交数据
$('#register .submit').click(function(){
// 通过 ajax 提交交
$.ajax({
type:'post',
url:'/api/user/register',
data:{
username:$('#register').find('[name="username"]').val(),
password:$('#register').find('[name="password"]').val(),
repassword:$('#register').find('[name="repassword"]').val()
},
dataType:'json',
success:function(data){
console.log(data);
}
});
});
允许网站,输入用户名密码点击注册。
虽然报错,但是在 chrome 的 network 下的 header 可以看到之前提交的信息。
挺好,挺好。
八. body-paser 的使用:后端的基本验证
后端怎么响应前台的 ajax 请求?
首先,找到 API 的模块,增加一个路由,回到 api.js——当收到前端 ajax 的 post 请求时,路由打印出一个 register 字符串。
var express=require('express');
// 创建一个路由对象,此对象将会监听 api 文件夹下的 url
var router=express.Router();
router.post('/user/register',function(req,res,next){
console.log('register');
});
module.exports=router;// 把 router 的结果作为模块的输出返回出去!
这时候,就不会显示 404 了。说明路由处理成功。
如何获取前端 post 的数据?
这就需要用到新的第三方模块——body-parser
。
相关文档地址:https://github.com/expressjs/body-parser
bodyParser.urlencoded(options)
Returns middleware that only parses
urlencoded
bodies. This parser accepts only UTF-8 encoding of the body and supports automatic inflation 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
});
});
});// 获取总页数
});
可以封装成函数了——一下就少了三分之二的代码量。
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 %}
<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 部分代码。再改改行距,自适应图片等等。让文章好看些。
十七. 收尾
到目前为止,这个博客就基本实现了。
前端需要一些后端的逻辑,才能对产品有较为深刻的理解。
下面关于 Node.js 的内容你可能也喜欢:
在 Ubuntu 14.04/15.04 上安装配置 Node.js v4.0.0 http://www.linuxidc.com/Linux/2015-10/123951.htm
如何在 CentOS 7 安装 Node.js http://www.linuxidc.com/Linux/2015-02/113554.htm
Ubuntu 14.04 下搭建 Node.js 开发环境 http://www.linuxidc.com/Linux/2014-12/110983.htm
Ubunru 12.04 下 Node.js 开发环境的安装配置 http://www.linuxidc.com/Linux/2014-05/101418.htm
Node.Js 入门[PDF+ 相关代码] http://www.linuxidc.com/Linux/2013-06/85462.htm
Node.js 开发指南 高清 PDF 中文版 + 源码 http://www.linuxidc.com/Linux/2014-09/106494.htm
Linux 下安装 Node.js 详细完整教程 http://www.linuxidc.com/Linux/2017-01/139726.htm
Ubuntu 16.04 64 位 搭建 Node.js NodeJS 环境 http://www.linuxidc.com/Linux/2016-09/135487.htm
Node.js 的详细介绍:请点这里
Node.js 的下载地址:请点这里
本文永久更新链接地址:http://www.linuxidc.com/Linux/2017-02/140115.htm