koa框架搭建

主要是学习了慕课网从0到1打造超好用Web框架 一步到位 掌握KOA2服务端开发的一个总结和复盘。
原生的koa2还是不太好用,需要增加插件和中间件才能变得好用,合适我们。所以主要的总结包括几点:
1.整体框架的搭建
2.路由系统的改造
3.异常处理
4.基于LinValidator实现的校验器
5.Sequelize的基本使用规则
6.JWT令牌与Auth权限控制中间件
7.小程序中携带令牌

1.整体框架的搭建

1)首先就是要安装koa框架,安装完之后新建一个app.js的文件
const Koa = require('koa')
const app = new Koa()

app.listen(3000, () => {
  console.log('listening on port 3000')
})

这样就算是能启动一个项目了。

2)由于原生koa获取参数比较困难,所以还需要装koa-bodyparser。
const bodyparser = require('koa-bodyparser')
app.use(bodyparser())
3)假如需要使用到外部访问服务器里面的静态资源,还可以使用koa-static

koa-static指定了目录之后,在访问路径后面加上文件夹里面的路径和文件就可以访问了。
例如:服务器地址为http://localhost:3000 而资源是static/image/001.jpg 那么就可以通过http://localhost:3000/image/001.jpg 来获取资源

const static = require('koa-static')
const path = require('path')
app.use(static(path.join(__dirname, '/static')))

这样在app.js里面就基本完成了,初始化代码一般不写在这里,假如将所有的代码都写在这里,就会变得十分混乱,不好管理,所以需要创建多个文件,进行归类。

4)在根目录下创建core文件夹,并且在里面创建一个init.js的文件作为初始文件。

我们可以通过在app.js中将koa的实例化对象app传入该类里面,然后在里面写核心代码,大部分的代码都是通过app.use,中间件的形式来实现的。
init文件里面主要做的就是注册路由,将通用的方法和配置放在global里面,global为nodejs里面的全局对象。通过将配置和通用方法放在global里面,就可以随时调用这些配置和方法了。而一般要放的就是配置和抛出异常的函数。

init文件:

class InitManager {
  static InitCore(app) {
    InitManager.app = app
    InitManager.loadConfig()
  }
  static loadConfig(path='') {
    const configPath = path || process.cwd() + '/config/config.js'
    const config = require(configPath)
    global.config = config
  }
}
module.exports = InitManager

目前主要是将根目录的config文件夹中的config传入到全局变量里面。
至此,整体框架的搭建就算是基本完成了。

2.路由系统的改造

1)对于koa的路由,最重要的就是安装koa-router

安装完之后可以新建一个app/api的文件夹,在里面将要放上路由文件。
路由文件需要根据模块来进行归类,一个模块新建一个文件。


koa框架搭建_第1张图片
路由所放路径

文件夹里面的v1代表的是第一个版本,因为在迭代当中,直接更改原先文件的话可能会造成以前的冲突,而且不好更改,所以可以进行复制,然后在里面更改,及时版本报错也可以及时切换。

2)路由的使用

首先引入koa-router
然后创建一个Router的实例,里面可以传入参数,最主要的就是传入前缀,可以在写路径的时候写少一点。

const Router = require('koa-router')
const router = new Router({
   prefix : '/v1/book'
})

router.get('/hot-list', async (ctx, next) => {
  ctx.body = ctx.request.body
})

module.exports = router

这样,一个最基本的路由就算是完成了。
然后只需要在app中利用中间件的形式引入即可

app.use(r.routes())
3)自动引入路由文件

每写入一个路由文件,就需要引入一次,这样子的手动方法比较麻烦,所以我们需要使用require-directory来进行自动加载,获取到文件中的模块之后,只需要使用app.use加载就行了。
因为是按照中间件的方法引入的,所以我们可以写在InitManager中,程序刚开启的时候就导入路由模块。

const requireDirectory = require('require-directory') // 自动加载模块
const Router = require('koa-router')

class InitManager{
  static InitLoadRouters() {
    const requireRouterPath = `${process.cwd()}/app/api` // process.cwd 获取程序根目录
    requireDirectory(module, requireRouterPath, {
      visit: r => {
        if (r instanceof Router) {
          InitManager.app.use(r.routes())
        }
      }
    })
  }
}

