reitit 前后端路由(未完成,修改中)
reitit是我们的前端/后端路由库,路由(Routing)是个非常古老的话题,最早通讯领域的含义是建立通路,如何从A到B。代表了一种映射关系:
除了这个映射,还会常见方法的模式:
- 中间件:Ring
- 拦截器:Re-Frame
- 控制器:Kee-Frame
Reitit
- 数据驱动
- 数据验证clojure.spec
- 高性能
- 清晰的API
- 前后端通用:JVM/浏览器
- 路由和路由处置方法
Reitit的基础
- reitit.core/route 来创建一个路由
- 通过名称/路径来匹配,通过名称来匹配的方式称为反向路由(reverse routing)
- 路由的接口定义
(defprotocol Router (router-name [this]) (routes [this]) (compiled-routes [this]) (options [this]) (route-names [this]) (match-by-path [this path]) (match-by-name [this name] [this name path-params]))
(require '[reitit.core :as r]) (def router (r/router [["/ping" ::ping] ["/users/:id" ::user]])) (r/match-by-path router "/ping") ;#Match{:template "/ping" ; :data {:name :user/ping} ; :result nil ; :path-params {} ; :path "/ping"}
(r/match-by-name router ::ping) ;#Match{:template "/ping" ; :data {:name :user/ping} ; :result nil ; :path-params {} ; :path "/ping"}
路由的语法(hiccup)
路径逐级拼接,数据做递归合并,类似leiningen配置文件的方式 https://github.com/weavejester/meta-merge
(r/router ["/api" {:interceptors [::api]} ["/ping" ::ping] ["/admin" {:roles #{:admin}} ["/users" ::users] ["/db" {:interceptors [::db] :roles ^:replace #{:db-admin}}]]])) (r/router [["/api/ping" {:interceptors [::api] :name ::ping}] ["/api/admin/users" {:interceptors [::api] :roles #{:admin} :name ::users}] ["/api/admin/db" {:interceptors [::api ::db] :roles #{:db-admin}}]])
以上是静态数据结构,路由也支持编程方式生成
(r/router ["/api" {:interceptors [::api]} ["/ping" ::ping] ["/admin" {:roles #{:admin}} ["/users" ::users] ["/db" {:interceptors [::db] :roles ^:replace #{:db-admin}}]]])) (r/router [["/api/ping" {:interceptors [::api] :name ::ping}] ["/api/admin/users" {:interceptors [::api] :roles #{:admin} :name ::users}] ["/api/admin/db" {:interceptors [::api ::db] :roles #{:db-admin}}]])
路由只是数据结构,可以灵活的组合
- 合并,嵌套路由
- 可以有空路由,类似Reagent的 :<>
(-> (r/router [["" {:interceptors [::cors]} ["/api" ::api] ["/ipa" ::ipa]] ["/ping" ::ping]]) (r/routes)) ;[["/api" {:interceptors [::cors], :name ::api}] ; ["/ipa" {:interceptors [::cors], :name ::ipa}] ; ["/ping" {:name ::ping}]]
解决路由冲突的问题:
Reitit fails-fast by default on path & name conflicts
(require '[reitit.core :as r]) (require '[reitit.dev.pretty :as pretty]) (r/router [["/ping"] ["/:user-id/orders"] ["/bulk/:bulk-id"] ["/public/*path"] ["/:version/status"]] {:exception pretty/exception})
路由上挂载的数据
- 可以是任意的数据
- 成功的时候返回一个Match
- 可以在一个Route上查询
使用的例子
- 使用 :role 做用户授权
- 前端返回一个 :view
- 路由处理使用 :middleware :interceptors
- 有状态的路由处理使用 :controllers
- 数据验证使用 :parameters 和 :coercion
前端的例子
(require '[reitit.frontend :as rf]) (require '[reitit.frontend.easy :as rfe]) (defn frontpage-view [match] ...) (defn topics-view [match] ... (rf/nested-view match) (defn topic-view [match] ...) (def router (rf/router [["/" {:name :frontpage :views [frontpage-view]}] ["/topics" {:controllers [load-topics] :views [topics-view]} ["" {:name :topics}] ["/:id" {:name :topic :parameters {:path {:id int?}} :controllers [load-topic] :views [topic-view]}]]])) (rfe/start! router ...)
路由数据验证
- 路由的数据可以是任何数据结构,搞不好会弄得很乱
- 使用clojure.spec 在创建路由的时候验证
- 定义和强制使用路由规范
- 编译时间发现确实的,多余的,拼写错误的key
- 注意使用 spec-tools 和 spell-spec
数据验证失败的例子
(require '[reitit.spec :as spec]) (require '[clojure.spec.alpha :as s]) (s/def ::role #{:admin :user}) (s/def ::roles (s/coll-of ::role :into #{})) (r/router ["/api/admin" {::roles #{:adminz}}] {:validate spec/validate :exception pretty/exception})
第一时间检错:
- 最好是写代码的时候通过linter、静态分析发现
- 编译期间发现也不错(macro,def)
- 开发期间(spec)
- 运行时💩
- 运行时的corner case 💩💩
Coercion 检错的过程是数据类型转化的时候进行,(Json <-> EDN, String <-> EDN)
- 路由数据 :parameters & :coercion
- 使用 reitit.coercion
- 验证使用 clojure.spec
(defprotocol Coercion "Pluggable coercion protocol" (-get-name [this]) (-get-options [this]) (-get-apidocs [this specification data]) (-compile-model [this model name]) (-open-model [this model]) (-encode-error [this error]) (-request-coercer [this type model]) (-response-coercer [this model]))
我们后端选型是Ring + Reitit
Ring是自带路由的,是一个独立的模块
- 根据Path和 :request-method 进行路由
- 支持 :middleware
- middleware可以是一个chain
- ring-handler
- 同步/异步 都支持
- 没有魔法,没有default middleware
例子:
(require '[reitit.ring :as ring]) (def app (ring/ring-handler (ring/router ["/api" {:middleware [wrap-api wrap-roles]} ["/ping" {:get ping-handler]} ["/users" {:middleware [db-middleware] :roles #{:admin} ;; who reads this? :get get-users :post add-user}]]) (ring/create-default-handler))) (app {:uri "/api/ping" :request-method :get}) ; {:status 200, :body "pong"}
访问路由数据
- Match 和 Router 会插入request(作为handler的 参数)
- 其他在chain上的中间件可以读到reqeust上的Match和Router信息
Middleware即数据
- Ring middleware是不透明的函数
- reitit的中间件是一组value
- reitit中间件带有doc,spec,依赖关系
- 替代ring的middleware
- 运行时0负担,是个编译期间行为
(defn roles-middleware [] {:name ::roles-middleware :description "Middleware to enforce roles" :requires #{::session-middleware} :spec (s/keys :opt-un [::roles]) :wrap wrap-roles})
编译middleware
- 中间件知道endpoint
- 路由数据在创建时传入(而不是运行时)
(def roles-middleware {:name ::roles-middleware :description "Middleware to enforce roles" :requires #{::session-middleware} :spec (s/keys :opt-un [::roles]) :compile (fn [{required :roles} _] ;; unmount if there are no roles required (if (seq required) (fn [handler] (fn [{:keys [roles] :as request}] (if (not (set/subset? required roles)) {:status 403, :body "forbidden"} (handler request))))))})
Partial Specs 例子
所有/account 下面的path都需要有role
"All routes under /account should require a role" ;; look ma, not part of request processing! (def roles-defined {:name ::roles-defined :description "requires a ::role for the routes" :spec (s/keys :req-un [::roles])}) ["/api" {:middleware [roles-middleware]} ["/ping"] ["/account" {:middleware [roles-defined]} ["/admin" {:roles #{:admin}}] ["/user" {:roles #{:user}}] ["/manager"]]]
Middleware chain
- 所有的endpoint都有一个middleware的vector
- 这个chain的文档
- chain可以在创建时灵活的:
- 记录
- 完成
- 交叉
- 比如说交叉diff reitit.ring.middleware.dev/print-request-diffs
总结,Reitit:
- 前后端统一的路由库
- 数据驱动
- 动态语言,提前检错
- 使用clojure.spec 检错,数据转化
- 了解性能参数,充分测试
- 多看文档