Node.js 博客搭建

一. 学习需求

Node 的安装运行

会安装node,搭建node环境

会运行node。

基础模块的使用

Buffer:二进制数据处理模块

Event:事件模块

fs:文件系统模块

Net:网络模块

Http:http模块

...

NPM(node包管理工具)

第三方node模块(包)的管理工具,可以使用该下载工具安装第三方模块。,当然也可以创建上传自己的模块。

参考

假定已经理解并掌握了入门教程的所有内容。在易出错的地方将进行简要的说明。

其它

这是最不起眼,但也是最必不可少的——你得准备一个博客的静态文件。

博客的后台界面,登录注册界面,文章展示界面,首页等。


二. 项目需求分析

一个博客应当具备哪些功能?

前台展示

  • 点击下一页,可以点击分类导航。
  • 可以点击进入到具体博文页面
  • 下方允许评论。显示发表时间。允许留言分页。
  • 右侧有登录注册界面。

后台管理

  • 管理员账号:登陆后看到页面不一样,有后台页面。
  • 允许添加新的分类。从后台添加新的文章。
  • 编辑允许markdown写法。
  • 评论管理。

三. 项目创建,安装及初始化

技术框架

本项目采用了以下核心技术:

  • Node版本:6.9.1——基础核心的开发语言

    (安装后查看版本:cmd窗口:node -v)

