Home Resources

Clojure Web 开发

Table of Contents

HTTP 服务

Clojure 中,一般使用库将 HTTP 1请求抽象成一个普通的 map,其中包含 uri, method, headers, body(可以参照这类抽象的一个流行库 ring 对请求的抽象),然后书写针对这个 map 输入的函数用于处理该请求。

(fn [_req] {:status 200 :body "hello world"})

不过,一般我们会引入一个路由管理的库,来将请求 dispatch 到特定的请求处理函数,目前最为流行的路由管理的库应该就是 reitit 了2

(require '[reitit.core :as r])

(def router (r/router
             [["/api/ping" ::ping]
              ["/api/orders/:id" ::order]]))

(r/match-by-path router "/api/ping")
;; #Match{:template "/api/ping"
;;        :data {:name ::ping}   ; 很容易对路由给定一个请求的处理函数
;;        :result nil
;;        :path-params {}
;;        :path "/api/ping"}

另外,有时希望执行一些通用的请求处理功能,比如校验请求验证信息(如请求头 Authentication 中的 token 信息),我们可以在自己的业务处理函数中调用相关逻辑,不过一般会通过 middleware3 或者 interceptor4 这种方式复用这部分逻辑。

;; a middleware
(defn wrap-check-auth [h tokens]
  (fn [{:keys [request] :as req}]
    (let [authed? (some-> request (get-in [:headers "Authorization"]) tokens)]
      (h (assoc req :authed? authed?)))))

(defn hello [{:keys [authed?] :as req}]
  {:status 200 :body (str "hello " authed?)})
(defn bye [{:keys [authed?] :as req}]
  {:status 200 :body (str "bye " authed?)})

(def tokens #{"token-0"})
(def hello' (wrap-check-auth hello tokens))
(def bye'   (wrap-check-auth bye tokens))

诸如 cookie 解析,form 数据解析,内容格式协商与转换(content negotiation)这类常见功能都有现成的 middleware/interceptor 的库5

Clojure 也有一些类似 web 开发框架的库,比如 kit-clj, Luminus,可以使用它们生成项目,很容易从这些生成的项目入手开始定制你自己的项目。

数据建模

Clojure 是一门动态语言,它鼓励你使用非常通用的数据结构对你的领域数据进行建模6,也即,大部分时候,我们使用构成这门语言本身的那些数据结构进行数据建模:keyword, symbol, map, list, set, vector.

而类型给我们提供的好处,通过 spec, malli, schema 加上 clj-kondo 针对它们的静态分析支持,也都能很好的得到。

而像 malli 这种数据驱动的 schema 定义方式,让 schema 本身也成为普通数据,任你方便的操作,让整个建模过程更为有趣。

状态管理

一种常见的实践是,我们将一个启动的应用看成是一些相互依赖的组件(其中一些是状态组件)构成的系统。

手工维护这些组件依赖关系与它们初始化是一件无聊的事情,有一些库帮助我们实现这个操作:Integrant, Component, Mount.

特别的,Integrant 更是允许你将应用系统的构建编程数据驱动的。

(require '[integrant.core :as ig])
;; 实现组件 :adapter/jetty & :handler/greet
(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}] ...)
(defmethod ig/init-key :handler/greet [_ {:keys [name]}] ...)

;; 系统配置(数据)
(def config {:adapter/jetty {:port 8080, :handler #ig/ref :handler/greet}
             :handler/greet {:name "Alice"}})
;; 配置 -> 运行时系统
(def system (ig/init config))

Stuart Sierra 的 reloaded workflow 这篇文章也值得一读。

待续

Footnotes:

Author: lotuc, Published at: 2023-05-09 Tue 00:00, Modified At: 2023-05-08 Mon 23:55 (orgmode - Publishing)