3

前言

你好,这是一篇入门级前后端分离的全栈教程。前端用React构建,后端Node + Koa2 使用Sequelize链接数据库,数据库采用 mysql 虽然是入门级但还是希望你了解过React和Node本篇不会对如react组件、Node.js模块、koa路由、koa中间件等基本内容作详细讲解,由于时间关系ui样式不作为本次学习的重点使用了antd-mobil框架。

项目截图

源码获取

react前端源码

node+koa2后端源码

Node 后端开发

环境安装

  • mysql的版本号: mysql-5.7.27
  • node的版本号 : 10.15.3
  • Navicat 是数据库可视化工具方便我们查看数据库中的表,同学们选择进行安装。
  • Postman 用于调试接口的工具。

Node 初体验

1) 创建项目

新建文件夹 koa-diary 在文件夹下新建 package.json 写入

{
  "name": "koa-diary",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "koa": "^2.7.0",
    "koa-bodyparser": "^4.2.1",
    "koa-router": "^7.4.0",
    "lodash": "^4.17.11",
    "mysql2": "^1.6.5",
    "require-directory": "^2.1.1",
    "sequelize": "^5.6.1",
    "validator": "^10.11.0"
  },
  "scripts": {
    "start": "nodemon --inspect-brk",
    "start:prod": "node app.js"
  },
  "dependencies": {
    "koa2-cors": "^2.0.6",
    "loadsh": "0.0.4"
  }
}

执行 yarn 或者 npm install 安装依赖
这些依赖到用到的时候再去解释先冲就完事了!!!?

2) 第一个 node 程序

我们一般程序主入口会放在根目录下,新建app.js 添加代码:

// app.js
const Koa = require('koa')
const app = new Koa()
app.listen(3000)
console.log('listen 3000')

打开命令行执行 node app.js

如果终端有打印 listen 3000 node 监听了 3000端口,接下来打开浏览器输入 http://localhost:3000/ 如果浏览器返回如图


说明我们的服务器已经成功启动了但为什么返回 Not Found 因为我们的服务确实是什么都没返回啊!!!!

接下来我们写点东西让node返回点东西给前端! app.js 修改后

const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
  ctx.response.body = '<h1>Hello, koa2!</h1>'
})

app.listen(3000)
console.log('listen 3000')

再次访问http://localhost:3000/ 不出意外你会看到

3) 使用 koa-router

打开 app.js 增加几行代码

const Koa = require('koa')
const Router = require('koa-router')
const router = new Router()
const app = new Koa()
router.get('/hello', (ctx, next) => {
  ctx.body = 'hello koa'
})
app.use(router.routes())
app.listen(3000)
console.log('listen 3000')

打开浏览器访问 http://localhost:3000/hello 不出意外如下图所示:

浏览器输出了 hello koa 用 koa-router 写接口要比node原生的更简洁

这难道就是传说中的接口吗??大佬大佬!!

接下来我们就要进行实战了,如果看到这里你还是有点吃力的话建议你去把Node.js以及koa2基础补一下再来食用哦?

4) 使用nodemon监控进程

nodemon是一款监控进程的工具
nodemon可以让我们改变代码后不需要手动重启服务,它会在我们改变代码后自动重启服务。

1 全局安装nodemon
yarn add nodemon -g

2 打开vscode点击左边的爬虫选择创建lunch.json文件,接着选择Node.js环境

3 打开lunch.js文件 把配置改为nodemon 去监听进程

{
  // 使用 IntelliSense 了解相关属性。
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "nodemon",
      "runtimeExecutable": "nodemon",
      "program": "${workspaceFolder}/app.js",
      "restart": true,
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    },
    {
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "program": "${workspaceFolder}/app.js"
    }
  ]
}

4 配置完成后把选项切换为nodemon 点击调试按钮就是那个播放按钮

如果你看到控制台有如下输出,那么nodemon已经运行成功了。

现在你随便改几行代码保存一下就会发现nodemon在自动帮我们重启,偷懒的感觉怎么样。

5) 构建项目目录

一个好的的项目必须要有合理的目录,新建如下图一些文件夹。

接下来会和大家解释这些目录的用处不要捉急!!??

6) 全局动态注册路由

目前我们是采取app.use(router.routes())的方式手动去注册路由。而且这很不优雅但是真实的开发场景路由一般会多达几十个甚至上百个再去手动的方式注册路由就显得有点蠢了,我们鼓励程序员 “偷懒” 这能让我们的代码看上去更简洁

