路由器中实现web框架

文章引自:一文读懂主流web框架中路由的实现原理 – 掘金 (juejin.cn)

1.路由概述

1.基本概述

url地址到业务处理代码的映射

通俗来讲就是处理用户输入的url的返回内容

当用户点击登录时,服务器应该做登录相关的事情,并给用户返回登录成功或失败的页面。当用户点击退出时,服务器应该做和退出相关的事情(比如清理用户登录的数据),并返回给用户退出之后的页面

2.路由的概念

一个url到一个具体处理函数即是一条路由

URL → 处理函数

多条线路就可以组成路由表

根据不同的路由表的组织形式,不同查找方式

简单的路由表就是map,以key-value匹配

/login → func login()
/home → func home()
/detail →func detail( )

3.路由查找

给定一个url,找到对应的处理函数的过程叫做路由查找

示例:

image-20240605142842599
web系统中一个路由系统由路由、路由表、路由匹配三部分功能组成

2.路由的实现(基于映射表)

1.概述

再go的net/nttp包中,路由及与map实现,这个映射通过serveMux类型管理

Screenshot_2024-06-05-16-07-48-90_c9ddfdcc587d441

思路简化理解:

m=map(key-value结构,即是路由表),key是路由的路径,value是muxEntry的对象,pattern是对应路径,h是处理函数

调用http.Handle(“/”, &HomeHandler{})注册路由=将路径和对象(HomeHandler)构建成一个muxEntry的一个对象存放到ServerMux的对象m中

type ServeMux struct {
mu   sync.RWMutex
m     map[string]muxEntry
es   []muxEntry // slice of entries sorted from longest to shortest.
hosts bool       // whether any patterns contain hostnames
}

路由的查找就是通过map查找对应的muxEntry获取对应handler处理

2.map存储路径与处理函数的对应关系(详细解释)

1.ServeMux结构体

1.HTTP请求的多路复用器结构体实现:

type ServeMux struct {
   mu    sync.RWMutex
   m     map[string]muxEntry
   hosts bool // 是否需要根据host来路由
}

2.字段解析

mu sync.RWMutex:

  • 类型:sync.RWMutex
  • 作用:用于保护映射表(map[string]muxEntry)的并发访问RWMutex是一个读写锁,可以同时允许多个读操作,但写操作是互斥的(确保了在注册路由或处理请求时,ServeMux的数据是线程安全的)

m map[string]muxEntry:

  • 类型:map[string]muxEntry
  • 作用:存储路径(URL路径)与处理器条目(muxEntry)的映射关系(键是请求路径,值是muxEntry结构体)
  • 这是路由映射表,负责将路径映射到具体的处理器

hosts bool:

  • 类型:bool
  • 作用:指示是否需要根据请求的Host字段来进行路由(不同的域名或子域来路由请求时使用)
  • 如果设置为true,那么路由时不仅会考虑路径,还会考虑Host字段

2.muxEntry结构体

1.映射表中的一个条目,包含了一个处理器和与之相关的路径模式结构体实现:

type muxEntry struct {
   explicit bool
   h        Handler
   pattern  string
}

2.字段解析

explicit bool:

  • 类型:bool
  • 作用:指示该条目是否为显式注册
  • 显式注册的路径是通过ServeMuxHandleHandleFunc方法明确添加的
  • explicittrue时,表示该路径是用户显式注册的,否则可能是默认处理器等隐式添加的

h Handler:

  • 类型:Handler
  • 作用:处理该路径请求的处理器(handler)。Handler是一个接口,定义了一个ServeHTTP方法,用于处理HTTP请求
  • 这是实际处理请求的函数或方法,当请求路径匹配时,会调用对应的处理器来处理请求

pattern string:

  • 类型:string
  • 作用:该条目对应的路径模式(匹配请求路径的字符串)
  • 例如,/static//api/等,路由器会根据这个模式来匹配和路由请求

3.实现机制

1.注册路由

当调用Handle将路径模式和处理器注册到ServerMux中是存储在map中

