LoginSignup
11
4

More than 3 years have passed since last update.

elm reactorとwebpackを使って快適なElm開発環境を構築する

Last updated at Posted at 2019-12-13

この記事はElm Advent Calendar 2019 の14日目の記事です。
Elm入門時に快適な環境構築するための手順書です。

使用したソースはGitHubにアップしています。Scalaのコードが混ざっていますが、あまり気にしないでください(理由は一番最後に)。Elmのコードの部分はここで確認できます。

今回のゴール

快適なElm開発環境を構築する。

Elm Reactorを使って開発する

Elmには、Elm Reactorという超強力なツールがあります。これがあるだけで快適なElm開発環境を手に入れられます。

Elm Reactorとは?

Elm ReactorはElm専用の組み込みサーバーで、コンパイラ機能とサーバー機能が一緒になった超便利なツールです。こいつはElmをインストールするだけで標準で付いてくる。いつもお世話になっています。

以下のコマンドを実行するだけで画面が立ち上がってきます。

 > elm reactor
Go to <http://localhost:8000> to see your project dashboard.

Elm Reactorのちょっと微妙なこと

Elm Reactorでは、外部ファイル化されたCSSファイルを使うことが少し難しいです。Elmのソース内で外部のCSSファイルを読み込んだりはできません(多分)。

CSS in Elm?

CSS in Elmという手法があるようですが、イチからCSSを手組みする時ならまだしも、既存のCSSファイルやCSSフレームワークを使用する場合は嬉しさよりも面倒さの方が勝つ気がします。試してはいません。

CSSフレームワークを使うには?

各種CSSフレームワークを使用する場合はElm Bootstrapelm-bulmaのようなパッケージもありますが、きっと純粋にCSSフレームワークの用意してくれたクラスだけを使ってレイアウトするってこともないはず。これらを使用した際にElm Reactorで正しくスタイルされて描画されるのかもやっぱり試していません:confused:

Elm Reactorを使う時は妥協する!?

まぁ、CSSが読み込めなくってもロジックの妥当性は作り込むことはできるはずなので、ゴリゴリ実装している間はスタイルを無視する前提でElm Reactorを使いながら開発するという手もあるのですが、まぁやっぱり画面が寂しいのは気になるわけです。。。

解決方法

webpackを使います。
Elm ReactorはElm専用のツールというわけではなく、HTML+jsなページも普通に動かすことができます。

つまり、、、

1. Elmでコードを書いて、
2. それをwebpackにトランスパイルさせて、
3. トランスパイルされたjsを使用するHTMLをElm Reactorで開く。

これによって、上記3.の際に合わせてCSSファイルを読み込むことができるので、Elm Reactorを使いながらキレイに整ったレイアウトのページでの動作を確認することができるようになります。

今回使用したツール等のバージョン

Elm 0.19.0
webpack 4.33.0

その他はGitHubpackage.jsonを見てください。CSSにはBulmaを使用します。

やってみる

まずはwebpackなしで画面を作成してみる

Elmのコード
src/elm/Example/StyleExample.elm
module Example.StyleExample exposing (Model, Msg(..), init, main, update, view)

import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)


main : Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , view = view
        , update = update
        }


type alias Model =
    { firstName : String
    , familyName : String
    , fullName : String
    }


init : Model
init =
    { firstName = ""
    , familyName = ""
    , fullName = ""
    }


type Msg
    = InputFirstName String
    | InputFamilyName String


update : Msg -> Model -> Model
update msg model =
    case msg of
        InputFirstName firstName ->
            { model | firstName = firstName }

        InputFamilyName familyName ->
            { model | familyName = familyName }


makeFullName : Model -> String
makeFullName model =
    model.familyName ++ " " ++ model.firstName


