Koa系列-基础功能实现 | 掘金技术征文-双节特别篇

1,850 阅读6分钟

Koa系列-基础功能实现

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

Koa与Express风格类似,不同在于默认异步解决方案和采用洋葱圈模型的中间件。

Koa没有绑定任何中间件,简单的同时也缺失了很多Web程序基础的功能,现在我们实现这些基础的功能: koa架构.png

路由

实现路由功能我们使用到三个中间件,分别是koa-routerkoa-bodykoa-parameter

  1. 其中koa-router来实现最基础的路由功能,将不同的url分发到相应的处理函数中;
  2. koa-body对post请求的参数进行处理,将处理结果解析到ctx.request.body中,koa-body也能够处理上传文件,文件会被解析到ctx.request.files中;
  3. koa-parameter对传参进行校验,get请求会对query进行校验,post请求则对body进行校验,校验方法基于parameter

server/index.js中引用中间件:

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

app.use(require('koa-body')({
  multipart: true,
  formidable: {
      maxFileSize: 200*1024*1024    // 设置上传文件大小最大限制,默认2M
  }
}))

require('koa-parameter')(app)

app.use(require('./api'))

app.listen(3000)

server/api.js中分发请求进行相应的处理:

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

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

api.post('/test', ctx => {
  // 使用koa-parameter对参数进行校验
  ctx.verifyParams({
    name: { type: "string", required: true }
  })
  // koa-body会将参数解析到ctx.request.body
  ctx.body = ctx.request.body
})

api.post('/upload', ctx => {
  // 文件在ctx.request.files中以对象的形式保存,如果多个文件的key相同,则value是一个File对象组成的数组,结构{ key: <File|File[]>value }
  Object.keys(ctx.request.files).forEach(key => {
    const file = ctx.request.files[key]
    const reader = fs.createReadStream(file.path)
    const upStream = fs.createWriteStream(path.join(__dirname, '../dist/' + file.name))

    reader.pipe(upStream)
  })
  ctx.body = 'upload success'
})

module.exports = api.routes()

执行命令node ./server/index.js,运行程序,使用postman访问路由,访问/api/test时,如果没有参数name,状态码为422,提示Validation Failed 20201001155137.jpg

而我们带上参数name,返回结果为我们请求的参数 20201001155221.jpg

使用postman模拟文件上传,调用/api/upload接口,上传成功显示upload success,我们项目中的dist文件夹也多出了上传的文件(dist文件夹需要先创建,不然程序会报错)。 20201001155246.jpg

为了方便我们调试程序,我们使用nodemon启动程序,首先运行yarn run nodemon --dev,然后在package.json中添加命令"dev": "nodemon ./server/index.js",之后我们启动程序只需要运行yarn run dev即可,如果项目进行了修改,程序自动会自动重新运行。

页面与资源

我们使用koa-static来实现静态资源的访问;生成页面一般会使用koa-views+相应的模板引擎的方式来实现,但是我准备使用atr-tempate来生成页面,根据官网的说明我们使用koa-art-template即可:

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

const render = require('koa-art-template')
render(app, {
  root: path.join(__dirname, 'view'),
  extname: '.art',
  debug: process.env.NODE_ENV !== 'production'
})

新建页面和样式文件,并且添加路由:

<!-- server/view/index.art -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{title}}</title>
</head>
<body>
  <div id="app">
    Hello World
  </div>
  <link rel="stylesheet" href="style.css">
</body>
</html>
/* dist/style.css */
#app {
  color: red;
  font-size: 24px;
}
// server/routes.js
const Router = require('koa-router')
const page = new Router()

page.get('/index',async ctx => {
  await ctx.render('index', { title: 'Hello' })
})

module.exports = page.routes()

连接数据库

数据库用于持久化保存数据,服务端的开发往往离不开数据库的使用。MongoDB 是一个基于分布式文件存储的开源数据库系统。在node.js中我们可以通过mongoose操作MongoDB数据库。

首先我们参考菜鸟教程安装好MongoDB数据库,然后为项目添加mongoose

在项目中操作数据库,第一步先连接数据库,新建dbconnect.js文件:

const mongoose = require('mongoose')
const db = require('../config/db')
mongoose.connect(db.dbname, {useNewUrlParser: true, useUnifiedTopology: true}, err => {
    if (err) {
        log.fatal({msg: '[Mongoose] database connect failed!', err})
    } else {
        console.log('[Mongoose] database connect success!')
    }
})
module.exports = mongoose

Mongoose中所有东西都是从SchemaSchema类似于MySQL中的数据结构,Schema约束了MongoDB中每个集合的字段结构,限制程序随意修改数据库;Model是根据Schema定义的结构编译生成的高级构造函数,Model的实例被称为DocumentModel负责从底层MongoDB数据库中创建和读取文档;关于Mongoose的其他概念,还有SchemaModelDocument三者相关的接口可以查看Mongoose官方文档

在项目中新建model/User.js

const { Schema, model } = require('mongoose')

const UserSchema = new Schema({
  username: { type: String, require: true, unique: true },
  password: { type: String, require: true }
})

module.exports = model('User', UserSchema)

在这个文件中,我们定义了Schema,然后生成Model导出使用,要操作数据库,我们只需要使用Model相应的方法即可。

身份验证

Passport是Node.js的身份验证中间件。 Passport极其灵活和模块化,可以毫不费力地放入任何基于Express的Web应用程序中。一套全面的策略支持使用用户名和密码,Facebook,Twitter等进行身份验证。

我们准备使用jwt进行身份验证,暂时不使用第三方授权登录,如果想要了解第三方授权登录或者Passport更多的信息可以阅读官方文档;在项目安装koa-passport和jwt对应的策略passport-jwt,新建auth.js

const keys = require('../config/keys')
const User = require('./model/User')


const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const opts = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: keys.secretOrkey
}

module.exports = passport => {
  passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
    User.findById(jwt_payload._id).then(user => {
      if (user) {
        done(null, user)
      } else {
        done(null, false)
      }
    })
  }))
}

然后在项目中使用:

const passport = require('koa-passport')
require('./auth')(passport)
app.use(passport.initialize())

新建接口,并使用passport验证身份信息:

api.post('/auth', passport.authenticate('jwt', { session: false }), async ctx => {
  ctx.body = 'auth'
})

结果提示Unauthorized,证明身份验证中间件已经生效。

接下来我们来实现用户的注册和登录,完善身份验证的整个流程,注册和登录我们会用到bcryptjsonwebtokenbcrypt用于密码的加密和比较,jsonwebtoken用于生成token;新增login和register接口:

api.post('/login', async ctx => {
  ctx.verifyParams({
    username: { type: "string", required: true },
    password: { type: "string", required: true },
  })
  const { username, password } = ctx.request.body
  const user = await User.findOne({ username })
  if (user && bcrypt.compareSync(password, user.password)) {
    const token = jwt.sign({ _id: user._id, username }, keys.secretOrkey, { expiresIn: 3600 })
    ctx.status = 200
    ctx.body = { token: 'Bearer ' + token }
  } else if (user) {
    ctx.status = 500
    ctx.body = { error: '密码错误' }
  } else {
    ctx.status = 500
    ctx.body = { error: '用户名不存在' }
  }
})

api.post('/register', async ctx => {
  const { username, password } = ctx.request.body
  const users = await User.find({ username })
  if (users.length > 0) {
    ctx.status = 500
    ctx.body = { error: '用户名已被占用' }
  } else {
    await User.create({ username, password: bcrypt.hashSync(password, keys.salt) }).then(user => {
      ctx.body = user
    }).catch(err => {
      ctx.status = 500
      ctx.body = { error: err }
    })
  }
})

调用这两个接口获取用户登录的token,再次访问auth接口,状态码200,正常返回访问信息。 20201004163733.jpg

日志

log4js是Node.js的日志工具,它提供丰富的日志功能,详细的功能的使用可以查看log4js 完全讲解官方文档,我们现在只实现最简单的路由访问日志打印功能:

const log4js = require("log4js");
const logger = log4js.getLogger();
logger.level = "info";

app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  logger.info(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

访问接口控制台会打印简单的访问日志 20201004171433.jpg 至此一个简单koa项目的基础功能我们就实现好了,具体代码实现可以查看项目地址

本文使用 mdnice 排版

🏆 掘金技术征文|双节特别篇