Введение
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.