view : Model -> Html Msg
view model =
    div
        []
        [ section
            [ class "section" ]
            [ div
                [ class "container" ]
                [ div
                    [ class "column is-one-third" ]
                    [ div
                        [ class "field" ]
                        [ div
                            [ class "control" ]
                            [ input
                                [ type_ "text", class "input is-success is-small", placeholder "familyName", value model.familyName, onInput InputFamilyName ]
                                []
                            ]
                        ]
                    , div
                        [ class "field" ]
                        [ div
                            [ class "control" ]
                            [ input
                                [ type_ "text", class "input is-rounded is-large", placeholder "firstName", value model.firstName, onInput InputFirstName ]
                                []
                            ]
                        ]
                    , div
                        [ class "field" ]
                        [ div
                            [ class "control" ]
                            [ p
                                [ class "label has-text-info" ]
                                [ text (makeFullName model) ]
                            ]
                        ]
                    ]
                ]
            ]
        ]

さっそくElm Reactorで確認してみましょう。

範囲を選択_012.png

↓↓↓

範囲を選択_015.png

だっせえええええええええええええ!!!!!1

今回は、姓名を入力するとフルネームをラベルで表示するようにしています。

範囲を選択_014.png

Elmのロジックは正しく動作するので業務ロジックの妥当性は確認できますが、画面が寂しすぎてやる気が一気にゼロになりますね。

webpackを使用する

ショボショボな画面を見てしまって既にやる気はゼロになっているわけですが、ここからやる気を復活させるための設定をしていきましょう。
まずはwebpack。webpackとかよくわからないのでごちゃごちゃ書くとわけわからなくなるので、webpack.config.jsは最小限の設定だけにしています。必要に応じて良い感じに加工してください。(誤りがあったら指摘をお願いします)

ポイントは以下の部分です。

webpack.config.js
// Elmのコードパス
const elmBasePath = path.resolve(__dirname, 'src/elm');
// Elmを呼び出すjs(後述)のコードパス
const jsBasePath = path.resolve(__dirname, 'src/');
// Elmのコードのサブディレクトリ
// 対象のサブディレクトリが増えたら追記してやらないといけない
const elmCompileFolders = ['Example'];

// 対象ファイルをセットするオブジェクト
const entries = {};

// Elmのソースファイルをセット
const elmTargets = glob.sync(`${elmBasePath}/+(${elmCompileFolders.join('|')})/*.elm`);
elmTargets.forEach(value => {
  const re = new RegExp(`${elmBasePath}/`);
  const key = value.replace(re, '');

  entries[key] = value;
});

// jsのソースファイルをセット
// 本当はelmとディレクトリ構成を合わせて/src/js/Example/*.jsにしたいんだけど、
// なぜかwebpackがエラーになるのでjsは雑にsrc直下に置いています・・・
const jsTargets = glob.sync(`${jsBasePath}/*.js`);
jsTargets.forEach(value => {
  const re = new RegExp(`${jsBasePath}/`);
  const key = value.replace(re, '');

  entries[key] = value;
});

// 〜 途中省略 〜

var common = {

  // 上記で指定したファイルを対象とする
  entry: entries,

  // 出力の設定
  output: {
    // 出力するファイル名
    filename: '[name]',
    // 出力先のパス(※1)
    path: path.join(__dirname, 'public/js')
  },

  // 〜 途中省略 〜
};

また、webpackにファイルの変更を監視させることで、コードの編集時に自動的に再トランスパイルが発生するようにしています。これで、『コードをエディタでコードを編集したら、いったんターミナルでmakeして・・・』みたいな手間が発生しないようにします。

webpack/config.js

module.exports = merge(common, {

    // 〜 途中省略 〜

    watch:true,
    watchOptions: {
      ignored: /node_modules/,
      aggregateTimeout: 300,
      // 5秒毎にポーリング
      poll: 5000
    },
  });

webpackでトランスパイルされたjsを使用する

HTMLを用意する

まずはElmで書いたコードを埋め込むHTMLを用意します。

public/html/StyleExample.html
<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Elm Advent Calendar 2019</title>
    <link rel="stylesheet" href="/public/css/bulma-0.8.0/css/bulma.min.css">
  </head>

  <body>
    <div id="elm"></div>
    <script type="text/javascript" src="/public/js/StyleExample.js"></script>
  </body>

