June 2, 2020
By: Kevin

cljs代码规范和最佳实践2021-03-27版(不断更新)

  1. cljs
    1. 在cljs中不要直接操作js对象
    2. cljs小数计算
    3. re-frame 和 kee-frame
      1. reg-event-db和reg-event-fx的返回值
      2. 只修改db的场景使用reg-event-db,而不是reg-event-fx
    4. Reagent 相关
      1. reagent使用方括号来表示组件
      2. for生成的组件需要指定key
      3. doall
      4. 不要拿atom重新生成atom

cljs

在cljs中不要直接操作js对象

我们发布环境(release版本)的js代码和开发环境下的js代码是不一样的, release版经过了google closure的深度编译, js的变量, 函数会被混淆. 会出现本地运行ok,但是到发布环境就会出问题的情况. 在cljs代码中,尽量不要直接用interop的方式来调用和读取js对象的属性和方法.

参见:我们blog中高级编译选项的章节

我们cljs使用antd的组件,on-change、on-submit等很多函数中都涉及操作当前target的对象,本身是个js对象,都知道用js->clj,但是要避免直接使用类似下面的代码

:on-submit (fn [e]
            ((:validateFields this)
             (fn [err values]
                (rf/dispatch [:fabric/select-fabric-list (.-category_id values)]))))

这段代码在dev环境是没有问题的,但是category_id这个属性在生产打包时js会做混淆压缩,hash只有就没有这个属性了,这不是警告,是个bug,方式是用js->clj后用map取值:

:on-submit (fn [e]
            ((:validateFields this)
             (fn [err values]
                (rf/dispatch [:fabric/select-fabric-list
                (:category_id (js->clj values))]))))

cljs小数计算

cljs环境中, ratio没有实现, bigdec不可用, 前端做高精度计算浮点数似乎也不是个好点子.

如果非要用, 使用个第三方的库吧, 比如mathjs

re-frame 和 kee-frame

  1. 事件处理reg-event-db,reg-event-fx都应该使用kee-frame的版本, 避免re-frame的版本,kee-frame有更好,更统一的log.
  2. subevent分两个namespace,两个文件定义
  3. 事件命名不清晰时, 加上业务注释
reg-event-db和reg-event-fx的返回值
  1. reg-event-db的值是更新后的db
  2. reg-event-fx的value是nil或者更新后的coeffect.

除此之外的值都是错的. reg-event-fx返回的coeffect接下来会生成一个atom. 事件队列会修改它. 类型不匹的返回值会引发异常

