0. 流程图
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>