(查看方式:cmd窗口:node -v

  • Express

    一个简洁灵活的node.js WEB应用框架,提供一系列强大的特性帮助我们创建web应用。

  • Mongodb

    用于保存产生的数据

还有一系列第三方模块和中间件:

  • bodyParser,解析post请求数据
  • cookies:读写cookie
  • swig:模板解析引擎
  • mongoose:操作Mongodb数据
  • markdown:语法解析生成模块

...

初始化

在W ebStorm创建一个新的空工程,指定文件夹。

打开左下角的Terminal输入:

npm init

回车。然后让你输入name:(code),输入项目名称,然后后面都可以不填,最后在Is it OK?处写上yes。

完成这一步操作之后,系统就会在当前文件夹创建一个package.json的项目文件。

项目文件下面拥有刚才你所基本的信息。后期需要更改的话可直接在这里修改。

第三方插件的安装

  • 以Express为例

    在命令行输入:

    npm install --save express
    

    耐心等待一段时间,安装完成后,json文件夹追加了一些新的内容:

    {
      //之前内容........
      "author": "",
      "license": "ISC",
      "dependencies": {
        "express": "^4.14.0"
      }
    

    表示安装成功。

同理,使用npm install --save xxx的方法安装下载以下模块:

  • body-parser
  • cookies
  • markdown
  • mongoose
  • swig

所以安装完之后的package.json文件是这样的。

{
  "name": "blog",
  "version": "1.0.0",
  "description": "this is my first blog.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.15.2",
    "cookies": "^0.6.2",
    "express": "^4.14.0",
    "markdown": "^0.5.0",
    "mongoose": "^4.7.5",
    "swig": "^1.4.2"
  }
}

在这个json中,就能通过依赖模块(dependencies)看到各个第三方模块的版本信息

切记:依赖模块安装,要联网!

安装完成之后

第二个文件夹放的是你的第三方模块。

此外还需要别的文件,完整的结构是这样的——

接下来就把缺失的文件目录自己建立起来。

完成着一系列操作之后,就把app.js作为应用程序的启动(入口页面)。

创建应用

以下代码创建应用,监听端口

// 加载express
var express=require('express');
//创建app应用,相当于=>Node.js Http.createServer();
var app=express();
//监听http请求
app.listen(9001);

运行(ctrl+shift+c)之后就可以通过浏览器访问了。

用户访问:

  • 用户通过URL访问web应用,比如http://localhost:9001/

这时候会发现浏览器呈现的内容是这样的。

  • web后端根据用户访问的url处理不同的业务逻辑。

  • 路由绑定——

    在Express框架下,可以通过app.get()app.post()等方式,把一个url路径和(1-n)个函数进行绑定。当满足对应的规则时,对应的函数将会被执行,该函数有三个参数——

    app.get('/',function(req,res,next){
      // do sth.
    });
    // req:request对象,保存客户请求相关的一些数据——http.request
    // res:response对象,服务端输出对象,停工了一些服务端相关的输出方法——http.response
    // next:方法,用于执行下一个和路径相匹配的函数(行为)。
    
  • 内容输出

通过res.send(string)发送内容到客户端。

app.get('/',function(req,res,next){
    res.send('<h1>欢迎光临我的博客!</h1>');
});

运行。这时候网页就打印出了h1标题的内容。

注意,js文件编码如果不为UTF-8,网页文件显示中文会受到影响。


三. 模板引擎的配置和使用

使用模板

现在,我想向后端发送的内容可不是一个h1标题那么简单。还包括整个博客页面的html内容,如果还是用上面的方法,麻烦就大了。

怎么办呢?关键步骤在于html和js页面相分离(类似结构和行为层的分离)。

模板的使用在于后端逻辑和前端表现的分离(前后端分离)。

模板配置

基本配置如下

// 定义模板引擎,使用swig.renderFile方法解析后缀为html的文件
var swig=require('swig');
app.engine('html',swig.renderFile);

// 设置模板存放目录
app.set('views','./views');
// 注册模板引擎
app.set('view engine','html');

swig.setDefaults({cache:false});

配置模板的基本流程是:

请求swig模块=>定义模板引擎=>注册模板引擎=>设置调试方法

我们可以使用var swig=require('swig');定义了swig方法。

以下进行逐行解析——

定义模板引擎

app.engine('html',swig.renderFile);

第一个参数:模板引擎的名称,同时也是模板引擎的后缀,你可以定义打开的是任何文件格式,比如json,甚至tdl等。
第二个参数表示用于解析处理模板内容的方法。
第三个参数:使用swig.renderFile方法解析后缀为html的文件。

设置模板目录

现在就用express组件提供的set方法标设置模板目录:

app.set('views','./views');

定义目录时也有两个参数,注意,第一个参数必须views!第二个参数可以是我们所给出的路径。因为之前已经定义了模板文件夹为views。所以,使用对应的路径名为./views

注册模板引擎

app.set('view engine','html');

还是使用express提供了set方法。
第一个参数必须是字符串'view engine'
第二个参数和app.engine方法定义的模板引擎名称(第一个参数)必须是一致的(都是“html”)。

重回app.get

现在我们回到app.get()方法里面,使用res.render()方法重新渲染指定内容

app.get('/',function(req,res,next){

    /*
    * 读取指定目录下的指定文件,解析并返回给客户端
    * 第一个参数:模板文件,相对于views目录,views/index.html
    * */

    res.render('index');
});

这时候,我们定义了返回值渲染index文件,就需要在views文件夹下新创建一个index.html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h1>欢迎来到我的第一个博客!<h1>
</body>
</html>

render方法还可以接受第二个参数,用于传递模板使用的第二个数据。

好了。这时候再刷新页面,就出现了index的内容。

调试方法

我们在不停止服务器的情况下,重新修改index的文件内容,发现并没有刷新。

什么问题呢?出于性能上考虑,node把第一次读取的index放到了内容中,下次访问时,就是缓存中的内容了,而不是真正的index文件。因此需要重启。

开发过程中,为了减少麻烦,需要取消模板缓存。

swig.setDefaults({cache:false});

当然,当项目上线时,可以把这一段删除掉。


四. 静态文件托管

在写模板文件时,经常引入一些外链的css,js和图片等等。

css怎么引入?

如果我们直接在首页的head区域这么写:

<link rel="stylesheet" type="text/css" href="css.css"/>

再刷新,发现对css.css的引用失败了。

问题不在于css.css是否存在,而在于请求失败。因为外链文件本质也是一个请求,但是在app.js中还没有对应设置。

如果这么写:

app.get('/css.css', function (req,res,next) {
    res.send('body {background: red;}');
});

发现没有效果。

打开http://localhost:9001/css.css发现内容是这样的:

搞笑了。默认发送的是一个html。因此需要设定一个header

app.get('/css.css', function (req,res,next) {
    res.setHeader('content-type','text/css');
    res.send('body {background: red;}');
});

ctrl+F5,就解析了红色背景了。

同样的,静态文件需要完全分离,因此这种方法也是不行的。

静态文件托管目录

最好的方法是,把所有的静态文件都放在一个public的目录下,划分并存放好。

然后在开头就通过以下方法,把public目录下的所有静态文件都渲染了:

app.use('/public',express.static(__dirname+'/public'));

以上方法表示:当遇到public文件下的文件,都调用第二个参数里的方法(注意是两个下划线)。

当用户访问的url以public开始,那么直接返回对应__dirname+'public'下的文件。因此我们的css应该放到public下。

引用方式为:

<link rel="stylesheet" type="text/css" href="../public/css.css"/>

然后到public文件下创建一个css.css,设置body背景为红色。原来的app.get方法就不要了。

至此,静态文件什么的都可以用到了

小结

在以上的内容中,我们实现了初始化项目,可以调用html和css文件。基本过程逻辑是:

用户发送http请求(url)=>解析路由=>找到匹配的规则=>指定绑定函数,返回对应内容到用户。

访问的是public:静态——直接读取指定目录下的文件,返回给用户。

=>动态=>处理业务逻辑

那么整个基本雏形就搭建起来了。


五. 分模块开发与实现

把整个网站放到一个app.js中,是不利于管理和维护的。实际开发中,是按照不同的功能,管理代码。

根据功能划分路由(routers)

根据本项目的业务逻辑,分为三个模块就够了。

  • 前台模块
  • 后台管理模块
  • API模块:通过ajax调用的接口。

或者,使用app.use(路由设置)划分:

  • app.use('/admin',require('./routers/admin'));

    解释:当用户访问的是admin文件下的内容,这调用router文件夹下admin.js文件。下同。

  • app.use('/api',require('./routers/api'));后台

  • app.use('/',require('./routers/main'));前台

好了。重写下以前的代码,去掉多余的部分。

// 加载express
var express=require('express');
//创建app应用,相当于=>Node.js Http.createServer();
var app=express();

// 设置静态文件托管
app.use('/public',express.static(__dirname+'/public'))

// 定义模板引擎,使用swig.renderFile方法解析后缀为html的文件
var swig=require('swig');
app.engine('html',swig.renderFile);

// 设置模板存放目录
app.set('views','./views');
// 注册模板引擎
app.set('view engine','html');
// 调试优化
swig.setDefaults({cache:false});

//app.use('/admin',require('./routers/admin'));
//app.use('/api',require('./routers/api'));
//app.use('/',require('./routers/main'));


//监听http请求
app.listen(9001);

routers创建一个admin.js,同理再创建一个api.js,一个main.js

怎么访问不同文件夹下的文件?

比如,我想访问一个如http://localhost:9001/admin/user这样的地址,这样按理来说就应该调用admin.js(分路由)。

所以编辑admin.js

var express=require('express');

// 创建一个路由对象,此对象将会监听admin文件下的url
var router=express.Router();

router.get('/user',function(req,res,next){
    res.send('user');
});

module.exports=router;//把router的结果作为模块的输出返回出去!

注意,在分路由中,不需要写明路径,就当它是在admin文件下的相对路径就可以了。

储存,然后回到app.js,应用app.use('/admin',require('./routers/admin'));

再打开页面,就看到结果了。

同理,api.js也如法炮制。

var express=require('express');

// 创建一个路由对象,此对象将会监听api文件夹下的url
var router=express.Router();

router.get('/user',function(req,res,next){
    res.send('api-user');
});

module.exports=router;//把router的结果作为模块的输出返回出去!

再应用app.use('api/',require('./routers/api'))。重启服务器,结果如下

首页也如法炮制

路由的细分

前台路由涉及了相当多的内容,因此再细化分多若干个路由也是不错的选择。

每个内容包括基本的分类和增删改

  • main模块

    /——首页

    /view——内容页

  • api模块

    /——首页

    /login——用户登陆

    /register——用户注册

    /comment——评论获取

    /comment/post——评论提交

  • admin模块

    /——首页

    • 用户管理

      /user——用户列表

    • 分类管理

      /category——分类目录

      /category/add——分类添加

      /category/edit——分类编辑

      /category/delete——分类删除

    • 文章管理

      /article——内容列表

      /article/add——添加文章

      /article/edit——文章修改

      /article/delete——文章删除

    • 评论管理

      /comment——评论列表

      /comment/delete——评论删除

开发流程

功能开发顺序

用户——栏目——内容——评论

一切操作依赖于用户,所以先需要用户。

栏目也分为前后台,优先做后台。

内容和评论相互关联。

编码顺序

  • 通过Schema定义设计数据储存结构
  • 功能逻辑
  • 页面展示

六. 数据库连接,表结构

比如用户,在SCHEMA文件夹下新建一个users.js

如何定义一个模块呢?这里用到mongoose模块

var mongoose=require('mongoose');//引入模块

除了在users.js请求mongoose模块以外,在app.js也需要引入mongoose。

// 加载express
var express=require('express')

//创建app应用,相当于=>Node.js Http.createServer();
var app=express();

// 加载数据库模块
var mongoose=require('mongoose');

// 设置静态文件托管
app.use('/public',express.static(__dirname+'/public'))

// 定义模板引擎,使用swig.renderFile方法解析后缀为html的文件
var swig=require('swig');
app.engine('html',swig.renderFile);

// 设置模板存放目录
app.set('views','./views');
// 注册模板引擎
app.set('view engine','html');
// 调试优化
swig.setDefaults({cache:false});

/*
* 根据不同的内容划分路由器
* */
app.use('/admin',require('./routers/admin'));
app.use('/api',require('./routers/api'));
app.use('/',require('./routers/main'));



//监听http请求
mongoose.connect();
app.listen(9001);

建立连接数据库(每次运行都需要这样)

mongoose使用需要安装mongodb数据库。

mongodb安装比较简单,在官网上下载了,制定好路径就可以了。

找到mongodb的bin文件夹。启动mongod.exe——通过命令行

命令行依次输入:

f:
cd Program Files\MongoDB\Server\3.2\bin

总之就是根据自己安装的的路径名来找到mongod.exe就行了。

开启数据库前需要指定参数,比如数据库的路径。我之前已经在项目文件夹下创建一个db文件夹,然后作为数据库的路径就可以了。

除此之外还得指定一个端口。比如27018

mongod --dbpath=G:\node\db --port=27018

然后回车

信息显示:等待链接27018,证明开启成功

下次每次关机后开启服务器,都需要做如上操作。

接下来要开启mongo.exe。

命令行比较原始,还是可以使用一些可视化的工具进行连接。在这里我用的是robomongo。

直接在国外网站上下载即可,下载不通可能需要科学上下网。

名字随便写就行了,端口写27018

点击链接。

回到命令行。发现新出现以下信息:

表示正式建立连接。

数据保存

链接已经建立起来。但里面空空如也。

接下来使用mongoose操作数据库。

可以上这里去看看文档。文档上首页就给出了mongoose.connect()方法。

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var Cat = mongoose.model('Cat', { name: String });

var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
  if (err) {
    console.log(err);
  } else {
    console.log('meow');
  }
});

