docker-compose で apache2+PHP 開発環境 (silex, CLI, mysql, redis)

docker-compose でなるべく楽をして PHP 開発環境を作成する流れについて、一応使えそうになったのでまとめます。

ローカルに PHP を入れたりせず、Docker さえあればアプリケーションの CLI や composer を使って開発ができることを目的にします。

想定する PHP アプリ

  • Silex
  • MySQL、Redis
  • apache2 + PHP7.1

作業環境

Windows, Mac どちらでも Docker が動けば問題ないはずです。いちおう下記で基本動作を確認。

開発環境

ディレクトリ構成

基本的には Silex Skeleton に従います。

├── bin
│   └── console
├── composer.json
├── composer.lock
├── config
├── docker
│   └── app
│        ├── app.conf
│        ├── Dockerfile
│        ├── mpm_prefork.conf
│        └── php.ini
├── docker-compose.yaml
├── etc
│   ├── mysql.php
│   └── redis.php
├── README.md
├── src
├── templates
├── tests
├── var
└── web

アプリのルートに docker-compose.yaml を、docker/app 以下にアプリケーションコンテナ関連のファイルを置いています。

composer install

composer install します。composer 本体はDocker公式レポジトリのイメージ をバイナリのように使えます。

アプリのルートで以下のコマンドを実行します。

$ docker run --rm --interactive --tty --volume ${PWD}:/app composer install

無事 vendor ディレクトリが生成されるはずです。

msys2 など cygwin 互換環境の場合は --volume `cygpath $PWD -w`:/app とするとパスが解決できるかと思います。(以下 volume オプション使用時同様)

主題とは関係ないですが、日本で使う場合には composer.json にミラーの設定をしておくと速度が上がって多少苦しみが緩和されると思います。

...
"repositories": {
    "packagist": {
         "type": "composer",
         "url": "https://packagist.jp"
    }
},
...

アプリケーションコンテナ

apache2 + PHP が動くコンテナをビルドします。 手軽さを優先し、Docker公式レポジトリのPHPイメージ を使います。

docker/app/Dockerfile を書きます。今回は MySQL, Redis を使いたいので extension を追加します。

docker/app/Dockerfile

FROM php:7.1-apache

RUN docker-php-ext-install -j$(nproc) mbstring opcache pdo pdo_mysql

RUN apt-get update && apt-get install -y zlib1g-dev \
 && pecl install redis-3.1.3 \
 && docker-php-ext-enable redis \
 && apt-get autoremove --purge -qq \
 && apt-get clean \
 && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*

COPY php.ini /usr/local/etc/php/
COPY app.conf /etc/apache2/sites-available/app.conf
RUN a2dissite '*default*' \
 && a2ensite app \
 && service apache2 restart

公式イメージのページにも十分な説明があるとは言えないのですが、docker-php-ext-install だけですんなり入るものとそうでないものがあります。phpredis はビルドの必要がありました。他にも、例えば CURLdocker-php-ext-install の候補に含まれているものの、事前に libcurl3-dev が必要だったりするので、目的の物によっては GitHub issues を探す必要はあるかもしれません。

ここでは、MySQL、Redis 関連の extension 追加の他に、php.ini と apache の設定をイメージに追加しています。

apache2 には後述の docker-compose で調整するマウントポイントが DocumentRoot になるように設定しておきます。この辺はお好みです。

docker/app/app.conf

<VirtualHost *:80>
    DocumentRoot /app/web

    <Directory /app/web>
        Options all
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

php.ini には、開発用に

docker/app/php.ini

log_errors = On
error_log = /dev/stderr

を記述しておくことで、PHP ログを docker 標準のログとしてとり扱うことができます。

MySQL、Redis コンテナ

今回は公式のイメージそのままで使います。docker-compose.yaml の記述のみですむので専用の Dockerfile は用意しません。

docker-compose で環境立ち上げ

docker-compose でアプリケーション、MySQL、Redis を立ち上げます。

docker-compose.yaml

version: '3'
services:
  app:
    build:
      context: ./docker/app
    volumes:
      - .:/app
    working_dir: /app
    ports:
      - "3000:80"
    environment:
      - SYMFONY_ENV=dev
      - DB_MASTER_HOST=mysql
      - DB_MASTER_USER=app
      - DB_MASTER_PASSWORD=app
      - DB_REPLICA_HOST=mysql
      - DB_REPLICA_USER=app
      - DB_REPLICA_PASSWORD=app
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:5.7
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
    volumes:
      - "mysql-myapp-data:/var/lib/mysql"
    environment:
      - "MYSQL_DATABASE=myapp_dev"
      - "MYSQL_ROOT_PASSWORD=root"
      - "MYSQL_USER=app"
      - "MYSQL_PASSWORD=app"
    ports:
      - "3306:3306"

  redis:
    image: redis