第一步 在app目录下新建api文件夹用于存放所有api
第二步 在api文件夹下新建test.js写入如下代码:

// api/test.js
const Router = require('koa-router')
const router = new Router({
  prefix: '/test'
})
router.get('/', async ctx => {
  ctx.body = '测试'
})
module.exports = router

第三步 在根目录下的core文件夹下新建 init.js 写入如下代码:

// core/init.js
const Router = require('koa-router')
const Directory = require('require-directory')

class InitManager {
  static initCore(app) {
    InitManager.app = app
    InitManager.initLoadRouters()
  }

  static initLoadRouters() {
    function checkRouter(obj) {
      if (obj instanceof Router) {
        InitManager.app.use(obj.routes())
      }
    }
    const path = process.cwd()
    Directory(module, `${path}/app/api`, { visit: checkRouter })
  }
}

module.exports = InitManager

第四步 改造app.js 重启程序

// app.js
const Koa = require('koa')
const InitManager = require('./core/init')
const app = new Koa()
InitManager.initCore(app)
app.listen(3000)
console.log('listen 3000')

打开 http://localhost:3000/test 不出意外的话浏览器显示如下图:

现在我们已经可以完成动态注册路由了,只要我们把路由写在api文件夹下程序就会自动帮我们注册。

7) 全局异常捕获

当我们程序出了故障应该明确地提示给前端用户也方便我们调试代码。我们使用中间件的方式写全局异常捕获,这需要了解node.js著名的洋葱模型,因为不是本节课重点所以不会详细讲解它。如果不懂什么是洋葱模型的话一定要先去了解清楚!!

第一步 在config文件夹下新建 config.js 写入代码:

// config/config.js
module.exports = {
  // prod
  environment: 'dev'
}

第二步 打开core文件夹下的init.js 完整代码如下:

// core/init.js
const Router = require('koa-router')
const Directory = require('require-directory')

class InitManager {
  static initCore(app) {
    InitManager.app = app
    InitManager.initLoadRouters()
    InitManager.initLoadConfig()
  }

  static initLoadRouters() {
    function checkRouter(obj) {
      // console.log('obj', obj)

      if (obj instanceof Router) {
        InitManager.app.use(obj.routes())
      }
    }
    const path = process.cwd()
    Directory(module, `${path}/app/api`, { visit: checkRouter })
  }

  static initLoadConfig() {
    const path = process.cwd() + '/config/config.js'
    global.config = require(path)
  }
}

module.exports = InitManager

第二步 在core下新建http-exception.js 写入代码:

// core/http-exception.js
class HttpException extends Error {
  constructor(msg = '服务器异常', code = 400, errorCode = 10001) {
    super()
    this.msg = msg
    this.code = code
    this.errorCode = errorCode
  }
}

module.exports = {
  HttpException
}

第三步 在根目录下的middlewares文件夹下新建exception.js 写入代码:

// middlewares/exception.js
const { HttpException } = require('../core/http-exception')
const catchError = async (ctx, next) => {
  try {
    await next()
  } catch (error) {
    const isHttpException = error instanceof HttpException
    const isDev = global.config.environment === 'dev'
    if (isDev) {
      if (!isHttpException) {
        throw error
      }
    }

    if (isHttpException) {
      ctx.body = {
        message: error.msg,
        errorCode: error.errorCode,
        requestUrl: `${ctx.method} ${ctx.path}`
      }
      ctx.status = error.code
    } else {
      ctx.body = {
        message: '服务器发生了点问题请稍后再试',
        errorCode: '9999',
        requestUrl: `${ctx.method} ${ctx.path}`
      }
      ctx.status = 500
    }
  }
}

module.exports = catchError

第四步 修改app.js 完整代码如下:

// app.js
const Koa = require('koa')
const bodyParser = require('koa-bodyparser') // 获取Body参数
const InitManager = require('./core/init') //动态注册路由 || 全局挂载config
const catchError = require('./middlewares/exception') // 全局异常
const cors = require('koa2-cors') // 解决跨域

const app = new Koa()

app.use(cors())
app.use(catchError)
app.use(bodyParser())
InitManager.initCore(app)
app.listen(3000)

接下来我们修改下 api文件夹下的test.js测试代码抛出一个服务端异常:

// api/test.js
const Router = require('koa-router')