// Handle 方法将一个处理器注册到指定的路径模式
func (mux *ServeMux) Handle(pattern string, handler Handler) {
  // 使用写锁保证并发安全
  mux.mu.Lock()
   defer mux.mu.Unlock()

  // 检查路径模式是否为空
   if pattern == "" {
      panic("http: invalid pattern") // 如果路径模式为空,则引发恐慌(panic)
   }
   
  // 检查处理器是否为 nil
   if handler == nil {
      panic("http: nil handler") // 如果处理器为 nil,则引发恐慌(panic)
   }

  // 将路径模式和处理器存储到映射表 m 中
  mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
}

mux.mu.Lock() / defer mux.mu.Unlock():

  • 使用写锁 Lock 来确保在并发环境下注册处理器的操作是安全的。
  • defer 语句保证在函数返回时释放锁。

if pattern == “”:

  • 检查路径模式是否为空。
  • 如果路径模式为空,程序会引发恐慌(panic),并输出 “http: invalid pattern” 错误信息。

if handler == nil:

  • 检查处理器是否为 nil。
  • 如果处理器为 nil,程序会引发恐慌(panic),并输出 “http: nil handler” 错误信息。

mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}:

  • 将路径模式和处理器存储到 ServeMux 的映射表 m 中。
  • 创建一个 muxEntry,将 explicit 字段设置为 true,表示这是一个显式注册的路径模式。
  • h 字段是传入的处理器,pattern 字段是路径模式。

2.处理请求

url请求达到后,ServerMux根据请求查找对应的路由器,调用ServerHTTP方法处理请求

// ServeHTTP 是 ServeMux 实现的核心方法,用于处理 HTTP 请求
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
// 如果请求的 URI 是 "*",返回 400 错误,并关闭连接
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}

// 查找对应的处理器
h, _ := mux.Handler(r)
// 调用处理器的 ServeHTTP 方法来处理请求
h.ServeHTTP(w, r)
}

// Handler 方法根据请求返回对应的处理器和匹配的模式
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// 使用读锁保证并发安全
mux.mu.RLock()
defer mux.mu.RUnlock()

// 调用 match 方法进行路径匹配
return mux.match(r.Host, r.URL.Path)
}

// match 方法查找与给定主机和路径匹配的处理器
func (mux *ServeMux) match(host, path string) (h Handler, pattern string) {
// 首先尝试精确匹配路径
if e, ok := mux.m[path]; ok {
return e.h, e.pattern
}

// 如果没有精确匹配,尝试前缀匹配
var n = 0
for k, e := range mux.m {
if !hasPathPrefix(path, k) {
continue
}
// 找到最长的匹配前缀
if n == 0 || len(k) > n {
n = len(k)
h = e.h
pattern = e.pattern
}
}
if h != nil {
return h, pattern
}

// 如果没有找到匹配的处理器,返回默认的 NotFound 处理器
return NotFoundHandler(), ""
}

// hasPathPrefix 是一个辅助函数,用于判断 path 是否以 pattern 为前缀
func hasPathPrefix(path, pattern string) bool {
if len(path) < len(pattern) {
return false
}
return path[:len(pattern)] == pattern
}

// NotFoundHandler 返回一个默认的 404 处理器
func NotFoundHandler() Handler {
return HandlerFunc(func(w ResponseWriter, r *Request) {
http.Error(w, "404 page not found", http.StatusNotFound)
})
}

3.路径匹配

Server先及逆行精确匹配,若失败则进行前缀匹配

  1. 精确匹配:路径完全匹配某个注册的路径模式
  2. 前缀匹配:路径以某个注册的路径模式为前缀

4.限制

不能对路由进行分组、不能限定路由的请求方法(GET、POST或其他)、不能对路由加中间件

3.基于正则表达式的路由实现

