Это может быть более искренняя интерпретация исходного кода Koa2 на рынке.

Node.js JavaScript koa ECMAScript 6

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]