koa介绍

koa是一个新的web框架,由express幕后的原班人马打造,致力于web应用和api开发领域中的一个更小,更富有表现力的更健壮的基石。
官网:https://koajs.com
github仓库:https://github.com/koajs/koa
一个翻译的中文网:https://koa.bootcss.com

koa的内部原理和express很像,但是语法和内部结构结构进行了升级。

  • koa使用es6编写,它的主要特点是:通过async函数,帮你丢掉了回调函数。
    koa1是基于es2015中的generator生成器函数结合co模块
    koa2完全抛弃了generator和co,升级为es2017中的async/await函数。

正是由于koa内部基于最新的异步处理方式,所以使用koa处理异常更加简单。
koa中的提供了ctx上下文对象
express是扩展了req和res

koa没有捆绑任何中间件,而是提供了一套优雅的方法,帮助您快速的编写服务端的应用程序。

很多开发工具都是基于koa的:1.egg.js. 2.构建工具vite

请添加图片描述

  • koa vs express
  1. koa更好用,能更简单的完成功能。
  2. koa不是完全取代express,每个人爱好不同。
  3. koa2社区不如express。
  4. koa1和koa2在思想上是一致的,但是koa2是基于async/await的实现更漂亮。

koa基本使用

npm init -y
npm install koa

// 使用koa启动一个服务
const Koa = require('koa')

// 创建koa实例
const app = new Koa()

// koa没有路由系统,只有中间件功能
// ctx: content上下文队形
// 响应
// 请求
// app.use不会判断路径和方法
app.use(ctx => {
	ctx.body = 'hello koa' // 发送响应
})

app.listen(3000, () => {
	console.log('http://localhost:3000')
})

nodemon app.js

koa中的context上下文对象

Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。

ctx.req:Node 的 request 对象.
ctx.res: Node 的 response 对象.

ctx.request:koa 的 Request 对象.
ctx.response:koa 的 Response 对象.
请添加图片描述
请添加图片描述

绕过 Koa 的 response 处理是 不被支持的. 应避免使用以下 node 属性:
res.statusCode
res.writeHead()
res.write()
res.end()

Koa中的路由

koa中是没有对路由进行处理,需要自己编码判断。如下:

app.use(ctx => {
	// 自己判断路由
	const path = ctx.path
	if (path === '/') {
		ctx.body = 'hello koa' // 发送响应
	} else if (path === '/bar') {
		ctx.body = 'bar' // 发送响应
	}.....
})

使用开源库:@koa/router

const Router = require('@koa/router')
const router = new Router()

router.get('/', ctx => {})
router.post('/', ctx => {})
router.get('/bar', ctx => {
	ctx.body = 'bar'
})
router.get('/user/:id', ctx => {
	console.log(ctx.params)
	ctx.body = 'user page'
})

// 挂载所有的路由
app
	.use(router.routes())
	.use(router.allowMethods()) // 允许的请求方法

koa中的静态资源托管(例如:图片,字体等等 koa-static)

npm install koa-static

const static = require('koa-static')
const path = require('path')
const mount = require('koa-mount')

// 配置托管的路径
// app.use(static('./public'))