connect方法接收的第一个参数,就是这个'mongodb://localhost:27018'。第二个参数是回调函数。

数据库链接失败的话,是不应该开启监听的,所以要把listen放到connect方法里面。

mongoose.connect('mongodb://localhost:27018/blog',function(err){
    if(err){
        console.log('数据库连接错误!');
    }else{
        console.log('数据库连接成功!');
      	app.listen(9001);
    }
});

运行,console显示,数据库链接成功。

注意,如果出现错误,还是得看看编码格式,必须为UTF-8。

回到users.js的编辑上来,继续看mongoose文档。

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var blogSchema = new Schema({
  title:  String,
  author: String,
  body:   String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs:  Number
  }
});

通过mongoose.Schema构造函数,生成一个Schema对象。

new出的Schema对象包含很多内容,传入的对象代表数据库中的一个表。每个属性代表表中的每一个字段,每个值代表该字段存储的数据类型。

在这里,users.js需要暴露的内容就是用户名和密码。

// 加载数据库模块
var mongoose=require('mongoose');

// 返回用户的表结构
module.exports= new mongoose.Schema({

    // 用户名
    username: String,
    // 密码
    password: String

});

然后在通过模型类来操作表结构。在项目的models文件夹下创建一个User.js

var mongoose=require('mongoose');

var usersSchema=require('../schemas/users');

module.exports=mongoose.model('User',usersSchema);

这样就完成了一个模型类的创建。

模型怎么用?还是看看文档给出的使用方法。

// 创建一个表结构对象
var schema = new mongoose.Schema({ name: 'string', size: 'string' });
// 根据表结构对象创建一个模型类
var Tank = mongoose.model('Tank', schema);

构造函数如何使用:

var Tank = mongoose.model('Tank', yourSchema);

var small = new Tank({ size: 'small' });
small.save(function (err) {
  if (err) return handleError(err);
  // saved!
})

// or

Tank.create({ size: 'small' }, function (err, small) {
  if (err) return handleError(err);
  // saved!
})

七. 用户注册的前端逻辑

引入首页

用户注册首先得加载一个首页。

在views下面新建一个main文件夹,然后把你之前写好的index.html放进去。

所以回到main.js中。渲染你已经写好的博客首页。

var express=require('express');

// 创建一个路由对象,此对象将会监听前台文件夹下的url
var router=express.Router();

router.get('/',function(req,res,next){
    res.render('main/index');
});

module.exports=router;//把router的结果作为模块的输出返回出去!

保存,然后重启app.js,就能在localhost:9001看到首页了。

当然这个首页很丑,你可以自己写一个。

原来的路径全部按照项目文件夹的结构进行修改。

逻辑

注册登录一共有三个状态。

一开始就是注册,如果已有账号就点击登录,出现登录弹窗。

如果已经登录,则显示已经登录状态。并有注销按钮。

			<div class="banner-wrap">
                <div class="login" id="register">
                <h3>注册</h3>
                <span>用户:<input name="username" type="text"/></span><br/>
                <span>密码:<input name="password" type="text"/></span><br/>
                <span>确认:<input name="repassword" type="text"/></span><br/>
                <span><input class="submit" type="button" value="提交"/></span>
                <span>已有账号?马上<a href="javascript:;">登录</a></span>
            </div>

            <div class="login" id="login" style="display:none;">
                <h3>登录</h3>
                <span>用户:<input type="text"/></span><br/>
                <span>密码:<input type="text"/></span><br/>
                <span><input type="button" value="提交"/></span>
                <span>没有账号?马上<a href="javascript:;">注册</a></span>
            </div>

jquery可以这么写:

$(function(){
    // 登录注册的切换
    $('#register a').click(function(){
        $('#login').show();
        $('#register').hide();
    });

    $('#login a').click(function(){
        $('#login').hide();
        $('#register').show();
    });
});

当点击注册按钮,应该允许ajax提交数据。地址应该是api下的user文件夹的register,该register文件暂时没有创建,所以不理他照写即可。

// 点击注册按钮,通过ajax提交数据
    $('#register .submit').click(function(){
        // 通过ajax提交交
        $.ajax({
            type:'post',
            url:'/api/user/register',
            data:{
                username:$('#register').find('[name="username"]').val(),
                password:$('#register').find('[name="password"]').val(),
                repassword:$('#register').find('[name="repassword"]').val()
            },
            dataType:'json',
            success:function(data){
                console.log(data);
            }
        });
    });

允许网站,输入用户名密码点击注册。

虽然报错,但是在chrome的network下的header可以看到之前提交的信息。

挺好,挺好。


八. body-paser的使用:后端的基本验证

后端怎么响应前台的ajax请求?

首先,找到API的模块,增加一个路由,回到api.js——当收到前端ajax的post请求时,路由打印出一个register字符串。

var express=require('express');

// 创建一个路由对象,此对象将会监听api文件夹下的url
var router=express.Router();

router.post('/user/register',function(req,res,next){
    console.log('register');
});

module.exports=router;//把router的结果作为模块的输出返回出去!

这时候,就不会显示404了。说明路由处理成功。

如何获取前端post的数据?

这就需要用到新的第三方模块——body-parser

相关文档地址:https://github.com/expressjs/body-parser

bodyParser.urlencoded(options)

Returns middleware that only parses urlencoded bodies. This parser accepts only UTF-8 encoding of the body and supports automatic inflation 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
            });
        });
    });//获取总页数
});

可以封装成函数了——一下就少了三分之二的代码量。