const router = new Router({
  prefix: '/test'
})

router.get('/', async ctx => {
  throw new Error('抛出异常')
  ctx.body = '测试'
})

module.exports = router

刷新浏览器查看如果浏览器出现 Internal Server Error:

打开控制台查看:

我们已经成功捕获到了非http异常,一般这样的异常是不曝露给用户看到的研发自己知道就可以了。接下来我们用测试代码演示抛出一个http异常

修改 test.js 修改后如下:

// api/test.js
const Router = require('koa-router')
const { HttpException } = require('../../core/http-exception')

const router = new Router({
  prefix: '/test'
})

router.get('/', async ctx => {
  // throw new Error('抛出异常')
  throw new HttpException()
  ctx.body = '测试'
})

module.exports = router

刷新 http://localhost:3000/test 如图:

此时我们已经成功捕获到了异常并成功返回给了前端。

用户注册

前面做了那么多,比如异常处理、全局动态注册路由、都是为了接口做准备的。
1)mysql 配置

打开config文件下的config.js新增mysql配置代码如下:

// config.js
module.exports = {
  // prod
  environment: 'dev',
  database: {
    dbName: 'remind',
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: '123456'
  }
}

我们通过sequelize.js去操作数据库,在core目录下新增文件db.js:

// db.js
const { Sequelize, Model } = require('sequelize')
const { unset, clone, isArray } = require('loadsh')

const {
  dbName,
  host,
  port,
  user,
  password
} = require('../config/config').database
const sequelize = new Sequelize(dbName, user, password, {
  dialect: 'mysql', // 连接数据库
  host,
  port,
  timezone: '+08:00',
  define: {
    logging: true,
    timestamps: true, // 时间字段
    paranoid: true, // 删除字段
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    deletedAt: 'deleted_at',
    underscored: true, // 驼峰转化下划线
    freezeTableName: true
  }
})

sequelize.sync({
  force: false
})

Model.prototype.toJSON = function() {
  let data = clone(this.dataValues)
  unset(data, 'updated_at')
  unset(data, 'deleted_at')

  if (isArray(this.exclude)) {
    this.exclude.forEach(value => {
      unset(data, value)
    })
  }

  return data
}

module.exports = {
  sequelize
}

api目录下新建user.js删除之前用于测试的test.js,新增代码:

// user.js
const Router = require('koa-router')

const router = new Router({
  prefix: '/user'
})
router.post('/register', async ctx => {
  
})
module.exports = router
接着我们给用户注册写 校验器,校验器也是通过中间件的方式去实现的。如果发生异常会直接被我们的全局异常所捕获到

打来core目录的http-exception.js新增ParameterException异常修改如下:

// http-exception.js
class HttpException extends Error {
  constructor(msg = '服务器异常', code = 400, errorCode = 10001) {
    super()
    this.msg = msg
    this.code = code
    this.errorCode = errorCode
  }
}

class ParameterException extends HttpException {
  constructor(msg = '参数错误', errorCode) {
    super()
    this.msg = msg
    this.errorCode = errorCode || 10001
  }
}

module.exports = {
  HttpException,
  ParameterException
}

接下来在app目录下新建文件夹validators存放我们的校验器。在validators下新建validator.js 写入:

// validator.js

const validator = require('validator')
const { ParameterException } = require('../../core/http-exception')

const RegisterValidator = async function(ctx, next) {
  const { email, password1, password2 } = ctx.request.body
  let v = validator.isLength(email, { min: 6, max: 64 })
  if (!v) {
    throw new ParameterException('email长度必须在6~64个字符')
  }
  v = validator.isEmail(email)
  if (!v) {
    throw new ParameterException('email格式错误')
  }
  v = validator.isLength(password1, { min: 6, max: 32 })
  if (!v) {
    throw new ParameterException('密码至少6个字符,最多32个字符')
  }
  v = validator.isLength(password2, { min: 6, max: 32 })
  if (!v) {
    throw new ParameterException('密码至少6个字符,最多32个字符')
  }
  if (password1 !== password2) {
    throw new ParameterException('两个密码必须相同')
  }
  await next()
}

module.exports = {
  RegisterValidator
}

最后我们修改下api目录下的user.js,把我们刚刚编写的校验器用上。修改后的user.js:

// user.js
const Router = require('koa-router')
const { RegisterValidator } = require('../validators/validator')

const router = new Router({
  prefix: '/user'
})

