ハッキングバカ

プログラミングに関するメモ

Clojure で チャットアプリを作る [Websocket]

環境: macOS 10.13.6, Leiningen 2.8.1 on Java 1.8.0_112

Clojure の Web フレームワークである Luminus を使えば Websocket を用いたチャットアプリが手軽に実現でき、パフォーマンスもすぐれているらしい。
まずは

$ lein new luminus chat

でプロジェクトを作成。
サーバサイドの Websocket コードを配置。

; src/clj/chat/routes/websocket.clj

(ns chat.routes.websocket
  (:require [compojure.core :refer [GET defroutes wrap-routes]]
            [clojure.tools.logging :as log]
            [immutant.web.async :as async]))

(defonce channels (atom #{}))

(defn connect! [channel]
  (log/info "channel open")
  (swap! channels conj channel))

(defn disconnect! [channel {:keys [code reason]}]
  (log/info "close code:" code "reason:" reason)
  (swap! channels #(remove #{channel} %)))

(defn notify-clients! [channel msg]
  (doseq [channel @channels]
    (async/send! channel msg)))

(def websocket-callbacks
  "WebSocket callback functions"
  {:on-open connect!
   :on-close disconnect!
   :on-message notify-clients!})

(defn ws-handler [request]
  (async/as-channel request websocket-callbacks))

(defroutes websocket-routes
  (GET "/ws" [] ws-handler))

これを handler から呼び出す。

; src/clj/chat/handler.clj
(ns chat.handler
  (:require [chat.middleware :as middleware]
            [chat.layout :refer [error-page]]
            [chat.routes.home :refer [home-routes]]
            [chat.routes.websocket :refer [websocket-routes]] ; ここと
            [compojure.core :refer [routes wrap-routes]]
            [ring.util.http-response :as response]
            [compojure.route :as route]
            [chat.env :refer [defaults]]
            [mount.core :as mount]))

(mount/defstate init-app
  :start ((or (:init defaults) identity))
  :stop  ((or (:stop defaults) identity)))

(mount/defstate app
  :start
  (middleware/wrap-base
    (routes
      websocket-routes ; ここを追加
      (-> #'home-routes
          (wrap-routes middleware/wrap-csrf)
          (wrap-routes middleware/wrap-formats))
          (route/not-found
             (:body
               (error-page {:status 404
                            :title "page not found"}))))))

html ファイル。デフォの home を変更。

<!-- resource/templates/home.html -->
<input type="text" placeholder="メッセージ" id="chat_form">
<input  type="button" value="送信" id="chat_send">
<div id="messages"></div>
<script src="/js/ws.js"></script>

クライアントサイドの js ファイルを作成。公式では clojurescript を使っているがここではプレーン js を使う。
また、直接 public/js 配下においているがきちんとしたものを作るときは別のディレクトリでやるべきだろう。

// resource/public/js/ws.js
let socket = new WebSocket("ws://" + location.host + "/ws");

socket.onmessage = function(event) {
	let chat = JSON.parse(event.data)["chat"];
	let messages = document.getElementById("messages");
	let first_child = messages.firstChild;
	let msg = document.createElement("div");
	msg.innerHTML = chat
	messages.insertBefore(msg, first_child);
}

window.onload = function () {
	document.getElementById('chat_send').addEventListener('click',
		(function () { socket.send(JSON.stringify({chat: document.getElementById('chat_form').value}));
			document.getElementById('chat_form').value=""}), false);
}

これで完成。

$ lein run

でサーバを起動。
http://localhost:3000/ にアクセスする。
複数のブラウザを立ち上げてメッセージが同期していることを確認。
f:id:hackbaka:20180803171851p:plain
参考: Luminus - a Clojure web framework