首页 > 其他 > 详细

clojure GUI编程-3

时间:2019-05-31 16:50:36      阅读:115      评论:0      收藏:0      [点我收藏+]
clojure GUI编程-3

clojure GUI编程-3

1 简介

这部分主要是使用re-frame构建一个SPA程序,完成okex行情信息的显示。

关于re-frame的设计理念和使用方法,参考官方文档

2 实现过程

2.1 创建项目

使用re-frame-template创建项目:

lein new re-frame okex-web +10x +re-com +cider

+cider配合emacs使用, +re-com使用现成的web gui组件, +10x 用于re-frame的调试。

在emacs下使用cider-jack-in-cljs后,执行下面的代码转到cljs repl:

(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。

由于要使用ajax请求API,需要添加http-fx依赖,最后的project.clj如下:

 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

对于chrome,使用下面的命令行启动:

chromium --disable-web-security --user-data-dir ./chromeuser

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:

 63: (ns okex-web.events
 64:   (:require
 65:    [re-frame.core :as re-frame]
 66:    [okex-web.db :as db]
 67:    [okex-web.utils :refer [evt-db2]]
 68:    [ajax.core :as ajax]
 69:    [goog.string :as gstring]
 70:    [goog.string.format]
 71:    [camel-snake-kebab.core :as csk]
 72:    [com.rpl.specter :as s :refer-macros [select select-one transform]]
 73:    [day8.re-frame.tracing :refer-macros [fn-traced defn-traced]]
 74:    ))
 75: 
 76: ;;;;;;;;;;;;;;;;;;;;;;; helper functions
 77: (defn format-map-keys
 78:   "把map的keyword转换为clojure格式"
 79:   [m]
 80:   (s/transform [s/ALL s/MAP-KEYS] csk/->kebab-case-keyword m))
 81: 
 82: (defn format-depth-data
 83:   "格式化深度数据"
 84:   [data]
 85:   (transform [(s/multi-path :asks :bids) s/INDEXED-VALS]
 86:              (fn [[idx [price amount order-count]]]
 87:                [idx {:pos idx
 88:                      :price price
 89:                      :amount amount
 90:                      :order-count order-count}])
 91:              data))
 92: 
 93: (defn get-instrument-id
 94:   "获得当前币对名称"
 95:   [db]
 96:   (let [base-coin (:base-coin db)
 97:         quote-coin (:quote-coin db)]
 98:     (s/select-one [s/ALL
 99:                    #(and (= (:base-currency %) base-coin)
100:                          (= (:quote-currency %) quote-coin))
101:                    :instrument-id]
102:                   (:instruments db))))
103: 
104: (defn get-quote-coins
105:   [db base-coin]
106:   (->> (:instruments db)
107:        (select [s/ALL #(= (:base-currency %) base-coin) :quote-currency])
108:        set
109:        sort))
110: 
111: ;;;;;;;;;;;;;;;;;;;;;;;;; timer event
112: 
113: (defn dispatch-timer-event
114:   []
115:   (let [now (js/Date.)]
116:     (re-frame/dispatch [:timer now])))  ;; <-- dispatch used
117: 
118: ;; 200毫秒刷新1次
119: (defonce do-timer (js/setInterval dispatch-timer-event 200))
120: 
121: ;;;;;;;;;;;;;;;;;;;;;;; event db
122: (re-frame/reg-event-db
123:  ::initialize-db
124:  (fn-traced [_ _]
125:    db/default-db))
126: 
127: ;; 设置标题
128: (evt-db2 :set-name [:name])
129: 
130: ;; 保存所有币对信息
131: (re-frame/reg-event-db
132:  :set-instruments
133:  (fn-traced [db [_ data]]
134:             (->> (format-map-keys data)
135:                  (assoc db :instruments))))
136: 
137: (evt-db2 :set-quote-coins [:quote-coins])
138: 
139: (evt-db2 :set-quote-coin [:quote-coin])
140: 
141: (re-frame/reg-event-db
142:  :set-depth-data
143:  (fn-traced [db [_ data]]
144:             (->> (format-depth-data data)
145:                  (assoc db :depth-data))))
146: 
147: (re-frame/reg-event-db
148:  :set-base-coin
149:  (fn-traced [db [_ base-coin]]
150:             (re-frame/dispatch [:set-quote-coins (get-quote-coins db base-coin)])
151:             (assoc db :base-coin base-coin)))
152: 
153: ;; 保存错误信息
154: (re-frame/reg-event-db
155:  :set-error
156:  (fn-traced [db [_ path error]]
157:             (assoc db :error {:path path
158:                               :msg error})))
159: 
160: ;; 清除错误信息
161: (re-frame/reg-event-db
162:  :clear-error
163:  (fn-traced [db _]
164:             (assoc db :error nil)))
165: 
166: ;;; ================ api 请求
167: (re-frame/reg-event-fx
168:  ::fetch-instruments
169:  (fn-traced [_ _]
170:             {:dispatch [:clear-error]
171:              :http-xhrio {:method :get
172:                           :uri "https://www.okex.com/api/spot/v3/instruments"
173:                           :timeout 8000
174:                           :response-format (ajax/json-response-format {:keywords? true})
175:                           :on-success [:set-instruments]
176:                           :on-failure [:set-error :fetch-instruments]}}))
177: 
178: (re-frame/reg-event-fx
179:  ::fetch-depth-data
180:  (fn-traced [_ [_ instrument-id]]
181:             {:dispatch [:clear-error]
182:              :http-xhrio {:method :get
183:                           :uri (gstring/format "https://www.okex.com/api/spot/v3/instruments/%s/book" instrument-id)
184:                           :timeout 8000
185:                           :response-format (ajax/json-response-format {:keywords? true})
186:                           :on-success [:set-depth-data]
187:                           :on-failure [:set-error :fetch-depth-data]}}))
188: 
189: ;;; =================== fx event
190: (re-frame/reg-event-fx
191:  :timer
192:  (fn [{:keys [db]} _]
193:    (when-let [instrument-id (get-instrument-id db)]
194:      {: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:

195: (ns okex-web.subs
196:   (:require
197:    [re-frame.core :as re-frame]
198:    [com.rpl.specter :as s :refer-macros [select transform]]
199:    ))
200: 
201: ;; 标题,懒得改名字了
202: (re-frame/reg-sub
203:  ::name
204:  (fn [db]
205:    (:name db)))
206: 
207: ;; 币对信息
208: (re-frame/reg-sub
209:  ::instruments
210:  (fn [db]
211:    (:instruments db)))
212: 
213: ;; 深度数据
214: (re-frame/reg-sub
215:  ::depth-data
216:  (fn [db]
217:    (:depth-data db)))
218: 
219: ;; 注意base-coins是基于instruments更新的,不能通过直接访问db的方式获取base-coins,
220: ;; 否则instruments刷新,base-coins的订阅不会自动刷新。
221: (re-frame/reg-sub
222:  ::base-coins
223:  :<- [::instruments]
224:  (fn [instruments]
225:    (-> (select [s/ALL :base-currency] instruments)
226:        set
227:        sort)))
228: 
229: (re-frame/reg-sub
230:  ::quote-coins
231:  (fn [db]
232:    (:quote-coins db)))
233: 
234: (re-frame/reg-sub
235:  ::base-coin
236:  (fn [db]
237:    (:base-coin db)))
238: 
239: (re-frame/reg-sub
240:  ::quote-coin
241:  (fn [db]
242:    (:quote-coin db)))
243: 
244: ;; 错误信息
245: (re-frame/reg-sub
246:  ::error
247:  (fn [db]
248:    (:error db)))

2.6 界面代码

订阅subs,显示界面,具体参考views.cljs:

249: (ns okex-web.views
250:   (:require
251:    [re-frame.core :as re-frame]
252:    [re-com.core :as re-com]
253:    [reagent.core :refer [atom]]
254:    [okex-web.utils :refer [>evt <sub]]
255:    [com.rpl.specter :as s]
256:    [okex-web.subs :as subs]
257:    ))
258: 
259: (defn depth-table
260:   [title data]
261:   [:div.container
262:    [:h4.text-center title]
263:    [:table.table.table-bordered
264:     [:thead
265:      [:tr
266:       [:th "价位"]
267:       [:th "价格"]
268:       [:th "数量"]
269:       [:th "订单数"]]]
270:     [:tbody
271:      (for [row data]
272:        ^{:key (str title (:pos row))}
273:        [:tr
274:         [:td (:pos row)]
275:         [:td (:price row)]
276:         [:td (:amount row)]
277:         [:td (:order-count row)]])]]])
278: 
279: (defn vec->dropdown-choices
280:   ([v] (vec->dropdown-choices v nil))
281:   ([v group]
282:    (map #(hash-map :id % :label % :group group) v)))
283: 
284: (defn depth-view []
285:   (let [base-coins (re-frame/subscribe [::subs/base-coins])
286:         quote-coins (re-frame/subscribe [::subs/quote-coins])
287:         base-coin (re-frame/subscribe [::subs/base-coin])
288:         quote-coin (re-frame/subscribe [::subs/quote-coin])
289:         depth-data (re-frame/subscribe [::subs/depth-data])]
290:     [re-com/v-box
291:      :gap "10px"
292:      :children [[re-com/h-box
293:                  :gap "10px"
294:                  :align :center
295:                  :children [[re-com/single-dropdown
296:                              :choices (vec->dropdown-choices @base-coins)
297:                              :model @base-coin
298:                              :placeholder "选择基准币种"
299:                              :filter-box? true
300:                              :on-change #(>evt [:set-base-coin %])]
301:                             [re-com/gap :size "10px"]
302:                             [re-com/single-dropdown
303:                              :choices (vec->dropdown-choices @quote-coins @base-coin)
304:                              :model @quote-coin
305:                              :placeholder "选择计价币种"
306:                              :on-change #(>evt [:set-quote-coin %])
307:                              ]
308:                             ]]
309:                 [re-com/h-split
310:                  :panel-1 [depth-table "买入信息" (:bids @depth-data)]
311:                  :panel-2 [depth-table "卖出信息" (:asks @depth-data)]]
312:                 ]]))
313: 
314: 
315: (defn title []
316:   [re-com/title
317:    :label (<sub [::subs/name])
318:    :class "center-block"
319:    :level :level1])
320: 
321: (defn error
322:   "显示错误"
323:   []
324:   (let [error (re-frame/subscribe [::subs/error])]
325:     (when @error
326:       [re-com/alert-box
327:        :alert-type :danger
328:        :heading (str "错误!!!   " (:path @error))
329:        :body [:span (str (:msg @error))]])))
330: 
331: (defn main-panel []
332:   [:div.container
333:    [re-com/v-box
334:     :height "100%"
335:     :children [[title]
336:                [error]
337:                [depth-view]
338:                ]]])

2.7 发布

使用以下命令编译生成js文件到resources/public文件夹:

lein do clean, cljsbuild once min

可以看到release发布只有一个app.js,文件大小不到900K。 在浏览打开index.html就可以使用了。注意必须关掉浏览器的CORS限制。

3 总结

re-frame写SPA程序非常强大,整体架构比较清晰,值得学习。项目完整代码。

脚注:

1

关于ApplicationState的官方文档

2

关于coeffects的官方文档

作者: ntestoc

Created: 2019-05-31 五 16:47

clojure GUI编程-3

原文:https://www.cnblogs.com/ntestoc/p/10955523.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!