router.post('/register', RegisterValidator, async ctx => {})

module.exports = router

运行nodemon在postman测试一下先输入一些错误的参数:

可以看到校验器可以成功运行,接下来就是编写业务代码往数据库插入数据了。

我们通过模型的方式去操作数据库,在api目录下新建models文件夹,接着在models下新建user.js写入代码:

// user.js
const { Model, DataTypes } = require('sequelize')
const { sequelize } = require('../../core/db')

class User extends Model {}

User.init(
  {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    email: {
      type: DataTypes.STRING,
      unique: true
    },
    nickname: DataTypes.STRING,
    password: DataTypes.STRING
  },
  {
    sequelize,
    tableName: 'user'
  }
)

module.exports = {
  User
}

先把异常类补充完整 http-exception.js 完整代码如下:

// http-exception.js
class HttpException extends Error {
  constructor(msg = '服务器异常', code = 400, errorCode = 10001) {
    super()
    this.msg = msg
    this.code = code
    this.errorCode = errorCode
  }
}

class ParameterException extends HttpException {
  constructor(msg = '参数错误', errorCode) {
    super()
    this.msg = msg
    this.errorCode = errorCode || 10001
  }
}

class Success extends HttpException {
  constructor(msg = 'ok') {
    super()
    this.msg = msg
    this.code = 201
    this.errorCode = 0
  }
}

class NotFound extends HttpException {
  constructor(msg = '资源未找到', errorCode) {
    super()
    this.msg = msg
    this.code = 401
    this.errorCode = errorCode || 10002
  }
}

class EmailRepetition extends HttpException {
  constructor(msg = '用户已存在') {
    super()
    this.msg = msg
    this.code = 402
    this.errorCode = 10003
  }
}

class LikeError extends HttpException {
  constructor(msg = '你已经点过赞了', errorCode) {
    super()
    this.msg = msg
    this.code = 402
    this.errorCode = errorCode || 10004
  }
}

module.exports = {
  HttpException,
  ParameterException,
  Success,
  NotFound,
  EmailRepetition,
  LikeError
}

模型创建好之后打开api目录下的user.js 修改后代码如下:

// user.js
const Router = require('koa-router')
const { RegisterValidator } = require('../validators/validator')
const { EmailRepetition } = require('../../core/http-exception')
const { User } = require('../models/user')

const router = new Router({
  prefix: '/user'
})

router.post('/register', RegisterValidator, async ctx => {
  const { email, nickname, password1 } = ctx.request.body
  const user = await User.findOne({
    where: {
      email
    }
  })
  if (user) {
    throw new EmailRepetition()
  }
  await User.create({ email, nickname, password: password1 })
})

module.exports = router

用postman调用一下:

返回 Not Found 刷新下 user表:

这条记录已经产生我们还需要给客户端一个明确的成功提示

app目录下新建lib文件夹新建帮助函数:

// lib/helper.js
const { Success } = require('../../core/http-exception')
function success(msg) {
  throw new Success(msg)
}

function formatDate(timeObj) {
  const date = new Date(timeObj)
  const y = date.getFullYear()
  const m =
    (date.getMonth() + 1).toString().length === 1
      ? '0' + (date.getMonth() + 1)
      : date.getMonth() + 1
  const d =
    date.getDate().toString().length === 1
      ? '0' + date.getDate()
      : date.getDate()
  return y + '年' + m + '月' + d + '日'
}

module.exports = {
  success,
  formatDate
}

返回两个函数success是这次我们需要的,formatDate我们后面做日期格式化的时候会用到

改写api文件夹下的user.js在操作成功后返回success函数:

// api/user.js
const Router = require('koa-router')
const { RegisterValidator } = require('../validators/validator')
const { EmailRepetition } = require('../../core/http-exception')
const { success } = require('../lib/helper')
const { User } = require('../models/user')

const router = new Router({
  prefix: '/user'
})

router.post('/register', RegisterValidator, async ctx => {
  const { email, nickname, password1 } = ctx.request.body
  const user = await User.findOne({
    where: {
      email
    }
  })
  if (user) {
    throw new EmailRepetition()
  }
  await User.create({ email, nickname, password: password1 })
  success()
})

router.post('/login', async ctx => {
  const { email, password } = ctx.request.body
  const user = await User.validatorEmail(email, password)
  ctx.body = {
    user
  }
})

module.exports = router