function renderAdminTable(obj,type,limit){
    router.get('/'+type+'/', function (req,res,next) {
        var page=req.query.page||1;

        var count=0;

        obj.count().then(function(_count){
            count=_count;
            var pages=Math.ceil(count/limit);

            page=Math.min(page,pages);
            page=Math.max(page,1);

            var skip=(page-1)*limit;

            obj.find().limit(limit).skip(skip).then(function(data){

                res.render('admin/'+type+'_index',{
                    type:type,
                    userInfo:req.userInfo,
                    data:data,
                    page:page,
                    pages:pages,
                    limit:limit,
                    count:count
                });
            });
        });//获取总页数
    });
}
//调用时,
//用户管理首页
var User=require('../models/User');
renderAdminTable(User,'user',1);
//分类管理首页
// 添加分类及保存方法
var Category=require('../models/Categories');
renderAdminTable(Category,'category',2);

模板

{% extends 'layout.html' %}

{% block main %}

<h3>分类列表</h3>

<table class="users-list">
    <thead>
    <tr>
        <th>id</th>
        <th>分类名</th>
        <th>备注</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    {% for category in data %}
    <tr>
        <td>{{category._id.toString()}}</td>
        <td>{{category.name}}</td>
        <td>
          <a href="/admin/category/edit">修改 </a>
            |<a href="/admin/category/edit"> 删除</a>
      	</td>
        <td></td>
    </tr>
    {% endfor %}
    </tbody>
</table>

{%include 'page.html'%}
{% endblock %}

博客分类的修改与删除

基本逻辑

删除的按钮是/admin/category/delete?id={{category._id.toString()}},同理修改的按钮是/admin/category/edit?id={{category._id.toDtring()}}(带id的请求)。

这意味着两个新的页面和路由:

分类修改,分类删除。

删除和修改都遵循一套比较严谨的逻辑。其中修改的各种判断相当麻烦,但是,修改和删除的逻辑基本是一样的。

当一个管理员在进行修改时,另一个管理员也可能修改(删除)了数据。因此需要严格判断。

修改(update)

修改首先做的是逻辑,根据发送请求的id值进行修改。如果id不存在则返回错误页面,如果存在,则切换到新的提交页面

// 分类修改
router.get('/category/edit',function(req,res,next){

    // 获取修改的分类信息,并以表单的形式呈现,注意不能用body,_id是个对象,不是字符串
    var id=req.query.id||'';

    // 获取要修改的分类信息
    Category.findOne({
        _id:id
    }).then(function(category){
        if(!category){
            res.render('admin/error',{
                userInfo:req.userInfo,
                message:'分类信息不存在!'
            });
            return Promise.reject();
        }else{
            res.render('admin/edit',{
                userInfo:req.userInfo,
                category:category
            });
        }
    });
});

然后是一个提交页,post返回的是当前页面

{% extends 'layout.html' %}

{% block main %}

<h3>分类管理    <small>>编辑分类</small></h3>

<form method="post">
    <span>分类名</span><br/>
    <input type="text" value="{{category.name}}" name="name"/>
    <button type="submit">提交</button>
</form>

还是以post请求保存数据。

  • 提交数据同样也需要判断id,当id不存在时,跳转到错误页面。

  • 当id存在,而且用户没有做任何修改,就提交,直接跳转到“修改成功”页面。实际上不做任何修改。

  • 当id存在,而且用户提交过来的名字和非原id({$ne: id})下的名字不同时,做两点判断:

    • 数据库是否存在同名数据?是则跳转到错误页面。

    • 如果数据库不存在同名数据,则更新同id下的name数据值,并跳转“保存成功”。

      更新的方法是

      Category.update({
        _id:你的id
      },{
        要修改的key:要修改的value
      })
      

根据此逻辑可以写出这样的代码。

//分类保存
router.post('/category/edit/',function(req,res,next){
    var id=req.query.id||'';

    var name=req.body.name||name;
    Category.findOne({
        _id:id
    }).then(function(category){

        if(!category){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'分类信息不存在!'
            });
            return Promise.reject();
        }else{
            // 如果用户不做任何修改就提交
            if(name==category.name){
                res.render('admin/success',{
                    userInfo:req.body.userInfo,
                    message:'修改成功!',
                    operation:{
                        url:'/admin/category',
                        operation:'返回分类管理'
                    }
                });
                return Promise.reject();
            }else{
                // id不变,名称是否相同
                Category.findOne({
                    _id: {$ne: id},
                    name:name
                }).then(function(same){

                    if(same){
                        res.render('admin/error',{
                            userInfo:req.body.userInfo,
                            message:'已经存在同名数据!'
                        });
                        return Promise.reject();
                    }else{

                        Category.update({
                            _id:id
                        },{
                            name:name
                        }).then(function(){
                            res.render('admin/success',{
                                userInfo:req.body.userInfo,
                                message:'修改成功!',
                                operation:{
                                    url:'/admin/category',
                                    operation:'返回分类管理'
                                }
                            });
                        });


                    }
                });
            }
        }
    });
});

为了防止异步问题,可以写得更加保险一点。让它每一步都返回一个promise对象,

//分类保存
router.post('/category/edit/',function(req,res,next){
    var id=req.query.id||'';

    var name=req.body.name||name;
    Category.findOne({
        _id:id
    }).then(function(category){

        if(!category){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'分类信息不存在!'
            });
            return Promise.reject();
        }else{
            // 如果用户不做任何修改就提交
            if(name==category.name){
                res.render('admin/success',{
                    userInfo:req.body.userInfo,
                    message:'修改成功!',
                    operation:{
                        url:'/admin/category',
                        operation:'返回分类管理'
                    }
                });
                return Promise.reject();
            }else{
                // 再查询id:不等于当前id
                return Category.findOne({
                    _id: {$ne: id},
                    name:name
                });
            }
        }
    }).then(function(same){
        if(same){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'已经存在同名数据!'
            });
            return Promise.reject();
        }else{
            return Category.update({
                _id:id
            },{
                name:name
            });
        }
    }).then(function(resb){
        res.render('admin/success',{
            userInfo:req.body.userInfo,
            message:'修改成功!',
            operation:{
                url:'/admin/category',
                operation:'返回分类管理'
            }
        });
    });
});

这样就能实现修改了。

删除(remove)

删除的逻辑类似。但是要简单一些,判断页面是否还存在该id,是就删除,也不需要专门去写删除界面。,只需要一个成功或失败的界面就OK了。

删除用的是remove方法——把_id属性为id的条目删除就行啦

// 分类的删除
router.get('/category/delete',function(req,res){
    var id=req.query.id;

    Category.findOne({
        _id:id
    }).then(function(category){
        if(!category){
            res.render('/admin/error',{
                userInfo:req.body.userInfo,
                message:'该内容不存在于数据库中!',
                operation:{
                    url:'/admin/category',
                    operation:'返回分类管理'
                }
            });
            return  Promise.reject();
        }else{
            return Category.remove({
             _id:id
             })
        }
    }).then(function(){
        res.render('admin/success',{
            userInfo:req.body.userInfo,
            message:'删除分类成功!',
            operation:{
                url:'/admin/category',
                operation:'返回分类管理'
            }
        });
    });
});

