04 使用 ClojureScript 进行客户端验证,并使用 Reagent 组件化页面

04 使用 ClojureScript 进行客户端验证,并使用 Reagent 组件化页面.png

配置 project.clj

添加本章依赖

;; Domina 库 
[domina "1.0.3"]

;; 前端组件库 
[reagent "0.8.1"]

;; 前端组件工具库
[reagent-utils "0.3.1"]

配置前后端共享代码文件夹

修改 project.clj ,将共享代码路径添加到源文件路径配置中去

;; 指定源文件和资源文件路径
:source-paths ["src"   "src/cljc"]

;; 设置 cljsbuild 编译器参数
:cljsbuild {
    :builds {
        ;; 开发环境
        :dev {
            ;; 源代码目录
            :source-paths ["src-cljs"   "src/cljc"] 
            
            ......

静态文件

修改 index.html 以适用于组件化

  • 需要一个空的 div 元素,作为组件挂载的容器即可
  • 另外要调用组件化脚本中的函数

文件:resources/index.html

{% extends "base.html" %}

{% block content %}
<div id="app">

</div>
{% endblock %}


{% block page-script %}
<script>soul_talk.core.init();</script>
{% endblock %}

修改 login.html 以适用于组件化

文件:resources/login.html

{% extends "base.html" %}


{% block page-title %}
Soul Talk Login 
{% endblock %}


{% block page-css %}
<link rel="stylesheet" href="/css/login.css">
{% endblock %}


{% block content %}

<!-- 挂载组件的元素 -->
<div class="container" id="content">
</div>

{% endblock %}

{% block page-script %}
<script>soul_talk.login.init();</script>
{% endblock %}

ClojureScript

命名空间的问题(本节不是项目中的代码,只是作为讲解)

如果多个 JS 模块中都有 init 函数,最后都被编译到 main.js 中,会出现命名冲突冲突

解决问题的方法:

  • ^:export 标记 init 函数,则函数必须使用命名空间名限定才能访问
  • 不再将 init 函数绑定到 window.onload 上,而是直接再页面中调用该函数 <script>soul_talk.core.init();</script>

core.cljs 脚本代码如下修改:

;; 为 Form 绑定 onsubmit 处理函数
;; 导出该函数
(defn ^:export init []
  (if (and js/document (.-getElementById js/document))
    (let [login-form (.getElementById js/document "loginForm")]
      (set! (.-onsubmit login-form) validate-form))))

;; 为 Window 绑定 onload 处理函数,不再需要
;;(set! (.-onload js/window) init)

login.html 页面代码修改如下:

{% block page-script %}
<script>soul_talk.core.init();</script>
{% endblock %}

ClojureScript 命名空间的相互引用(本节不是项目中的代码,只是作为讲解)

soul-talk.core 为什么要引入 soul-talk.login??

  • core.cljs 是全局入口,其代码会被编译到 main.js 中;main.js 又被 base.html 模板 引入,其中的代码会被自动执行
  • login.cljs 没有被页面明确引入,因此其中的代码页面看不到
  • core.cljs 中引入 login.cljs,相当于 main.js 引入了 login.cljs 中的代码。 之后,任何引入了 main.js 的页面都能看到 login 命名空间了

因此在 soul-talk.core 中有以下代码

(ns soul-talk.core
  (:require 
    [soul-talk.login]))

创建前后端共享代码

新建 cljc/soul_talk/auth_validate.cljc 文件

注意:文件和文件夹必须使用下划线,在代码中使用中划线

(ns soul-talk.auth-validate)

;; 密码格式
(def ^:dynamic *password-re* #"^(?=.*\d).{4,128}$")
;; Email 格式
(def ^:dynamic *email-re* #"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$")


;; 验证 Email 是否为空
;; 参数变为文本,而不是 HTML 元素
(defn validate-email [email]
  (if (and (string? email)
           (re-matches *email-re* email))
    true
    false))
    

;; 验证密码是否为空
;; 参数变为文本,而不是 HTML 元素
(defn validate-passoword [password]
  (if (and (string? password)
           (re-matches *password-re* password))
    true
    false))

组件化首页面

修改 soul-talk/core.cljs 文件,原先只有原生代码,现在增加客户端库的相关引用。注意:

  • Session 库是客户端 Session,和服务端没任何关系
  • 当前代码,登录状态并不会显示出来,因为客户端 Session 并没有设置
(ns soul-talk.core
  (:require [soul-talk.login :as login]
            [reagent.core :as r]

            ;; 可以创建和管理客户端 Session ,注意和服务端没关系
            [reagent.session :as session]
            
            [domina :as dom]))


(defonce posts (r/atom []))
(defonce navs (r/atom []))
(defonce archives (r/atom []))

(defn blog-header-component []
  (fn []
    [:div.blog-header.py-3
     [:div.row.flex-nowrap.justify-content-between.align-items-center
      [:div.col-4.pt-1
       [:a.text-muted {:href "#"} "订阅"]]
      [:div.col-4.text-center
       [:a.blog-header-logo.text-dark {:href "/"} "Soul Talk"]]
      [:div.col-4.d-flex.justify-content-end.align-items-center
       (if (session/get :identity)
         (let [name (session/get :identity)]
           [:span.navbar-text (str "欢迎你 " name)]
           [:a.btn.btn-sm.btn-outline-secondary {:href "/logout"} "退出"])
         [:a.btn.btn-sm.btn-outline-secondary {:href "/login"} "登录"])]]]))

(defn nav-scroller-header-component [navs]
  (fn []
    [:div.nav-scroller.py-1.mb-2
     [:nav.nav.d-flex.justify-content-between
      (for [{:keys [href value] :as nav} navs]
        ^{:key nav} [:a.p-2.text-muted {:href href :id value} value])]]))

(defn jumbotron-header-component []
  (fn []
    [:div.jumbotron.p-3.p-md-5.text-white.rounded.bg-dark
     [:div.col-md-6.px-0
      [:h1.display-4.font-italic "Title of a longer featured blog post"]
      [:p.lead.mb-0
       [:a.text-white.font-weight-bold {:href "#"} "Continue reading..."]]]]))

(defn header-component []
  (fn []
    [:div.container
     [blog-header-component]
     [nav-scroller-header-component @navs]
     [jumbotron-header-component]]))

(defn footer-component []
  (fn []
    [:div.container.blog-footer
     [:p "Blog template built for"
      [:a {:href "https://getbootstrap.com/"} "Bootstrap"]
      " by "
      [:a {:href "https://twitter.com/mdo"} "@mdo"]
      "."]
     [:p
      [:a {:href "#"} "Back to top"]]]))

(defn blog-post-component [posts]
  (fn []
    [:div.col-md-8.blog-main
     [:h3.pb-3.mb-4.font-italic.border-bottom
      "From the Firehose"]
     (for [{:keys [id title meta author content] :as post} posts]
       ^{:key post} [:div.blog-post
                     [:h2.blog-post-title title]
                     [:p.blog-post-meta meta
                      [:a {:href "#" :id id} author]]
                     [:p content]])
     [:nav.blog-pagination
      [:a.btn.btn-outline-primary {:href "#"} "Older"]
      [:a.btn.btn-outline-secondary.disabled {:href "#"} "Newer"]]]))

(defn main-component []
  (fn []
    [:div.container {:role "main"}
     [:div.row
      [blog-post-component @posts]
      [:aside.col-md-4.blog-sidebar
       [:div.p-3.mb-3.bg-light.rounded
        [:h4.font-italic "About"]
        [:p.mb-0 "Etiam porta <em>sem malesuada magna</em> mollis euismod."]]
       [:div.p-3
        [:h4.font-italic "Archives"]
        [:ol.list-unstyled.mb-0
         (for [{:keys [time href] :as archive} @archives]
           ^{:key archive} [:li [:a {:href href} time]])]]
       [:div.p-3
        [:h4.font-italic "Elsewhere"]
        [:ol.list-unsty
         [:li [:a {:href "#"} "GitHub"]]
         [:li [:a {:href "#"} "Weibo"]]
         [:li [:a {:href "#"} "Twitter"]]]]]]]))

(defn home-component []
  [:div
   [header-component]
   [main-component]
   [footer-component]])

(reset! navs [{:href "#"
                :value "World"}
              {:href "#"
               :value "China"}
              {:href "#"
               :value "China1"}
              {:href "#"
               :value "China2"}])

(reset! posts [{:id "post1"
               :title   "Sample blog post"
               :meta    "January 1, 2014 by"
               :author  "soul"
               :content "asasfasfasffsd"}
              {:id "post2"
               :title   "Another blog post"
               :meta    "December 23, 2013 by "
               :author  "jiesoul"
               :content "Cum sociis natoque penatibus et magnis"}])

(reset! archives [{:href "#"
                    :time "March 2018"}
                  {:href "#"
                   :time "May 2018"}])


(defn ^:export init []
  (if (and js/document
           (.-getElementById js/document))
    (r/render
      [home-component]
      (dom/by-id "app"))))

组件化登陆页面

文件:src-cljs/soul_talk/login.cljs

注意:两个输入框的 required 属性得删除,否则会影响逻辑流程

(ns soul-talk.login
  (:require [domina :as dom]
            [domina.events :as ev]
            [reagent.core :as reagent :refer [atom]]
            ;; 引入共享代码
            [soul-talk.auth-validate :refer [validate-email validate-password]]))


;; 这个函数提交的时候被调用,验证输入是否正确
(defn validate-form []
  (let [email (dom/by-id "email")
        password (dom/by-id "password")]
    (if (and (-> email dom/value validate-email ) (-> password dom/value validate-password))
      true
      (do
        (js/alert "email和密码不能为空")
        false))))


;; 如果验证不成功,则在输入框上增加样式;
;; 如果验证成功,则移除样式
;; 这个函数,输入框失去焦点的时候被调用
(defn validate-invalid [input-id vali-fun]
  (if-not (vali-fun (dom/value input-id)) ;; 修改,验证函数传入文本,而不是 HTML 元素
    (dom/add-class! input-id "is-invalid")
    (dom/remove-class! input-id "is-invalid")))
    
    
;; 组件化登陆表单
(defn login-component []
  ;; 登陆表单
  [:form#loginForm.form-signin {:action "/login" :method "post"}
    ;; 标题
    [:h1.h3.mb-3.font-weight-normal "Please sign in"]

    ;; Email 部分
    [:div.form-group
      ;; Email 标签
      [:label.sr-only "email" "email"]
      ;; Email 输入框
      [:input#email.form-control
        {:type "text" 
         :name "email" 
         :auto-focus true 
         :placeholder "Email Address"
         ;; 焦点丢失的时候,调用验证函数
         :on-blur #(validate-invalid (dom/by-id "email") validate-email)}] 
      ;; 错误提示信息
      [:div.invalid-feedback "无效的 Email"]] 

    ;; 密码部分
    [:div.form-group
      ;; 密码输入框
      [:label.sr-only "password" "password"]
      [:input#password.form-control
        {:type "password" 
         :name "password" 
         :placeholder "password"
         ;; 焦点丢失的时候,调用验证函数
         :on-blur    #(validate-invalid (dom/by-id "password") validate-password)}]
      ;; 错误提示信息
      [:div.invalid-feedback "无效的密码"]] 


    ;; “记住我” 复选框
    [:div.form-group.form-check
      [:input#rememeber.form-check-input {:type "checkbox"}]
      [:label "记住我"]]

    ;; 错误信息
    [:div#error]

    ;; 提交按钮
    [:input#submit.btn.btn-lg.btn-primary.btn-block {:type "submit" :value "登录"}]

    ;; 版权信息
    [:p.mt-5.mb-3.text-muted "&copy @2018"]])


;; 渲染登陆表单组件,并挂载到 `content` div元素上
(reagent/render
  [login-component] (dom/by-id "content"))



;; 为 Form 绑定 onsubmit 处理函数
;; 导出该函数,从页面调用
(defn ^:export init []
  ;; 渲染登陆表单组件,并挂载到 `div#content` 元素上
  (reagent/render
    [login-component] (dom/by-id "content"))

  (if (and js/document (.-getElementById js/document))
    (let [login-form (dom/by-id "loginForm")]
      (set! (.-onsubmit login-form) validate-form))))

注意最后这里:必须先挂在组件,然后再绑定元素事件,否则元素不存在会报错。

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 7,247评论 1 24
  • # 传智播客vue 学习## 1. 什么是 Vue.js* Vue 开发手机 APP 需要借助于 Weex* Vu...
    再见天才阅读 2,704评论 0 6
  • 文/冰雪伊人 —词曲系列— 微雨暗长汀,春愁向晚灯。待风来,一缕飘轻。纸上墨痕诗里字。江南韵、总分明。 湖色正疏凝...
    冰雪伊人阅读 1,404评论 14 43
  • 借口之一: 我要考虑考虑 借口之二:太贵了 借口之三:别家更便宜 借口之四:超出预算 借口之五:我很满意目前的所用...
    胖子叔阅读 1,988评论 0 2
  • 我常从明亮的梦中醒来 睁眼瞬间的恍惚 是不知身处何乡的迷茫 那一刻 仿佛灵魂脱离了身体漂浮着 我在黑暗中睁眼 沉默...
    嘘看陨石阅读 35评论 0 2