clojure GUI编程-3
目录
1 简介
这部分主要是使用re-frame构建一个SPA程序,完成okex行情信息的显示。 css
关于re-frame的设计理念和使用方法,参考官方文档。 html
2 实现过程
2.1 建立项目
使用re-frame-template建立项目: java
lein new re-frame okex-web +10x +re-com +cider
+cider配合emacs使用, +re-com使用现成的web gui组件, +10x 用于re-frame的调试。 python
在emacs下使用cider-jack-in-cljs后,执行下面的代码转到cljs repl: react
(use 'figwheel-sidecar.repl-api) (start-figwheel!) (cljs-repl)
发现cljs不能正确输入,会出现一个stdin的minibuffer,解决方法参考 https://clojureverse.org/t/emacs-figwheel-main-why-stdin-in-the-minibuffer/3955/8, 修改figwheel-sidecar的版本号为"0.5.18",cider/piggieback的版本号为"0.4.1",主要是为了兼容nrepl 0.6。 git
因为要使用ajax请求API,须要添加http-fx依赖,最后的project.clj以下: github
1: (defproject okex-web "0.1.0-SNAPSHOT" 2: :dependencies [[org.clojure/clojure "1.10.0"] 3: [org.clojure/clojurescript "1.10.520"] 4: [reagent "0.8.1"] 5: [re-frame "0.10.6"] 6: [re-com "2.4.0"] 7: [day8.re-frame/http-fx "0.1.6"] 8: [camel-snake-kebab "0.4.0"] ;; 命名转换 9: [com.rpl/specter "1.1.2"] ;; data selector 10: ] 11: 12: :plugins [[lein-cljsbuild "1.1.7"]] 13: 14: :min-lein-version "2.5.3" 15: 16: :source-paths ["src/clj" "src/cljs"] 17: 18: :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 19: 20: :figwheel {:css-dirs ["resources/public/css"]} 21: 22: :profiles 23: {:dev 24: {:dependencies [[binaryage/devtools "0.9.10"] 25: [day8.re-frame/re-frame-10x "0.3.7-react16"] 26: [day8.re-frame/tracing "0.5.1"] 27: [figwheel-sidecar "0.5.18"] 28: [cider/piggieback "0.4.1"]] 29: :repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]} 30: :plugins [[lein-figwheel "0.5.18"]]} 31: 32: :prod { :dependencies [[day8.re-frame/tracing-stubs "0.5.1"]]} 33: } 34: 35: :cljsbuild 36: {:builds 37: [{:id "dev" 38: :source-paths ["src/cljs"] 39: :figwheel {:on-jsload "okex-web.core/mount-root"} 40: :compiler {:main okex-web.core 41: :output-to "resources/public/js/compiled/app.js" 42: :output-dir "resources/public/js/compiled/out" 43: :asset-path "js/compiled/out" 44: :source-map-timestamp true 45: :preloads [devtools.preload 46: day8.re-frame-10x.preload] 47: :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true 48: "day8.re_frame.tracing.trace_enabled_QMARK_" true} 49: :external-config {:devtools/config {:features-to-install :all}} 50: }} 51: 52: {:id "min" 53: :source-paths ["src/cljs"] 54: :compiler {:main okex-web.core 55: :output-to "resources/public/js/compiled/app.js" 56: :optimizations :advanced 57: :closure-defines {goog.DEBUG false} 58: :pretty-print false}} 59: 60: 61: ]} 62: )
2.2 绕过CORS
由于要跨域使用API,须要绕过浏览器的跨域限制,具体方法参考Bypass CORS Errors When Testing APIs Locally。 web
对于chrome,使用下面的命令行启动: ajax
chromium --disable-web-security --user-data-dir ./chromeuser
后来发现Allow CORS插件比较好用,支持主流浏览器,建议使用。 sql
2.3 re-frame的核心思想
re-frame内部使用一个ratom做为db层进行数据存储1。
修改db的事件使用reg-event-db注册,而后其它地方(其它事件中,或者view中)就能够经过dispatch这个事件发布消息(至关于发布者)。
经过reg-sub注册对db的访问,在view中经过subscribe订阅注册的sub(订阅者),当sub指向的数据更改,view就会自动刷新。
2.4 注册事件
主要是进行数据修改的事件,如:保存币对信息,设置当前选择的基准货币和交易货币信息,保存深度数据和异步请求API等。 具体参考events.cljs:
1: (ns okex-web.events 2: (:require 3: [re-frame.core :as re-frame] 4: [okex-web.db :as db] 5: [okex-web.utils :refer [evt-db2]] 6: [ajax.core :as ajax] 7: [goog.string :as gstring] 8: [goog.string.format] 9: [camel-snake-kebab.core :as csk] 10: [com.rpl.specter :as s :refer-macros [select select-one transform]] 11: [day8.re-frame.tracing :refer-macros [fn-traced defn-traced]] 12: )) 13: 14: ;;;;;;;;;;;;;;;;;;;;;;; helper functions 15: (defn format-map-keys 16: "把map的keyword转换为clojure格式" 17: [m] 18: (s/transform [s/ALL s/MAP-KEYS] csk/->kebab-case-keyword m)) 19: 20: (defn format-depth-data 21: "格式化深度数据" 22: [data] 23: (transform [(s/multi-path :asks :bids) s/INDEXED-VALS] 24: (fn [[idx [price amount order-count]]] 25: [idx {:pos idx 26: :price price 27: :amount amount 28: :order-count order-count}]) 29: data)) 30: 31: (defn get-instrument-id 32: "得到当前币对名称" 33: [db] 34: (let [base-coin (:base-coin db) 35: quote-coin (:quote-coin db)] 36: (s/select-one [s/ALL 37: #(and (= (:base-currency %) base-coin) 38: (= (:quote-currency %) quote-coin)) 39: :instrument-id] 40: (:instruments db)))) 41: 42: (defn get-quote-coins 43: [db base-coin] 44: (->> (:instruments db) 45: (select [s/ALL #(= (:base-currency %) base-coin) :quote-currency]) 46: set 47: sort)) 48: 49: ;;;;;;;;;;;;;;;;;;;;;;;;; timer event 50: 51: (defn dispatch-timer-event 52: [] 53: (let [now (js/Date.)] 54: (re-frame/dispatch [:timer now]))) ;; <-- dispatch used 55: 56: ;; 200毫秒刷新1次 57: (defonce do-timer (js/setInterval dispatch-timer-event 200)) 58: 59: ;;;;;;;;;;;;;;;;;;;;;;; event db 60: (re-frame/reg-event-db 61: ::initialize-db 62: (fn-traced [_ _] 63: db/default-db)) 64: 65: ;; 设置标题 66: (evt-db2 :set-name [:name]) 67: 68: ;; 保存全部币对信息 69: (re-frame/reg-event-db 70: :set-instruments 71: (fn-traced [db [_ data]] 72: (->> (format-map-keys data) 73: (assoc db :instruments)))) 74: 75: (evt-db2 :set-quote-coins [:quote-coins]) 76: 77: (evt-db2 :set-quote-coin [:quote-coin]) 78: 79: (re-frame/reg-event-db 80: :set-depth-data 81: (fn-traced [db [_ data]] 82: (->> (format-depth-data data) 83: (assoc db :depth-data)))) 84: 85: (re-frame/reg-event-db 86: :set-base-coin 87: (fn-traced [db [_ base-coin]] 88: (re-frame/dispatch [:set-quote-coins (get-quote-coins db base-coin)]) 89: (assoc db :base-coin base-coin))) 90: 91: ;; 保存错误信息 92: (re-frame/reg-event-db 93: :set-error 94: (fn-traced [db [_ path error]] 95: (assoc db :error {:path path 96: :msg error}))) 97: 98: ;; 清除错误信息 99: (re-frame/reg-event-db 100: :clear-error 101: (fn-traced [db _] 102: (assoc db :error nil))) 103: 104: ;;; ================ api 请求 105: (re-frame/reg-event-fx 106: ::fetch-instruments 107: (fn-traced [_ _] 108: {:dispatch [:clear-error] 109: :http-xhrio {:method :get 110: :uri "https://www.okex.com/api/spot/v3/instruments" 111: :timeout 8000 112: :response-format (ajax/json-response-format {:keywords? true}) 113: :on-success [:set-instruments] 114: :on-failure [:set-error :fetch-instruments]}})) 115: 116: (re-frame/reg-event-fx 117: ::fetch-depth-data 118: (fn-traced [_ [_ instrument-id]] 119: {:dispatch [:clear-error] 120: :http-xhrio {:method :get 121: :uri (gstring/format "https://www.okex.com/api/spot/v3/instruments/%s/book" instrument-id) 122: :timeout 8000 123: :response-format (ajax/json-response-format {:keywords? true}) 124: :on-success [:set-depth-data] 125: :on-failure [:set-error :fetch-depth-data]}})) 126: 127: ;;; =================== fx event 128: (re-frame/reg-event-fx 129: :timer 130: (fn [{:keys [db]} _] 131: (when-let [instrument-id (get-instrument-id db)] 132: {:dispatch [::fetch-depth-data instrument-id]})))
注意reg-event-fx和reg-event-db传递的函数参数是不一样的,reg-event-db的第一个参数是db,reg-event-fx的第一个参数是coeffects2。
2.5 注册订阅
用于访问db层的数据,具体参考subs.cljs:
1: (ns okex-web.subs 2: (:require 3: [re-frame.core :as re-frame] 4: [com.rpl.specter :as s :refer-macros [select transform]] 5: )) 6: 7: ;; 标题,懒得更名字了 8: (re-frame/reg-sub 9: ::name 10: (fn [db] 11: (:name db))) 12: 13: ;; 币对信息 14: (re-frame/reg-sub 15: ::instruments 16: (fn [db] 17: (:instruments db))) 18: 19: ;; 深度数据 20: (re-frame/reg-sub 21: ::depth-data 22: (fn [db] 23: (:depth-data db))) 24: 25: ;; 注意base-coins是基于instruments更新的,不能经过直接访问db的方式获取base-coins, 26: ;; 不然instruments刷新,base-coins的订阅不会自动刷新。 27: (re-frame/reg-sub 28: ::base-coins 29: :<- [::instruments] 30: (fn [instruments] 31: (-> (select [s/ALL :base-currency] instruments) 32: set 33: sort))) 34: 35: (re-frame/reg-sub 36: ::quote-coins 37: (fn [db] 38: (:quote-coins db))) 39: 40: (re-frame/reg-sub 41: ::base-coin 42: (fn [db] 43: (:base-coin db))) 44: 45: (re-frame/reg-sub 46: ::quote-coin 47: (fn [db] 48: (:quote-coin db))) 49: 50: ;; 错误信息 51: (re-frame/reg-sub 52: ::error 53: (fn [db] 54: (:error db)))
2.6 界面代码
订阅subs,显示界面,具体参考views.cljs:
1: (ns okex-web.views 2: (:require 3: [re-frame.core :as re-frame] 4: [re-com.core :as re-com] 5: [reagent.core :refer [atom]] 6: [okex-web.utils :refer [>evt <sub]] 7: [com.rpl.specter :as s] 8: [okex-web.subs :as subs] 9: )) 10: 11: (defn depth-table 12: [title data] 13: [:div.container 14: [:h4.text-center title] 15: [:table.table.table-bordered 16: [:thead 17: [:tr 18: [:th "价位"] 19: [:th "价格"] 20: [:th "数量"] 21: [:th "订单数"]]] 22: [:tbody 23: (for [row data] 24: ^{:key (str title (:pos row))} 25: [:tr 26: [:td (:pos row)] 27: [:td (:price row)] 28: [:td (:amount row)] 29: [:td (:order-count row)]])]]]) 30: 31: (defn vec->dropdown-choices 32: ([v] (vec->dropdown-choices v nil)) 33: ([v group] 34: (map #(hash-map :id % :label % :group group) v))) 35: 36: (defn depth-view [] 37: (let [base-coins (re-frame/subscribe [::subs/base-coins]) 38: quote-coins (re-frame/subscribe [::subs/quote-coins]) 39: base-coin (re-frame/subscribe [::subs/base-coin]) 40: quote-coin (re-frame/subscribe [::subs/quote-coin]) 41: depth-data (re-frame/subscribe [::subs/depth-data])] 42: [re-com/v-box 43: :gap "10px" 44: :children [[re-com/h-box 45: :gap "10px" 46: :align :center 47: :children [[re-com/single-dropdown 48: :choices (vec->dropdown-choices @base-coins) 49: :model @base-coin 50: :placeholder "选择基准币种" 51: :filter-box? true 52: :on-change #(>evt [:set-base-coin %])] 53: [re-com/gap :size "10px"] 54: [re-com/single-dropdown 55: :choices (vec->dropdown-choices @quote-coins @base-coin) 56: :model @quote-coin 57: :placeholder "选择计价币种" 58: :on-change #(>evt [:set-quote-coin %]) 59: ] 60: ]] 61: [re-com/h-split 62: :panel-1 [depth-table "买入信息" (:bids @depth-data)] 63: :panel-2 [depth-table "卖出信息" (:asks @depth-data)]] 64: ]])) 65: 66: 67: (defn title [] 68: [re-com/title 69: :label (<sub [::subs/name]) 70: :class "center-block" 71: :level :level1]) 72: 73: (defn error 74: "显示错误" 75: [] 76: (let [error (re-frame/subscribe [::subs/error])] 77: (when @error 78: [re-com/alert-box 79: :alert-type :danger 80: :heading (str "错误!!! " (:path @error)) 81: :body [:span (str (:msg @error))]]))) 82: 83: (defn main-panel [] 84: [:div.container 85: [re-com/v-box 86: :height "100%" 87: :children [[title] 88: [error] 89: [depth-view] 90: ]]])
2.7 发布
使用如下命令编译生成js文件到resources/public文件夹:
lein do clean, cljsbuild once min
能够看到release发布只有一个app.js,文件大小不到900K。 在浏览打开index.html就可使用了。注意必须关掉浏览器的CORS限制。
图1 网页运行界面截图
3 总结
re-frame写SPA程序很是强大,总体架构比较清晰,值得学习。示例项目完整代码。