Help us understand the problem. What is going on with this article?

elm & servantでwebアプリ - その4.CSS・javascriptの読み込み

前回(その3)で作成した画面はちょっと見た目が寂しいのでCSSを作って適用してみます。

VIEWにHTMLの属性(class)を書いていく

まずMain.elmのVIEWにCSSで定義するclassを指定していきます。

Main.elm
view : Model -> Html Msg
view model =
    div
        [ class "container" ] -- クラス指定を追加
        [ div
            [ class "input_area" ] -- クラス指定を追加
            [ label
                [ class "label_zip" ] -- クラス指定を追加
                [ text "郵便番号" ]
            , input
                [ class "input_box_zip1" -- クラス指定を追加
                , type_ "text"
                , onInput InputZip1
                , value model.zip1
                , pattern "^[0-9]+$" -- ついでに入力可能な値に数値を指定
                , maxlength 3        -- 入力可能な最大長も指定
                ]
                []
            , span
                [ class "zip_sep" ] -- クラス指定を追加
                [ text "-" ]
            , input
                [ class "input_box_zip2"
                , type_ "text"
                , onInput InputZip2
                , value model.zip2
                , pattern "^[0-9]+$"
                , maxlength 4
(以下省略)

CSSを作って、top.htmlと同じディレクトリに保存しておきます。

top.css
.container
    { margin-top   : 20px
    ; margin-left  : 20px
    ; padding-top  : 20px
    ; padding-left : 20px
    }

.input_area
    { width             : 35%
    ; padding-top       : 10px
    ; padding-bottom    : 10px
    ; margin-bottom     : 10px
    ; background        : #f3fafb
    }

(以下省略)

これを取り込むためにHTMLの<link>タグに作成したcssを指定します。

top.html
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <title>Main</title>
  <style>body { padding: 0; margin: 0; }</style>
  /* ↓ここにCSSの指定を追加 */
  <link rel="stylesheet" href="top.css">
</head>

<body>
(以下省略)

表示してみます。まずはservantサーバを起動して

[user@remote ~/webapp/backend]$ stack exec backend-exe

ブラウザからアクセスしてみると、、、
20200113_elm&Haskell_04-01.png

変わりませんでした。
firefoxの開発ツールを起動(Ctrl + Shift + I)後、ページをリロードしてみます。
20200113_elm&Haskell_04-01.png

top.cssを取得しようとして404になっています。
top.htmlの<link>タグに記載されたCSS(top.css)をサーバに要求したものの、
サーバ側ではこれに対する実装を行っていないためこうなったのだと推測できます。
というわけで、top.cssをきちんと返すようにサーバ側(Main.hs)を修正します。

Servantでcssを返す

その1で作成したMain.hsをもう一度見てみます。

Main.hs
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE MultiParamTypeClasses #-}

import Servant
import Network.Wai.Handler.Warp
import Network.HTTP.Media ((//), (/:))
import qualified Data.ByteString.Lazy as BS

data HTML

instance Accept HTML where
    contentType _ = "text" // "html" /: ("charset", "utf-8")

instance MimeRender HTML BS.ByteString where
    mimeRender _ bs = bs

type API = Get '[HTML] BS.ByteString

api :: Proxy API
api = Proxy

server :: BS.ByteString -> Server API
server top = return top

main :: IO ()
main = do
    top <- BS.readFile "/home/user/webapp/frontend/top.html"
    run 8080 $ serve api (server top)

CSSを返すには/top.cssというパスへのアクセスがあった際、CSSを読み込んで返せばOKなので、
HTMLを返した時と同様に以下のようにすれば返せるのですが、、、。

Main.hs(抜粋)
import Control.Monad.IO.Class (liftIO)

-- CSS用に追加
data CSS

instance Accept CSS where
    contentType _ = "text" // "css" /: ("charset", "utf-8")

instance MimeRender CSS BS.ByteString where
    mimeRender _ bs = bs

-- APIに/top.cssへのルーティング定義を追加
type API
    =    Get '[HTML] BS.ByteString
    :<|> "top.css" :> Get '[CSS] BS.ByteString -- 追加

-- APIに応じた処理を追加
server :: BS.ByteString -> BS.ByteString -> Server API -- css分を引数に追加
server top css
    =    (liftIO $ return top) -- 修正
    :<|> (liftIO $ return css) -- 追加

main :: IO ()
main = do
    top <- BS.readFile "/home/user/webapp/frontend/top.html"
    css <- BS.readFile "/home/user/webapp/frontend/top.css" -- 追加
    run 8080 $ serve api (server top css) -- serverにcssも渡すよう変更

でも、用意するCSSやjavascript全部に対してこれを書くのはしんどいので何とかしたいです。

Servant.Server.StaticFiles

Servant.Server.StaticFilesのserveDirectoryWebAppを使うとCSSやjavascriptといったファイルを取り扱うことができます。
Servant.Server.StaticFilesを利用したMain.hsの全量は以下のようになります。

Main.hs
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE MultiParamTypeClasses #-}

import Servant
import Network.Wai.Handler.Warp
import Network.HTTP.Media ((//), (/:))
import qualified Data.ByteString.Lazy as BS
import Control.Monad.IO.Class (liftIO)

data HTML

instance Accept HTML where
    contentType _ = "text" // "html" /: ("charset", "utf-8")

instance MimeRender HTML BS.ByteString where
    mimeRender _ bs = bs

api :: Proxy API
api = Proxy

type API
    =    Get '[HTML] BS.ByteString
    :<|> "static" :> Raw

server :: BS.ByteString -> Server API
server top
    =    (liftIO $ return top)
    :<|> serveDirectoryWebApp "/home/user/webapp/frontend"

main :: IO ()
main = do
    top <- BS.readFile "/home/user/webapp/frontend/top.html"
    run 8080 $ serve api (server top)

"static" :> Rawとすることで、/staticで始まるパスへアクセスがあった際に、これをserveDirectoryWebApp に渡している文字列に変換してくれます。
したがって、上記の例だと/static/top.cssでアクセスされた際に/home/user/webapp/frontend/top.cssを返してくれます。

ということでtop.htmlのほうに記載しているCSSのパスを/static/top.cssに変えます。

top.html
  <link rel="stylesheet" href="/static/top.css"/>

これでもう一度ルートにアクセスすれば、、、
20200113_elm&Haskell_04-03.png

CSSが無事に取得されて適用されました。

elmコードからjavascriptファイルを出力する

これまではelm makeでHTMLを生成しましたが、javascriptを生成することもできて、CSSと同様にして読み込ませることができます。詳しくはこちらをご参照。

elm make src/Main.elm --output=top.js

これでjavascriptのファイルができあがるので、top.htmlに書かれていたjavascriptのコードを削除して、代わりに出力されたjavascriptファイルを読み込ませるようにしてあげます。

top.html
<head>
    <link rel="stylesheet" href="/static/top.css"/>
    <script src="/static/top.js"></script>
</head>
<body>
    <div id="elm"></div>

    <script>
        var app = Elm.Main.init({
            node: document.getElementById('elm')
        });
    </script>
</body>

サーバを再起動してトップページへ再度アクセスすれば、
きちんと表示され、郵便番号入力からの住所表示も確認できます。

まとめ

  • elm make 入力ファイル --output=出力ファイル でelmコードからjavascriptファイルを生成することができる。
  • CSSやjavascriptなどの静的コンテンツをServantで返す際にはServant.Server.StaticFilesが便利。

次回はフロント側に持たせてしまっていた住所データをDBに移して、郵便番号をキーにしてDBから取得する。ということをやってみようと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした