01 Clojure Web 程序基本架构

0. 流程图

01 - Clojure Web 程序基本结构.png

1. 创建项目

lein new soul-talk

2. 配置 Git

初始化 Git 仓库

git init

设置 .gitignore 忽略项

/target
/classes
/checkouts
/target
/classes
/checkouts
profiles.clj
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
/.prepl-port
.hgignore
.hg/

figwheel_server.log
/resources/public/js
db.sqlite

提交到 GitHub

到 Github 创建仓库,然后

git remote add origin https://github.com/myqiao/soul-talk.git
git push -u origin master

3. 配置依赖

配置国内源

lein 的官方源经常访问不到,在项目配置文件中添加镜像

 ;; 配置镜像库
  :mirrors {"central"  {:name "aliyun"
                        :url  "https://maven.aliyun.com/repository/central"}
            #"clojars" {:name         "tsinghua"
                        :url          "https://mirrors.tuna.tsinghua.edu.cn/clojars/"
                        :repo-manager true}}

添加各种依赖库

:dependencies [
        ;; Clojure 主运行时库
        [org.clojure/clojure "1.9.0"]

        ;; Ring 库
        [ring "1.7.1"]

        ;; 基于 Ring 的 Response 简化工具库
        [metosin/ring-http-response "0.9.1"]

        ;; 常用中间件集合
        [ring/ring-defaults "0.3.2"]

        ;; 路由库
        [compojure "1.6.1"]]

配置并运行 lein-ancient 依赖版本管理插件

在配置文件 project.clj 中添加

  :profiles {
        :user {
            :dependencies []
            :plugins [[lein-ancient "0.6.15"]]}}

运行命令 lein ancient upgrade :interactive ,将项目的依赖升级到最新版本

4. Handler 原理讲解

Handler 就是一个普通函数,接受一个 request ,返回一个 response ,二者都是 Clojure 哈希表结构

5. 中间件

中间件原理讲解

中间件就是一个普通函数,他接受一个 handler 函数,返回一个 handler 函数

这有点像俄罗斯套娃:

  • 中间件返回的 handler 是外层的套娃
  • 外层套娃持有一个内层套娃,就是中间件接收的那个 handler

每个中间件都会增加一层套娃,可以一层一层的套下去

请求响应数据流

  • 最外层的 handler 总是最先接受到服务器请求
  • 请求从最外层一层一层向内传递,直到最内层
  • 每一层在向内传递的请求过程中可以对请求进行处理
  • 到最内层后,根据请求生成响应
  • 响应从最内层一层一层向外传递,直到最外层
  • 每一层在向外传递响应的过程中可以对响应进行处理
  • 最外层的 handler 处理完响应后,返回给服务器

创建自定义中间件 wrap-nocache

创建一个自定义的中间件 wrap-nocache,只处理响应,不处理请求。功能是在响应头信息中添加不缓存设置

;; 自定义中间件:加入不缓存头信息
(defn wrap-nocache [handler]
  (fn [request]
    (-> request
        handler
        (assoc-in [:headers "Pragma"] "no-cache"))))

引入 Ring 内置热重载中间件 wrap-reload

这是一个 Ring 内置的中间件 wrap-reload,但是目前自动载入还不好使,必须等 Ring 插件配置好才好使

(ns soul-talk.core
  (:require ......
            [ring.middleware.reload :refer [wrap-reload]])) ;; 添加

引入默认常用中间件 ring-defaults

依赖:[ring/ring-defaults "0.3.2"]

ring-defaults 库包含了四种中间件:

  • api-defaults
  • site-defaults
  • secure-api-defaults
  • secure-site-defaults

相当于启用了会话、快闪、调试、头信息、文件上传等等一系列内置中间件

(ns soul-talk.core
  (:require ......
    ;; 引入常所用中间件
    [ring.middleware.defaults :refer :all]))

6. 添加静态资源

创建静态 index.html 模板页面

静态资源一般放置在 resources 下,这个路径可以直接被 io/resource 函数和其他 io 函数读取

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
这是一个主页;
</body>
</html>

7. 创建自定义 Handler

再次强调,Handler 处理请求,返回响应

创建 home-handle

home-handle 中,直接读取 index.html 返回

(ns soul-talk.core
  (:require ......
    [clojure.java.io :as io]))

;; 自定义 Handler,读取静态页面返回
(defn home-handle [request]
  (io/resource "index.html"))

注意:在 resources 目录 下的资源可以被程序读取,而且不需要加路径,但是不能被用户直接访问

响应的生成方式(原理讲解)

响应就是一个字符串或者一个哈希表 ,其生成方式有以下几种

  • 直接返回一个字符串
  • 直接返回一个哈希表
  • 直接读取静态页面返回
  • 利用 Response 库构造响应哈希表

演示:使用简化 Response 库构造响应

由于 Ring 自带的 ring.util.response 的功能比较基础,需要自己写返回码、头信息等等,我们可以使用 ring-http-response 库来简化代码。

下面的代码并不是项目中的代码,只是用来演示

(ns soul-talk.core
  (:require ......
    [ring.util.http-response :as resp]))

(defn home-handle [request]
  ;; 这里简化了代码
  (resp/ok (str "<html><body><body>your IP is"
                (:remote-addr request) 
                "</body></html>")))

8. 路由

路由原理讲解

对路径字符串进行模式匹配,返回对应的 handler,并将这个 handler 绑定到路由变量上。因此路由变量就是一个 handler