requireDirectory接受的第三个参数为选项,而visit相当于导入后的回调函数,所以我们可以在导入之后判断是否属于Router的类,是的话就进行中间件加载路由。
自此路由的改造就算是完成了,能进行自动加载,只需要在app/api的文件夹中写入的文件都能进行加载。

3异常处理与抛出异常

处理异常非常有必要,决定了在什么时候展现什么内容给用户看,而且开发环境和生产环境的抛出异常也略有区别。开发环境假如是系统错误,则需要抛出来。而假如是生产环境当中,则需要只告诉用户有错误,并且记录下错误就行了,并不需要具体告诉客户是什么原因。
这里我们可以利用koa的中间件原理,在一开始的时候就用try catch来进行包裹整个流程,然后在catch中进行异常的处理即可。

1)创建异常中间件

首先我们需要新建一个middleware的文件夹,用来专门存放中间件的,然后再创建一个exception.js,由于中间件都是一个函数,里面接受一个ctx为上下文,next为执行下一步,所以我们只需在里面写入函数,然后导出,最后再用app.use该函数即可。

const catchError = async (ctx, next) => {
  try {
    await next()
  } catch (e) {
      // 进行异常的处理
  }
}

module.exports = catchError

大致的框架就是这样,我们只需要在catch里面编写异常处理即可。
在里面我们需要判断是自定义错误,还是系统错误,判断的已经也很简单,只需要查看抛出的e是否继承自自定义的函数即可。这一步等下再写。先判断是系统错误的时候,假如是系统错误,又分为两类,一类是生产环境,第二类是开发环境。
假如是开发环境,我们只需要再一次抛出错误即可。
而假如是生产环境,我们只需要告诉用户出错了,并记录到日志里即可。
所以首先,我们需要在config的文件里面定义我们所属的环境。

module.exports = {
  env: 'dev'// prod
}

然后再在异常中进行判断,抛出错误。

const catchError = async (ctx, next) => {
  try {
    await next()
  } catch (e) {
    
    if (e属于自定义错误) {
      /
    } else {
      // 原生未知错误
      if (global.config.env === 'dev') {
        throw e
      }
      ctx.body = {
        msg: e.message,
        error_code: 999,
        request: `${ctx.method} ${ctx.path}`
      }
      ctx.status = 500
    }
  }
}
2)自定义错误

很多时候,我们需要自定义错误,不能所有都依靠直接抛出错误,这样不好对错误进行归类。所以要进行自定义错误的编写。而且有些时候是用户操作不当,某些字段没填或者填错了而提交的,也需要抛出自定义错误来告诉他哪里发生错误了。
其实原理就是定义一个类,让他继承Error这个类,那么我们就可以抛出错误了。又因为我们需要里面的关键词和状态码还有关键信息,所以只需要在里面定义一些属性,又因为在try catch中捕捉到的e是自定义错误类的一个实例,那么这个类就会有我们需要的信息,并将这些信息返回给用户即可。

class HttpException extends Error {
  constructor(msg = '未定义错误', errorCode = 10000, code = 400) {
    super()
    this.msg = msg
    this.errorCode = 10000
    this.code = 400
  }
}

这个就是自定义错误的基类了。往后的错误类只需要继承这个类即可。然后在try catch中的e,就能获取到e.msg等信息了。
然后我们再来补全一下之前的异常处理代码。

const catchError = async (ctx, next) => {
  try {
    await next()
  } catch (e) {
      
    if (e instanceof HttpException) { // 判断成功的都为自定义错误
      ctx.body = {
        msg: e.msg,
        error_code: e.errorCode,
        request: `${ctx.method} ${ctx.path}`
      }
      ctx.status = e.code
    } else {
      // 原生未知错误
      if (global.config.env === 'dev') {
        throw e
      }
      ctx.body = {
        msg: e.message,
        error_code: 999,
        request: `${ctx.method} ${ctx.path}`
      }
      ctx.status = 500
    }
  }
}

这样就能抛出我们想要的信息了。

3)全局使用异常

因为所有的异常类都在一个文件里面,但我们又想在每个文件都能使用,这时候就需要使用之前说过的global了。然后在一开始导入异常的文件,并将其作为global的属性存在。即可在全局抛出异常了。在init里面运行该方法即可。

   static loadHttpException() {
        const httpException = require('./http-exception')
        global.errors = httpException
  }

