共计 28213 个字符,预计需要花费 71 分钟才能阅读完成。
抛砖引玉
先从官网示例开始, 咱们先简略实现启动服务的代码
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {ctx.body = 'Hello World';});
app.listen(3000);
在应用原始 Nodejs, 咱们启动一个服务的写法是这样的
const http = require('http')
const server = http.createServer((req, res) => {res.end('hello world')
})
server.listen(3000)
咱们大略能够看出几点不同
- 通过构造函数 Koa 实例化
- 新增 use 办法和 ctx 传参
构建 Application
从官网看 API 阐明
app.use(function)
将给定的中间件办法增加到此应用程序。app.use()
返回 this
, 因而能够链式表白.
Koa 应用程序是一个蕴含一组中间件函数的对象,它是依照相似堆栈的形式组织和执行的。咱们跟着这个思路实现一个根本类
application.js
const http = require('http')
class Koa {constructor() {
// 中间件队列
this.middlewares = []}
// 启动服务器
listen(...args) {const server = http.createServer((req, res) => {
// 先遍历执行
this.middlewares.forEach(middleware => middleware(req, res))
})
return server.listen(...args)
}
// 增加中间件
use(middleware) {this.middlewares.push(middleware)
// 返回链式调用
return this
}
}
module.exports = Koa
中间件执行办法前面再补充, 先引入模块执行测试
// index.js
const Koa = require('./application');
const app = new Koa();
app.use((req, res) => {console.log('middleware1')
}).use((req, res) => {res.end('构建 Application')
});
app.listen(3000);
相干代码已上传至 lesson1
上下文(Context)
Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的办法。这些操作在 HTTP 服务器开发中频繁应用,它们被增加到此级别而不是更高级别的框架,这将强制中间件从新实现此通用性能。
每个 申请都将创立一个 Context,并在中间件中作为接收器援用,或者 ctx 标识符
咱们先创立上下文, 申请, 和响应扩大原型, 外面含有对应的扩大属性和办法, 咱们先留空占位
// context.js
module.exports = {}
// request.js
module.exports = {}
// response.js
module.exports = {}
application
引入下面模块, 设置一个创立上下文办法
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa {constructor() {
// 中间件队列
this.middlewares = [];
// 扩大属性
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// 启动服务器
listen(...args) {const server = http.createServer((req, res) => {const ctx = this.createContext(req, res)
// 先遍历执行
this.middlewares.forEach(middleware => middleware(ctx))
})
server.listen(...args)
}
// 创立上下文
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.response = response
response.request = request
// 赋值 url
context.originalUrl = request.originalUrl = req.url
// 上下文状态
context.state = {}
return context
}
// 增加中间件
use(middleware) {this.middlewares.push(middleware)
return this
}
}
module.exports = Koa
index.js
const Koa = require('./application');
const app = new Koa();
app.use(ctx => {console.log('middleware1')
}).use(ctx => {ctx.res.end('上下文(Context)')
});
app.listen(3000);
这里只是简略实现上下文, 前面再补充细节实现
相干代码已上传至 lesson2
级联(洋葱圈模型及中间件传递)
Koa 中间件以更传统的形式级联,您可能习惯应用相似的工具 – 之前难以让用户敌对地应用 node 的回调。然而,应用 async 性能,咱们能够实现“实在”的中间件。比照 Connect 的实现,通过一系列性能间接传递管制,直到一个返回,Koa 调用“上游”,而后控制流回“上游”。
当一个中间件调用 next()
则该函数暂停并将管制传递给定义的下一个中间件。当在上游没有更多的中间件执行后,堆栈将开展并且每个中间件复原执行其上游行为。
compose
koa 里级联的源码都封装在 koa-cpmose
里, 自身代码不多, 咱们能够间接跟着思路手写进去, 次要实现几个点
- 要可能依照调用程序来回执行中间件并且能够应用
async
性能, 所以必须革新成Promise
模式实现 - 利用第一点能够在中间件决定什么时候让出控制权, 往下执行, 当执行到最初中间件并且实现时返回控制权往上执行, 这就是洋葱圈模型的原理, 咱们须要一个
next
函数通知程序什么时候往下执行 - 为了保障不被局部中间件有意无意批改上下文影响, 每个中间件须要失去同样的上下文
- 还有一些必须的容错解决避免代码解体(递归内存溢出, 程序出错引起流程中断等)
function compose(middleware) {return (context, next) => {
// 最初一个被执行的中间件
let index = -1
// 开始执行
return dispatch(0)
function dispatch(i) {
// 避免屡次执行
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 以后赋值
index = i
const fn = middleware[i]
// 执行到最初一个中间件时失常不应该执行 next, 这时候 next=undefined, 即便有调用后续也有容错解决
if (i === middleware.length) fn = next
// 如果没有申明 next 则终止执行, 开始回溯执行
if (!fn) return Promise.resolve()
try {// 中间件最终按序执行的代码, 并且每个中间件都传递雷同的上下文, 避免被其中某些中间件扭转影响后续执行,next 即传入的 dispatch.bind(null, i + 1)等于下一个中间件执行函数, 因为是 Promise 函数所以能够利用 async await 中断让出以后函数控制权往下执行
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
// 容错中断
return Promise.reject(err)
}
}
}
}
module.exports = compose
applaction.js
咱们再将本来 listen
办法按繁多职责划分成启动服务, 申请回调, 中间件执行三个函数
const http = require('http')
const compose = require('./compose');
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa {
//... 省略
// 启动服务器
listen(...args) {
// 将启动回调抽离
const server = http.createServer(this.callback())
server.listen(...args)
}
// 启动回调
callback() {
// 洋葱圈模型流程管制的外围, 上面详解
const fn = compose(this.middlewares)
return (req, res) => {
// 强制中间件从新实现新上下文
const ctx = this.createContext(req, res)
return this.hadnleRequest(ctx, fn)
}
}
// 回调申请
hadnleRequest(ctx, fnMiddleware) {
// 响应解决
const handleResoponse = () => respond(ctx)
return fnMiddleware(ctx).then(handleResoponse)
}
}
// 响应加强
function respond(ctx) {
const res = ctx.res
let body = ctx.body
body = JSON.stringify(body)
res.end(body)
}
module.exports = Koa
这两块大家看来可能云里雾里, 理论整个流程串联起来的后果就是
compose(this.middlewares)(ctx).then(() => (ctx) => ctx.res.end(JSON.stringify(ctx.body)))
能够看到由此至终 ctx
贯通其中, 而其中 compose
的外围代码是这段
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
还是的联合上下文运行一遍好好了解原理
index.js
最初咱们再运行测试一下
const Koa = require('./application');
const app = new Koa();
app.use(async (ctx, next) => {console.log('middleware1 start')
await next();
console.log('middleware1 end')
});
app.use(async (ctx, next) => {console.log('middleware2 start')
await next();
console.log('middleware2 end')
});
app.use(async ctx => {ctx.body = '级联(洋葱圈模型及中间件传递)'
});
app.listen(3000);
终端输入
middleware1 start
middleware2 start
middleware2 end
middleware1 end
相干代码已上传至 lesson3
引入 Cookies 模块
Cookies 是一个设置和获取 HTTP(S)的 cookies 的 nodejs 模块, 能够应用 Keygrip 对 cookie 进行签名以避免篡改, 它能够作为 nodejs http 库或者 Connect/Express 中间件
applaction.js
先减少可选配置参数
class Koa {constructor(options) {
// 可选项
options = options || {};
// cookies 签名
if (options.keys) this.keys = options.keys;
// 中间件队列
this.middlewares = [];
// 扩大属性
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// 省略其余
}
上下文减少 cookies 办法
context.js
/*
https://www.npmjs.com/package/cookies
cookie 是一个 node.js 模块,用于获取和设置 HTTP cookie。能够应用 Keygrip 对 cookie 进行签名以避免篡改。它能够与内置的 node.js HTTP 库一起应用,也能够作为 Connect/Express 中间件应用。*/
const Cookies = require('cookies');
// symbol 值作为对象属性的标识符
const COOKIES = Symbol('context#cookies');
module.exports = {get cookies() {if (!this[COOKIES]) {this[COOKIES] = new Cookies(this.req, this.res, {
// 应用凭证启用基于 SHA1 HMAC 的加密签名
keys: this.app.keys,
// 明确指定连贯是否平安,而不是此模块查看申请
secure: this.requset.secure
})
}
return this[COOKIES]
},
set cookies(_cookies) {this[COOKIES] = _cookies
}
}
相干代码已上传至 lesson4
解析 / 设置申请 / 响应头
request.js
// ------ 省略其余 ------
/*
https://www.npmjs.com/package/only
返回对象的白名单属性。*/
const only = require('only');
/*
https://www.npmjs.com/package/accepts
基于 negotiator 的高级内容协商, 从 koa 提取用于惯例应用
*/
const accepts = require('accepts');
/*
https://www.npmjs.com/package/typeis
类型查看
*/
const typeis = require('type-is');
/*
https://www.npmjs.com/package/content-type
依据 RFC 7231 创立解析 HTTP Content-Type 头
*/
const contentType = require('content-type');
module.exports = {
// ------ 省略其余 ------
// 查看施行
inspect() {if (!this.req) return;
return this.toJSON();},
// 返回的指定配置的 JSON 示意数据
toJSON() {
return only(this, [
'method',
'url',
'header'
]);
},
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},
// 返回对给定申请头值
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] || '';
}
},
// 获取字符编码
get charset() {
try {const { parameters} = contentType.parse(this.req);
return parameters.charset || '';
} catch (e) {return '';}
},
// 返回解析后的内容长度
get length() {const len = this.get('Content-Length');
if (len === '') return;
return ~~len;
},
/*
查看给定的“类型”是否能够承受,返回最佳匹配时为真,否则为假,其中状况你应该回应 406“不可承受”。*/
accepts(...args) {return this.accept.types(...args);
},
get accept() {return this._accept || (this._accept = accepts(this.req));
},
set accept(obj) {this._accept = obj;},
// 依据“encodings”返回已承受的编码或最适宜的编码(Accept-Encoding: gzip, deflate), 返回['gzip', 'deflate']
acceptsEncodings(...args) {return this.accept.encodings(...args);
},
// 依据“charsets”返回已承受的字符集或最适宜的字符集(Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5), 返回['utf-8', 'utf-7', 'iso-8859-1']
acceptsCharsets(...args) {return this.accept.charsets(...args);
},
// 依据“Language”返回已承受的语言或最适宜的语言(Accept-Language: en;q=0.8, es, pt), 返回['es', 'pt', 'en']
acceptsLanguages(...args) {return this.accept.languages(...args);
},
/*
查看进来的申请是否蕴含 Content-Type 头, 其中是否蕴含给定的 mime 类型
如果没有申请体, 返回 null
如果没有内容类型, 返回 false
其余, 返回第一个匹配的类型
*/
is(type, ...types) {return typeis(this.req, type, ...types);
},
// 返回申请 mime 类型 void, 参数如“字符集”。get type() {const type = this.get('Content-Type');
if (!type) return '';
return type.split(';')[0];
},
get method() {return this.req.method;},
set method(val) {this.req.method = val;},
// 查看申请是否 idempotent
get idempotent() {const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
return !!~methods.indexOf(this.method);
},
}
下面援用了一个 only 库, 它的作用就是筛选属性返回, 例如
var obj = {
name: 'tobi',
last: 'holowaychuk',
email: 'tobi@learnboost.com',
_id: '12345'
};
var user = only(obj, 'name last email');
/* 返回后果
{
name: 'tobi',
last: 'holowaychuk',
email: 'tobi@learnboost.com'
}
*/
其余大部分都是些解析类, 就不细说了
response
这一章节代码会比拟多, 然而我尽量把有关系的代码块放一起上下文分割,, 理论也是写解析解决的操作, 缓缓看下来也没有什么难点
const extname = require('path').extname;
const Stream = require('stream');
/*
https://www.npmjs.com/package/only
Return whitelisted properties of an object.
*/
const only = require('only');
/*
https://www.npmjs.com/package/type-is
Infer the content-type of a request.
*/
const typeis = require('type-is').is;
/*
https://www.npmjs.com/package/vary
Manipulate the HTTP Vary header
*/
const vary = require('vary');
/*
https://www.npmjs.com/package/cache-content-type
The same as mime-types's contentType method, but with result cached.
*/
const getType = require('cache-content-type');
/*
https://www.npmjs.com/package/content-disposition
reate and parse HTTP Content-Disposition header
*/
const contentDisposition = require('content-disposition');
/*
https://www.npmjs.com/package/assert
With browserify, simply require('assert') or use the assert global and you will get this module.
The goal is to provide an API that is as functionally identical to the Node.js assert API as possible. Read the official docs for API documentation.
*/
const assert = require('assert');
/*
https://www.npmjs.com/package/statuses
HTTP status utility for node.
This module provides a list of status codes and messages sourced from a few different projects:
*/
const statuses = require('statuses');
/*
https://www.npmjs.com/package/on-finished
Execute a callback when a HTTP request closes, finishes, or errors.
*/
const onFinish = require('on-finished');
/*
https://www.npmjs.com/package/destroy
Destroy a stream.
This module is meant to ensure a stream gets destroyed, handling different APIs and Node.js bugs.
*/
const destroy = require('destroy');
module.exports = {flushHeaders() {this.res.flushHeaders();
},
inspect() {if (!this.res) return;
const o = this.toJSON();
o.body = this.body;
return o;
},
toJSON() {
return only(this, [
'status',
'message',
'header'
]);
},
get type() {const type = this.get('Content-Type');
if (!type) return '';
return type.split(';', 1)[0];
},
// 返回传入数据是否指定类型之一
is(type, ...types) {return typeis(this.req, type, ...types);
},
get header() {const { res} = this;
return typeof res.getHeaders === 'function'
? res.getHeaders()
: res._headers || {}; // Node < 7.7},
get headers() {return this.header;},
get(field) {return this.header[field.toLowerCase()] || '';
},
has(field) {
return typeof this.res.hasHeader === 'function'
? this.res.hasHeader(field)
// Node < 7.7
: field.toLowerCase() in this.headers;},
set type(type) {type = getType(type);
if (type) {this.set('Content-Type', type);
} else {this.remove('Content-Type');
}
},
attachment(filename, options) {
// 获取扩展名
if (filename) this.type = extname(filename);
// 重设 Content-Disposition 头字段
this.set('Content-Disposition', contentDisposition(filename, options));
},
get headerSent() {return this.res.headersSent;},
// 操纵扭转头字段
vary(field) {if (this.headerSent) return;
vary(this.res, field);
},
set(field, val) {if (this.headerSent) return;
if (2 === arguments.length) {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(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(field) {if (this.headerSent) return;
this.res.removeHeader(field);
},
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;
if (this.body && statuses.empty) this.body = null;
},
get message() {return this.res.statusMessage || statuses[this.status];
},
set message(msg) {this.res.statusMessage = msg;},
get body() {return this._body;},
set body(val) {
const original = this._body;
this._body = val;
// no content
if (null == val) {if (!statuses.empty[this.status]) 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 ('string' === typeof val) {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) {
// 当申请敞开, 实现或者谬误都会执行回调, 这里是销毁 Stream
onFinish(this.res, destroy.bind(null, val));
if (original != val) {val.once('error', err => this.ctx.onerror(err));
// overwriting
if (null != original) this.remove('Content-Length');
}
if (setType) this.type = 'bin';
return;
}
// json
this.remove('Content-Length');
this.type = 'json';
},
set length(n) {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 ('string' === typeof body) return Buffer.byteLength(body);
if (Buffer.isBuffer(body)) return body.length;
return Buffer.byteLength(JSON.stringify(body));
},
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;
},
}
相干代码已上传至 lesson5
解析 / 设置 URL
request
// ------ 省略其余 ------
const stringify = require('url').format;
const net = require('net');
/*
https://www.npmjs.com/package/url
这个模块具备与 node.js 外围 URL 模块雷同的 URL 解析和解析工具。*/
const URL = require('url').URL;
/*
https://www.npmjs.com/package/parseurl
Parse a URL with memoization
*/
const parse = require('parseurl');
/*
https://www.npmjs.com/package/querystringjs
一个查问字符串解析实用程序,能够正确处理一些边界状况。当您想要正确地解决查问字符串时,请应用此选项。*/
const qs = require('querystring');
module.exports = {
// ------ 省略其余 ------
// 缓存解析后的 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;
},
get url() {return this.req.url;},
set url(val) {this.req.url = val;},
// 当应用 TLS 申请返回 http 或者 https 协定字符串, 当代理设置 "X-Forwarded-Proto" 头会被信赖, 如果你正在启用一个 http 反向代理这将被启动
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';
},
// 是否 https
get secure() {return 'https' === this.protocol;},
// 解析 Host 头, 当启动代理反对 X -Forwarded-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 '';
if ('[' === host[0]) return this.URL.hostname || ''; // IPv6
return host.split(':', 1)[0];
},
/*
返回数组类型子域名
子域名是主机在主域名之前以点分隔的局部
应用程序的域。默认状况下,应用程序的域是最初两个域
主机的一部分。这能够通过设置“app.subdomainOffset”来扭转。例如,如果域名是“tobi.ferrets.example.com”:
如果 app.subdomainOffset 没有设置。子域是["ferrets", "tobi"]
如果 app.subdomainOffset 是 3。子域["tobi"]。*/
get subdomains() {
const offset = this.app.subdomainOffset;
const hostname = this.hostname;
if (net.isIP(hostname)) return [];
return hostname
.split('.')
.reverse()
.slice(offset);
},
get origin() {return `${this.protocol}://${this.host}`;
},
get href() {
// support: `GET http://example.com/foo`
if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
return this.origin + this.originalUrl;
},
get path() {return parse(this.req).pathname;
},
set path(path) {const url = parse(this.req);
if (url.pathname === path) return;
url.pathname = path;
url.path = null;
this.url = stringify(url);
},
get querystring() {if (!this.req) return '';
return parse(this.req).query || '';
},
set querystring(str) {const url = parse(this.req);
if (url.search === `?${str}`) return;
url.search = str;
url.path = null;
this.url = stringify(url);
},
get query() {
const str = this.querystring;
const c = this._querycache = this._querycache || {};
return c[str] || (c[str] = qs.parse(str));
},
set query(obj) {this.querystring = qs.stringify(obj);
},
get search() {if (!this.querystring) return '';
return `?${this.querystring}`;
},
set search(str) {this.querystring = str;},
}
response
// ------ 省略其余 ------
/*
https://www.npmjs.com/package/escape-html
Escape string for use in HTML
*/
const escape = require('escape-html');
/*
https://www.npmjs.com/package/encodeurl
Encode a URL to a percent-encoded form, excluding already-encoded sequences
*/
const encodeUrl = require('encodeurl');
module.exports = {
// ------ 省略其余 ------
redirect(url, alt) {
// location
if ('back' === url) 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}.`;
},
}
相干代码已上传至 lesson6
缓存机制
### request
// ------ 省略其余 ------
/*
https://www.npmjs.com/package/fresh
HTTP 响应测试
*/
const fresh = require('fresh');
module.exports = {
// ------ 省略其余 ------
// 查看申请是否最新,Last-Modified 或者 ETag 是否匹配
get fresh() {
const method = this.method;
const s = this.ctx.status;
// GET or HEAD for weak freshness validation only
if ('GET' !== method && 'HEAD' !== method) return false;
// 2xx or 304 as per rfc2616 14.26
if ((s >= 200 && s < 300) || 304 === s) {return fresh(this.header, this.response.header);
}
return false;
},
// 查看是否旧申请,Last-Modified 或者 ETag 是否扭转
get stale() {return !this.fresh;},
}
response
// ------ 省略其余 ------
/*
https://www.npmjs.com/package/fresh
HTTP 响应测试
*/
const fresh = require('fresh');
module.exports = {
// ------ 省略其余 ------
set lastModified(val) {if ('string' === typeof val) val = new Date(val);
this.set('Last-Modified', val.toUTCString());
},
get lastModified() {const date = this.get('last-modified');
if (date) return new Date(date);
},
set etag(val) {if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
this.set('ETag', val);
},
get etag() {return this.get('ETag');
},
}
相干代码已上传至 lesson7
IP 相干
request
// ------ 省略其余 ------
const IP = Symbol('context#ip');
module.exports = {
// ------ 省略其余 ------
/*
当 app.proxy 是 true, 解析 X -Forwarded-For ip 地址列表
例如如果值是 "client, proxy1, proxy2", 会失去数组 `["client", "proxy1", "proxy2"]`
proxy2 是最远的上游
*/
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;
},
// 返回申请的近程地址, 当 app.proxy 是 true, 解析 X -Forwarded-For ip 地址列表并返回第一个
get ip() {if (!this[IP]) {this[IP] = this.ips[0] || this.socket.remoteAddress || '';
}
return this[IP];
},
set ip(_ip) {this[IP] = _ip;
},
}
相干代码已上传至 lesson8
谬误标准解决
context
// ------ 省略其余 ------
const util = require('util');
/*
Create HTTP errors for Express, Koa, Connect, etc. with ease.
https://github.com/jshttp/http-errors
*/
const createError = require('http-errors');
/*
Assert with status codes. Like ctx.throw() in Koa, but with a guard.
https://github.com/jshttp/http-assert
*/
const httpAssert = require('http-assert');
/*
https://github.com/jshttp/statuses
HTTP status utility for node.
This module provides a list of status codes and messages sourced from a few different projects:
*/
const statuses = require('statuses');
module.exports = {
// ------ 省略其余 ------
/*
The API of this module is intended to be similar to the Node.js assert module.
Each function will throw an instance of HttpError from the http-errors module when the assertion fails.
* @param {Mixed} test
* @param {Number} status
* @param {String} message
* @api public
*/
assert: httpAssert,
/*
* @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);
},
onerror(err) {
// 这里之所以没用全等, 我感觉可能是因为双等下 null == undefined 也返回 true
if (null == err) return
const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error;
// 创立一个格式化后的字符串,应用第一个参数作为一个相似 printf 的格局的字符串,该字符串能够蕴含零个或多个格局占位符。每个占位符会被对应参数转换后的值所替换
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);
// 在这里咱们做不了任何事件, 将其委托给利用层级的处理程序和日志
if (headerSent) {return;}
const {res} = this;
// 革除头字段
if (typeof res.getHeaderNames === 'function') {res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {res._headers = {}; // Node < 7.7
}
// 设置指定的
this.set(err.headers);
// 强制 text/plain
this.type = 'text';
let statusCode = err.status || err.statusCode;
// ENOENT support
if ('ENOENT' === err.code) statusCode = 404;
// default to 500
if ('number' !== typeof statusCode || !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);
}
}
相干代码已上传至 lesson9
作用域委托
咱们利用 delegates
实现不同的委托形式
getter: 内部对象能够通过该办法拜访外部对象的值。
setter:内部对象能够通过该办法设置外部对象的值。
access: 该办法蕴含 getter 和 setter 性能。
method: 该办法能够使内部对象间接调用外部对象的函数
context
咱们在这里把申请和响应的相干变量办法都委托到以后对象上, 同时减少输入格局
// ------ 省略其余 ------
/*
Node method and accessor delegation utilty.
https://github.com/tj/node-delegates
*/
const delegate = require('delegates');
// 赋值
const proto = module.exports = {
// ------ 省略其余 ------
inspect() {if (this === proto) return this;
return this.toJSON();},
// 这里咱们在每个对象上显式地调用. tojson(),否则迭代将因为 getter 而失败,并导致诸如 clone()之类的实用程序失败。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>'
};
},
}
/**
* 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');
相干代码已上传至 lesson10
利用减少谬误机制和补充上下文
Application
// ------ 省略其余 ------
const Stream = require('stream');
/*
A tiny JavaScript debugging utility modelled after Node.js core's debugging technique. Works in Node.js and web browsers.
https://github.com/visionmedia/debug
*/
const debug = require('debug')('koa:application');
/*
Is this a native generator function?
https://github.com/inspect-js/is-generator-function
*/
const isGeneratorFunction = require('is-generator-function');
/*
Convert koa legacy (0.x & 1.x) generator middleware to modern promise middleware (2.x).
https://github.com/koajs/convert
*/
const convert = require('koa-convert');
const deprecate = require('depd')('koa');
/*
Execute a callback when a HTTP request closes, finishes, or errors.
https://www.npmjs.com/package/on-finished
*/
const onFinished = require('on-finished');
/*
HTTP status utility for node.
This module provides a list of status codes and messages sourced from a few different projects:
https://www.npmjs.com/package/statuses
*/
const statuses = require('statuses');
/*
https://www.npmjs.com/package/only
返回对象的白名单属性。*/
const only = require('only');
class Koa {
// ------ 省略其余 ------
/**
* @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 {boolean} [options.proxyIpHeader] proxy ip header, default to X-Forwarded-For
* @param {boolean} [options.maxIpsCount] max ips read from proxy ip header, default to 0 (means infinity)
*/
constructor(options) {super();
// 可选项
options = options || {};
// cookies 签名
if (options.keys) this.keys = options.keys;
// 中间件队列
this.middlewares = [];
// 扩大属性
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 减少配置项
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';
}
// 启动服务器
listen(...args) {debug('listen');
// 将启动回调抽离
const server = http.createServer(this.callback())
server.listen(...args)
}
// 启动回调
callback() {
// 洋葱圈模型流程管制的外围, 上面详解
const fn = compose(this.middlewares)
// 减少监听事件
if (!this.listenerCount('error')) this.on('error', this.onerror);
return (req, res) => {
// 强制中间件从新实现新上下文
const ctx = this.createContext(req, res)
return this.hadnleRequest(ctx, fn)
}
}
// 回调申请
hadnleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
// 响应解决
const handleResoponse = () => respond(ctx)
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResoponse)
}
// 创立上下文
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
// 赋值 url
context.originalUrl = request.originalUrl = req.url
// 上下文状态
context.state = {}
return context
}
// 增加中间件
use(fn) {if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3.' +
'See the documentation for examples of how to convert old middleware' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middlewares.push(fn)
return this
}
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 (404 === err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
}
toJSON() {
return only(this, [
'subdomainOffset',
'proxy',
'env'
]);
}
inspect() {return this.toJSON();
}
}
// 响应加强
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res
let body = ctx.body
const code = ctx.status;
// 如果状态码须要一个空的主体
if (statuses.empty) {
// strip headers
ctx.body = null;
return res.end();}
if ('HEAD' === ctx.method) {if (!res.headersSent && !ctx.response.has('Content-Length')) {const { length} = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();}
// status body
if (null == body) {if (ctx.response._explicitNullBody) {ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();}
if (ctx.req.httpVersionMajor >= 2) {body = String(code);
} else {body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' === typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
body = JSON.stringify(body)
if (!res.headersSent) {ctx.length = Buffer.byteLength(body);
}
res.end(body)
}
相干代码已上传至 lesson11
继承事件机制和导出谬误类
// ------ 省略其余 ------
const Emitter = require('events');
/*
Create HTTP errors for Express, Koa, Connect, etc. with ease.
https://github.com/jshttp/http-errors
*/
const {HttpError} = require('http-errors');
module.exports = class Application extends Emitter {// ------ 省略其余 ------}
/**
* Make HttpError available to consumers of the library so that consumers don't
* have a direct dependency upon `http-errors`
*/
module.exports.HttpError = HttpError;
相干代码已上传至 lesson12
完结
咱们大略从根本起步理解整个架构流程, 而后逐步补充构造脉络, 最初联合成一个残缺的 Koa 库, 总的来说不论是代码量还是难度都不算简单, 更多是外面各种标准化解决操作, 有些细节解说不够清晰或者了解有误的话也心愿有人能够指出