読者です 読者をやめる 読者になる 読者になる

Make: Electronics ―作ってわかる電気と電子回路の基礎

book

Make: Electronics ―作ってわかる電気と電子回路の基礎 ((Make:PROJECTS))

Make: Electronics ―作ってわかる電気と電子回路の基礎 ((Make:PROJECTS))

入門用に電子回路ないし電子工作の分野の本を物色した結果、いつも大好きオライリーのこの本を購入しました。この種類の本は数多上梓されていますがこの本選んだ理由は、他がなんか貧乏くさい印象の紙面だったから。どうも電子工作分野の様々が全体的にチープ雰囲気漂ってる中で、紙面も装丁もきれいでいい感じだった。

読みながら手を動かすところあり、基本知識のコラムありで、もちろん内容も良いと思います。

Intel EditsonとRaspberry PI Camera

ガジェット

Intel Edison

f:id:masataka_k:20161121134937j:plain

やはり、小さいです。まずは標準アプリを母艦としたMBPに入れ初期設定しました。アプリでは、ファームアップデート・パスワード設定・WiFi設定の三つだけをやるもので、GUI操作だけで簡単にセットアップは終わります。動くところまで、Raspberry PI以上に超簡単。そこまでで週末に時間なかったので手をとめる。

Raspberry PI Camera

Amazon.com: Official Raspberry Pi 3 Case - Red/White: Computers & Accessories

Amazon.com: Raspberry Pi Camera Module V2 - 8 Megapixel,1080p: Computers & Accessories

f:id:masataka_k:20161121134718j:plain

Raspberry PIの方は、ケースとカメラを買ってみました。ケースは組み立てると完全に覆うものなのですが、壁の取り外しで基盤へのエントリもしやすくできてるなかなかの秀作です。そんな中、長女にねだられてRaspbianのフル版の方に入れ替え、モニタ・キーボード・マウスをつないでMineCraft PI Edition機になってしまいました。MineCraftはPI Editionだけプログラミング環境がくっついているらしく、Python操作できるらしい。いろいろWEBで調べながらコード書いて楽しんでます。カメラは。。。よって、つないで写真撮っただけで、通電もできず。

日本へ

明日から12月15日まで約3週間、日本出張です。Intel Edisonは週末用に持って行こうかと思います。Raspberryは占有されているので置いていかざるを得ません。

Ubuntu Core断念

ガジェット

Raspberry PIでサクッといっちゃったんで物足りずに、活用云々よりも先に違うLinuxディストロをいれてみようかと。で断念しました。

Snappy Ubuntu Core

Raspberry Pi 2/3 | Ubuntu developer portal

Raspberry PIの公式サイトでは公式の中の公式としてRaspbian(この名前はRaspberry + Debianなんでしょうけど、由来になんか違うものがあるんじゃないかと勘ぐってしまう)が掲げられていますけど、サードパーティWindowsなんかも載ってます。非デスクトップ用途で軽量に使えそうなLinuxということでは、Ubuntu Coreがありました。

これもダウンロードして、ddして、とRaspbianと同様なやり方でSDカードを用意します。。。がそこで問題発生。Ubuntu Coreは以下の理由からHeadlessで進まない。

  • デフォルトユーザーがなく、Ubuntu OneというクラウドサービスのIDが必要
  • そのUbuntu Oneと結びつけるためにはローカル実行、つまりディスプレイとキーボードをつなげてUbuntu Oneで登録したEメールアドレスを入力しないといけない
  • Ubuntu OneにID登録し、公開鍵登録し、ディスプレイとキーボードをつなげて作業をしたところ、どういうわけか認証がローカルもSSH越しもどちらも通らない
  • 試行錯誤するも通すことできず断念

セキュリティの都合なのか、クラウドサービスにユーザーを集めたかったからなのか、無いものは無いとしてしょうがないのですが、他のようにubuntu/ubuntuを用意しておいてくれてさえいれば困難もなかっただろうに。ということで、Raspbianに戻しました。

Raspberry PI ケースとカメラ

Raspberry PIは専用ケースをAmazon.comでポチって収納しました。赤白でかわいらしい。さらに基盤にリボンケーブルをつなぐ端子のよこに、CAMERAって書いてあったところから調べると、専用のカメラパーツがあったのでそれも。

Intel Edison