4.基于LinValidator实现的校验器

1)前置准备

LinVlalidator是由七月老师所写的校验器,其本身也是基于validator来进行编写的,所以在使用之前需要先安装validator。因为LinValidator里面使用了loadsh,所以也需要安装lodash。然后在源码中下载LinValidator-v2.js,放入和异常同级的目录下。因为里面当验证不成功的时候还需要抛出异常。


koa框架搭建_第2张图片
lin-validator位置
2)编写验证器

当完成上面的步骤之后,就可以使用lin-validator了。首先,我们需要创建一个文件,用来存放各个验证器的。每个验证器都是继承自lin-validator。
然后我们可以在constructor里面定义需要验证的字段。

const { LinValidator, Rule } = require('../../core/lin-validator-v2')

class PositiveIntegerValidator extends LinValidator {
  constructor() {
    super()
    this.id = [new Rule('isInt', '需要是正整数', { min: 1 })]
  }
}

就好比这个验证器,验证的是id,是否为正整数。因为this.id是一个数组,所以可以有多项验证规则。
当数组中存在new Rule('isOptional')的时候,则表示该字段为非必填字段。

this.secret = [
      new Rule('isOptional'),
      new Rule('isLength', '至少6个字符', {
        min: 6,
        max: 128
      })
    ]

还可以添加自定义规则。自定义规则为以validate开头,然后后面写方法名称,写在类的方法里面,写了之后验证器就会执行里面的方法。

class RegisterValidator extends LinValidator {
  constructor(){
    ....
  }
// 自定义规则需要以validate开头,参数为所有值
  validatePassword(vals) {
    const pwd1 = vals.body.password1
    const pwd2 = vals.body.password2
    if (pwd1 !== pwd2) {
      throw new Error('两个密码不相等')
    }
  }
}

new Rule()里面的选项具体可以参考validator使用
这样,一个验证器就算完成了。接下来就是在路由里面的使用了。

3)路由中的使用

使用则比较简单了,只需要在router中添加,并且传入ctx即可。

const v = await new PositiveIntegerValidator().validate(ctx)

注:这里加await是因为lin-validator-v2需要使用同步的方法才行,因为在自定义方法中,会出现查询数据库等获取信息,判断是否存在的情况,属于异步操作,所以需要加上await才能保证查询完成。
lin-validator还支持传入别名的方法,比如虽然在验证器里面写的是this.id = xxx ,但假如要book_id也使用这个方法。就需要使用别名了。使用方法就是在validate里面传入第二个参数。第二个参数为接受一个对象,需要更改哪个字段,字段就作为key,更改后的名字作为value即可。

const v = await new PositiveIntegerValidator().validate(ctx,{id:'book_id'})

当判断完成之后,我们就可以通过v来进行获取参数了。如下所示。

const v = await new PositiveIntegerValidator().validate(ctx)

v.get('path.id')  // 获取:id
v.get('body.id') // 通过post传入的id
v.get('query.id') // 通过在网址的后面添加的?id=xxx

5.Sequelize的基本使用规则

1)安装和配置

首先需要npm install sequelize
然后就是配置了
首先我们在config文件夹下建立一个database.js的文件用于存放数据库的相关代码,由于在线上和生产环境上是不一样的账号密码和数据库地址,所以我们可以根据之前在global中配置的config的env来决定是线上环境还是生产环境

let data_conf = {}

if (global.config.env === 'dev') {
  data_conf = {
    dbName: 'island',
    user: 'root',
    password: '',
    host: 'localhost',
    port: 3306
  }
} else if (global.config.env === 'prod') {
  data_conf = {
    dbName: 'island',
    user: 'root',
    password: '',
    host: 'localhost',
    port: 3306
  }
}
module.exports = {
  data_conf
}

这样配置文件就算是写完了。
然后就是需要在core文件夹中,创建一个db.js
主要是用于通过sequelize连接数据库和对数据库进行一些初始化的配置的。

const { Sequelize} = require('sequelize')
const {
  dbName,
  host,
  port,
  user,
  password
} = require('../config/database').data_conf