接着我们改变点参数通过postman调用一下结果应该能正确返回:

ok 成功地返回出了 message errorCode 和 requestUrl

到目前为止我们终于写好了一个接口不容易啊 ?

用户登录

用户登录除了对用户邮箱和密码的校验,首先查询邮箱是否存在,其次再进行密码的比对。有兴趣的同学可以对密码进行加密更符合真实的开发场景。

我们把对数据库的操作写在模型上,打开models目录下的user.js 代码如下:

新增了两个静态方法,validatorUser验证用户id是否存在 validatorEmail 验证用户邮箱是否存在

// models/user.js
const { Model, DataTypes, Sequelize } = require('sequelize')
const { sequelize } = require('../../core/db')
const { NotFound } = require('../../core/http-exception')

class User extends Model {
  static async validatorUser(id) {
    const user = await User.findOne({
      where: {
        id
      }
    })
    if (!user) {
      throw new NotFound('账号不存在')
    }
    return user
  }

  static async validatorEmail(email, password) {
    const user = await User.findOne({
      where: {
        email
      }
    })
    if (!user) {
      throw new NotFound('账号不存在')
    }
    if (user.password !== password) {
      throw new NotFound('密码错误')
    }
    return user
  }
}

User.init(
  {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    email: {
      type: DataTypes.STRING,
      unique: true
    },
    nickname: DataTypes.STRING,
    password: DataTypes.STRING
  },
  {
    sequelize,
    tableName: 'user'
  }
)

module.exports = {
  User
}

接着打开api目录下的user.js 新增路由login:

这个路由就是调用了User模型上的validatorEmail方法把查询到的User返回给前端。

// api/user.js
const Router = require('koa-router')
const { RegisterValidator } = require('../validators/validator')
const { EmailRepetition } = require('../../core/http-exception')
const { success } = require('../lib/helper')
const { User } = require('../models/user')

const router = new Router({
  prefix: '/user'
})

router.post('/register', RegisterValidator, async ctx => {
  const { email, nickname, password1 } = ctx.request.body
  const user = await User.findOne({
    where: {
      email
    }
  })
  if (user) {
    throw new EmailRepetition()
  }
  await User.create({ email, nickname, password: password1 })
  success()
})

router.post('/login', async ctx => {
  const { email, password } = ctx.request.body
  const user = await User.validatorEmail(email, password)
  ctx.body = {
    user
  }
})

module.exports = router

新增日记

新增日记只需要前端传用户id和content内容,用户昵称我们可以根据用户id去user表里面去读,阅读数量和点赞数都是0可以设置默认值。

1)新建diary模型

打开models下新建diary.js

定义表名和字段需要注意的是设置favor_numslook_nums的默认值为0,content的长度大家可视情况。

// models/diary.js

const { Model, DataTypes } = require('sequelize')
const { sequelize } = require('../../core/db')

class Diary extends Model {}

Diary.init(
  {
    uid: DataTypes.INTEGER,
    nickname: DataTypes.STRING,
    content: DataTypes.STRING(2000),
    create_time: DataTypes.DATE,
    favor_nums: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    look_nums: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    }
  },
  {
    sequelize,
    tableName: 'diary'
  }
)

module.exports = {
  Diary
}

2)编写diary校验器

diary对应的接口就必要多了因此校验器也比较多,增删改查我们都要对参数进行校验。

打开validators目录下的validator.js 完整代码如下:

// validators/validator.js
const validator = require('validator')
const { ParameterException } = require('../../core/http-exception')

const RegisterValidator = async function(ctx, next) {
  const { email, password1, password2 } = ctx.request.body
  let v = validator.isLength(email, { min: 6, max: 64 })
  if (!v) {
    throw new ParameterException('email长度必须在6~64个字符')
  }
  v = validator.isEmail(email)
  if (!v) {
    throw new ParameterException('email格式错误')
  }
  v = validator.isLength(password1, { min: 6, max: 32 })
  if (!v) {
    throw new ParameterException('密码至少6个字符,最多32个字符')
  }
  v = validator.isLength(password2, { min: 6, max: 32 })
  if (!v) {
    throw new ParameterException('密码至少6个字符,最多32个字符')
  }
  if (password1 !== password2) {
    throw new ParameterException('两个密码必须相同')
  }
  await next()
}

const DiaryValidator = async (ctx, next) => {
  const { content } = ctx.request.body
  if (content.length > 1000) {
    throw new ParameterException('内容超过1000字')
  }
  await next()
}