1.简述 gorilla/mux包

  1. 路由变量:可以通过定义路由模式中的变量来动态地匹配URL路径中的部分,并在处理程序中访问这些变量的值
  2. 正则表达式路由:允许你使用正则表达式来定义更复杂的路由模式
  3. 子路由:可以创建嵌套的路由器,用于组织和管理应用程序的不同部分
  4. 中间件:可以在处理请求之前或之后执行一系列的处理函数,用于实现例如日志记录、身份验证、跨域资源共享等功能
  5. 支持HTTP方法:支持常见的HTTP方法,如GET、POST、PUT、DELETE等,并可以方便地为不同的方法注册处理函数

2.基本使用

package main

import (
"fmt"
"net/http"

"github.com/gorilla/mux"
)

func main() {
// 创建一个新的路由器
r := mux.NewRouter()

// 定义处理根路径 "/" 的处理函数 HomeHandler
r.HandleFunc("/", HomeHandler)

// 定义处理路径 "/products" 的处理函数 ProductsHandler
r.HandleFunc("/products", ProductsHandler)

// 定义处理 RESTful 风格的路径,例如 "/product/12345",并且使用正则表达式指定 id 只能是数字
r.HandleFunc("/product/{id:[0-9]+}", ProductHandler)

// 启动HTTP服务器,监听在端口8000上,并使用之前定义的路由器 r 处理请求
http.ListenAndServe(":8000", r)
}

// HomeHandler 处理根路径的函数,接收一个 ResponseWriter 和一个 Request 作为参数
func HomeHandler(response http.ResponseWriter, request *http.Request) {
response.Write([]byte("Hi, this is Home page"))
}

// ProductsHandler 处理 "/products" 路径的函数
func ProductsHandler(response http.ResponseWriter, request *http.Request) {
response.Write([]byte("Hi, this is Product page"))
}

// ProductHandler 处理 "/product/{id}" 路径的函数
func ProductHandler(response http.ResponseWriter, request *http.Request) {
// 从请求中获取路径参数中的 id 值
vars := mux.Vars(request)
id := vars["id"]

// 将响应写入 ResponseWriter 中,包含产品的ID值
response.Write([]byte("Hi, this is product:" + id))
}

3.实现原理

1.概述

通过mux.NewRouter()返回了对Router结构体的对象(实现ServeHttp的方法,即是对路由的匹配和转发),覆盖作为http.ListenAndServe的第二个参数,替代了默认的路由分发对象DefaultServeMux

2.简析

// ServeHTTP 实现了http.Handler接口的方法,用于处理HTTP请求
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 通过调用匹配函数进行路由匹配
match := r.match(req.Method, getPath(req.URL))
if match == nil {
// 如果没有找到匹配的路由,则返回404 Not Found
http.NotFound(w, req)
return
}

// 执行匹配到的路由的处理函数
match.handler.ServeHTTP(w, req)
}
  1. ServeHTTP方法是gorilla/mux包中的Router结构体实现的http.Handler接口的方法,因此Router结构体可以作为一个HTTP处理器使用
  2. 在ServeHTTP方法中,首先通过调用match方法进行路由匹配,找到与请求路径和方法匹配的路由
  3. 如果找到了匹配的路由,则执行该路由的处理函数(handler)的ServeHTTP方法来处理请求
  4. 如果没有找到匹配的路由,则返回404 Not Found

3.Router结构体

1.结构图

image-20240606163148222

2.简析

Router是gorilla/mux包中的核心结构体,负责管理和分发HTTP请求

  • routes:一个包含所有已注册Route的切片
  • nameRoute:一个映射表,用于根据名称查找Route
  • routeConf:存储路由配置的结构体
1.Route结构体

Route结构体表示单个路由规则,其主要成员有:

  • handler:处理HTTP请求的处理器。
  • name:路由的名称。
  • namedRoutes:一个映射表,用于根据名称查找Route。
  • routeConf:存储路由配置的结构体。
2.routeConf结构体

routeConf结构体包含路由的配置信息,其主要成员有:

  • matchers:一个matcher接口的切片,用于进行路由匹配。
  • regexp:一个RouteRegexpGroup结构体,用于存储正则表达式路由规则。
3.matcher接口

matcher接口定义了路由匹配的方法:

  • Match:根据HTTP请求和路由匹配信息进行匹配。