const sequelize = new Sequelize(dbName, user, password, {
  dialect: 'mysql',
  host,
  port,
  // logging: true, //执行mysql语句时打印在控制台 默认打印到控制台
  timezone: '+08:00', // 以北京时间为准
  define: {
    // 自定义配置
    timestamps: true, // 创建表时增加create update字段
    paranoid: true, // 创建delete 软删除
    createdAt: 'created_at',
    updatedAt: 'updated_at',
    deletedAt: 'deleted_at',
    underscored: true, // 字段名驼峰转成下划线
    freezeTableName: true,  // 默认false修改表名为复数,true不修改表名,与数据库表名同步 
    scopes: {
      bh: {
        attributes: {
          exclude: ['updatedAt', 'deletedAt', 'createdAt']
        }
      }
    }
  }
})

sequelize.sync({
  force: false // 强制删除数据库再重建
}) // 不加这句不导入数据库

module.exports = {
  sequelize
}

其中里面的scopes类似于一种预定义的代码,在执行语句中,只要写上上面定义好的bh,就可以执行attribute这里的将时间屏蔽进行输出了。

2)创建表

定义了数据库我们就可以进行表的创建了。在根目录新建一个model的文件夹,里面存放的是一张张的表,一张表代表一个文件。然后再在里面进行表的创建。
以user表为例

const bcyrpt = require('bcrypt')
const { Sequelize, Model } = require('sequelize')
const { sequelize } = require('../../core/db')
class User extends Model {
  static async verifyEmailPassword(email, plainPassword) {
    ........
  }
  static async getUserByOpenId(openid) {
     .......
  }
  static async registerByOpenId(openid) {
     .......
  }
}

// 初始化数据库表
User.init(
  {
    nickname: Sequelize.STRING,
    email: {
      type: Sequelize.STRING(128),
      unique: true
    },
    password: {
      type: Sequelize.STRING,
      set(val) {
        // model内置的方法
        const salt = bcyrpt.genSaltSync(10) // 生成盐
        const psw = bcyrpt.hashSync(val, salt) // 生成密码串
        this.setDataValue('password', psw)
      }
    },
    openid: {
      type: Sequelize.STRING(64),
      unique: true
    }
  },
  { sequelize, tableName: 'user' }
)

module.exports = {
  User
}

我们需要新建一个类来继承sequelize里面的Model,然后再用静态方法init来定义表里面的信息,而默认id不用定义,生成表的时候会自动附带上,而字段的类型则需要使用Sequelize.STRING来进行定义。还有各种字段可以到官网上进行查看。
在这里需要说的是,定义字段的时候可以接受一个set的属性,那么在插入密码的时候,会走这段代码,不用写在新建用户的代码里面,这样就显得比较简洁了。设置的时候需要使用this.setDataValue()才行。

3)查询数据库

创建表了之后,导出的这个继承自Model的对象就具备了查询该表数据库的功能,主要用到的有
(1)findOne
(2)findAll
(3)count 计算数量
(4)create

基本上就这几个,里面接受的第一个参数为查询条件

const { Sequelize, Model, Op } = require('sequelize')

await Favor.findAll({
      where: {
        uid,
        type: {
          [Op.not]: 400
        }
      }
    })

这里的Op为条件符号,需要引入sequelize里面的op才行。有Op.not,Op.In (表示一个范围)
假如要使用事务,则需要这样写。

sequelize.transaction(async t => {
      await Favor.create(
        {
          art_id,
          uid,
          type
        },
        { transaction: t }
      )
      const art = await Art.getData(art_id, type)
      await art.increment('fav_nums', { by: 1, transaction: t })
 })

在事务里面进行操作,并在每次操作中带上事务t
查询到的数据还可以使用increment,decrement等对某一字段进行增加或者减少的操作。
自此,有关node使用数据库的方面就算基本说完了。

6.JWT令牌与Auth权限控制中间件

由于是要对外开放接口,但不是所有人都能访问的,这时候就需要颁发令牌,然后让用户携带领令牌才能进行访问。而权限这块在课程里面只是做了个数字类型的,令牌中的数字小于需要访问的接口的数字,则不给访问。
JWT令牌比较简单,是一个无状态的令牌,不需要将信息存储于服务器内。主要就是使用jwt内置的方法,加上secretkey和时间即可。在解密的时候使用secretkey来破译获取存储的信息即可。

1)颁发令牌
const jwt = require('jsonwebtoken')

