Чтение источника Koa [2]-koa-router

задняя часть исходный код регулярное выражение koa

Третья статья посвящена более важному промежуточному ПО в экосистеме koa:koa-router

первый раз:чтение исходного кода koa-0
Вторая статья:чтение исходного кода koa-1-koa и koa-compose

что такое коа-маршрутизатор

Во-первых, поскольку koa — это платформа для управления промежуточным ПО, регистрация промежуточного ПО используетuseвыполнить.
Независимо от того, какой запрос, все промежуточное ПО будет выполнено один раз (если не на полпути).
Таким образом, разработчики будут очень обеспокоены: если мы хотим выполнять маршрутизацию, как мы должны писать логику?

app.use(ctx => {
  switch (ctx.url) {
    case '/':
    case '/index':
      ctx.body = 'index'
      break
    case 'list':
      ctx.body = 'list'
      break
    default:
      ctx.body = 'not found'
  }
})

Согласитесь, это простой метод, но он точно не подходит для больших проектов, где через один проходят десятки интерфейсов.switchЭто слишком сложно контролировать.
Не говоря уже о том, что запросы могут поддерживать толькоgetилиpost, и этот метод не очень хорошо поддерживает запросы с параметрами в URL/info/:uid.
существуетexpressНет такой проблемы вget,postи т. д. сMETHODОдноименная функция используется для регистрации обратного вызова:
express

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('hi there.')
})

ноkoaБыло сделано много упрощений, и большая часть логики была разделена, чтобы существовать как независимое промежуточное программное обеспечение.
так привести к многоexpressПроект перенесен вkoaКогда вам нужно установить дополнительное ПО промежуточного слоя,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('/', async ctx => {
  ctx.body = 'hi there.'
})

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

Кажется, кода действительно немного больше, ведь много логики передается из фреймворка в middleware для обработки.
Его можно рассматривать как то, что выбрано для поддержания краткой структуры коа.
Логика koa-router действительно сложнее логики koa.Вы можете представить koa как рынок, а koa-router это один из прилавков
koa нужно только обеспечить стабильную работу рынка, а реальная сделка с клиентами - это действительно koa-маршрутизатор, который устанавливает киоск внутри

Общая структура koa-router

koa-routerСтруктура не очень сложная, и она разбита на два файла:

.
├── layer.js
└── router.ja

layerВ основном для инкапсуляции некоторой информации основное дорожное полотноrouterпоставка:

File Description
layer Хранение информации: путь, МЕТОД, соответствующий обычному пути, соответствует параметрам пути, путь, соответствующий промежуточному программному обеспечению
router Основная логика: выставить функции для регистрации маршрутов, предоставить промежуточное ПО для обработки маршрутов, проверить запрошенный URL и вызвать обработку маршрута на соответствующем уровне

Работающий процесс koa-router

Возьмите базовый пример, приведенный выше, чтобы проиллюстрироватьkoa-routerЧто представляет собой процесс исполнения:

const router = new Router() // 实例化一个Router对象

// 注册一个路由的监听
router.get('/', async ctx => {
  ctx.body = 'hi there.'
})

app
  .use(router.routes()) // 将该Router对象的中间件注册到Koa实例上,后续请求的主要处理逻辑
  .use(router.allowedMethods()) // 添加针对OPTIONS的响应处理,以及一些METHOD不支持的处理

Некоторые вещи при создании экземпляра

Первый вkoa-routerПри создании экземпляра можно передать параметр элемента конфигурации в качестве информации конфигурации инициализации.
Однако этот элемент конфигурацииreadmeпросто описывается как:

Param Type Description
[opts] Object
[opts.prefix] String префиксные пути маршрутизатора

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

