配置 project.clj
添加本章依賴
;; Domina 庫
[domina "1.0.3"]
;; 前端組件庫
[reagent "0.8.1"]
;; 前端組件工具庫
[reagent-utils "0.3.1"]
配置前后端共享代碼文件夾
修改 project.clj
涌庭,將共享代碼路徑添加到源文件路徑配置中去
;; 指定源文件和資源文件路徑
:source-paths ["src" "src/cljc"]
;; 設(shè)置 cljsbuild 編譯器參數(shù)
:cljsbuild {
:builds {
;; 開發(fā)環(huán)境
:dev {
;; 源代碼目錄
:source-paths ["src-cljs" "src/cljc"]
......
靜態(tài)文件
修改 index.html
以適用于組件化
- 需要一個空的
div
元素芥被,作為組件掛載的容器即可 - 另外要調(diào)用組件化腳本中的函數(shù)
文件: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
命名空間的問題(本節(jié)不是項目中的代碼,只是作為講解)
如果多個 JS 模塊中都有 init
函數(shù)坐榆,最后都被編譯到 main.js
中拴魄,會出現(xiàn)命名沖突沖突
解決問題的方法:
- 用
^:export
標(biāo)記init
函數(shù),則函數(shù)必須使用命名空間名限定才能訪問 - 不再將
init
函數(shù)綁定到window.onload
上席镀,而是直接再頁面中調(diào)用該函數(shù)<script>soul_talk.core.init();</script>
core.cljs
腳本代碼如下修改:
;; 為 Form 綁定 onsubmit 處理函數(shù)
;; 導(dǎo)出該函數(shù)
(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 處理函數(shù)匹中,不再需要
;;(set! (.-onload js/window) init)
login.html
頁面代碼修改如下:
{% block page-script %}
<script>soul_talk.core.init();</script>
{% endblock %}
ClojureScript 命名空間的相互引用(本節(jié)不是項目中的代碼,只是作為講解)
soul-talk.core
為什么要引入 soul-talk.login
豪诲?顶捷?
-
core.cljs
是全局入口,其代碼會被編譯到main.js
中屎篱;main.js
又被base.html
模板 引入服赎,其中的代碼會被自動執(zhí)行 -
login.cljs
沒有被頁面明確引入,因此其中的代碼頁面看不到 - 在
core.cljs
中引入login.cljs
交播,相當(dāng)于main.js
引入了login.cljs
中的代碼重虑。 之后,任何引入了main.js
的頁面都能看到login
命名空間了
因此在 soul-talk.core
中有以下代碼
(ns soul-talk.core
(:require
[soul-talk.login]))
創(chuàng)建前后端共享代碼
新建 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})$")
;; 驗(yàn)證 Email 是否為空
;; 參數(shù)變?yōu)槲谋救崩鳎皇?HTML 元素
(defn validate-email [email]
(if (and (string? email)
(re-matches *email-re* email))
true
false))
;; 驗(yàn)證密碼是否為空
;; 參數(shù)變?yōu)槲谋荆皇?HTML 元素
(defn validate-passoword [password]
(if (and (string? password)
(re-matches *password-re* password))
true
false))
組件化首頁面
修改 soul-talk/core.cljs
文件隧土,原先只有原生代碼提针,現(xiàn)在增加客戶端庫的相關(guān)引用。注意:
- Session 庫是客戶端 Session次洼,和服務(wù)端沒任何關(guān)系
- 當(dāng)前代碼关贵,登錄狀態(tài)并不會顯示出來,因?yàn)榭蛻舳?Session 并沒有設(shè)置
(ns soul-talk.core
(:require [soul-talk.login :as login]
[reagent.core :as r]
;; 可以創(chuàng)建和管理客戶端 Session 卖毁,注意和服務(wù)端沒關(guān)系
[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]]))
;; 這個函數(shù)提交的時候被調(diào)用,驗(yàn)證輸入是否正確
(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))))
;; 如果驗(yàn)證不成功亥啦,則在輸入框上增加樣式炭剪;
;; 如果驗(yàn)證成功,則移除樣式
;; 這個函數(shù)翔脱,輸入框失去焦點(diǎn)的時候被調(diào)用
(defn validate-invalid [input-id vali-fun]
(if-not (vali-fun (dom/value input-id)) ;; 修改奴拦,驗(yàn)證函數(shù)傳入文本,而不是 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"}
;; 標(biāo)題
[:h1.h3.mb-3.font-weight-normal "Please sign in"]
;; Email 部分
[:div.form-group
;; Email 標(biāo)簽
[:label.sr-only "email" "email"]
;; Email 輸入框
[:input#email.form-control
{:type "text"
:name "email"
:auto-focus true
:placeholder "Email Address"
;; 焦點(diǎn)丟失的時候届吁,調(diào)用驗(yàn)證函數(shù)
: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"
;; 焦點(diǎn)丟失的時候错妖,調(diào)用驗(yàn)證函數(shù)
:on-blur #(validate-invalid (dom/by-id "password") validate-password)}]
;; 錯誤提示信息
[:div.invalid-feedback "無效的密碼"]]
;; “記住我” 復(fù)選框
[: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 "登錄"}]
;; 版權(quán)信息
[:p.mt-5.mb-3.text-muted "© @2018"]])
;; 渲染登陸表單組件绿鸣,并掛載到 `content` div元素上
(reagent/render
[login-component] (dom/by-id "content"))
;; 為 Form 綁定 onsubmit 處理函數(shù)
;; 導(dǎo)出該函數(shù),從頁面調(diào)用
(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))))
注意最后這里:必須先掛在組件潮模,然后再綁定元素事件,否則元素不存在會報錯痴施。