前提: 你需要对node的http模块比较熟悉,同时了解相关的http知识,这很重要
目录结构
Application
application.js
主要是对 App 做的一些操作,包括创建服务、在 ctx 对象上挂载 request、response 对象,以及处理异常等操作。接下来将对这些实现进行详细阐述。
Koa 创建服务的原理
- Node 原生创建服务
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
});
server.listen(4000, () => {
console.log("server start at 4000");
});
module.exports = class Application extends Emitter {
/**
* 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 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);
}
};
中间件实现原理
中间件使用例子
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
console.log("---1--->");
await next();
console.log("===6===>");
});
app.use(async (ctx, next) => {
console.log("---2--->");
await next();
console.log("===5===>");
});
app.use(async (ctx, next) => {
console.log("---3--->");
await next();
console.log("===4===>");
});
app.listen(4000, () => {
console.log("server is running, port is 4000");
});
注册中间件
Koa 注册中间件是用app.use()
方法实现的
module.exports = class Application extends Emitter {
constructor(options) {
this.middleware = [];
}
/**
* 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;
}
};
Application 类的构造函数中声明了一个名为 middleware 的数组,当执行 use()方法时,会一直往 middleware 中的 push()方法传入函数。其实,这就是 Koa 注册中间件的原理,middleware 就是一个队列,注册一个中间件,就进行入队操作。
koa-compose
中间件注册后,当请求进来的时候,开始执行中间件里面的逻辑,由于有 next 的分割,一个中间件会分为两部分执行。
midddleware 队列是如何执行?
const compose = require("koa-compose");
module.exports = class Application extends Emitter {
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;
}
};
探究下koa-compose
的核心源码实现:
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose(middleware) {
// 参数必须是数组
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
// 数组的每一项必须是函数,其实就是注册中间件的回调函数
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
// 返回闭包,由此可知koa this.callback的函数后续一定会使用这个闭包传入过滤的上下文
return function (context, next) {
// last called middleware #
// 初始化中间件函数数组执行下标值
let index = -1;
// 返回递归执行的Promise.resolve去执行整个中间件数组
// 从第一个开始
return dispatch(0);
function dispatch(i) {
// 检验上次执行的下标索引不能大于本次执行的下标索引i,如果大于,可能是下个中间件多次执行导致的
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
// 当前执行的中间件函数
let fn = middleware[i];
// 如果当前执行下标等于中间件数组长度,放回Promise.resolve()即可
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
// 递归执行每个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
如何封装 ctx
module.exports = class Application extends Emitter {
// 3个属性,Object.create分别继承
constructor(options) {
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount("error")) this.on("error", this.onerror);
const handleRequest = (req, res) => {
// 创建context对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
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;
}
};
中间件中的ctx
对象经过createContext()
方法进行了封装,其实ctx
是通过Object.create()
方法继承了this.context
,而this.context
又继承了lib/context.js
中导出的对象。最终将http.IncomingMessage
类和http.ServerResponse
类都挂载到了context.req
和context.res
属性上,这样是为了方便用户从ctx
对象上获取需要的信息。
单一上下文原则: 是指创建一个 context 对象并共享给所有的全局中间件使用。也就是说,每个请求中的 context 对象都是唯一的,并且所有关于请求和响应的信息都放在 context 对象里面。
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);
}
错误处理
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`)
}
Context 核心实现(TODO)
Context 可以理解为上下文,其实就是我们常用的 ctx 对象。
Koa 要把 ctx.request 和 ctx.response 中的属性挂载到 ctx 上的原因,即更方便获取相关属性。
委托机制
context.js 中的委托机制使用了一个包 delegates。可以帮我们方便快捷地使用设计模式当中的委托模式(Delegation Pattern),即外层暴露的对象将请求委托给内部的其他对象进行处理。
- getter:外部对象可以直接访问内部对象的值
- setter:外部对象可以直接修改内部对象的值
- access:包含 getter 与 setter 的功能
- method:外部对象可以直接调用内部对象的函数
具体的用法请查看文档,这里不过多介绍。
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
如果 this 不是 Delegator 的实例的话,则调用 new Delegator(proto, target)。通过这种方式,可以避免在调用初始化函数时忘记写 new 造成的问题,因为此时下面两种写法是等价的:
- let x = new Delegator(petShop, 'dog')
- let x = Delegator(petShop, 'dog')
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
__defineGetter__
,它可以在已存在的对象上添加可读属性,其中第一个参数为属性名,第二个参数为函数,返回值为对应的属性值:
const obj = {};
obj.__defineGetter__('name', () => 'elvin');
console.log(obj.name);
// => 'elvin'
obj.name = '旺财';
console.log(obj.name);
// => 'elvin'
需要注意的是尽管 defineGetter 曾被广泛使用,但是已不被推荐,建议通过 Object.defineProperty 实现同样功能,或者通过 get 操作符实现类似功能:
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'elvin',
});
Object.defineProperty(obj, 'sex', {
get() {
return 'male';
}
});
const dog = {
get name() {
return '旺财';
}
};
不过我看 github 上别人提的 PR 到现在都没合并
setter
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
__defineSetter__
,它可以在已存在的对象上添加可读属性,其中第一个参数为属性名,第二个参数为函数,参数为传入的值:
const obj = {};
obj.__defineSetter__('name', function(value) {
this._name = value;
});
obj.name = 'elvin';
console.log(obj.name, obj._name);
// undefined 'elvin'
同样地,虽然 defineSetter 曾被广泛使用,但是已不被推荐,建议通过 Object.defineProperty 实现同样功能,或者通过 set 操作符实现类似功能:
const obj = {};
Object.defineProperty(obj, 'name', {
set(value) {
this._name = value;
}
});
const dog = {
set(value) {
this._name = value;
}
};
method
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
method 的实现也十分简单,只需要注意这里 apply 函数的第一个参数是内部对象 this[target],从而确保了在执行函数 thistarget 时,函数体内的 this 是指向对应的内部对象。
Cookie 的操作
Koa 的服务一般都是 BFF 服务,涉及前端服务时通常会遇到用户登录的场景。Cookie 是用来记录用户登录状态的,Koa 本身也提供了修改 Cookie 的功能。
request 具体实现
request.js 的实现比较简单,就是通过 set()和 get()方法对一些属性进行封装,方便开发者调用一些常用属性。
- 获取并设置 headers 对象
get header() {
return this.req.headers
}
set header(val) {
this.req.headers = val
}
get headers() {
return this.req.headers
}
set headers(val) {
this.req.headers = val
}
header
和headers
属性一样,兼容写法。
- 获取设置 res 对象的 URL
get url () {
return this.req.url
},
set url (val) {
this.req.url = val
},
- 获取 URL 的来源,包括 protocol 和 host。
get origin () {
return `${this.protocol}://${this.host}`
},
- 获取完整的请求 URL
get href () {
// support: `GET http://example.com/foo`
if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl
return this.origin + this.originalUrl
},
- 获取请求 method()方法
get method () {
return this.req.method
},
- 获取请求中的 path
get path() {
return parse(this.req).pathname
}
- 获取请求中的 query 对象
get query () {
const str = this.querystring
const c = this._querycache = this._querycache || {}
return c[str] || (c[str] = qs.parse(str))
},
获取 query 使用了querystring
模块const qs = require('querystring')
,最终得到的是一个 Object。
- 获取请求中的 query 字符串。
get querystring () {
if (!this.req) return ''
return parse(this.req).query || ''
},
ctx.request.querystring 输出结果如下。
page=10
- 获取带问号的 querystring,与上面 get querystring()方法的区别是这里多了个问号。
get search () {
if (!this.querystring) return ''
return `?${this.querystring}`
},
- 获取主机(hostname:port),当 app.proxy 为 true 时,支持 X-Forwarded-Host,否则使用 Host。
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]
},
- 存在时获取主机名
get hostname () {
const host = this.host
if (!host) return ''
// 如果主机是IPV6,koa解析到WHATWG URL API,注意,这可能会影响性能
if (host[0] === '[') return this.URL.hostname || '' // IPv6
return host.split(':', 1)[0]
},
- 获取完整 URL 对象属性
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
},
- 使用请求和响应头检查响应的新鲜度,会通过 Last-Modified 或 Etag 判断缓冲是否过期。
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
},
- 使用请求和响应头检查响应的陈旧度(和 fresh 相反)。
get stale () {
return !this.fresh
},
- 检测 this.method 是否是
['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']
中的方法。
get idempotent () {
const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']
return !!~methods.indexOf(this.method)
},
- 获取请求中的 socket 对象。
get socket () {
return this.req.socket
},
- 获取请求中的字符集。
get charset () {
try {
const { parameters } = contentType.parse(this.req)
return parameters.charset || ''
} catch (e) {
return ''
}
}
- 以 number 类型返回请求的 Content-Length。
get length () {
const len = this.get('Content-Length')
if (len === '') return
return ~~len // 字符串转数字
},
- 返回请求协议:“https”或“http”。当 app.proxy 是 true 时支持 X-Forwarded-Proto。
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'
},
- 通过 ctx.protocol == "https"来检查请求是否通过 TLS 发出。
get secure () {
return this.protocol === 'https'
},
- 当 app.proxy 为 true 时,解析 X-Forwarded-For 的 IP 地址列表
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
},
- 获取请求远程地址。
get ip () {
if (!this[IP]) {
this[IP] = this.ips[0] || this.socket.remoteAddress || ''
}
return this[IP]
},
- 以数组形式返回子域。
get subdomains () {
const offset = this.app.subdomainOffset
const hostname = this.hostname
if (net.isIP(hostname)) return []
return hostname
.split('.')
.reverse()
.slice(offset)
},
- 获取请求Content-Type。
get type () {
const type = this.get('Content-Type')
if (!type) return ''
return type.split(';')[0]
},
response 具体实现 (TODO)
response.js的整体实现思路和request.js大体一致,也是通过set()和get()方法封装了一些常用属性。
- 返回socket实例
get socket () {
return this.res.socket
},
- 返回响应头
get header () {
const { res } = this
return typeof res.getHeaders === 'function'
? res.getHeaders()
: res._headers || {} // Node < 7.7
},
get headers () {
return this.header
},
- 设置并获取响应状态码。
http.ServerResponse对象的属性:
- headerSent 当头部已经有响应后,res.headerSent 为 true 否则为false 可以通过这个属性来判断是否已经响应.
- statusCode
- sendDate true/fasle 当为 false 时,将删除头部时间
get status () {
return this.res.statusCode
},
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 message () {
return this.res.statusMessage || statuses[this.status]
},
set message (msg) {
this.res.statusMessage = msg
},
- 设置并获取响应体body
get body () {
return this._body
},
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'
},
- 设置并获取Content-Length。
set length (n) {
if (!this.has('Transfer-Encoding')) {
this.set('Content-Length', n)
}
},
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))
},
- 设置并获取Content-Type。
set type (type) {
type = getType(type)
if (type) {
this.set('Content-Type', type)
} else {
this.remove('Content-Type')
}
},
get type () {
const type = this.get('Content-Type')
if (!type) return ''
return type.split(';', 1)[0]
},
- 设置并获取lastModified。
get lastModified () {
const date = this.get('last-modified')
if (date) return new Date(date)
},
set lastModified (val) {
if (typeof val === 'string') val = new Date(val)
this.set('Last-Modified', val.toUTCString())
},
- 设置并获取Etag
set etag (val) {
if (!/^(W\/)?"/.test(val)) val = `"${val}"`
this.set('ETag', val)
},
get etag () {
return this.get('ETag')
},
总结
koa本质上是对node的http模块进行二次封装,所以需要对使用者对http模块有比较深的理解。同时也对洋葱模型的实现原理进行了说明,包括koa如何对request
、response
模块对象的代理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。