前台分类导航展示与排序

前台的导航分类是写死的,现在是时候把它换成我们需要的内容了。

因为我个人项目的关系,我一级导航是固定的。所以就在文章分类下实现下拉菜单。

从数据库读取前台首页内容,基于main.js

为此还得引入Category

var Category=require('../models/Categories');
router.get('/',function(req,res,next){
    // 读取分类信息
    Category.find().then(function(rs){
        console.log(rs)
    });

    res.render('main/index',{
        userInfo:req.userInfo
    });
});

运行后打印出来的信息是:

就成功拿到了后台数据。

接下来就是把数据加到模板里面去啦

var Category=require('../models/Categories');

router.get('/',function(req,res,next){

    // 读取分类信息
    Category.find().then(function(categories){
        console.log(categories);
        res.render('main/index',{
            userInfo:req.userInfo,
            categories:categories
        });
    });

});

前端模板这么写:

<ul class="nav-article">
                            {% if !userInfo._id %}
                            <li><a href="javascript:;">仅限注册用户查看!</a></li>
                            {% else %}
                            {% for category in categories %}
                            <li><a href="javascript:;">{{category.name}}</a></li>
                            {% endfor %}
                            {% endif %}
                        </ul>

你在后台修改分类,

结果就出来了。挺好,挺好。

然而有一个小问题,就是我们拿到的数据是倒序的。

思路1:在后端把这个数组reverse一下。就符合正常的判断逻辑了。

res.render('main/index',{
            userInfo:req.userInfo,
            categories:categories.reverse()
        });

但这不是唯一的思路,从展示后端功能的考虑,最新添加的理应在最后面,所以有了思路2

思路2:回到admin.js对Category进行排序。

id表面上看是一串毫无规律的字符串,然而它确实是按照时间排列的。

那就行了,根据id用sort方法排序

obj.find().sort({_id:-1})......
//-1表示降序,1表示升序

博客分类管理这部分到此结束了。


十五. 文章管理(1):后台

文章管理还是基于admin.js

<!--layout.html-->
<li><a href="/admin/content">文章管理</a></li>

增加一个管理首页

<!--content.html-->
{% extends 'layout.html' %}

{% block main %}

<h3>文章管理 </h3>
<a href="content/add">添加新的文章!</a>

<!--表格-->

{% endblock %}

再增加一个编辑文章的界面,其中,要获取分类信息

{% extends 'layout.html' %}

{% block main %}

<h3>文章管理    <small>>添加文章</small></h3>

<form method="post">

    <span>标题</span>
    <input type="text" name="title"/>
    <span>分类</span>
    <select name="categories">
        {% for category in categories %}
        <option value="{{category._id.toString()}}">{{category.name}}</option>
        {% endfor %}
    </select>
    <button type="submit">提交</button><br>
    <span style="line-height: 30px;">内容摘要</span><br>
    <textarea id="description" cols="150" rows="3" placeholder="请输入简介" name="description">

    </textarea>
    <br>
    <span style="line-height: 20px;">文章正文</span><br>
    <textarea id="article-content">

    </textarea>

</form>

{% endblock %}

效果如下

再写两个路由。

// admin.js
// 内容管理
router.get('/content',function(req,res,next){

    res.render('admin/content_index',{
        userInfo:req.userInfo
    });
});

// 添加文章
router.get('/content/add',function(req,res,next){
    Category.find().then(function(categories){
        console.log(categories)
        res.render('admin/content_add',{
            userInfo:req.userInfo,
            categories:categories
        });
    })
});

获取数据

还是用到了schema设计应该存储的内容。

最主要的当然是文章相关——标题,简介,内容,发表时间。

还有一个不可忽视的问题,就是文章隶属分类。我们是根据分类id进行区分的

// schemas文件夹下的content.js

var mongoose=require('mongoose');

module.exports=new mongoose.Schema({
    // 关联字段 -分类的id
    category:{
        // 类型
        type:mongoose.Schema.Tpyes.ObjectId,
        // 引用,实际上是说,存储时根据关联进行索引出分类目录下的值。而不是存进去的值。
        ref:'Category'
    },

    // 标题
    title:String,

    // 简介
    description:{
        type:String,
        default:''
    },

    // 文章内容
    content:{
        type:String,
        default:''
    },

    // 当前时间
    date:String

});

接下来就是创建一个在models下面创建一个Content模型

// model文件夹下的Content.js

var mongoose=require('mongoose');
var contentsSchema=require('../schemas/contents');

module.exports=mongoose.model('Content',contentsSchema);

内容保存是用post方式提交的。

因此再写一个post路由

//admin.js
// 内容保存
router.post('/content/add',function(req,res,next){

    console.log(req.body);
});

在后台输入内容,提交,就看到提交上来的数据了。

不错。

表单验证

简单的验证规则:不能为空

验证不能为空的时候,应该调用trim方法处理之后再进行验证。

// 内容保存
router.post('/content/add',function(req,res,next){
    console.log(req.body)
    if(req.body.category.trim()==''){
        res.render('admin/error',{
            userInfo:req.userInfo,
            message:'分类信息不存在!'
        });
        return Promise.reject();
    }

    if(req.body.title.trim()==''){
        res.render('admin/error',{
            userInfo:req.userInfo,
            message:'标题不能为空!'
        });
        return Promise.reject();
    }

    if(req.body.content.trim()==''){
        res.render('admin/error',{
            userInfo:req.userInfo,
            message:'内容忘了填!'
        });
        return Promise.reject();
    }


});

还有个问题。就是简介(摘要)

保存数据库数据

保存和渲染相关的方法都是通过引入模块来进行的。

var Content=require('../models/Contents');
····

new Content({
        category:req.body.category,
        title:req.body.title,
        description:req.body.description,
        content:req.body.content,
  		date:new Date().toDateString()
    }).save().then(function(){
            res.render('admin/success',{
                userInfo:req.userInfo,
                message:'文章发布成功!'
            });
        });
····

然后你发布一篇文章,验证无误后,就会出现“发布成功”的页面。

然后你就可以在数据库查询到想要的内容了

这个对象有当前文章相关的内容,也有栏目所属的id,也有内容自己的id。还有日期

为了显示内容,可以用之前封装的renderAdminTable函数

{% extends 'layout.html' %}

{% block main %}

<h3>文章管理 </h3>
<a href="content/add">添加新的文章!</a>

<table class="users-list">
    <thead>
    <tr>
        <th>标题</th>
        <th>所属分类</th>
        <th>发布时间</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    {% for content in data %}

    <tr>
        <td>{{content.title}}</td>
        <td>{{content.category}}</td>
        <td>
            {{content.date}}
        </td>
        <td>
            <a href="/admin/content/edit?id={{content._id.toString()}}">修改 </a>
            |<a href="/admincontent/delete?id={{content._id.toString()}}"> 删除</a>
        </td>
    </tr>
    {% endfor %}
    </tbody>