volumes:
  mysql-myapp-data:

app はアプリケーションを動かす apache2+PHP コンテナです。docker/app 以下をビルドし、カレント(アプリのルート)ディレクトリを /app にマウントしています。また、後々コマンドラインが使いやすいように working_dir を設定しています。環境変数では主に MySQL 関連の情報を渡しています。

以前の compose file v1 の頃からの差分になりますが、v3 では link を書かなくてもデフォルトで他のコンテナを名前解決できるようになっています。depends_on は起動順番を指定するものですが、MySQL コンテナは起動後準備できるまでにラグがあり、これを書いたからといって app 起動前に他の環境が全て ready というわけでもなくちょっと微妙です。

mysql では永続化ボリュームを設定しています。

ここまでアプリの動作環境がそろいました。

$ docker-compose up

で初回のアプリケーションコンテナのビルド、MySQL、Redis コンテナの Pull が行われるはずです。

立ち上がったら http://localhost:3000 でアプリケーションにアクセスできます。

CLI を使う

Silex Skeleton では bin/consoleCLI が使えるようになっています。他のフレームワークでも Railsbin/ のように CLI ツールを置くことがあるでしょう。

今回のようにローカルに実行言語を入れない場合でも、app コンテナを使って CLI を叩くことができます。

$ docker-compose exec app bin/console -h
Usage:
  list [options] [--] [<namespace>]

Arguments:
  namespace            The namespace name

Options:
...

今回はお試し CLI として、doctrine/migrations で DB のマイグレーションをやってみます。doctorin 自体の使い方はこちらの Silexだってdoctorin/migrations使う を参考にさせていただきました。

├── bin
│   └── console
├── composer.json
├── composer.lock
├── config
├── db
│  ├── migrations
│  ├── migrations.yml
│  └── migrations-db.php
...
├── src
│  ├── console.php

db/ 以下に関連ファイルを置きました。

db/migrations.yml

name: Doctrine Migrations
migrations_namespace: MyApp\Db\Migrations
table_name: doctrine_migration_versions
migrations_directory: ./db/migrations

db/migrations-db.php

<?php

return [
    'driver'    => 'pdo_mysql',
    'host'      => getenv('DB_MASTER_HOST'),
    'dbname'    => 'myapp_' . getenv('SYMFONY_ENV'),
    'user'      => getenv('DB_MASTER_USER'),
    'password'  => getenv('DB_MASTER_PASSWORD'),
    'charset'   => 'utf8mb4',
];

参考記事のように、console.php に migration コマンドを登録します。

use Doctrine\DBAL\Migrations\Tools\Console\Command\DiffCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\LatestCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand;

$console = new Application('My Silex Application', 'n/a');
...

/** Doctrine\DBAL\Migrations */
$console->add(new DiffCommand());
$console->add(new ExecuteCommand());
$console->add(new GenerateCommand());
$console->add(new LatestCommand());
$console->add(new MigrateCommand());
$console->add(new StatusCommand());
$console->add(new VersionCommand());

準備できたら、migration 定義を生成、

$ docker-compose exec app bin/console migrations:generate --configuration db/migrations.yml --db-configuration db/migrations-db.php

db/migrations 以下に生成されたファイルを編集し、migration 実行します。

$ docker-compose exec app bin/console migrations:migrate --configuration db/migrations.yml --db-configuration db/migrations-db.php

array(5) {
  ["host"]=>
  string(5) "mysql"
  ["dbname"]=>
  string(9) "myapp_dev"
  ["user"]=>
  string(3) "app"
  ["password"]=>
  string(3) "app"
  ["charset"]=>
  string(7) "utf8mb4"
}
Loading configuration from command option: db/migrations.yml

                    Doctrine Migrations


WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 2017xxxxxxxxxx from 0

  ++ migrating 2017xxxxxxxxxx

     -> CREATE TABLE example (col1 LONGTEXT NOT NULL) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.33s)

  ------------------------

  ++ finished in 0.33s
  ++ 1 migrations executed
  ++ 1 sql queries

(migration の中身は適当です) 無事コマンド実行できました。

まとめ

docker-compose でローカル環境の影響を最小限に開発環境が整いました。

引き続き、lint ツールの統合やデプロイについてもまとめる予定です。

特に参考にさせていただいた記事

Windows で zsh と開発環境を整備

最近 Surface Pro 4 を購入した勢いで Windows のシェル環境整備し一新したまとめ。Surface Pro 4 よいです。