そして、なんか不完全燃焼な気持ちを鎮めるべく、Intel Edison with Breakout board Kitを新たにポチりました。金曜に届くそうなので次の週末に。ライトユーザーとしては選択余地がPI3の他になさそうなのですが、IoTというかWiFi+GPIO+Golangってことのみ要件としたときに、Raspberry PI3はHDMIやノーマルサイズのUSBx4やオーディオ出力に有線LANとそのまま一人前のPCとして使えるスペックとなってることが逆にロマンを欠いた気がしてます。はじめは長女きっかけでしたが、これはこれで教養としてひととおり修めてみようと調べてみれば、やっぱりブランドモノでIntelやっとくかな、最初に選ばなかったけどEdison触ってみたいなあ、そんな程度のことです。Edisonでは電池駆動を目指したい。

Raspberry PIを箱から出してGoアプリを動かす

ガジェット

Hello Raspberry!

8th gradeの長女(Mayaa)が最近いきなり電子工作に興味をもってブレッドボードなどを買ってきたので、対抗してRaspberry PIをはじめました。幸いにまだ臭いとかは言わないまでも、いずれ生ゴミ扱い仕掛けて来かねない思春期の娘に対して、父が一度はドヤ顔をする予定。

Raspberry PIを購入し、家にあるものかき集め

  • Raspberry PI 3 model B、今回唯一の購入品。Amazon.comで新品$36.91
  • micro SSD 4GBとSSDジャケット
  • iPad用アダプタ 12wとmini USBケーブル。2.4Aなので推奨2.5A以上というのを満たさないが問題ない
  • ルーターとLANケーブル、Comcastの引込み線が居間にあるうえ、Google OnHubなので有線ポートが一つしかないから設置条件が著しく制限されてるけど、一旦WiFi設定したら抜いて自室へ移動させるので問題ない
  • MacBook Pro。一世代前のものなのでSSDが直接挿せる
  • iPhone 5s。普段使いのスマホで、今回はOnHubを見るためのもの。

OS書き込み、起動

f:id:masataka_k:20161112111533p:plain

まずとにかくOSをダウンロードしてきて、SSDに書き込む。SSDが4GBしかないし、キーボードもモニターも余ってないのでヘッドレスで全部やることとして、GUIは諦めてRaspbianのJessie Liteの方にしました。MBPにジャケットを着せたmicro SSDを挿してOSインストールします。SSDはすでにFAT32でフォーマットしてあった。

Installing operating system images on Mac OS - Raspberry Pi Documentation

$ sudo diskutil unmountDisk /dev/disk2
$ sudo dd bs=1m if=~/2016-09-23-raspbian-jessie-lite.img of=/dev/rdisk2

SSDを抜き、Raspberry PI本体に挿す。LANケーブルとマイクロUSB電源ケーブルを接続すると、起動。

f:id:masataka_k:20161112112327j:plain

写真はインスタグラムにアップするために撮ったんだけど、実はこのときは書き込み失敗してたみたいでちゃんと上がってませんでした。書き込み成功すると勝手にマウントされるのでわかります。正常動作中は通電の赤LEDが常時点灯+動作状態表示の緑LEDが点滅します。

間違ってLinux PCでの作業ドキュメント読んでてddコマンドのパラメータでbs=4mとあったので1回目は間違ってそうしてましたがそれがいけなかったのか、それともフォーマットが悪かったのか。フォーマットしなおした後でbs=1mでの書き込みで成功しています。

SSHでアクセス

SSH using Linux or Mac OS - Raspberry Pi Documentation

iPhoneのOnHubアプリでIPを調べる。"raspberrypi"で見つかった。今回は192.168.86.107。

$ ssh pi@192.168.86.107
The authenticity of host '192.168.86.107 (192.168.86.107)' can't be established.
ECDSA key fingerprint is SHA256:C3eTyJeoxwm5/+PgxlEwFy7bQYUSHn4v3beRUGiWg8E.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.86.107' (ECDSA) to the list of known hosts.
pi@192.168.86.107's password: 

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

WiFiを起こす

Setting WiFi up via the command line - Raspberry Pi Documentation

pi@raspberrypi:~ $ sudo vi /etc/wpa_supplicant/wpa_supplicant.conf

あらかじめ $ sudo wpa_passphrase P807NETWORK password でつくったnetwork設定ブレースを末尾にコピペ。P807NETWORKはウチのSSIDで、米国にくる前に住んでたマンションの部屋番号由来です。pskはところどころxでつぶしてます。

デフォルトではcountryがGBだったのでUSに変更。Raspberry PIはたしか英国発でしたね。

# /etc/wpa_supplicant/wpa_supplicant.conf
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="P807NETWORK"
    psk=70a7fc3xx0aa1f72f8xfa58xxcd7aa833xxx6023e4xx1d621119cb9d3e20bd81
}