4.RouteRegexpGroup结构体

RouteRegexpGroup结构体用于存储路由的正则表达式匹配信息,其主要成员有:

  • host:用于匹配主机名的routeRegexp结构体指针。
  • path:用于匹配路径的routeRegexp结构体指针。
  • queries:用于匹配查询参数的routeRegexp结构体切片。
5.routeRegexp结构体

routeRegexp结构体用于存储单个正则表达式匹配规则,其主要成员有:

  • template:模板字符串。
  • regexpType:正则表达式的类型。
  • regexp:正则表达式对象。
  • reverse:反向解析字符串。
  • varsR:路径变量的名称切片。
  • varsR:路径变量的正则表达式切片。

4.路由匹配查找基本流程

image-20240606164037671

5.路由支持的功能及对应的正则

1.匹配特定域名或子域名

使用Host方法为路由器设置特定的主机名匹配规则

r := mux.NewRouter()
// Only matches if domain is "www.example.com".
r.Host("www.example.com")

设置了一个新的路由器r,设置主机名匹配规则,取得主机名必须是www.example.com,否则不会处理该请求

// Matches a dynamic subdomain.
r.Host("{subdomain:[a-z]+}.example.com")

创建一个还有动态子域名匹配的主机名匹配规则

使用了占位符{subdomain:[a-z]+},表示可以匹配任何由小写字母组成的子域名(subdomain是一个变量名,它匹配由小写字母组成的子域名部分。你可以在请求处理函数中通过mux.Var函数来访问这个变量的值)

2.路径增加前缀

r.PathPrefix("/products/")

只有路径中是以/products为前缀的才能匹配到该路由

最终编译成的正则表达式是^/products

3. 限制路由的请求方法(GET、POST等)

r.Methods("GET", "POST")

不经过正则

将允许的方法(GET、POST)转换成一个methodMatcher类型(本质上是一个字符串切片)

实现了Match方法,也就是matcher接口

// methodMatcher 是一个字符串切片类型,用于存储多个HTTP方法(如GET、POST等)。
type methodMatcher []string

// Match 方法用于检查 HTTP 请求的方法是否在 methodMatcher 定义的方法列表中。
// 如果请求的方法在 methodMatcher 列表中,则返回 true;否则返回 false。
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
// 调用 matchInArray 函数,检查 r.Method 是否在 methodMatcher (m) 列表中。
return matchInArray(m, r.Method)
}

4.支持路由分组

完整的示例代码

package main

import (
"net/http"
"github.com/gorilla/mux"
)

// 处理 "/user/info" 路径的请求
func UserInfoHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("User Info Page"))
}

// 处理 "/user/settings" 路径的请求
func UserSettingsHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("User Settings Page"))
}

// 处理 "/admin/dashboard" 路径的请求
func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Admin Dashboard Page"))
}

// 处理 "/admin/users" 路径的请求
func AdminUsersHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Admin Users Page"))
}

func main() {
// 创建一个新的路由器
r := mux.NewRouter()

// 创建一个带有路径前缀 "/user" 的子路由器
userRouter := r.PathPrefix("/user").Subrouter()
userRouter.HandleFunc("/info", UserInfoHandler)
userRouter.HandleFunc("/settings", UserSettingsHandler)

// 创建一个带有路径前缀 "/admin" 的子路由器
adminRouter := r.PathPrefix("/admin").Subrouter()
adminRouter.HandleFunc("/dashboard", AdminDashboardHandler)
adminRouter.HandleFunc("/users", AdminUsersHandler)

// 启动HTTP服务器,监听在端口8000上,并使用之前定义的路由器 r 处理请求
http.ListenAndServe(":8000", r)
}

1.创建用户相关子路由

