mirror of
https://github.com/honojs/hono.git
synced 2024-11-24 19:26:56 +01:00
perf(router): improve performance of router (#3526)
* perf(router): improve performance of router * coverage
This commit is contained in:
parent
234b083777
commit
10a5e65e85
@ -49,4 +49,17 @@ describe('LinearRouter', () => {
|
||||
expect(res[0][0]).toBe('GET /book')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Skip part', () => {
|
||||
const router = new LinearRouter<string>()
|
||||
|
||||
beforeEach(() => {
|
||||
router.add('GET', '/products/:id{d+}', 'GET /products/:id{d+}')
|
||||
})
|
||||
|
||||
it('GET /products/list', () => {
|
||||
const [res] = router.match('GET', '/products/list')
|
||||
expect(res.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -13,117 +13,123 @@ export class LinearRouter<T> implements Router<T> {
|
||||
routes: [string, string, T][] = []
|
||||
|
||||
add(method: string, path: string, handler: T) {
|
||||
;(checkOptionalParameter(path) || [path]).forEach((p) => {
|
||||
this.routes.push([method, p, handler])
|
||||
})
|
||||
for (
|
||||
let i = 0, paths = checkOptionalParameter(path) || [path], len = paths.length;
|
||||
i < len;
|
||||
i++
|
||||
) {
|
||||
this.routes.push([method, paths[i], handler])
|
||||
}
|
||||
}
|
||||
|
||||
match(method: string, path: string): Result<T> {
|
||||
const handlers: [T, Params][] = []
|
||||
ROUTES_LOOP: for (let i = 0, len = this.routes.length; i < len; i++) {
|
||||
const [routeMethod, routePath, handler] = this.routes[i]
|
||||
if (routeMethod !== method && routeMethod !== METHOD_NAME_ALL) {
|
||||
continue
|
||||
}
|
||||
if (routePath === '*' || routePath === '/*') {
|
||||
handlers.push([handler, emptyParams])
|
||||
continue
|
||||
}
|
||||
|
||||
const hasStar = routePath.indexOf('*') !== -1
|
||||
const hasLabel = routePath.indexOf(':') !== -1
|
||||
if (!hasStar && !hasLabel) {
|
||||
if (routePath === path || routePath + '/' === path) {
|
||||
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
|
||||
if (routePath === '*' || routePath === '/*') {
|
||||
handlers.push([handler, emptyParams])
|
||||
continue
|
||||
}
|
||||
} else if (hasStar && !hasLabel) {
|
||||
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
|
||||
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)
|
||||
|
||||
const lastIndex = parts.length - 1
|
||||
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
|
||||
const part = parts[j]
|
||||
const index = path.indexOf(part, pos)
|
||||
if (index !== pos) {
|
||||
continue ROUTES_LOOP
|
||||
const hasStar = routePath.indexOf('*') !== -1
|
||||
const hasLabel = routePath.indexOf(':') !== -1
|
||||
if (!hasStar && !hasLabel) {
|
||||
if (routePath === path || routePath + '/' === path) {
|
||||
handlers.push([handler, emptyParams])
|
||||
}
|
||||
pos += part.length
|
||||
if (j === lastIndex) {
|
||||
if (
|
||||
!endsWithStar &&
|
||||
pos !== path.length &&
|
||||
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
|
||||
) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
} else {
|
||||
const index = path.indexOf('/', pos)
|
||||
if (index === -1) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
pos = index
|
||||
}
|
||||
}
|
||||
handlers.push([handler, emptyParams])
|
||||
} else if (hasLabel && !hasStar) {
|
||||
const params: Record<string, string> = Object.create(null)
|
||||
const parts = routePath.match(splitPathRe) as string[]
|
||||
} else if (hasStar && !hasLabel) {
|
||||
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
|
||||
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)
|
||||
|
||||
const lastIndex = parts.length - 1
|
||||
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
|
||||
if (pos === -1 || pos >= path.length) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
|
||||
const part = parts[j]
|
||||
if (part.charCodeAt(1) === 58) {
|
||||
// /:label
|
||||
let name = part.slice(2)
|
||||
let value
|
||||
|
||||
if (name.charCodeAt(name.length - 1) === 125) {
|
||||
// :label{pattern}
|
||||
const openBracePos = name.indexOf('{')
|
||||
const pattern = name.slice(openBracePos + 1, -1)
|
||||
const restPath = path.slice(pos + 1)
|
||||
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
|
||||
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
name = name.slice(0, openBracePos)
|
||||
value = restPath.slice(...match.indices[0])
|
||||
pos += match.indices[0][1] + 1
|
||||
} else {
|
||||
let endValuePos = path.indexOf('/', pos + 1)
|
||||
if (endValuePos === -1) {
|
||||
if (pos + 1 === path.length) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
endValuePos = path.length
|
||||
}
|
||||
value = path.slice(pos + 1, endValuePos)
|
||||
pos = endValuePos
|
||||
}
|
||||
|
||||
params[name] ||= value as string
|
||||
} else {
|
||||
const lastIndex = parts.length - 1
|
||||
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
|
||||
const part = parts[j]
|
||||
const index = path.indexOf(part, pos)
|
||||
if (index !== pos) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
pos += part.length
|
||||
}
|
||||
|
||||
if (j === lastIndex) {
|
||||
if (pos !== path.length && !(pos === path.length - 1 && path.charCodeAt(pos) === 47)) {
|
||||
continue ROUTES_LOOP
|
||||
if (j === lastIndex) {
|
||||
if (
|
||||
!endsWithStar &&
|
||||
pos !== path.length &&
|
||||
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
|
||||
) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
} else {
|
||||
const index = path.indexOf('/', pos)
|
||||
if (index === -1) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
pos = index
|
||||
}
|
||||
}
|
||||
}
|
||||
handlers.push([handler, emptyParams])
|
||||
} else if (hasLabel && !hasStar) {
|
||||
const params: Record<string, string> = Object.create(null)
|
||||
const parts = routePath.match(splitPathRe) as string[]
|
||||
|
||||
handlers.push([handler, params])
|
||||
} else if (hasLabel && hasStar) {
|
||||
throw new UnsupportedPathError()
|
||||
const lastIndex = parts.length - 1
|
||||
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
|
||||
if (pos === -1 || pos >= path.length) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
|
||||
const part = parts[j]
|
||||
if (part.charCodeAt(1) === 58) {
|
||||
// /:label
|
||||
let name = part.slice(2)
|
||||
let value
|
||||
|
||||
if (name.charCodeAt(name.length - 1) === 125) {
|
||||
// :label{pattern}
|
||||
const openBracePos = name.indexOf('{')
|
||||
const pattern = name.slice(openBracePos + 1, -1)
|
||||
const restPath = path.slice(pos + 1)
|
||||
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
|
||||
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
name = name.slice(0, openBracePos)
|
||||
value = restPath.slice(...match.indices[0])
|
||||
pos += match.indices[0][1] + 1
|
||||
} else {
|
||||
let endValuePos = path.indexOf('/', pos + 1)
|
||||
if (endValuePos === -1) {
|
||||
if (pos + 1 === path.length) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
endValuePos = path.length
|
||||
}
|
||||
value = path.slice(pos + 1, endValuePos)
|
||||
pos = endValuePos
|
||||
}
|
||||
|
||||
params[name] ||= value as string
|
||||
} else {
|
||||
const index = path.indexOf(part, pos)
|
||||
if (index !== pos) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
pos += part.length
|
||||
}
|
||||
|
||||
if (j === lastIndex) {
|
||||
if (
|
||||
pos !== path.length &&
|
||||
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
|
||||
) {
|
||||
continue ROUTES_LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlers.push([handler, params])
|
||||
} else if (hasLabel && hasStar) {
|
||||
throw new UnsupportedPathError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,10 @@ export class PatternRouter<T> implements Router<T> {
|
||||
match(method: string, path: string): Result<T> {
|
||||
const handlers: [T, Params][] = []
|
||||
|
||||
for (const [pattern, routeMethod, handler] of this.routes) {
|
||||
if (routeMethod === METHOD_NAME_ALL || routeMethod === method) {
|
||||
for (let i = 0, len = this.routes.length; i < len; i++) {
|
||||
const [pattern, routeMethod, handler] = this.routes[i]
|
||||
|
||||
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
|
||||
const match = pattern.exec(path)
|
||||
if (match) {
|
||||
handlers.push([handler, match.groups || Object.create(null)])
|
||||
|
@ -30,9 +30,9 @@ export class SmartRouter<T> implements Router<T> {
|
||||
for (; i < len; i++) {
|
||||
const router = routers[i]
|
||||
try {
|
||||
routes.forEach((args) => {
|
||||
router.add(...args)
|
||||
})
|
||||
for (let i = 0, len = routes.length; i < len; i++) {
|
||||
router.add(...routes[i])
|
||||
}
|
||||
res = router.match(method, path)
|
||||
} catch (e) {
|
||||
if (e instanceof UnsupportedPathError) {
|
||||
|
@ -7,7 +7,6 @@ type HandlerSet<T> = {
|
||||
handler: T
|
||||
possibleKeys: string[]
|
||||
score: number
|
||||
name: string // For debug
|
||||
}
|
||||
|
||||
type HandlerParamsSet<T> = HandlerSet<T> & {
|
||||
@ -20,23 +19,20 @@ export class Node<T> {
|
||||
children: Record<string, Node<T>>
|
||||
patterns: Pattern[]
|
||||
order: number = 0
|
||||
name: string
|
||||
params: Record<string, string> = Object.create(null)
|
||||
|
||||
constructor(method?: string, handler?: T, children?: Record<string, Node<T>>) {
|
||||
this.children = children || Object.create(null)
|
||||
this.methods = []
|
||||
this.name = ''
|
||||
if (method && handler) {
|
||||
const m: Record<string, HandlerSet<T>> = Object.create(null)
|
||||
m[method] = { handler, possibleKeys: [], score: 0, name: this.name }
|
||||
m[method] = { handler, possibleKeys: [], score: 0 }
|
||||
this.methods = [m]
|
||||
}
|
||||
this.patterns = []
|
||||
}
|
||||
|
||||
insert(method: string, path: string, handler: T): Node<T> {
|
||||
this.name = `${method} ${path}`
|
||||
this.order = ++this.order
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
@ -76,7 +72,6 @@ export class Node<T> {
|
||||
const handlerSet: HandlerSet<T> = {
|
||||
handler,
|
||||
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
|
||||
name: this.name,
|
||||
score: this.order,
|
||||
}
|
||||
|
||||
@ -100,12 +95,14 @@ export class Node<T> {
|
||||
const processedSet: Record<string, boolean> = Object.create(null)
|
||||
if (handlerSet !== undefined) {
|
||||
handlerSet.params = Object.create(null)
|
||||
handlerSet.possibleKeys.forEach((key) => {
|
||||
const processed = processedSet[handlerSet.name]
|
||||
for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) {
|
||||
const key = handlerSet.possibleKeys[i]
|
||||
const processed = processedSet[handlerSet.score]
|
||||
handlerSet.params[key] =
|
||||
params[key] && !processed ? params[key] : nodeParams[key] ?? params[key]
|
||||
processedSet[handlerSet.name] = true
|
||||
})
|
||||
processedSet[handlerSet.score] = true
|
||||
}
|
||||
|
||||
handlerSets.push(handlerSet)
|
||||
}
|
||||
}
|
||||
@ -132,7 +129,7 @@ export class Node<T> {
|
||||
|
||||
if (nextNode) {
|
||||
nextNode.params = node.params
|
||||
if (isLast === true) {
|
||||
if (isLast) {
|
||||
// '/hello/*' => match '/hello'
|
||||
if (nextNode.children['*']) {
|
||||
handlerSets.push(
|
||||
@ -177,10 +174,10 @@ export class Node<T> {
|
||||
continue
|
||||
}
|
||||
|
||||
if (matcher === true || (matcher instanceof RegExp && matcher.test(part))) {
|
||||
if (matcher === true || matcher.test(part)) {
|
||||
if (typeof key === 'string') {
|
||||
params[name] = part
|
||||
if (isLast === true) {
|
||||
if (isLast) {
|
||||
handlerSets.push(...this.gHSets(child, method, params, node.params))
|
||||
if (child.children['*']) {
|
||||
handlerSets.push(...this.gHSets(child.children['*'], method, params, node.params))
|
||||
|
@ -13,8 +13,8 @@ export class TrieRouter<T> implements Router<T> {
|
||||
add(method: string, path: string, handler: T) {
|
||||
const results = checkOptionalParameter(path)
|
||||
if (results) {
|
||||
for (const p of results) {
|
||||
this.node.insert(method, p, handler)
|
||||
for (let i = 0, len = results.length; i < len; i++) {
|
||||
this.node.insert(method, results[i], handler)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user