About
- @mizchi
- Qiita のフロントエンドエンジニア
- React勝手エヴァンジェリスト
- 4ヶ月で14kg痩せた (80kg => 66kg)
近況
- ビジネスドメインのリファクタが主でアウトプット少ない
- 泥臭系の知見は溜まってる
- 会社のメンバーが増えた + 自分以外のフロント系の人が増えたので、設計を明示しないといけない
この資料は何
- 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主観)
- npm/browserify or webpackで依存を解決
- Babel/ES2015
- React/Flux
- Testable
- No More jQuery plugins
- No Side Effect on module loaded
Qiita 2015
- CoffeeScript
- Sprockets / グローバル名前空間渡し
- Backbone
4. JSのテストはjasmineで数件 - jQuery plugins や jQuery UI まみれ
※ Railsのリクエストスペックは豊富
初見での評価
- Qiitaは2011末から開発されている
- 当時の選択基準としてはスジ悪ではない
-
十分にユーザーに価値を提供している(重要)
- 価値を生まないコードはメンテする価値が無い
- ただちょっとCTO(yuku_t)の手癖がちょっと強いかな…
降ってきた仕事
=> なんかよくしてくれ
なんかよくする
なんかよく
- モダンな開発環境を導入する
今のコードを洗練させる- 再利用するコードとできないコードを分別する
最初の失敗
失敗
- 編集画面を書き換えようとした
書き換えようとした理由
- コードが多いので置き換えれば古いコードをごっそり消せる
- 拡張の要望が多い
- エディタならKobitoの開発のノウハウを活かせる
破綻へ
- まずやっぱり分量が多い
- つつくと無限に知らない仕様が出てくる
- そもそもドメイン知識がなかった
- 判明する仕様を継ぎ接ぎするとコードが綺麗にならない…
進捗
- 「2週間ぐらいで終わらせるわ〜」
- 書き直したコードが読み易くならず辛い
- => 2ヶ月たっても終わらず
結果
- 一部のぞいてコードを破棄して中断
教訓
- 仕様を理解してないもののコードは書けない
- モジュールの境界面が明示されてないものは分解できない
- 「別実装で元の仕様を完全再現」はエネルギーの無駄
- 見積もりは失敗する
振り出しに戻る
決意
- エコシステムを整理しよう
- 現時点での負債と使える部分を認識しよう
ゴールの設定
- React/Babelで新規モジュールを受け入れられる環境
- Turbolinks(PushState)が導入可能な初期化フローを作る(使うかはともかく)
Rails上のフロントエンドエンジニアの設定
- React.Component提供おじさん
- history.pushState が導入できる状態を作る
- 運用するかは別
- コードのリファクタ目標として有用
足元を見直す
やったこと
- npmに依存ライブラリを集約
- Sprockets => browserify(-rails)
- ユニットテストの導入
- 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
# require_tree ./foo_dir
# require app
必ず asset_root(app/assets/javascripts) から解決
書き換える
書き換えたい…
- 分量が多い
- 平行して開発している機能がたくさんある
- ちんたらやってると無限にコンフリクトする
解法
- スクリプト書いて一発
- すべてを相対パスに書き換える
- browserify - app/assets/javascripts以下のJSを全てcommonjsのrequireに書き換える - Qiita http://qiita.com/mizchi/items/20f529a9d783552d7c7d
- なんかやる
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;
}
達成
- ファイル単位の依存は明示された
- グローバル変数依存なのは変わらず
Qiitaのモジュールシステム(旧)
- Sprocketsでファイル連結
- グローバル変数渡し
new Qiita.views.HomeView();
旧
//= require foo
Qiita.util.foo();
Qiita.util.bar = function(){...};
新
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
最初の書き換え
+ module.exports =
Qiita.util.foo = function() {...}
※ coffee が多いので commonjs
テスト
import foo from "../../src/util/foo";
describe("util", () => {
describe("foo", () => {
it("returns foo", () => {
assert.equal(foo(), "foo");
});
});
});
潰す
git grep 'Qiita.util.foo'
- 全部requireに書き換える
- Jasmineのコードを全部置き換える
テストの例
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は消す
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 一択
3 Reactに書き換え
ヘッダ
スライド機能(これ)
コメントフォーム
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でハッシュに変換するヘルパを用意
def comment_to_component_props(comment)
{ ... }
end
- Railsではこのメソッドをテストする
- 将来的にGraphQLを使う場合、たぶんここを置き換える
react-railsの次
今は代替手段がたくさん
あたりが気になってる
Flowtype
Flowtypeの導入
- 新規で書く部分はBabelで
- 古いモジュールも今後も開発する部分をBabelで
- Babelだったら段階的にFlow入れられるやん => 導入
Flowtypeの選定理由
- 段階的に導入していける
- React/JSX に対してかなり型が効く
- 推論が優秀
- フロントのテストしづらい部分をせめてでもカバー
参考: 型なき世界のためのflowtype入門 - Qiita
Typescript を選ばなかった理由
- トップレベルで全部のコードを扱えないと不便
- babel/coffeeと組わせるとgulpの複雑なタスクを組む必要
- (最近の開発方針がなんか気に食わない)
色々辛かった
今やってること
編集画面のReact化
最後に
- がんばっていきましょう