nodejs+koa实战

145 阅读14分钟

项目初始化

// 初始化项目,生成package.json
npm init

指定入口文件 main.js

项目的基础搭建

创建 src 工作目录

创建 main.js 主入口文件

在 main.js 中引入 koa

const koa = require('koa')
const app = new Koa()

app.listen(3000, () => {
  // 监听成功的回调
  console.log('server is running:http://localhost:3000')
})

node main.js 后即可通过访问 http://localhost:3000,访问到此项目

项目的基本优化

自动重启

// 热更新,只在开发模式下才会用的到
npm install nodemon -D

这时候我们安装的 nodemon 会在 package.json 中的 devDependencies 下

修改 script 选项

"scripts":{
   "dev":"nodemon ./src/main.js"
}

使用 nodemon 启动,开发过程中的改动会自动重启

配置文件

我们开发的过程中还需要区分环境,开发、正式、测试等

// 安装dotenv
npm install dotenv -S

在项目的根目录下创建.env文件

尽可能早的在项目中引入 dotenv 并进行环境变量的配置

const dotenv = require('dotenv')
dotenv.config()
// 经过了上面的配置,我们在.env文件中所配置的环境变量就已经被加载进process.env中了
// 可以将环境变量导出,在需要用到的时候进行引入
module.exports = process.env

这样我们就在项目中配置了环境变量,配置环境变量还有另外一种方式,就是在 package.json 中的 script 中配置执行的命令,并指定环境变量,这样我们就不用新开一个文件在 js 文件中引用了

添加路由

// 这是一个构造函数
const Router = require('koa-router')
const router = new Router({ prefix: '/user' })

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

通过引入 koa 的路由中间件 koa-router,我们可以设置项目的路由,通过在构造函数中传入prefix:'/user'可以设置路由的前缀,以作为不同功能模块的区分

目录结构的划分

我们在 main.js 中引入了 koa 启动了服务

又在 main.js 中引入了 koa-router 设置了项目的路由

但是随着功能的逐渐增多,项目变大,我们不能把所有的东西都写在 main.js 中,我们需要做功能模块的区分

抽离路由

在 src 目录下新建 router 文件夹,这个文件夹专门存放并管理项目中的路由。

  • 如果需要新增 user 的路由,就新建 user.route.js 文件

  • 如果需要新增 order 的路由,就新建 order.route.js 文件

    const Router = require('koa-router')
    const router = new Router({ prefix: '/order' })
    router.post('/add', addOrderController)
    
    module.exports = router
    

新建好了各个功能模块的路由,我们需要一个index.js文件来作为路由的总入口文件,它负责引入各个功能模块的路由

const Router = require('koa-router')
const router = new Router()
const fs = require('fs')

// 需要使用nodejs的fs模块,来进行文件的读取和引入
fs.readdirSync(__dirname).forEach((file) => {
  // 读取当前目录下的文件['user.route.js','order.route.js']
  if (file !== 'index.js') {
    const currentFile = require('./' + file)
    // 注册路由
    router.use(currentFile.routes())
  }
})

module.exports = router

这样我们所有的路由都注册在了index.js中的总路由中,我们只需要在main.js中注册在 app 上,就可以实现路由的功能

const router = require('./router') // 引入index.js可以不用写

app.use(router.routes())
// 这个是路由做的http允许的请求方法处理,如果不写这条语句,那么在使用别的httpMethod请求时,会抛出500的错误,加上了这一句,在请求方法不当的时候,会进行提示
app.use(router.allowedMethods())

抽离 app 服务

我们需要在 src 底下新建一个 app 文件夹专门管理我们的服务,因为有时候我们可能在一个项目中使用多个服务。可能是 express、可能是 koa、也可能是 node 中 http 模块

  • 在 src 下新建 app 目录
  • 在 app 目录下新建一个index.js文件,这个文件用于编写我们现在这个项目中主要用的服务,例如 koa,其余的服务可新开文件编写。
const koa = require('koa')
const app = new Koa()
const router = require('./router')

app.use(router.routes())
app.use(router.allowedMethods())

// 注册号路由之后将app导出
module.exports = app

这样我们将服务抽离出来,在main.js中进行引入的时候,将 app 服务引入并监听即可

const app = require('./app/index.js')