const Router = require('koa-router')
const router = new Router({
  prefix: '/my/awesome/prefix'
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /my/awesome/prefix/index => pong!

P.S. Но имейте в виду, что еслиprefixк/В конце регистрации маршрута можно опустить префикс/, иначе появится/повторяющийся случай

создавать экземплярRouterкод, когда:

function Router(opts) {
  if (!(this instanceof Router)) {
    return new Router(opts)
  }

  this.opts = opts || {}
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ]

  this.params = {}
  this.stack = []
}

только один видимыйmethodsНазначение , но после просмотра другого исходного кода я обнаружил, что в дополнение кprefixЕсть также некоторые параметры, которые передаются во время создания экземпляра, но неясно, почему документация не упоминает об этом:

Param Type Default Description
sensitive Boolean false Строго ли соответствовать регистру
strict Boolean false Если установленоfalseзатем соответствует следующему/не является обязательным
methods Array[String] ['HEAD','OPTIONS','GET','PUT','PATCH','POST','DELETE'] Установите МЕТОД, который может поддерживать маршрут
routerPath String null

sensitive

если установленоsensitive, маршрут будет отслеживаться по более строгим правилам сопоставления, регистр в URL не будет игнорироваться, и он будет сопоставляться точно по регистрации:

const Router = require('koa-router')
const router = new Router({
  sensitive: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index => pong!
// curl /Index => 404

strict

strictа такжеsensitiveПохожий по функциям, он также используется для установки сопоставления, чтобы сделать путь более строгим.По умолчанию путь в конце/Необязателен, если этот параметр включен, если хвост не добавляется при регистрации маршрута/, соответствующий маршрут не должен добавляться./конец:

const Router = require('koa-router')
const router = new Router({
  strict: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index  => pong!
// curl /Index  => pong!
// curl /index/ => 404

methods

methodsЗначение существования элементов конфигурации заключается в том, что если у нас есть интерфейс, который необходимо поддерживать одновременноGETа такжеPOST,router.get,router.postТакое письмо обязательно будет некрасивым.
Таким образом, мы могли бы подумать об использованииrouter.allупростить вещи:

const Router = require('koa-router')
const router = new Router()

router.all('/ping', ctx => { ctx.body = 'pong!' })

// curl -X GET  /index  => pong!
// curl -X POST /index  => pong!

Это просто идеально, он может легко удовлетворить наши потребности, но если мы поэкспериментируем с каким-то другимmethodsПозже произошли неприятные вещи:

> curl -X DELETE /index  => pong!
> curl -X PUT    /index  => pong!

Это явно не то, на что мы рассчитывали, поэтому в данном случае, исходя из текущегоkoa-routerДля достижения того, что мы хотим, необходимы следующие модификации:

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

const app = new Koa()
// 修改处1
const methods = ['GET', 'POST']
const router = new Router({
  methods
})

// 修改处2
router.all('/', async (ctx, next) => {
  // 理想情况下,这些判断应该交由中间件来完成
  if (!~methods.indexOf(ctx.method)) {
    return await next()
  }

  ctx.body = 'pong!'
})

Эти две модификации могут реализовать ожидаемую функцию:

> curl -X GET    /index  => pong!
> curl -X POST   /index  => pong!
> curl -X DELETE /index  => Not Implemented
> curl -X PUT    /index  => Not Implemented

я лично считаю что этоallowedMethodsЛогичная проблема реализации, но возможно я не дошел до автора,allowedMethodsНекоторые из наиболее важных исходных кодов:

Router.prototype.allowedMethods = function (options) {
  options = options || {}
  let implemented = this.methods

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

      // 如果进行了ctx.body赋值,必然不会执行后续的逻辑
      // 所以就需要我们自己在中间件中进行判断
      if (!ctx.status || ctx.status === 404) {
        if (!~implemented.indexOf(ctx.method)) {
          if (options.throw) {
            let notImplementedThrowable
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented() // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented()
            }
            throw notImplementedThrowable
          } else {
            ctx.status = 501
            ctx.set('Allow', allowedArr.join(', '))
          }
        } else if (allowedArr.length) {
          // ...
        }
      }
    })
  }
}

первый,allowedMethodsОн существует как промежуточное программное обеспечение поста, потому что он вызывается первым в возвращаемой функции.next, с последующимMETHOD, и одним из следствий этого является то, что если мы сделаем что-то подобное в обратном вызове маршрутаctx.body = XXX, фактически изменит запросstatusзначение, чтобы оно не стало404и не может вызвать правильноMETHODПроверьте логику.
нужен правильный триггерMETHODЛогика, надо вручную судить в мониторинге роутингаctx.methodЭто то, что мы хотим, тогда пропустим выполнение текущего промежуточного программного обеспечения.
И шаг этого суждения фактически такой же, какallowedMethodsв промежуточном программном обеспечении!~implemented.indexOf(ctx.method)Логика полностью повторяющаяся и не очень понятнаяkoa-routerПочему ты делаешь это.

Конечно,allowedMethodsОно не может существовать как предварительное промежуточное ПО, потому чтоKoaможет висеть в несколькихRouter,RouterКонфигурации могут различаться междуRouterоба и текущиеRouterобрабатываемыйMETHODэто то же самое.
Итак, личное ощущениеmethodsСуществование параметров не имеет большого смысла. .

routerPath

существование этого параметра. . Такое ощущение, что это может привести к очень странным ситуациям.
Это означает, что после регистрации промежуточного ПОrouter.routes()Операция делается:

Router.prototype.routes = Router.prototype.middleware = function () {
  let router = this
  let dispatch = function dispatch(ctx, next) {
    let path = router.opts.routerPath || ctx.routerPath || ctx.path
    let matched = router.match(path, ctx.method)
    // 如果匹配到则执行对应的中间件
    // 执行后续操作
  }
  return dispatch
}

потому что мы на самом делеkoaРегистрация — это такое промежуточное ПО, которое будет выполняться каждый раз при отправке запроса.dispatch, пока вdispatchопределить, следует ли ударить по определенномуrouter, будет использоваться этот элемент конфигурации, такое выражение:router.opts.routerPath || ctx.routerPath || ctx.path,routerПредставительRouterэкземпляр, то есть, если мы создаемRouter, если заполненоrouterPath, что приведет к тому, что любой запрос будет иметь приоритетrouterPathкак проверка маршрута:

const router = new Router({
  routerPath: '/index'
})

router.all('/index', async (ctx, next) => {
  ctx.body = 'pong!'
})
app.use(router.routes())

app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))

