Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

by mizchi
1 / 74

これは何

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


書き換えようとした理由

  • コードが多いので置き換えればごっそり消せる
  • 拡張の要望が多い
  • エディタなら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つのファイルにビルド
  • グローバル変数を使わずにモジュールの解決ができるようになる

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

結果

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


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

状況

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

テスト対象

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

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


テストを書く


書き換え

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

テスト

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%まで低下)
  • 下がる
  • テストを書く => カバレッジ上がる

テストの例

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


3 Reactに書き換え


Qiitaのヘッダ

fit


React化

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

flumpt

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

fit


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 がおすすめ


まとめ


まとめ


これから

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

おわり

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away