October 5, 2019
By: Kevin

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 检错,数据转化
  • 了解性能参数,充分测试
  • 多看文档
Tags: clojure clojurescript