この記事は 2016/05現在プレビュー版である bash on Ubuntu on Windowns とは無関係です。そちらは正式版が出たら試したいと思います。

bash on Ubuntu on Windows が来るならそれでシェル環境間に合うかなとも思ったんですが、仕組み上 Linux サブシステム側から Windwos アプリを立ちあげられるようにはならなそうなので、それはそれ、これはこれという感じで。

やりたいこと

  • アプリのパッケージ管理したい
  • zsh 使いたい
  • putty ではなく普通の ssh がほしい
  • git
  • Ruby 少々
  • node 少々
  • NTEmacs 使いたい

普段の仕事では OSX / Linux 中心なので、なるべくどこでも同じような使用感が得られる環境を目指します。

Ruby/node 少々というのは、ガチ開発になるとは辛いけどサンプル動かすくらいはできる、程度の意味です。

検討結果

結論としては、以下のツール郡でなかなかよい感じになりました。

  • パッケージ管理 : Chocolatey + MSYS2内 pacman の2段構成
    • なるべく Chocolatey で導入する
    • MSYS2 シェル内でしか使わないものは pacman で入れる
  • シェル : MSYS2 の zsh
  • 端末エミュレータ : ConEmu
  • OpenSSH : MSYS2 pacman パッケージ
  • Ruby : Chocolatey パッケージ
  • node : Chocolatey パッケージ
  • Emacs : https://github.com/chuntaro/NTEmacs64 のビルド(手動インストール)

それぞれ詳細

パッケージ管理

まずはパッケージマネージャについてです。

Windows が諸 Linux に最も差をつけられているのがここだと思っていましたが、Windows10 では MS 製の PowerShell モジュール PackageManager (旧OneGetと同じ?) が使えるようになりましたし、サードパーティ製ではあるものの、apt 的な使い勝手をもつ Chocolatey も成熟してきているようで、2016 年現在ほとんど不便は解消していそうです。

パッケージマネージャは環境構築の基礎になるところなので、できれば長いサポートが見込める公式のものを使いたいところです。 なので PackageManager を…と思いましたが、これがまだイマイチこなれてないような…のと、新しいせいで情報が少なめです。よく見るとこの MS PackageManager は複数リポジトリプロバイダを統括する層のツールで、サポート対象には Chocolatey のリポジトリも含まれていました。これなら Chocolatey の今後もしばらく安心できそうですし、何より既にシェアが大きいようで情報も多いので Chocolatey をメインで使うようにしました。

Chocolatey のインストールは、公式トップにある手順で問題なくできました。 https://chocolatey.org/

シェル環境、端末エミュレータ

MSYS2 はパッケージマネージャがまともそうで、生まれが比較的新しいことに期待して選びました。MSYS2 installer にしたがってインストールします。

MSYS2 はデフォルト端末として mintty が起動するようになっています。mintty は見た目も悪くないのですが、node や irb など対話型の扱いに問題がありましたので、ConEmu から MSYS2 のシェルを起動する形をとります。

まずは付属の MSYS2.shell を起動し zsh をインストールします。

$ pacman -S zsh

次に ConEmu をインストールします。今度は Chocolatey で入れます。PowerShell を管理者権限で起動し、以下の様にインストールします。

PS :> choco install ConEmu

インストールできたら、ConEmu で MSYS2 zsh を起動する設定をします。[Settings] の [Startup] > [Tasks] に zsh 起動コマンドを追加します。

  • set CHERE_INVOKING=1 & %ConEmuDrive%\msys64\usr\bin\zsh.exe --login -i -new_console:C:"%ConEmuDrive%\msys64\msys2.ico"
  • "Default Shell" にチェック

conemu screenshot

これで ConEmu + zsh が起動するようになると思います。

f:id:Zarari:20160504113020p:plain

HOME 環境変数を設定する

シェル内や後述の Emacs の動作を Unix 系に近づけるため、環境変数 HOME を設定します。

C:\Users\username あたりが HOME になってほしいので、%USERPROFILE% をそのまま設定で ok です。

ちなみに、PowerShellenv のように環境変数を一覧したい場合は PS > Get-ChildItem env: で参照できます。

OpenSSH

Windows 版 OpenSSH もあるのですが、自分が試した限りでは ssh-agent が MSYS2 環境で動かせなかったので MSYS2 の OpenSSH を使用します。OpenSSH はどうやら残念ながら $HOME/.ssh/ を見てくれないようなので「msys2での$HOMEとOpenSSHでのホームディレクトリの違い」を参考に、$HOME/home/username にマウントする方法をとります。

/etc/fstab に追加

# User home
C:/Users/username /home/username