app.listen(3000, () => {
  // 监听成功的回调
  console.log('server is running:http://localhost:3000')
})

这样main.js就变的更加简洁了

抽离 controller

我们在 user.route.js 中写下了这样的代码

router.post('/register', (ctx, next) => {})
// (ctx,next)=>{} 这个是用来处理register逻辑的函数,我们可以把它抽离成一个controller,专门用于处理各个访问的逻辑
  • 在 src 目录下新建一个文件夹 controller

  • 在 controller 文件夹下新建一个文件,叫做 user.controller.js

  • 在 controller 文件夹下新建一个文件,叫做 order.controller.js

class UserController {
  async registerUser() {
    // 这里处理register的逻辑
  }
  async loginUser() {
    // 这里处理login的逻辑
  }
}

module.exports = new UserController()

抽离出来之后在路由文件中引入相应的 controller

抽离 service

我们在 controller 中要进行数据库的操作,我们把操作数据库的这一部分,抽离为 service

  • 在 src 文件夹下创建 service 文件夹
  • 在 service 文件夹下创建 user.service.js
class UserService {
  // 处理创建用户的service
  async createUser() {
    // 这个内部封装了数据库的操作
  }

  // 处理更新用户的service
  async updateUser() {}
}

抽离数据库定义

sequelize这个包专门用于项目中处理关系型数据库的操作,它是基于 promise 的

我们需要借助它来对数据库进行操作

  • npm install sequelize -S先安装
  • 在 src 下新建一个 db 目录用于管理此项目需要连接的数据库
  • 在 db 目录下新建一个 seq.js
const { Sequelize } = require('sequelize')
const { HOST, PORT /*等等需要的配置*/ } = process.env

// 实例化sequelize对象
const seq = new Sequelize(
  '要连接的数据库名称',
  '数据路的用户名',
  '数据库的密码',
  {
    // options
    host: '要连接的数据库的host',
    port: '要连接的数据路的端口',
    dialect: 'mysql', // 要操作的数据库类型
  }
)

// 实例化过后就进行连接
seq
  .authenticate()
  .then((res) => {
    console.log(res, '连接成功的回调')
  })
  .catch((err) => {
    console.log(err, '连接失败的回调')
  })

module.exports = seq

抽离 model

连接好了数据库之后,我们需要定义数据库表,这时候需要抽离一个 model 层,来定义数据库的表结构

  • 在 src 下新建一个 model 目录
  • 在 model 中新建一个 user.model.js,进行如下定义:
const seq = require('../db/seq')

// 创建User表,表名为user,user中有各项字段
const User = seq.define('user', {
    {
      userName:{
       type:DataTypes.String,// DataTypes是Sequelize中为我们提供的类型,需要引入
       allowNull:false, // 是否允许空值,参考Sequelize文档
       unique:true,// 是否允许唯一
       comment:'字段注释',
    },
    {
      password:{
        type:DataTypes.String,
        allowNull:false,
        //...
      }
    }
  }
})

module.exports = User

当我们定义好了 User 的 model 之后,就可以在 user.service.js 中引入并使用它

const User = require('../model/user.model.js')

class UserService {
  // 处理创建用户的service
  async createUser(userName, password) {
    // 这个内部封装了数据库的操作,都是基于promise,需要进行try...catch错误捕获
    const res = User.create({ userName, password })
    return res.dataValues
  }

  // 处理更新用户的service
  async updateUser() {}
}

module.exports = new UserService()

抽离中间件

当我们完成了这一系列的操作之后,就搭建起了一个接口编写的框架。我们可以在每一个 controller 中编写对应的业务处理。但是在我们编写接口的过程中,时常会碰到相同或相似的处理模块,这时候我们为了避免重复冗余的代码,需要把这些相同或相似的功能抽离成中间件。

  • 在 src 下新建一个文件夹叫做 middleware
  • 在 middleware 中新建一个文件叫做 user.middleware.js
const validateUserInfo = (ctx, next) => {
  // 这里可以填充用户登录或注册时的校验方法
}

const comparePassword = (ctx, next) => {
  // 这里可以填充修改密码时,两个密码进行对比的方法
}

module.exports = {
  validateUserInfo,
  comparePassword,
}

抽离错误处理