const GetDiaryValidator = async (ctx, next) => {
  const { start, count } = ctx.request.query
  if (!start || !count) {
    await next()
    return
  }
  let v = validator.isInt(start)
  if (!v) {
    throw new ParameterException('start不符合规范')
  }
  v = validator.isInt(count)
  if (!v) {
    throw new ParameterException('count不符合规范')
  }
  await next()
}

const PutDiaryValidator = async (ctx, next) => {
  const { content } = ctx.request.body
  let v = validator.isEmpty(content)
  if (v) {
    throw new ParameterException('content不能为空')
  }
  await next()
}

const PostFavorValidator = async (ctx, next) => {
  const { uid, diary_id } = ctx.request.body
  let v = validator.isInt(uid.toString())
  if (!v) {
    throw new ParameterException('uid 不符合规范')
  }
  v = validator.isInt(diary_id.toString())
  if (!v) {
    throw new ParameterException('diary_id 不符合规范')
  }
  await next()
}

module.exports = {
  RegisterValidator,
  DiaryValidator,
  GetDiaryValidator,
  PutDiaryValidator,
  PostFavorValidator
}

3)编写diary接口

接着在api目录下新建diary.js,新增日记的接口比较简单就是把用户输入的内容插到diary表里去。

// api/diary.js

const Router = require('koa-router')
const { DiaryValidator } = require('../validators/validator')
const { success } = require('../lib/helper')
const { User } = require('../models/user')
const { Diary } = require('../models/diary')

const router = new Router({
  prefix: '/diary'
})

router.post('/', DiaryValidator, async ctx => {
  const { id, content } = ctx.request.body
  const user = await User.validatorUser(id)
  const date = new Date()
  await Diary.create({
    uid: id,
    content,
    nickname: user.nickname,
    create_time: date,
    favor_num: 0
  })
  success()
})

module.exports = router

postman调用一下:

刷新Navicat后新增一条记录

查看日记

查看日记的功能比较简单,就是根据日记的id去diary表里取对应的记录就ok。

需要注意的是我们还有一个阅读次数的功能。所以每查看一次我们就在对应记录下look_nums递增1次。

打开api目录下的diaryl.js新增代码如下:

formatDate函数对输出的时间戳转化为xxxx年xx月xx日格式。

// api/diary.js
const Router = require('koa-router')
const { DiaryValidator } = require('../validators/validator')
const { success } = require('../lib/helper')
const { User } = require('../models/user')
const { Diary } = require('../models/diary')
const { formatDate } = require('../lib/helper')

const router = new Router({
  prefix: '/diary'
})

router.post('/', DiaryValidator, async ctx => {
  const { id, content } = ctx.request.body
  const user = await User.validatorUser(id)
  const date = new Date()
  await Diary.create({
    uid: id,
    content,
    nickname: user.nickname,
    create_time: date,
    favor_num: 0
  })
  success()
})

router.get('/:id', async ctx => {
  const { id } = ctx.params
  const diary = await Diary.findOne({
    where: {
      id: parseInt(id)
    }
  })
  if (!diary) {
    throw new NotFound('文章未找到')
  }
  await diary.increment('look_nums', {
    by: 1
  })
  diary.setDataValue('create_time', formatDate(diary.create_time))
  ctx.body = diary
})

module.exports = router

使用postman调用一下:

ok成功查出日记并且look_nums也增加了1。

点赞日记

我们需要一个表来记录谁对这条日记进行的点赞,并且在日记的列表页面需要把这篇日记的点赞数量统计出来。

1)编写favor模型

打开models目录新建favor.js,并创建静态方法increment
首先查询是否这篇日记已经被点赞了,如果是则抛出异常。如果没有被点赞则往favor表里增加一条记录且为diary表里的favor_nums字段加1,为了保持数据库数据一致性用了数据库事务的方式。

const { Model, DataTypes } = require('sequelize')
const { sequelize } = require('../../core/db')
const { NotFound, LikeError } = require('../../core/http-exception')
const { User } = require('./user')