.ssh/configeval `ssh-agent` も問題ありません。

Git, Ruby, Node

Git, Ruby, Node は chocolatey で入れます。

PC > choco install git ruby nodejs

RubyC:\tools\ruby22 にインストールされるようなので、MSYS2 側で /c/tools/ruby22/bin にパスを通します。

$ git --version
git version 2.8.1.windows.1
$ ruby -v                                                                                              
ruby 2.2.4p230 (2015-12-16 revision 53155) [x64-mingw32]
$ node -v                                                                                              
v5.10.1

また、Windows 版 git は改行コードを勝手に変換してしまうので、自動変換を無効化します。

$ git config --global core.autoCRLF false

Emacs

GUI 版の Emacs については flycheck の都合で libxml2 同梱のものが欲しかったため、https://github.com/chuntaro/NTEmacs64IME パッチ版を使用します。これはパッケージ管理から漏れますが諦めました…。

runemacs ショートカットに[作業フォルダー]を指定することで、起動直後の場所を自由に設定できます。

f:id:Zarari:20160504164737p:plain

その他

  • Ctrl と Caps Lock の入れ替えには MS 製ユーティリティの Ctrl2Cap を使っています。
  • 全体的なキー割当カスタマイズには Keyhac が使い易かったです。
  • 管理者権限でシェルを起動してくれる Sudo が地味に便利です。

以上です。簡単な Web 系開発であれば快適に作業できるようになりました!

React + Redux に取り組んだ際のハマりポイント振り返り

javascript 初学者が React + Redux に取り組んだ際のハマりポイント、時間がかかったポイントを時系列で書いていきます。

振り返りのまとめ

  • React も Redux もドキュメント充実してるのでちゃんと読むべき
  • フロントエンド開発環境は空気読むのが難しい

動機

  • javascript ぜんぜん書いてないので、よさそうな ES2015 とやらが出たこの機会に触っておきたかった
  • Single Page Application を作る機会があった
    • 一人でさっと作る類のものなので、せっかくなので近年話題のものを調べてみたい

振り返り

※ 最終的にできあがったサンプルはこちらに置いています

ハマり Points

React について

そもそも正確には何をするライブラリで、どの範囲を指している言葉なのか? これはハマりというか事前の理解が違っていました。ぼんやりと複雑なものを想像していましたが、概念的にはむしろシンプルで小さいというのが今の理解です。

https://facebook.github.io/react/docs/getting-started.html

公式のドキュメントがしっかりしています。StarterKit によると

  • Just the UI
  • Virtual DOM
  • Data flow

の UI ライブラリだそうです。この時点ではちょっとピンと来ていなかったですが、フレームワークの類ではなく本当に View でしかないものです。

もう少し React

雑に言えば、Component というのをたくさん作ってそれを組み合わせ仮想 DOM を構築し、それをよい具合にリアル DOM に変換してくれるライブラリ…でよい?

Component

Component とはパラメータを渡せる UI 部品で

  • JSX という HTML 風シンタックスを使って書ける
  • ライフサイクルイベントを持っている。
  • 最小限にすべきだが、Component ごとの state を持つこともできる

というものです。

Component の組み合わせ方としては、通常の HTML 要素を組むように Component のツリー構造を作り、最上位の ReactDOM.render() に渡せばよいようです。公式チュートリアル が良かったです。

このチュートリアルは cdn からライブラリを取得し index.htmlexample.js にモリモリ書いていくだけなのでハマりどころが無くわかりやすかったです。一点 <script type="text/babel"> ってなんだ…?と思いつつも、簡単に動くものが作れます。

フロントエンドの開発環境

チュートリアルまで終えたところで、じゃあ npm 使ってパッケージ管理して書いていこうと思ったわけですが、この辺で javascript というかフロントエンド開発環境の複雑さに気づきました…。

まだ過渡期なのかもしれませんがちょっとバラバラしすぎている印象が…。調べながら現代に追いつくのが困難でした。ググッて 2014 くらいだと既に変わっていることが多いので、2015後半〜 くらいの期間指定検索で追いかけていました。最近は「一新した」系記事が多いところを見るとそろそろ落ち着いたのかな…?

babel

動機の一つであった ES2015 はブラウザ処理系ではほぼ使えないので、トランスパイラをはさむのが普通らしく、これは babel というのを使っておけばよさそうです。しかし、どうビルドしていくかについては、facebook の StarterKit の段階では grunt なるものを使うガイドになっていましたが、glup がいいよとか npm の script で十分行けるとか、webpack がよいとか調べるほど混乱が増えてさっそく一度折れかけました。

