Node-Blog整套前后端学习记录

Node-Blog

后端使用node写的一个一整套的博客系统

#### 主要功能

  • 登录
  • 注册
  • 发表文章
  • 编辑/删除文章
  • 添加/删除/编辑文章分类
  • 账号的管理
  • 评论功能
  • ...

所用技术

  • node
  • express
  • swig渲染模板
  • body-parser中间件
  • cookies
  • mongod(mongoose) 数据库
  • html css js ajax等

主要页面展示

  • index

  • 详情页

    ?

  • 后台

一、项目初始化

1.1 创建目录

├─models 存放数据库数据模型
├─public 存放静态资源
├─routers 路由文件
├─schemas 数据库Schema表
└─views 静态页面

│ .gitignore github仓库上传忽略文件
│ app.js 主程序入口文件
│ package-lock.json
│ package.json
│ README.md

1.2 装包

使用npm安装项目要使用的包

1.3 创建基本app服务

var express = require('express')
var mongoose = require('mongoose')

var app = express()

// 连接数据库
mongoose.connect('mongodb://localhost/node-blog', { useNewUrlParser: true });

app.listen(3000, function () {
  console.log('http://localhost:3000')
})

二、开发开始

2.1 模板使用 swig

// 定义模板引擎
app.engine('html', swig.renderFile)
// 设置模板文件存放目录
app.set('views', './views')
// 注册模板引擎
app.set('view engine', 'html')

//设置swig页面不缓存
swig.setDefaults({
  allowErrors: false,
  autoescape: true,
  cache: false
})

2.2 静态文件托管

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

知识点1:在 Express 中提供静态文件

为了提供诸如图像、CSS 文件和 JavaScript 文件之类的静态文件,请使用 Express 中的 express.static 内置中间件函数。

app.use(express.static('public'));

这样后 我们就可以访问public文件中的任意目录的任意文件:

http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js

注意: Express 相对于静态目录查找文件,因此静态目录的名称不是此 URL 的一部分

可以多次使用static函数开启多个静态资源入口。

自定义文件目录名称

上面的例子中我们可以访问 http://localhost:3000/js/app.js这个目录 但是如果我想通过http://localhost:3000/static/js/app.js来访问,我们可以使用:

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

来创建虚拟路径前缀(路径并不实际存在于文件系统中)

当然,在项目中一般使用绝对路径来保证代码的可行性:

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

2.3 连接数据库

// 连接数据库
mongoose.connect('mongodb://localhost/node-blog' { useNewUrlParser: true });

mongod会在第一个数据创建的时候新建我们的node-blog数据库,不需要我们手动创建

后面的一个配置项最好加上。不报错的话可不加。

2.4 分模块开发与实现

路由
  • 前台模块 main模块

    • / 首页
    • / 内容页
  • 后台管理模块 admin模块
  • API模块 api模块
// 路由
app.use('/admin', require('./routers/admin'))
app.use('/api', require('./routers/api'))
app.use('/', require('./routers/main'))

知识点2:express.Router的使用

使用 express.Router 类来创建可安装的模块化路由处理程序。Router 实例是完整的中间件和路由系统;因此,常常将其称为“微型应用程序”。

使用express.Router,可以将路由更加模块化

比如:在 routers文件夹下新建 main.js

var express = require('express')
var router = express.Router()
...

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

router.get('/view',(req, res) => {
    ...
}

module.exports = router

末尾使用module.exports = router 将router对象暴露出去

我们将其安装在主应用程序app.js的路径中

...
app.use('/', require('./routers/main'))
...

此时的 ‘/’ 路径请求的就是 main.js中的 ’/‘

/view --> main.js 中的 ‘/view‘

开发顺序

功能模块开发顺序

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

编码顺序

  • Schema 定义存储结构
  • 功能逻辑
  • 页面展示

三、注册 登录 登出

3.1 userSchema创建

新建并编写 schemas/user.js

var mongoose = require('mongoose')

// 用户表结构
module.exports = new mongoose.Schema({
  username: {
    type: String
  },
  password: {
    type: String
  }
})

3.2 创建User model

var mongoose = require('mongoose')
var userSchema = require('../schemas/user')

module.exports = mongoose.model('User', userSchema)

知识点3:mongoose中的 Schema 和 Model

Mongoose 的一切始于 Schema。每个 schema 都会映射到一个 MongoDB collection ,并定义这个collection里的文档的构成

关于schema的官方文档

  • 定义一个schema

      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
        }
      });
  • 创建一个model

    我们要把 schema 转换为一个 Model, 使用 mongoose.model(modelName, schema) 函数:

      var Blog = mongoose.model('Blog', blogSchema);

    Models 是从 Schema 编译来的构造函数。 它们的实例就代表着可以从数据库保存和读取的 documents。 从数据库创建和读取 document 的所有操作都是通过 model 进行的。

    第一个参数是跟 model 对应的集合( collection )名字的 单数 形式。 Mongoose 会自动找到名称是 model 名字 复数形式的 collection 。 对于上例,Blog这个 model 就对应数据库中 blogs 这个 collection。.model() 这个函数是对 schema 做了拷贝(生成了 model)。

    你要确保在调用 .model() 之前把所有需要的东西都加进 schema 里了

    一个model就是创造了一个mongoose实例,我们才能将其操控。

    我的片面理解把Schema和model的关系 想成 构造函数和实例之间的关系