Sequelize 是基于 promise 的数据库操作工具,我们在进行数据库操作或者日常代码编写的时候要进行错误处理,将错误处理的这一部分抽离出来,也会方便我们排查问题。

  • 在 src 下新建一个 constant 问价夹,专门用来存放代码中需要用到的常量
  • 在 constant 文件夹下新建一个 err.type.js 用来存储返回给前端的错误提示
module.exports = {
  UserValidError = {
    code:'10001',
    message:'用户校验失败',
    result:''
  },
  UserLoginError = {
    code:'10002',
    message:'用户登录失败',
    result:''
  }
}

将错误归集起来了之后,我们只需要在捕获到这个错误的时候使用它

// ctx中提供了当前的app,其中有一个emit的方法,可以传递一个事件,后面为该事件需要的参数
ctx.app.emit('error', UserValidError, ctx)
// 在app中使用on作为接收
app.on('error', errHandler)
// errHandler
module.exports = (error, ctx) => {
  // 这里的error就是UserValidError
  // ctx 就是传递过来的ctx上下文
  ctx.body = error // 把当前错误返回给前端
}

至此我们就完成了项目中的功能拆分,接下来就是在每个模块中填充相应的内容

注册接口的编写

注册的逻辑一般为用户提供用户名密码,传递给后端,后端拿到用户名和密码以后,首先要判断数据库中是否已经存在此用户,如果已经存在了这个用户,就返回提示码并告知前端,此用户已经注册。如果不存在,则对用户传递过来的密码进行加密,然后存储到数据库中。

这里只记录加密接口的步骤

// 使用bcryptjs
const bcrypt = require('bcrypt')

const cryptPassword = async (ctx, next) => {
  const { password } = ctx.request.body
  const salt = bcrypt.genSaltSync(10) // 加盐
  const hash = bcrypt.hashSync(password, salt)
  ctx.request.body.password = hash
  await next()
}

经过此步骤加密之后,我们就可以往下处理注册是逻辑

  • 从 request.body 中取出加密之后的密码
  • 存储用户名和密码至数据库
  • 向用户返回结果

登录接口的编写

登录的逻辑一般为,用户输入用户名和密码进行登录。我们拿取到用户名和密码之后,要和数据库中的用户名和密码进行比较,如果比较失败,则返回用户失败的结果,否则登录成功,成功之后需要下发 token 以及 cookie 等。

这里只记录密码对比和 token 下发的步骤

// 密码对比
bcrypt.compareSync('当前密码', '用户传递过来的密码') // 如果相同返回true,如果不同返回false

// 下发token  需要用到jsonwebtoken这个库
// npm install jsonwebtoken -S
const jwt = require('jsonwebtoken')
// 从数据库中拿取出数据之后,除了密码以外,将其它的信息都用于token下发,也可以用作userInfo返回
const { password, ...res } = await getUserInfo({ userName })
// 那么这个res就是我们下发token需要用到
const token = jwt.sign(res, '自己设置的加密串', {
  expiresIn: '1d' /*token的有效时间*/,
})
ctx.body = {
  code: 200,
  message: '登录成功',
  result: {
    token,
  },
}

这样我们就完成了登录的流程,将 token 下发给用户之后,用户以后的资源请求都需要将 token 携带过来,我们进行验证,如果验证成功,那么可以进行后续的操作,如果验证失败,那么用户就不能获取我们的真实资源。

验证中间件的编写

由于我们下发 token 之后的每一个接口都要通过验证之后才能向下进行,所以我们需要编写一个验证 token 的中间件

  • 在 middleware 这个文件加下创建 auth.middleware.js 文件
const jwt = require('jsonwebtoken')

const auth = (ctx, next) => {
  // 这里编写验证token的相关内容
  const { authorization } = ctx.request.header
  const token = authorization.replace('Bearer ', '')

  try {
    // 如果通过验证,会把我们先前用来生成token的信息返回,这里也就是除了password之外的其他用户信息
    const user = jwt.verify(token, '我们先前设置的加密串')
    ctx.state.user = user // 我们把通过验证的用户信息放入state属性下的user中
  } catch (error) {
    // 如果没有通过验证,那么有几种情况
    // error.name === TokenExpiredError
    // error.name === JsonWebTokenError
    // 详情参考jsonwebtoken这个库的介绍
  }
}

验证的中间件编写完成之后,我们的每一次需要验证 token 的请求,都会使用到它

数据上传

