Играем с Koa -- Анализ принципа koa-router

Node.js JavaScript
Играем с Koa -- Анализ принципа koa-router

Введение

  Koa не связывает промежуточное ПО, чтобы упростить себе задачу. Но в реальной разработке нам приходится иметь дело со всеми типами промежуточного ПО, и в этой статье будет проанализировано часто используемое промежуточное ПО для маршрутизации — koa-router.

  Если вы не знаете принцип Коа, вы можете сначала проверить егоАнализ принципа Коа.

2. Обзор koa-router

В исходном коде   koa-router есть только два файла: router.js и layer.js, соответствующие объекту Router и объекту Layer соответственно.

Объект   Layer — это управление одним маршрутом, который содержит такую ​​информацию, как путь маршрутизации (путь), метод запроса маршрутизации (метод) и функцию выполнения маршрутизации (промежуточное ПО), а также предоставляет методы проверки маршрутизации и анализа параметров params.

  По сравнению с объектом Layer, объект Router представляет собой унифицированную обработку всех зарегистрированных маршрутов, а его API предназначен для разработчиков.

   Далее мы всесторонне проанализируем принцип реализации koa-router со следующих аспектов:

  • Реализация объекта Layer
  • регистрация маршрута
  • сопоставление маршрутов
  • Процесс выполнения маршрута

3. Слой

