React on 現場 ~ あるいは Modern JavaScript on Rails ~

## これは何

[JSer.info 5周年記念イベント - connpass](http://jser.connpass.com/event/24202/ "JSer.info 5周年記念イベント - connpass") (2016/01/16) にて発表した資料。特に理由はないが公開するのを忘れていた。

スライドモードのリリースにあたって公開する

----

## 近況(2016/01/16)

- 昨年9月 Kobito for Windows => Qiita開発チーム
- モダンなJS(当社比)を導入しようとした

----------

## モダンなJSとは(mizchi主観2016版)

1. npm/browserifyで依存を解決
2. Babel/ES2015
3. React/Flux
4. Testable
5. No More **jQuery plugins**

※これらの基準について今回は割愛

------

## 現実(2015)

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

--------

## 初見での評価

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

--------

## 降ってきた仕事

=> なんかよくしてくれ

--------

# なんかよくする

--------

## なんかよく

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

----

## 最初の失敗

-----

# 失敗

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

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

-------

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

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

----------

# 破綻へ

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

----------

## 進捗

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

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

## 結果

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

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

## 教訓

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

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

## 振り出しに戻る

-----------

## 決意

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

-------

## ゴールの設定

- 新規モジュールを負債を引き継ぐことなく受け入れられる環境
- Turbolinks(PushState)が導入可能な初期化フローを作る(使うかはともかく)

--------

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

- React.Component提供おじさん

-------

## やらないこと

- CoffeeScriptのコードはそのまま(段階的に破棄する)
- Backboneのコードはそのまま(段階的に破棄する)

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

## 足元を見直す

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

## やったこと

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つのファイルにビルド
- グローバル変数を使わずにモジュールの解決ができるようになる

----------

2. そして 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
- たまに変なキャッシュ握って更新されない
  - なんか tmp/cache/browserify-rails 消すと動くぞ！
- ぶっちゃけwatchifyの方が速い
  - babelの初期化の差
- いろんなトレードオフ考えてアリ

-----------

## 結果

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

----------

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

---------

## 状況

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

----------

## テスト対象

- 書きなおして分解された再利用性のコード
- 新規に書かれる react component

重要: Backbone.View/Backbone.Router はテストしない

---------

## テストを書く

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

----------

## 書き換え

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

---------

## テスト

```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のコードを全部置き換える

----------

## カバレッジ

----------

## カバレッジ環境を作る

- isparta/mocha
- ES6 でテスト書ける(SourceMap)

----------

## npm run test-cov

```
$(npm bin)/babel-node $(npm bin)/isparta cover -x '**/vendor/*' --report text \
  node_modules/mocha/bin/_mocha -- --reporter dot -r \
  client/spec/spec-helper.js --timeout 10000 --recursive client/spec
```

長い

----------

## とにかくカバレッジを下げる

- 気合でモックする(jsdom使った)
- 読み込めるだけ読み込む(カバレッジ30%まで低下)
- 下がる
- テストを書く => カバレッジ上がる

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

## テストの例

```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に引っかかってしまうと再定義されない

-----------

## 3 Reactに書き換え

-----------

Qiitaのヘッダ

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

-----------

## React化

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

----------

## flumpt

- ヘッダみたいな独立した小さいコンポーネントで小さくFluxできるのが必要
- https://github.com/mizchi/flumpt 作った
- 本番でちょっとだけ使ってる

-----------

![fit](http://img3.wikia.nocookie.net/__cb20130219070742/darksouls/images/e/e7/Frampt02.jpg)

----------

## react-rails

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

-----------

## react-unit

- pzavolinsky/react-unit: Lightweight unit test library for ReactJS https://github.com/pzavolinsky/react-unit
- reactのテストユーティリティ
- 使い続けるか迷ってる
- ReactのshallowRendererのラッパー

-----

## 追記(2016/06/20)

今なら [airbnb/enzyme: JavaScript Testing utilities for React](https://github.com/airbnb/enzyme "airbnb/enzyme: JavaScript Testing utilities for React") がおすすめ

----------

## まとめ

--------


## まとめ

- 必要なのは「仕様理解」と「勇気」
- フロントエンドに秩序を取り戻す方法 // Speaker Deck https://speakerdeck.com/fand/hurontoendonizhi-xu-woqu-rili-sufang-fa とか勇気出た

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

## これから

Qiitaよくするんで待ってて

------

## 参考1

- babel-plugin-typecheck を使って flowtype 文法で書かれたJSをランタイムチェックする - Qiita
- Node.js - nodeのテストの前後で名前空間が拡張されてないか確認する - Qiita
- JavaScript - テストがないJS環境にモダンなテスト環境を導入していく - Qiita

---------

## 参考2

- node環境下で .jade ファイルを react-jadeとして読み込む - Qiita
- browserify - app/assets/javascripts以下のJSを全てcommonjsのrequireに書き換える - Qiita
- redux への 不満を解消する為に, flumptというFlux実装を作った - Qiita

------

## おわり