</table>

{%include 'page.html'%}
{% endblock %}

分类名显示出来的是个object

分类名用的是data.category

但如果换成data.category.id就能获取到一个buffer对象,这个buffer对象转换后,应该就是分类信息。

但是直接用的话,又显示乱码。

这就有点小麻烦了。

回看schema中的数据库,当存储后,会自动关联Category模对象(注意:这里的Category当然是admin.js的Category)进行查询。查询意味着有一个新的方法populate。populate方法的参数是执行查询的属性。在这里我们要操作的属性是category

// 这是一个功能函数
function renderAdminTable(obj,type,limit,_query){
    router.get('/'+type+'/', function (req,res,next) {
        var page=req.query.page||1;
        var count=0;

        obj.count().then(function(_count){
            count=_count;
            var pages=Math.ceil(count/limit);
            page=Math.min(page,pages);
            page=Math.max(page,1);

            var skip=(page-1)*limit;
            /*
            * sort方法排序,根据id,
            * */
            var newObj=_query?obj.find().sort({_id:-1}).limit(limit).skip(skip).populate(_query):obj.find().sort({_id:-1}).limit(limit).skip(skip);
            newObj.then(function(data){
                console.log(data);

                res.render('admin/'+type+'_index',{
                    type:type,
                    userInfo:req.userInfo,
                    data:data,
                    page:page,
                    pages:pages,
                    limit:limit,
                    count:count
                });
            });
        });//获取总页数
    });
}

diao调用时写法为:renderAdminTable(Content,'content',2,'category');

打印出来的data数据为:

发现Category的查询结果就返回给data的category属性了

很棒吧!那就把模板改了

不错不错。

修改和删除

修改和删除基本上遵照同一个逻辑。

修改

请求的文章id如果在数据库查询不到,那就返回错误页面。否则渲染一个编辑页面(content_edit)——注意,这里得事先获取分类。

// 修改
router.get('/content/edit',function(req,res,next){
    var id=req.query.id||'';

    Content.findOne({
        _id:id
    }).then(function(content){
       if(!content){
           res.render('admin/error',{
               userInfo:req.userInfo,
               message:'该文章id事先已被删除了。'
           });
           return Promise.reject();
       }else{
           Category.find().then(function(categories){
               // console.log(content);
               res.render('admin/content_edit',{
                   userInfo:req.userInfo,
                   categories:categories,
                   data:content
               });
           });
       }
    });
});

把前端页面显示出来之后就是保存。

保存的post逻辑差不多,但实际上可以简化。

// 保存文章修改
router.post('/content/edit',function(req,res,next){
    var id=req.query.id||'';

    Content.findOne({
        _id:id
    }).then(function(content){
        if(!content){
            res.render('admin/error',{
                userInfo:req.body.userInfo,
                message:'文章id事先被删除了!'
            });
            return Promise.reject();
        }else{
            return Content.update({
                _id:id
            },{
                category:req.body.category,
                title:req.body.title,
                description:req.body.description,
                content:req.body.content
            });
        }
    }).then(function(){
        res.render('admin/success',{
            userInfo:req.body.userInfo,
            message:'修改成功!',
            operation:{
                url:'/admin/content',
                operation:'返回分类管理'
            }
        });
    });
});

删除

基本差不多。

router.get('/content/delete',function(req,res,next){
   var id=req.query.id||'';

    Content.remove({
        _id:id
    }).then(function(){
        res.render('admin/success',{
            userInfo:req.userInfo,
            message:'删除文章成功!',
            operation:{
                url:'/admin/content',
                operation:'返回分类管理'
            }
        });
    });
});

信息扩展(发布者,点击量)

可以在数据表结构中再添加两个属性

user: {
        //类型
        type:mongoose.Schema.Types.objectId,
        //引用
        ref:'User'
    },
    
    views:{
      type:Number,
      default:0
    }

然后在文章添加时,增添一个user属性,把req.userInfo._id传进去。

显示呢?实际上populate方法接受一个字符串或者有字符串组成的数组。所以数组应该是xxx.populate(['category','user'])。这样模板就能拿到user的属性了。

然后修改模板,让它展现出来:


十六. 文章管理(2):前台

先给博客写点东西吧。当前的文章确实太少了。

当我们写好了文章,内容就已经存放在服务器上了。前台怎么渲染是一个值得考虑的问题。显然,这些事情都是main.js完成的。

这时候注意了,入门一个领域,知道自己在干什么是非常重要的。

获取数据集

由于业务逻辑,我的博客内容设置为不在首页展示,需要在/article页专门展示自己的文章,除了全部文章,分类链接渲染的是:/article?id=xxx

先看全部文章下的/article怎么渲染吧。

文章页效果预期是这样的:

  • 文章页需要接收文章的信息。
  • 文章需要接收分页相关的信息。

文章页需要接收的信息比较多,所以写一个data对象,把这些信息放进去,到渲染时直接用这个data就行了。

//main.js
var express=require('express');

var router=express.Router();

var Category=require('../models/Categories');
var Content=require('../models/Content');
/*
*省略首页路由
*
*/
router.get('/article',function(req,res,next){
    var data={
        userInfo:req.userInfo,
        categories:[],
        count:0,
        page:Number(req.query.page||1),
        limit:3,
        pages:0
    };

    // 读取分类信息
    Category.find().then(function(categories){
        data.categories=categories;

        return Content.count();
    }).then(function(count){
        data.count=count;
        //计算总页数
        data.pages=Math.ceil(data.count/data.limit);
        // 取值不超过pages
        data.page=Math.min(data.page,data.pages);
        // 取值不小于1
        data.page=Math.max(data.page,1);
        // skip不需要分配到模板中,所以忽略。
        var skip=(data.page-1)*data.limit;

        return Content.find().limit(data.limit).skip(skip).populate(['category','user']).sort(_id:-1);


    }).then(function(contents){
        data.contents=contents;
        console.log(data);//这里有你想要的所有数据
        res.render('main/article',data);
    })
});

该程序反映了data一步步获取内容的过程。

前台应用数据

  • 我只需要对文章展示做个for循环,然后把数据传进模板中就可以了。

     {% for content in contents %}
                    <div class="cell">
                        <div class="label">
                            <time>{{content.date.slice(5,11)}}</time>
                            <div>{{content.category.name.slice(0,3)+'..'}}</div>
                        </div>
                        <hgroup>
                            <h3>{{content.title}}</h3>
                            <h4>{{content.user.username}}</h4>
                        </hgroup>
                        
                        <p>{{content.description}}</p>
                        <address>推送于{{content.date}}</address>
                    </div>
                    {% endfor %}
    
  • 侧边栏有一个文章内容分类区,把数据传进去就行了。

  • 分页按钮可以这样写

    <div class="pages-num">
                        <ul>
                            <li><a href="/article?page=1">第一页</a></li>
                            {% if page-1!==0 %}
                            <li><a href="/article?page={{page-1}}">上一页</a></li>
                            {%endif%}
    
                            {% if page+1<=pages %}
                            <li><a href="/article?page={{page+1}}">下一页</a></li>
                            {% endif %}
                            <li><a href="/article?page={{pages}}">最后页</a></li>
                        </ul>
                    </div>
    