反映。

pi@raspberrypi:~ $ sudo ifdown wlan0
ifdown: interface wlan0 not configured
pi@raspberrypi:~ $ sudo ifup wlan0

LANケーブルはもう抜いちゃう。

確認。

OnHubアプリでもう一回IPを探すと、同じ"raspberrypi"で今回は192.168.86.113にみつかりました。sshで再接続。

pi@raspberrypi:~ $ ifconfig
eth0      Link encap:Ethernet  HWaddr b8:27:eb:44:d1:0b  
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:149 errors:0 dropped:2 overruns:0 frame:0
          TX packets:39 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:25148 (24.5 KiB)  TX bytes:6210 (6.0 KiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

wlan0     Link encap:Ethernet  HWaddr b8:27:eb:11:84:5e  
          inet addr:192.168.86.113  Bcast:192.168.86.255  Mask:255.255.255.0
          inet6 addr: fe80::e987:c595:7e09:9dae/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:2449 errors:0 dropped:25 overruns:0 frame:0
          TX packets:175 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:387234 (378.1 KiB)  TX bytes:24169 (23.6 KiB)

ストレージのほうも確認しておきます。。。が、何をどう見ればいいんでしょうか。。。うーんと、fdisk。

pi@raspberrypi:~ $ sudo fdisk -l
Device         Boot  Start     End Sectors  Size Id Type
/dev/mmcblk0p1        8192  137215  129024   63M  c W95 FAT32 (LBA)
/dev/mmcblk0p2      137216 7774207 7636992  3.7G 83 Linux

4GBのSDカードだから、おそらくいじる必要ないんじゃないかな。

netatalkでファイル共有する

ファイルをリモートで共有することで、ssh上で書き物するめんどくささを解消する。

pi@raspberrypi:~ $ sudo apt-get update
pi@raspberrypi:~ $ sudo apt-get install netatalk

あとはFinderで"移動" > "サーバへ接続..."のダイアログで、afp://192.168.86.113へ接続する。

f:id:masataka_k:20161112111440p:plain

時刻設定

/etc/ntp.conf を書き換え。http://www.pool.ntp.org/zone/us より、以下を書けといわれた。

# /etc/ntp.conf

(省略)

server 0.us.pool.ntp.org
server 1.us.pool.ntp.org
server 2.us.pool.ntp.org
server 3.us.pool.ntp.org

設定書き換えた後、ntpをリスタートする。

pi@raspberrypi:~ $ sudo service ntp restart

Go Raspberry!

まずは、Go SDKのディストロをダウンロードしてきて、MBPとRaspberry PIのファイル共有で転送します。ファイル大きくないし1個だからすぐ。

Downloads - The Go Programming Language

ファイルはarmv6lのフィックスが入ったファイル名のもの。最新1.7.3でもちゃんとありました。

pi@raspberrypi:~ $ sudo tar -C /usr/local/ -xzf ~/go1.7.3.linux-armv6l.tar.gz

展開はちょっと時間かかる。インストールは以上。

pi@raspberrypi:~ $ mkdir go
pi@raspberrypi:~ $ mkdir go/vendor
pi@raspberrypi:~ $ vi .bash_profile

Go関係の環境変数を設定します。

# .bash_profile
export PATH=/usr/local/go/bin:$PATH
export GOPATH=~/go/vendor:~/go

GOPATHはVendoring対応しときました。おもむろにgo getしてみようとしたら、gitがないって怒られた。

pi@raspberrypi:~ $ source .bash_profile
pi@raspberrypi:~ $ go version
go version go1.7.3 linux/arm
pi@raspberrypi:~ $ sudo apt-get install git
pi@raspberrypi:~ $ go get goji.io

予定通りきれいにgoji.ioのソース群がvendorフォルダの下に入ってくれました。

helloサーバー起動

Goji

goji.ioのトップにあるサンプルをそのままコピペしてgo runして。。。ブラウザから192.168.86.113:8000が見えない。接続が拒否された。。。sshで別に接続してローカルにcurlすると動く。。。あ、localhostの名前解決の問題か!

// ~/go/src/hello/hello.go
package main
import (
        "fmt"
        "net/http"
        "goji.io"
        "goji.io/pat"
)

func hello(w http.ResponseWriter, r *http.Request) {
        name := pat.Param(r, "name")
        fmt.Fprintf(w, "Hello, %s!", name)
}

func main() {
        mux := goji.NewMux()
        mux.HandleFunc(pat.Get("/hello/:name"), hello)
        http.ListenAndServe(":8000", mux)
}

上記のようにGoソースを書き換えて、ビルド。

pi@raspberrypi:~ $ go build hello
pi@raspberrypi:~ $ ./hello&
[1] 15120

最初のgoji.ioサンプルはhttp.ListenAndServeの引数で"localhost:8000"と指定しているからダメでした。上記のように":8000"とポート番号だけにしておけばOK。ブラウザからhttp://192.168.86.113:8000/hello/masatakaをたたくと、ちゃんとでてきます。

GPIO, PMW

GPIO: Models A+, B+, Raspberry Pi 2 B and Raspberry Pi 3 B - Raspberry Pi Documentation

Raspberry Pi with Gobot

GitHub - sarfata/pi-blaster: PWM on the Raspberry pi - done properly (in hardware, stable)

ブレッドボードとつなぐのはGPIOというもの利用するんですね。汎用目的入出力。ブレッドボードのキットの方に繫ぐための線がついていました。ドキュメントにLED光らせるのがあるので、始めるならここからかな。そして抽象化したライブラリのGobotと、GobotのインフラとしてのPI-Blasterでコントロールする。。。

pi@raspberrypi:~ $ sudo apt-get install autoconf
pi@raspberrypi:~ $ git clone https://github.com/sarfata/pi-blaster.git
pi@raspberrypi:~ $ cd pi-blaster
pi@raspberrypi:~/pi-blaster$ ./autogen.sh 
pi@raspberrypi:~/pi-blaster$ ./configure 
pi@raspberrypi:~/pi-blaster$ make
pi@raspberrypi:~/pi-blaster$ sudo make install

PI-Blasterのドキュメントにあるとおりに$ sudo apt-get install pi-blaster でインストールしてみようとしたら、一時的なのかわかりませんが"E: Unable to locate package pi-blaster"とエラー出しましたので、githubからクローンしてRaspberry上でmakeする方法としています。ドキュメントのExampleから、PI-Blasterは標準入力でPINに命令を送る仕組みと理解しました。そんなこんなで知らない単語として「PWM」というのが出てきたので調べると。。。

PWM(Pulse Width Modulation)とは、半導体を使った電力を制御する方式の1つです。オンとオフの繰り返しスイッチングを行い、出力される電力を制御します。

引用:PWMとは | 東芝 ストレージ&デバイスソリューション社

。。。気をとりなおして、Gobotをgo get。

pi@raspberrypi:~/pi-blaster$ cd ~
pi@raspberrypi:~$ go get -d -u github.com/hybridgroup/gobot/...
pi@raspberrypi:~$ go install github.com/hybridgroup/gobot/platforms/raspi

LEDチカチカ

// ~/go/src/blinker/blinker.go
package main
import (
        "time"
        "github.com/hybridgroup/gobot"
        "github.com/hybridgroup/gobot/platforms/gpio"
        "github.com/hybridgroup/gobot/platforms/raspi"
)

func main() {
        gbot := gobot.NewGobot()
        r := raspi.NewRaspiAdaptor("raspi")
        led := gpio.NewLedDriver(r, "led", "7")
        work := func() {
                gobot.Every(1*time.Second, func() {
                        led.Toggle()
                })
        }
        robot := gobot.NewRobot("blinkBot",
                []gobot.Connection{r},
                []gobot.Device{led},
                work,
        )
        gbot.AddRobot(robot)
        gbot.Start()
}

こちらのGobotページにあったサンプルをそのまま流し込みました。そしてビルド&実行。

pi@raspberrypi:~$ mkdir ~/go/src/blinker
pi@raspberrypi:~$ vi ~/go/src/blinker/blinker.go
pi@raspberrypi:~$ go build blinker
pi@raspberrypi:~$ ./blinker 
2016/11/12 01:41:39 Initializing Robot blinkBot ...
2016/11/12 01:41:39 Initializing connections...
2016/11/12 01:41:39 Initializing connection raspi ...
2016/11/12 01:41:39 Initializing devices...
2016/11/12 01:41:39 Initializing device led ...
2016/11/12 01:41:39 Starting Robot blinkBot ...
2016/11/12 01:41:39 Starting connections...
2016/11/12 01:41:39 Starting connection raspi...
2016/11/12 01:41:39 Starting devices...
2016/11/12 01:41:39 Starting device led on pin 7...
2016/11/12 01:41:39 Starting work...
^C2016/11/12 01:42:27 Stopping Robot blinkBot ...

Crrl+Cで止めました。きちんと動いているっぽいけど、LEDを接続まだしていないので見かけ変わらない。。。ここで長女登場でブレッドボードを組んでもらいました。

f:id:masataka_k:20161113192539j:plain

サンプルコードで、NewLedDriverの引数に渡されている「7」が物理pin番号の7。GPIOの7番ではない。この7番pinにプラス、39番pin(一番外側のground)にマイナスで、組んだ回路につなげて実行したら、プログラムどおりに1秒毎にトグルされてブレッドボードに刺したLEDがゆっくりとチカチカしました!

このとき注意ポイントは実行時にはsudoで実行すること。最初は知らずにしばらく沈黙してました。GPIO操作はルート権限。まあ、そうでしょうね。

電源オフ

pi@raspberrypi:~ $ sudo shutdown -h now

おー、久しぶりにこのコマンド叩いたよ!クラウド時代には絶対唱えないshutdown。しばらく基盤の緑LEDがチカチカしたのちに基盤の赤LEDのみ点灯となります。電源ケーブル抜いて、おしまい。

感想

想像以上に整備されちゃってて普通のLinux&普通のGoの世界まで速攻で来れちゃいました。今回程度のことなら容量もパワーも十分にあったのでRaspberry PIの上にSDKいれてgo getからアプリケーションビルドまでしましたが、クロスコンパイルする方が筋がよいかもしれません。

モニター&キーボードをつながないなら、Intel Edisonのほうが攻めてる感じになったかなと考えましたが、Raspberry PIのほうが価格が安く、それゆえかケースなどの周辺も充実してるんでOK。

macOS Sierra上でGAE/Go SDKが動くようになった

Golang

https://storage.googleapis.com/appengine-sdks/featured/go_appengine_sdk_darwin_amd64-1.9.46.zip

gcloudツールからのコンポーネント管理では現時点アップデートされていませんでしたが、GAE/Go単体SDKはGo1.6.3ベースにアップデートされていました。

再度GAEで動くように環境変数を設定しなおして、GAEお休みのために一時避難的に書いてたmain.main()がAppEngine SDKが補うものとぶつかってコンパイルエラーになったのをコメントアウトしたら、それまでのようにいきなりクラッシュするようなことなく、元気に動いてます。

ディスク容量を圧迫しているので、ちょっと様子みてからにしますが仮想El Capitan環境は削除しようと思う。

reduxとreact-routerの間でURLとstateの同期を行う

JavaScript

reactのルーティング機能?というかテンプレートシステムというか?にreact-routerがあります。リクエストパスに応じて画面構成要素を組み立てるのでreact-routerを使ってアプリケーションを作ると当然URLと画面は同期することができます。URLを直打ちしてもコードでURL遷移させても、適切な画面に切り替わるように作れるのです。

reduxはその際にどうかというと、URLや画面構成といったものについて素で働きかけるものではないので、URLが変化してもreduxのステートマシンにはその情報は反映されません。そこにはもう一つ上乗せするものが必要です。いくつか同じ守備領域のライブラリがあるようですが、わたしはここのところしばらく、中でも主流感のあるreact-router-reduxを用いています。

GitHub - reactjs/react-router-redux: Ruthlessly simple bindings to keep react-router and redux in sync

render、Route、Store

react-router-reduxはURLをreact-routerのURL管理の仕組みHistoryをフックすることでURL変化を取得します。

import React from 'react';
import { render } from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory, IndexRedirect } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