// 配置动态的路径
app.use(static(path.json(__dirname, './public'))

// 配置请求的虚拟路径 koa-mount:npm install koa-mount
app.use(mount('/public', static(path.json(__dirname, './public')))  // 在浏览器中打开的时候,必须以/public/路径,访问。

Koa中的路由重定向(ctx.redirect)

原理:当请求/bar的时候会将status码设置为302,location设置为/foo,告诉客户端重定向到哪里。

重定向:针对的是同步请求,针对异步请求是无效的。

router.get('/bar', ctx => {
	// 重定向为/foo
	ctx.redirect('/foo')
})

Koa中的中间件执行栈结构

koa最重要的一个设计就是中间件。为了理解中间件我们看一下Logger(打印日志)功能的实现。

const main = ctx => {
	console.log(`${Date.now()}`)
	ctx.response.body = 'hello world'
}

中间件栈:洋葱模型。

  • 多个中间件会形成一个栈结构,以先进后出的顺序执行。
  • 最外的中间件首先执行。
  • 调用next函数,把执行权交个下一个中间件。
  • 最内层的中间件最后执行。
  • 执行结束后,把执行权交给上一层的中间件。
  • 最外层的中间件收回执行权之后,执行next函数后面的代码。

中间件栈如下:

const one = (ctx, next) => {
	console.log('>> one')
	next()
	console.log('<< one')
}
const two = (ctx, next) => {
	console.log('>> two')
	next()
	console.log('<< two')
}
const three = (ctx, next) => {
	console.log('>> three')
	next()
	console.log('<< three')
}


app.use(one)
app.use(two)
app.use(three)

// 执行结果:
>> one
>> tow
>> three
<< three
<< two
<< one

Koa中的异步中间件(async/await)

主要使用async/await,但是一定要注意await要跟promise对象,因此需要使用util中的promisify。

const util = require('util')
app.use(async (ctx, next) => {
	const data = await util.promisify(fs.readFile)('./index.html')
	ctx.type = 'html'
	ctx.body = data
	next()
})

Koa中的中间件合并处理(koa-compose)

koa-compose. npm install koa-compose

const compose = require('koa-compose')
const one = (ctx, next) => {
	console.log('>> one')
	next()
	console.log('<< one')
}
const two = (ctx, next) => {
	console.log('>> two')
	next()
	console.log('<< two')
}
const three = (ctx, next) => {
	console.log('>> three')
	next()
	console.log('<< three')
}


// 合并挂载中间件: koa-compose. npm install koa-compose
app.use(compose([one, two, three]))


Koa的中间件异常处理(ctx.throw)

方式1: try catch
方式2: ctx.throw


app.use(async (ctx, next) => {
// 方法1:
	try{
		ctx.body = 'koa'
	}catch(err) {
		ctx.response.status = 500
		ctx.response.body = '服务端内部错误'
	}

// 方法2:
ctx.throw(500) // 服务端内部错误
ctx.throw(404) // 请求资源不存在
})

统一处理多个中间件中的错误:

const util = require('util')
const fs = require('fs')
const readFile = util.promisify(fs.read)

// 这个中间件要放在最顶端
// 在中间件的最外层添加异常捕获的中间件,利用洋葱结构。
app.use(async (ctx, next) => {
	try{
		next()// 捕获同步错误。
		await next() // 捕获异步异常
	}catch(err) {
		ctx.response.status = 500
		ctx.response.body = '服务端内部错误'
	}
})


app.use(async (ctx, next) => {
	const data = await readFile('./package.json')
	ctx.type ='html'
	ctx.body = data
})

Koa中的异常处理

全局监听err时间
app.on(‘error’, () => {})

// 这个中间件要放在最顶端
// 在中间件的最外层添加异常捕获的中间件,利用洋葱结构。
app.use(async (ctx, next) => {
	try{
		// next()// 捕获同步错误。
		await next() // 捕获异步异常
	}catch(err) {
		ctx.response.status = 500
		ctx.response.body = '服务端内部错误'
+	    ctx.app.emit('error', err, ctx)
	}
})



// 这段代码写在哪里都可以。
+ app.on('error', (err) => {
+	console.log('err', err)
+ })

Koa实现原理-源码目录结构

  • application.js
  • context.js
  • request.js
  • response.js

Koa实现原理-基础结构

npm init -y
“main”: “lib/application”

lib/application

const http = require('http')
class Application{
	listen(...args) {
		const server = http.createServer((req, res) => {
			res.end('my koa')
		})
		server.listen(...gsar)
	}
}


koa/app.js

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

app.use('/', (ctx, next) => {
	ctx.body = 'hello koa'
})

app.listen(3000)

Koa实现原理-中间件

lib/application

const http = require('http')
class Application{
	constructor() {
		this.middleware = [] //  保存所有的中间件处理函数
	}
	 
	listen(...args) {
		const server = http.createServer(this.callback())
		server.listen(...gsar)
	}
	// 异步递归遍历调用中间件处理函数
	compose(middleware) {
		return function () {
			const dispatch = index => {
				if(inde>=middleware.length) {
					return Promise.resolve()
				}
				const fn = middleware[index]
				// {}:  上下文对象
				// ()=> dispatch(index+1): next函数
				
				return Promise.resolve(
					// 处理上下文
					fn({}, ()=> dispatch(index+1))
				)
			}
			return dispatch(0) // 返回第一个中间件函数
		}
	}
	// 执行中间件
	callback() {
		const fnMiddleware = this.compose(this.middleware)
		const handleRequest = (req, res) => {
			fnMiddleware()
			.then(() => { // 表示处理流程结束了
				
			}).catch(() => { // 表示处理流程出错了 
			
			})
		}
	}
	// 存储中间件
	use(fn){
		this.middleware.push(fn)
		
	}
}

分析Context对象的内容组成

/**
 * Koa Context
 */


const Koa = require('./koa')

const app = new Koa()

app.use(async (ctx, next) => {
  // Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。
  // 每个请求都将创建一个 Context,并在中间件中作为参数引用
  // console.log(ctx) // Context 对象
  // console.log(ctx.req.url)
  // console.log(ctx.req.method)
  // console.log(ctx.request.req.url)
  // console.log(ctx.request.req.method)

  // console.log(ctx.req) // Node 的 request 对象
  // console.log(ctx.res) // Node 的 response 对象
  // console.log(ctx.req.url)
  
  // console.log(ctx.request) // Koa 中封装的请求对象
  // console.log(ctx.request.header) // 获取请求头对象
  // console.log(ctx.request.method) // 获取请求方法
  // console.log(ctx.request.url) // 获取请求路径
  // console.log(ctx.request.path) // 获取不包含查询字符串的请求路径
  // console.log(ctx.request.query) // 获取请求路径中的查询字符串

  // Request 别名
  // 完整列表参见:https://koa.bootcss.com/#request-
  console.log(ctx.header)
  console.log(ctx.method)
  console.log(ctx.url)
  console.log(ctx.path)
  console.log(ctx.query)


  // Koa 中封装的响应对象
  // console.log(ctx.response)
  // ctx.response.status = 200
  // ctx.response.message = 'Success'
  // ctx.response.type = 'plain'
  // ctx.response.body = 'Hello Koa'
  
  // Response 别名
  // ctx.status = 200
  // ctx.message = 'Success'
  // ctx.type = 'plain'

  ctx.body = 'Hello Koa'
})

app.listen(3000)

初始化Context上下文对象

lib/response

const response = {
  set status (value) {
    this.res.statusCode = value
  }
}

module.exports = response

lib/request

const url = require('url')
const request = {
	// 对象属性访问器
	get method() {
		console.log(this) // 
		return this.req.method
	}
	get header() {
		return this.req.headers
	}
	get url() {
		return this.req.url
	}
	get path() {
		return url.parse(this.req.url).pathname
	}
	get query() {
		return url.parse(this.req.url, true).query
	}
	set header(val) {
		this.req.headers = val
	}
}

module.exports =  request

lib/context

const context = {
	/*
	get method() {
		this.resuest.method
	}
	get url() {
		this.resuest.url
	}
	*/
	
}

function defineProperty(target, name) {
	context.__defineGetter__(name, function () {
    	return this[target][name]
  	})
  	/*
	Object.defindeProperty(context, name, {
		get() {
			return this[target][name]
		}	
	})*/
}

// 测试
defineProperty('request', 'method')
defineProperty('request', 'url')

module.exports =  context

lib/application

const http = require('http')
const { nextTick } = require('process')
const context = require('./context')
const request = require('./request')
const response = require('./response')

class Application {
  constructor() {
    this.middleware = [] // 保存用户添加的中间件函数

    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }

  listen(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }

  use(fn) {
    this.middleware.push(fn)
  }

  // 异步递归遍历调用中间件处理函数
  compose(middleware) {
    return function (context) {
      const dispatch = index => {
        if (index >= middleware.length) return Promise.resolve()
        const fn = middleware[index]
        return Promise.resolve(
          // () => dispatch(index + 1): next函数
          // context: 上下文对象
          fn(context, () => dispatch(index + 1)) // 这是 next 函数
        )
      }

      // 返回第 1 个中间件处理函数
      return dispatch(0)
    }
  }

  // 构造上下文对象
  createContext(req, res) {
    // 一个实例会处理多个请求,而不同的请求应该拥有不同的上下文对象,为了避免请求期间的数据交叉污染,所以这里又对这个数据做了一份儿新的拷贝
    const context = Object.create(this.context)
    const request = (context.request = Object.create(this.request))
    const response = (context.response = Object.create(this.response))

    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
  }

  // 执行中间件
  callback() {
    const fnMiddleware = this.compose(this.middleware)
    const handleRequest = (req, res) => {
      // 每个请求都会创建一个独立的 Context 上下文对象,它们之间不会互相污染
      const context = this.createContext(req, res)
      fnMiddleware(context)
        .then(() => {
          res.end('My Koa')
        })
        .catch(err => {
          res.end(err.message)
        })
    }

    return handleRequest
  }
}

module.exports = Application

koa源码:

lib/aplication.js


'use strict'

/**
 * Module dependencies.
 */

const debug = require('debug')('koa:application')
const onFinished = require('on-finished')
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')
const statuses = require('statuses')
const Emitter = require('events')
const util = require('util')
const Stream = require('stream')
const http = require('http')
const only = require('only')
const { HttpError } = require('http-errors')

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */

module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */

  /**
    *
    * @param {object} [options] Application options
    * @param {string} [options.env='development'] Environment
    * @param {string[]} [options.keys] Signed cookie keys
    * @param {boolean} [options.proxy] Trust proxy headers
    * @param {number} [options.subdomainOffset] Subdomain offset
    * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
    * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
    *
    */

  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
    }
  }

  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */

  listen (...args) {
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }

  /**
   * Return JSON representation.
   * We only bother showing settings.
   *
   * @return {Object}
   * @api public
   */

  toJSON () {
    return only(this, [
      'subdomainOffset',
      'proxy',
      'env'
    ])
  }

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect () {
    return this.toJSON()
  }

  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback () {
    const fn = compose(this.middleware)

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

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  handleRequest (ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */

  createContext (req, res) {
    const context = Object.create(this.context)
    const request = context.request = Object.create(this.request)
    const response = context.response = Object.create(this.response)
    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */

  onerror (err) {
    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err))

    if (err.status === 404 || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`)
  }

  /**
   * Help TS users comply to CommonJS, ESM, bundler mismatch.
   * @see https://github.com/koajs/koa/issues/1513
   */

  static get default () {
    return Application
  }
}

/**
 * Response helper.
 */

function respond (ctx) {
  // allow bypassing koa
  if (ctx.respond === false) 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 (ctx.method === 'HEAD') {
    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 (body == null) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type')
      ctx.response.remove('Transfer-Encoding')
      ctx.length = 0
      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 (typeof body === 'string') 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)
}

/**
 * Make HttpError available to consumers of the library so that consumers don't
 * have a direct dependency upon `http-errors`
 */

module.exports.HttpError = HttpError

lib/context.js


'use strict'

/**
 * Module dependencies.
 */

const util = require('util')
const createError = require('http-errors')
const httpAssert = require('http-assert')
const delegate = require('delegates')
const statuses = require('statuses')
const Cookies = require('cookies')

const COOKIES = Symbol('context#cookies')

/**
 * Context prototype.
 */

const proto = module.exports = {

  /**
   * util.inspect() implementation, which
   * just returns the JSON output.
   *
   * @return {Object}
   * @api public
   */

  inspect () {
    if (this === proto) return this
    return this.toJSON()
  },

  /**
   * Return JSON representation.
   *
   * Here we explicitly invoke .toJSON() on each
   * object, as iteration will otherwise fail due
   * to the getters and cause utilities such as
   * clone() to fail.
   *
   * @return {Object}
   * @api public
   */

  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>'
    }
  },

  /**
   * Similar to .throw(), adds assertion.
   *
   *    this.assert(this.user, 401, 'Please login!');
   *
   * See: https://github.com/jshttp/http-assert
   *
   * @param {Mixed} test
   * @param {Number} status
   * @param {String} message
   * @api public
   */

  assert: httpAssert,

  /**
   * Throw an error with `status` (default 500) and
   * `msg`. Note that these are user-level
   * errors, and the message may be exposed to the client.
   *
   *    this.throw(403)
   *    this.throw(400, 'name required')
   *    this.throw('something exploded')
   *    this.throw(new Error('invalid'))
   *    this.throw(400, new Error('invalid'))
   *
   * See: https://github.com/jshttp/http-errors
   *
   * Note: `status` should only be passed as the first parameter.
   *
   * @param {String|Number|Error} err, msg or status
   * @param {String|Number|Error} [err, msg or status]
   * @param {Object} [props]
   * @api public
   */

  throw (...args) {
    throw createError(...args)
  },

  /**
   * Default error handling.
   *
   * @param {Error} err
   * @api private
   */

  onerror (err) {
    // don't do anything if there is no error.
    // this allows you to pass `this.onerror`
    // to node-style callbacks.
    if (err == null) return

    // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
    // See https://github.com/koajs/koa/issues/1466
    // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err))

    let headerSent = false
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true
    }

    // delegate
    this.app.emit('error', err, this)

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return
    }

    const { res } = this

    // first unset all headers
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name))
    } else {
      res._headers = {} // Node < 7.7
    }

    // then set those specified
    this.set(err.headers)

    // force text/plain
    this.type = 'text'

    let statusCode = err.status || err.statusCode

    // ENOENT support
    if (err.code === 'ENOENT') statusCode = 404

    // default to 500
    if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500

    // respond
    const code = statuses[statusCode]
    const msg = err.expose ? err.message : code
    this.status = err.status = statusCode
    this.length = Buffer.byteLength(msg)
    res.end(msg)
  },

  get cookies () {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      })
    }
    return this[COOKIES]
  },

  set cookies (_cookies) {
    this[COOKIES] = _cookies
  }
}

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */

/* istanbul ignore else */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect
}

/**
 * Response delegation.
 */

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')

/**
 * Request delegation.
 */

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')

lib/request.js


'use strict'

/**
 * Module dependencies.
 */

const URL = require('url').URL
const net = require('net')
const accepts = require('accepts')
const contentType = require('content-type')
const stringify = require('url').format
const parse = require('parseurl')
const qs = require('querystring')
const typeis = require('type-is')
const fresh = require('fresh')
const only = require('only')
const util = require('util')

const IP = Symbol('context#ip')

/**
 * Prototype.
 */

module.exports = {

  /**
   * Return request header.
   *
   * @return {Object}
   * @api public
   */

  get header () {
    return this.req.headers
  },

  /**
   * Set request header.
   *
   * @api public
   */

  set header (val) {
    this.req.headers = val
  },

  /**
   * Return request header, alias as request.header
   *
   * @return {Object}
   * @api public
   */

  get headers () {
    return this.req.headers
  },

  /**
   * Set request header, alias as request.header
   *
   * @api public
   */

  set headers (val) {
    this.req.headers = val
  },

  /**
   * Get request URL.
   *
   * @return {String}
   * @api public
   */

  get url () {
    return this.req.url
  },

  /**
   * Set request URL.
   *
   * @api public
   */

  set url (val) {
    this.req.url = val
  },

  /**
   * Get origin of URL.
   *
   * @return {String}
   * @api public
   */

  get origin () {
    return `${this.protocol}://${this.host}`
  },

  /**
   * Get full request URL.
   *
   * @return {String}
   * @api public
   */

  get href () {
    // support: `GET http://example.com/foo`
    if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl
    return this.origin + this.originalUrl
  },

  /**
   * Get request method.
   *
   * @return {String}
   * @api public
   */

  get method () {
    return this.req.method
  },

  /**
   * Set request method.
   *
   * @param {String} val
   * @api public
   */

  set method (val) {
    this.req.method = val
  },

  /**
   * Get request pathname.
   *
   * @return {String}
   * @api public
   */

  get path () {
    return parse(this.req).pathname
  },

  /**
   * Set pathname, retaining the query string when present.
   *
   * @param {String} path
   * @api public
   */

  set path (path) {
    const url = parse(this.req)
    if (url.pathname === path) return

    url.pathname = path
    url.path = null

    this.url = stringify(url)
  },

  /**
   * Get parsed query string.
   *
   * @return {Object}
   * @api public
   */

  get query () {
    const str = this.querystring
    const c = this._querycache = this._querycache || {}
    return c[str] || (c[str] = qs.parse(str))
  },

  /**
   * Set query string as an object.
   *
   * @param {Object} obj
   * @api public
   */

  set query (obj) {
    this.querystring = qs.stringify(obj)
  },

  /**
   * Get query string.
   *
   * @return {String}
   * @api public
   */

  get querystring () {
    if (!this.req) return ''
    return parse(this.req).query || ''
  },

  /**
   * Set query string.
   *
   * @param {String} str
   * @api public
   */

  set querystring (str) {
    const url = parse(this.req)
    if (url.search === `?${str}`) return

    url.search = str
    url.path = null

    this.url = stringify(url)
  },

  /**
   * Get the search string. Same as the query string
   * except it includes the leading ?.
   *
   * @return {String}
   * @api public
   */

  get search () {
    if (!this.querystring) return ''
    return `?${this.querystring}`
  },

  /**
   * Set the search string. Same as
   * request.querystring= but included for ubiquity.
   *
   * @param {String} str
   * @api public
   */

  set search (str) {
    this.querystring = str
  },

  /**
   * Parse the "Host" header field host
   * and support X-Forwarded-Host when a
   * proxy is enabled.
   *
   * @return {String} hostname:port
   * @api public
   */

  get host () {
    const proxy = this.app.proxy
    let host = proxy && this.get('X-Forwarded-Host')
    if (!host) {
      if (this.req.httpVersionMajor >= 2) host = this.get(':authority')
      if (!host) host = this.get('Host')
    }
    if (!host) return ''
    return host.split(/\s*,\s*/, 1)[0]
  },

  /**
   * Parse the "Host" header field hostname
   * and support X-Forwarded-Host when a
   * proxy is enabled.
   *
   * @return {String} hostname
   * @api public
   */

  get hostname () {
    const host = this.host
    if (!host) return ''
    if (host[0] === '[') return this.URL.hostname || '' // IPv6
    return host.split(':', 1)[0]
  },

  /**
   * Get WHATWG parsed URL.
   * Lazily memoized.
   *
   * @return {URL|Object}
   * @api public
   */

  get URL () {
    /* istanbul ignore else */
    if (!this.memoizedURL) {
      const originalUrl = this.originalUrl || '' // avoid undefined in template string
      try {
        this.memoizedURL = new URL(`${this.origin}${originalUrl}`)
      } catch (err) {
        this.memoizedURL = Object.create(null)
      }
    }
    return this.memoizedURL
  },

  /**
   * Check if the request is fresh, aka
   * Last-Modified and/or the ETag
   * still match.
   *
   * @return {Boolean}
   * @api public
   */

  get fresh () {
    const method = this.method
    const s = this.ctx.status

    // GET or HEAD for weak freshness validation only
    if (method !== 'GET' && method !== 'HEAD') return false

    // 2xx or 304 as per rfc2616 14.26
    if ((s >= 200 && s < 300) || s === 304) {
      return fresh(this.header, this.response.header)
    }

    return false
  },

  /**
   * Check if the request is stale, aka
   * "Last-Modified" and / or the "ETag" for the
   * resource has changed.
   *
   * @return {Boolean}
   * @api public
   */

  get stale () {
    return !this.fresh
  },

  /**
   * Check if the request is idempotent.
   *
   * @return {Boolean}
   * @api public
   */

  get idempotent () {
    const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']
    return !!~methods.indexOf(this.method)
  },

  /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket () {
    return this.req.socket
  },

  /**
   * Get the charset when present or undefined.
   *
   * @return {String}
   * @api public
   */

  get charset () {
    try {
      const { parameters } = contentType.parse(this.req)
      return parameters.charset || ''
    } catch (e) {
      return ''
    }
  },

  /**
   * Return parsed Content-Length when present.
   *
   * @return {Number}
   * @api public
   */

  get length () {
    const len = this.get('Content-Length')
    if (len === '') return
    return ~~len
  },

  /**
   * Return the protocol string "http" or "https"
   * when requested with TLS. When the proxy setting
   * is enabled the "X-Forwarded-Proto" header
   * field will be trusted. If you're running behind
   * a reverse proxy that supplies https for you this
   * may be enabled.
   *
   * @return {String}
   * @api public
   */

  get protocol () {
    if (this.socket.encrypted) return 'https'
    if (!this.app.proxy) return 'http'
    const proto = this.get('X-Forwarded-Proto')
    return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http'
  },

  /**
   * Shorthand for:
   *
   *    this.protocol == 'https'
   *
   * @return {Boolean}
   * @api public
   */

  get secure () {
    return this.protocol === 'https'
  },

  /**
   * When `app.proxy` is `true`, parse
   * the "X-Forwarded-For" ip address list.
   *
   * For example if the value was "client, proxy1, proxy2"
   * you would receive the array `["client", "proxy1", "proxy2"]`
   * where "proxy2" is the furthest down-stream.
   *
   * @return {Array}
   * @api public
   */

  get ips () {
    const proxy = this.app.proxy
    const val = this.get(this.app.proxyIpHeader)
    let ips = proxy && val
      ? val.split(/\s*,\s*/)
      : []
    if (this.app.maxIpsCount > 0) {
      ips = ips.slice(-this.app.maxIpsCount)
    }
    return ips
  },

  /**
   * Return request's remote address
   * When `app.proxy` is `true`, parse
   * the "X-Forwarded-For" ip address list and return the first one
   *
   * @return {String}
   * @api public
   */

  get ip () {
    if (!this[IP]) {
      this[IP] = this.ips[0] || this.socket.remoteAddress || ''
    }
    return this[IP]
  },

  set ip (_ip) {
    this[IP] = _ip
  },

  /**
   * Return subdomains as an array.
   *
   * Subdomains are the dot-separated parts of the host before the main domain
   * of the app. By default, the domain of the app is assumed to be the last two
   * parts of the host. This can be changed by setting `app.subdomainOffset`.
   *
   * For example, if the domain is "tobi.ferrets.example.com":
   * If `app.subdomainOffset` is not set, this.subdomains is
   * `["ferrets", "tobi"]`.
   * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
   *
   * @return {Array}
   * @api public
   */

  get subdomains () {
    const offset = this.app.subdomainOffset
    const hostname = this.hostname
    if (net.isIP(hostname)) return []
    return hostname
      .split('.')
      .reverse()
      .slice(offset)
  },

  /**
   * Get accept object.
   * Lazily memoized.
   *
   * @return {Object}
   * @api private
   */

  get accept () {
    return this._accept || (this._accept = accepts(this.req))
  },

  /**
   * Set accept object.
   *
   * @param {Object}
   * @api private
   */

  set accept (obj) {
    this._accept = obj
  },

  /**
   * Check if the given `type(s)` is acceptable, returning
   * the best match when true, otherwise `false`, in which
   * case you should respond with 406 "Not Acceptable".
   *
   * The `type` value may be a single mime type string
   * such as "application/json", the extension name
   * such as "json" or an array `["json", "html", "text/plain"]`. When a list
   * or array is given the _best_ match, if any is returned.
   *
   * Examples:
   *
   *     // Accept: text/html
   *     this.accepts('html');
   *     // => "html"
   *
   *     // Accept: text/*, application/json
   *     this.accepts('html');
   *     // => "html"
   *     this.accepts('text/html');
   *     // => "text/html"
   *     this.accepts('json', 'text');
   *     // => "json"
   *     this.accepts('application/json');
   *     // => "application/json"
   *
   *     // Accept: text/*, application/json
   *     this.accepts('image/png');
   *     this.accepts('png');
   *     // => false
   *
   *     // Accept: text/*;q=.5, application/json
   *     this.accepts(['html', 'json']);
   *     this.accepts('html', 'json');
   *     // => "json"
   *
   * @param {String|Array} type(s)...
   * @return {String|Array|false}
   * @api public
   */

  accepts (...args) {
    return this.accept.types(...args)
  },

  /**
   * Return accepted encodings or best fit based on `encodings`.
   *
   * Given `Accept-Encoding: gzip, deflate`
   * an array sorted by quality is returned:
   *
   *     ['gzip', 'deflate']
   *
   * @param {String|Array} encoding(s)...
   * @return {String|Array}
   * @api public
   */

  acceptsEncodings (...args) {
    return this.accept.encodings(...args)
  },

  /**
   * Return accepted charsets or best fit based on `charsets`.
   *
   * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
   * an array sorted by quality is returned:
   *
   *     ['utf-8', 'utf-7', 'iso-8859-1']
   *
   * @param {String|Array} charset(s)...
   * @return {String|Array}
   * @api public
   */

  acceptsCharsets (...args) {
    return this.accept.charsets(...args)
  },

  /**
   * Return accepted languages or best fit based on `langs`.
   *
   * Given `Accept-Language: en;q=0.8, es, pt`
   * an array sorted by quality is returned:
   *
   *     ['es', 'pt', 'en']
   *
   * @param {String|Array} lang(s)...
   * @return {Array|String}
   * @api public
   */

  acceptsLanguages (...args) {
    return this.accept.languages(...args)
  },

  /**
   * Check if the incoming request contains the "Content-Type"
   * header field and if it contains any of the given mime `type`s.
   * If there is no request body, `null` is returned.
   * If there is no content type, `false` is returned.
   * Otherwise, it returns the first `type` that matches.
   *
   * Examples:
   *
   *     // With Content-Type: text/html; charset=utf-8
   *     this.is('html'); // => 'html'
   *     this.is('text/html'); // => 'text/html'
   *     this.is('text/*', 'application/json'); // => 'text/html'
   *
   *     // When Content-Type is application/json
   *     this.is('json', 'urlencoded'); // => 'json'
   *     this.is('application/json'); // => 'application/json'
   *     this.is('html', 'application/*'); // => 'application/json'
   *
   *     this.is('html'); // => false
   *
   * @param {String|String[]} [type]
   * @param {String[]} [types]
   * @return {String|false|null}
   * @api public
   */

  is (type, ...types) {
    return typeis(this.req, type, ...types)
  },

  /**
   * Return the request mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

  get type () {
    const type = this.get('Content-Type')
    if (!type) return ''
    return type.split(';')[0]
  },

  /**
   * Return request header.
   *
   * The `Referrer` header field is special-cased,
   * both `Referrer` and `Referer` are interchangeable.
   *
   * Examples:
   *
   *     this.get('Content-Type');
   *     // => "text/plain"
   *
   *     this.get('content-type');
   *     // => "text/plain"
   *
   *     this.get('Something');
   *     // => ''
   *
   * @param {String} field
   * @return {String}
   * @api public
   */

  get (field) {
    const req = this.req
    switch (field = field.toLowerCase()) {
      case 'referer':
      case 'referrer':
        return req.headers.referrer || req.headers.referer || ''
      default:
        return req.headers[field] || ''
    }
  },

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect () {
    if (!this.req) return
    return this.toJSON()
  },

  /**
   * Return JSON representation.
   *
   * @return {Object}
   * @api public
   */

  toJSON () {
    return only(this, [
      'method',
      'url',
      'header'
    ])
  }
}

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */

/* istanbul ignore else */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect
}