3.3 注册

注册逻辑

  • 表单验证
  • 数据库验证
  • 前台 ajax
  1. 静态页面
  2. 处理 前端ajax注册
        // 注册
        $register.find('.user_register_btn').on('click', function () {
            $.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 (result) {
                    $register.find('.user_err').html(result.message)
    
                    if (!result.code) {
                        setTimeout(() => {
                            $('.j_userTab span')[0].click()
                        }, 1000)
                    }
                }
            })
        })
  3. 后台api路由

    在api.js中编写后台注册相关代码

    /*
    注册:
      注册逻辑
      1. 用户名不能为空
      2. 密码不能为空
      3. 两次密码一致
    
      数据库查询
      1. 用户名是否已经被注册
    */
    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 (password !== repassword ) {
        responseData.code = 3
        responseData.message = '两次密码不一致啊'
        res.json(responseData)
        return
      }
    // -------------------------------
    
    // -------数据库验证验证-----------
      User.findOne({
        username: username
      }).then((userInfo) => {
        if (userInfo) {
          // 数据库中已有用户
          responseData.code = 4
          responseData.message = '用户名有了,去换一个'
          res.json(responseData)
          return
        }
        // 保存用户注册信息
        var user = new User({
          username: username,
          password: password
        })
        return user.save()
      }).then((newUserInfo) => {
        responseData.message = '耶~ 注册成功'
        res.json(responseData)
      })
    // -------------------------------
    
    })

    后台通过简单的验证,将结果通过 res.json 的方式来返还给 前台 ajax 再通过json信息来处理页面展示。

    ?

    知识点4:使用body-parser中间件来处理post请求

    关于express的更多中间件

    使用案例

    var express = require('express')
    var bodyParser = require('body-parser')
    
    var app = express()
    
    // parse application/x-www-form-urlencoded
    app.use(bodyParser.urlencoded({ extended: false }))
    
    // parse application/json
    app.use(bodyParser.json())

    通过以上的配置,我们就可以获取通过 req.body 来获取 post 请求总的参数了

    ...
      var username = req.body.username
      var password = req.body.password
      var repassword = req.body.repassword
    ...

    知识点5: mongoose中数据库的操作

    前段时间总结过一些mongoose的增删查操作笔记:

    ? node中的mongodb和mongoose

    ?

#### 3.4 登录

  1. 前台ajax

    // 登录
        $login.find('.user_login_btn').on('click', function () {
            $.ajax({
                type: 'post',
                url: 'api/user/login',
                data: {
                    username: $login.find('[name="username"]').val(),
                    password: $login.find('[name="password"]').val(),
                },
                dataType: 'json',
                success: function (result) {
                    $login.find('.user_err').html(result.message)
                    // 登录成功
                    if (!result.code) {
                        window.location.reload()
                    }
                }
            })
        })
  2. 后台路由处理及数据库查询
    // 登录逻辑处理
    router.post('/user/login', (req, res) => {
      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((userInfo) => {
        if (!userInfo) {
          responseData.code = 2
          responseData.message = '用户名或密码错啦'
          res.json(responseData)
          return
        }
        // 正确 登录成功
        responseData.message = '耶~ 登录成功'
        responseData.userInfo = {
          _id: userInfo._id,
          username: userInfo.username
        }
        req.cookies.set('userInfo', JSON.stringify({
          _id: userInfo._id,
          username: escape(userInfo.username)
        }))
        res.json(responseData)
      })
    })

3.5 cookies

