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

[前回(その３)](https://qiita.com/niruneru/items/5e2bdb9a94e075b1e457)で作成した画面はちょっと見た目が寂しいので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と同じディレクトリに保存しておきます。

```css: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を指定します。

```html: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318524/37d425b0-5bcb-368b-155b-8530066df45c.png)

変わりませんでした。
firefoxの開発ツールを起動(Ctrl + Shift + I)後、ページをリロードしてみます。
![20200113_elm&Haskell_04-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318524/1e359583-0f23-f898-f5a4-d3e9073f7ce1.png)

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

#Servantでcssを返す
[その１](https://qiita.com/niruneru/items/e85e7f22bc0192892466)で作成したMain.hsをもう一度見てみます。

```haskell: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を返した時と同様に以下のようにすれば返せるのですが、、、。

```haskell: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](http://hackage.haskell.org/package/servant-server-0.16.2/docs/Servant-Server-StaticFiles.html)のserveDirectoryWebAppを使うとCSSやjavascriptといったファイルを取り扱うことができます。
Servant.Server.StaticFilesを利用した`Main.hs`の全量は以下のようになります。

```haskell: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に変えます。

```html:top.html
  <link rel="stylesheet" href="/static/top.css"/>
```
これでもう一度ルートにアクセスすれば、、、
![20200113_elm&Haskell_04-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318524/5206de2f-574d-fcf9-6a15-a3bfb0b2c3a1.png)

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

# elmコードからjavascriptファイルを出力する
これまでは`elm make`でHTMLを生成しましたが、javascriptを生成することもできて、CSSと同様にして読み込ませることができます。詳しくは[こちら](https://guide.elm-lang.jp/interop/)をご参照。

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

```html: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](http://hackage.haskell.org/package/servant-server-0.16.2/docs/Servant-Server-StaticFiles.html)が便利。

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