June 2, 2020
By: Kevin

clj/cljs代码规范和最佳实践2020-06-01版(不断更新)

  1. 整体风格
  2. cljs/clj通用
    1. 命名问题
    2. Keyword的命名规范
    3. 文件命名规范
    4. docstring的位置
    5. if/when
    6. 判断sequence是否有元素
    7. 使用thread macro简化代码
    8. 使用set来做包含判断
    9. 不要出现长行
    10. 使用字面变量
    11. 注意作用域
    12. 从seq中取满足条件的任意一个, 使用some不要用 filter + first
    13. 小心contains?函数
    14. 使用specter简化数据操作
    15. 灵活扩展,使用多态, 避免手写分支
    16. 覆盖全面
    17. 内外之分
    18. 更多使用format, 而不是str
    19. 区别seq? sequential? 和 coll?
    20. 小心lazysequence
  3. cljs
  4. clj

整体风格

首先请参考:clojure风格指南, 代码中的具体问题参照本文.

风格指南的作者是Bozhidar Batsov, 一个保加利亚人,emacs中很多便利拜他所赐.

  • cider
  • projectile

人很Nice, 在clojure/emacs社区很活跃.

cljs/clj通用

命名问题

Clj/Cljs里推荐使用中划线连接单词 但是日常使用时,与外界(Json、数据库等)有数据交换或者对接的时候,经常会遇到要转换成下划线的情况。 所以我们规约一下:

定义Json、数据库字段等需要对外提供的命名,我们使用下划线,避免多余转换,比如user_idcategory_ids

Keyword的命名规范

本规范的来源是解决类似keeframe的事件查找的便利性问题。

  • 当rf/kf中,使用keyword作为事件时,需要被其他文件(页面)使用时,请使用:namespace/you-keyword的形式命名
  • 当不需要被其他文件,只在本文件内使用时,请使用::your-keyword的形式命名

文件命名规范

我们约定所有的文件命名用_作为分隔符, 包括css文件

docstring的位置

;;not even bad
(defn f [x]
  "一个被忽略的字符串"
  x)
;;good
(defn f
  "放在这儿才是doc string"
  [x]
  x)

if/when

有两个分支用if, 一个分支用when.

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

;; good
(when (< 1 (count @jiemoyan-q))
  (reset! jiemoyan-questionnaire @jiemoyan-q)
  (re-frame/dispatch [:clear-jiemoyan]))

判断sequence是否有元素

不要使用(empty ...), (zero? (count ...))之类的函数, 统一使用seq. 全场景适用!!, 用于判断:

  • vector 数组
  • list 列表
  • map Map
  • set 集合
  • string 字符串
  • nil本身
