• 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写后台,完成了一次前后端的完整交互,最终要的还是做后台的一种思想,一种处理前后台关系的逻辑。

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

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

  • 相关阅读:
    html5页面资源预加载(Link prefetch)
    html5页面资源预加载(Link prefetch)
    纯CSS制作的图形效果
    echarts 较全面的参数设置分析
    设置css样式背景色透明 字体颜色的不透明 设置select 箭头样式
    this.refs['hh']获取dom对象,this.refs['hh'].value获取dom对象的值
    浏览器运行的时候 事件打印不出来,提示 此页面出现代码禁用了反向和正向缓存(由于默认事件导致的)
    react 点击事件以及原始event与react封装好的事件点击区别
    react中 props与forEach的用法
    基于webpack的react的环境项目搭建
  • 原文地址:https://www.cnblogs.com/noobakong/p/9824469.html
Copyright © 2020-2023  润新知