class Favor extends Model {
  static async increment(uid, diary_id) {
    await User.validatorUser(uid)
    const f = await Favor.findOne({
      where: {
        uid,
        diary_id
      }
    })
    if (f) {
      throw new LikeError()
    }
    return sequelize.transaction(async t => {
      await Favor.create(
        {
          uid,
          diary_id
        },
        { transaction: t }
      )
      const { Diary } = require('./diary')
      const diary = await Diary.findOne({
        where: {
          id: diary_id
        }
      })
      if (!diary) {
        throw new NotFound('日记不存在')
      }
      await diary.increment('favor_nums', { by: 1, transaction: t })
    })
  }
}

Favor.init(
  {
    uid: DataTypes.INTEGER,
    diary_id: DataTypes.INTEGER
  },
  {
    sequelize,
    tableName: 'favor'
  }
)

module.exports = {
  Favor
}

2) 编写点赞接口

老样子在api目录下新建favor.js:

// api/favor.js
const Router = require('koa-router')
const { PostFavorValidator } = require('../validators/validator')
const { success } = require('../lib/helper')
const { Favor } = require('../models/favor')

const router = new Router({
  prefix: '/favor'
})

router.post('/', PostFavorValidator, async ctx => {
  const { uid, diary_id } = ctx.request.body
  await Favor.increment(uid, diary_id)
  success()
})

module.exports = router

postman调试一下:

Navicate 刷新favor表查看:

日记列表

分为两个列表,一个是所有的日记列表,一个是我的日记列表。需要注意的是前端的显示页面需要显示日记的点赞状态,点赞状态从favor表里得到。

我的日记列表

我的日记列表逻辑需要用户id查询,并且从favor表里查出当前用户id的点赞的日记然后取到diary_id,再与从我的日记列表进行对比如果有纪录则说明这条日记被点赞了。

打开models目录下的diary.js新增静态方法getDiary 改变后的代码如下:

const { Model, DataTypes, Op } = require('sequelize')
const { sequelize } = require('../../core/db')
const { NotFound } = require('../../core/http-exception')
const { formatDate } = require('../lib/helper')
const { User } = require('./user')
const { Favor } = require('./favor')

class Diary extends Model {
  static async getDiary(id, start = 0, count = 10) {
    await User.validatorUser(id)
    const favors = await Favor.findAll({
      where: {
        uid: id
      }
    })
    const diaryIds = favors.map(f => {
      return f.diary_id
    })
    const diary = await Diary.findAll({
      order: [['id', 'DESC']],
      where: {
        uid: id
      },
      offset: parseInt(start),
      limit: parseInt(count)
    })
    diary.forEach(item => {
      item.dataValues.create_time = formatDate(item.dataValues.create_time)
      if (diaryIds.includes(item.id)) {
        item.dataValues.isFavor = 1
      } else {
        item.dataValues.isFavor = 0
      }
    })
    return diary
  }
}

Diary.init(
  {
    uid: DataTypes.INTEGER,
    nickname: DataTypes.STRING,
    content: DataTypes.STRING(2000),
    create_time: DataTypes.DATE,
    favor_nums: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    look_nums: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    }
  },
  {
    sequelize,
    tableName: 'diary'
  }
)

module.exports = {
  Diary
}

打开api目录下的diary.js 修改如下:

// api/diary.js
const Router = require('koa-router')
const {
  DiaryValidator,
  GetDiaryValidator
} = require('../validators/validator')
const { NotFound } = require('../../core/http-exception')
const { success } = require('../lib/helper')
const { User } = require('../models/user')
const { Diary } = require('../models/diary')
const { formatDate } = require('../lib/helper')

const router = new Router({
  prefix: '/diary'
})

router.post('/', DiaryValidator, async ctx => {
  const { id, content } = ctx.request.body
  const user = await User.validatorUser(id)
  const date = new Date()
  await Diary.create({
    uid: id,
    content,
    nickname: user.nickname,
    create_time: date,
    favor_num: 0
  })
  success()
})

router.get('/myDiary', GetDiaryValidator, async ctx => {
  const { id, start, count } = ctx.request.query
  const diarys = await Diary.getDiary(id, start, count)
  ctx.body = diarys
})

router.get('/:id', async ctx => {
  xq
  const { id } = ctx.params
  const diary = await Diary.findOne({
    where: {
      id: parseInt(id)
    }
  })
  if (!diary) {
    throw new NotFound('文章未找到')
  }
  await diary.increment('look_nums', {
    by: 1
  })
  diary.setDataValue('create_time', formatDate(diary.create_time))
  ctx.body = diary
})

module.exports = router

postman测试一下:

其中 isFavor 就是点赞状态:0为没点赞1为点赞

