01 Clojure Web 程序基本架构

流程图

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

创建项目

lein new soul-talk

配置 Git

初始化 Git 仓库

git init

设置 .gitignore 忽略项

/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-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

配置 project.clj

project.clj 中修改以下内容

添加各种依赖库

: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 ,可以将项目的依赖都升级到最新版本

指定程序启动入口函数

可以指定命名空间中的某个函数作为程序启动入口函数

;; 程序运行入口
:main soul-talk.core/foo

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

;; 程序运行入口
:main soul-talk.core

配置中间件

一个中间件也是一个普通的函数,但是他接收一个 Handler ,返回一个 Handler

创建自定义中间件

创建一个自定义的中间件 wrap-nocache,功能是在头信息中添加不缓存设置

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

引入 Ring 内置中间件

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

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

引入默认常用中间件

依赖:[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]))

添加静态资源

创建静态 index.html 模板页面

resources 目录下新建 index.html 模板页面

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

创建自定义 Handler

一个 Handler 就是接收一个 Request ,返回一个 Response 。二者在 Clojure 中都以哈希表的形式出现。

在 Handler 中读取 index.html 作为响应模板

修改 home-handle 函数,直接读取 index.html 返回

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

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

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

在 Handler 中可以使用简化 Response 库

依赖:[metosin/ring-http-response "0.9.1"]

由于 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>")))

配置路由

使用 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>")))

创建服务启动入口 Handler

将所有的 Handler 、中间件、路由,通过链式调用,合并成一个启动 Handler ,绑定到一个名字上。注意:路由总是作为链式调用的第一个参数

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

添加服务启动代码

从主函数启动

在主函数中调用 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}

启动服务

从主函数 -main 启动

lein run

从 Ring 插件启动

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

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

最终代码

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>

推荐阅读更多精彩内容