Real World React 2 - 2016/09/27 - React Meetup #4

## About

- @mizchi
- Qiita のフロントエンドエンジニア
- React勝手エヴァンジェリスト
- 4ヶ月で14kg痩せた (80kg => 66kg)

---

## 近況

- ビジネスドメインのリファクタが主でアウトプット少ない
- 泥臭系の知見は溜まってる
- 会社のメンバーが増えた + 自分以外のフロント系の人が増えたので、設計を明示しないといけない

---

## この資料は何

- [JSer.info 5周年記念イベント - connpass](http://jser.connpass.com/event/24202/ "JSer.info 5周年記念イベント - connpass") (2016/01/16) にて発表した資料を加筆したもの
- 如何にしてReactを「ふつうのウェブアプリ」に導入していくか

---

## ふつうのウェブアプリ

- **NOT** SPA
    - SPA でなくともReactは使える
    - jQuery に支配された現代のフロントエンドを改善したい

=> 複雑なモジュールを局所的に解決するのにReactが有用であることを示したい

----

## フロントエンドの流れ

- AJAX以前(~2003)
- jQuery時代(2004~)
- 構造化jQueryの時代(2011~)
    - Backbone
- データバインディングの興隆
    - Knockout
    - Angular 1/2
    - Vue
- 仮想DOMの時代
    - React

(次は仮想DOMの隠蔽が次に来そう)

-----

## Reactの難しさ再考

大きく2つ

- Reactを使うための前提となるエコシステム構築
- Reactの複雑化に対処のためのFlux概念

今回話すのは前者を倒す

----------

## Reactが乗る為のモダンなJSとは(mizchi主観)

1. npm/browserify or webpackで依存を解決
2. Babel/ES2015
3. React/Flux
4. Testable
5. No More **jQuery plugins**
6. No Side Effect on module loaded


------

## Qiita 2015

1. CoffeeScript
2. Sprockets / グローバル名前空間渡し
3. Backbone
    4. JSのテストはjasmineで数件 
5. jQuery plugins や jQuery UI まみれ

※ Railsのリクエストスペックは豊富

--------

## 初見での評価

- Qiitaは2011末から開発されている
  - 当時の選択基準としてはスジ悪ではない
- **十分にユーザーに価値を提供している**(重要)
  - 価値を生まないコードはメンテする価値が無い
- ただちょっとCTO(yuku_t)の手癖がちょっと強いかな…

--------

## 降ってきた仕事

=> なんかよくしてくれ

--------

# なんかよくする

--------

## なんかよく

- モダンな開発環境を導入する
- ~~今のコードを洗練させる~~
- 再利用するコードとできないコードを分別する

----

## 最初の失敗

-----

# 失敗

- 編集画面を書き換えようとした

![inline](https://i.gyazo.com/6b075f3f4e3f761be20816ec496fe483.png)

-------

# 書き換えようとした理由

- コードが多いので置き換えれば古いコードをごっそり消せる
- 拡張の要望が多い
- エディタならKobitoの開発のノウハウを活かせる

----------

# 破綻へ

- まずやっぱり分量が多い
- つつくと無限に知らない仕様が出てくる
  - そもそもドメイン知識がなかった
- 判明する仕様を継ぎ接ぎするとコードが綺麗にならない…

----------

## 進捗

- 「2週間ぐらいで終わらせるわ〜」
- 書き直したコードが読み易くならず辛い
- => 2ヶ月たっても終わらず

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

## 結果

- 一部のぞいてコードを破棄して中断

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

## 教訓

- 仕様を理解してないもののコードは書けない
- モジュールの境界面が明示されてないものは分解できない
- 「別実装で元の仕様を完全再現」はエネルギーの無駄
- **見積もりは失敗する**

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

## 振り出しに戻る

-----------

## 決意

- エコシステムを整理しよう
- 現時点での負債と使える部分を認識しよう

-------

## ゴールの設定

- React/Babelで新規モジュールを受け入れられる環境
- Turbolinks(PushState)が導入可能な初期化フローを作る(使うかはともかく)

--------

## Rails上のフロントエンドエンジニアの設定

- React.Component提供おじさん
- history.pushState が導入できる状態を作る
    - 運用するかは別
    - コードのリファクタ目標として有用

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

## 足元を見直す

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

## やったこと

1. npmに依存ライブラリを集約
2. Sprockets => browserify(-rails)
3. ユニットテストの導入
4. Reactでコンポーネントを置き換え

----------

## 1. npmに依存ライブラリを集約

----------

## npmに依存ライブラリを集約: 元の状態

- ライブラリごとに異なるCDNを参照
- オーバーヘッド大
- どのライブラリのどのバージョンを使ってるか見通しが悪い

----------

## npmに依存ライブラリを集約: 変更後

- npmとbowerでライブラリの依存を解決するようにした
- 1つのファイルに固めて自前の S3から CloudFront で配信
- 更にnpmに集約を進めてbowerを削除
- bowerやめろ

---------

## 2. Sprockets => browserify

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

## browserify導入

- commonjs形式で書かれたファイルを静的解析して1つのファイルにビルド
- グローバル変数を使わずにモジュールの解決ができるようになる

----------

## そして Sprockets を捨てる

----------

## Sprockets

- Rails上のデファクトの標準モジュールシステム
- 拡張子ごとに変換

-----------

## Sprocketsの問題

- ファイルスコープで返り値を持てない
- nodeで動かない
- rubyの問題とjsの問題を切り分けられない
- JSエコシステムに乗れない
- Sprocketsで動くJS系のgemメンテされない

-----------

`hoge.js.coffee.erb`

```coffee
# require_tree ./foo_dir
# require app
```

必ず asset_root(app/assets/javascripts) から解決


---------

![](https://i.gyazo.com/646bf1555f1af10998b03ddde8d7c66a.png)

----------

## 書き換える

---------

## 書き換えたい…

- 分量が多い
- 平行して開発している機能がたくさんある
- ちんたらやってると無限にコンフリクトする

----------

## 解法

- スクリプト書いて一発
- すべてを相対パスに書き換える
- browserify - app/assets/javascripts以下のJSを全てcommonjsのrequireに書き換える - Qiita http://qiita.com/mizchi/items/20f529a9d783552d7c7d

--------

- なんかやる

```js
function convertSprocketPathToCommonjsPath(root, fpath, spath) {
  if (/^\./.test(spath)) {
    return spath;
  }
  let relToRoot = path.relative(fpath, root);
  let rel = path.join(relToRoot, spath).replace(/^(\.\.\/)/, "");
  return rel.indexOf("..") > -1 ? rel : "./" + rel;
}
```

https://gist.github.com/mizchi/5ee5cd7d0ca9447b92d5

---------

![fit](https://i.gyazo.com/dd3b74cb97e4d7cb528d894ad2e11f74.png)


----------

## 達成

- ファイル単位の依存は明示された
- グローバル変数依存なのは変わらず

---------

## Qiitaのモジュールシステム(旧)

- Sprocketsでファイル連結
- グローバル変数渡し

```js
new Qiita.views.HomeView();
```
---------

旧

```js
//= require foo
Qiita.util.foo();
Qiita.util.bar = function(){...};
```

新

```js
const foo = require('./foo');
foo();
module.exports = function bar() {...}
```

-----------

## browserify によってもたらされるもの

- 依存がそれぞれのファイルで完結した状態
- 単体テスト可能な閉じた参照の提供
- 名前空間の初期化順に左右されなくなる

----------

## この過程でやったこと

-------

## gulp

- 「フロントが更新されたら npm install と gulp を叩いてください~」
- => しない
- 「動かないんだけど」
- => 対応で一日が終わる

-----

## Sprocketsを捨てる準備

- browserify-rails
- 参考: モダンJavaScript開発環境 on Rails - クックパッド開発者ブログ http://techlife.cookpad.com/entry/2015/12/14/130041

------

## browserify-railsについて

- 中で使ってるのはbrowserify-incremental
- ぶっちゃけwatchifyの方が速い
  - babelの初期化の差
- いろんなトレードオフ考えてアリ

-----------

## 結果

すべてを browserifyのtransform で解決した

----------

## 3. ユニットテストの導入

---------

## 状況

- browserifyによって各モジュールの依存が明示された
- jasmineが重いのでnodeでユニットテストしたい
- E2Eテストはまだ考えない

----------

## テスト対象

- モジュールで分解された再利用性のコード
- 新規に書かれる react component
- 依存が壊れてないか、読み込んだだけで副作用が発生しないか検知

---------

## テストを書く

- JavaScript - テストがないJS環境にモダンなテスト環境を導入していく - Qiita http://qiita.com/mizchi/items/bdf84f0b1c11c2870290
- node/mocha/isparta/jsdom

----------

## 最初の書き換え

```diff
+ module.exports =
Qiita.util.foo = function() {...}
```

※ coffee が多いので commonjs

---------

## テスト

```js
import foo from "../../src/util/foo";
describe("util", () => {
  describe("foo", () => {
    it("returns foo", () => {
      assert.equal(foo(), "foo");
    });
  });
});
```

---------

## 潰す

- `git grep 'Qiita.util.foo'`
- 全部requireに書き換える
- Jasmineのコードを全部置き換える

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

## テストの例

```js
  beforeEach(() => {
    deleteSrcCache();
    let jsdom = require('jsdom').jsdom;
    global.document = jsdom('<html><body></body></html>')
    global.window = document.defaultView;
    global.navigator = window.navigator;
    global.location = window.location;
  });

  afterEach(() => {
    delete global.document;
    delete global.window;
    delete global.navigator;
  });
```

-----------

## 毎回 require cacheは消す

```js
export function deleteSrcCache() {
  Object.keys(require.cache).map(cachePath => {
    if (/src/.test(cachePath)) {
      delete require.cache[cachePath];
    }
  });
}
```

グローバル変数に副作用があると require cacheに引っかかってしまうと再定義されない

---

## テストフレームワークの選定

- Mocha が RSpec に雰囲気似てるので書いてもらいやすい
- ava もいいけど独自概念が多くてめんどかった

---

## React のテスト

今は [airbnb/enzyme: JavaScript Testing utilities for React](https://github.com/airbnb/enzyme "airbnb/enzyme: JavaScript Testing utilities for React") 一択

-----------

## 3 Reactに書き換え

-----------

## ヘッダ

![fit](https://i.gyazo.com/fe9086d42e429dcf567ca2553328b745.png)

---------

## スライド機能(これ)

![](https://i.gyazo.com/9850fdc441643a12f08ddae58f261083.png)

----

## コメントフォーム

![](https://i.gyazo.com/ddeb66dee5291828422d40258bb1e2fc.png)

-----------

## React化

- Fluxの選定面倒だったのでベタ書きした
- react-rails の prerender: false で書き出し

----------

## react-dispatcher-decorator

- https://github.com/mizchi/react-dispatcher-decorator
- デコレータでevent-emitterのpubsubの関数をDI
- https://github.com/mizchi/flumpt から切り出した

---

## なぜ Redux ではないか

- 非SPAに統合的な管理機構が(たぶん)ワークしない
    - シングルストアにするのは困難
- 解決したい問題に対してコードが大げさに

---

## シングルストア設計を目指すには

- DOMに紐付かない仮想ルートオブジェクトと、画面に出現しうる要素の紐付を行うモジュールを自作する必要

----------

## Rails上のReact

-----

## react-rails

- Rails側で `= react_component("Header", {a: 1}, {prerender: false})`
- `<div data-props='<serialize化されたjson>'..>...</div>`
- Rails => JSのデータ受け渡しプロトコルがほしかった
- SSRはサーバーの負荷みて段階的に導入(したい)

----

## react-rails でのデータの渡し方

- ApplicationHelperでハッシュに変換するヘルパを用意

```ruby
def comment_to_component_props(comment)
  { ... }
end
```

- Railsではこのメソッドをテストする
- 将来的にGraphQLを使う場合、たぶんここを置き換える

-----------

## react-railsの次

今は代替手段がたくさん

- https://github.com/airbnb/hypernova
- https://github.com/alt-j/fast-react-server

あたりが気になってる

-------

## Flowtype

-------

## Flowtypeの導入

- 新規で書く部分はBabelで
- 古いモジュールも今後も開発する部分をBabelで
- Babelだったら段階的にFlow入れられるやん => 導入

-------

## Flowtypeの選定理由

- 段階的に導入していける
- React/JSX に対してかなり型が効く
- 推論が優秀
- フロントのテストしづらい部分をせめてでもカバー

参考: [型なき世界のためのflowtype入門 - Qiita](http://qiita.com/mizchi/items/95ee0101ac22e4b7b662 "型なき世界のためのflowtype入門 - Qiita")

----

## Typescript を選ばなかった理由

- トップレベルで全部のコードを扱えないと不便
    - babel/coffeeと組わせるとgulpの複雑なタスクを組む必要
- (最近の開発方針がなんか気に食わない)

--------

## 色々辛かった


-----

## 今やってること

![](https://i.gyazo.com/4569eb8465fa3a88b91641174190d120.png)

編集画面のReact化

--------

## 最後に

- がんばっていきましょう