userRouter := r.PathPrefix("/user").Subrouter()
userRouter.HandleFunc("/info", UserInfoHandler)
userRouter.HandleFunc("/settings", UserSettingsHandler)
  • userRouter := r.PathPrefix(“/user”).Subrouter():创建一个子路由器userRouter,其路径前缀为/user
  • userRouter.HandleFunc(“/info”, UserInfoHandler):为路径/user/info定义处理函UserInfoHandler
  • userRouter.HandleFunc(“/settings”, UserSettingsHandler):为路径/user/settings`定义处理函数UserSettingsHandler

2.创建管理员相关的子路由

adminRouter := r.PathPrefix("/admin").Subrouter()
adminRouter.HandleFunc("/dashboard", AdminDashboardHandler)
adminRouter.HandleFunc("/users", AdminUsersHandler)
  • adminRouter := r.PathPrefix(“/admin”).Subrouter() 创建一个子路由器adminRouter,其路径前缀为/admin
  • adminRouter.HandleFunc(“/dashboard”, AdminDashboardHandler) 为路径/admin/dashboard定义处理函数AdminDashboardHandler
  • adminRouter.HandleFunc(“/users”, AdminUsersHandler) 为路径/admin/users定义处理函数AdminUsersHandler
image-20240606224805291

5.支持中间件

1.路由器定义和中间件应用

func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 记录请求的URI
log.Println(r.RequestURI)
// 调用下一个处理器
next.ServeHTTP(w, r)
})
}

loggingMiddleware中间件应用到这个路由器上。这意味着所有通过这个路由器处理的请求都会首先经过这个中间件

2.请求处理函数

func HomeHandler(response http.ResponseWriter, request *http.Request) {
response.Write([]byte("Hi, this is Home page"))
}

3.中间件的定义和类型

1. MiddlewareFunc 类型
type MiddlewareFunc func(http.Handler) http.Handler

接收一个 http.Handler 作为参数,并返回一个新的 http.Handler

(在请求处理链中执行一些额外的逻辑)

2.Middleware 方法
func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler {
return mw(handler)
}

调用 MiddlewareFunc 类型的实例(即中间件函数)并传入一个 http.Handler,返回一个新的 http.Handler

4.Use 方法添加中间件

func (r *Router) Use(mwf ...MiddlewareFunc) {
for _, fn := range mwf {
r.middlewares = append(r.middlewares, fn)
}
}

将一个或多个中间件函数添加到 Router 的 middlewares 列表中

  • 接收变长参数 mwf …MiddlewareFunc,可以传入多个中间件函数
  • 遍历传入的中间件函数,将每个中间件函数添加到 middlewares 列表中

5. 路由匹配时执行中间件

match方法

func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
for _, route := range r.routes {
if route.Match(req, match) {
// 构建中间件链条
if match.MatchErr == nil {
for i := len(r.middlewares) - 1; i >= 0; i-- {
match.Handler = r.middlewares[i].Middleware(match.Handler)
}
}
return true
}
}
// 省略其他代码
}
  • 遍历 Router 的 routes 列表,检查每个路由是否匹配请求
  • 如果找到匹配的路由,并且 match.MatchErr 为 nil,则构建中间件链条
  • 倒序遍历 middlewares 列表,将每个中间件函数应用到 match.Handler 上,构建一个新的处理链条

6. 中间件的包装和执行过程

1.例:loggingMiddleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.RequestURI)
next.ServeHTTP(w, r)
})
}

loggingMiddleware 是一个中间件函数,接收一个 http.Handler 类型的 next 参数,返回一个新的 http.Handler

流程:

  1. 返回的 http.Handler 首先记录请求的 URI(log.Println(r.RequestURI))
  2. 然后调用 next.ServeHTTP(w, r),将请求传递给下一个处理器(即下一个中间件或最终的处理器)
2.中间链条的执行
r.HandleFunc("/", HomeHandler)
r.Use(loggingMiddleware)

在 Router 匹配到根路径 “/” 的请求时:

  1. match.Handler 初始为 HomeHandler
  2. oggingMiddleware 包装 HomeHandler,返回一个新的 http.Handler(先执行日志记录逻辑,然后调用 HomeHandler)

4.gin框架中的路由实现

1.gin框架中的路由

1.定义概述

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
// 创建一个新的 Gin 实例,不带任何默认中间件
g := gin.New()

// 定义 POST 路由 /abc/info,使用 InfoHandler 处理请求
g.POST("/abc/info", InfoHandler)

// 定义 POST 路由 /abc/info/detail,使用 InfoHandler 处理请求
g.POST("/abc/info/detail", InfoHandler)

// 定义 POST 路由 /abc/list,使用 HomeHandler 处理请求
g.POST("/abc/list", HomeHandler)

// 启动 HTTP 服务器,监听端口 8000
g.Run(":8000")
}

// HomeHandler 处理 /abc/list 路由的请求
func HomeHandler(ctx *gin.Context) {
// 写入响应内容 "Hi, this is Home page"
ctx.Writer.Write([]byte("Hi, this is Home page"))
}

// InfoHandler 处理 /abc/info 和 /abc/info/detail 路由的请求
func InfoHandler(ctx *gin.Context) {
// 写入响应内容 "Hi, this is info"
ctx.Writer.Write([]byte("Hi, this is info"))
}
  1. 通过gin.New()初始化一个gin对象g
  2. 然后通过g.POST或g.GET等方法就可以注册路由
  3. 限制了请求反射光hi,但是Any方法是允许任何请求的

2.Any方法

Any方法本质上是定义了一组方法名,然后依次调用对应的方法将该路由进行注册

var anyMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
http.MethodTrace,
}

// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
// 遍历 anyMethods 列表中的每一个 HTTP 方法
for _, method := range anyMethods {
// 调用 group.handle 方法,注册每一种 HTTP 方法的路由
group.handle(method, relativePath, handlers)
}

// 返回 RouterGroup 实例
return group.returnObj()
}

2. 前缀树路由的实现原理

利用字符串公共前缀来减少查询时间,减少无谓的字符串比较(相比较map/hash字典)

1. 路由中限制请求方法的实现

type methodTree struct {
method string
root *node
}
  • methodTree是一个结构体,用于表示一个 HTTP 方法的路由树
  • method字段是一个字符串,表示 HTTP 方法(如 GET、POST 等)
  • root字段是一个指向 node类型的指针,表示路由树的根节点

当服务器接收到一个 HTTP 请求时,可以通过查找 methodTrees 中对应的 methodTree 来找到路由树,然后在树中查找具体的路由处理函数

image-20240607191846039

2.路由树节点的数据结构

image-20240607194220467

3.路由树的创建

1.第一个路由注册

g.POST("/abc/info", InfoHandler)

第一个路由注册,路由树是空的

直接构建一个node节点,然后将该node节点作为POST方法路由树的根节点插入即可

image-20240607195605696

3.注册第二个路由

g.POST("/abc/info/detail", DetailHandler)

和路由”/abc/info”有共同的前缀,所以会将该路由作为第一个路由的子节点放到children中

image-20240607195659823

关键:

  • 根节点的priority由1变成了2
  • children中多了一个子节点路由
  • 一个是indices字段的值变成了”/”(这个是第一个子节点的path字段的第一个字符,用于匹配时索引使用)

注意(path的值)

因为前缀是”/abc/info”了,所以这里path是”/detail”

fullPath依然是注册时完整的路

3.注册第三个路由

g.POST("/abc/list", ListHandler)
和前两个路由有共同的前缀"/abc/"

将现在的根节点进行拆分,拆分成"/abc/" 和"info"

info和原来的"/abc/info/detail" 又有共同的前缀info

原来的"/abc/info/detail"就变成了info的子节点

而"/abc/list"除去前缀"/abc/"后,剩余"list"子节点,作为"/abc/"的子节点
image-20240607201114264

路由树结构图

image-20240607201312896

关键:

  • handlers变为nil(因为该节点不是一个具体的路径,只是一个前缀,所以具体的handler下移到了子节点info节点)
  • path变为了前缀”/abc/”
  • indices字段值变为了”il”,其中i是第一个子节点中path字段的第一个字符,l是第二个子节点中path字段的第一个字符
  • priority字段变成3:代表从自身开始及子节点共有4个
  • children字段变成了两个直接子节点
  • fullPath字段变为了”/abc/
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