Clojureでルーティング機能を実装する

前々からClojureの勉強を続けていて、今回はルーティングの実装について。

komi.hatenadiary.com

Clojureでサーバーを立てて、そのあと順調に色々実装して最終的にRESTful APIの実装までやりたいな〜なんて思っていたのだけれど、その合間で色々Clojureについて分からないことが多々発生して、それらを対処していて気がついたらだいぶ時間が空いてしまった。

そんなこんなでようやくClojureでサーバー開発の進捗を生むことに。

今回はルーティングについての実装をやっていく。

ルーティングについて

例えばツイッターのURLは

https://twitter.com

となっている。

そして、自分のアカウント(今回は@komi_edtr_1230とする)へのURLは

https://twitter.com/komi_edtr_1230

という感じ。

ここで、/komi_edtr_1230というような追加部分によって自分のアカウントへとコントロールされるようになっている。

このように、今まではローカルでサーバーを立ててポート番号を3000としていた場合

http://localhost:3000

にブラウザからアクセスすることによって表示していた何らかの文章を表示していたが、それを

http://localhost:3000/hoge
http://localhost:3000/hoge/piyo

などと階層化したりする。

厳密にはもう少しちゃんとした説明があると思うが、とりあえずこんな感じのものをルーティングと呼ぶ。

とりあえずサーバー立てようぜ

概念は理解したし、とりあえずサーバーを立てようと思う。

例の如く、最初にLeiningenでプロジェクトの雛形を作る。

lein new app mini_server

そして、でき上がった雛形に対し、project.cljをいじって今回使うライブラリを書き込む。

(defproject mini_server "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [ring "1.7.1"]]
  :main ^:skip-aot mini-server.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

今回使うのはRingのみなので、:dependenciesにringを追記する。

さて、src/mini_server/core.cljにコードをゴリゴリ書いていく。

(ns mini-server.core
  (:gen-class)
  (:require [ring.adapter.jetty :as jetty]))

(defonce server (atom nil))

(defn handler [req]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hi ! This is test server !"})

(defn start-server []
  (when-not @server
    (reset! server
            (jetty/run-jetty handler
                              {:port  3000
                               :join? false}))))

(defn stop-server []
  (when @server
    (.stop @server)
    (reset! server nil)))

(defn reset-server []
  (when @server
    (stop-server)
    (start-server)))

(defn -main
  "start this server"
  [& args]
  (start-server))

前回までの作業はこんな具合で、

lein run

として、ブラウザでhttp://localhost:3000を開いたらHi ! This is test server !と表示されていると思う。

ところでリクエストって?

今のところ、handlerが決め打ちでメッセージを出すような感じになっている。

ルーティングの実装ではこのhandlerを触っていくわけだが、最初にサーバーを動かした際のリクエストについて簡単な解説を。

上記のコードでは(defn handler [req] ...)というようにリクエストを引数にとって実装をしていたわけだけど、このリクエストは一体何者なんだろう。

それの中身を見るために、一度途中にprintlnを挟んで見てみる。

(defn handler [req]
  (println req)
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hi ! This is test server !"})

そうすると、このような感じ。

{:ssl-client-cert nil, 
 :protocol HTTP/1.1,
 :remote-addr 0:0:0:0:0:0:0:1, 
 :headers {sec-fetch-site none, host localhost:3000, user-agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36, sec-fetch-user ?1, connection keep-alive, upgrade-insecure-requests 1, accept text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3, accept-language en-US,en;q=0.9,ja;q=0.8, accept-encoding gzip, deflate, br, sec-fetch-mode navigate, cache-control max-age=0}, 
 :server-port 3000, 
 :content-length nil, 
 :content-type nil, 
 :character-encoding nil, 
 :uri /, 
 :server-name localhost, 
 :query-string nil, 
 :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x7600f8af HttpInputOverHTTP@7600f8af[c=0,q=0,[0]=null,s=STREAM]], 
 :scheme :http, 
 :request-method :get}

わかりやすさのために改行した。

リクエストはマップの形をしていてのだが、アクセス元の端末の情報やどのブラウザの情報かなどの情報が来ている。

ちなみにデータ構造としてはマップになっていて、これはライブラリのRingがいい感じに処理してくれているのでリクエストの情報がClojure的に扱いやすいようになっている。

さて、ここで注目したいのが:uriで、これは/となっている。

これはどういうことかというと、URLとしてはhttps://localhost:3000/にアクセスしていることを意味している。

なので、ルーティング機能を実装する際はこのURIが実装のヒントとなっていて、リクエストから(:uri req)ような感じでURIの情報を取ってきてテキトーに条件分岐させるのが良さげな気がする。

ルーティングを実装する

まず手始めに、受付可能なURIをこちら側で決めておこうと思う。

今回は//hogeがアクセス可能なURIとし、それ以外は何もしないような設定としたい。

つまり、ルーティングのマップとしては

(def routes
  {"/" home
   "/hoge" hoge-page})

としようと思う。

まず、ホーム画面へのリクエストを受けた際のページを書く。

(defn home-view [req]
  "<h1>Home</h1><a href=\"/hoge\">やっほー!ルーティングのテストのためのページだよ!ここはホーム画面!</a>")

次に、このページからのリクエストにステータス情報(200とか404とか)を付加し、さらにHTMLにおけるheadersを追加するようにする。

(defn ok [body]
  {:status 200
   :body   body})

(defn html [res]
  (assoc res
         :headers
         {"Content-Type" "text/html; charset=utf-8"}))

(defn home [req]
  (-> (home-view req)
      ok
      html))

さて、今度はhogeページの場合についても書く。

(defn hoge-view [req]
  "<h1>Hogeのページ</h1>コード書いてるときってなんかhogeって打ちたくなる...コード書いてるときってなんかhogeって打ちたくならない?")

(defn hoge-page [req]
  (-> (hoge-view req)
      ok
      html))

これらとは別で、指定されてないURIが呼ばれた際に404 page not foundが表示されるようにしたい。

(defn not-found []
  {:status 404
   :body "<h1>404 page not found</h1>"})

では各ページの準備が整ったので、最後に各ページに遷移するためのhandlerを書く。

(defn match-route [uri]
  (get routes uri))

(defn handler [req]
  (let [uri (:uri req)
        maybe-fn (match-route uri)]
    (println req)
    (if maybe-fn
      (maybe-fn req)
      (not-found))))

これらを実装して、実行してみる。

そうすると以下のように見えるはず。

f:id:komi1230:20191106234551p:plain
ホーム画面

f:id:komi1230:20191106234617p:plain
Hogeのページ

うんうん、ちゃんとURLが変わって各ページが見えてる。

まとめ

今回はリクエストについて簡単に調べたあとClojureでルーティング機能を実装し、各ページごとに表示を変えるようなものを作った。

ちなみに今回は割愛したが、ルーティングを簡単に実装するようなライブラリもあり、実際にプロダクトを作るようなケースにおいてはそういうものも試していきたい。

次回以降ではRESTful APIを実装してJSONでやり取りする何かを作っていく予定。

これからもがんばっていくぞい!