結局、今の時点でツールを増やしたくないので npm script でやります。http://mae.chab.in/archives/2765 を参考にし、watchify + babelify で ruby の Guard っぽいことができるということでこれを使ってみます。

npm install --save-dev を知りました。

$ npm install --save-dev babel-plugin-transform-object-rest-spread
$ npm install --save-dev babel-preset-es2015 babel-preset-react browserify watchify babelify

.babelrc

{
  "presets": ["react", "es2015"],
  "plugins": ["transform-object-rest-spread"]
}

package.json

  "scripts": {
    "watch": "watchify -v -t babelify -o public/bundle.js src/index.jsx"
  }

監視開始

$ npm run watch

src/index.jsx を編集するそばから即反映されるようになりました。欲しかった import も、 () => {} も、 const/let も、{...obj} も使えます。よさそうです。

ファイル拡張子については、JSX が含まれるものは .jsx, そうでないものは .js としてみます。(全部 .js のパターンのほうが多そうで迷ってます。どうなんでしょう)

スタイルガイド

書き始めると、自分では良いスタイルが全然わからないので、Rubocop のようなものがほしいところです。

普段は Emacs で書きながら FlyCheck でチェックさせているので、近いものをさがしていたらまさにFlyCheck で ESLint するという記事を書かれている方がいましたので、参考にさせていただきました。

$ npm install -g eslint

.eslintrc

{
  "parser": "babel-eslint",
  "env": {
    "es6": true
  },
  "ecmaFeatures": {
    "jsx": true
  }
}

本来は eslint も package.json 管理下とするのがよさそうですが、現在の自分のエディタ設定の都合でグローバルに入れちゃいます。そのうちなんとかしたい。

JSX のシンタックスって?

単一の要素しか返せないのか? パラメータをまとめて渡すにはどうすれば?

などで無駄につまづいてしまいました。これは最初に https://facebook.github.io/react/docs/jsx-in-depth.html を読むべきでした。React.createElement() の別の記法だと理解していれば非常にシンプルです。

var Nav, Profile;
// Input (JSX):
var app = <Nav color="blue"><Profile>click</Profile></Nav>;
// Output (JS):
var app = React.createElement(
  Nav,
  {color:"blue"},
  React.createElement(Profile, null, "click")
);

パラメータについても、1つずつ <Comp param={value} /> でも良いしまとめて <Comp {...props} /> でも渡せます。

Debug どうすれば

トランスパイラをかませるとデバッガ上の表示が元ソースと別になる問題でしたが、source map という仕組みがありました。近年のブラウザは問題なく対応してるようで割と何年も前からあるみたいです。常識でしたか…

watchify に -d オプションと exorcist 挟むのを追加。

$ npm install --save-dev exorcist

package.json

  "scripts": {
    "watch": "watchify -v -t babelify -o \"exorcist ./public/bundle.js.map > public/bundle.js\" src/index.jsx -d",
    "build": "watchify -v -t babelify -o public/bundle.js src/index.jsx"
  }

エラー行の対応付け、ブレークポイントの設定ができるようになりました。しかし生成されるファイルは非常に巨大になるので dev 専用です。build と分けました。

Redux って?

Reducer, actionCreator, connect, mapXXXToProps, ... 見慣れない言葉が多くて全体が見えず、二度目の折れかけポイントでしたが、資料は少なくないので路頭に迷わずにはすみました。

最初に http://redux.js.org/index.html2.Basics まで目を通します。その後、こちらのビデオ をざーっと見て概要を理解できた気になれました。

概要はわかったものの、動く状態が無いとよくわからないので無理やり動くものを用意します。ファイル分割は一旦おいといて、index.jsx にまとめて書いてみました。

$ npm install --save react react-dom redux react-redux

index.jsx

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { connect, Provider } from 'react-redux'

/// actionCreators >>>
function action1(value) {
  return {
    type: 'ACTION1',
    value
  }
}
/// <<<

/// reducers >>>
function appReducer(state, action) {
  switch (action.type) {
    case 'ACTION1':
      return Object.assign({}, state, { v1: action.value });
    default:
      return state
  }
}
/// <<<

/// components >>>
const Comp1 = (props) => (
  <div>{console.log(props)}
    <div>This is Comp1. value is '{props.value}'.</div>
    <button onClick={() => props.action('VALUE')} >doAction1</button>
  </div>
);
/// <<<

class RootApp extends Component {
  render() {
    return (
      <div>
        Hello, this is root component.
        <Comp1
            value={this.props.value1}
            action={this.props.action1}
        />
      </div>
    )
  }
}

