clojure GUI編程-3

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 Locallyweb

對於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限制。

https://img2018.cnblogs.com/blog/1545892/201905/1545892-20190531200434625-1037160174.jpg

圖1  網頁運行界面截圖

3 總結

re-frame寫SPA程序很是強大,總體架構比較清晰,值得學習。示例項目完整代碼

腳註:

1

關於ApplicationState的官方文檔

2

關於coeffects的官方文檔

做者: ntestoc

Created: 2019-05-31 五 20:04

相關文章
相關標籤/搜索