0, Предисловие
Перед прочтением этой статьи необходимо иметь определенное представление о koa2 (далее Koa) и es6. Эта статья будет в следующем порядке:
- 1. Что такое коа;
- 2. Сначала прочитайте исходный код koa;
- 3. Интенсивно читать исходный код koa;
- 3.1, Интерпретация механизма промежуточного программного обеспечения
- 3.2, как преобразовать функцию генератора в асинхронную функцию
- 3.3, единый механизм обработки ошибок
- 3.4, как контекст реализует прокси для запроса и ответа
- 3.5 и другие моменты, о которых я еще не думал
Объедините исходный код для интерпретации платформы Koa. После прочтения этой статьи, помимо всестороннего понимания Koa, вы также сможете получить более глубокое понимание синтаксиса js Promise, Generator и Async.
1. Что такое коа
KOA - это обтекаемая веб-каркас, которая в основном имеет следующие вещи:
- Объект для объектов запроса и реагирования и упаковки в контекстный объект на основе их(занимает больше всего места в коде, но его легко понять)
- Механизм контейнера промежуточного программного обеспечения на основе async/await(Самое главное, самое ценное, код очень лаконичный, но сложный для понимания)
2. Сначала прочитайте исходный код koa
Структура исходного кода koa очень проста, всего 4 файла.
── lib
├── application.js
├── context.js
├── request.js
└── response.js
Ниже приведена сокращенная версия четырех файлов с аннотациями, и их чтение может дать вам общее представление о koa:
application.js
//作者注:引入第三方库,实际不仅是下面几个,列出来的几个是比较关键的
const response = require('./response');
const compose = require('koa-compose');//作者注:实现基于async/await的洋葱式调用顺序的中间件容器的关键库,下文会重点介绍
const context = require('./context');
const request = require('./request');
const Emitter = require('events');//作者注:node的基础库,koa应用集成于它,主要用了其事件机制,来实现异常的处理
const http = require('http');//作者注:node实现web服务器功能的核心库
const convert = require('koa-convert');//作者注:为了支持koa1的generator中间件写法,对于使用generator函数实现的中间件函数,需要通过koa-convert转换
module.exports = class Application extends Emitter {
constructor() {
super();
this.middleware = [];//作者注:该数组存放所有通过use函数引入的中间件函数
//作者注:创建context、request、response对象
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
//作者注:创建服务器实例
listen(...args) {
debug('listen');
//作者注:通过执行callback函数返回的函数来作为处理每次请求的回调函数
const server = http.createServer(this.callback());
return server.listen(...args);
}
/*
作者注:通过调用koa应用实例的use函数,形如:
app.use(async (ctx, next) => {
await next();
});
来加入中间件
*/
use(fn) {
//作者注:如果是generator函数,则需要通过koa-convert转换成类似类async/await函数
//其核心原理是将 Generator 函数和自动执行器,包装在一个函数里。后文会重点解释
if (isGeneratorFunction(fn)) {
fn = convert(fn);
}
//作者注:将中间件加入middleware数组中
this.middleware.push(fn);
return this;
}
//作者注:返回一个形如(req, res) => {}的函数,该函数会作为参数传递给上文listen函数中的http.createServer函数,作为处理请求的回调函数
//具体细节会在下文重点解释
callback() {
//作者注:将所有中间件函数通过koa-compose组合一下
const fn = compose(this.middleware);
//作者注:该函数会作为参数传递给上文listen函数中的http.createServer函数,
const handleRequest = (req, res) => {
//作者注:基于req和res,封装一个更强大的context对象
const ctx = this.createContext(req, res);
//作者注:当有请求过来时,需要基于办好了request和response信息的ctx和所有中间件函数,来处理请求。
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
//作者注:略
}
//作者注:基于req和res对象,创建context对象
createContext(req, res) {
//作者注:略
}
};
context.js
const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const statuses = require('statuses');
const proto = module.exports = {
//作者注:
//一些不甚重要的函数
};
/*
作者注:
在application.js的createContext函数中,
被创建的context对象上会挂载基于response.js实现的response对象和基于request.js实现的request对象,
下面两个delegate函数的作用是让context对象代理response和request的部分方法和属性
*/
delegate(proto, 'response')
.method('attachment')
...
.getter('writable');
/**
* Request delegation.
*/
var a = delegate(proto, 'request')
.method('acceptsLanguages')
...
.getter('ip');
request.js
module.exports = {
//作者注,在application.js中的createContext函数中,会把node服务器的req对象作为request对象的属性,
//request对象会基于req封装很多便利的函数和属性
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
//作者注:省略了大量类似的工具属性和方法
}
response.js
Подобно request.js, он в основном основан на объекте res сервера узла, инкапсулирующем ряд удобных функций и свойств.
3. Интенсивное чтение исходного кода koa
В конце концов, вышеизложенное является лишь беглым взглядом. В академическом духе, чтобы добраться до сути, необходимо тщательно размышлять о большом количестве деталей. Далее будут пояснены детали, которые я считаю очень важными. Грамматика имеет более глубокое понимание.
3.1, оптимизированный, но важный механизм промежуточного программного обеспечения
Промежуточное ПО в koa по сути представляет собой асинхронную функцию, которая выглядит так:
async (ctx, next) => {
await next();
}
Эта функция принимает два параметра, ctx и next, ctx — атрибут контекста приложения, который инкапсулирует req и res; следующая функция используется для передачи управления программой следующему промежуточному программному обеспечению. С помощью функции использования экземпляра приложения koa ПО промежуточного слоя можно добавить в массив ПО промежуточного слоя экземпляра koa. Когда служба узла запускается, массив промежуточного программного обеспечения организуется в объект fn с помощью функции компоновки koa-compose. Когда есть запрос на доступ, будет вызываться функция handleRequest внутри функции обратного вызова, которая в основном делает две вещи:
- Создайте объект контекста на основе req и res;
- Выполните функцию handleRequest экземпляра koa (обратите внимание на различие между двумя функциями handleRequest); функция handleRequest экземпляра koa позволяет вызывать функцию промежуточного программного обеспечения в стиле лука через ее последнюю строку кода (подробности см.:Связь). Код, соответствующий следующим трем частям, описанным выше, выглядит следующим образом:
const server = http.createServer(this.callback());
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
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);
}
Здесь важны две детали:
Во-первых, что koa-compose делает с промежуточным программным обеспечением;
Во-вторых, реализован ли вызов в стиле лука?
Ответ: Ниже приведен сокращенный исходный код koa-compose.
module.exports = compose
function compose(middleware) {
return function (context, next) {
//略
}
}
Функция compose получает массив промежуточного программного обеспечения в качестве параметра, и каждый объект в промежуточном программном обеспечении является асинхронной функцией, она возвращает функцию, которая принимает контекст и следующий в качестве входных параметров.Назовем ее fnMiddleware так же, как исходный код;
Затем запустите промежуточное ПО:
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
Здесь нужно позаботиться о реализации fnMiddleware:
return function (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
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
Прежде чем объяснять, позвольте мне сделать предположение: предположим, что добавлено два промежуточных программного обеспечения, и код выглядит следующим образом:
app.use(async (ctx,next) => {
console.log("1-start");
await next();
console.log("1-end");
});
app.use(async (ctx, next) => {
console.log("2-start");
await next();
console.log("2-end");
});
Затем проходим через:
0, работает fnMiddleware(ctx);
0, отправка (0);
0, введите функцию отправки, выполните
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
На данный момент fn является первым промежуточным программным обеспечением,Это асинхронная функция.Асинхронная функция возвращает объект Promise.Если объект Promise передается в Promise.resolve(), то Promise.resolve вернет объект Promise без каких-либо изменений..
0 введите первый промежуточный код: сначала выполните 'console.log("1-start");';
0, затем выполните 'await next();' и начните ждать завершения следующего выполнения;
1. После входа в следующую функцию она в основном выполняет dispatch(1), поэтому старая функция dispatch(0) запихивается в стек и начинает выполнять dispatch(1) с самого начала, то есть вторая промежуточная функция оплачивается на fn, а затем начинает реализацию,Этот шаг завершает передачу управления программой от первого промежуточного ПО ко второму промежуточному ПО..
2. Введите второй промежуточный код: сначала выполните console.log("2-start");', затем выполните 'await next();' и начните ждать завершения следующего выполнения;
3. После ввода следующей функции в основном выполняется диспетчеризация (2), поэтому старая функция диспетчеризации (1) помещается в стек и начинает выполнять диспетчеризацию (2) с самого начала, поскольку программа удовлетворяет следующим условиям. В настоящее время:
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
Поэтому верните Promise.resolve(), и вернется следующая функция второго промежуточного слоя.
2, поэтому затем выполните console.log("2-end");
1. Выполнение второго промежуточного программного обеспечения завершено, и право управления программой передается первому промежуточному программному обеспечению. Первое промежуточное ПО выполняет console.log("1-end");.
0 выполнение всего промежуточного программного обеспечения окончательно завершено.Если в середине нет исключения, он возвращает Promise.resolve() и выполняет обратный вызов handleResponse; если есть исключение, он возвращает Promise.reject(err) и выполняет обратный вызов при ошибке.
3.2, как преобразовать функцию генератора в асинхронную функцию
Для совместимости с предыдущей версией, если функция промежуточного программного обеспечения является функцией генератора, она будет преобразована в «асинхронную» функцию с помощью koa-convert. (Однако в третьей версии эта функция будет отменена).
Как конвертировать, эта тема до сих пор очень привлекательна.
Давайте подумаем о разнице между функцией генератора и асинхронностью? Разница лишь в том, что асинхронная функция выполняется автоматически, а генератору нужно каждый раз вызывать следующую функцию. Это все.
Таким образом, возникает вопрос: что, если вы позволите функции генератора выполняться самой?
Если не учитывать асинхронную ситуацию, то это очень просто, достаточно использовать цикл while, но если есть асинхронность, то это сложнее.
Вспомните знание генератора: каждый раз, когда выполняется следующая функция генератора, он будет возвращать объект {значение: xxx, done: false}.После возврата объекта, если следующая функция генератора может быть выполнена снова, цель автоматического выполнения генератора может быть достигнута. См. код ниже:
function * gen(){
yield new Promise((resolve,reject){
//异步函数1
if(成功){
resolve()
}else{
reject();
}
});
yield new Promise((resolve,reject){
//异步函数2
if(成功){
resolve()
}else{
reject();
}
})
}
let g = gen();
let ret = g.next();
На этом этапе ret = {value: promise instance, done: false}; На этом этапе, если вы получаете объект обещания, вы можете самостоятельно определить функцию обратного вызова для успеха/неудачи. который:
ret.value.then(()=>{
g.next();
})
Выполняет ли приведенный выше код автоматически функцию генератора после завершения выполнения первой асинхронной функции? Внимательно прочитайте приведенные выше строки кода, вы чувствуете себя немного запутанным? Вопрос в том, как сделать код более общим и позволить автоматическому выполнению идти до конца? В упрощенной версии исходного кода co:
function co(gen) {
return new Promise(function(resolve, reject) {
//1,不管三七二十一先执行onFulfilled函数
onFulfilled();
function onFulfilled(res) {
var ret;
try {
//2,第一次执行gen函数,返回一个{ value: xxx, done: false }
ret = gen.next(res);
} catch (e) {
return reject(e);
}
//3,然后执行next函数,并把ret作为入参
next(ret);
}
function onRejected(err) {
}
function next(ret) {
if (ret.done) return resolve(ret.value);
//4,按照之前的说法,如果返回的是个Promise实例,就可以根据Promise实例的then回调来继续执行generator的next方法了。所以先要把ret.value转换为一个Promise实例
var value = toPromise.call(ctx, ret.value);
//5,让成功的回调指向onFulfilled函数,其实就是又从第1步开始执行了
//这样就实现了自动执行generator
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
}
});
}
правильно! Имейте хороший вкус, это так просто.
В дополнение к использованию Promises, вы также можете использовать Trunks для достижения этой цели. Для получения дополнительной информации, пожалуйста, обратитесь к Уважаемому учителю Жуань Ифэну.статьяНе буду здесь вдаваться в подробности, идея очень похожа на Promise.Основная идея такова:
1. Выполнить функцию cb (название я взял вслепую), вернуть хэндл после вызова next,
2. Вы можете настроить функцию обратного вызова в соответствии с этим дескриптором и установить функцию обратного вызова как cb. И так далее.
3.3, единый механизм обработки ошибок
[TODO]
3.4, как контекст реализует прокси для запроса и ответа
[TODO]
3.5 и другие моменты, о которых я еще не думал
[TODO]