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))))

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • # 传智播客vue 学习## 1. 什么是 Vue.js* Vue 开发手机 APP 需要借助于 Weex* Vu...
    再见天才阅读 3,525评论 0 6
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 文/冰雪伊人 —词曲系列— 微雨暗长汀,春愁向晚灯。待风来,一缕飘轻。纸上墨痕诗里字。江南韵、总分明。 湖色正疏凝...
    西域冰雪阅读 3,557评论 14 43
  • 借口之一: 我要考虑考虑 借口之二:太贵了 借口之三:别家更便宜 借口之四:超出预算 借口之五:我很满意目前的所用...
    胖子叔阅读 4,395评论 0 2