import { BookList, BookReader, Login, NotFound, Navigation, Theme } from './containers';
import app from './reducers/AppReducer';
import routing from './reducers/RoutingReducer';

// routingという名前はreact-router-reduxの仕様として固定
const store = createStore(combineReducers({ app, routing }));

render(
    <Provider store={store}>
        <Router history={syncHistoryWithStore(browserHistory, store)} >
            <Route path="/" component={Theme}>
                <IndexRedirect to="book" />
                <Route path="book" component={Navigation}>
                    <IndexRedirect to="list" />
                    <Route path="list" component={BookList} />
                    <Route path=":title" component={BookReader} />
                </Route>
                <Route path="*" component={NotFound} />
            </Route>
        </Router>
    </Provider>,
    window.document.getElementById('application')
);

syncHistoryWithStore(browserHistory, store) でHistoryとreduxのstoreを結びつけていますが、このstoreの中で同期のコードを書くことになります。その前にまず、画面遷移をするためのAction Creatorは以下のようにしました。

// /actions/index.js
import { createActions } from 'redux-actions';

export default createActions(
    {
        MOVE_TO: (title, page = 0) => {
            const finalPage = page < 0 ? 0 : page;
            return { title, page: finalPage };
        },
    },
);

この’MOVE_TO'/moveToは文字列のtitleと数字のpageを設定するものです。サンプルとしてページがマイナスにならないようにだけチェックしてます。redux-actionsの書き方として、昨日の記事のように名前だけでAction Creatorを用意する方法の他、このようにaction.payloadに搭載する内容をコードで調整することも可能です。

