March 19, 2021
By: 马海强

clojure web开发入门

  1. 后端基础知识
    1. 编码规约
    2. 后端知识点
      1. 数据库 migration管理
      2. mount:状态管理
      3. ring:web server
      4. reitit:路由,数据驱动
      5. middleware中间件
      6. spec API参数定义和校验
      7. conman、HugSQL
      8. mysql命名规范
        1. 表名命名规范
        2. 字段命名规范
        3. 字段类型规范
          1. SQL语句规范
      9. 单元测试
    3. 项目相关
      1. 创建项目
      2. 项目运行
      3. 项目打包
  2. 前端
    1. 前端规约
    2. 有用的库
    3. 推荐官方教程
    4. 必看的公司blog
  3. 视频教程
  4. Tools
  5. 其他推荐

后端基础知识

编码规约

后端知识点

后端接口编程是以ring作为基础展开的,下面介绍luminus这个框架里已经集成和我们公司实践后推荐的库。

数据库 migration管理

migrate依赖https://github.com/yogthos/migratus 这个库,

  • 创建migrate sql文件对,在repl执行(create-migrate "add-user"),创建出如下两个文件 resources/migrations/20210112154000-add-user.up.sqlresources/migrations/20210112154000-add-user.down.sql ,在up文件中编写DDL的创建和修改语句,如:
