Riot.js + RiotControl で Flux を実装してみる - 開発環境構築編


## はじめに

以下の記事の **開発環境構築編** です。

[Riot.js と RiotControl で Flux を実装してみる - Qiita](http://qiita.com/kotarella1110/items/f9958195c929da6508b6 "Riot.js と RiotControl で Flux を実装してみる - Qiita")

Webpack2 を利用して Riot.js + RiotControl の環境を構築していきます。
以下の方であれば難なく理解できる内容となっています。:ok_woman:

* Webpack をある程度理解している方
* [Migrating from v1 to v2](https://webpack.js.org/guides/migrating/ "Migrating from v1 to v2") を読み Webpack v1 と v2 との違いを理解している方

いい感じに開発環境を構築した:sunglasses: ( と思っている:confounded: ) ので参考にしてもらえると助かります！

実際に作成したプロジェクト ( 開発環境 ) は以下にリポジトリを置いています。

[kotarella1110/riot-starter at v1.0.0](https://github.com/kotarella1110/riot-starter/tree/v1.0.0 "kotarella1110/riot-starter at v1.0.0")

## 開発環境構築

今回構築する開発環境は以下がベースとなっています。

* [Riot.js](http://riotjs.com/ "Riot.js — Simple and elegant component-based UI library · Riot.js")
* [RiotControl](https://github.com/jimsparkman/RiotControl/ "jimsparkman/RiotControl: Event Controller / Dispatcher For RiotJS, Inspired By Flux")
* [Babel](https://babeljs.io/ "Babel · The compiler for writing next generation JavaScript")
* [Sass](http://sass-lang.com/ "Sass: Syntactically Awesome Style Sheets")
* [Webpack2](https://webpack.js.org/ "webpack")

### 準備

プロジェクトを作成します。

```bash
$ mkdir riot-starter
$ cd riot-starter
```

Node.js のバージョン管理ツールとして [nvm](https://github.com/creationix/nvm "creationix/nvm: Node Version Manager - Simple bash script to manage multiple active node.js versions") をインストールしてください。

まずは、Node.js のバージョンを固定するために、バージョン番号を含む _.nvmrc_ ファイルをプロジェクトに作成します。

※ バージョン番号は現在 ( 2016/04 ) LTS の v6.10.2 を指定しています

```file:.nvmrc
6.10.2
```

以下のコマンドにより、_.nvmrc_ ファイルに記載されたバージョンの Node.js をインストールして使用する事ができます。

```bash
$ nvm install
$ nvm use
```

パッケージマネージャとして `yarn` をインストールしてください。`npm install -g yarn` でインストールする事ができます。

`yarn` を利用して _package.json_ ファイルを作成します。

```bash
$ yarn init -y
```

### Riot.js

```bash:dependencies
$ yarn add riot riot-route riotcontrol
```

> * `riot`: シンプルで洗練されたコンポーネントベースの UI ライブラリ
> * `riot-route`: シンプルなクライアント側のルータ
> * `riotcontrol`: Flux のインスピレーションを受けた Riot.js のためのシンプルな Central Event Controller / Dispatcher

```bash:devDependencies
$ yarn add --dev riot-tag-loader riot-hot-reload
```

> * `riot-tag-loader`: Riot.js のタグをトランスパイルする Webpack ローダー
> * `riot-hot-reload`: Riot.js を拡張してタグをホットリロード可能にする Api

### Babel

Promise など、最新の ES の仕様を使いたい場合には、`babel-polyfill` をクライアントコードに含める必要があります。

この polyfill を使うと、バンドルのサイズが増えるため、これらの機能を使いたい場合のみ追加するようにしてください。

```bash:dependencies
$ yarn add --dev babel-polyfill
```

> * `babel-polyfill`: Babel の polyfill

```bash:devDependencies
$ yarn add --dev babel-core babel-loader babel-preset-latest babel-preset-es2015-riot
```

> * `babel-core`: Babel のコアパッケージ
> * `babel-loader`: JavasScript をトランスパイルする Webpack ローダー
> * `babel-preset-latest`: JavaScript のプリセット
> * `babel-preset-es2015-riot`: Riot.js のプリセット

`.babelrc` ファイルを作成します。
`babel-preset-es2015-riot` を設定しています。

```js:.babelrc
{
  presets: [ 'es2015-riot' ]
}
```

### Sass

変換には3つの別々のローダーとnode-sassライブラリーが関わっています。

```bash:devDependencies
$ yarn add --dev style-loader css-loader sass-loader node-sass
```

> * `style-loader`: ドキュメントの `<style>` タグ内にCSSを出力する Webpack ローダー
> * `css-loader`: CSS を解析して JavaScript で扱えるようにして、すべての依存オブジェクトを解決する Webpack ローダー
> * `sass-loader`: SassファイルをCSSへ変換する Webpack ローダー
> * `node-sass`: Sass のスタイルシートプリプロセッサの C 言語バージョン LibSass を Node.js で使用できるようにしたライブラリモジュール

### 画像

```bash:devDependencies
$ yarn add --dev file-loader url-loader
```

> * `file-loader` `url-loader`: 画像等を取り扱う Webpack ローダー

### Webpack2

```bash:devDependencies
$ yarn add --dev webpack webpack-dev-server webpack-merge
```

> * `webpack`: JavaScript ファイルをブラウザで使用するためのバンドルや、あらゆるリソースやアセットを変換、バンドル、パッケージ化することができるモジュールバンドラ
> * `webpack-dev-server`: Webpack を使用したライブリロードを提供する開発サーバー
> * `webpack-merge`: Webpack の config 等をマージする関数

```bash:devDependencies
$ yarn add --dev html-webpack-plugin clean-webpack-plugin extract-text-webpack-plugin
```

> * `html-webpack-plugin`: バンドルに対応する HTML ファイルの作成を簡単にする Webpack プラグイン
> * `clean-webpack-plugin`: ビルドフォルダーをビルド実行する前に削除する Webpack プラグイン
> * `extract-text-webpack-plugin`: モジュールをバンドルから抽出してファイルに出力する ( Sass からコンパイルされた CSS を JavaScript バンドルファイルから抜き出し、CSSバンドルとして抽出する ) Webpack プラグイン

#### ディレクトリ作成

```bash
$ mkdir webpack src dist
```

> * _webpack_: Webpack の config 管理用のディレクトリ
> * _src_: コンパイル入力元のディレクトリ
> * _dist_: コンパイル出力先のディレクトリ

#### config ファイル作成

異なる環境で複数の設定を念頭に置いているため、環境ごとに別々の js ファイルを作成しています。

まずは、Webpack で扱うパスを保持したオブジェクトを返す paths ファイルを作成します。

```js:webpack/path.js
import path from 'path';

export default {
  srcHtml: path.join(__dirname, '../src/index.html'),
  srcJs: path.join(__dirname, '../src/index.js'),
  distDir: path.join(__dirname, '../dist')
}
```

開発環境と本番環境に適用する共通の config ファイルを作成します。

```js:webpack/common.config.js
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import CleanWebpackPlugin from 'clean-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import paths from './paths.js';

export default (env) => {
  return {
    entry: {
      polyfills: [
        'babel-polyfill'
      ],
      main: [
        paths.srcJs
      ]
    },
    output: {
      path: paths.distDir,
      filename: '[name].bundle.js',
      // publicPath: publicPath,
      sourceMapFilename: '[name].map'
    },
    module: {
      rules: [
        { // js
          test: /\.(js|tag)$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'post',
          loader: 'babel-loader',
          options: {
            babelrc: false,
            presets: [
              [ 'latest', { modules: false } ]
            ]
          }
        },
        { // css
          test: /\.css$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'post',
          use: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: 'css-loader'
          })
        },
        { // sass
          test: /\.(scss|sass)$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'post',
          use: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: [
              'css-loader',
              'sass-loader'
            ]
          })
        },
        { // img
          test: /\.(jpg|png|gif)$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'post',
          use: 'file-loader'
        },
        { // font
          test: /\.(woff|woff2|eot|ttf|svg)$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'post',
          use: {
            loader: 'url-loader',
            options: {
              limit: 100000
            }
          }
        }
      ]
    },
    resolve: {
      extensions: ['.js'],
    },
    plugins: [
      new webpack.ProvidePlugin({
        riot: 'riot'
      }),
      new HtmlWebpackPlugin({
        template: paths.srcHtml,
        inject: true
      }),
      new CleanWebpackPlugin(['*'], {
        root: paths.distDir,
        exclude: '.gitkeep'
      }),
      new ExtractTextPlugin('main.css')
    ]
  };
}
```

`babel-preset-latest` に ES モジュールの書き換えを無効化する事で [Tree Shaking](https://webpack.js.org/guides/tree-shaking/ "Tree Shaking") 機能を有効にして、エクスポートしたのに使われていない部分をバンドルファイルから取り除いてサイズを抑えます。

```js
presets: [
  [ 'latest', { modules: false } ]
]
```

開発環境のみに適用する config ファイルを作成します。

```js:webpack/dev.config.js
import webpack from 'webpack';
import env from '../env.js';
import paths from './paths.js';

export default () => {
  return {
    entry: {
      vendor: [
        'riot-hot-reload'
      ]
    },
    module: {
      rules: [
        { // riot
          test: /\.(tag)$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'pre',
          loader: 'riot-tag-loader',
          options: {
            type: 'es6', // transpile the riot tags using babel
            hot: true,
            debug: true
          }
        }
      ]
    },
    plugins: [
      new webpack.DefinePlugin({
        'process.env': {
          'NODE_ENV': JSON.stringify('development'),
        }
      }),
      new webpack.HotModuleReplacementPlugin()
    ],
    devtool: 'inline-source-map',
    devServer: {
      contentBase: paths.distDir,
      host: env.devServer.host || 'localhost',
      port: env.devServer.port || 8080,
      open: true,
      hot: true,
      inline: true,
    }
  };
}
```

本番環境のみに適用する config ファイルを作成します。

```js:webpack/prod.config.js
import webpack from 'webpack';

export default () => {
  return {
    module: {
      rules: [
        { // riot
          test: /\.(tag)$/,
          include: /src/,
          exclude: /node_modules/,
          enforce: 'pre',
          loader: 'riot-tag-loader',
          options: {
            type: 'es6', // transpile the riot tags using babel
          }
        }
      ]
    },
    plugins: [
      new webpack.DefinePlugin({
        'process.env': {
          'NODE_ENV': JSON.stringify('production'),
        }
      }),
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: true,
          drop_debugger: true
        },
        output: {
          comments: false
        }
      })
    ]
  };
}
```

_webpack.config.babel.js_ ファイルを作成します。
`webpack-merge` を利用して、共通の config ファイルと環境に応じた config ファイルをマージして返しています。

`webpack --env=production --progress --profile --colors` と実行することで、 `env.production` が `true` になります。

```js:webpack.config.babel.js
import webpackMerge from 'webpack-merge';
import commonConfig from './webpack/common.config.js';
import devConfig from './webpack/dev.config.js';
import prodConfig from './webpack/prod.config.js';

export default (env = {}) => {
  let isProd = !!env.production;
  if(isProd) {
    return webpackMerge(commonConfig(env), prodConfig());
  }
  return webpackMerge(commonConfig(env), devConfig());
}
```

_env.example.js_ ファイルを作成します。

```js:env.example.js
export default {
  devServer: {
    host: 'localhost',
    port: 8080
  }
};
```

`yarn install` の後に _env.example.js_ をコピーして _env.js_ を作成するようにしています。

_env.js_ ファイルで定義されたホストとポートは _webpack/dev.config.js_ ファイルで使われます。

```js:webpack/dev.config.js
...
import env from '../env.js';
...
      host: env.devServer.host || 'localhost',
      port: env.devServer.port || 8080,
...
```

_env.js_ ファイルのホストとポートを変更に応じて、`webpack-dev-server` のホストとポートも変更でるようにしています。

### npm-script

例えば、筆者の場合 `webpack-dev-server` を起動する場合以下のようなコマンドを実行します。

`webpack-dev-server --progress --colors`

とても長くて覚えることができません。

以下のように、npm-script を使うことで `yarn start` という短いコマンドで `webpack-dev-server` を起動することができます。

```diff:package.json
+  "scripts": {
+    "postinstall": "cp env.example.js env.js",
+    "start": "webpack-dev-server --progress --colors",
+    "build": "webpack -d --progress --colors",
+    "build:prod": "webpack --env.production --progress --colors",
+    "watch": "webpack --watch --progress --colors",
+    "watch:prod": "webpack --watch --env.production --progress --colors"
+  },
```

`postinstall` は `npm install` ( `yarn install` ) の後に実行されます。

### git

空ディレクトリも管理できるように _.gitkeep_ ファイルを作成します。

```
touch webpack/.gitkeep src/.gitkeep dist/.gitkeep
```

_.gitignore_ を作成します。

_dist_ 以下と _env.js_ を git 管理対象外としています。

```file:.gitignore
# Created by https://www.gitignore.io/api/osx,node,linux,windows

### Linux ###
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env


### OSX ###
*.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk

# End of https://www.gitignore.io/api/osx,node,linux,windows

/dist/*
/env.js
```

### 完成した開発環境の確認

プロジェクトのディレクトリ構造を確認してください。

```tree
.
├── dist
│   └── .gitkeep
├── src
│   └── .gitkeep
├── node_modules/
├── webpack
│   ├── .gitkeep
│   ├── common.config.js
│   ├── dev.config.js
│   ├── paths.js
│   └── prod.config.js
├── .babelrc
├── .gitignore
├── .nvmrc
├── env.example.js
├── package.json
├── webpack.config.babel.js
└── yarn.lock
```

_package.json_ を確認にしてください。

```json:package.json
{
  "name": "riot-starter",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "postinstall": "cp env.example.js env.js",
    "start": "webpack-dev-server --progress --colors",
    "build": "webpack -d --progress --colors",
    "build:prod": "webpack --env.production --progress --colors",
    "watch": "webpack --watch --progress --colors",
    "watch:prod": "webpack --watch --env.production --progress --colors"
  },
  "dependencies": {
    "riot": "^3.4.3",
    "riot-route": "^3.1.1",
    "riotcontrol": "^0.0.3"
  },
  "devDependencies": {
    "babel-core": "^6.24.1",
    "babel-loader": "^7.0.0",
    "babel-polyfill": "^6.23.0",
    "babel-preset-es2015-riot": "^1.1.0",
    "babel-preset-latest": "^6.24.1",
    "clean-webpack-plugin": "^0.1.16",
    "css-loader": "^0.28.0",
    "extract-text-webpack-plugin": "^2.1.0",
    "file-loader": "^0.11.1",
    "html-webpack-plugin": "^2.28.0",
    "node-sass": "^4.5.2",
    "riot-hot-reload": "^0.0.2",
    "riot-tag-loader": "^1.0.0",
    "sass-loader": "^6.0.3",
    "style-loader": "^0.16.1",
    "url-loader": "^0.5.8",
    "webpack": "^2.4.1",
    "webpack-dev-server": "^2.4.4",
    "webpack-merge": "^4.1.0"
  }
}
```

やっと開発環境構築が完了です！:sparkles:

## サンプルアプリを動かしてみる

開発環境整ったので、試しに Riot.js のアプリケーションを動かしてみましょう。

[examples/todo-app at gh-pages · riot/examples](https://github.com/riot/examples/tree/gh-pages/todo-app "examples/todo-app at gh-pages · riot/examples") を ES6 と Sass で書き直した [riot-starter/src at v1.0.0 · kotarella1110/riot-starter](https://github.com/kotarella1110/riot-starter/tree/v1.0.0/src "riot-starter/src at v1.0.0 · kotarella1110/riot-starter") を _src_ ディレクトリ以下に格納してください。

 `yarn start` と実行することでブラウザが立ち上がります。:v:

## 最後に

筆者が作成した[開発環境](https://github.com/kotarella1110/riot-starter/tree/v1.0.0 "kotarella1110/riot-starter at v1.0.0")に誤りがある場合はプルリクエストやコメント等でご指摘お願いします。:thumbsup:

どうでもいい話ですが、今回 `yarn` に習って初めて絵文字使ってみました！！:laughing:

## 参考文献

* [webpack](https://webpack.js.org "webpack")
* [Building for Production](https://webpack.js.org/guides/production-build/ "Building for Production")
* [サンプルで学ぶwebpack 2入門：Web開発を超効率化する必須ツールの使い方 - WPJ](https://www.webprofessional.jp/beginners-guide-to-webpack-2-and-module-bundling/ "サンプルで学ぶwebpack 2入門：Web開発を超効率化する必須ツールの使い方 - WPJ")
* [micooz/es6-webpack2-starter: A template project for es6/7, webpack2, sass and postcss](https://github.com/micooz/es6-webpack2-starter "micooz/es6-webpack2-starter: A template project for es6/7, webpack2, sass and postcss")