アプリケーションのReducer

// /reducers/AppReducer.js
import { handleActions } from 'redux-actions';
import { LOCATION_CHANGE } from 'react-router-redux';

 // DANGER: It's an internal API of react-router!
import { matchPattern } from 'react-router/lib/PatternUtils';

import actions from '../actions';

// URL -> History -> Locationの情報からtitleとpageを抜き出す
function decodeTitleAndPage({ pathname, query }) {
    const m = matchPattern('/book/:title', pathname);
    if (!m) {
        return { title: null, page: 0 };
    }
    const { paramNames, paramValues } = m;
    let title = null;
    for (let i = 0; i < paramNames.length; i += 1) {
        if (paramNames[i] === 'title') {
            title = decodeURI(paramValues[i]);
            break;
        }
    }
    const page = (query.page && Number(query.page)) || 0;
    // titleとpageを返している!
    return { title, page };
}

export default handleActions({
    // こちらはアプリケーションの基本的なReducer。なにはなくともこれは書くことでしょう
    [actions.moveTo]: (state, { payload: { title, page } }) => ({
        ...state,
        title,
        page,
    }),
    // こちらがreact-router-reduxの作法として実装すべき内容。LOCATION_CHANGEをappとして監視する。
    [LOCATION_CHANGE]: (state, { payload }) => ({
        ...state,
        ...decodeTitleAndPage(payload),
    }),
}, { /* default state here */ });