只修改db的场景使用reg-event-db,而不是reg-event-fx
;; bad
(kf/reg-event-fx
 :custom/change-model
 (fn [{:keys [db]} [index]]
   {:db (update-in db [:custom :choices]
                   #(merge % {:currentIndex index}))}))
;; good
(kf/reg-event-db
 :custom/change-model
 (fn [db [index]]
   (update-in db [:custom :choices]
              #(merge % {:currentIndex index}))))

Reagent 相关

reagent使用方括号来表示组件

使用方括号,而不是圆括号

方括号的是reagent component, 有自己的完整生命周期;

父组件刷新的时候, 子组件不一定需要刷新. 性能上有优势,而且, form2, form3必须使用方括号.

所以, 请使用方括号!!

参见下面的比较:

(defn greet
  [name]
  (prn "I am child" name)
  [:div  "Hello " name])

(def r (r/atom "round"))
(def s (r/atom "squire"))

(defn greet-family-round
  [member1 member2 member3]
  (prn "I am round parent")
  [:div
    (greet member1)
    (greet member2)
    (greet member3)
    (greet @r)])



(defn greet-family-square
  [member1 member2 member3]
  (prn "I am square parent")
  [:div
    [greet member1]
    [greet member2]
    [greet member3]
    [greet @s]])
(reagent.core/render-component [greet-family-round "a" "b" "c"]  js/klipse-container)
(reagent.core/render-component [greet-family-square "a" "b" "c"]  js/klipse-container)

此处刷新圆括号

(reset! r "well, well, well")

此处刷新方括号, 注意区别

(reset! s "well, well, well")
for生成的组件需要指定key

参见咱们的reagent深入学习第三部分 使用for的时候最好指定个每个元素的key, 要不然会出现讨厌的warning:

Warning: Each record in table should have a unique `key` prop,or set `rowKey` to an unique primary key. 
;; bad
(defn list-by [entities sort-k]
  [:ul
   (for [[k v] (sort-by (comp sort-k val) @entities)]
     [:li (:name v)])])

;; good
(defn list-by [entities sort-k]
  [:ul
   (for [[k v] (sort-by (comp sort-k val) @entities)]
     ^{:key k}
     [:li (:name v)])])


;; 元素少的时候,也可以用into
(defn list-by [entities sort-k]
  (into [:ul]
        (for [[k v] (sort-by (comp sort-k val) @entities)]
          [:li (:name v)])))

例子

(def favorites
  (r/atom
   {"d632" {:name "Princess torte."
            :order 2}
    "1ae2" {:name "Black forest gateau."
            :order 3}
    "5117" {:name "Apple pie."
            :order 4}
    "42ae" {:name "Ice cream."
            :order 1}}))

(defn list-by [entities sort-k]
  [:ul
   (for [[k v] (sort-by (comp sort-k val) @entities)]
     ^{:key k}
     [:li (:name v)])])

(defn favorites-by-order-and-name []
  [:div
   [:h3 "By order"]
   [list-by favorites :order]
   [:h3 "By name"]
   [list-by favorites :name]]) 
doall

参见咱们的reagent深入学习第三部分

如果在layseq中去deref一个atom这是个bug, 一定全力避免.

Warning: Reactive deref not supported in lazy seq, it should be wrapped in doall
(def message (r/atom "一切尽在掌握!!!!"))
;; bad
(defn bad []
  [:div
   [:button
    {:on-click
     (fn [e]
       (reset! message "还有没有王法!!!"))}
    "Panic!"]
   (for [i (range 3)]
     [:h3 @message])])
;;good
(defn good []
  [:div
   [:button
    {:on-click
     (fn [e]
       (reset! message "还有没有王法!!!"))}
    "Panic!"]
   (doall
    (for [i (range 3)]
      [:h3 @message]))])
不要拿atom重新生成atom

主意这儿有两个atom, 外部atom和组件内部的atom 即使内部的atom在接下来的render函数中deref, 组件也不会刷新. 因为新生成的atom只会得到上层atom的快照. 如果说在该组件生存期内外部atom的值发生变化,比如某个ajax请求后为它赋值, 则会有肉眼可见的bug发生

(defn add-questionnaire []
  (let [currentTab (atom 0)
        jiemoyan-questionnaire (atom @jiemoyan-q)    ;; <-- 不要这么干,除非很确定jiemoyan-g 在组件生存期不变
        xiaochuan-questionnaire (atom @xiaochuan-q)]
    (fn []
      (if (< 1 (count @jiemoyan-q))
        (do
          (reset! jiemoyan-questionnaire @jiemoyan-q)
          (re-frame/dispatch [:clear-jiemoyan])))

      (if (< 1 (count @xiaochuan-q))
        (do
          (reset! xiaochuan-questionnaire @xiaochuan-q)
          (re-frame/dispatch [:clear-xiaochuan])))

      [:div.main-body
       [:div.main-content
        [:> ant-tabs
         {:tabs tabs
          :initial-page 0
          :animated true
          :useOnPan true
          :on-change (fn [tab, index] (reset! currentTab index))}
         [:div.tabs-page-view {:style {:height "100%"}}
          (if (zero? (count @jiemoyan-questionnaire)) [empty-view] [jiemoyan jiemoyan-questionnaire])]
         [:div.tabs-page-view {:style {:height "100%"}}
          (if (zero? (count @xiaochuan-questionnaire)) [empty-view] [xiaochuan xiaochuan-questionnaire])]]]])))
Tags: style clojurescript