Если такой код есть, какой бы URL ни запрашивался, он будет считаться/indexсоответствовать:

> curl http://127.0.0.1:8888
pong!
> curl http://127.0.0.1:8888/index
pong!
> curl http://127.0.0.1:8888/whatever/path
pong!

Умелое использование routerPath для реализации функции переадресации

Точно так же этот оператор короткого замыкания имеет три выражения, второеctxявляется контекстом текущего запроса, то есть если у нас естьroutesВыполняемому промежуточному программному обеспечению также могут быть присвоены значения для изменения используемой оценки маршрутизации.URL:

const router = new Router()

router.all('/index', async (ctx, next) => {
  ctx.body = 'pong!'
})

app.use((ctx, next) => {
  ctx.routerPath = '/index' // 手动改变routerPath
  next()
})
app.use(router.routes())

app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))

Такой код также может достичь того же эффекта.
прошел в инстанцированииrouterPathНепредсказуемое, но изменение промежуточного программного обеспеченияrouterPathНайти подходящую сцену по-прежнему можно.Просто это можно понимать как реализацию переадресации.Процесс переадресации невидим для клиента.С точки зрения клиента исходный URL по-прежнему доступен, но в промежуточном программном обеспечении Изменитьctx.routerPathЛегко сопоставить маршрут туда, куда мы хотим его переслать.

// 老版本的登录逻辑处理
router.post('/login', ctx => {
  ctx.body = 'old login logic!'
})

// 新版本的登录处理逻辑
router.post('/login-v2', ctx => {
  ctx.body = 'new login logic!'
})

app.use((ctx, next) => {
  if (ctx.path === '/login') { // 匹配到旧版请求,转发到新版
    ctx.routerPath = '/login-v2' // 手动改变routerPath
  }
  next()
})
app.use(router.routes())

Это обеспечивает простую переадресацию:

> curl -X POST http://127.0.0.1:8888/login
new login logic!

Слушатели для зарегистрированных маршрутов

Все вышесказанное касается инстанцированияRouterНекоторые операции в то время, давайте поговорим о наиболее часто используемых операциях, связанных с маршрутизацией регистрации, наиболее знакомыми должны бытьrouter.get,router.postЭти действуют.
Но на самом деле это всего лишь ярлыки, внутренне вызывающиеRouterизregisterметод:

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {}

  let router = this
  let stack = this.stack

  // support array of paths
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts)
    })

    return this
  }

  // create route
  let 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)
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param])
  }, this)

  stack.push(route)

  return route
}

В комментариях метод помечен как приватный, но некоторые его параметры не отражены в разных местах кода.Призрак знает, почему эти параметры сохранены, но раз он существует, то вам нужно знать, что он делает
Это основной метод мониторинга маршрутизации.Сигнатура функции примерно следующая:

Param Type Default Description
path String/Array[String] - Один или несколько путей
methods Array[String] - Какие маршруты должен прослушивать этот маршрут?METHOD
middleware Function/Array[Function] - Массив промежуточного программного обеспечения, состоящий из функций, которые направляют фактическую вызываемую функцию обратного вызова.
opts Object {} Некоторые параметры конфигурации при регистрации маршрутов, упомянутые вышеstrict,sensitiveа такжеprefixпроявляется здесь

Как видите, функция примерно реализует этот процесс:

  1. экзаменpathЯвляется ли это массивом, если да, пройдитеitemпозвонить самому себе
  2. создать экземплярLayerобъект, установите некоторые параметры инициализации
  3. Установить обработку промежуточного ПО для определенных параметров (если есть)
  4. Поместите созданный объект вstackсредний накопитель

Итак, прежде чем вводить эти параметры, кратко опишитеLayerКонструктор необходим:

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {}
  this.name = this.opts.name || null
  this.methods = []
  this.paramNames = []
  this.stack = Array.isArray(middleware) ? middleware : [middleware]

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD')
    }
  }, this)

  // ensure middleware is a function
  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
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