const App = connect(
  (state) => { return { value1: state.v1} }, // mapStateToProps
  (dispatch) => { return { action1: (v1) => dispatch(action1(v1)) } }// mapDispatchToProps
)(RootApp);

const rootElement = document.getElementById('content');
const initialValues = { v1: 'initialvalue' };
const store = createStore(appReducer, initialValues);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

これで最低限は動くはずです。意図的に変な名前の部分と便利なヘルパーを使ってない部分がありますが、最終的には整理しています。

確認を兼ねてちょっと説明

先ほどの例で上のほうにあるのが Redux 世界のもので、

  • actionCreator は action を返す関数。
  • action というのはただのオブジェクト。type が必須。
  • reducer は、dispatch(action) された時に呼ばれる関数。全体の状態ツリーである state と action が渡ってくるので、action に応じて新しい state オブジェクトを生成して返す。
  • createStore() で全体の状態を保持する store を作成する。これには reducer と、必要なら初期値を与える。    - reducer は、state, action を受け取って新しい state を返す関数。

ここまでが Redux の枠組みかと思います。UI に直接は関係しないので、この時点で store.dispatch(action) のようにテストできます。(とビデオでもやってました)

次に、Redux 世界と React との接続については

  • おまじない的に store を与えた <Provider> をルートに置き、実質のルートになる <App> コンポーネントを入れる。   - <Provider> には1つしかコンポーネントを入れられない。
  • <App> は、connect() されたコンポーネントである必要がある   - この connect() というのは、Redux の store を React の props に接続するもの   - 第一引数が、reducer で返された state を props にマップする関数   - 第二引数が、actionCreators を props にマップする関数

となります。Component に props が渡った先は React の文脈になるので、普通に細かく Component を書いていけば OK です。ひとつ違うのは、Redux では state は1つなので、Component 自体の state を変更はできず、動的な値を扱うには上からパラメータを渡してもらうしかありません。

Redux のデバッグ

redux-logger ミドルウェアを入れると、dispatch の度に state を表示してくれます。

$ npm install --save redux-logger

redux-devtools なる本格的なのもあるようですがこちらはまだ試していません。

Redux で非同期 dispatch

どこで非同期リクエストのコードを書けば良いのかについて、http://redux.js.org/index.html の 3. Advanced 以降に詳しく書いてあります。このあたりredux-thunk ミドルウェア を使って actionCreator で fetch すればよいです。action を細かく分けると混乱せずに済みます。

$ npm install --save redux-thunk

非同期 API

前後しますが、そもそも非同期リクエストって何使えばよいの…って時点でもつまづいていました。

React Tutorial では、わかりやすさを重視したのか jQuery$.ajax を使っていましたが、リクエスト投げたいだけなのに jQuery を読み込むのは間違っている気がしますし、単独の非同期リクエスト用ライブラリで何がデファクト標準なのかを探すのに時間を取られてしまいました。

GitHub star や雰囲気を見ながらですと superagentaxios もよさそうですが、複雑なことはしないので単純でよいというのもあり、redux-async の例で使われていた fetch(isomorphic-fetch) でやっています。

$ npm install --save isomorphic-fetch

Redux で form

React の form の例を探すと、該当 Component に input 値の state を持たせるパターンばかりが見つかります…。

Redux で使えないため、値を伝搬させるのを自力で書こうかと思いましたが明らかに面倒くさいので redux-form ミドルウェア を使います。

$ npm install --save redux-form

非同期の redux-thunk のときもそうでしたが、こういう基本的な機能は本体付属のライブラリ無いのかな?と思いましたが無いようです。何気なくつぶやいたら Twitter で教えて頂けてとても安心しました。雰囲気としてどんどん外部ライブラリ使っていく、というものなんでしょう。

Redux のディレクトリ、ファイル構成

多くの例にあるとおりにしています。

src
├── actions                # redux actionCreators
├── reducers               # redux reducer functions
├── store                  # redux middleware 使えるようにするところ
├── containers             # react-redux 接続部分
├── components             # UI 部品
└── index.jsx              # エントリポイント

特に containers と components について、公式ドキュメントの http://redux.js.org/docs/basics/UsageWithReact.html が重要そうです。シンプルな場合なら、components には Redux 世界のものは全く含まれない状態になると思います。

redux-form の接続部分は containers なのか component なのかまだ理解が浅いです…。

Components の書き方

一つ一つの Component は割とスカスカになりそうに見えます。ライフサイクルイベント(componentDidMount とか) がどうしても必要な場合を除き、関数スタイルで書くのがすっきり書けてよさそうです。

import React, { PropTypes } from 'react'

const MyItem = (props) => {
  return (
    <span>{props.name} : {props.value}</span>
  )
}

