配置 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 "© @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))))
注意最后这里:必须先挂在组件,然后再绑定元素事件,否则元素不存在会报错。