Уровень отвечает за хранение информации о мониторинге маршрута, URL-адреса каждый раз, когда маршрут регистрируется, регулярного выражения, сгенерированного URL-адресом, параметров, существующих в URL-адресе, и промежуточного программного обеспечения, соответствующего маршруту.
все сданоLayerДля сохранения основное внимание уделяется параметрам массива в процессе создания экземпляра:

  • methods
  • paramNames
  • stack

methodsТо, что хранится, является действительным, соответствующим мониторингу маршрутаMETHOD, и будет нацелен во время создания экземпляраMETHODВыполните преобразование регистра.
paramNamesИз-за используемого плагина это выглядит не так однозначно, но на самом делеpathToRegExpвнутреннее собраниеparamNamesэтот массивpushоперации, может быть удобнее смотреть на это такpathToRegExp(path, &this.paramNames, this.opts), в сплайсингеhashЭтот массив используется, когда параметр пути структуры
stackСохраняется функция промежуточного программного обеспечения, соответствующая прослушивателю маршрута,router.middlewareЧасть логики будет зависеть от этого массива

path

Логика обработки в заголовке функции в основном предназначена для поддержки одновременной регистрации нескольких путей.Если найден первыйpathПосле того, как параметр является массивом, он будет проходитьpathпараметры для вызова самого себя.
Итак, для несколькихURLТа же самая маршрутизация может быть обработана следующим образом:

router.register(['/', ['/path1', ['/path2', 'path3']]], ['GET'], ctx => {
  ctx.body = 'hi there.'
})

Это точно действительная установка:

> curl http://127.0.0.1:8888/
hi there.
> curl http://127.0.0.1:8888/path1
hi there.
> curl http://127.0.0.1:8888/path3
hi there.

methods

И оmethodsпараметр, он считается массивом по умолчанию, даже если он слушает только одинMETHODВам также необходимо передать массив в качестве параметра, если это пустой массив, даже еслиURLсовпадение, оно также будет пропущено напрямую, и будет выполнено следующее промежуточное ПО, которое будет выполнено в последующемrouter.routesупоминается в

middleware

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

P.S. вkoa-router, также предоставляет метод, называемыйrouter.use, это зарегистрируетrouterПО промежуточного слоя экземпляра

opts

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

Param Type Default Description
name String - Установите маршрут, соответствующийname,имяrouter
prefix String - Очень безвкусные параметры, совершенно бесполезные, вроде задает префикс маршрута, но на самом деле бесполезно
sensitive Boolean false Следует ли строго соответствовать регистру, переопределить создание экземпляраRouterконфигурация в
strict Boolean false Следует ли строго соблюдать регистр, если установлено значениеfalseзатем соответствует следующему/не является обязательным
end Boolean true Соответствует ли путь концу полного URL-адреса
ignoreCaptures Boolean - Следует ли игнорировать группы захвата в маршруте, соответствующем обычным результатам.
name

прежде всегоname, в основном используется в этих местах:

  1. Более удобное позиционирование при возникновении исключения
  2. в состоянии пройтиrouter.url(<name>),router.route(<name>)получить соответствующийrouterИнформация
  3. Когда промежуточное ПО выполняется,nameбудет зажатctx.routerNameсередина
router.register('/test1', ['GET'], _ => {}, {
  name: 'module'
})

router.register('/test2', ['GET'], _ => {}, {
  name: 'module'
})

console.log(router.url('module') === '/test1') // true

try {
  router.register('/test2', ['GET'], null, {
    name: 'error-module'
  })
} catch (e) {
  console.error(e) // Error: GET `error-module`: `middleware` must be a function, not `object`
}

если несколькоrouterиспользуйте то же имя, затем передайтеrouter.urlВызов возвращает тот, который был зарегистрирован первым:

// route用来获取命名路由
Router.prototype.route = function (name) {
  var routes = this.stack

  for (var len = routes.length, i=0; i<len; i++) {
    if (routes[i].name && routes[i].name === name) {
      return routes[i] // 匹配到第一个就直接返回了
    }
  }

  return false
}

// url获取该路由对应的URL,并使用传入的参数来生成真实的URL
Router.prototype.url = function (name, params) {
  var route = this.route(name)

  if (route) {
    var args = Array.prototype.slice.call(arguments, 1)
    return route.url.apply(route, args)
  }

  return new Error('No route found for name: ' + name)
}
Отвлекитесь, чтобы поговорить об этих вещах о router.url

Если в проекте вы хотите настроить таргетинг на некоторыеURLпрыгать, использоватьrouter.urlгенерироватьpathхороший выбор:

router.register(
  '/list/:id', ['GET'], ctx => {
    ctx.body = `Hi ${ctx.params.id}, query: ${ctx.querystring}`
  }, {
    name: 'list'
  }
)

router.register('/', ['GET'], ctx => {
  // /list/1?name=Niko
  ctx.redirect(
    router.url('list', { id: 1 }, { query: { name: 'Niko' } })
  )
})