MyItem.propTypes = {
  name: PropTypes.string.isRequired,
  value: PropTypes.string.isRequired
}

export default MyItem

最終的に

こうなりました(再掲) https://github.com/komazarari/react-redux-form-example

すっきりしましたが、特に reducer や form にまだ改善の余地がありそうです。

足りないこと

今回は CSS について触れていません。webpack とやらを使えばすっきりするらしいので引き続き試してみたいと思います。

日本人向け Packer スクリプト差分 for Ubuntu Trusty

Packer?

仮想マシン作成をコード化し、同一のソースから様々なフォーマットのVMイメージを作成する Packer というツールがあります。私は主にこれを VagrantBox 作成の自動化のために使ってます。

このための設定ファイルやスクリプトhttps://github.com/opscode/bento や veewee から変換して取得することができます。

最低限のローカライズをする

前述の設定ファイルをそのまま使えばほとんどの場合問題無いものの、自分用に作るならせめてタイムゾーンや近いミラーサーバくらいはデフォルトからいじっておいたほうが使い勝手がよいので、いくつか追記/変更しておく。

ひな形は bento

https://github.com/opscode/bento をちょっといじる

build JSON

packer/ubuntu-14.04-amd64.json について。packer の設定ファイル。

キーボード

最初のこれは正直どっちでもよいのだが、boot_command の時点で日本語キーボードに合わせてしまう

+ ” keyboard-configuration/layout=Japanese<wait>",
+ ” keyboard-configuration/variant=Japanese<wait>",

preseed.cfg

Debian 系のインストール自動化が行える設定ファイル。RH系の Kickstart みたいなもの json に preseed のパスが指定できる。今回はhttp経由で packer/http/ubuntu14.04/preseed.cfg 等が使われる

タイムゾーン

下記を編集

--- a/packer/http/ubuntu-14.04/preseed.cfg
+++ b/packer/http/ubuntu-14.04/preseed.cfg
@@ -27,3 +27,3 @@ d-i pkgsel/update-policy select unattended-upgrades
 d-i pkgsel/upgrade select full-upgrade
-d-i time/zone string UTC
+d-i time/zone string Japan
 d-i user-setup/allow-password-weak boolean true

d-i clock-setup/utc はシステム時刻の設定なので特に変えないほうがよい。

country

apt のミラーがデフォルトで us.archive.ubuntu.com になってしまうので日本のミラーにしておく

+ d-i debian-installer/country string JP
+ d-i mirror/http/mirror select jp.archive.ubuntu.com

を加える

その他のカスタマイズ

あとは細々とやりやすいように変更する

  • chef の最新版を入れておきたい
  • VirtualBox GuestAdditions をビルドできる環境はクリーンアップせずに残しておきたい

結果

こんな感じにしました。 https://github.com/komazarari/packer

Capistrano ver.3 tasks for puma

Capistrano を version 3 にしようとしてつまづいた

ずいぶん変わっているので、各所でつまづきの声が聞こえています。

しかし Rails から切り離されてることでシンプルになって汎用性が上がり、withintest など便利なメソッドも使えて良い感じ!

ver.2 までを使っていたら気をつけること

  • multistage と color がデフォルトに!
  • Rails oriented なツールではない
  • ver 2 を流用しようと思わずに一から書き直したほうが早い
  • SSHKit をちゃんと読む https://github.com/capistrano/sshkit
  • 公式のドキュメントもちゃんと読む。他には Update Guide とかも読む
  • scm git しか使えない
  • deploy_via :copy は使えない
  • set したら fetch()

Rails 関連で気をつけること

  • gem 'capistrano-rails', :require => false in Gemfile と require 'capistrano/rails' in Capfile しよう
  • 勝手に log とか tmp とか作ったりしないので、set :linked_dirs, ...

puma タスクを Capistrano 3 対応する

puma 本家puma/capistrano は 2014/01時点で Capistrano 3 に対応していないので、悩むより自分で書いたほうがすぐできる感じ。

設定ファイルや start/stop/restart はもともとの puma/capistrano から、全体の構成はこちらのunicorn向けtaskを真似させてもらっています。

