大前端 - nodejs - koa
koa介绍koa是一个新的web框架,由express幕后的原班人马打造,致力于web应用和api开发领域中的一个更小,更富有表现力的更健壮的基石。官网:https://koajs.comgithub仓库:https://github.com/koajs/koa一个翻译的中文网:https://koa.bootcss.comkoa的内部原理和express很像,但是语法和内部结构结构进行了升级。k
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
- koa更好用,能更简单的完成功能。
- koa不是完全取代express,每个人爱好不同。
- koa2社区不如express。
- 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
- koa-bodyparser: 解析请求体
- koa-router:路由功能
- koa-views+ejs:视图模板渲染
- koa-static:处理静态资源
- koa-jwt:token验证
- koa-compress:压缩响应体
- koa-compose:合并中间件
- koa-logger:输出请求日志
ctx.request和ctx.req的区别
1.ctx.request是koa2中context经过封装的请求对象,它用起来更直观和简单。
2.ctx.req:是nodejs提供的原声http请求对象。这个虽然不直观,但是包含的信息更多,适合我们更深的编程。
ctx.method 得到请求类型
Koa2中提供了ctx.method属性,可以轻松的得到请求的类型,然后根据请求类型编写不同的相应方法,这在工作中非常常用。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)