clj/cljs代码规范和最佳实践2020-06-01版(不断更新)
- 整体风格
- cljs/clj通用
- 命名问题
- Keyword的命名规范
- 文件命名规范
- docstring的位置
- if/when
- 判断sequence是否有元素
- 使用thread macro简化代码
- 使用set来做包含判断
- 不要出现长行
- 使用字面变量
- 注意作用域
- 从seq中取满足条件的任意一个, 使用
some不要用filter + first - 小心
contains?函数 - 使用specter简化数据操作
- 灵活扩展,使用多态, 避免手写分支
- 覆盖全面
- 内外之分
- 更多使用format, 而不是str
- 区别seq? sequential? 和 coll?
- 小心lazysequence
- cljs
- clj
(require '[reagent.core :as r])
整体风格
首先请参考:clojure风格指南, 代码中的具体问题参照本文.
风格指南的作者是Bozhidar Batsov, 一个保加利亚人,emacs中很多便利拜他所赐.
- cider
- projectile
人很Nice, 在clojure/emacs社区很活跃.
cljs/clj通用
命名问题
Clj/Cljs里推荐使用中划线连接单词 但是日常使用时,与外界(Json、数据库等)有数据交换或者对接的时候,经常会遇到要转换成下划线的情况。 所以我们规约一下:
定义Json、数据库字段等需要对外提供的命名,我们使用下划线,避免多余转换,比如
user_id、category_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}]))