node koa 笔记

Koa 笔记

Koa 旨在为Web应用程序和API提供更小、更丰富和更强大的能力,相对于 express 具有更强的异步处理能力,Koa的核心代码只有1600+行,是一个更加轻量级的框架。

koa 注册的中间件提供了两个参数

  1. ctx:上下文(Context)对象
    • koa并没有像express一样,将req和res分开,而是将它们作为 ctx的属性
    • ctx代表依次请求的上下文对象
    • ctx.request:获取请求对象
    • ctx.response:获取响应对象
  2. next:本质上是一个dispatch,类似于 express 的 next
const Koa = require('koa');

const app = new Koa()

app.use((ctx, next) => {
  console.log('use 1')
  next()
})

app.use((ctx, next) => {
  ctx.response.body = 'test' 
  // ctx.response 已经被 Koa 封装了,没有 end 方法,直接使用 .body 进行返回
  // 可简写为 ctx.body = 'test'
})

// 如果没有返回结果,会自动返回 Not Found
app.listen(8000, () => {
  console.log('启动成功')
})

Koa中间件

  • koa通过创建的app对象,注册中间件只能通过use方法
    • koa通过创建的app对象,注册中间件只能通过use方法
    • 没有 提供请求方法中间件,如 app.get(...)
    • 没有 提供 path 中间件来匹配路径,如 app.use('/path', callback)
    • **没有 **提供连续注册中间件的方式,如 app.use(callback1,callback2)
  • 如何将路径和method分离呢

    • 可以通过,ctx.request.path (.method) 来判断

      app.use((ctx, next) => {
        const req = ctx.request
        if (req.path === '/user') {
          if (req.method === 'POST') {
            ctx.body = 'create user'
          } else {
            ctx.body = 'user list'
          }
        }
      })
      
    • 也可以使用第三方路由中间件

      const Koa = require('koa');
      const app = new Koa()
      // 使用 koa-router 
      const Route = require('koa-router')
      // 创建路由
      const userRouter = new Route({ prefix: '/user' })
      
      // 使用
      userRouter.get('/', (ctx, next) => {
        ctx.body = 'user get'
      })
      
      userRouter.post('/', (ctx, next) => {
        ctx.body = 'user post'
      })
      
      // 导入路由
      app.use(userRoute.routes())
      
      // 使用 allowedMethods 方法
      app.use(userRoute.allowedMethods())
      // 请求 get,那么是正常的请求,因为有实现 get 方法
      // 请求 put、delete、patch,那么就自动报错:Method Not Allowed,状态码:405
      // 请求 link、copy、lock,那么久自动报错:Not Implemented,状态码:501
      

参数解析

params&query
const userRouter = new Route({ prefix: '/user' })

userRouter.get('/:id', (ctx, next) => {
  console.log(ctx.request.params)
  console.log(ctx.request.query)
  ctx.body = 'user get'
})

// /user/123?name=test&age=18

// { id: '123' }
// [Object: null prototype] { name: 'test', age: '18' }
json&x-www-form-urlencoded
// 使用 koa-bodyparser
const bodyparser = require('koa-bodyparser')

const userRouter = new Route({ prefix: '/user' })

app.use(bodyparser())
userRouter.get('/', (ctx, next) => {
  console.log(ctx.request.body) // 通过 ctx.request.body 接收
  ctx.body = 'user get'
})
form-data
// 使用 koa-multer 
// 文件上传
const Koa = require('koa');
const Route = require('koa-router')
const multer = require('koa-multer')

const app = new Koa()

const uploadRouter = new Route({ prefix: '/upload' })

// 上传配置
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploads/') // 保存路径
  },
  filename: (req, file, cb) => {
    cb(null, file.originalname) // 文件名
  }
})

const upload = multer({
  storage
})

// 上传接口
uploadRouter.post('/', upload.single('file'), (ctx, next) => {
  console.log(ctx.req.file)
/**
 * {
      fieldname: 'file',        
      originalname: 'img.jpg',  
      encoding: '7bit',
      mimetype: 'image/jpeg',   
      destination: './uploads/',
      filename: 'img.jpg',      
      path: 'uploads\\img.jpg',
      size: 34462
	}
*/
  ctx.body = 'upload file'
})

app.use(uploadRouter.routes())
app.use(uploadRouter.allowedMethods())

app.listen(8000, () => {
  console.log('启动成功')
})

响应数据类型

response.status 不设置,默认为 200 或 204

body 响应类型
  • 字符串(string)
  • Buffer数据
  • 数据流(stream)
  • 对象
  • 数组
  • null(空)

