Elm 0.19で作るTodoアプリ?(0) docker + webpack で環境構築

## はじめに

前回、[elm0.19の開発環境を構築](https://qiita.com/hibohiboo/items/b19519b4a9dbb5ec11b0)したので、
elmを試すために、[Elm 0.18で作るTodoアプリ(1)](https://qiita.com/tomluck/items/872787cda6682834a3a1)を試してみる。
ElmをSPAではなく一部適用にしたいので調整する。

## 開発環境

* Windows 10
* Vagrant 2.1.5
* Virtualbox 5.2.18
* Ubuntu 18.04 LTS (Bionic Beaver)
* Docker version 18.06.0-ce, build 0ffa825
* docker-compose version 1.22.0, build f46880fe

## ファイル構成イメージ

以下のようなファイル構成としたい。

```yaml
- src
  - card.js # elmを読み込むためのjsファイル
  - html
    - card.html # jsファイルを読み込むhtmlファイル
  - Card
    - Main.elm     # 大元のプログラム
    - Handout.elm  # 見た目を設定するためのファイル
```

## ファイル変更点

### 環境設定

<details>
<summary>webpack.config.js全文</summary><div>

```js:webpack.config.js
const path = require("path");
const webpack = require("webpack");
const merge = require("webpack-merge");
const history = require('koa-connect-history-api-fallback');
const CopyWebpackPlugin = require("copy-webpack-plugin");
const HTMLWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

var MODE = process.env.npm_lifecycle_event === "prod" ? "production" : "development";
const filename = MODE == "production" ? "[name]-[hash].js" : "[name].js";
const files = {index:"./src/index.js", card:"./src/card.js"};

var common = {
    mode: MODE,
    entry: files,
    output: {
        path: path.join(__dirname, "dist"),
        filename: filename
    },
    plugins: [
        new HTMLWebpackPlugin({
            template: "src/index.html",
            inject: "body",
            chunks:['index']
        }),
        new HTMLWebpackPlugin({
          filename: "card.html",
          template: "src/html/card.html",
          chunks:['card']
        })
    ],
    resolve: {
        modules: [path.join(__dirname, "src"), "node_modules"],
        extensions: [".js", ".elm", ".scss", ".png"]
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            },
            {
                test: /\.scss$/,
                exclude: [/elm-stuff/, /node_modules/],
                loaders: ["style-loader", "css-loader", "sass-loader"]
            },
            {
                test: /\.css$/,
                exclude: [/elm-stuff/, /node_modules/],
                loaders: ["style-loader", "css-loader"]
            },
            {
                test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                exclude: [/elm-stuff/, /node_modules/],
                loader: "url-loader",
                options: {
                    limit: 10000,
                    mimetype: "application/font-woff"
                }
            },
            {
                test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                exclude: [/elm-stuff/, /node_modules/],
                loader: "file-loader"
            },
            {
                test: /\.(jpe?g|png|gif|svg)$/i,
                loader: "file-loader"
            }
        ]
    },
};

if (MODE === "development") {
    console.log("Building for dev...");
    module.exports = merge(common, {
        plugins: [
            new webpack.NamedModulesPlugin(),
            new webpack.NoEmitOnErrorsPlugin()
        ],
        module: {
            rules: [
                {
                    test: /\.elm$/,
                    exclude: [/elm-stuff/, /node_modules/],
                    use: [
                        { loader: 'elm-hot-webpack-loader' },
                        {
                            loader: "elm-webpack-loader",
                            options: {
                                debug: true,
                                forceWatch: true,
                                // optimize: true // debugと同時には使えない
                            }
                        }
                    ]
                }
            ]
        },
        serve: {
            inline: true,
            stats: "errors-only",
            content: [path.join(__dirname, "src/assets")],
            add: (app, middleware, options) => {
                app.use(history());
            },
            devMiddleware: {
              watch:true, 
              watchOptions:{
                aggregateTimeout: 300,
                poll:1000
              }
            },
            hotClient:{
              host: {
                client: '192.168.50.10',
                server: '0.0.0.0', 
              },
              port:{
                server:3002,
                client: 3002
                },
              allEntries: true
            },
        },
      watch:true,
      watchOptions: {
        ignored: /node_modules/,
        aggregateTimeout: 300,
        poll: 5000
      },
    });
}

if (MODE === "production") {
    console.log("Building for Production...");
    module.exports = merge(common, {
        plugins: [
            new CleanWebpackPlugin(["dist"], {
                root: __dirname,
                exclude: [],
                verbose: true,
                dry: false
            }),
            new CopyWebpackPlugin([
                {
                    from: "src/assets"
                }
            ]),
            new MiniCssExtractPlugin({
                filename: "[name]-[hash].css"
            })
        ],
        module: {
            rules: [
                {
                    test: /\.elm$/,
                    exclude: [/elm-stuff/, /node_modules/],
                    use: [
                        { loader: "elm-webpack-loader", options:{optimize: true} }
                    ]
                },
            {
                test: /\.css$/,
                exclude: [/elm-stuff/, /node_modules/],
                loaders: [MiniCssExtractPlugin.loader, "css-loader"]
            },
            {
                test: /\.scss$/,
                exclude: [/elm-stuff/, /node_modules/],
                loaders: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
            }
            ]
        }
    });
}
```

</div></details>

card.jsのバンドルファイルを作る設定を追加する

```js
var MODE = process.env.npm_lifecycle_event === "prod" ? "production" : "development";
const filename = MODE == "production" ? "[name]-[hash].js" : "[name].js";
const files = {index:"./src/index.js", card:"./src/card.js"};

var common = {
    mode: MODE,
    entry: files,
```

card.htmlをwebpackに読み込ませるための設定。

```js
    plugins: [
        new HTMLWebpackPlugin({
            template: "src/index.html",
            inject: "body",
            chunks:['index'] // card.jsを適用したくないのでindexのみに
        }),
        new HTMLWebpackPlugin({
          filename: "card.html",
          template: "src/html/card.html",
          inject: false // htmlのほうにjsも記述
        })
    ],
```

hot-loadをindex.html以外のcard.htmlにも適用する。

```
            hotClient:{
              allEntries: true
            },
```

### HTMLファイル
マテリアルデザインを適用してみる。 → 失敗。elmと相性が悪い？

```html:card.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css" />
    <!--materialize.jsは後から読むと一瞬デザインがちらつくのでhead内で読み込む-->
    <script src='https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js'></script>
    <title>ハンドアウト</title>
  </head>
  <h1>ハンドアウト</h1>
  <body>
    <div class="print">
        <div id="cards"></div>
    </div>
    
  </body>
</html>
```

### jsファイル

id="cards"の要素にelm要素をひもづける。

```js:card.js
'use strict';

require("./styles.scss");

const {Elm} = require('./Card/Main');
const mountNode = document.getElementById('cards');
const app = Elm.Main.init({flags: 6, node: mountNode});
```

### ライブラリ

```json:package.json
{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "elm-test",
    "start": "npm run dev",
    "dev": "webpack-serve --port 3000",
    "build": "webpack",
    "prod": "webpack -p"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.0.1",
    "@babel/preset-env": "^7.0.0",
    "babel-loader": "^8.0.2",
    "clean-webpack-plugin": "^0.1.19",
    "copy-webpack-plugin": "^4.5.2",
    "css-loader": "^1.0.0",
    "elm": "^0.19.0-bugfix2",
    "elm-format": "^0.8.0",
    "elm-hot-webpack-loader": "^1.0.1",
    "elm-verify-examples": "^3.0.0",
    "elm-webpack-loader": "^5.0.0",
    "file-loader": "^2.0.0",
    "html-webpack-plugin": "^3.2.0",
    "koa-connect-history-api-fallback": "^0.3.1",
    "mini-css-extract-plugin": "^0.4.2",
    "node-sass": "^4.9.3",
    "resolve-url-loader": "^2.3.1",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.0",
    "url-loader": "^1.1.1",
    "webpack": "^4.18.0",
    "webpack-cli": "^3.1.0",
    "webpack-merge": "^4.1.4",
    "webpack-serve": "^2.0.2"
  },
  "dependencies": {
    "purecss": "^1.0.0"
  }
}
```

## 実行

http://192.168.50.10:3000/card.html (仮想環境のIP:今回設定したポート)にアクセスし、hot loadができていることを確認する。