效果:

你会发现,模板的代码越写越简单。

获取分类下的页面(where方法)

现在来解决分类的问题。

之前我们写好的分类页面地址为/article?category={{category._id.toString()}}

所以要对当前的id进行响应。如果请求的category值为不空,则调用where显示。

router.get('/article',function(req,res,next){
    var data={
        userInfo:req.userInfo,
        category:req.query.category||'',
        categories:[],
        count:0,
        page:Number(req.query.page||1),
        limit:3,
        pages:0
    };
    var where={};
    if(data.category){
        where.category=data.category
    }
  //...
  return Content.where(where).find().limit(data.limit).skip(skip).sort({_id:-1}).populate(['category','user']);

这样点击相应的分类,就能获取到相应的资料了。

但是页码还是有问题。原因在于count的获取,也应该根据where进行查询。

return Content.where(where).count();

另外一个页码问题是,页码的链接写死了。

只要带上category就行了。

所以比较完整的页码判断是:

<ul>
                        {% if pages>0 %}
                        <li><a href="/article?category={{category.toString()}}&page=1">第一页</a></li>
                        {% if page-1!==0 %}
                        <li><a href="/article?category={{category.toString()}}&page={{page-1}}">上一页</a></li>
                        {%endif%}

                        <li style="background:rgb(166,96,183);"><a style="color:#fff;"  href="javascript:;">{{page}}/{{pages}}</a></li>

                        {% if page+1<=pages %}
                        <li><a href="/article?category={{category.toString()}}&page={{page+1}}">下一页</a></li>
                        {% endif %}
                        <li><a href="/article?category={{category.toString()}}&page={{pages}}">最后页</a></li>
                        {% else %}
                        <li style="width: 100%;text-align: center;">当前分类没有任何文章!</li>
                        {% endif %}
                    </ul>

然后做一个当前分类高亮显示的判断

<ul>
                        {% if category=='' %}
                        <li><a style="border-left: 6px solid #522a5c;" href="/article">全部文章</a></li>
                        {%else%}
                        <li><a href="/article">全部文章</a></li>
                        {% endif %}
                        {% for _category in categories %}
                        {% if category.toString()==_category._id.toString() %}
                        <li><a style="border-left: 6px solid #522a5c;" href="/article?category={{_category._id.toString()}}">{{_category.name}}</a></li>
                        {% else %}
                        <li><a href="/article?category={{_category._id.toString()}}">{{_category.name}}</a></li>
                        {% endif %}
                        {% endfor %}
                    </ul>

展示文章详细信息

同理内容详情页需要给个链接,然后就再写一个路由。在这里我用的是/view?contentid={{content._id}}

基本逻辑

需要哪些数据?

  • userInfo
  • 全部分类信息
  • 文章内容(content)——包括当前文章所属的分类信息

查询方式:contentId

router.get('/view/',function(req,res,next){
    var contentId=req.query.contentId||'';
    var data={
        userInfo:req.userInfo,
        categories:[],
        content:null
    };

    Category.find().then(function(categories){
        data.categories=categories;
        return Content.findOne({_id:contentId});
    }).then(function(content){
        data.content=content;
        console.log(data);
        res.render('main/view',data);
    });
});

发现可以打印出文章的主要内容了。

接下来就是写模板。

新建一个article_layout.html模板,把article.html的所有内容剪切进去。

博客展示页的主要区域在于之前的内容列表。所以把它抽离出来。

把一个个内容按照逻辑加上去,大概就是这样。

阅读数的实现

很简单,每当用户点击文章,阅读数就加1.

router.get('/view/',function(req,res,next){
    var contentId=req.query.contentId||'';
    var data={
        userInfo:req.userInfo,
        categories:[],
        content:null
    };

    Category.find().then(function(categories){
        data.categories=categories;
        return Content.findOne({_id:contentId});
    }).then(function(content){
        data.content=content;
        content.views++;//保存阅读数
        content.save();
        console.log(data);
        res.render('main/view',data);
    });
});

内容评论

先把评论的样式写出来吧!大概是这样

评论是通过ajax提交的。是在ajax模块——api.js

评论的post提交到数据库,应该放到数据库的contents.js中。

// 评论
    comments: {
        type:Array,
        default:[]
    }

每条评论包括如下内容:

评论者,评论时间,还有评论的内容。

在api.js中写一个post提交的路由

// 评论提交
router.post('/comment/post',function(req,res,next){
    // 文章的id是需要前端提交的。
    var contentId=req.body.contentId||'';
    var postData={
        username:req.userInfo.username,
        postTime: new ConvertDate().getDate(),
        content: req.body.content
    };

    // 查询当前内容信息
    Content.findOne({
        _id:contentId
    }).then(function(content){
        content.comments.push(postData);
        return content.save()
    }).then(function(newContent){//最新的内容在newContent!
        responseData.message='评论成功!';
        res.json(responseData);
    })

});

然后在你的view页面相关的文件中写一个ajax方法,我们要传送文章的id

但是文章的id最初并没有发送过去。可以在view页面写一个隐藏的input#contentId,把当前文章的id存进去。然后通过jQuery拿到数据。

// 评论提交
    $('#messageComment').click(function(){
        $.ajax({
            type:'POST',
            url:'/api/comment/post',
            data:{
                contentId:$('#contentId').val(),
                content:$('#commentValue').val(),
            },
            success:function(responseData){
                console.log(responseData);
            }
        });
        return false;

    });

很简单吧!

评论提交后,清空输入框,然后下方出现新增加的内容。

最新的内容从哪来呢?在newContent处。所以我们只需要让responseData存进newContent,就能实现内容添加。

// api.js
//...
// 查询当前内容信息
    Content.findOne({
        _id:contentId
    }).then(function(content){
        content.comments.push(postData);
        return content.save()
    }).then(function(newContent){
        responseData.message='评论成功!';
        responseData.data=newContent;
        res.json(responseData);
    })

//...

看,这样就拿到数据了。

接下来就在前端渲染页面:

用这个获取内容。

function renderComment(arr){
    var innerHtml='';
    for(var i=0;i<arr.length;i++){
        innerHtml='<li><span class="comments-user">'+arr[i].username+' </span><span class="comments-date">'+arr[i].postTime+'</span><p>'+arr[i].content+'</p></li>'+innerHtml;
    }
    return innerHtml;
}
    // 评论提交
    $('#messageComment').click(function(){
        $.ajax({
            type:'POST',
            url:'/api/comment/post',
            data:{
                contentId:$('#contentId').val(),
                content:$('#commentValue').val(),
            },
            success:function(responseData){
                console.log(responseData);
                alert(responseData.message);
                var arr= responseData.data.comments;
                //console.log(renderComment(arr));
                $('.comments').html(renderComment(arr));
            }
        });
        return false;

    });

这样就可以显示出来了。但是发现页面一刷新,内容就又没有了——加载时就调用ajax方法。

api是提供一个虚拟地址,ajax能够从这个地址获取数据。

从新写一个路由:

//api.js
// 获取指定文章的所有评论
router.get('/comment',function(req,res,next){
    var contentId=req.query.contentId||'';
    Content.findOne({
       _id:contentId
    }).then(function(content){
        responseData.data=content;
        res.json(responseData);
    })
});

注意这里是get方式

 //每次文章重载时获取该文章的所有评论
    $.ajax({
        type:'GET',
        url:'/api/comment',
        data:{
            contentId:$('#contentId').val(),
            content:$('#commentValue').val(),
        },
        success:function(responseData){
            console.log(responseData);
            var arr= responseData.data.comments;
            //console.log(renderComment(arr));
            $('.comments').html(renderComment(arr));
            $('#commentValue').val('');
            $('#commentsNum').html(arr.length)
        }
    });

评论分页

分因为是ajax请求到的数据,所以完全可以在前端完成。

评论分页太老旧了。不如做个伪瀑布流吧!

预期效果:点击加载更多按钮,出现三条评论。

之所以说是伪,因为评论一早就拿到手了。只是分段展示而已。当然你也可以写真的。每点击一次都触发新的ajax请求。只请求三条新的数据。

评论部分完全可以写一个对象。重置方法,加载方法,获取数据方法。

写下来又是一大篇文章。

// 加载评论的基本逻辑
function Comments(){
    this.count=1;
    this.comments=0;
}

在ajax请求评论内容是时,给每条评论的li加一个data-index值。

// 获取评论内容
Comments.prototype.getComment=function(arr){
    var innerHtml='';
    this.comments=arr.length;//获取评论总数
    for(var i=0;i<arr.length;i++){
        innerHtml=
            '<li data-index='+(arr.length-i)+'><span class="comments-user">'+
            arr[i].username+
            ' </span><span class="comments-date">'+
            arr[i].postTime+
            '</span><p>'+
            arr[i].content+
            '</p></li>'+innerHtml;
    }
    
    return innerHtml;
};

在每次加载页面,每次发完评论的时候,都初始化评论页面。首先要做的是解绑加载按钮可能的事件。当评论数少于三条,加载按钮变成“没有更多了”。超过三条时,数据自动隐藏。

Comments.prototype.resetComment=function (limit){
    this.count=1;
    this.comments=$('.comments').children().length;//获取评论总数
    $('#load-more').unbind("click");

    if(this.comments<limit){
        $('#load-more').text('..没有了');
    }else{
        $('#load-more').text('加载更多');
    }

    for(var i=1;i<=this.comments;i++){
        if(i>limit){
            $('.comments').find('[data-index='+ i.toString()+']').css('display','none');
        }
    }
};

点击加载按钮,根据点击计数加载评论

Comments.prototype. loadComments=function(limit){
    var _this=this;
    $('#load-more').click(function(){
        //console.log([_this.comments,_this.count]);
        if((_this.count+1)*limit>=_this.comments){
            $(this).text('..没有了');

        }
        _this.count++;

        for(var i=1;i<=_this.comments;i++){
            if(_this.count<i*_this.count&&i<=(_this.count)*limit){
                $('.comments').find('[data-index='+ i.toString()+']').slideDown(300);
            }
        }
    });
};

然后就是在网页中应用这些方法:

$(function(){
    //每次文章重载时获取该文章的所有评论
    $.ajax({
        type:'GET',
        url:'/api/comment',
        data:{
            contentId:$('#contentId').val(),
            content:$('#commentValue').val(),
        },
        success:function(responseData){

            var arr= responseData.data.comments;
            //渲染评论的必要方法
            var renderComments=new Comments();

            //获取评论内容
            $('.comments').html(renderComments.getComment(arr));

            //清空评论框
            $('#commentValue').val('');

            //展示评论条数
            $('#commentsNum').html(arr.length);

            //首次加载展示三条,每点击一次加载3条
            renderComments.resetComment(3);
            renderComments.loadComments(3);


            // 评论提交
            $('#messageComment').click(function(){
                $.ajax({
                    type:'POST',
                    url:'/api/comment/post',
                    data:{
                        contentId:$('#contentId').val(),
                        content:$('#commentValue').val(),
                    },
                    success:function(responseData){

                        alert(responseData.message);
                        var arr= responseData.data.comments;
                        $('.comments').html(renderComments.getComment(arr));
                        $('#commentValue').val('');
                        $('#commentsNum').html(arr.length);

                        renderComments.resetComment(3);
                        renderComments.loadComments(3);
                    }
                });
                return false;
            });
            
            
            
        }
    });

});

发布者信息和文章分类展示

get方式获取的内容中虽然有了文章作者id,但是没有作者名。也缺失当前文章的内容。所以在get获取之后,需要发送发布者的信息。

另一方面,由于view.html继承的是article的模板。而article是需要在在发送的一级目录下存放一个category属性,才能在模板判断显示。

因此需要把data.content.category移到上层数性来。

}).then(function(content){
        //console.log(content);
        data.content=content;
        content.views++;
        content.save();

       return User.find({
            _id:data.content.user
        });

    }).then(function(rs){
        data.content.user=rs[0];
        data.category=data.content.category;
        res.render('main/view',data);
    });