Объект   Layer в основном предназначен для управления одним маршрутом и является наименьшим блоком обработки во всем koa-маршрутизаторе.Обработка последующих модулей неотделима от методов на уровне, что является важной причиной для введения уровня первым.

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  // 支持路由别名
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  // 将路由执行函数保存在stack中,支持输入多个处理函数
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    // HEAD请求头部信息与GET一致,这里就一起处理了。
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD');
    }
  }, this);

  // 确保类型正确
  this.stack.forEach(function(fn) {
    var type = (typeof fn);
    if (type !== 'function') {
      throw new Error(
        methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`"
      );
    }
  }, this);

  this.path = path;
  // 1、根据路由路径生成路由正则表达式
  // 2、将params参数信息保存在paramNames数组中
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);
};

Конструктор   Layer в основном используется для инициализации пути маршрутизации, массива методов запроса маршрутизации, массива функций обработки маршрутизации, регулярного выражения маршрутизации и массива информации о параметрах params.path-to-regexpМетод генерирует регулярное выражение на основе строки пути, с помощью которого может быть реализовано сопоставление маршрутов и захват параметров params:

// 验证路由
Layer.prototype.match = function (path) {
  return this.regexp.test(path);
}

// 捕获params参数
Layer.prototype.captures = function (path) {
  // 后续会提到 对于路由级别中间件 无需捕获params
  if (this.opts.ignoreCaptures) return [];
  return path.match(this.regexp).slice(1);
}

  В соответствии с информацией о параметрах в paramNames и методе captrues можно получить пару ключ-значение параметра params текущего маршрута:

Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {};
  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) {
      var c = captures[i];
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
    }
  }
  return params;
};

Необходимо обратить внимание на метод safeDecodeURIComponent в приведенном выше коде, чтобы избежать получения сервером непредсказуемых запросов, любой контент, введенный пользователем как часть URI, должен быть экранирован с помощью encodeURIComponent, иначе при вводе контента пользователем содержит '&', '=', '?' и другие символы, могут возникнуть непредвиденные ситуации. Когда мы получаем параметры в URL-адресе, нам нужно декодировать его с помощью decodeURIComponent, а decodeURIComponent может декодировать кодировку только с помощью метода encodeURIComponent или аналогичных методов.Если метод кодирования не соответствует требованиям, decodeURIComponent выдаст ошибку URIError, поэтому автор здесь Метод обрабатывается безопасно:

function safeDecodeURIComponent(text) {
  try {
    return decodeURIComponent(text);
  } catch (e) {
    // 编码方式不符合要求,返回原字符串
    return text;
  }
}

  Layer также предоставляет метод предварительной обработки одного параметра:

Layer.prototype.param = function (param, fn) {
  var stack = this.stack;
  var params = this.paramNames;
  var middleware = function (ctx, next) {
    return fn.call(this, ctx.params[param], ctx, next);
  };
  middleware.param = param;
  var names = params.map(function (p) {
    return p.name;
  });
  var x = names.indexOf(param);
  if (x > -1) {
    stack.some(function (fn, i) {
      if (!fn.param || names.indexOf(fn.param) > x) {
        // 将单个param前置处理函数插入正确的位置
        stack.splice(i, 0, middleware);
        return true; // 跳出循环
      }
    });
  }

  return this;
};

   Причина поиска единственной функции обработки параметров с помощью метода some в приведенном выше коде заключается в следующих двух моментах:

  • Держите обработчики параметров перед другими обработчиками маршрутов;
  • В маршруте есть несколько параметров параметров, и необходимо поддерживать порядок функций обработки параметров.
Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path; // 拼接新的路由路径
    this.paramNames = [];
    // 根据新的路由路径字符串生成正则表达式
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  }
  return this;
};

Метод setPrefix в   Layer используется для установки префикса пути маршрутизации, что особенно важно при реализации вложенной маршрутизации.

   Наконец, Layer также предоставляет метод для создания URL-адресов на основе маршрутов, в основном с использованиемpath-to-regexpКомпиляция и синтаксический анализ заменяют параметр в пути маршрутизации, а в процессе объединения запроса, как упоминалось выше, паре ключ-значение необходимо выполнить утомительную операцию encodeURIComponent.urijsПредоставил краткий API для обработки.

В-четвертых, регистрация маршрута

1. Конструктор маршрутизатора

   Во-первых, давайте взглянем на конструктор Router:

function Router(opts) {
  if (!(this instanceof Router)) {
    // 限制必须采用new关键字
    return new Router(opts);
  }

  this.opts = opts || {};
  // 服务器支持的请求方法, 后续allowedMethods方法会用到
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];

  this.params = {}; // 保存param前置处理函数
  this.stack = []; // 存储layer
};

  Параметры и свойства стека, инициализированные в конструкторе, являются наиболее важными.Первые используются для сохранения функции предварительной обработки параметров, а вторые используются для сохранения созданного объекта Layer. И эти два атрибута тесно связаны с регистрацией маршрутизации, которая будет обсуждаться далее.

  koa-router предоставляет два способа регистрации маршрутов:

  • Метод регистрации конкретного HTTP-глагола, например: router.get('/users', ctx => {})
  • Поддерживаются все методы регистрации глаголов HTTP, например: router.all('/users', ctx => {})
2. HTTP МЕТОДЫ

   используется в исходном кодеmethodsМодуль получает имя метода HTTP-запроса, внутренняя реализация этого модуля в основном зависит от модуля http:

http.METHODS && http.METHODS.map(function lowerCaseMethod (method) {
  return method.toLowerCase()
})
3. router.verb() и router.all()

   Внутренняя реализация этих двух способов регистрации маршрутов в основном аналогична.В качестве примера ниже приведен исходный код router.verb():

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    // 1、处理是否传入name参数
    // 2、middleware参数支持middleware1, middleware2...的形式
    if (typeof path === 'string' || path instanceof RegExp) {
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }
    
    // 路由注册的核心处理逻辑
    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

   Первая часть этого метода — обработка входящих параметров, обработка параметров промежуточного слоя напомнит вам остальные параметры в ES6, но между остальными параметрами и аргументами есть фатальная разница:

  rest参数只包含那些没有对应形参的实参,而arguments则包含传给函数的所有实参。

   Если используется оставшийся параметр, вышеуказанная функция должна требовать от разработчика передачи параметра имени. Но вы также можете интегрировать параметры имени и пути в объект, а затем объединить остальные параметры:

Router.prototype[method] = function (options, ...middleware) {
  let { name, path } = options
  if (typeof options === 'string' || options instanceof RegExp) {
    path = options
    name = null
  }
  // ...
  return this;
};

   С новыми функциями ES6 код становится намного проще.

Вторая часть — это метод register.Форма входящего параметра метода является самой большой разницей между router.verb() и router.all(). Метод, передаваемый в router.verb(), является единственным методом, а последний — один метод, все методы HTTP-запроса передаются в виде массива, поэтому разницы между реализацией этих двух методов регистрации практически нет.

4. зарегистрироваться
Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var router = this;
  var stack = this.stack;

  // 注册路由中间件时,允许path为数组
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });
    return this;
  }

  // 实例化Layer
  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || "",
    ignoreCaptures: opts.ignoreCaptures
  });

  // 设置前缀
  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix);
  }

  // 设置param前置处理函数
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  stack.push(route);

  return route;
};

Метод   register в основном отвечает за создание экземпляров объектов Layer, обновление префиксов маршрутизации и функций обработки предварительных параметров.Эти операции упоминались в Layer, и я считаю, что все должны быть с ними знакомы.

5. использовать

  Студенты, знакомые с Koa, знают, что использование — это метод регистрации промежуточного ПО.По сравнению с глобальным промежуточным ПО в Koa, промежуточное ПО koa-router находится на уровне маршрутизации.

Router.prototype.use = function () {
  var router = this;
  var middleware = Array.prototype.slice.call(arguments);
  var path;

  // 支持多路径在于中间件可能作用于多条路由路径
  if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
    middleware[0].forEach(function (p) {
      router.use.apply(router, [p].concat(middleware.slice(1)));
    });

    return this;
  }
  // 处理路由路径参数
  var hasPath = typeof middleware[0] === 'string';
  if (hasPath) {
    path = middleware.shift();
  }

  middleware.forEach(function (m) {
    // 嵌套路由
    if (m.router) {
      // 嵌套路由扁平化处理
      m.router.stack.forEach(function (nestedLayer) {
        // 更新嵌套之后的路由路径
        if (path) nestedLayer.setPrefix(path);
        // 更新挂载到父路由上的路由路径
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);

        router.stack.push(nestedLayer);
      }); 

      // 不要忘记将父路由上的param前置处理操作 更新到新路由上。
      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key]);
        });
      }
    } else {
      // 路由级别中间件 创建一个没有method的Layer实例
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
    }
  });

  return this;
};

Метод регистрации промежуточного программного обеспечения   koa-router в основном выполняет две функции:

  • Сгладить вложенную структуру маршрутизации, которая включает обновление пути маршрутизации и вставку функции предварительной обработки параметров;
  • Промежуточное ПО уровня маршрута управляется путем регистрации экземпляра слоя без метода.

Пять, сопоставление маршрутов

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];
    if (layer.match(path)) {
      // 路由路径满足要求
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // layer.methods.length === 0 该layer为路由级别中间件
        // ~layer.methods.indexOf(method) 路由请求方法也被匹配
        matched.pathAndMethod.push(layer);
        // 仅当路由路径和路由请求方法都被满足才算是路由被匹配
        if (layer.methods.length) matched.route = true;
      }
    }
  }
  return matched;
};

Метод   match в основном фильтрует слой с помощью метода layer.match и атрибута method.Возвращаемый совпадающий объект содержит следующие части:

  • путь: сохраняет все слои, пути маршрутизации которых совпадают;
  • pathAndMethod: при условии, что путь маршрута совпадает, сохраните слой, чье промежуточное ПО уровня маршрута и метод запроса маршрута совпадают;
  • маршрут: этот маршрут сопоставляется только тогда, когда есть слой, путь маршрута которого и метод запроса маршрута совпадают.

   Кроме того, до ES7, чтобы судить о том, содержит ли массив элемент, его нужно было реализовать методом indexOf, а этот метод возвращает индекс элемента, поэтому логическое значение должно быть получено путем сравнения с -1:

  if (layer.methods.indexOf(method) > -1) {
    ...
  }

   В то время как автор ловко использует побитовые операции для сохранения "противного -1", конечно, метод include можно успешно использовать в ES7:

  if (layer.methods.includes(method)) {
    ...
  }

6. Процесс выполнения маршрутизации

   Понять концепцию маршрутизации в koa-router и способ регистрации маршрута, а затем как выполнить это в koa как промежуточном программном обеспечении.

Способ регистрации промежуточного ПО koa-router в   koa следующий:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

app
  .use(router.routes())
  .use(router.allowedMethods());

   Из кода видно, что koa-router предоставляет два промежуточных метода: route и allowMethods.

1. разрешенные методы ()
Router.prototype.allowedMethods = function (options) {
  options = options || {};
  var implemented = this.methods;

  return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};

      if (!ctx.status || ctx.status === 404) {
        ctx.matched.forEach(function (route) {
          route.methods.forEach(function (method) {
            allowed[method] = method;
          });
        });

        var allowedArr = Object.keys(allowed);

        if (!~implemented.indexOf(ctx.method)) {
          // 服务器不支持该方法的情况
          if (options.throw) {
            var notImplementedThrowable;
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented();
            } else {
              notImplementedThrowable = new HttpError.NotImplemented();
            }
            throw notImplementedThrowable;
          } else {
            // 响应 501 Not Implemented
            ctx.status = 501;
            ctx.set('Allow', allowedArr.join(', '));
          }
        } else if (allowedArr.length) {
          if (ctx.method === 'OPTIONS') {
            // 获取服务器对该路由路径支持的方法集合
            ctx.status = 200;
            ctx.body = '';
            ctx.set('Allow', allowedArr.join(', '));
          } else if (!allowed[ctx.method]) {
            if (options.throw) {
              var notAllowedThrowable;
              if (typeof options.methodNotAllowed === 'function') {
                notAllowedThrowable = options.methodNotAllowed();
              } else {
                notAllowedThrowable = new HttpError.MethodNotAllowed();
              }
              throw notAllowedThrowable;
            } else {
              // 响应 405 Method Not Allowed
              ctx.status = 405;
              ctx.set('Allow', allowedArr.join(', '));
            }
          }
        }
      }
    });
  };
};

Промежуточное ПО   allowedMethods() в основном используется для обработки запросов опций и ответа на статусы 405 и 501. ctx.matched в приведенном выше коде сохраняет именно путь в совпавшем объекте (заданный в методе route, который будет упомянут позже), при условии, что массив путей в совпавшем объекте не пуст:

  • Сервер не поддерживает текущий метод запроса и возвращает код состояния 501;
  • Текущий метод запроса — OPTIONS, и возвращается код состояния 200;
  • Слой в пути не поддерживает этот метод и возвращает статус 405;
  • Для трех вышеуказанных случаев сервер установит заголовок ответа Allow и вернет метод запроса, поддерживаемый на пути маршрутизации.
2. маршруты ()
Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;
  // 返回中间件处理函数
  var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    // 【1】为后续的allowedMethods中间件准备
    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    // 未匹配路由 直接跳过
    if (!matched.route) return next();

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    layerChain = matchedLayers.reduce(function(memo, layer) {
      // 【3】路由的前置处理中间件 主要负责将params、路由别名以及捕获数组属性挂载在ctx上下文对象中。
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);
    // 【4】利用koa中间件组织的方式,形成一个‘小洋葱’模型
    return compose(layerChain)(ctx, next);
  };

  // 【2】router属性用来use方法中区别路由级别中间件
  dispatch.router = this;
  return dispatch;
};

Промежуточное ПО   routes() в основном реализует четыре функции.

  • Смонтируйте атрибут пути совпадающего объекта в ctx.matched и предоставьте его последующему промежуточному программному обеспечению allowMethods. (См. [1] в коде)

  • Установите свойство router возвращаемой функции отправки, чтобы различать промежуточное ПО уровня маршрута и вложенные маршруты в вышеупомянутом методе Router.prototype.use. (См. [2] в коде)

  • Вставьте новое промежуточное программное обеспечение предварительной обработки маршрута и смонтируйте объект params, псевдоним маршрута и массив захвата, проанализированные слоем в контексте ctx.Точно так же Koa создает объект контекста перед обработкой запроса. (См. [3] в коде)

  • А для маршрутов, соответствующих многим слоям, koa-router обрабатывает через koa-compose, что аналогичноКак koa работает с промежуточным ПОТо же самое, поэтому koa-router — это совсем маленькая луковичная модель.

7. Резюме

Хотя koa-router является промежуточным ПО koa, он также содержит много промежуточного ПО. Эти промежуточные ПО разделены в соответствии с различными путями маршрутизации через объекты Layer, так что они больше не выполняются при каждом запросе, как промежуточное ПО koa. , а используют метод match для сопоставления соответствующее ПО промежуточного слоя для каждого запроса, а затем используйте koa-compose для формирования цепочки выполнения ПО промежуточного слоя.

   Вышеизложенное является полным содержанием принципа реализации koa-router, я надеюсь, что это поможет вам лучше понять koa-router.