// curl -L http://127.0.0.1:8888 => Hi 1, query: name=Niko

можно увидеть,router.urlна самом деле звонитLayerпримерurlметод, который в основном используется для обработки некоторых параметров, переданных при генерации.
Адрес источника:layer.js#L116
Функция принимает два параметра,paramsа такжеoptions, потому что самLayerЭкземпляр хранит соответствующийpathинформация, такая какparamsЭто замена некоторых параметров, хранящихся в пути,optionsВ текущем коде есть только одинqueryполе, используемое для сращиванияsearchДанные позади:

const Layer = require('koa-router/lib/layer')
const layer = new Layer('/list/:id/info/:name', [], [_ => {}])

console.log(layer.url({ id: 123, name: 'Niko' }))
console.log(layer.url([123, 'Niko']))
console.log(layer.url(123, 'Niko'))
console.log(
  layer.url(123, 'Niko', {
    query: {
      arg1: 1,
      arg2: 2
    }
  })
)

Все вышеперечисленные методы вызова допустимы, и в исходном коде есть соответствующая обработка. Первый — оценка нескольких параметров.paramsНе одинobject, будем считать, что черезlayer.url(参数, 参数, 参数, opts)называется так.
преобразовать его вlayer.url([参数, 参数], opts)Форма.
Логика в настоящее время должна иметь дело только с тремя случаями:

  1. Подстановка параметров в виде массива
  2. hashподстановка параметров формы
  3. нет параметров

Эта замена параметра относится кURLпройдетсторонние библиотекиИспользуется для обработки части параметра ссылки, т.е./:XXXэту часть, затем перейдите вhashРеализовать операции, аналогичные замене шаблона:

// 可以简单的认为是这样的操作:
let hash = { id: 123, name: 'Niko' }
'/list/:id/:name'.replace(/(?:\/:)(\w+)/g, (_, $1) => `/${hash[$1]}`)

потомlayer.urlОбработка заключается в генерации различных параметров, таких какhashТакая структура со временем заменяетhashполучить полныйURL.

prefix

экземпляр вышеLayerв процессе появленияopts.prefixВес выше, но тогда есть логика суждения для вызова нижеsetPrefixПереназначение, перерыв весь исходный код, я обнаружил, что разница только в том, что будетdebugЗаявка на регистрациюrouterвходящийprefix, и везде будет создан экземплярRouterвремяprefixПокрытый.

И если вы хотите направить правильное приложениеprefix, вам нужно позвонитьsetPrefix,Потому чтоLayerВо время создания оpathХранилище от дальнего входящегоpathпараметр.
при подаче заявленияprefixПрефикс необходимо активировать вручнуюsetPrefix:

// Layer实例化的操作
function Layer(path, methods, middleware, opts) {
  // 省略不相干操作
  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

// 只有调用setPrefix才会应用前缀
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
}

Это отражено в нескольких методах, предоставляемых пользователям, подобныхget,setтак же какuse.
Конечно, в документе также предусмотрено, что вы можете напрямую установить всеrouterпрефиксный метод,router.prefix: В документации просто сказано, что вы можете установить префикс,prefixВнутренне он вызовет всеlayer.setPrefix:

router.prefix('/things/:thing_id')

Но глядя наlayer.setPrefixПосле исходников я узнал, что тут на самом деле темная яма.
потому чтоsetPrefixРеализация заключается в том, чтобы получитьprefixпараметры, сращенные с текущимиpathголова.
Это создает проблему, если мы вызываем несколько разsetPrefixвызовет несколькоprefixНаложение, а не замена:

router.register('/index', ['GET'], ctx => {
  ctx.body = 'hi there.'
})

router.prefix('/path1')
router.prefix('/path2')

// > curl http://127.0.0.1:8888/path2/path1/index
// hi there.

Префиксный метод накладывает префикс вместо переопределения префикса.

чувствительный и строгий

Об этих двух параметрах сказать нечего, т. е. они переопределяют конкретизацию.RouterДва параметра, передаваемые одновременно, имеют одинаковый эффект.

end

endочень интересный параметр, это вkoa-routerЭто отражено в других модулях, упомянутых в,path-to-regexp:

if (end) {
  if (!strict) route += '(?:' + delimiter + ')?'

  route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'
} else {
  if (!strict) route += '(?:' + delimiter + '(?=' + endsWith + '))?'
  if (!isEndDelimited) route += '(?=' + delimiter + '|' + endsWith + ')'
}

return new RegExp('^' + route, flags(options))

endWithЭто может быть просто понято как регулярное$, то есть конец матча.
Глядя на логику кода, грубо говоря, если установленоend: true, то он в любом случае будет добавлен в конце$Обозначает конец матча.
и еслиend: false, то только если установить одновременноstrict: falseилиisEndDelimited: falseбудет запущен.
Таким образом, мы можем добиться нечеткого сопоставления URL-адресов с помощью этих двух параметров:

router.register(
  '/list', ['GET'], ctx => {
    ctx.body = 'hi there.'
  }, {
    end: false,
    strict: true
  }
)

То есть окончательное регулярное выражение, сгенерированное вышеприведенным кодом для сопоставления маршрутов, вероятно, будет таким:

/^\/list(?=\/|$)/i

// 可以通过下述代码获取到正则
require('path-to-regexp').tokensToRegExp('/list/', {end: false, strict: true})

окончание$является необязательным, что заставляет нас просто отправлять любые/listЗапрос будет получен этим промежуточным ПО.

ignoreCaptures

ignoreCapturesПараметр используется, чтобы указать, следует ли возвращатьURLв соответствующем параметре пути к промежуточному программному обеспечению.
и если установленоignoreCapturesЭти два параметра станут пустыми объектами:

router.register('/list/:id', ['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // ['1'], { id: '1' }
})

// > curl /list/1

router.register('/list/:id', ['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // [ ], {  }
}, {
  ignoreCaptures: true
})
// > curl /list/1

Это вызывается во время выполнения промежуточного программного обеспечения изlayerполучен двумя способами.
первый звонокcapturesПолучить все параметры, если они установленыignoreCapturesЭто приведет к тому, что пустой массив будет возвращен напрямую.
тогда позвониparamsПередайте все параметры, сгенерированные при регистрации маршрута, и фактические значения параметров, а затем сгенерируйте полныйhashвводить вctxВ объекте:

// 中间件的逻辑
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
// 中间件的逻辑 end

// layer提供的方法
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []
  return path.match(this.regexp).slice(1)
}

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
}

// 所做的事情大致如下:
// [18, 'Niko'] + ['age', 'name']
// =>
// { age: 18, name: 'Niko' }

Роль router.param

Выше было описание некоторых параметров при регистрации маршрутов, вы можете увидеть вregisterэкземпляр вLayerВедь объект не ставится прямоstackВ середине он будет только выталкиваться в этой операции.stack:

Object.keys(this.params).forEach(function (param) {
  route.param(param, this.params[param])
}, this)

stack.push(route) // 装载

Здесь используется для добавленияURLАргументы, обрабатываемые промежуточным программным обеспечением, сrouter.paramЭти два тесно связаны:

Router.prototype.param = function (param, middleware) {
  this.params[param] = middleware
  this.stack.forEach(function (route) {
    route.param(param, middleware)
  })
  return this
}

Эти две операции похожи, первая используется для добавления всех новых прослушивателей маршрута.paramПромежуточное ПО, а последнее добавляется ко всем существующим маршрутамparamпромежуточное ПО.
Потому чтоrouter.paramимеет вthis.params[param] = XXXоперация присваивания.
Таким образом, при последующем мониторинге нового маршрута зацикливайте напрямуюthis.paramsВы можете получить все промежуточное ПО.

router.paramОперации также описаны в документе,адрес документа
Грубо говоря, его можно использовать для выполнения некоторых операций, таких как проверка параметров, но поскольку вlayer.paramесть немногоспециальная обработка, так что нам не о чем беспокоитьсяparamпорядок исполнения,layerбудет гарантироватьparamДолжен выполняться перед промежуточным программным обеспечением, которое зависит от этого параметра:

router.register('/list/:id', ['GET'], (ctx, next) => {
  ctx.body = `hello: ${ctx.name}`
})

router.param('id', (param, ctx, next) => {
  console.log(`got id: ${param}`)
  ctx.name = 'Niko'
  next()
})

router.param('id', (param, ctx, next) => {
  console.log('param2')
  next()
})


// > curl /list/1
// got id: 1
// param2
// hello: Niko

Наиболее часто используемые ярлыки, такие как get/post

И закончил основной метод вышеregister, мы можем взглянуть на несколько представленных разработчикамrouter.verbметод:

// get|put|post|patch|delete|del
// 循环注册多个METHOD的快捷方式
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    let middleware

    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
  }
})

Router.prototype.del = Router.prototype['delete'] // 以及最后的一个别名处理,因为del并不是有效的METHOD

Это разочаровывает,verbметод будет многоoptsПараметры обрезаны, оставлены только значения по умолчаниюnameполе.
Это просто очень простой процесс именованияnameЛогика, связанная с маршрутизацией, а затем вызовregisterЗавершите операцию.

router.use — промежуточное ПО внутри маршрутизатора

И упоминалось вышеrouter.useЕго можно использовать для регистрации использования промежуточного программного обеспечения.useПромежуточное ПО для регистрации делится на два случая:

  1. Обычная промежуточная функция
  2. поставить существующийrouterЭкземпляры передаются как промежуточное ПО