const generateToken = function(uid, scope) {
  const secretKey = global.config.security.secretKey
  const expiresIn = global.config.security.expiresIn
  const token = jwt.sign(
    {
      uid,
      scope
    },
    secretKey,
    {
      expiresIn
    }
  )
  return token
}

函数中,将secretKey和到期时间预先放入config配置文件中。然后使用jwt的sign方法即可得到token,拿到token之后返回给用户,用户将其存放如storage里面即可。

2)解密令牌

只需使用verify将token和secretKey存入里面即可,假如过期会抛出异常,假如正确则会返回存入token里面的信息。

try {
        decode = jwt.verify(userToken.name, global.config.security.secretKey)
      } catch (e) {
        if (e.name === 'TokenExpiredError') {
          errMsg = 'token已过期'
        }
        throw new global.errors.Forbidden(errMsg)
}
3)使用中间件验证token

每个路由当中都可以传入多个中间件,存入之后,就可以依次执行里面的中间件。所以利用这一原理,我们可以在需要验证token的api中加上验证token的中间件,然后下一个参数才是路由里面的具体方法。
同时,由于每个路由可能访问的权限也是不一样的,那么就可以通过存入参数,或者是通过一个实例,在新建实例的时候就将权限的大小存入里面,作为实例的属性而存在,就可以解决权限的问题了。
所以首先在middleware里面新建一个验证的类

const basicAuth = require('basic-auth')
const jwt = require('jsonwebtoken')
class Auth {
  constructor(level = 1) {
    this.level = level
    Auth.USER = 8
    Auth.ADMIN = 16
    Auth.SUPER_ADMIN = 32
  }
  get m() {
    return async (ctx, next) => {
      const userToken = basicAuth(ctx.req)
      let errMsg = 'token不合法'
      if (!userToken || !userToken.name) {
        throw new global.errors.Forbidden(errMsg)
      }
      let decode
      try {
        decode = jwt.verify(userToken.name, global.config.security.secretKey)
      } catch (e) {
        if (e.name === 'TokenExpiredError') {
          errMsg = 'token已过期'
        }
        throw new global.errors.Forbidden(errMsg)
      }
      if (decode.scope < this.level) {
        errMsg = '权限不足'
        throw new global.errors.Forbidden(errMsg)
      }
      ctx.auth = {
        uid: decode.uid,
        scope: decode.scope
      }
      await next()
    }
  }
  static verifyToken (token) {
    try{
      jwt.verify(token, global.config.security.secretKey)
      return true
    } catch(e) {
      return false
    }
  }
}

module.exports = { Auth }

basicAuth是用于获取传入的token的。token不是在body里面,而是通过在Authorization里面传入服务器当中的。而basicAuth包括了name和password,而token存放在name里面,所以在name里获取就行了。
需要权限控制的时候,在新建一个Auth()的实例里面传入level即可,默认为1。
然后只需要在需要用到权限的地方新建一个Auth实例,并且传入它的m的方法即可。

router.get('/favors', new Auth().m, async (ctx, next) => {
   // 具体代码
})

7.小程序中携带令牌

上面已经说过如何获取令牌了,现在要说的就是发送令牌,由于上面利用了BasicAuth,在传输token的时候需要在header中设置authorization来进行传输。
主要步骤为:
首先安装base64加密包。js-base64
然后对token进行base64加密,然后放入header中的authorization即可。

import { Base64 } from 'js-base64'
Page({
  onGetBookComment() {
    wx.request({
      url: 'http://localhost:3000/v1/book/1/short_comment',
      method: 'GET',
      header: {
        Authorization: this._encode()
      },
      success: (res) => {
        console.log(res.data)
      }
    })
  },
  // 定义的一个加密的方法
  _encode() {
    const token = wx.getStorageSync('token')
    const base64Token = Base64.encode(token + ':')
    return `Basic ${base64Token}`
  }
})

这样即可传输token到服务器上,需要注意的是,因为Basic Auth有name和password的说法,而我们是在name中获取的token,所以需要在加密的时候用:来进行区分。将token放在前面即可。

至此,nodejs搭建的服务器就基本理清楚了,课程中并没有关于文件的操作,对于nodejs的api用得也不是很多,主要的还是得根据业务需求来进行学习和掌握才行。

你可能感兴趣的:(koa框架搭建)