12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Immutant+Liberator+ReagentでWebサービスを作る素振り

Last updated at Posted at 2015-02-08

Webサービスを作る例。

いろんな人が散々やっているところではあるが、とりあえず練習で作っておけばいざというときに役に立つという、素振り練習的な意味で。

全体はこちら: https://github.com/t2ru/webexample

サーバ側

使用ライブラリ

  • Immutant (JBoss)
  • Liberator
  • Compojure
  • org.clojure/
    • data.json
    • java.jdbc

設計ポリシ

  • ImmutantでJBossのインフラをなるべく活用する。
    • Webサーバ
    • トランザクション制御
  • DBアクセスはclojure.java.jdbcを直で使う。
    • 後でチューニングしやすいように、SQLの抽象化は使わない。
    • XAトランザクションで囲う。(ただし、XAの信じすぎ良くない。)
    • サンプルのDBはH2(ファイルDB)を使う。商用でJBossのコンテナに載せるときに、datasourceに変えてコネクションプールを使えば良い。
  • WebサービスはLiberatorを使ってREST準拠にする。
    • URLパスでのルーティングにはCompojureを使う。
  • Compojureのハンドラ内に全部書いてしまう。
    • API内で使っているSQLを全部把握できるため、非効率なSQL発行は一目瞭然。
    • 認可情報やSQLなどを全て一覧できるようにすることで、目視のセキュリティチェックをやりやすくなる。
  • SQLインジェクションやXSSの攻撃をされない設計

コード例

src/webexample/core.clj
(defroutes task-service
  ;; コンテナ系リソース
  (GET "/task" {:keys [db]}
    (resource
      :allowed-methods [:get]
      :available-media-types ["application/json"]
      :handle-ok (list-tasks db)))

  (POST "/task" {:keys [db body]}
    (let [data (try (as-json body :key-fn keyword :eof-error? false)
                    (catch Exception e nil))
          new-id (or (:newid (first (next-task-id db))) 0)]
      (resource
        :allowed-methods [:post]
        :available-media-types ["application/json"]
        :malformed? (nil? (:title data))
        :handle-malformed (pr-str data)
        :post! (fn [_] (new-task! db new-id (:title data)))
        :handle-created {:id new-id})))

  ;; エレメント系リソース
  (GET "/task/:id" [id :as {:keys [db]}]
    (resource
      :allowed-methods [:get]
      :available-media-types ["application/json"]
      :handle-ok (fn [_] (first (get-task db)))))

  (PUT "/task/:id" [id :as {:keys [db body]}]
    (let [data (try (as-json body :key-fn keyword :eof-error? false)
                    (catch Exception e nil))]
      (resource
        :allowed-methods [:put]
        :available-media-types ["application/json"]
        :malformed? (nil? (:title data))
        :put! (fn [_] (update-task! db (:title data) id))
        :new? false)))

  (DELETE "/task/:id" [id :as {:keys [db]}]
    (resource
      :allowed-methods [:delete]
      :available-media-types ["application/json"]
      :delete! (fn [_] (delete-task! db id)))))

SQLはyesqlを使って外出し。

resource/data/task.sql
-- name: create-task-table!
CREATE TABLE IF NOT EXISTS task (
	id INTEGER PRIMARY KEY,
	title VARCHAR(255))

-- name: list-tasks
SELECT id, title FROM task ORDER BY id DESC

-- name: next-task-id
SELECT MAX(id)+1 AS newid FROM task

-- name: new-task!
INSERT INTO task (id, title) VALUES (:id, :title)

-- name: get-task
SELECT id, title FROM task WHERE id = :id

-- name: update-task!
UPDATE task SET title = :title WHERE id = :id

-- name: delete-task!
DELETE FROM task WHERE id = :id

設計のこころ

重要なものを明示し、一覧できるようにすること。そうすることで、性能問題やセキュリティ問題の発生を防ぐ。テストやセキュリティチェックツールに頼らず1、作る時にバグを排除できるようにする。 (Correctness by Construction)

クライアント側

使用ライブラリ

  • Reagent (react.js)
  • cljs-ajax

設計ポリシ

  • DOM生成部分とハンドラ部分に分けて整理する。
    • react.js (ClojureからはReagent) を使えば、細かいハンドラ部分をかなり省略できる。
  • Webデザイナとの分業を視野に入れ、JavaScriptのDOM生成で全部作ってしまうのは避ける。(サンプルではやってない)
  • レンダリングをなるべく一度で済ませるようにして、性能劣化を防ぐ。
    • Reagentになるべく任せて自分でやらないほうがいい気がする。
  • JQuery UIみたいなUI要素ごとの部品化(これもやってない)

コード例

aは、アプリケーション状態を保存するatom。

サーバへのCRUDとブラウザ内部の状態更新を行う。

ハンドラ部(src-cljs/webexample/core.cljs)
(defn list-tasks [a]
  (GET "task"
       {:handler #(reset! a %)
        :error-handler #(js/alert %)}))

(defn add-task [a]
  (let [elem (dom/get-element "newtask")
        title (.-value elem)
        f (fn [v response] (cons {"id" (get response "id") "title" title} v))]
    (when-not (= title "")
      (set! (.-value elem) "")
      (POST "task"
            {:params {:title title}
             :format :json
             :handler #(swap! a f %)
             :error-handler #(js/alert %)})))
  false)

(defn update-task [a id]
  (let [elem (dom/get-element "newtitle")
        new-title (.-value elem)
        f (fn [v] (map (fn [{xid "id" xtitle "title" :as t}]
                         (if (= id xid) {"id" id "title" new-title} t)) v))]
    (when-not (= new-title "")
      (PUT (str "task/" id)
           {:params {:title new-title}
            :format :json
            :handler #(swap! a f)
            :error-handler #(js/alert %)})))
  false)

(defn delete-task [a id]
  (let [f (fn [v] (filter #(not= id (get % "id")) v))]
    (DELETE (str "task/" id)
            {:format :json
             :handler #(swap! a f)
             :error-handler #(js/alert %)})))
DOM生成部(src-cljs/webexample/core.cljs)
(defn task-app []
  (let [task-list (atom nil)
        editing (atom nil)]
    (list-tasks task-list)
    (fn []
      [:div
       [:input {:type :submit :value "Sync"
                :on-click (fn []
                             (reset! editing nil)
                             (list-tasks task-list))}]
       [:form {:on-submit #(add-task task-list)}
        [:input#newtask {:type :text}]
        [:input {:type :submit}]]
       (->> (for [task @task-list
                  :let [{id "id" title "title"} task]]
              [:li {:on-click #(reset! editing id)}
               id " "
               (if (= id @editing)
                 [:input#newtitle
                  {:type :text :defaultValue title
                   :on-blur
                   (fn []
                     (reset! editing nil)
                     (update-task task-list id))}]
                 [:span title " "
                  [:span {:on-click #(delete-task task-list id)} "[x]"]])])
            (cons :ul)
            vec)])))

(defn main-app []
  [:div
   [:h1 "list tasks"]
   [(task-app)]])

(defn ^:export run []
  (reagent/render-component [main-app] (.-body js/document)))

まだやってないこと

  • 認証
  • 入力のバリデーション
  • 一覧のページング
  • 国際化
  • 見た目のデザイン
  • ネット接続が不安定な環境への対応
  • core.asyncが使えるかもしれない。主にクライアント側で。
  • etc...

気が向いたら更新するかも。

  1. テストいらないということではなく、バグ発見をテストに頼るべきでないという意味。

12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?