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

  • 91
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

About

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

近況

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

この資料は何


ふつうのウェブアプリ

  • 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
    1. JSのテストはjasmineで数件
  4. jQuery plugins や jQuery UI まみれ

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


初見での評価

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

降ってきた仕事

=> なんかよくしてくれ


なんかよくする


なんかよく

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

最初の失敗


失敗

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

inline


書き換えようとした理由

  • コードが多いので置き換えれば古いコードをごっそり消せる
  • 拡張の要望が多い
  • エディタなら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

# require_tree ./foo_dir
# require app

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



書き換える


書き換えたい…

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

解法


  • なんかやる
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


達成

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

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について

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

結果

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


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


状況

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

テスト対象

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

テストを書く


最初の書き換え

+ 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に書き換え


ヘッダ

fit


スライド機能(これ)


コメントフォーム


React化

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

react-dispatcher-decorator


なぜ 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化


最後に

  • がんばっていきましょう
  • この記事は以下の記事からリンクされています
  • React.js meetup #4からリンク