まずアプリケーション側のReducerですが、こちらはreact-router-reduxの定義する"LOCATION_CHANGE" Actionを処理するものを作ります。このActionはpayloadにURL由来のLocationを積んでるので、そこからリクエストされているパスやクエリの内容を取り出します。取り出しているのは、上記のdecodeTitleAndPage()と書いてあるところです。

こちらでは手抜きでreact-routerの内部APIを使ってます。パスを分解して当たり判定するコードを書くのが大変なのでreact-routerで用いられているmatchPattern関数を引っ張ってきました。この関数を用いてreact-routerの仕様で書かれたルートパラメータであるtitleを取り出すことができました。取り出した値はアプリケーションのstateに保持されます。このことによってURLからRedux Storeへの方向の同期ができるようになりました。

ルーティングのためのReducer

// reducers/RoutingReducer.js
import { handleActions } from 'redux-actions';
import { LOCATION_CHANGE } from 'react-router-redux';
import actions from '../actions';

// titleとpageからLocationオブジェクトを作る
function encodeTitleAndPage({ locationBeforeTransitions }, { title, page }) {
    const pathname = `/book/${encodeURI(title)}`;
    return {
        ...locationBeforeTransitions,
        pathname,
        query: { page: page.toString() },
        search: `?page=${page}`,
        action: 'PUSH',
    };
}

export default handleActions({
    // こちらがreact-router-reduxの作法として実装すべき内容。moveToをroutingとして監視する。
    [actions.moveTo]: (state, { payload }) => ({
        ...state,
        locationBeforeTransitions: encodeTitleAndPage(state, payload),
    }),
    // こちらはreact-router-reduxのお約束。
    [LOCATION_CHANGE]: (state, { payload }) => ({
        ...state,
        locationBeforeTransitions: payload,
    }),
}, { locationBeforeTransitions: null });

もうひとつ、逆にRedux StoreからURLへの方向の同期を行うためのものです。このReducerは”routing”の名前でStoreに登録されるべきもので、冒頭にでてきてStoreとHistoryを結びつけたsyncHistoryWithStoreから参照されるものになります。stateのrouting.locationBeforeTransitionsが変化すると、HistoryのAPIを呼び出してURL遷移します。URLの変化によってsyncHistoryWithStoreが投げるLOCATION_CHANGEに対応するものに加えて、アプリケーションコードからtitleとpageを指定することで期待する画面遷移を、moveToのReducerで書きます。これは例でencodeTitleAndPageとした関数で書いていて、moveToのpayloadに積まれているtitleとpageをLocationオブジェクトに反映しています。

  • Location
    • pathname: 遷移先パス
    • query: クエリパラメータ
    • search: 遷移先URLに追加される、クエリパラメータを含んだ文字列。
    • action: PUSHいれておけばOK。ちょっと他のケースが思いつかない。

