简单易用强大的路由库

hyahm · · 1025 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

# xmux, go语言 路由(router) 应该是基于原生net.http 极简并强大的路由, 内嵌接口文档,告别另外写文档的烦恼 专注前后端分离项目, 良好的设计可以大量减少代码冗余 ### 特性 - [x] xmux.NewGroupRoute() - [x] 支持路由分组 - [x] 支持全局请求头, 组请求头, 私有请求头 - [x] 支持自定义method, - [x] 支持正则匹配和参数获取 - [x] 完全匹配优先于正则匹配 - [x] 正则匹配支持(int(\d+), word(\w+), re, string, all(.*?),不写默认 word - [x] 支持三大全局 HanleFavicon, HandleNotFound, HandleOptions) - [x] 强大的模块让你的代码模块化变得非常简单 - [x] 中间件支持 - [x] 内嵌接口文档 - [x] 数据绑定 - [x] 增加websocket, 可以学习,不建议使用, 如果其他的不好可以试试 - [x] 集成pprof, router.AddGroup(xmux.Pprof()) - [x] 支持权限控制 - [x] 进入时和处理完成时的钩子函数 ### 安装 ``` go get github.com/hyahm/xmux ``` ### 最简单的运行 ```go package main import ( "net/http" "github.com/hyahm/xmux" ) func main() { router := xmux.NewRouter() router.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("<h1>hello world!<h1>")) }) router.Run() } ``` 打开 localhost:8080 就能看到 hello world! ### 请求方式 ```go package main import ( "net/http" "github.com/hyahm/xmux" ) func main() { router := xmux.NewRouter() // 只是例子不建议下面的写法, 而是使用 router.Reqeust("/",nil, "POST", "GET") router.Get("/",nil) // get请求 router.Post("/",nil) // post请求 router.Reqeust("/getpost",nil, "POST", "GET") // 同时支持get,post请求 router.Any("/any",nil) // 支持除了options 之外的所有请求 router.Run() } ``` ### 添加了组的概念 ```go // aritclegroup.go func hello(w http.ResponseWriter, r *http.Request) { fmt.Println(xmux.Var(r)["id"]) w.Write([]byte("hello world!!!!")) return } var Article *xmux.GroupRoute func init() { Article = xmux.NewGroupRoute() Article.Get("/{int:id}", hello) } ``` ```go // main.go func main() { router := xmux.NewRouter() router.AddGroup(aritclegroup.Article) router.Run() } ``` ### 更灵活的匹配 ```go func main() { router := xmux.NewRouter() router.Get("/foo/{all:age}", foo) router.Get("/{all:age}", show) // 这个可以匹配任何路由 router.Post("/{all:age}", Who) // 这个可以匹配任何路由 router.Run() } ``` 记住, 是100%, 此路由优先匹配完全匹配规则, 匹配不到再寻找 正则匹配, 加快了寻址速度 访问 /foo/get get -> 识别 foo 访问 /foo/get get -> 识别 show 访问 /foo/post post -> 识别 Who ### 自动检测重复项 ```go func main() { router := xmux.NewRouter() router.Get("/get",show) // 不同请求分别处理 router.Get("/get",show) // 不同请求分别处理 router.Run() } 写一大堆路由, 有没有重复的都不知道 运行上面将会报错, 如下 2019/11/29 21:51:11 pattern duplicate for /get ``` ### 自动格式化url 将任意多余的斜杠去掉例如 /asdf/sadf//asdfsadf/asdfsdaf////as///, 转为-》 /asdf/sadf/asdfsadf/asdfsdaf/as ```go func main() { router := xmux.NewRouter() router.IgnoreSlash = true router.Get("/get",show) // 不同请求分别处理 router.Get("/get/",show) // 不同请求分别处理 router.Run() } 如果 router.IgnoreSlash = false 那么运行上面将会报错,/get/被转为 /get 如下 2019/11/29 21:51:11 pattern duplicate for /get ``` ### 三大全局handle ```go HandleOptions: handleoptions(), //这个是全局的options 请求处理, 前端预请求免除每次都要写个预请求的处理, 默认会返回ok, 也可以自定义 HandleNotFound: handleNotFound(), // 默认返回404 , 也可以自定义 HanleFavicon: methodNotAllowed(), // 默认请求 favicon // 默认调用的方法如下, 没有找到路由 func handleNotFound() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) return }) } ``` ### 模块 (主要用来做验证, 比如token验证, 权限验证, 数据解析<需搭配Bind()>) - 模块类 优先级 全局路由 > 组路由 > 私有路由 (如果存在优先级大先执行,。 如果不想用可以在不想使用的路由点或路由组 DelModule 来单独删除) ```go func home(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello world home")) return } func mid() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Println("77777") return }) } func hf(w http.ResponseWriter, r *http.Request) bool { fmt.Println("44444444444444444444444444") r.Header.Set("name", "cander") return true } func hf1(w http.ResponseWriter, r *http.Request) bool { fmt.Println("66666") fmt.Println(r.Header.Get("name")) return false } func main() { router := xmux.NewRouter().AddModule(hf).SetHeader("name", "cander") router.Get("/home/{test}",home).AddModule(hf1) // 此处会先执行 hf -> hf1 -> home router.Get("/test/{test}",home).DelModule(hf) // 此处直接执行 home router.Run() } ``` ### 中间件 - 中间件最多只能有一个, 功能较多建议使用模块 优先级与header 一样, 中间件如下, 这是个计算执行时间的例子 计算的时间不包含路由匹配和模块的时间 ```go func GetExecTime(handle func(http.ResponseWriter, *http.Request), w http.ResponseWriter, r *http.Request) { start := time.Now() handle(w, r) fmt.Printf("url: %s -- addr: %s -- method: %s -- exectime: %f\n", r.URL.Path, r.RemoteAddr, r.Method, time.Since(start).Seconds()) } ``` ### Enter, Exit 进入和退出的钩子 ```go func exit(start time.Time, w http.ResponseWriter, r *http.Request) { // 主要为了打印执行的时间 fmt.Println(time.Since(start).Seconds(), r.URL.Path) } // 与module一样的效果, return true 就是直接返回, return false 就是继续 但是不支持 xmux.GetInstence(r)传参 // 主要用来过滤请求和调试 func enter( w http.ResponseWriter, r *http.Request) bool { // 任何请求都会进入到这里,比如过滤ip, 域名 fmt.Println(time.Since(start).Seconds(), r.URL.Path) } func main() { router := xmux.NewRouter().AddModule(hf).SetHeader("name", "cander") router.Exit = exit router.Enter = enter router.Get("/home/{test}",home).AddModule(hf1) // 此处会先执行 hf -> hf1 -> home router.Get("/test/{test}",home).DelModule(hf) // 此处直接执行 home router.Run() } ``` ### SetHeader 跨域主要是添加请求头的问题, 其余框架一般都是借助中间件来设置 但是本路由借助上面请求头设置 大大简化跨域配置 优先级 私有路由 > 组路由 > 全局路由 (如果存在优先级大的就覆盖优先级小的) ```go // 跨域处理的例子, 设置下面的请求头后, 所有路由都将挂载上请求头, // 如果某些路由有单独请求头, 可以单独设置 func main() { router := xmux.NewRouter() router.IgnoreSlash = true router.SetHeader("Access-Control-Allow-Origin", "*") // 主要的解决跨域, 因为是全局的请求头, 所以后面增加的路由全部支持跨域 router.SetHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token,X-Token,Origin,smail,authorization") // 新增加的请求头 router.Get("/", index) router.Run() } ``` ### 传值 > 生命周期从定义开始, 到此handle执行完毕将被释放 ```go func filter(w http.ResponseWriter, r *http.Request) bool { fmt.Println("login mw") xmux.GetInstance(r).Set("name","xmux") r.Header.Set("bbb", "ccc") return false } func name(w http.ResponseWriter, r *http.Request) { name := xmux.GetInstance(r).Get("name").(string) // 这里是filter 里面传值的name urlName := xmux.Var(r)["name"] // 这里是url的name值 fmt.Println(name) fmt.Println(urlName) w.Write([]byte("hello world " + name)) return } func main() { router := xmux.NewRouter() router.Get("/aaa/{name}", name).AddModule(filter) // 通过 xmux.GetInstance(r) 可以再在module handle 进行值的传递 router.Run() } ``` ``` curl http://localhost:8080/aaa/name login mw name ``` ### 获取正则匹配的参数 ```go func Who(w http.ResponseWriter, r *http.Request) { fmt.Println(xmux.Var(r)["name"]) fmt.Println(xmux.Var(r)["age"]) w.Write([]byte("yes is mine")) return } func main() { router := xmux.NewRouter() router.Get("/aaa/{name}/{int:age}", Who) router.Run() } ``` ### 数据绑定(Bind(), 与Module 一起使用,DelModule必须放在AddModule之后) 将数据结构绑定到此 Handle 里, 通过读取r.Body 来解析数据 因为解析的代码都是一样的, 绑定后可以共用同一份代码 ```go func JsonToStruct(w http.ResponseWriter, r *http.Request) bool { // 任何报错信息, 直接return true, 就是此handle 直接执行完毕了, 不继续向后面走了 if goconfig.ReadBool("debug", false) { b, err := ioutil.ReadAll(r.Body) if err != nil { return true } err = json.Unmarshal(b, xmux.GetInstance(r).Data) if err != nil { return true } } else { err := json.NewDecoder(r.Body).Decode(xmux.GetInstance(r).Data) if err != nil { return true } } return false } type DataName struct{} type DataStd struct{} type DataFoo struct{} func AddName(w http.ResponseWriter, r *http.Request) { df := xmux.GetInstance(r).Data.(*DataName) fmt.Printf("%#v", df) } func AddStd(w http.ResponseWriter, r *http.Request) { df := xmux.GetInstance(r).Data.(*DataStd) fmt.Printf("%#v", df) } func AddFoo(w http.ResponseWriter, r *http.Request) { df := xmux.GetInstance(r).Data.(*DataFoo) fmt.Printf("%#v", df) } func main() { router := xmux.NewRouter() router.Post("/important/name", AddName).Bind(&DataName{}).AddModule(JsonToStruct) router.Post("/important/std", AddStd).Bind(&DataStd{}).AddModule(JsonToStruct) router.Post("/important/foo", AddFoo).Bind(&DataFoo{}).AddModule(JsonToStruct) // 也可以直接使用内置的 router.Post("/important/foo/by/json", AddFoo).BindJson(&DataFoo{}) // 如果是json格式的可以直接 BindJson 与上面是类似的效果 router.Run() } ``` ### 自动修复请求的url 例如: 请求的url 是这个样子的 http://www.hyahm.com/mmm///af/af, 默认是请求不到的 但是设置后 ```go router := xmux.NewRouter() router.IgnoreSlash = true ``` 是可以直接访问 http://www.hyahm.com/mmm/af/af 这个地址的请求 ### 匹配路由 支持以下5种 word 只匹配数字和字母下划线(默认) string 匹配所有不含/的字符 int 匹配整数 all: 匹配所有的包括/ re: 自定义正则 /aaa/{name} 这个和下面一个一样, 省略类型, 默认是string /aaa/{string:name} 这个和上面一样, string类型 /aaa/{int:name} 这个匹配int类型 /aaa/adf{re:([a-z]{1,4})sf([0-9]{0,10})sd:name,age} 这个是一段里面匹配了2个参数 name, age, 大括号表示是个匹配规则,里面2个冒号分割了3部分 起头的 第一个: re表示用到自定义正则,只有re才会有2个冒号分割, 第二个: 正则表达式, 里面不能出现: 需要提取的参数用()括起来, 第三个: 参数名, 前面有多少对(), 后面就需要匹配多少个参数, 用逗号分割 例如: /aaa/adfaasf16sd 这个是匹配的, name: aa age: 16 ```go xmux.Var(r)["name"] ``` 后面会增加自定义正则匹配 ### 内置websocket, 下面是一个完整的例子 ```go package main import ( "fmt" "log" "net/http" "sync" "time" "github.com/hyahm/xmux" ) type client struct { msg string c *xmux.BaseWs } var msgchan chan client var wsmu sync.RWMutex var ps map[*xmux.BaseWs]byte func sendMsg() { for { c := <-msgchan for p := range ps { if c.c == p { // 不发给自己 continue } fmt.Println(c.msg) // 发送的msg的长度不能超过 1<<31, 否则掉内容, 建议分包 p.SendMessage([]byte(c.msg), ps[p]) } } } func ws(w http.ResponseWriter, r *http.Request) { p, err := xmux.UpgradeWebSocket(w, r) if err != nil { w.Write([]byte(err.Error())) return } p.SendMessage([]byte("hello"), xmux.TypeMsg) wsmu.Lock() ps[p] = xmux.TypeMsg wsmu.Unlock() tt := time.NewTicker(time.Second * 2) go func() { for { <-tt.C if err := p.SendMessage([]byte(time.Now().String()), xmux.TypeMsg); err != nil { break } } }() for { if p.Conn == nil { return } // 封包 msgType, msg, err := p.ReadMessage() if err != nil { fmt.Println(err.Error()) // 连接断开 wsmu.Lock() delete(ps, p) wsmu.Unlock() break } ps[p] = msgType c := client{ msg: msg, c: p, } msgchan <- c } } func main() { router := xmux.NewRouter() wsmu = sync.RWMutex{} msgchan = make(chan client, 100) ps = make(map[*xmux.BaseWs]byte) router.SetHeader("Access-Control-Allow-Origin", "*") router.Get("/{int:uid}", ws) go sendMsg() if err := http.ListenAndServe(":8080", router); err != nil { log.Fatal(err) } } ``` ### 获取当前的连接数 ```go xmux.GetConnents() ``` ### 优雅的停止 ```go xmux.StopService() ``` ### 内置路由缓存 ```go xmux.NewRouter(cache ...uint64) // cache 是一个内置lru 路径缓存, 不写默认缓存10000, 请根据情况自己修改 ``` ### 权限控制 - 页面权限 思路来自前端框架路由组件 meta 的 roles 通过给定数组来判断 > 以github前端star最多的vue后端项目为例子 https://github.com/PanJiaChen/vue-element-admin ```javascript src/router/index.js 里面的页面权限路由 { path: '/permission', component: Layout, redirect: '/permission/page', alwaysShow: true, // will always show the root menu name: 'Permission', meta: { title: 'Permission', icon: 'lock', roles: ['admin', 'editor'] // you can set roles in root nav }, children: [ { path: 'page', component: () => import('@/views/permission/page'), name: 'PagePermission', meta: { title: 'Page Permission', roles: ['admin'] // or you can only set roles in sub nav } }, { path: 'directive', component: () => import('@/views/permission/directive'), name: 'DirectivePermission', meta: { title: 'Directive Permission' // if do not set roles, means: this page does not require permission } }, { path: 'role', component: () => import('@/views/permission/role'), name: 'RolePermission', meta: { title: 'Role Permission', roles: ['admin'] } } ] }, ``` > xmux 对应的写法 ```go func AddName(w http.ResponseWriter, r *http.Request) { fmt.Printf("%v", "AddName") } func AddStd(w http.ResponseWriter, r *http.Request) { fmt.Printf("%v", "AddStd") } func AddFoo(w http.ResponseWriter, r *http.Request) { fmt.Printf("%v", "AddFoo") } func role(w http.ResponseWriter, r *http.Request) { fmt.Printf("%v", "role") } func DefaultPermissionTemplate(w http.ResponseWriter, r *http.Request) (post bool) { // 拿到对应uri的权限, 也就是AddPageKeys和DelPageKeys所设置的 pages := xmux.GetInstance(r).Get(xmux.PAGES).(map[string]struct{}) // 如果长度为0的话,说明任何人都可以访问 if len(pages) == 0 { return false } // 拿到用户对应的 role,判断是都在 roles := []string{"admin"} //从数据库中获取或redis获取用户的权限 for _, role := range roles { if _, ok := pages[role]; ok { // 这里匹配的是存在这个权限, 那么久继续往后面的走 return false } } // 没有权限 w.Write([]byte("no permission")) return true } func main() { router := xmux.NewRouter().AddPageKeys("admin", "editor") router.AddModule(DefaultPermissionTemplate) router.Post("/permission", AddName) router.Post("/permission/page", AddStd).DelPageKeys("editor") router.Post("/permission/directive", AddFoo) // 也可以直接使用内置的 router.Post("/permission/role", role).DelPageKeys("editor") router.Run() } ``` - 更加细致的增删改查权限但不限于 增删改查 想过最简单的是根据 handle 的函数名 来判断, 以下内容来自xmuxd的权限模板 xmux.DefaultPermissionTemplate ```go // 页面权限示例 类似 setheader // // AddPageKeys("admin", "me", "xxx") // 添加 roles 角色, 类似 前端路由的的roles字段 // DelPageKeys("admin") // 某些节点或组删除掉这些角色权限 // CURD 权限, 需要统一handle 函数命令才可以, 比如增删改查对应的 handle 就是 // Create Update Delete List // 通过 module 来过滤细致权限 func DefaultPermissionTemplate(w http.ResponseWriter, r *http.Request) (post bool) { // 如果是管理员的,直接就过 // if uid == <adminId> { // retrun false // } // roles := []string{"env", "important"} // 内置的方法最大支持8种权限,如果想要更多可以自己实现 var pl = []string{"Read", "Create", "Update", "Delete"} // map 的key 对应页面的value value 对应二进制位置(从右到左) permissionMap := make(map[string]int) for k, v := range pl { permissionMap[v] = k } // 假如权限拿到二进制对应的10进制数据是下面 perm := make(map[string]uint8) perm["env"] = 14 // 00001110 {"Delete", "Create", "Update"} perm["important"] = 10 // 00001010 {"Create", "Delete"} perm["project"] = 4 // 00000100 {"Update"} // pages := GetInstance(r).Get(PAGES).(map[string]struct{}) // 如果长度为0的话,说明任何人都可以访问 if len(pages) == 0 { return false } // 请求/project/read map[admin:{} project:{}] // 判断 pages 是否存在 perm // 注意点: 这里的页面权限本应该只会匹配到一个, 这个是对于的页面权限的值 page := "" // 判断页面权限的 hasPerm := false for role := range perm { if _, ok := pages[role]; ok { hasPerm = true page = role break } } if !hasPerm { w.Write([]byte("没有页面权限")) return true } // permMap := make(map[string]bool) result := GetPerm(pl, perm[page]) handleName := GetInstance(r).Get(CURRFUNCNAME).(string) // 这个值就是判断有没有这个操作权限 if !result[permissionMap[handleName]] { w.Write([]byte("没有权限")) return true } // 先拿到pl 对应名称的 索引 // 8 4 2 1 // delete update create read // bit 0 0 0 0 /* 用户表 id 1 权限表 id uid roles perm 1 1 "env" 0-15 2 1 "important" */ return false } ``` ### 客户端文件下载(官方内置方法 mp4文件为例) ```go func PlayVideo(w http.ResponseWriter, r *http.Request) { filename := xmux.Var(r)["filename"] f, err := os.Open(<mp4file>) if err != nil { w.WriteHeader(404) return } defer f.Close() w.Header().Set("Content-Type", "video/mp4") w.Header().Set("X-Download-Options", "noopen") http.ServeContent(w, r, filename, time.Now(), f) ``` ### 客户端文件上传(官方内置方法) ```go func UploadFile(w http.ResponseWriter, r *http.Request) { // 官方默认上传文件的大小最大是32M, 可以通过方法设置新的大小 r.ParseMultipartForm(100 << 20) // 100M // 读取文件 file, header, err := r.FormFile("file") if err != nil { return } f, err := os.OpenFile(<storefilepath>, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() _, err := io.Copy(f, file) if err != nil { return } } ``` ### 编写接口文档, > 使用接口文档, 第一个参数是组路由名, 第二个参数是挂载的路由uri == 组路由里面的静态文件 默认挂在 /-/css/xxx.css 和 /-/js/xxx.js 下 == == 动态路由 /-/api/{int}.html == ```go // 所有的文档相关的方法都以Api开头, 文档只支持单路由的单请求方式, 多请求方式会乱, 调用的时候只会显示到当前位置以上的路由 router := xmux.NewRouter() api := router.ShowApi("/doc") router.ShowApi(api). ApiCreateGroup("test", "api test", "apitest"). //增加了侧边栏 所有组路由或单路由必须加上这个才会显示, 第一个参数是组key, 第二个是组的标题, 第三个是侧边栏url显示的文字 , 或者添加到某个组上 ApiAddGroup(key), 组路由添加的key 会被子路由继承, 如果不想显示可以ApiAddGroup 挂载到其他路由或者 ApiExitGroup, 移除此组 ApiDescribe("这是home接口的测试"). // 接口的简述 ApiReqHeader("content-type", "application/json"). // 接口请求头 ApiReqStruct(&Home{}). // 接口请求参数, 由struct tag 提供(可以是结构体,也可以是结构体指针) ApiRequestTemplate(`{"addr": "shenzhen", "people": 5}`). // 接口请求示例 ApiResStruct(Call{}). // 接口返回参数, 由struct tag 提供 (可以是结构体,也可以是结构体指针) ApiResponseTemplate(`{"code": 0, "msg": ""}`). // 接口返回示例 ApiSupplement("这个是接口的说明补充, 没补充就不填"). // 接口补充 ApiCodeField("133"). // 错误码字段 ApiCodeMsg("1", "56").ApiCodeMsg("3", "akhsdklfhl") // 错误码说明, 多次调用添加多次 ``` > 接口请求参数tag 示例 ```go type Home struct { Addr string `json:"addr" type:"string" need:"是" default:"深圳" information:"家庭住址"` People int `json:"people" type:"int" need:"是" default:"1" information:"有多少个人"` } ``` > 接口接收参数tag 示例, 比请求示例少了 default ``` type Call struct { Code int `json:"code" type:"int" need:"是" information:"错误返回码"` Msg string `json:"msg" type:"string" need:"否" information:"错误信息"` } ``` ### 性能分析 ```go func main() { router := xmux.NewRouter() router.Post("/", nil) // 也可以直接使用内置的 router.AddGroup(xmux.Pprof()) router.Run() } ``` > open http://localhost:8080/debug/pprof can see pprof page ### 查看某handel详细的中间件模块等信息 ```go // 查看某个指定路由的详细信息 router.DebugAssignRoute("/user/info") // 查看某个正则火匹配路由的固定uri来获取某路由的详细 router.DebugIncludeTpl("") // 显示全部的, 不建议使用, router.DebugRoute() router.DebugTpl() ``` > out ``` 2022/01/22 17:16:11 url: /user/info, method: GET, header: map[], module: xmux.funcOrder{"github.com/hyahm/xmux.DefaultPermissionTemplate"}, midware: "" , pages: map[string]struct {}{} ```

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1025 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传