lib/response.js


'use strict'

/**
 * Module dependencies.
 */

const contentDisposition = require('content-disposition')
const getType = require('cache-content-type')
const onFinish = require('on-finished')
const escape = require('escape-html')
const typeis = require('type-is').is
const statuses = require('statuses')
const destroy = require('destroy')
const assert = require('assert')
const extname = require('path').extname
const vary = require('vary')
const only = require('only')
const util = require('util')
const encodeUrl = require('encodeurl')
const Stream = require('stream')

/**
 * Prototype.
 */

module.exports = {

  /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket () {
    return this.res.socket
  },

  /**
   * Return response header.
   *
   * @return {Object}
   * @api public
   */

  get header () {
    const { res } = this
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {} // Node < 7.7
  },

  /**
   * Return response header, alias as response.header
   *
   * @return {Object}
   * @api public
   */

  get headers () {
    return this.header
  },

  /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */

  get status () {
    return this.res.statusCode
  },

  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status (code) {
    if (this.headerSent) return

    assert(Number.isInteger(code), 'status code must be a number')
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`)
    this._explicitStatus = true
    this.res.statusCode = code
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
    if (this.body && statuses.empty[code]) this.body = null
  },

  /**
   * Get response status message
   *
   * @return {String}
   * @api public
   */

  get message () {
    return this.res.statusMessage || statuses[this.status]
  },

  /**
   * Set response status message
   *
   * @param {String} msg
   * @api public
   */

  set message (msg) {
    this.res.statusMessage = msg
  },

  /**
   * Get response body.
   *
   * @return {Mixed}
   * @api public
   */

  get body () {
    return this._body
  },

  /**
   * Set response body.
   *
   * @param {String|Buffer|Object|Stream} val
   * @api public
   */

  set body (val) {
    const original = this._body
    this._body = val

    // no content
    if (val == null) {
      if (!statuses.empty[this.status]) {
        if (this.type === 'application/json') {
          this._body = 'null'
          return
        }
        this.status = 204
      }
      if (val === null) this._explicitNullBody = true
      this.remove('Content-Type')
      this.remove('Content-Length')
      this.remove('Transfer-Encoding')
      return
    }

    // set the status
    if (!this._explicitStatus) this.status = 200

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type')

    // string
    if (typeof val === 'string') {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
      this.length = Buffer.byteLength(val)
      return
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin'
      this.length = val.length
      return
    }

    // stream
    if (val instanceof Stream) {
      onFinish(this.res, destroy.bind(null, val))
      if (original !== val) {
        val.once('error', err => this.ctx.onerror(err))
        // overwriting
        if (original != null) this.remove('Content-Length')
      }

      if (setType) this.type = 'bin'
      return
    }

    // json
    this.remove('Content-Length')
    this.type = 'json'
  },

  /**
   * Set Content-Length field to `n`.
   *
   * @param {Number} n
   * @api public
   */

  set length (n) {
    if (!this.has('Transfer-Encoding')) {
      this.set('Content-Length', n)
    }
  },

  /**
   * Return parsed response Content-Length when present.
   *
   * @return {Number}
   * @api public
   */

  get length () {
    if (this.has('Content-Length')) {
      return parseInt(this.get('Content-Length'), 10) || 0
    }

    const { body } = this
    if (!body || body instanceof Stream) return undefined
    if (typeof body === 'string') return Buffer.byteLength(body)
    if (Buffer.isBuffer(body)) return body.length
    return Buffer.byteLength(JSON.stringify(body))
  },

  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */

  get headerSent () {
    return this.res.headersSent
  },

  /**
   * Vary on `field`.
   *
   * @param {String} field
   * @api public
   */

  vary (field) {
    if (this.headerSent) return

    vary(this.res, field)
  },

  /**
   * Perform a 302 redirect to `url`.
   *
   * The string "back" is special-cased
   * to provide Referrer support, when Referrer
   * is not present `alt` or "/" is used.
   *
   * Examples:
   *
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   *
   * @param {String} url
   * @param {String} [alt]
   * @api public
   */

  redirect (url, alt) {
    // location
    if (url === 'back') url = this.ctx.get('Referrer') || alt || '/'
    this.set('Location', encodeUrl(url))

    // status
    if (!statuses.redirect[this.status]) this.status = 302

    // html
    if (this.ctx.accepts('html')) {
      url = escape(url)
      this.type = 'text/html; charset=utf-8'
      this.body = `Redirecting to <a href="${url}">${url}</a>.`
      return
    }

    // text
    this.type = 'text/plain; charset=utf-8'
    this.body = `Redirecting to ${url}.`
  },

  /**
   * Set Content-Disposition header to "attachment" with optional `filename`.
   *
   * @param {String} filename
   * @api public
   */

  attachment (filename, options) {
    if (filename) this.type = extname(filename)
    this.set('Content-Disposition', contentDisposition(filename, options))
  },

  /**
   * Set Content-Type response header with `type` through `mime.lookup()`
   * when it does not contain a charset.
   *
   * Examples:
   *
   *     this.type = '.html';
   *     this.type = 'html';
   *     this.type = 'json';
   *     this.type = 'application/json';
   *     this.type = 'png';
   *
   * @param {String} type
   * @api public
   */

  set type (type) {
    type = getType(type)
    if (type) {
      this.set('Content-Type', type)
    } else {
      this.remove('Content-Type')
    }
  },

  /**
   * Set the Last-Modified date using a string or a Date.
   *
   *     this.response.lastModified = new Date();
   *     this.response.lastModified = '2013-09-13';
   *
   * @param {String|Date} type
   * @api public
   */

  set lastModified (val) {
    if (typeof val === 'string') val = new Date(val)
    this.set('Last-Modified', val.toUTCString())
  },

  /**
   * Get the Last-Modified date in Date form, if it exists.
   *
   * @return {Date}
   * @api public
   */

  get lastModified () {
    const date = this.get('last-modified')
    if (date) return new Date(date)
  },

  /**
   * Set the ETag of a response.
   * This will normalize the quotes if necessary.
   *
   *     this.response.etag = 'md5hashsum';
   *     this.response.etag = '"md5hashsum"';
   *     this.response.etag = 'W/"123456789"';
   *
   * @param {String} etag
   * @api public
   */

  set etag (val) {
    if (!/^(W\/)?"/.test(val)) val = `"${val}"`
    this.set('ETag', val)
  },

  /**
   * Get the ETag of a response.
   *
   * @return {String}
   * @api public
   */

  get etag () {
    return this.get('ETag')
  },

  /**
   * Return the response mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

  get type () {
    const type = this.get('Content-Type')
    if (!type) return ''
    return type.split(';', 1)[0]
  },

  /**
   * Check whether the response is one of the listed types.
   * Pretty much the same as `this.request.is()`.
   *
   * @param {String|String[]} [type]
   * @param {String[]} [types]
   * @return {String|false}
   * @api public
   */

  is (type, ...types) {
    return typeis(this.type, type, ...types)
  },

  /**
   * Return response header.
   *
   * Examples:
   *
   *     this.get('Content-Type');
   *     // => "text/plain"
   *
   *     this.get('content-type');
   *     // => "text/plain"
   *
   * @param {String} field
   * @return {any}
   * @api public
   */

  get (field) {
    return this.res.getHeader(field)
  },

  /**
   * Returns true if the header identified by name is currently set in the outgoing headers.
   * The header name matching is case-insensitive.
   *
   * Examples:
   *
   *     this.has('Content-Type');
   *     // => true
   *
   *     this.get('content-type');
   *     // => true
   *
   * @param {String} field
   * @return {boolean}
   * @api public
   */

  has (field) {
    return typeof this.res.hasHeader === 'function'
      ? this.res.hasHeader(field)
      // Node < 7.7
      : field.toLowerCase() in this.headers
  },

  /**
   * Set header `field` to `val` or pass
   * an object of header fields.
   *
   * Examples:
   *
   *    this.set('Foo', ['bar', 'baz']);
   *    this.set('Accept', 'application/json');
   *    this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
   *
   * @param {String|Object|Array} field
   * @param {String} val
   * @api public
   */

  set (field, val) {
    if (this.headerSent) return

    if (arguments.length === 2) {
      if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v))
      else if (typeof val !== 'string') val = String(val)
      this.res.setHeader(field, val)
    } else {
      for (const key in field) {
        this.set(key, field[key])
      }
    }
  },

  /**
   * Append additional header `field` with value `val`.
   *
   * Examples:
   *
   * ```
   * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
   * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
   * this.append('Warning', '199 Miscellaneous warning');
   * ```
   *
   * @param {String} field
   * @param {String|Array} val
   * @api public
   */

  append (field, val) {
    const prev = this.get(field)

    if (prev) {
      val = Array.isArray(prev)
        ? prev.concat(val)
        : [prev].concat(val)
    }

    return this.set(field, val)
  },

  /**
   * Remove header `field`.
   *
   * @param {String} name
   * @api public
   */

  remove (field) {
    if (this.headerSent) return

    this.res.removeHeader(field)
  },

  /**
   * Checks if the request is writable.
   * Tests for the existence of the socket
   * as node sometimes does not set it.
   *
   * @return {Boolean}
   * @api private
   */

  get writable () {
    // can't write any more after response finished
    // response.writableEnded is available since Node > 12.9
    // https://nodejs.org/api/http.html#http_response_writableended
    // response.finished is undocumented feature of previous Node versions
    // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js
    if (this.res.writableEnded || this.res.finished) return false

    const socket = this.res.socket
    // There are already pending outgoing res, but still writable
    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
    if (!socket) return true
    return socket.writable
  },

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect () {
    if (!this.res) return
    const o = this.toJSON()
    o.body = this.body
    return o
  },

  /**
   * Return JSON representation.
   *
   * @return {Object}
   * @api public
   */

  toJSON () {
    return only(this, [
      'status',
      'message',
      'header'
    ])
  },

  /**
   * Flush any set headers and begin the body
   */

  flushHeaders () {
    this.res.flushHeaders()
  }
}

/**
 * Custom inspection implementation for node 6+.
 *
 * @return {Object}
 * @api public
 */

/* istanbul ignore else */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect
}

koa2常用个中间件

常用中间件:https://www.jianshu.com/p/f69852835699

  1. koa-bodyparser: 解析请求体
  2. koa-router:路由功能
  3. koa-views+ejs:视图模板渲染
  4. koa-static:处理静态资源
  5. koa-jwt:token验证
  6. koa-compress:压缩响应体
  7. koa-compose:合并中间件
  8. koa-logger:输出请求日志

ctx.request和ctx.req的区别

1.ctx.request是koa2中context经过封装的请求对象,它用起来更直观和简单。
2.ctx.req:是nodejs提供的原声http请求对象。这个虽然不直观,但是包含的信息更多,适合我们更深的编程。

ctx.method 得到请求类型

Koa2中提供了ctx.method属性,可以轻松的得到请求的类型,然后根据请求类型编写不同的相应方法,这在工作中非常常用。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