使用 Compojure 路由库

依赖:[compojure "1.6.1"]

;; 引入路由函数
(ns soul-talk.core
  (:require ......
        [compojure.core :refer [routes GET]]
        [compojure.route :as route]))

定义路由规则

定义路由规则。注意:最终的路由变量 app-route 其实也是一个 Handler

;; 创建路由规则,最终返回的是一个普通的 Handler
(def app-routes
  (routes
    (GET "/" request (home-handle request))
    (GET "/about" [] (str "这是关于我的页面"))
    (route/not-found "<h1>Page not found</h1>")))

9. 程序启动入口

需要告诉程序,哪个函数是程序启动函数。可以在配置文件中直接指定启动入口函数

;; 直接指定启动入口函数
:main soul-talk.core/foo

也可以只指定命名空间,则命名空间中的 -main 函数自动成为启动入口函数

;; 指定入口模块,`-main` 函数自动成为启动入口函数
:main soul-talk.core

10. 请求入口

原理讲解:请求入口

==前面讲过,服务器接收到的请求,会首先送给就是套娃最外层的 handler ,即最后一个中间件返回的 handler ==

路由变量是一个 handler,一般要作为最内层的 handler ,因此它会作为第一个中间件的参数

而最后一个中间件返回的handler ,就是最外层的 handler,即请求入口 handler

创建请求入口 Handler

将服务器请求、中间件、Handler 、路由组合成一个 Handler ,相当于一个成品套娃,命名为 app

==注意:路由总是作为链式调用的第一个参数,即作为最内层的原生 Handler==

(def app
  (-> app-routes  ;; 链式调用的第一个参数为路由 Handler
      wrap-nocache
      ;; 自动重载中间件
      wrap-reload
      ;; 常用中间件
      (wrap-defaults site-defaults)))

11. 启动服务

原理讲解:服务器启动方式

服务器启动,只需要把请求入口 handler 传给 jetty-run 函数,并配置一定的参数即可。有两种方式

从主函数启动

在主函数中调用 jetty/run-jetty ,将服务启动入口 Handler 传给他即可

(ns soul-talk.core
  (:require ......
        [ring.adapter.jetty :as jetty]))
        
(defn -main []
  (jetty/run-jetty app {:port 3000 :join? false}))

从 Ring 插件启动

使用 lein-ring 插件,需要给插件指定服务启动入口 Handler。在 project.clj 中添加以下代码:

:plugins [[lein-ring "0.12.4"]]

;; 插件不通过 main 函数启动,只需要指定一个入口 Handler
:ring {:handler soul-talk.core/app}

12. 测试运行

从主函数 -main 启动

lein run

从 Ring 插件启动

使用插件启动后,自动载入中间件才好使

lein ring server ;; 默认端口 3000
lein ring server 4000 ;; 
lein ring server-headless ;; 不会打开浏览器

13. 最终代码

project.clj

(defproject soul-talk "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.9.0"]
                 
                 ;; Ring 库
                 [ring "1.7.1"]
                 
                 ;; 基于 Ring 的 Response工具库
                 [metosin/ring-http-response "0.9.1"]
                 
                 ;; 常用中间件集合
                 [ring/ring-defaults "0.3.2"]
                 
                 ;; 路由库
                 [compojure "1.6.1"]]
  
  
  ;; 基于 Lein 的 Ring 插件
  :plugins [[lein-ring "0.12.4"]]

  ;; 插件不通过 main 函数启动,只需要指定一个入口 Handler
  :ring {:handler soul-talk.core/app}
  
  ;; 不使用插件的时候,程序仍然从 main 函数启动
  :main soul-talk.core
  
  
  :profiles {
        :user {
            :dependencies []
            :plugins [[lein-ancient "0.6.15"]]}})

src/soul-takl/core.clj

(ns soul-talk.core
  (:require
    ;; 标准库 io 函数
    [clojure.java.io :as io]

    ;; 响应简化库
    [ring.util.http-response :as resp]

    ;; 中间件
    [ring.middleware.reload :refer [wrap-reload]]
    [ring.middleware.defaults :refer :all]

    ;; 路由库
    [compojure.core :refer [routes GET]]
    [compojure.route :as route]

    ;; 服务启动函数
    [ring.adapter.jetty :as jetty])) 


;; ************************************************
;; Handler 区域
;; ************************************************

;; 自定义 Handler,读取静态页面返回
(defn home-handle [request]
  (io/resource "index.html"))



;; ************************************************
;; 路由 区域
;; ************************************************

;; 创建路由规则,最终返回的是一个普通的 Handler
(def app-routes
  (routes
    (GET "/" request (home-handle request))
    (GET "/about" [] (str "这是关于我的页面"))
    (route/not-found "<h1>Page not found</h1>")))


;; ************************************************
;; 中间件 区域
;; ************************************************

;; 自定义中间件:加入不缓存头信息
(defn wrap-nocache [handler]
  (fn [request]
    (-> request
        handler
        (assoc-in [:headers "Pragma"] "no-cache"))))



;; ************************************************
;; 启动代码 区域
;; ************************************************

(def app
  (-> app-routes  ;; 链式调用的第一个参数为路由 Handler
      wrap-nocache
      ;; 自动重载中间件
      wrap-reload
      ;; 常用中间件
      (wrap-defaults site-defaults)))


(defn -main []
  (jetty/run-jetty app {:port 3000 :join? false}))

resources/index.html

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

推荐阅读更多精彩内容