обычное использование

вотuseКлючевой код метода:

Router.prototype.use = function () {
  var router = this
  middleware.forEach(function (m) {
    if (m.router) { // 这里是通过`router.routes()`传递进来的
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path)
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) // 调用`use`的Router实例的`prefix`
        router.stack.push(nestedLayer)
      })

      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key])
        })
      }
    } else { // 普通的中间件注册
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath })
    }
  })
}

// 在routes方法有这样的一步操作
Router.prototype.routes = Router.prototype.middleware = function () {
  function dispatch() {
    // ...
  }

  dispatch.router = this // 将router实例赋值给了返回的函数

  return dispatch
}

Первый - более традиционный способ, передача функции, необязательнаяpath, чтобы зарегистрировать промежуточное ПО.
Однако следует отметить, что.use('path')Для такого использования промежуточное ПО не может существовать независимо, и должен быть прослушиватель маршрута, который может соответствовать его пути:

router.use('/list', ctx => {
  // 如果只有这么一个中间件,无论如何也不会执行的
})

// 必须要存在相同路径的`register`回调
router.get('/list', ctx => { })

app.use(router.routes())

Причина в следующем:

  1. .useа также.getоснованы на.registerдобиться, но.useсуществуетmethodsПереданный параметр является пустым массивом
  2. Когда путь совпадает, все подходящее промежуточное ПО будет удалено, а затем соответствующийmethods,еслиlength !== 0Это пометит группу для текущего матчаflag
  3. Прежде чем выполнять промежуточное ПО, оно сначала определит, есть ли этоflag, если нет, значит не все мидлвары в этом пути установленыMETHOD, он сразу перейдет к другим процессам (например, разрешенный метод)
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)) {
        matched.pathAndMethod.push(layer)

        // 只有在发现不为空的`methods`以后才会设置`flag`
        if (layer.methods.length) matched.route = true
      }
    }
  }

  return matched
}

// 以及在`routes`中有这样的操作
Router.prototype.routes = Router.prototype.middleware = function () {
  function dispatch(ctx, next) {

    // 如果没有`flag`,直接跳过
    if (!matched.route) return next()
  }

  return dispatch
}
Передать в другие экземпляры маршрутизатора

Вы можете увидеть это, если вы выберетеrouter.routes()Чтобы мультиплексировать промежуточное ПО, вы пройдете все маршруты этого экземпляра, а затем установитеprefix.
и будет измененоlayerвыкатиться на текущийrouterсередина.
Итак, теперь мы должны обратить внимание, это уже было упомянуто выше,LayerизsetPrefixПрошита, не обтянута.
а такжеuseбудет работатьlayerобъект, поэтому такое использование также приведет к изменению предыдущего пути к промежуточному ПО.
И если вы пройдете вuseПромежуточное ПО уже зарегистрировано вkoaприведет к тому, что одно и то же промежуточное ПО будет выполняться дважды (если есть звонокnextесли):

const middlewareRouter = new Router()
const routerPage1 = new Router({
  prefix: '/page1'
})

const routerPage2 = new Router({
  prefix: '/page2'
})

middlewareRouter.get('/list/:id', async (ctx, next) => {
  console.log('trigger middleware')
  ctx.body = `hi there.`
  await next()
})

routerPage1.use(middlewareRouter.routes())
routerPage2.use(middlewareRouter.routes())

app.use(middlewareRouter.routes())
app.use(routerPage1.routes())
app.use(routerPage2.routes())

Как и в приведенном выше коде, на самом деле есть две проблемы:

  1. Окончательный допустимый путь доступа:/page2/page1/list/1,потому чтоprefixБудет прошит вместо чехла
  2. Когда мы вызываем промежуточное ПОnextпосле,console.logбудет выведено три раза подряд, потому что всеroutesна самом деле все динамическиеprefixбыли изменены на/page2/page1

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

обработка запросов

И, наконец, дошел до последнего шага, когда пришел запрос,Routerкак это обрабатывается.
ОдинRouterЭкземпляры могут создавать два промежуточных ПО, зарегистрированных наkoaначальство:

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

routesОтвечает за основную логику.
allowedMethodsотвечает за предоставление постаMETHODПроверьте промежуточное ПО.

allowedMethodsНечего сказать, основано на текущем запросеmethodВыполняются некоторые проверки и возвращаются некоторые сообщения об ошибках.
И многие из представленных выше методов на самом деле предназначены для окончательногоroutesСлужить:

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

    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) {
      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)
    }, [])

    return compose(layerChain)(ctx, next)
  };

  dispatch.router = this

  return dispatch
}