上面的案例中,为了记录我们的登录状态,我们使用了第三发包 -- cookies 来存储登录信息

  1. app 引入 cookies模块

    var Cookies = require('cookies')
  2. 在 api.js 中获取 cookies
    req.cookies.set('userInfo', JSON.stringify({
          _id: userInfo._id,
          username: escape(userInfo.username)
        }))

    ?

  3. 在 app.js 中解析登录用户的cookies
    // 设置cookies
    app.use((req, res, next) => {
      req.cookies = new Cookies(req, res)
    
      // 解析登录用户的cookies
      req.userInfo = {}
      if (req.cookies.get('userInfo')) {
        try {
          req.userInfo = JSON.parse(req.cookies.get('userInfo'))
    
          // 获取用户是否是管理员
          User.findById(req.userInfo._id).then((userInfo) => {
            req.userInfo.isAdmin = Boolean(userInfo.isAdmin)
            next()
          })
        } catch (e) {
          next()
        }
      } else {
        next()
      }
    }
  4. 用 swig 渲染模板控制 index页面

3.6登出

ajax --》 api.js --> cookies设置为空 -> 刷新页面

登出的实现就比较简单,只需将cookies设置为空即可

  1. 前台ajax

        // 登出
        $('#logout').on('click', function () {
            $.ajax({
                url: '/api/user/logout',
                success: function(result) {
                    if (!result.code) {
                        window.location.reload()
                    }
                }
            })
        })
  2. api路由
    // 退出登录
    router.get('/user/logout', (req, res) => {
      req.cookies.set('userInfo', null)
      res.json(responseData)
    })

3.7 中文用户名登录异常

原因 cookies在存储中午时出现乱码
解决办法 将username进行转码再解码

使用 encodedecode 来进 编码和解码

3.8 区分管理员

给userInfo 添加 isAdmin 属性

使用swig 选择渲染

四、后台管理

4.1 bootstrap 模板建立页面

4.2 使用继承模板

公用的继承

{% extends 'layout.html' %}

特殊的重写

{% block main %}
  <div class="jumbotron">
    <h1>Hello, {{userInfo.username}}!</h1>
    <p>欢迎进入后台管理</p>
  </div>
{% endblock%}

admin 首页

// 首页
router.get('/', (req, res, next) => {
  res.render('admin/index', {
    userInfo: req.userInfo
  })
})

4.3 admin/user 用户管理

  • 建立静态 user_index.html
  • 处理路由及分页逻辑
    // 用户管理
    router.get('/user', (req, res) => {
    
      /*
      从数据库中读取所有的用户数据
       limit(number) 限制获取的数据条数
       skip(number) 忽略数据的条数
        每页显示 5 条
        第一页: 1-5  skip:0  -> (当前页 1 - 1) * 每页的条数
        第二页: 6-10 skip:5  -> (当前页 2 - 1) * 每页的条数
        ...
        ...
        User.count() 查询总数据量
       */
      var page = Number(req.query.page || 1)
      var pages = 0
      var limit = 10
    
      User.count().then((count) => {
        // 计算总页数
        pages = Math.ceil(count / limit)
        // 取值不能超过 pages
        page = Math.min(page, pages)
        // 取值不能小于1
        page = Math.max(page, 1)
        var skip = (page - 1) * limit
    
        // 读取数据库中所有用户数据
        User.find().limit(limit).skip(skip).then((users) => {
          res.render('admin/user_index', {
            userInfo: req.userInfo,
            users: users,
            page: page,
            pages: pages,
            count: count,
            limit: limit
          })
        })
      })
    
    })
  • 页面展示 --table表格
  • 分页
    • 数据里 limit
    • skip()
    • 分页原理
        /*
        从数据库中读取所有的用户数据
         limit(number) 限制获取的数据条数
         skip(number) 忽略数据的条数
          每页显示 5 条
          第一页: 1-5  skip:0  -> (当前页 1 - 1) * 每页的条数
          第二页: 6-10 skip:5  -> (当前页 2 - 1) * 每页的条数
          ...
          ...
         */
        var page = req.query.page || 1
        var limit = 5
        var skip = (page - 1) * limit
        User.find().limit(limit).skip(skip).then((users) => {
          res.render('admin/user_index', {
            userInfo: req.userInfo,
            users: users
          })
        })
    • 客户端实现
        <nav aria-label="...">
          <ul class="pager">
            <li class="previous"><a href="/admin/user?page={{page-1}}"><span aria-hidden="true">&larr;</span>上一页</a></li>
            <li>
              一共有 {{count}} 条数据 || 每页显示 {{limit}} 条数据 || 一共 {{pages}} 页 || 当前第 {{page}} 页
            </li>
            <li class="next"><a href="/admin/user?page={{page+1}}">下一页<span aria-hidden="true">&rarr;</span></a></li>
          </ul>
        </nav>
    • 服务端代码
      /*
        从数据库中读取所有的用户数据
         limit(number) 限制获取的数据条数
         skip(number) 忽略数据的条数
          每页显示 5 条
          第一页: 1-5  skip:0  -> (当前页 1 - 1) * 每页的条数
          第二页: 6-10 skip:5  -> (当前页 2 - 1) * 每页的条数
          ...
          ...
          User.count() 查询总数据量
         */
      
        var page = Number(req.query.page || 1)
        var pages = 0
        var limit = 5
      
        User.count().then((count) => {
          // 计算总页数
          pages = Math.ceil(count / limit)
          // 取值不能超过 pages
          page = Math.min(page, pages)
          // 取值不能小于1
          page = Math.max(page, 1)
          var skip = (page - 1) * limit
      
          User.find().limit(limit).skip(skip).then((users) => {
            res.render('admin/user_index', {
              userInfo: req.userInfo,
              users: users,
              page: page,
              pages: pages,
              count: count,
              limit: limit
            })
          })
        })
    • 抽取page 使用 include 语法以后复用

4.4 文章分类相关

  1. 分类首页

    category_index.html

  2. 添加分类

    category_add.html

    • get 渲染页面
    • post 提交页面
    • 设计表结构
      schemas/categories.js
      models/categories.js
    • 相关代码
/*
添加分类页面
 */
router.get('/category/add', (req, res) => {
  res.render('admin/category_add', {
    userInfo: req.userInfo
  })
})

/*
添加分类的保存
 */
router.post('/category/add', (req, res) => {
  var name = req.body.name || ''
  if (name == '') {
    res.render('admin/error', {
      userInfo: req.userInfo,
      message: '名称不能为空'
    })
    return
  }

  // 是否已有分类
  Category.findOne({
    name: name
  }).then((result) => {
    if (result) {
      // 数据库中已经存在
      res.render('admin/error', {
        userInfo: req.userInfo,
        message: '分类已经存在'
      })
      return Promise.reject()
    } else {
      // 数据库中不存在分类
      return new Category({
        name: name
      }).save()
    }
  }).then((newCategory) => {
    res.render('admin/success', {
      userInfo: req.userInfo,
      message: '分类保存成功',
      url: '/admin/category'
    })
  })
})

通过判断 渲染 error 或者 success 的页面 两个页面都在 admin/error.htmladmin/success.html

  1. 首页展示展示

    同用户管理首页展示一样

/*
分类首页
 */
router.get('/category', (req, res) => {
  var page = Number(req.query.page || 1)
  var pages = 0
  var limit = 10

  Category.count().then((count) => {
    // 计算总页数
    pages = Math.ceil(count / limit)
    // 取值不能超过 pages
    page = Math.min(page, pages)
    // 取值不能小于1
    page = Math.max(page, 1)
    var skip = (page - 1) * limit

    Category.find().limit(limit).skip(skip).then((categories) => {
      res.render('admin/category_index', {
        userInfo: req.userInfo,
        categories: categories,
        page: page,
        pages: pages,
        count: count,
        limit: limit
      })
    })
  })
})
  1. 分类修改 删除

    在渲染的分类首页的分类表格中加入

      <td>
        <a href="/admin/category/edit?id={{category._id.toString()}}" class="btn btn btn-primary">修改</a>
        <a href="/admin/category/delete?id={{category._id.toString()}}" class="btn btn-danger">删除</a>
      </td>

通过query的传值分类的id 值 我们来操作id

  • 修改

    get

    /*
     分类修改 get
     */
    router.get('/category/edit', (req, res) => {
      // 获取要修改的分类信息 表单形式展现出来
    
      var id = req.query.id || ''
      // 获取修改的分类信息
      Category.findById(id).then((category) => {
        if (!category) {
          res.render('admin/error', {
            userInfo: req.userInfo,
            message: '分类信息不存在'
          })
          return Promise.reject()
        } else {
          res.render('admin/category_edit', {
            userInfo: req.userInfo,
            category: category
          })
        }
      })
    })

    post

    /*
     分类修改 post
     */
    router.post('/category/edit', (req, res) => {
      var id = req.query.id || ''
      var name = req.body.name || ''
    
      Category.findById(id).then((category) => {
        if (!category) {
          res.render('admin/error', {
            userInfo: req.userInfo,
            message: '分类信息不存在'
          })
          return Promise.reject()
        } else {
          // 当前用户没有做任何修改而提交
          if (name == category.name) {
            res.render('admin/success', {
              userInfo: req.userInfo,
              message: '修改成功',
              url: '/admin/category'
            })
            return Promise.reject()
          } else {
            // 要修改的分类名称是否已经在数据库中
            return Category.findOne({
              // id 不等于当前的id
              _id: {$ne: id},
              name: name
            })
          }
        }
      }).then((sameCategory) => {
        if (sameCategory) {
          res.render('admin/error', {
            userInfo: req.userInfo,
            message: '已存在同名分类'
          })
          return Promise.reject()
        } else {
          return Category.findByIdAndUpdate(id, {
            name: name
          })
        }
      }).then(() => {
        res.render('admin/success', {
          userInfo: req.userInfo,
          message: '修改分类名称成功',
          url: '/admin/category'
        })
      })
    })
  • 删除
    /*
     分类删除
     */
    router.get('/category/delete', (req, res) => {
      // 获取id
      var id = req.query.id || ''
    
      Category.remove({
        _id: id
      }).then(() => {
        res.render('admin/success', {
          userInfo: req.userInfo,
          message: '删除成功',
          url: '/admin/category'
        })
      })
    })

    ?

4.5 内容管理 -内容首页和内容添加

/*
内容首页
 */
router.get('/content', (req, res) => {
  res.render('admin/content_index', {
    userInfo: req.userInfo
  })
})

/*
内容添加
 */
router.get('/content/add', (req, res) => {

  Category.find().sort({_id: -1}).then((categories) => {
    console.log(categories)
    res.render('admin/content_add', {
      userInfo: req.userInfo,
      categories: categories
    })
  })
})

4.6内容提交保存

  • 新建 schemas/content.js 和 models/content.js 建立content模型
  • 处理路由

    post

    后台

       // 保存内容到数据库
       new Content({
         category: req.body.category,
         title: req.body.title,
         description: req.body.description,
         content: req.body.content
       }).save().then((content) => {
         res.render('admin/success', {
           userInfo: req.userInfo,
           message: '内容保存成功',
           url: '/admin/content'
         })
       })
     })

