如何在 Clojure 中使用 Stuart Sierra 的组件库
How to use Stuart Sierra's component library in Clojure
我正在努力了解如何使用 Stuart Sierra's component library within a Clojure app. From watching his Youtube video,我想我已经很好地掌握了导致他创建库的问题;然而,我正在努力研究如何在一个新的、相当复杂的项目中实际使用它。
我意识到这听起来很模糊,但感觉我缺少一些关键概念,一旦理解了它,我就会很好地掌握如何使用组件。换句话说,Stuart 的文档和视频相当详细地介绍了组件的内容和原因,但我错过了方法。
是否有任何详细的 tutorial/walkthrough 涉及:
- 为什么要将组件用于重要的 Clojure 应用程序
- 一种方法,用于分解重要的 Clojure 应用程序中的功能,以便可以以合理的最佳方式实现组件。当你所拥有的只是例如一个数据库、一个应用程序服务器和一个 Web 服务器层,但我很难理解如何将它用于具有许多不同层的系统,这些层都需要一致地协同工作
- 接近development/testing/failover/etc的方法。在使用组件构建的重要 Clojure 应用程序中
提前致谢
简而言之,Component是一个专门的DI框架。它可以在给定两个映射的情况下设置注入系统:系统映射和依赖映射。
让我们看看一个虚构的 Web 应用程序(免责声明,我在没有实际 运行 的情况下在表格中输入它):
(ns myapp.system
(:require [com.stuartsierra.component :as component]
;; we'll talk about myapp.components later
[myapp.components :as app-components]))
(defn system-map [config] ;; it's conventional to have a config map, but it's optional
(component/system-map
;; construct all components + static config
{:db (app-components/map->Db (:db config))
:handler (app-components/map->AppHandler (:handler config))
:server (app-components/map->Server (:web-server config))}))
(defn dependency-map
;; list inter-dependencies in either:
;; {:key [:dependency1 :dependency2]} form or
;; {:key {:name-arg1 :dependency1
;; :name-arg2 :dependency2}} form
{:handler [:db]
:server {:app :handler})
;; calling this creates our system
(def create-system [& [config]]
(component/system-using
(system-map (or config {})
(dependency-map)))
这允许我们在需要时调用 (create-system)
来创建整个应用程序的新实例。
使用(component/start created-system)
,我们可以运行 系统提供的服务。在这种情况下,它是侦听端口和打开的数据库连接的网络服务器。
最后,我们可以用 (component/stop created-system)
来停止它以从 运行ning 停止系统(例如 - 停止网络服务器,断开与数据库的连接)。
现在让我们看看我们的应用程序 components.clj
:
(ns myapp.components
(:require [com.stuartsierra.component :as component]
;; lots of app requires would go here
;; I'm generalizing app-specific code to
;; this namespace
[myapp.stuff :as app]))
(defrecord Db [host port]
component/Lifecycle
(start [c]
(let [conn (app/db-connect host port)]
(app/db-migrate conn)
(assoc c :connection conn)))
(stop [c]
(when-let [conn (:connection c)]
(app/db-disconnect conn))
(dissoc c :connection)))
(defrecord AppHandler [db cookie-config]
component/Lifecycle
(start [c]
(assoc c :handler (app/create-handler cookie-config db)))
(stop [c] c))
;; you should probably use the jetty-component instead
;; https://github.com/weavejester/ring-jetty-component
(defrecord Server [app host port]
component/Lifecycle
(start [c]
(assoc c :server (app/create-and-start-jetty-server
{:app (:handler app)
:host host
:port port})))
(stop [c]
(when-let [server (:server c)]
(app/stop-jetty-server server)
(dissoc c :server)))
那么我们刚刚做了什么?我们得到了一个可重新加载的系统。我认为一些使用 figwheel 的 clojurescript 开发人员开始看到相似之处。
这意味着我们可以在重新加载代码后轻松重启系统。进入 user.clj
!
(ns user
(:require [myapp.system :as system]
[com.stuartsierra.component :as component]
[clojure.tools.namespace.repl :refer (refresh refresh-all)]
;; dev-system.clj only contains: (def the-system)
[dev-system :refer [the-system]])
(def system-config
{:web-server {:port 3000
:host "localhost"}
:db {:host 3456
:host "localhost"}
:handler {cookie-config {}}}
(def the-system nil)
(defn init []
(alter-var-root #'the-system
(constantly system/create-system system-config)))
(defn start []
(alter-var-root #'the-system component/start))
(defn stop []
(alter-var-root #'the-system
#(when % (component/stop %))))
(defn go []
(init)
(start))
(defn reset []
(stop)
(refresh :after 'user/go))
对于运行一个系统,我们可以在我们的repl中输入:
(user)> (reset)
这将重新加载我们的代码,并重新启动整个系统。如果正在运行,它将关闭正在 运行ning 的现有系统。
我们得到其他好处:
- 端到端测试很简单,只需编辑配置或替换组件以指向进程内服务(我用它指向进程内 kafka 服务器进行测试)。
- 理论上您可以为同一个 JVM 多次生成您的应用程序(不像第一点那么实用)。
- 当您更改代码并且必须重新启动服务器时,您不需要重新启动 REPL
- 与环形重新加载不同,我们得到了一种统一的方式来重新启动我们的应用程序,无论其用途如何:后台工作者、微服务或机器学习系统都可以以相同的方式构建。
值得注意的是,由于一切都在进行中,因此组件不会处理任何与故障转移、分布式系统或错误代码相关的事情;)
有很多 "resources"(也称为有状态对象)组件可以帮助您在服务器内进行管理:
- 与服务(队列、数据库等)的连接
- 时间流逝(调度程序、cron 等)
- 日志记录(应用日志记录、异常日志记录、指标等)
- 文件 IO(blob 存储、本地文件系统等)
- 传入客户端连接(网络、套接字等)
- OS 资源(设备、线程池等)
如果您只有一个 Web 服务器 + 数据库,那么组件似乎有点矫枉过正。但是现在很少有网络应用程序是这样的。
旁注: 将 the-system
移动到另一个名称空间可减少在开发时刷新 the-system
var 的可能性(例如 - 调用 refresh
而不是 reset
).
我正在努力了解如何使用 Stuart Sierra's component library within a Clojure app. From watching his Youtube video,我想我已经很好地掌握了导致他创建库的问题;然而,我正在努力研究如何在一个新的、相当复杂的项目中实际使用它。
我意识到这听起来很模糊,但感觉我缺少一些关键概念,一旦理解了它,我就会很好地掌握如何使用组件。换句话说,Stuart 的文档和视频相当详细地介绍了组件的内容和原因,但我错过了方法。
是否有任何详细的 tutorial/walkthrough 涉及:
- 为什么要将组件用于重要的 Clojure 应用程序
- 一种方法,用于分解重要的 Clojure 应用程序中的功能,以便可以以合理的最佳方式实现组件。当你所拥有的只是例如一个数据库、一个应用程序服务器和一个 Web 服务器层,但我很难理解如何将它用于具有许多不同层的系统,这些层都需要一致地协同工作
- 接近development/testing/failover/etc的方法。在使用组件构建的重要 Clojure 应用程序中
提前致谢
简而言之,Component是一个专门的DI框架。它可以在给定两个映射的情况下设置注入系统:系统映射和依赖映射。
让我们看看一个虚构的 Web 应用程序(免责声明,我在没有实际 运行 的情况下在表格中输入它):
(ns myapp.system
(:require [com.stuartsierra.component :as component]
;; we'll talk about myapp.components later
[myapp.components :as app-components]))
(defn system-map [config] ;; it's conventional to have a config map, but it's optional
(component/system-map
;; construct all components + static config
{:db (app-components/map->Db (:db config))
:handler (app-components/map->AppHandler (:handler config))
:server (app-components/map->Server (:web-server config))}))
(defn dependency-map
;; list inter-dependencies in either:
;; {:key [:dependency1 :dependency2]} form or
;; {:key {:name-arg1 :dependency1
;; :name-arg2 :dependency2}} form
{:handler [:db]
:server {:app :handler})
;; calling this creates our system
(def create-system [& [config]]
(component/system-using
(system-map (or config {})
(dependency-map)))
这允许我们在需要时调用 (create-system)
来创建整个应用程序的新实例。
使用(component/start created-system)
,我们可以运行 系统提供的服务。在这种情况下,它是侦听端口和打开的数据库连接的网络服务器。
最后,我们可以用 (component/stop created-system)
来停止它以从 运行ning 停止系统(例如 - 停止网络服务器,断开与数据库的连接)。
现在让我们看看我们的应用程序 components.clj
:
(ns myapp.components
(:require [com.stuartsierra.component :as component]
;; lots of app requires would go here
;; I'm generalizing app-specific code to
;; this namespace
[myapp.stuff :as app]))
(defrecord Db [host port]
component/Lifecycle
(start [c]
(let [conn (app/db-connect host port)]
(app/db-migrate conn)
(assoc c :connection conn)))
(stop [c]
(when-let [conn (:connection c)]
(app/db-disconnect conn))
(dissoc c :connection)))
(defrecord AppHandler [db cookie-config]
component/Lifecycle
(start [c]
(assoc c :handler (app/create-handler cookie-config db)))
(stop [c] c))
;; you should probably use the jetty-component instead
;; https://github.com/weavejester/ring-jetty-component
(defrecord Server [app host port]
component/Lifecycle
(start [c]
(assoc c :server (app/create-and-start-jetty-server
{:app (:handler app)
:host host
:port port})))
(stop [c]
(when-let [server (:server c)]
(app/stop-jetty-server server)
(dissoc c :server)))
那么我们刚刚做了什么?我们得到了一个可重新加载的系统。我认为一些使用 figwheel 的 clojurescript 开发人员开始看到相似之处。
这意味着我们可以在重新加载代码后轻松重启系统。进入 user.clj
!
(ns user
(:require [myapp.system :as system]
[com.stuartsierra.component :as component]
[clojure.tools.namespace.repl :refer (refresh refresh-all)]
;; dev-system.clj only contains: (def the-system)
[dev-system :refer [the-system]])
(def system-config
{:web-server {:port 3000
:host "localhost"}
:db {:host 3456
:host "localhost"}
:handler {cookie-config {}}}
(def the-system nil)
(defn init []
(alter-var-root #'the-system
(constantly system/create-system system-config)))
(defn start []
(alter-var-root #'the-system component/start))
(defn stop []
(alter-var-root #'the-system
#(when % (component/stop %))))
(defn go []
(init)
(start))
(defn reset []
(stop)
(refresh :after 'user/go))
对于运行一个系统,我们可以在我们的repl中输入:
(user)> (reset)
这将重新加载我们的代码,并重新启动整个系统。如果正在运行,它将关闭正在 运行ning 的现有系统。
我们得到其他好处:
- 端到端测试很简单,只需编辑配置或替换组件以指向进程内服务(我用它指向进程内 kafka 服务器进行测试)。
- 理论上您可以为同一个 JVM 多次生成您的应用程序(不像第一点那么实用)。
- 当您更改代码并且必须重新启动服务器时,您不需要重新启动 REPL
- 与环形重新加载不同,我们得到了一种统一的方式来重新启动我们的应用程序,无论其用途如何:后台工作者、微服务或机器学习系统都可以以相同的方式构建。
值得注意的是,由于一切都在进行中,因此组件不会处理任何与故障转移、分布式系统或错误代码相关的事情;)
有很多 "resources"(也称为有状态对象)组件可以帮助您在服务器内进行管理:
- 与服务(队列、数据库等)的连接
- 时间流逝(调度程序、cron 等)
- 日志记录(应用日志记录、异常日志记录、指标等)
- 文件 IO(blob 存储、本地文件系统等)
- 传入客户端连接(网络、套接字等)
- OS 资源(设备、线程池等)
如果您只有一个 Web 服务器 + 数据库,那么组件似乎有点矫枉过正。但是现在很少有网络应用程序是这样的。
旁注: 将 the-system
移动到另一个名称空间可减少在开发时刷新 the-system
var 的可能性(例如 - 调用 refresh
而不是 reset
).