namespace :puma do
  task :env do
    set :puma_state,  "#{shared_path}/tmp/sockets/puma.state"
  end

  def start_puma(bind_addr)
    execute :bundle, :exec, :puma, start_options(bind_addr)
  end

  def stop_puma
    # Now use 'halt' to stop puma, because 'stop' remains puma process living
    # and remove pumactl.sock by halves. It seems a puma's bug.
    # https://github.com/puma/puma/issues/306
    execute :bundle, :exec, :pumactl, "-S #{puma_state_path} halt"
  end

  def restart_puma(phased_restart = nil)
    restart = phased_restart ? 'phased-restart' : 'restart'
    execute :bundle, :exec, :pumactl, "-S #{puma_state_path} #{restart}"
  end

  def start_options(bind_addr = '127.0.0.1')
    if config_file
      "-q -d -e #{app_env} -C #{config_file}"
    else
      "-q -d -e #{app_env} -b 'tcp://#{bind_addr}:3000' -S #{fetch(:puma_state)} --control 'tcp://localhost:3001'"
    end
  end

  def config_file
    # @ToDo
    nil
  end

  def puma_state_path
    (config_file ? configuration.options[:state] : nil) || fetch(:puma_state)
  end

  def configuration
    require 'puma/configuration'

    config = Puma::Configration.new(config_file: config_file)
    config.load
    config
  end

  desc "Start puma"
  task :start => :env do
    on roles(:app) do |host|
      within current_path do
        start_puma(host)
      end
    end
  end

  desc "Stop puma"
  task :stop  => :env do
    on roles(:app) do
      within current_path do
        stop_puma
      end
    end
  end

  desc "Restart puma"
  task :restart => :env do
    on roles(:app) do |host|
      within current_path do
        if test("[ -f #{puma_state_path} ]")
          restart_puma
        else
          start_puma(host)
        end
      end
    end
  end

  desc "Phased restart puma"
  task :phased_restart => :env do
    on roles(:app) do |host|
      within current_path do
        if test("[ -f #{puma_state_path} ]")
          restart_puma('phased-restart')
        else
          start_puma(host)
        end
      end
    end
  end

  desc "Force clean up puma"
  task :cleanup => :env do
    on roles(:app) do
      if test("[ -f #{puma_state_path} ]")
        execute <<-EOC
kill -9 `grep pid #{puma_state_path} | awk '{print $2;}'`
rm -f #{puma_state_path}
EOC
      else
        execute <<-EOC
kill -9 `ps ax | grep ruby | grep puma | awk '{print $1}'`
rm -f #{puma_state_path}
EOC
      end
    end
  end

  after 'deploy:publishing', 'puma:restart'
end

上記ではアプリ本体、Control Server ともに TCP で立ち上げているが、UNIX ソケット unix://... でもおっけー。

Config ファイルのロードはテストしていないので省いたけれど、puma 本家のものを流用できるはず…

Windows に MinGW と mintty を入れてシェル環境改善


何をするにしてもシェルがしょぼいとやる気を削がれます。 私用の Windows をもうちょっと使いやすくしたい。

やりたいこと

  • Git を使いたい。
  • 普段は Unix 系の仕事が主なので(PowerShellではなく)Unix系がいい。
  • cmd.exe よりカッコイイ見た目がいい。

正直 Cygwin で満たせるけど、もっとシンプルにしたかったので MinGW & mintty を使ってみます。Git も後からいれる。

Cygwin のようにフル機能は使えなくても、起動の速い MinGW Shell で最低限の作業ができるだけでだいぶ良いのです。

こんな

透過できること、フォント設定できる、Ctrl +- で拡大縮小できればだいたい満足。

mingw-mintty

やったこと

MinGW Shell & mintty

ダウンロードしてインストール。

http://sourceforge.net/projects/mingw/files/Installer/mingw-get-inst/

最新2012ですか… mingw-get-inst-20120426.exe をインストールし MinGW Shell を起動して

$ mingw-get update
$ mingw-get upgrade
$ mingw-get install mintty

カンタン!ついでに ssh もこっちでインストールしとく。

$ minwg-get install msys-openssh

そして mintty を起動。

$ mintty.exe

起動したらタスクバーにピン止めする。ここまでの状態だと、シェルが sh で起動してしまうので プロパティで実行コマンド mintty.exe の後ろにハイフン-をつける。これでログインシェルとして起動する。

prop of shortcut

Git

Git は msysgit とかじゃなくて、 http://git-scm.com/downloads のものを使う。で、Git に PATH が通るようにインストール。

git path

この状態で、PATH はこんな感じになってるはず。

$ echo $PATH
.:/usr/local/bin:/mingw/bin:/bin:
(...中略...)
:/c/Program Files (x86)/Git/cmd:...

実行できるか確認。

$ which git
/c/Program Files (x86)/Git/cmd/git.exe
$ git --version
git version 1.8.3.msysgit.0

$ which ssh
/bin/ssh.exe
$ ssh -v
OpenSSH_5.4p1, OpenSSL 1.0.0 29 Mar 2010
...

よさげ!

ここまで下準備。さらにガッツリ作業したいとき用にLinux VMを併用するつもり。