静态服务器

// 使用 koa-static
const Koa = require('koa')
const staticServer = require('koa-static')

const app = new Koa()

app.use(staticServer('./dist'))

app.listen(8000, () => {
  console.log('启动成功')
})

错误处理

const Koa = require('koa')

const app = new Koa()

app.use((ctx, next) => {
  // 可以通过 ctx.app 获取到 app
  // class Application extends Emitter,app 继承了 Emitter 可以使用订阅/发布的方式,发布报错信息
  ctx.app.emit('error', new Error('test error'), ctx)
})

// 监听 error 事件,返回错误信息
app.on('error', (err, ctx) => {
  console.log(err.message)
  ctx.body = err.message
})

app.listen(8000, () => {
  console.log('启动成功')
})

源码简单解析

const Emitter = require('events');
// Application 继承 Emitter
module.exports = class Application extends Emitter {
  // 初始化配置
  constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    this.middleware = []; // 中间件数组
    this.context = Object.create(context); // 执行上下文
    this.request = Object.create(request); // 请求
    this.response = Object.create(response); // 响应
    // util.inspect.custom support for node 6+
    /* istanbul ignore else */
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
  // 开启监听 
  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback()); // 传入监听回调
    return server.listen(...args);
  }
补充:http.createServer([options][, requestListener])
// 添加中间件
use(fn) {
    // 检查
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
        deprecate('Support for generators will be removed in v3. ' +
                  'See the documentation for examples of how to convert old middleware ' +
                  'https://github.com/koajs/koa/blob/master/docs/migration.md');
        fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 加入中间件数组
    this.middleware.push(fn);
    return this;
}
// 监听回调
callback() {
    // 处理中间件 const compose = require('koa-compose');, fn 的返回值 Promise
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
        const ctx = this.createContext(req, res); // 处理 ctx
        return this.handleRequest(ctx, fn); // 处理请求
    };

    return handleRequest;
}
// 中间件处理函数
function compose (middleware) {
  // 检查
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
  // 处理函数
  return function (context, next) {
    // last called middleware #
    let index = -1 // 闭包,缓存 index
    return dispatch(0) // 从 0 开始调用
    function dispatch (i) {
      // 判断是否已经调用过(index 已经 +1)
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i] // 拿到执行函数
      if (i === middleware.length) fn = next // 指向 next
      if (!fn) return Promise.resolve() // fn 为空,返回 Promise.resolve() 结束回调
      try {
        // 对函数进行 Promise 封装
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); // 回调调用
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
// 处理请求
handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx); // 处理响应函数
    onFinished(res, onerror);
    // 执行中间件,全部中间件执行完,才会调用 handleResponse, 因此执行中多次改变 ctx.body 会取最后一次的值
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// respond
function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' === typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

ctx(context)

context 内部使用了 delegates 对 request & response 的属性和方法进行了代理,因此可以使用 ctx.body 进行响应

const delegate = require('delegates');

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

ctx.request & ctx.response 都是经过 koa 封装的,ctx.req & ctx.res 才是 node 原来的对象

toJSON() {
    return {
        request: this.request.toJSON(),
        response: this.response.toJSON(),
        app: this.app.toJSON(),
        originalUrl: this.originalUrl,
        req: '<original node req>',
        res: '<original node res>',
        socket: '<original node socket>'
    };
},

补充

因为 next 使用了 Promise 封装 koa 可以方便的使用 async/await 进行异步处理,相同代码下 express 则无法做到,由于 express 内部没有进行 Promise 封装

const Koa = require('koa')

const app = new Koa()

app.use(async (ctx, next) => {
  ctx.msg = '111'
  await next()
  ctx.body = ctx.msg
})

app.use(async (ctx, next) => {
  await new Promise((resolve, reject) => {
    try {
      setTimeout(() => {
        ctx.msg += '222'
        resolve()
      }, 1000)
    } catch (e) {
      reject(e)
    }
  })
})

app.listen(8000, () => {
  console.log('启动成功')
})

// 请求 http://localhost:8000 一秒后输出 111222
const express = require('express')

const app = express()

app.use(async (req, res, next) => {
  req.msg = '111'
  await next()
  res.end(req.msg)
})

app.use(async (req, res, next) => {
  await new Promise((resolve, reject) => {
    try {
      setTimeout(() => {
        req.msg += '222'
        resolve()
      }, 1000)
    } catch (e) {
      reject(e)
    }
  })
})

app.listen(8000, () => {
  console.log('启动成功')
})

// 请求 http://localhost:8000 直接输出 111
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值