Сначала вы можете увидеть, чтоkoa-routerТакже предоставляет псевдонимmiddlewareдля достижения той же функции.
И вызов функции в конечном итоге вернет функцию промежуточного программного обеспечения, эта функция действительно привязана кkoaВверх.
koaПромежуточное ПО является чистым промежуточным ПО, а включенное промежуточное ПО выполняется независимо от запроса.
Поэтому не рекомендуется использоватьprefixпри создании несколькихRouterэкземпляр, что приводит кkoaСмонтировать несколько наdispatchИспользуется для проверки того, соответствует ли URL-адрес правилам

После входа в промежуточное программное обеспечение будет оцениваться URL-адрес, для чего мы упоминали выше.forawardгде это реализовано.
Соответствующий вызовrouter.matchметод, хотя кажется, что заданиеmatched.path, а на самом делеmatchВ реализации метода все они сопоставлены.LayerПример:

Router.prototype.match = function (path, method) {
  var layers = this.stack // 这个就是获取的Router实例中所有的中间件对应的layer对象
  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)) {
        // 将有效的中间件推入
        matched.pathAndMethod.push(layer)

        // 判断是否存在METHOD
        if (layer.methods.length) matched.route = true
      }
    }
  }

  return matched
}

// 一个简单的正则匹配
Layer.prototype.match = function (path) {
  return this.regexp.test(path)
}

И причина, по которой он существует, состоит в том, чтобы судить о том, существует лиctx.matchedвместо того, чтобы присваивать значение непосредственно этому свойству.
Это связано с тем, что, как было сказано выше,koaЭкземпляры могут регистрировать несколькоkoa-routerпример.
Это приводит кrouterПосле выполнения промежуточного программного обеспечения экземпляра могут быть другие последующие действия.routerэкземпляр также попал вURL, но это гарантируетmatchedОн всегда накапливается, а не перезаписывается каждый раз.

pathа такжеpathAndMethodобеmatchДва возвращенных массива, разница между ними в том, чтоpathВозвращаются данные, которые успешно соответствуют URL-адресу, иpathAndMethodЭто данные, которые соответствуют URL-адресу и соответствуют МЕТОДУ.

const router1 = new Router()
const router2 = new Router()

router1.post('/', _ => {})

router1.get('/', async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router1, matched length: ${ctx.matched.length}`)
  await next()
})

router2.get('/', async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router2, matched length: ${ctx.matched.length}`)
  await next()
})

app.use(router1.routes())
app.use(router2.routes())

// >  curl http://127.0.0.1:8888/
// => trigger router1, matched length: 2
// => trigger router2, matched length: 3

Что касается выполнения промежуточного программного обеспечения, вkoa-routerтакже используется вkoa-composeЧтобы объединить лук:

var matchedLayers = matched.pathAndMethod

layerChain = matchedLayers.reduce(function(memo, layer) {
  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)
}, [])

return compose(layerChain)(ctx, next)

Этот фрагмент кода будет предварять все подходящие промежуточные программы символомctxПромежуточная операция назначения атрибута, т.е.reduceВыполнение луковой модели сделает количество функций промежуточного программного обеспечения, соответствующих луковой модели, не менееX2.
Слой может содержать несколько промежуточных программ, не забывайтеmiddleware, именно поэтомуreduceиспользуется вconcatвместоpush
Поскольку перед выполнением каждого промежуточного программного обеспечения изменитеctxЭто некоторая информация, когда промежуточное программное обеспечение срабатывает.
Включая совпадающие параметры URL, а также текущее промежуточное ПО.nameинформация, такая как.

[
  layer1[0], // 第一个register中对应的中间件1
  layer1[1], // 第一个register中对应的中间件2
  layer2[0]  // 第二个register中对应的中间件1
]

// =>

[
  (ctx, next) => {
    ctx.params = layer1.params // 第一个register对应信息的赋值  
    return next()
  },
  layer1[0], // 第一个register中对应的中间件1
  layer1[1], // 第一个register中对应的中间件2
  (ctx, next) => {
    ctx.params = layer2.params // 第二个register对应信息的赋值  
    return next()
  },
  layer2[0]  // 第二个register中对应的中间件1
]

существуетroutesНаконец, он вызоветkoa-composeобъединитьreduceСгенерированный массив промежуточного программного обеспечения, а такжеkoa-composeВторые необязательные параметры, упомянутые в середине, используются для выполнения конечного процесса обратного вызова после выполнения лука.


Примечания

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

  • Почему реализованная в коде функция не отражена в документе?
  • Если в документации не сказано, что это можно использовать таким образом, то зачем в коде должна быть соответствующая реализация?

Два простейших доказательства:

  1. может быть изменен с помощьюctx.routerPathреализоватьforwardфункция, но она не скажет вам в документации
  2. в состоянии пройтиrouter.register(path, ['GET', 'POST'])быстро контролировать несколькоMETHOD,ноregisterотмечен как@private

Использованная литература:

Расположение примера кода в репозитории:learning-koa-router