markdown模块的使用

现在的博客内容是混乱无序的。

那就用到最后一个模块——markdown

按照逻辑来说,内容渲染不应该在后端进行。尽管你也可以这么做。但是渲染之后,编辑文章会发生很大的问题。

所以我还是采用熟悉的marked.js,因为它能比较好的兼容hightlight.js的代码高亮。

<script type="text/javascript" src="../../public/js/marked.js"></script>
<script type="text/javascript" src="../../public/js/highlight.pack.js"></script>
<script >hljs.initHighlightingOnLoad();</script>
// ajax方法
success:function(responseData){
           // console.log(responseData);
            var a=responseData.data.content;

            var rendererMD = new marked.Renderer();
            marked.setOptions({
                renderer: rendererMD,
                gfm: true,
                tables: true,
                breaks: false,
                pedantic: false,
                sanitize: false,
                smartLists: true,
                smartypants: false
            });


            marked.setOptions({
                highlight: function (code,a,c) {
                    return hljs.highlightAuto(code).value;
                }
            });
  //后文略...

在通过ajax请求到数据集之后,对内容进行渲染。然后插入到内容中去。

那么模板里的文章内容就不要了。

但是,浏览器自带的html标签样式实在太丑了!在引入样式库吧

highlight.js附带的样式库提供了多种基本的语法高亮设置。

然后你可以参考bootstrap的code部分代码。再改改行距,自适应图片等等。让文章好看些。


十七. 收尾

到目前为止,这个博客就基本实现了。

前端需要一些后端的逻辑,才能对产品有较为深刻的理解。

 posted on 2017-01-26 11:04  葡萄美酒夜光杯  阅读(4450)  评论(25编辑  收藏  举报