广场日记列表

其实这俩日记列表功能比较类似,我们把逻辑比较复杂的写在模型上。

打开models目录下diary.js,新增getAllDiary静态方法 更改后如下:

// models/diary.js

const { Model, DataTypes, Op } = require('sequelize')
const { sequelize } = require('../../core/db')
const { NotFound } = require('../../core/http-exception')
const { formatDate } = require('../lib/helper')
const { User } = require('./user')
const { Favor } = require('./favor')

class Diary extends Model {
  static async getDiary(id, start = 0, count = 10) {
    await User.validatorUser(id)
    const favors = await Favor.findAll({
      where: {
        uid: id
      }
    })
    const diaryIds = favors.map(f => {
      return f.diary_id
    })
    const diary = await Diary.findAll({
      order: [['id', 'DESC']],
      where: {
        uid: id
      },
      offset: parseInt(start),
      limit: parseInt(count)
    })
    diary.forEach(item => {
      item.dataValues.create_time = formatDate(item.dataValues.create_time)
      if (diaryIds.includes(item.id)) {
        item.dataValues.isFavor = 1
      } else {
        item.dataValues.isFavor = 0
      }
    })
    return diary
  }


  static async getAllDiary(start, count, uid) {
    const diary = await Diary.findAll({
      order: [['id', 'DESC']],
      offset: parseInt(start),
      limit: parseInt(count)
    })

    const favors = await Favor.findAll({
      where: {
        uid
      }
    })

    const diaryIds = favors.map(f => {
      return f.diary_id
    })

    diary.forEach(item => {
      item.dataValues.create_time = formatDate(item.dataValues.create_time)
      if (diaryIds.includes(item.id)) {
        item.dataValues.isFavor = 1
      } else {
        item.dataValues.isFavor = 0
      }
    })
    return diary
  }
}

Diary.init(
  {
    uid: DataTypes.INTEGER,
    nickname: DataTypes.STRING,
    content: DataTypes.STRING(2000),
    create_time: DataTypes.DATE,
    favor_nums: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    look_nums: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    }
  },
  {
    sequelize,
    tableName: 'diary'
  }
)

module.exports = {
  Diary
}

打开api目录下diary.js新增查询广场日记的路由:

router.get('/', GetDiaryValidator, async ctx => {
  const { start = 0, count = 10, uid } = ctx.request.query
  const diarys = await Diary.getAllDiary(start, count, uid)
  ctx.body = diarys
})

postman跑一下:

到这里日记列表开发完成了还剩下删除日记更新日记两个功能大家加油!

更新日记

更新日记的逻辑比较简单直接看代码:

打开models目录下diary.js新增 updateDiary方法:

static async updateDiary(uid, id, content) {
await User.validatorUser(uid)
const diary = await Diary.update(
  {
    content
  },
  {
    where: {
      id
    }
  }
)
return diary
}

接着新增接口在api目录下的diary.js新增:

// api/diary.js
router.put('/', PutDiaryValidator, async ctx => {
  const { uid, id, content } = ctx.request.body
  await Diary.updateDiary(uid, id, content)
  success()
})

用postman运行:

更新成功

删除日记

删除日记的逻辑就比较简单了,只要找到对应日记id删除就行。需要注意的是数据库有软删除和硬删除两种 。软删除是不删除记录另外给此条记录增加一个删除日期,硬删除就是去真正的删除这条记录,我们选择硬删除。

创建模型打开models目录下的diary.js增加删除方法:

// models/diary.js

static async deleteDiary(uid, id) {
    await User.validatorUser(uid)
    const diary = await Diary.destroy({
      force: true, // 硬删除
      where: {
        id
      }
    })
    return diary
}

打开api目录下diary.js新增:

router.delete('/', async ctx => {
  const { uid, id } = ctx.request.body
  await Diary.deleteDiary(uid, id)
  success()
})

运行postman调用接口:

刷新Navicate:

删除日记成功!

总结

到这里后端部分已经结束了,我相信如果你认真看完肯定会有收获的。我们简单总结下学到了哪些东西:

  • 全局动态注册路由
  • 使用中间件去处理异常
  • 使用Sequelize操作数据库
  • 使用async语法,同步的方式处理异步
  • 结构化组件化开发项目

最后,感谢您阅读这篇文章,有任何问题或反馈请给我留言。


Link
35 声望2 粉丝