docker + webpack-dev-server で s2sのshopping-cartのサンプルを試してみたメモ

[s2sをdockerで試してみたメモ](https://qiita.com/hibohiboo/items/14433aa1061bcf472a21#_reference-07849505c4b70a60e147)のつづき。
s2sを動かしてみたメモ。

## 動作環境

* windows10
* vagrant1.9.7
* virtualbox5.1.26
* ubuntu-16.04
* Docker version 17.09.0-ce, build afdb6d4
* docker-compose version 1.17.1, build 6d101fb

### ディレクトリ構成

```
- app
  - docker
    - s2s
      - Dockerfile
    - docker-compose.yml
  + src
```

## エディタの環境設定

Flowは使ったことがなかったのでVSCodeの拡張を適用。
`Flow Language Support`と`flow-vscode-ide`をとりあえずいれてみた。

```
npm i -D flow flow-bin
```

```json:package.json
{
  "devDependencies": {
    "flow": "^0.2.3",
    "flow-bin": "^0.59.0"
  }
}

```

```json:.vscode/settings.json
{
  "files.associations": {
    "*.js": "javascriptreact"
  }
  ,"javascript.validate.enable": false
  ,"flow.useNPMPackagedFlow": true
  ,"flow.pathToFlow": "./node_modules/.bin/flow"
}
```

![2017-11-18.png](https://qiita-image-store.s3.amazonaws.com/0/99876/2dbfd654-0c12-2a84-ead8-11c18449e0cd.png)


[この時点のソース](https://github.com/hibohiboo/develop/tree/973197fcb5d1749d14ec295c2f04256d6423da6d/tutorial/lesson/s2s)

## 実践

[さよならボイラーテンプレート][*3]のshopping-cartを試す。

### 起動

`cd docker && docker-compose up`でs2sを起動。

![2017-11-19.png](https://qiita-image-store.s3.amazonaws.com/0/99876/d578409d-b089-879a-ed8f-ba7fce9c61b6.png)

### actionTypes.jsの作成

まずは、actionTypes.jsを作成してみる。
空のファイルを作って名前をつけて保存する。
以下の文がs2sによって記述される。

```js:src/App/actionTypes.js
// @flow

export const Actions = {};

export type Action = ;
```

![2017-11-19 (1).png](https://qiita-image-store.s3.amazonaws.com/0/99876/99832f85-f00e-4684-ec8c-a59c27611b45.png)

`no such file or directory`とかのエラーが出ているけれど、まだreducer.jsを作っていないので無視。

### actionTypes.jsのActionにAddを追加

次に、`export type Action = ;`を完成させる。
`export type Action = Add;`まで書いて保存する。
以下のように補完される。

```js:src/App/actionTypes.js
// @flow
export const ADD: 'App/ADD' = 'App/ADD'

export const Actions = {
  ADD,
}

export type Add = {
  type: typeof ADD,
}

export type Action = Add
```

さらにactions.jsを作成してくれる。

```js:src/App/actions.js
// @flow
import { ADD } from './actionTypes'
import type { Add } from './actionTypes'

export function add(): Add {
  return {
    type: ADD,
  }
}
```

### actionに変数nを追加

`n: number`を以下のように追加して保存。

```js:src/App/actionTypes.js
// @flow
export const ADD: 'App/ADD' = 'App/ADD'

export const Actions = {
  ADD,
}

export type Add = {
  type: typeof ADD,
  n: number,
}

export type Action = Add
```
actions.jsが更新される。

```js:src/App/actions.js
// @flow
import { ADD } from './actionTypes'
import type { Add } from './actionTypes'

export function add(n: number): Add {
  return {
    type: ADD,
    n,
  }
}
```

### reducerの作成

空のファイルreducer.jsを作成する。
以下の文がs2sによって記述される。

```js:src/App/reducer.js
// @flow
import type { Action } from '../../types'
import { Actions } from './actionTypes'

export type State = {}

export const initialState: State = {}

export default function(
  state: State = initialState,
  action: Action
): Exact<State> {
  switch (action.type) {
    default:
      return state
  }
}
```

さらに、reducer.test.jsも作成される。

```js:src/App/reducer.test.js
// @flow
import reducer, { initialState } from './reducer'
import * as actions from './actions'

test('provide the initial state', () => {
  expect(reducer(undefined, { type: '@@INIT' })).toEqual(initialState)
})
```

### Actionの追加

reducer.jsがある状態で、actionTypes.jsを再度保存すると、reducer.jsにcaseが追加される。

```js:src/App/reducer.js
// @flow
import type { Action } from '../../types'
import { Actions } from './actionTypes'

export type State = {}

export const initialState: State = {}

export default function(
  state: State = initialState,
  action: Action
): Exact<State> {
  switch (action.type) {
    case Actions.ADD:
      return {
        ...state,
      }

    default:
      return state
  }
}
```

reducer.test.jsも更新される。

```js:src/App/reducer.test.js
// @flow
import reducer, { initialState } from './reducer'
import * as actions from './actions'

test('provide the initial state', () => {
  expect(reducer(undefined, { type: '@@INIT' })).toEqual(initialState)
})

test('handle ADD', () => {
  expect(reducer(initialState, actions.add())).toEqual({})
})
```

### stateの追加

reducer.jsの`export type State = {}`を`export type state={count:number}`に変更する。
保存すると、initialStateが設定される。
`export const initialState: State = { count: 0 }`


### reducerの完成（手作業）

自動化されているのはここまでっぽい。
Addのときに、nをcountに追加する部分を記述。

```js:src/App/reducer.js
// @flow
import type { Action } from '../../types'
import { Actions } from './actionTypes'

export type State = { count: number }

export const initialState: State = { count: 0 }

export default function(
  state: State = initialState,
  action: Action
): Exact<State> {
  switch (action.type) {
    case Actions.ADD:
      return {
        ...state,
        count: state.count + action.n,
      }

    default:
      return state
  }
}
```

reducer.test.jsにactions.add()の引数と、toEqualの結果を記述。

```js:reducer.test.js
// @flow
import reducer, { initialState } from './reducer'
import * as actions from './actions'

test('provide the initial state', () => {
  expect(reducer(undefined, { type: '@@INIT' })).toEqual(initialState)
})

test('handle ADD', () => {
  expect(reducer(initialState, actions.add(2))).toEqual({count: 2})
})
```

### テスト確認

package.jsonにテストを動かすスクリプトが書かれていたので実行してみる。

```bash 
cd docker && docker-compose run s2s npm run test
```

![2017-11-19 (3).png](https://qiita-image-store.s3.amazonaws.com/0/99876/df034f7a-4853-7f4b-386f-364f1ccb7e8a.png)

[この時点のソース](https://github.com/hibohiboo/develop/tree/012ba053a21f76e2c34820e34699741425570700/tutorial/lesson/s2s)

## s2sのサンプルを動かした環境

srcディレクトリ内にはgitからshopping-cartのsrcをコピー。

```yaml:docker/docker-compose.yml
version: '3'
services:
  s2s:
    build: ./s2s
    volumes:
      - ../src:/app/s2s/examples/shopping-cart/src
      - ./config/.flowconfig:/app/s2s/example/shopping-cart/.flowconfig
      - ./config/s2s.config.js:/app/s2s/example/shopping-cart/s2s.config.js
    command: [yarn, run, s2s]
```


```docker:docker/s2s/Dockerfile
FROM node:9.2.0

# コンテナ上の作業ディレクトリ作成
WORKDIR /app

# ソースダウンロード
RUN git clone --depth=1 https://github.com/akameco/s2s.git

# chokidarのポーリング設定
RUN sed -i -e "s/\(ignoreInitial: true,\)/\1\n    usePolling: true, /g" /app/s2s/packages/s2s-cli/src/index.js

# s2sをビルド
WORKDIR /app/s2s/
RUN yarn

# example用設定
WORKDIR /app/s2s/examples/shopping-cart
RUN yarn
RUN yarn run flow
```

```js:docker/config/s2s.config.js
// @flow
const plugins = require('s2s-plugins-redux')

module.exports = {
  watch: './**/*.js',
  plugins,
  templates: [
    { test: /containers\/.*\/index.js/, input: 'containers.js' },
    { test: /components\/.*\/index.js/, input: 'components.js' },
    { test: /components\/.*\/index.test.js/, input: 'component.test.js' },
    { test: /reducer.js/, input: 'reducer.js' },
    { test: /reducer.js/, input: 'reducer.test.js', output: 'reducer.test.js' },
    { test: /reducer.js/, input: 'actionTypes.js', output: 'actionTypes.js' },
    { test: /selectors.js/, input: 'selectors.js' },
    { test: /selectors.test.js/, input: 'selectors.test.js' },
    { test: /logic.js/, input: 'logic.js' },
  ],
}
```

```:docker/config/.flow
[ignore]
.*/templates/.*

[include]

[libs]

[lints]

[options]
```

## ブラウザで実行確認

`cd docker && docker-compose up`

そのまま動かそうとしたら以下のエラー。

```
Uncaught TypeError: Cannot read property 'apply' of undefined
    at compose.js:29
    at createStore (createStore.js:51)
    at exports.default (store.js:10)
    at Object.<anonymous> (index.js:10)
    at __webpack_require__ (bootstrap 7bc4a4fa37f65eb57f87:19)
    at Object.<anonymous> (bundle.js:2296)
    at __webpack_require__ (bootstrap 7bc4a4fa37f65eb57f87:19)
    at module.exports (bootstrap 7bc4a4fa37f65eb57f87:62)
    at bootstrap 7bc4a4fa37f65eb57f87:62
```

store.jsの` window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()`でエラーが出ていた。

[redux-devtools-extension](https://github.com/zalmoxisus/redux-devtools-extension) にしたがい[Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)をchromeに追加。
動いた。
![2017-11-20.png](https://qiita-image-store.s3.amazonaws.com/0/99876/55af9c83-a78e-0f85-550b-338c149cfb67.png)

[この時点のソース](https://github.com/hibohiboo/develop/tree/cb339722ff804a074966cad80ce9f927fa22561b/tutorial/lesson/s2s)

### 動作確認環境

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

```
- app
  - docker
    - config
      - .babelrc
      - .flowconfig
      - s2s.config.js
      - webpack.config.js
    - s2s
      - Dockerfile
    - webpack
      - Dockerfile
    - docker-compose.yml
  + src
  + public
```

#### ブラウザで確認した環境

publicディレクトリ内にはgitからshopping-cartのpublicをコピー。

ブラウザで確認するためにwebpack-dev-serverを使用する。

```yaml:docker/docker-compose.yml
version: '3'
services:
  s2s:
    build: ./s2s
    volumes:
      - ../src:/app/s2s/examples/shopping-cart/src
      - ./config/.flowconfig:/app/s2s/example/shopping-cart/.flowconfig
      - ./config/s2s.config.js:/app/s2s/example/shopping-cart/s2s.config.js
    command: [yarn, run, s2s]

  webpack:
    build: ./webpack
    volumes:
      - ../src:/app/src
      - ./config/.flowconfig:/app/.flowconfig
      - ./config/.babelrc:/app/.babelrc
      - ./config/webpack.config.js:/app/webpack.config.js
      - ../public:/app/public
      - ../dist:/app/dist
    ports:
      -  8080:8080
    command: [npm, run, dev-server, --, --history-api-fallback]
```

```docker:docker/webpack/Dockerfile
FROM node:9.2.0

# コンテナ上の作業ディレクトリ作成
WORKDIR /app

# package.jsonを作成
RUN npm init -y

# ビルドツール
RUN npm i -D webpack

# 開発用サーバ
RUN npm i -D webpack-dev-server

# es6用トランスパイラ
RUN npm i -D babel-loader
RUN npm i -D babel-preset-es2015
RUN npm i -D babel-preset-react
RUN npm i -D babel-preset-stage-2

# flow
RUN npm i -D flow
RUN npm i -D flow-bin
RUN npm i -D babel-plugin-transform-flow-strip-types

# jsViewライブラリreact
RUN npm i -S react
RUN npm i -S react-dom
RUN npm i -S react-redux
RUN npm i -S react-scripts

# フレームワーク
RUN npm i -S redux
RUN npm i -S redux-thunk

RUN sed -i -e "s/\(\"scripts\": {\)/\1\n    \"flow\": \"flow\",/g" /app/package.json
RUN sed -i -e "s/\(\"scripts\": {\)/\1\n    \"babel\": \"babel\",/g" /app/package.json
RUN sed -i -e "s/\(\"scripts\": {\)/\1\n    \"webpack\": \"webpack\",/g" /app/package.json
RUN sed -i -e "s/\(\"scripts\": {\)/\1\n    \"dev-server\": \"webpack-dev-server\", /g" /app/package.json
```

```json:docker/config/.babelrc
{
  "presets": [
    "react",
    "es2015",
    "stage-2"
  ],
  "plugins": [
    "transform-flow-strip-types"
  ]
}
```
```:docker/config/.flowconfig
[ignore]
.*/templates/.*
.*/node_modules/.*

[include]

[libs]

[lints]

[options]
```

```js:docker/config/webpack.js
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: '/app/dist'
  },
  devtool: "source-map",
  resolve: {
    extensions: [".jsx", ".js"]
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      }
    ]
  },
  performance: {
    hints:false
  },
  devServer: {
    // public/index.htmlをデフォルトのホームとする
    contentBase: './public',
    // インラインモード
    inline: true,
    // 8080番ポートで起動
    port: 8080,
    // dockerのコンテナ上でサーバを動かすときは以下の設定で全ての接続を受け入れる
    host:"0.0.0.0",
  },
  // vagrantの仕様でポーリングしないとファイルの変更を感知できない
  watchOptions: {
    aggregateTimeout: 300,
    // ５秒毎にポーリング
    poll: 5000
  }
};
```

## 参考

[type can be used by only '.ts' files][*1]
[Visual Studio CodeでFlowTypeのエラーを表示する][*2]
[さよならボイラープレート。s2sによる高速reduxアプリケーション構築][*3]
[Flowを使ってアプリケーション開発するためのエディタ環境いろいろ][*4]
[Flow導入時に最初にやることまとめ][*5]
[React + flow + Webpackの最小構成][*6]
[webpack history api fallback][*7]

[*1]:https://qiita.com/akameco/items/3e5402357ca32fd2dcaa
[*2]:https://qiita.com/nyamogera/items/c3a9d1d022c933699ea2
[*3]:https://qiita.com/akameco/items/e1489c6bbf3439ec6ca4#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%81%A8redux%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6
[*4]:https://blog.mmmcorp.co.jp/blog/2016/12/07/development-enviroment-with-flow/
[*5]:https://qiita.com/zaki-yama/items/3567b316f0fb62986809
[*6]:https://qiita.com/uryyyyyyy/items/19bfcbbb18b4b98e30bc
[*7]:http://js.studio-kingdom.com/webpack/api/webpack_dev_server