编写接口的同时我们要处理前端传递过来的数据,那么在 koa 中,数据上传需要用到一个中间件,就是koa-body

  • npm install koa-body -S 安装依赖
  • 在路由注册之前先注册koa-body
const KoaBody = require('koa-body')

app.use(
  KoaBody({
    // ...options
  })
)

// 注册路由
app.use(router.routes())

因为经过 koa-body 的处理,前端传递过来的请求数据会挂在ctx.request.body 上,我们在后续的路由处理中,在此处获取并处理即可

koa-body 有很多选项(比如是否支持文件上传等),具体参考手册

静态资源管理

如果想要前端通过浏览器的 uri 访问到本服务的静态资源,那么需要进行静态资源配置

需要使用到koa-static

  • npm install koa-static -S
const koaStatic = require('koa-static')

app.use(koaStatic('静态资源路径,最好借助path模块'))

通过了这样的静态资源配置,前端就可以在浏览器上输入 uri 来访问到本服务的静态资源

sequelize 的基本理解

模型 model 时 sequelize 的本质,是数据库中表的抽象,在 sequelize 中是一个类

比如说,我们要创建一个用户表,那么首先需要定义一个 User 类,这个 User 类就是 sequelize 的模型。表中的每一条数据都是一个对象,每一个对象都是这个类的实例。而我们对 User 类的操作,或者是对实例(表中的每一条数据)的操作,都是类似操作 js 对象一样思想。有了这样的认识,可以帮助我们更好的理解 sequelize 的各项操作。

sequelize 文档

增删改查

做完前面的一些基础工作之后,最常见也是最经常写的就是 CRUD 了

####新增接口

第一步:定义路由,遵守 restfull 规范,定义为router.post('/order','中间件1','中间件2')

第二步:在controller中定义处理该路由的中间件

第三步:在service中定义写入数据库的方法,如果这一步需要用到新的 model,则先在model中定义好数据字段

//需要借助sequelize来进行数据库操作
// 先把User模型给引进来
const User = require('../model/user.model.js')
// 新增操作需要在User表中新增一条数据,从类的角度来说,就是创建一个实例
// 假设我们此时是在/src/service/user.service.js
class UserService {
  // 创建一个用于处理User model的类
  async addUser({ id, userName }) {
    // ID,userName是从controller中解析的
    // 模型中有一个创建的方法
    // 方法一:
    const res = await User.create({ id, userName })
    // 在没有错误的情况下,执行完毕这个操作,就会在user表中新增一条数据
    // 方法二:
    // sequelize的model为我们提供了创建实例的方法build
    const res = User.build({ id, userName })
    // 但是此时的build的方法,仅仅是创建出的一个对象,表示可以将这个对象映射到数据库中的数据,这个对象还并未真正的保存在数据库中,我们应该使用save方法,将其同步
    return res.save()
    // 执行完这一步才算是真正的同步至了数据库中
    // 建议直接使用create方法,具体操作详见sequelize官方文档
  }
}
module.exports = new UserService()

第四步:注意错误捕获与错误处理

修改接口

第一步:定义路由,定义为router.put('/order/:id','中间件','中间件')

第二步:在controller中定义处理该路由的中间件

第三步:在 service 中定义修改数据库的方法

// 修改接口同新增接口
// 假设我们此时在/src/service/user.service.js
class UserService {
// 新增用户的接口
async addUser(){}
// 更新用户的接口
async updateUser({id,userName}){
 // sequelize中为我们提供的更新方法也有两种
 // 方法一:
 const res = await User.update({userName},{
   where:{
     id,
     userName
   }
 })
 // 方法二:
 const res = await User.create({id,userName})
 res.set({
   userName:'xxx'
 })
 return awaut res.save()
}
}

第四步:注意错误捕获与错误处理

删除接口

删除首先要确定是使用硬删除,还是软删除。这二者的区别为硬删除为直接从数据库中的记录抹去,软删除为在数据库中增加一个标识字段,该字段标记了就代表删除了,但不是真正意义上的删除。

第一步:定义路由,定义为router.delete('/order/:id','中间件','中间件','中间件')

第二步:在controller中定义处理该路由的中间件

第三步:在service中定义删除该数据的方法,此时可以选择硬删除,或者是软删除,详见sequelize文档

查询接口

查询接口的思路同上