冗長に思うけどqueryと両方設定してあげないとダメな作りだった。注意されるべし。

combineReducersで分割管理されたReducer群の挙動

この一連の作りを掘った際に深く理解できたのは、ReduxのcombineReducersでまとめたReducerの動きであり、一度Actionが投げつけられれば、分割されたすべてのReducerにActionがバインドされるってことです。アプリケーションとルーティングのための二つのReducerとしてappとroutingが同じstateの中に名前空間をつくってますが、appにもroutingにもmoveToが同時に発火し、LOCATION_CHANGEも同時に発火する。だからこの例のような作りができる。

感想

これで、URLを直で入力されてもステートマシンに適切に反映され、アプリケーションでreduxのactionをバインドするだけでURL遷移させることができるようになりました。これは結果としてブラウザの戻るボタンや進むボタンの対応も完全におこなわれたということであり、それがreact-router-reduxのサイトの説明にある謎の言葉「time travel」の説明となります。

You want to do time travel with your application state, but React Router doesn't navigate between pages when you replay actions. It controls an important part of application state: the URL.

(https://github.com/reactjs/react-router-redux より引用)

初見では何言ってんのかわかりませんでしたが、なるほど納得。この文はステートマシン->URLのことしか言ってないように読めますけど、説明したように逆も大事。react-router-reduxの標準のサンプルにはURL -> ステートマシンのことが書かれてなかったので見はじめてしばらくはダメダメなライブラリと思ってました。その後に全くの別件でcombineReducersのつかいこなし例を見たときにはじめてreact-router-reduxの思想に思い至り、ビビビと痺れた。routing Reducerは標準で用意されているものではなく、自分で書かないと真価が発揮されないものなのだと。つまりおしいかな、ライブラリで用意されたドキュメントとサンプルは説明が足りない。

備忘録

routing.locationBeforeTransitions がお約束の言葉。

reduxはredux-actionsギプスをつけて養成

JavaScript

reactアプリケーション開発においてコンポーネントに引数として渡されるpropsと、スコープの大きな実行時変数としてのcontextに加えて、もっぱらコンポーネント内部の状態管理としてのstateをあれこれ操作するのがデータフローの基本だったのですが、ここ一年ぐらいで様子が一気に変わってしまいました。reactの普及と対象とするアプリケーション規模が大きくなることによる複雑性の課題に応えてFluxが提唱され、その実装バリエーションで淘汰を経たreduxの登場です。それまで標準であったコンポーネントstate管理によって多数散在してしまった複数ステートマシンの連携を否定して、アプリケーションレベルで一つの大きなステートマシンをつくってpropsを通じてのみコンポーネントツリーに反映させるという。たくさんあるものをn:nに対応させていくってのは複雑を、常に抽象から具体へ1:nになる流れに修正すると一気にシンプルになり、わかりやすくなりました。

redux-actions

そんなreduxはこれまたライブラリとしてみると極薄で、実装の際に自由度がとても高い。これはテストや設計などでメリットとなる一方で、どう書いてもいいってのは、そもそもに開発者に悩みを与え、構成としては将来に技術的負債を生じさせてしまうリスクを持ってます。

GitHub - acdlite/redux-actions: Flux Standard Action utilities for Redux.

redux-actionsはその説明の冒頭にあるように、FSA(https://github.com/acdlite/flux-standard-action, =Flux Standard Action)として引いたガイドラインのライブラリです。reduxでマジにどうやったっていいactionCreatorの仕様と実装に堅めのガイドラインをFSAが引き、redux-actionsが実装の手助けをしてくれる。。。って今きがついたのですが、作者いっしょじゃん。なるほどねー。FSAの冒頭にあるように「A human-friendly standard for Flux action objects. 」なのはそのとおり。すでにreduxエコシステム界もこのFSAをスタイル標準として反応している様子なので、逆らわずに巻かれていいとおもう。

// /actions/index.js
import { createActions } from 'redux-actions';

export default createActions(
    'REQUEST_LIST',    // -> actionCreatorの識別子はrequestListと自動的に名付けられる
    'RECEIVE_LIST',      // -> action.typeは'RECEIVE_LIST'
);

ActionCreatorはこれでOK。それぞれの名前に応じて、引数をそのままaction.payloadに積むactionCreatorが用意されます。ドキュメントにも書かれてるけど、createActionsではなくて本当はcreateActionCreatorsとでもするべきなんでしょう。でもcreateActionsです。プレーンオブジェクトに用意したactionCreatorを詰め込んだものを返してくれますので、そのままexport defaultしました。

対するreducerもまっさらにswitch文を書く代わりにredux-actionsの支援を用いて書けます。

// /reducers/AppReducer.js
import { handleActions } from 'redux-actions';
import actions from '../actions';

export default handleActions({
    [actions.requestList]: state => ({
        ...state,
        fetch: true,
    }),
    [actions.receiveList]: {
        // 正常時
        next: (state, { payload }) => ({
            ...state,
            list: payload,
            errorMessage: null,
            fetch: false,
        }),
        // 例外時
        throw: (state, { payload }) => ({
            ...state,
            list: null,
            errorMessage: payload.message,
            fetch: false,
        }),
    },
}, { /* default state here */ });

こちらは間違いなくhandleActionsで名も実もいいですね。import actions from '../actions';から [actions.requestList]:としてるところは、createActionsで用意されたActionCreatorがtoString()でAction Typeを返してくれる実装をしてるからです。気がきいてます。handleActionsの馬鹿でかくなってしまう第一引数に続いて、第二引数に空オブジェクト(default state hereのところ)をおいてますが、これはstateの初期値設定です。このオブジェクトを省略するとヌルポが起きるので注意。

外のAPIを叩いてもどってくる作りのときには、FSAがActionCreatorに渡された引数を常にpayloadに積むことと、例外通知にはpayloadにエラーを突っ込んだ上で、action.errorをtrueにするということが効いてきます。reducerの実装が関数のときは正常時も例外時も関係なくそのまま実行するし、nextとthrowという名前のメソッドを持つオブジェクトを設定すれば、上の例のreceiveListがそうですが、正常時はnextを実行し例外時にはthrowを実行しわけるreducerを作れる。小さな規約でシンプルかつ堅い。

reduxでmiddleware書くなら、FSA前提でつくっちゃったほうが悩みが少ないです。たとえば以下のCookieと値をコピーするMiddlewareでは、action.metaというもう一つの規約をもちいて処理のきっかけをもたせた上で、値は常にaction.payloadに乗ってるという前提あっての実装です。そうじゃないとどうくるかわからないactionに対応するためにもっと複雑な作りになっちゃう。

import Cookies from 'browser-cookies';

export default function (options = { expires: 1 }) {
    return (/* store */) => next => (action) => {
        const name = action.meta && action.meta.cookie;
        if (name && !action.error) {
            if (action.payload) {
                Cookies.set(name, JSON.stringify(action.payload), options);
            } else {
                const value = Cookies.get(name);
                if (value) {
                    try {
                        next({ ...action, payload: JSON.parse(value) });
                    } catch (e) {
                        next({ ...action, payload: e, error: true });
                    }
                    return;
                }
            }
        }
        next(action);
    };
}

babel

ところで、ESの様々な新文法つかうとreduxって気持ちいいんですが、トランスパイルするためにはbabelで設定が必要。

  "babel": {
    "plugins": [
      "transform-decorators-legacy"
    ],
    "presets": [
      "es2015",
      "react",
      "stage-1"
    ]
  },

私はpackage.jsonでこのようなbabelの設定しています。presetsのstage-1がないと、reducerでstateを「...」を用いて展開するのができない。これができるとできないではコードに大きな違いが生じます。transform-decorators-legacyは今回とは別で、redux-formの私の書き癖のために登録しています。

ちなみにESLintは以下のように。WebStormの標準の体裁にあわせたほかは、airbnbです。去年書いてた時にはairbnbはどうにも馴染まないルールが多くて無理ゲーだったんだけど、いま書いてみると特に違和感ない。airbnbが変わったのか、私が変わったのか、どちらかは不明です。airbnbはreactでstateを直接設定するのを嫌っており、reduxが標準採用されてる予感です。

  "eslintConfig": {
    "extends": "airbnb",
    "env": {
      "browser": true,
      "jasmine": true
    },
    "parser": "babel-eslint",
    "rules": {
      "indent": [
        "error",
        4,
        {
          "SwitchCase": 1
        }
      ],
      "react/jsx-indent": [
        "error",
        4
      ],
      "react/jsx-indent-props": [
        "error",
        4
      ],
      "react/jsx-filename-extension": "off",
      "max-len": [
        "error",
        120
      ]
    }
  },

react/jsx-filename-extensionはオフにしないとimport文がうざいことになる。なんでこれオンになってるんだろう?遠回しにJSXを直接書くなってことなのかな?