4.7 关于内容分类的表关联关系

module.exports = new mongoose.Schema({
  title: {
    type: String
  },

  // 引用 关联字段
  category: {
    type: mongoose.Schema.Types.ObjectId,
    //引用 另外一张表的模型
    ref: 'Category'
  },

  description: {
    type: String,
    default: ''
  },
  content: {
    type: String,
    default: ''
  }
})

我们在 处理 content 的 category的时候 关联个 另外一个结构表

在渲染页面的时候用mongoose 中提供搞得 populate() 方法

知识点6: mongoose中的表关联

Population 可以自动替换 document 中的指定字段,替换内容从其他 collection 获取。 我们可以填充(populate)单个或多个 document、单个或多个纯对象,甚至是 query 返回的一切对象

简单的说,A表的可以关联B表,通过调用A表的属性数据取到B表内容的值,就像sql的join的聚合操作一样。

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

var personSchema = Schema({
  _id: Schema.Types.ObjectId,
  name: String,
  age: Number,
  stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

var storySchema = Schema({
  author: { type: Schema.Types.ObjectId, ref: 'Person' },
  title: String,
  fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

var Story = mongoose.model('Story', storySchema);
var Person = mongoose.model('Person', personSchema);

我们创建了Story 和 Person两个数据库实例。

Person model 的 stories 字段设为 ObjectId数组。 ref 选项告诉 Mongoose 在填充的时候使用哪个 model,本例中为 Story model。

接下来我们使用 Population 来填充使用

Story.
  findOne({ title: 'Casino Royale' }).
  populate('author').
  exec(function (err, story) {
    if (err) return handleError(err);
    console.log('The author is %s', story.author.name);
    // prints "The author is Ian Fleming"
  });

更多高级用法: Mongoose Populate

4.8 内容修改

/*
 修改内容
  */
router.get('/content/edit', (req, res) => {
  // 获取要修改的内容信息 表单形式展现出来

  var id = req.query.id || ''

  var categories = []
  // 获取分类信息
  Category.find().sort({ _id: -1 })
  .then((result) => {
    categories = result
    return Content.findById(id).populate('category')
  })
  .then((content) => {
    console.log(content)
    if (!content) {
      res.render('admin/error', {
        userInfo: req.userInfo,
        message: '指定内容不存在'
      })
      return Promise.reject()
    } else {
      res.render('admin/content_edit', {
        userInfo: req.userInfo,
        content: content,
        categories: categories
      })
    }
  })

4.9 内容保存

/*
   内容修改
   */
  router.post('/content/edit', function(req, res) {
    var id = req.query.id || ''

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

    if (req.body.description == '' || req.body.content == '') {
      res.render('admin/error', {
        userInfo: req.userInfo,
        message: '简介和内容不能为空'
      })
      return
    }

    Content.findByIdAndUpdate(id, {
      category: req.body.category,
      title: req.body.title,
      description: req.body.description,
      content: req.body.content
    }).then(() => {
      res.render('admin/success', {
        userInfo: req.userInfo,
        message: '内容保存成功',
        url: '/admin/content'
      })
    })

  })

})

4. 10内容删除

/*
内容删除
*/
router.get('/content/delete', (req, res) => {
  // 获取id
  var id = req.query.id || ''

  Content.remove({
    _id: id
  }).then(() => {
    res.render('admin/success', {
      userInfo: req.userInfo,
      message: '删除成功',
      url: '/admin/content'
    })
  })
})

添加一些文章 信息 -- 作者 创建时间 点击量

作者 -- 关联 user表

创建时间 -- new Date()

? 前台渲染

<td>{{content.addTime|date('Y-m-d H:i:s', -8*60)}}</td>

点击量 --》 先默认为 0

五、前台相关

有了后台的数据,我们接下来看前台的

修改 main.js

/*
首页渲染
 */
router.get('/', function (req, res, next) {
  req.userInfo.username = unescape(req.userInfo.username)

  var data = {
    userInfo: req.userInfo,
    categories: [],
    contents: [],
    count: 0,
    page : Number(req.query.page || 1),
    pages : 0,
    limit : 10
  }

  Category.find()
    .then((categories) => {
      data.categories = categories
      return Content.count()
  })
    .then((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)
      var skip = (data.page - 1) * data.limit

      return Content
        .find()
        .sort({ addTime: -1 })
        .limit(data.limit)
        .skip(skip)
        .populate(['category', 'user'])
    })
    .then((contents) => {
      data.contents = contents
      console.log(data)
      res.render('main/index', data)
    })
})

5.1 完善首页细节 改为后台传来的data显示

使用swig的渲染模板 完善页面信息,不在赘述

5.2 设置分页

{% if pages > 1 %}
<nav aria-label="..." id="pager_dh">
  <ul class="pager">
    {% if page <=1 %} <li class="previous"><span href="#"><span aria-hidden="true">&larr;</span>没有上一页了</span></li>
      {%else%}
      <li class="previous"><a href="/?category={{category}}&page={{page-1}}"><span aria-hidden="true">&larr;</span>上一页</a></li>
      {%endif%}
      <span class="page_text">{{page}} / {{pages}}</span>
      {% if page >=pages %}
      <li class="next"><span href="#">没有下一页了<span aria-hidden="true">&rarr;</span></li>
      {%else%}
      <li class="next"><a href="/?category={{category}}&page={{page+1}}">下一页<span aria-hidden="true">&rarr;</span></a></li>
      {%endif%}
  </ul>
</nav>
{%endif%}

5.3 content 创建时间的问题

我们创建addTime的时候,会发现mongod创建的数据的时间戳完全一样

我们不能使用new date()来创建默认时间 使用 Date.now

5.4 处理分类点击跳转

  var where = {}
  if (data.category) {
    where.category = data.category
  }

mongoose查询的时候使用 where 查询

5.5 分类高亮显示

      <nav class="head_nav">
        {% if category == ''%}
        <a href="/" id="inactive">首页</a>
        {%else%}
        <a href="/">首页</a>
        {%endif%}

        {% for cate in categories%}
          {% if category == cate.id%}
          <a href="/?category={{cate.id}}" id="inactive">{{cate.name}}</a>
          {%else%}
          <a href="/?category={{cate.id}}">{{cate.name}}</a>
          {%endif%}
        {% endfor %}
      </nav>

5.6 评论相关

评论使用ajax来操作

使用ajax操作不刷新页面来操作api

后台api代码

/*
进入详情获取评论
 */
router.get('/comment/post', (req, res) => {
  var contentid = req.query.contentid
  Content.findById(contentid)
    .then((content) => {
      responseData.data = content.comments
      res.json(responseData)
    })
})

/*
评论提交
 */
router.post('/comment/post', (req, res) => {
  var contentid = req.body.contentid
  var postData = {
    username: req.userInfo.username,
    postTime: Date.now(),
    content: req.body.content
  }

  // 查询文章内容信息
  Content.findById(contentid)
    .then((content) => {
      content.comments.push(postData)
      return content.save()
    })
    .then((newContent) => {
      responseData.message = '评论成功!'
      responseData.data = newContent
      res.json(responseData)
    })
})

评论代码

ajax的操作都封装在了 routers/api.js 中

评论相关操作我们都放在了js/comments.js 中

var limit = 4
var page = 1
var pages = 0
var comments = []

// 加载所有评论
$.ajax({
  type: 'get',
  url: 'api/comment/post',
  data: {
    contentid: $('#contentId').val(),
  },
  success: ((responseData) => {
    comments = responseData.data
    renderComment()
  })
})

$('.pager').delegate('a', 'click', function() {
  if ($(this).parent().hasClass('previous')) {
    page--
  } else {
    page++
  }
  renderComment()
})

// 提交评论
$('#commentBtn').on('click',function() {
  $.ajax({
    type: 'post',
    url: 'api/comment/post',
    data: {
      contentid: $('#contentId').val(),
      content: $('#commentContent').val()
    },
    success: ((responseData) => {
      $('#commentContent').val('')
      comments = responseData.data.comments
      renderComment(true)
    })
  })
})

function renderComment (toLaster) {
  $('#discuss_count').html(comments.length)

  var $lis = $('.pager li')
  pages = Math.ceil(comments.length / limit)
  if (!toLaster) {
    var start = (page-1) * limit
  } else {
    var start = (pages - 1) * limit
    page = pages
  }
  var end = (start + limit) > comments.length ? comments.length : (start + limit)
  if (pages <= 1) {
    $('.pager').hide()
  } else {
    $('.pager').show()
    $lis.eq(1).html(page + '/' + pages )

    if (page <= 1) {
      page = 1
      $lis.eq(0).html('<span>已是最前一页</span>')
    } else {
      $lis.eq(0).html('<a href="javacript:void(0);">上一页</a>')
    }

    if (page >= pages) {
      page = pages
      $lis.eq(2).html('<span>已是最后一页</span>')
    } else {
      $lis.eq(2).html('<a href="javacript:void(0);">下一页</a>')
    }
  }

  var html = ''
  if (comments.length) {
    for (var i = start; i < end; i++) {
      html += `
        <li>
            <p class="discuss_user"><span>${comments[i].username}</span><i>发表于 ${formatDate(comments[i].postTime)}</i></p>
            <div class="discuss_userMain">
                ${comments[i].content}
            </div>
        </li>
      `
    }
  }

  $('.discuss_list').html(html)
}

function formatDate(d) {
  var date1 = new Date(d)
  return date1.getFullYear() + '年' + (date1.getMonth()+1) + '月' + date1.getDate() + '日' + date1.getHours() + ':' + date1.getMinutes() + ':' + date1.getSeconds()
}

六、总结

项目这个阶段知识简单能跑痛而已,包括细节的优化,和程序的安全性都没有考虑,安全防范措施为零,这也是以后要学习的地方。

第一次使用node写后台,完成了一次前后端的完整交互,最终要的还是做后台的一种思想,一种处理前后台关系的逻辑。

收获了很多,越来越感觉自己要学的东西太多了,自己好菜。。

写总结文档有点累唉 _(°:з」∠)_秃头。

原文地址:https://www.cnblogs.com/noobakong/p/9824469.html

时间: 2024-10-09 18:12:35

Node-Blog整套前后端学习记录的相关文章

从.NET到Node.js谈前后端分离实践(by vczero)

一.最初的[无分离]实践 11年末的时候,用winForm开发程序,拖拖控件,点点按钮,连接数据库,做一些基本的管理系统:Java的JSP还能包揽一切,服务器端拼接模板,顶多使用servlet做一些业务逻辑,做到后端的MVC.那时候,带了一个学校的创新团队,做一些项目,但是基本上是一个人前端后一起搞,现在想想,真是[杂乱无章],后端MVC还好,倒是前端,基本上只能做一些简单效果,施展的空间不大,幸好,也基本能完成项目. 我称之为[无分离]实践,如果是像一个人搞,开发速度倒是挺快的. 前后端无分离

前后端学习日常

2018-2-27 1.前端打包工具webpack的安装与使用(当前版本为4.0.1),应用webpack打包前端资源来解决前端项目的复杂化,将分模块开发的功能进行打包 2.Animate.css - 预设css3动画库,用于前端交互展示动画特效 3.HTML-CSS-JS Prettify - sublime代码格式化(美化)插件 原文地址:https://www.cnblogs.com/sunbey/p/8482363.html

移动端开发者眼中的前端开发流程变迁与前后端分离

写在最开始 移动端与前端的区别 前端开发的混沌时代 后端 MVC MVC 方案实现 MVC 的缺点与改进 前端只写 Demo HTML 模板 后端 MVC 架构总结 AJAX 与前端 MVC 前后端分离的缺点 双端 MVC 不统一 SEO 性能不够 集中 Or 分离 Nodejs 前后端分离的哲学 Nodejs 分层 实战应用 风险控制 总结 参考资料 写在最开始 这是一篇面向移动端开发者的科普性文章,从前端开发的最初流程开始,结合示范代码,讨论开发流程的演变过程,希望能覆盖一部分前端开发技术栈

前后端分离与 restful api

为什么要前后端分离(优点): PC,APP,PAD 多端适应 单页面应用(Single Page Application)SPA开发模式开始流行 前后端开发职责不清 开发效率问题,前后端互相等待 前端一直配合着后端,能力受限 后台开发语言和模板高度耦合,导致开发语言依赖严重 前后端分离缺点: 前后端学习门槛都增加 数据依赖导致文档重要性增加,文档很重要在前后端分离模式中 搜索引擎优化SEO(Search Engine Optimization)的难度增大 后端开发模式迁移成本增加 restful

前后端分离开发与跨域问题

前后端分离 传统开发方式 曾几何时,JSP和Servlet为Java带来了无限风光,一时间大红大紫,但随着互联网的不断发展,这样的开发方式逐渐显露其弊端,在移动互联网炙手可热的今天,应用程序对于后台服务的要求发生了巨大的变化; 传统的项目开发与交互流程: 在传统的web开发中,页面展示的内容以及页面之间的跳转逻辑,全都由后台来控制,这导致了前后端耦合度非常高,耦合度高则意味着,扩展性差,维护性差,等等问题 传统开发的问题如下: 耦合度高 调试麻烦,出现问题时往往需要前后台一起检查 开发效率低,前

[转] 前后端分离开发模式的 mock 平台预研

引入 mock(模拟): 是在项目测试中,对项目外部或不容易获取的对象/接口,用一个虚拟的对象/接口来模拟,以便测试. 背景 前后端分离 前后端仅仅通过异步接口(AJAX/JSONP)来编程 前后端都各自有自己的开发流程,构建工具,测试集合 关注点分离,前后端变得相对独立并松耦合 开发流程 后台编写和维护接口文档,在 API 变化时更新接口文档 后台根据接口文档进行接口开发 前端根据接口文档进行开发 开发完成后联调和提交测试 面临问题 没有统一的文档编写规范,导致文档越来越乱,无法维护和阅读 开

【转】BLE 学习记录

原文网址:http://m.blog.csdn.net/blog/chiooo/43985401 BLE 学习记录 ANROID BLE 开发,基于 bluetoothlegatt 分析 mBluetoothAdapter = mBluetoothManager.getAdapter(); 得到 手机上蓝牙主机的适配器 mBluetoothAdapter public boolean initialize() { // For API level 18 and above, get a refe

从壹开始前后端分离【 .NET Core2.0 Api + Vue 3.0 + AOP + 分布式】框架之九 || 依赖注入IoC学习 + AOP界面编程初探

代码已上传Github,文末有地址 说接上文,上回说到了<从壹开始前后端分离[ .NET Core2.0 Api + Vue 2.0 + AOP + 分布式]框架之八 || API项目整体搭建 6.3 异步泛型+依赖注入初探>,后来的标题中,我把仓储两个字给去掉了,因为好像大家对这个模式很有不同的看法,嗯~可能还是我学艺不精,没有说到其中的好处,现在在学DDD领域驱动设计相关资料,有了好的灵感再给大家分享吧. 到目前为止我们的项目已经有了基本的雏形,后端其实已经可以搭建自己的接口列表了,框架已

java结合node.js非对称加密,实现密文登录传参——让前后端分离的项目更安全

前言   在参考互联网大厂的登录.订单.提现这类对安全性操作要求较高的场景操作时发现,传输的都是密文.而为了目前项目安全,我自己负责的项目也需要这方面的技术.由于,我当前的项目是使用了前后端分离技术,即node.js做前端,spring boot做后端.于是,我开始搜索有关node.js与java实现非对称加密的资料,然而,我却没有得到一个满意的答案.因此,我有了写本篇博客的想法,并希望给用到这类技术的朋友提供帮助. 一.明文密码传输对比 首先. 构建spring boot 2.0项目 引入we