zoukankan      html  css  js  c++  java
  • clojure GUI编程-3

    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
    

    后来发现Allow CORS插件比较好用,支持主流浏览器,建议使用。

    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

  • 相关阅读:
    性能测试系列(1)-性能测试基本概念
    性能篇综合汇总
    【CTFHUB】Web技能树
    Flash XSS
    绕过CDN找到⽬标站点真实IP
    【网鼎杯2020白虎组】Web WriteUp [picdown]
    【网鼎杯2020朱雀组】Web WriteUp
    【网鼎杯2020青龙组】Web WriteUp
    利用DNSLog实现无回显注入
    Cobalt Stike使用教程
  • 原文地址:https://www.cnblogs.com/ntestoc/p/10955523.html
Copyright © 2011-2022 走看看