;; good
(= nil
   (seq nil)
   (seq [])
   (seq '())
   (seq  #{})
   (seq {})
   (seq nil)
   (seq ""))

使用thread macro简化代码

;;bad
(reduce + (mapv get-custom-products-price-new (filterv #(or  (= "1" (:product_type %)) (= "0" (:product_type %))) (:products order))))

;;good
(->> order
     :products
     (filterv #(#{"0" "1"} (:product_type %)))
     (mapv get-custom-products-price-new)
     (reduce +))

使用set来做包含判断

set和map一样,都可以做函数.

;; works
(if (some #(= (:order_status order) %)
          [order-status/status-pay
           order-status/status-confirm
           order-status/status-feedback])
    order
  (throw-error 9007 order))

;; even better
(if (#{order-status/status-pay
     order-status/status-confirm
     order-status/status-feedback}
     (:order_status order) )
    order
  (throw-error 9007 order))

不要出现长行

一般不超过100个字符

;; bad
(defn radio-group2 [root subItem item]
  [:div {:key (hash subItem)}
   [:div.questionnaireTitle (:title subItem)]
   [:div.wrapRow
    (doall
     (for [childItem (:selections subItem)]
       [auto-radio #(onSelected root item subItem childItem) (> (-indexOf (get-in @root [:sections (-indexOf (:sections @root) item) :items (-indexOf (:items item) subItem) :choose]) (-indexOf (:selections subItem) childItem)) -1) childItem]))]])
;; good
(defn radio-group2 [root subItem item]
  [:div {:key (hash subItem)}
   [:div.questionnaireTitle (:title subItem)]
   [:div.wrapRow
    (doall
     (for [childItem (:selections subItem)]
       [auto-radio #(onSelected root item subItem childItem)
        (> (-indexOf (get-in
                      @root
                      [:sections (-indexOf (:sections @root) item)
                       :items (-indexOf (:items item) subItem) :choose])
                     (-indexOf (:selections subItem) childItem)) -1) childItem]))]])

使用字面变量

;; bad
;; bad
(vector 1 2 3)
(hash-set 1 2 3)
#{(func1) (func2)} ; will throw runtime exception if (func1) = (func2)

;; good
[1 2 3]
#{1 2 3}
(hash-set (func1) (func2)) ; values determined at runtime
;; bad
(assoc {} :message_id message-id :company_id company-id :target_id target-id :msg_type msg-type :msg msg :create_user_id user-id :update_user_id user-id)
;; good
{:message_id message-id
 :company_id company-id
 :target_id target-id
 :msg_type msg-type
 :msg msg
 :create_user_id user-id
 :update_user_id user-id}

注意作用域

不要在函数内定义全局变量,def是全局的, 要不然改成let, 要不然就拿出来.

执行函数,定义个全局变量是个很坏很坏的坏习惯!

;; very bad
(defn banner-form-page []
  (def type-selections "朕是定义在函数里的全局变量"))
(banner-form-page)
type-selections

从seq中取满足条件的任意一个, 使用some不要用 filter + first

;;bad, 需要遍历全部
(first (filter even? (range 1 10000000)))
;;good 读到2就返回了
(some #(when (even? %) %) (range 1 10000000))

小心contains?函数

contains?只用于map是否含有某元素,在序列元素,比如vector和list上行为比较...古怪。

;; 用于map,ok
(contains? {:a 1} :a)    ;=> true
(contains? {:a nil} :a)  ;=> true
(contains? {:a 1} :b)    ;=> false
;; 用于其他,no!!!
(contains? [1 2 3] 4294967296) ;=> true

使用specter简化数据操作

稍微复杂的数据结构操作(两层以上嵌套). 就应该使用specter

;; bad
(change-craftwork-item-fn
 [db [id model-flag]]
 (let [datas (get-in db [:custom :choices :datas])
       cur-index (get-in db [:custom :choices :currentIndex])
       new-datas (map-indexed
                  (fn [index item]
                    (if (= index cur-index)
                      (let [menu-id (get-in item [:default_data (if (= 1 model-flag)
                                                                  :craftwork_menu_id
                                                                  :craftwork_menu2_id)])
                            craftwork-list (get-in item [:default_data :craftwork])
                            ;; 判断当前菜单id的工艺是否在已选中的数组中(有些工艺没有默认工艺,初始时就没有)
                            is-exist (some #(= menu-id (:parent_id %)) craftwork-list)
                            ;; 1.如果存在就更新这个工艺;  2.如果不存在就追加到数组中
                            new-list (if is-exist
                                       (map (fn [c]
                                              (if (= menu-id (:parent_id c))
                                                (assoc c :craftwork_id id) c)) craftwork-list)
                                       (conj craftwork-list {:parent_id menu-id
                                                             :craftwork_id id}))]
                        (assoc-in item [:default_data :craftwork] new-list))
                      item)) datas)]
   (assoc-in db [:custom :choices :datas] new-datas)))
;; good
(defn change-craftwork-item-fn
  [db [id model-flag]]
  (let [cur-index (get-in db [:custom :choices :currentIndex])
        default-flag (if (= 1 model-flag)
                       :craftwork_menu_id
                       :craftwork_menu2_id)
        menu-id (get-in db  [:custom :choices :datas cur-index :default_data default-flag])
        craftwork-list-path [:custom :choices :datas cur-index :default_data :craftwork ]
        craftwork-list-all-path   (conj craftwork-list-path      s/ALL)
        craftwork-list-match-path (conj craftwork-list-all-path  #(= menu-id (:parent_id %)))]
    ;; 判断当前菜单id的工艺是否在已选中的数组中(有些工艺没有默认工艺,初始时就没有)
    (if (s/selected-any? craftwork-list-match-path db)
      ;; 1.如果存在就更新这个工艺;  2.如果不存在就追加到数组中
      (s/transform craftwork-list-match-path #(assoc % :craftwork_id id) db)
      (s/transform craftwork-list-path       #(conj %  {:parent_id menu-id
                                                        :craftwork_id id}) db))))

灵活扩展,使用多态, 避免手写分支

Clojure多态支持multi-dispatch, 比之Java/JS更加强大. 合适场景下代码更容易扩展.

  • multimethod
  • protocol 以multimethod举例:
;;bad
(cond
  (= :txt (:input-type x) )
  [:> ant/Input {:placeholder (str "请输入" (:title item-map))}]

  (= :num (:input-type x) )
  [:> ant/InputNumber {:placeholder "请输入序号" :style {:width "100%"} :min 0 :max 1000 :precision 0}])

;;good
(defmulti ^:private form-item (fn [x] (:input-type x)))
(defmethod form-item :txt [item-map]
  [:> ant/Input {:placeholder (str "请输入" (:title item-map))}])
(defmethod form-item :num [item-map]
  [:> ant/InputNumber {:placeholder "请输入序号" :style {:width "100%"} :min 0 :max 1000 :precision 0}])

(form-item item)

覆盖全面

case需要有默认分支

(case expr
  match1 ...
  match2 ...
  (default))

cond需要有else分支

(case cond
  match1 ...
  match2 ...
  :else (default))

multi-method 应该有:default method

;; good 应该有默认分支来处理异常
(defmulti change-order-status :order_status)

(defmethod change-order-status status-pay [{:keys [order_id]}]
  (db/update-order-status! {:order_id order_id :order_status status-pay :pay_date (jt/local-date-time)}))

(defmethod change-order-status status-confirm [{:keys [order_id]}]
  (db/update-order-status! {:order_id order_id :order_status status-confirm :confirm_date (jt/local-date-time)}))

(defmethod change-order-status status-cancel [{:keys [order_id]}]
  (db/update-order-status! {:order_id order_id :order_status status-cancel :cancel_date (jt/local-date-time)}))

(defmethod change-order-status status-refund [{:keys [order_id]}]
  (db/update-order-status! {:order_id order_id :order_status status-refund :redund_date (jt/local-date-time)}))

(defmethod change-order-status status-feedback [{:keys [order_id]}]
  (db/update-order-status! {:order_id order_id :order_status status-feedback :delivery_date (jt/local-date-time)}))

(defmethod change-order-status :default [{:keys [order_id] :as order}]
  (throw (IllegalArgumentException.
          (format "订单类型异常:%d, 订单详细信息" order_id (str order)))))

内外之分

只暴露我们想暴露的函数和变量定义, Clojure使用meta-data来做私有声明

对于变量, 使用^{:private ture}, 可以简写为^:private

对于函数, clojure提供了defn-宏来做,更加方便一些.

;;bad
(def a 1)
;;good
(def ^{:private true} a 1)
;; 等价于下面的
(def ^:private a 1)

;;god
(defmulti  table_columns_item (fn [x] (:input-type x)))
;;bad
(defmulti ^:private table_columns_item (fn [x] (:input-type x)))

;;bad
(defn my-add [a b] (+ a b))
;;good
(defn- my-add [a b] (+ a b))

更多使用format, 而不是str

clojure上使用format, ClojureScript使用goog.string/format, 比str要灵活很多.

复杂的字符串拼接建议使用.

(goog.string/format "%5d" 3)
(goog.string/format "Pad with leading zeros %07d" 5432)
(goog.string/format "Left justified :%-7d:" 5432)

注意:cljs中, format支持不完整, 只支持 s, f, d, i, u, 所以下面的格式前端是不支持的

(goog.string/format "Locale-specific group separators %,12d" 1234567)
(goog.string/format "decimal %d  octal %o  hex %x  upper-case hex %X" 63 63 63 63)
(goog.string/format "%2$d %1$s" "Positional arguments" 23)

区别seq? sequential? 和 coll?

  • seq? 是否实现了ISeq接口, 基本上等同list?
  • sequential? 是否实现了 clojure.lang.Sequential, 判断是不是以后顺序,所以vector,list都是符合的
  • coll? 是否实现了IPersistentCollection接口, 是否持久化存储数据结构,map,set,vector,list都符合
(vector
 ((juxt seq? sequential? coll?) ()) ; [true true true]
 ((juxt seq? sequential? coll?) []) ; [false true true]
 ((juxt seq? sequential? coll?) #{})); [false false tru]e

一个稍微复杂点的例子, 返回匹配函数的路径, 查找:v 对应的值为3的sub map的路径. 会拿到两条:

(defn find-path
  ([key-filter-fn x] (find-path key-filter-fn x []))
  ([key-filter-fn x p]
   (cond
     (and (map? x)
          (key-filter-fn x)) [p]
     (coll? x)  (->> (if (map? x) x (vec x))
                     (reduce-kv (fn [acc i v]
                                  (into (vec (find-path key-filter-fn v (conj p i))) acc)) []))
     :else nil)))


(def x {:v 0,
        :l 0,
        :c  [{:v 1, :l "1", :c [{:v 2, :l "2"} {:v 3, :l "3"}]} {:v 4, :l "4"}]
        :d [{:v 1, :l "1", :c [{:v 2, :l "2"} {:v 3, :l "3"}]} {:v 4, :l "4"}]})

(find-path (fn [e] (= 3 (:v e))) x)

小心lazysequence

  • doall 强迫求值, 且返回求值的结果, 在下文cljs部分以后单独章节
  • doseq 强迫求值, 且忽略求值结果,固定返回nil, 用于我们需要执行副作用的场景,比如写入数据库, 打印, 注册/触发事件, 修改全局变量(比如dom)
;; bad, 这个情况event是不会dispatch的, 因为map返回lazysequence, 我们没有后续的操作强迫这个序列去求值
(map
 (fn [id ] (rf/dispatch [:product/fetch-style-crafts {:category_id category-id :id id}]))
 style-ids)
;; good doseq 会强迫求值
(doseq [id style-ids]
  (rf/dispatch [:product/fetch-style-crafts {:category_id category-id :id id}]))

cljs

点击查看

clj

点击查看

Tags: clojure style clojurescript