CREATE TABLE `demo_user` (
  `id` varchar(40) NOT NULL,
  `first_name` varchar(30) DEFAULT NULL,
  `last_name` varchar(30) DEFAULT NULL,
  `email` varchar(30) DEFAULT NULL,
  `admin` tinyint(1) DEFAULT NULL,
  `last_login` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `is_active` tinyint(1) DEFAULT NULL,
  `pass` varchar(300) DEFAULT NULL,
  `remark` json DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='demo表';

down文件中编写DDL的与up里相反的回滚语句,如:

  DROP TABLE IF EXISTS `demo_user`;
  • 注意: 多个语句的中间需要使用--;;做分隔符
  DROP TABLE IF EXISTS `t_doctor`;
  --;;
  DROP TABLE IF EXISTS `t_hospital`;

mount:状态管理

  • mount通过defstate宏来记录各个state的编译顺序,比如db依赖config:
        ;; namespace config.core
        (defstate config
          :start (fn [] (load-config)))
        
        ;; namespace db.core
        (defstate db
          :state (connect config)
          :stop (disconnect db))

clojure的macro在编译期间展开执行,mount可以hook到clojure的编译器,以记录顺序

  • clojure中ns重新编译的时候,外部资源,比如文件描述符、数据库conn,会重建,老的会丢失. 这种情况下,使用defstate的start,stop函数来管理这些资源
        ;; meta info :stop :noop
        ;; :stop 会在reload的时候停掉
        ;; :noop reload的时候do nothing
        (defstate ^{:on-reload :stop} server )
        ;; namespace config.core
        (defstate config
          :start (fn [] (load-config)))
        ;; namespace db.core
        (defstate db
          :state (connect config)
          :stop (disconnect db))

ring:web server

在 Clojure 众多的 Web 框架中,Ring 以其简单统一的 HTTP 抽象模型脱颖而出。 Ring 规范里面有如下5个核心概念:

  • handlers,应用逻辑处理的主要单元,由一个普通的 Clojure 函数实现

  • middleware,为 handler 增加额外功能

  • adapter,将 HTTP 请求转为 Clojure 里的 map,将 Clojure 里的 map 转为 HTTP 相应

  • request map,HTTP 请求的 map 表示

  • response map,HTTP 相应的 map 表示 这5个组件的关系可用下图表示

#+BEGIN_SRC ditaa :file ring1.png :cmdline -r -s 0.8
  +---------------+
  |  Middleware   |
  |  +---------+  |             +---------+      +--------+
  |  |         |<-- request ----|         |      |        |
  |  | Handler |  |             | Adapter |<---->| Client |
  |  |         |--- response -->|         |      |        |
  |  +---------+  |             +---------+      +--------+
  +---------------+
#+END_SRC

一个例子,运行下面的程序,就可以启动一 Web 应用,然后在浏览器访问就可以返回Hello World,同时在控制台里面会打印出请求的 uri。

(ns learn-ring.core
  (:require [ring.adapter.jetty :refer [run-jetty]]))

(defn handler [req]
  {:headers {}
   :status 200
   :body "Hello World"})

(defn middleware [handler]
  "Audit a log per request"
  (fn [req]
    (println (:uri req))
    (handler req)))

(def app
  (-> handler
      middleware))

(defn -main [& _]
  (run-jetty app {:port 3000}))

ring默认使用jerry作servlet,在api开发中我们开发服务器采用 http-kit ,这是一个异步高性能 web 服务器,志在取缔 jetty,我们需要向 http-kit 注册一个 handler。当我们请求 http-kit 服务器时,http-kit 会调用事先注册好的 handler 。handler 必须返回一个 response map。格式如下:

{:status 200
   :header {"Content-Type" "text/html"}
   :body "hello world"}

http-kit 提供了极为简单易用的函数调用来启动一个服务:

(run-server handler & [request-map])

一个简单的例子:

(ns hello.core
  (:use [org.httpkit.server :only [run-server]]))

(defn handler [request]
  {:status 200
   :header {"Content-Type" "text/html"}
   :body "hello world\n"})

(defn -main [& args]
  (run-server handler {:port 5000})
  (println "Start Http-Kit Server On Port 5000..."))

在命令行执行:

$ lein run
Start Http-Kit Server On Port 5000...

访问:

$ curl http://localhost:5000
hello world

当然,ring自带response,在ring.util.response里,以上response里构造的map,也可以改成

(ns hello.core
  (:use [org.httpkit.server :only [run-server]]
        [ring.util.response :only [response]]))

(defn handler [request]
  (response "hello world\n"))

(defn -main [& args]
  (run-server handler {:port 5000})
  (println "Start Http-Kit Server On Port 5000..."))

reitit:路由,数据驱动

由于 Ring 只是提供了一个 Web 服务最基本的抽象功能,很多其他功能,像 url 路由规则,参数解析等均需通过其他模块实现。Compojure 是 Ring 生态里面默认的路由器,同样短小精悍,功能强大。基本用法如下:

(def handlers
  (routes
   (GET "/" [] "Hello World")
   (GET "/about" [] "about page")
   (route/not-found "Page not found!")))

使用这里的 handlers 代替上面 Hello World 的示例中的 handler 即可得到一个具有2条路由规则的 Web 应用,同时针对其他路由返回 Page not found!。 Compojure 通过 routes 把一系列 handler 封装起来,其内部调用 routing 方法找到正确的 handler。 因为我们实践中使用的是reitit处理路由,因为它更快,所以Compojure的使用不再多介绍。

Reitit知识点较多,使用中可能要翻阅reitit站点学习,这里有一篇入门使用,此处不再赘述。

middleware中间件

middleware见字知其意,中间件,实现数据格式化、拦截器、参数解析器等功能。

Ring 提供了 web 开发所需的基础构件,比如处理请求参数,cookie, session 等等。通过向 http-kit 或 jetty 注册 handler 的方式来提供服务。

handler 函数接收一个 request 参数,此参数由调用者传递(此处为 http-kit 或 jetty)。然而 http-kit 或 jetty 传递过来的 request 参数只包含了基本标准键,Ring 将通过中间件的方式提供更高级的功能。

以下为 request 包含的基本标准键:

:server-port ---------- 用于处理该请求的服务端口
:server-name ---------- 服务器的 IP 地址或是主机名
:remote-addr ---------- 客户端的 IP 地址
:query-string --------- 请求的查询字符串
:scheme --------------- 协议的类型,可以是 HTTP 或者 HTTPS
:request-method ------- 请求的方法,比如::get、:head、:options、:put、:post 或 :delete
:request-string ------- 请求的查询字符串
:content-type --------- 请求消息体的 MIME 类型
:content-length ------- 请求消息体的字节数
:character-encoding --- 请求采用的字符编码名称
:headers -------------- 包含了请求头部的map
:body ----------------- 可用于读取请求消息体的输入流
:context -------------- 当应用没有作为根来部署时,其所处的上下文
:uri ------------------ 服务端的 URI 全路径,包含了 :context(如果存在)的部分
:ssl-client-cert ------ 客户端的 SSL 证书

* 注意:此处列出的键,并不一定会出现在所有的请求中,比如 :ssl-client-cert

Ring 中间件的原理其实很简单。链式执行,前一个的输出作为下一个的输入,有点类似于管道。

Ring 提供了一些基础的中间件,它们或多或少的对 request 和 response 进行修改,以达到特定目的。ring-devel 对开发环境提供了支持,比如 wrap-reload 允许你不必重启即可在修改源码后自动重新载入等等。ring-core 提供了更多标准中间件,有wrap-params、wrap-not-modified、wrap-content-type等等。

Middleware 这一模式在函数式编程中非常常见,Clojure 生态里面新的构建工具 boot-clj 里面的 task 也是通过这种模式组合的。 以下是系统用法:

  1. 参数解析 它会把 QueryString 中的参数解析到 request map 中的:query-params key 中,表单中的参数解析到 request map 中的 :form-params。
  2. JSON 序列化 由于 RESTful 服务中,请求的数据与返回的数据通常都是 JSON 格式,所以需要增加两个额外的功能来实现 JSON 的序列化。
;; 首先引用 ring.middleware.json
(def app
  (-> handlers
      wrap-json-response
      wrap-json-body
      handler/api))
  1. 纪录请求时间 通常,我们需要纪录每个请求的处理时间,这很简单,实现个 record-response-time 即可:
(defn record-response-time [handler]
(fn [req]
  (let [start-date (System/currentTimeMillis)]
    (handler req)
    (let [res-time (- (System/currentTimeMillis) start-date)]
      (println (format  "%s took %d ms" (:uri req) res-time))))))

(def app
  (-> handlers
      wrap-json-response
      wrap-json-body
      handler/api
      record-response-time))

需要注意的是 record-response-time 需要放在 middleware 最外层,这样它才能纪录一个请求经过所有 middleware + handler 处理的时间。 4. 封装异常

(defn wrap-exception
[handler]
(fn [request]
  (try
    (handler request)
    (catch Throwable e
      (response {:code 20001
                 :msg  "inner error})))))

(def app
  (-> handlers
      wrap-json-response
      wrap-json-body
      handler/api
      wrap-exception
      record-response-time))

一个 App 中的 middleware 调用顺序非常重要,因为不同的 middleware 之间 request map 与 response map 是相互依赖的,所以在定义 middleware 时一定要注意顺序,最后的middleware被放在了最外层。最外层,最先处理request,最后处理response。

除了系统自定的format这样的middleware,我们也会自定义,比如自定义拦截,实现公共处理,实现参数解析器扩展等等。

几个建议用middleware实现的例子:

  • 前后端通讯统一使用时间戳,但是数据库中存时间,可以用middleware修改response里的时间参数
  • 授权拦截,比如一般接口需要有授权token才能访问,可以定义一个middleware验证token
  • 参数解析器扩展,在所有应用级、用户级的接口上,从token中解析出当前用户,配置到resquest的header中。

ring-middleware-format有我们已经在使用的middleware。

如果还是没有理解用法,有个中文demo可以看看。

spec API参数定义和校验

spec本身功能是非常强大的,不仅可以约束接口的request,response,而且也可以在前端使用。在我们实践中,一个主要的功能是定义api的request。

  • 常规使用spec 我们通常会引入下面2个库
[spec-tools.data-spec :as ds]
[clojure.spec.alpha :as s]

比如,定义一组部分字段可以为null,的有reqiure和非require的参数

(def size-data-spec
  {:part_id string?
   :part_code (s/nilable string?)
   :part_name string?
   :part_value (s/nilable string?)
   (ds/opt :craft_price) (s/nilable number?)
   (ds/opt :color) {:color_id string?
                    :color_name string?
                    :color_code (s/nilable string?)})

上面的定义简洁明了,但是对于使用swagger管理api的团队来说,需要一定的默契程度和业务熟悉要求。

  • 更加详细的写法 另一种稍微复杂的写法,借助另一个库,可以实现字段释义等。
[spec-tools.core :as st]

定义spec及使用

(s/def ::page
  (st/spec
    {:spec            (s/and int? #(> % 0))
     :description     "页码,从0开始"
     :swagger/default "0"
     :reason          "页码参数不能为空"}))

(s/def ::size
  (st/spec
    {:spec            (s/and int? #(< % 100))
     :description     "每页条数"
     :swagger/default 10
     :reason          "条数参数不能为空"}))

(s/def :base/role
  (st/spec
    {:spec        #{"PATIENT", "DOCTOR"}
     :description "角色"
     :reason      "角色不能为空"}))

在header里定义一个非必须的参数role,query里定义两个必须参数。

["/page"
    {:get {:summary    "分页获取字典数据"
           :parameters {:query (s/keys :req-un [::page ::size])
                        :header (s/keys :opt-un [:base/role])}
           :response   {200 {:body {:code int? :msg string? :data (s/keys :req-un [::page ::size])}}}
           :handler    (fn [{{{:keys [page, size]} :query} :parameters :as request}]
                         {:status 200
                          :body   {:code 1}})}}]

下面的写法针对上面的定义,但是不太常用,在此只是展示一下。 用coll-of定义出一个list结构

(s/def ::head-id id?)
(s/def ::url string?)
(s/def ::unmatched-head
  (s/keys :req [::head-id ::url]))

(s/def ::unmatched-head-result
  (st/spec
   {:spec (s/coll-of ::unmatched-head)}))

coll-of函数还接收可选的参数,用来对数组中的元素进行限制,可选参数有如下:

   (1):kind- - - -可以指定数组的类型,vector,set,list等;

   (2):count- - - -可以限定数组中元素的个数;

   (3):min-count- - - -限定数组中元素个数的最小值

   (4):max-count- - - -限定数组中元素个数的最大值

   (5):distinct- - - -数组没有重复的元素

   (6):into- - - -可以将数组的元素插入到[],(),{},#{}这些其中之一,主要是为了改变conform函数的返回结果。

conman、HugSQL

  • hugsql的使用需要从官网学习,
  • 还有一篇入门教程-clojure luminus开发之HugSQL
  • 对于主键自增的表,插入数据后要返回主键id,可以使用下面的sql,该函数放回一个map,在mysql环境下是{:generated_key id},这是通过向mysql获取genetatedKeys得到的。
-- :name insert-into-test-table-return-keys :i! :raw
insert into demo_user (name, mobile) values (:name, :mobile) 

特别说明的是,这个方式因为数据库,和jdbc链接方式的不同而不同,官方的demo如下:

(testing "insert w/ return of .getGeneratedKeys"
;; return generated keys, which has varying support and return values
;; clojure.java.jdbc returns a hashmap, clojure.jdbc returns a vector of hashmaps
  (when (= adapter-name :clojure.java.jdbc)
    (condp = db-name
      :postgresql
      (is (= {:id 8 :name "H"}
          (insert-into-test-table-return-keys db {:id 8 :name "H"} {})))

      :mysql
      (is (= {:generated_key 9}
             (insert-into-test-table-return-keys db {:id 9 :name "I"})))

      :sqlite
      (is (= {(keyword "last_insert_rowid()") 10}
             (insert-into-test-table-return-keys db {:id 10 :name "J"} {})))

      :h2
      (is (= {(keyword "scope_identity()") 11}
             (insert-into-test-table-return-keys db {:id 11 :name "J"} {})))

      ;; hsql and derby don't seem to support .getGeneratedKeys
      nil))

  (when (= adapter-name :clojure.jdbc)
    (condp = db-name
      :postgresql
      (is (= [{:id 8 :name "H"}]
             (insert-into-test-table-return-keys db {:id 8 :name "H"} {})))

      :mysql
      (is (= [{:generated_key 9}]
             (insert-into-test-table-return-keys db {:id 9 :name "I"})))

      :sqlite
      (is (= [{(keyword "last_insert_rowid()") 10}]
             (insert-into-test-table-return-keys db {:id 10 :name "J"} {})))

      :h2
      (is (= [{(keyword "scope_identity()") 11}]
             (insert-into-test-table-return-keys db {:id 11 :name "J"} {})))

      ;; hsql and derby don't seem to support .getGeneratedKeys
      nil)))

mysql命名规范

采用26个英文字母(区分大小写)和0-9这十个自然数,加上下划线'_'组成,共63个字符.不能出现其他字符(注 释除外). 注意事项:

  1. 以上命名都不得超过30个字符的系统限制.变量名的长度限制为29(不包括标识字符@).
  2. 数据 对象、变量的命名都采用英文字符,禁止使用中文命名.绝对不要在对象名的字符之间留空格.
  3. 小心保留词,要保证你的字段名没有和保留词、数据 库系统或者常用访问方法冲突
  4. 保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性.假如数据类型在一个表里是整 数,那在另一个表里可就别变成字符型了.
表名命名规范

数据表名使用小写英文以及下划线组成,尽量说明是那个应用或者系统在使用的。表名一律使用t_作为前缀。视图使用v_作前缀。 相关应用的数据表使用同一前缀,如 订单的表使用order_前缀。

比如:

  • t_order
  • t_order_detail
字段命名规范

字段名称使用单词组合完成,首字母小写,后面单词的首字母大写,最好是带表名前缀.

如 web_user 表的字段: user_id、user_nam、user_password

表与表之间的相关联字段要用统一名称,如 web_user 表 里面的 userId 和 web_group 表里面的 userId 相对应

字段类型规范
  • 规则:用尽量少的存储空间来存 数一个字段的数据.
    1. 比如能用int的就不用char或者varchar
    2. 能用tinyint的就不用int
    3. 能用 varchar(20)的就不用varchar(255)
    4. 时间戳字段尽量用int型,如created:表示从 '1970-01-01 08:00:00'开始的int秒数,采用英文单词的过去式;
    5. 时间和日期类型的,使用timestamp和date两个类型
    6. 表主键varchar长度设为40,varchar类型设置40,200,400,2000几个枚举,再长用textarea类型
    7. 所有表都包含delete_flag字段,0:未删除,1:已删除
    8. migrate的insert into语句不要包含数据库名称
SQL语句规范

所有sql关键词全部大 写,比如SELECT,UPDATE,FROM,ORDER,BY等, 所有的表名和库名都不要用特殊符号*`*包含如:

SELECT COUNT(*) FROM `cdb_members` WHERE `user_name` = 'aeolus';

单元测试

推荐:Clojure单元测试规范

项目相关

创建项目

创建hc-template的项目,带着管理系统的页面和接口。

lein new hc-template demo

tree一下创建的项目的服务端文件结构:

$ tree -a demo/src/clj/demo
demo/src/clj/demo
├── common
│   ├── aes_utils.clj
│   ├── biz_error.clj
│   ├── encrypt.clj
│   ├── file_util.clj
│   ├── result.clj
│   ├── token.clj
│   ├── utils.clj
│   └── utils_format.clj
├── config.clj
├── core.clj
├── db
│   ├── common_db.clj
│   ├── common_service.clj
│   ├── core.clj
│   ├── db_sys_dict.clj
│   └── redis.clj
├── handler.clj
├── middleware
│   ├── authentication.clj
│   ├── exception.clj
│   ├── formats.clj
│   └── log_interceptor.clj
├── middleware.clj
├── modules
│   ├── app
│   │   └── app_db.clj
│   ├── base
│   │   ├── base_amdin_routes.clj
│   │   ├── base_db.clj
│   │   └── base_specs.clj
│   ├── file
│   │   ├── file_admin_routes.clj
│   │   └── file_specs.clj
│   ├── queries
│   │   └── queries_db.clj
│   ├── sys
│   │   ├── auth_routes.clj
│   │   ├── auth_service.clj
│   │   ├── auth_specs.clj
│   │   ├── sys_dict_routes.clj
│   │   ├── sys_menu_service.clj
│   │   ├── sys_role_service.clj
│   │   ├── sys_routes.clj
│   │   ├── sys_specs.clj
│   │   ├── sys_user_db.clj
│   │   └── sys_user_service.clj
│   ├── user
│   │   └── user_db.clj
│   └── wx
│       ├── wx_routes.clj
│       └── wx_specs.clj
├── nrepl.clj
└── routes
    ├── base.clj
    └── demo.clj

12 directories, 44 files

tree一下登录和4个页面的代码结构:

$ tree -a demo/src/cljs/demo
demo/src/cljs/demo
├── common
│   ├── enums.cljs
│   ├── msg_fx.cljs
│   ├── reitit_router.cljs
│   ├── request.cljs
│   ├── route_mapping.cljs
│   ├── storage.cljs
│   ├── utils.cljs
│   └── utils_auth.cljs
├── components
│   ├── city.cljs
│   ├── common_page.cljs
│   ├── hc_img.cljs
│   ├── hc_upload.cljs
│   └── upload.cljs
├── config.cljs
├── core.cljs
├── index.cljs
├── layout
│   ├── global_footer.cljs
│   ├── layout_events.cljs
│   ├── layout_main.cljs
│   ├── layout_views.cljs
│   └── side_menu.cljs
├── login
│   ├── login_events.cljs
│   └── login_views.cljs
├── router.cljs
├── system
│   ├── dict
│   │   ├── sys_dict_events.cljs
│   │   ├── sys_dict_main.cljs
│   │   ├── sys_dict_sub.cljs
│   │   └── sys_dict_views.cljs
│   ├── menu
│   │   ├── sys_menu_events.cljs
│   │   ├── sys_menu_main.cljs
│   │   └── sys_menu_views.cljs
│   ├── role
│   │   ├── sys_role_events.cljs
│   │   ├── sys_role_main.cljs
│   │   └── sys_role_views.cljs
│   └── user
│       ├── sys_user_events.cljs
│       ├── sys_user_main.cljs
│       ├── sys_user_sub.cljs
│       └── sys_user_views.cljs
└── url.cljs

9 directories, 39 files

项目运行

  • 服务端

后端项目配置和启动

  • Java Run Time > 8.0
  • lein 2.0 以上版本(安装请参考安装Leiningen)
  • mysql 5.7
  • 本地开发配置需要链接本地测试库,为了防止冲突,这个文件需要本地创建 根目录下创建dev-config.edn, :database-url 是个必须配置的项目,否额项目无法启动 注意要使用serverTimezone=Hongkong指定时间,要不然会是格林尼治时间

本地开发配置文件:

  ;; WARNING
  ;; The dev-config.edn file is used for local environment variables, such as database credentials.
  ;; This file is listed in .gitignore and will be excluded from version control by Git.
  
  {:dev true
   :port 3000
   ;; when :nrepl-port is set the application starts the nREPL server on load
   :nrepl-port 7000
  
   ;; set your dev database connection URL here
   :database-url "jdbc:log4jdbc:mysql://localhost:3306/mydb?user=root&password=007a007b&useSSL=false&autoReconnect=true&useUnicode=true&amp&characterEncoding=UTF-8&serverTimezone=Hongkong"
  }

命令行启动:

    # 在程序根目录下执行以下命令,启动repl
      lein repl
    
    # 启动后在intellIDEA中创建远程repl连接,输入repl启动时自动选择的端口号。
    # 启动程序执行
        (start)
    # 下载系统依赖的库和插件后我们会进入repl,默认空间为users,在此空间下,执行迁移,会使用resource/migrations/下的升级文件,
    # 按照日期时间顺序从老到新,逐个执行`.*up.sql`完成数据库初始化
        (migrate)
    # 也有相应的rollback函数可以逐个回滚,需要多次,按照migrate时候的相反顺序,逐个执行‘.*down.sql’文件
    ----------------------------------------------------

emacs里启动

M-x 选择 cider-jack-in-clj
  • 前端页面

在项目跟目录执行以下命令

yarn &&
shadow-cljs server

启动后在浏览器访问8000端口即可。

项目打包

  • 前端页面
shadow-cljs release test-app

打包后的资源文件在target/cljsbuild/public/test/js/app.js,配合原项目resources里的css和image文件显示。

  • 服务端 打jar包将使用到project.clj的配置
    # 自定义环境的jar,比如测试环境
    lein with-profile uberjar-test uberjar
    
    # 打出默认的jar
    lein uberjar -Dprofile=test
    
    # 生产环境jar包
    lein uberjar
    
    # 生产环境war包
    lein with-profile test immutant war -name alk

前端

前端规约

  1. UI层代码要尽量简单,计算放到 subscription 不在UI层处理逻辑
  2. 务必起好名字,dispatch要反应用户意图,而不是某个具体的操作
  3. db组织要清晰,明确层级,不要把内容都堆砌在最外层
  • DB的订阅使用

    • reg-sub,以db和query vector为输入,得到某些输出
      (rf/reg-sub
        :time
        (fn [db _]     ;; db 是当前 app state. 第二个参数(没有用到)的query vector
          (:time db))) ;; 计算返回值,当然最简单的情况就是从db中get-in
    
    • subscribe, 在一个reaget组件中,订阅某些query,得到atom,保证db中对应的值更新后,这个组件也会更新
     (defn clock
       []
       [:div.example-clock
        {:style {:color @(rf/subscribe [:time-color])}}
        (-> @(rf/subscribe [:time])
            .toTimeString
            (clojure.string/split " ")
            first)])
    
  • 引用cljsjs的css 在public目录下的任意位置创建一个以.main.less结尾的文件,然后inline import库文件里面的css。 然后执行lein less4clj once 再然后在对应的html文件里直接用link tag引用生成的css文件 可参考/resource/public/css/antd.main.less文件

有用的库

推荐官方教程

  • Reagent: Clojurescript的库,最要作用:hiccup -> react 组件
  • Kee-frame: Clojurescript的状态管理
  • Shadow-cljs: 包管理,集成工具,需要首先安装npm install -g shadow-cljs
  • Hiccup clojure里书写html的库
  • Re-frame cljs状态管理,路由
  • AntD: js库老朋友,不多介绍了

必看的公司blog

视频教程

Tools

其他推荐