</html>

id="elm"を指定したdivにElmのviewで作成した内容を出力します。

Elmを呼び出すためのjsを用意する

HTMLではpublic/js/StyleExample.jsを呼び出していますが、このファイルは直接作成しません。public/jswebpack.config.jsの(※1)で出力先に指定した場所であり、StyleExample.jsはwebpackに出力させます。それでは、webpackに食わせるためのjsファイルを作成します。

src/StyleExample.js
'use strict';

const {Elm} = require('./elm/Example/StyleExample.elm');
const mountNode = document.getElementById('elm');
const app = Elm.Example.StyleExample.init({node: mountNode});

これでHTML <--> js <--> Elmが繋がりました。

動かしてみる

Elm Reactorで開発する

ここまで準備ができたらあとはElm Reactorで動かしてやるだけです。

 > elm reactor & webpack                                                                                                                                                                                                       (git)-[master]
[1] 22916
Go to <http://localhost:8000> to see your project dashboard.
Building for dev...

webpack is watching the files…

Hash: 94efc514a17db9519d85
Version: webpack 4.41.2
Time: 1629ms
Built at: 2019-12-04 01:50:26
                   Asset     Size                    Chunks             Chunk Names
Example/StyleExample.elm  276 KiB  Example/StyleExample.elm  [emitted]  Example/StyleExample.elm
         StyleExample.js  277 KiB           StyleExample.js  [emitted]  StyleExample.js
Entrypoint Example/StyleExample.elm = Example/StyleExample.elm
Entrypoint StyleExample.js = StyleExample.js
[./src/StyleExample.js] 192 bytes {StyleExample.js} [built]
[./src/elm/Example/StyleExample.elm] 261 KiB {Example/StyleExample.elm} {StyleExample.js} [built]

コードの変更は監視させているので、elm reactor & webpackは一度実行したらほったらかしです。

ブラウザで確認

今度は前回とは違って、Elmのファイルを指定して開かずに先ほど用意した/public/html/StyleExample.htmlを開いてみます。

範囲を選択_016.png

↓↓↓

範囲を選択_017.png

Elm Reactorで動かしているのにスタイルも反映されてますね!
実際に動かしてみます。

範囲を選択_018.png

完璧:laughing:

スタイルを追加してみる

これにスタイルを追加してみます。

public/css/custom/custom.css
html {
  height: 100%;
}

body {
  height: 100%;
  background-color: #E6E6FA;
}

.custom {
  font-size: 15em;
}

htmlのheadにカスタムのcssファイルを追加します。

StyleExample.html
<link rel="stylesheet" href="/public/css/custom/custom.css">

pclassにもスタイルの指定を追加してやります。

src/elm/Example/StyleExample.elm
[ p
  [ class "label has-text-info custom" ]
  [ text (makeFullName model) ]
]

Elmの修正を行いましたが、webpackが自動的に変更を監視して自動的にトランスパイルしてくれているので、ソースを修正したら即画面で確認できます。
それでは、まずは直接/src/elm/Example/StyleExample.elmを直接開いてみます。

範囲を選択_022.png

↓↓↓

ワークスペース 1_025.png

ほげえええええええええええwww!!!!!1
スタイルが反映されてなくて、やる気もなくなりますね。

お次は/public/html/StyleExample.htmlを開いてみる。

範囲を選択_023.png

↓↓↓

ワークスペース 1_024.png

実際にWebアプリとしても動かしてみて確認してみます。(UTLが微妙に違うのがわかると思います。GitHubにScalaのコードがアップされているのはこの確認のためです)

Screenshot from 2019-12-14 02-10-52.png

Webアプリとして動かした時とElm Reactorで表示した時で同じようにスタイルされています!見た目もキレイで、失ったやる気も俄然復活してきたのではないでしょうか:relaxed:
これでストレスの無い快適なElm開発環境を構築することができました!

免責事項

罠に陥ることもあるようです。


11
4
2

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
11
4