これは何
JSer.info 5周年記念イベント - connpass (2016/01/16) にて発表した資料。特に理由はないが公開するのを忘れていた。
スライドモードのリリースにあたって公開する
近況(2016/01/16)
- 昨年9月 Kobito for Windows => Qiita開発チーム
- モダンなJS(当社比)を導入しようとした
モダンなJSとは(mizchi主観2016版)
- npm/browserifyで依存を解決
- Babel/ES2015
- React/Flux
- Testable
- No More jQuery plugins
※これらの基準について今回は割愛
現実(2015)
- CoffeeScript
- Sprockets / グローバル名前空間渡し
- Backbone
- JSのテストはjasmineで数件 (※request specは豊富)
- jQuery plugins や jQuery UI まみれ
初見での評価
- Qiitaは2012年から開発されている
- 当時の選択基準としてはスジ悪ではない
-
十分にユーザーに価値を提供している(重要)
- 価値を生まないコードはメンテする価値が無い
- ただちょっとCTO(yuku_t)の手癖がちょっと強いかな…
降ってきた仕事
=> なんかよくしてくれ
なんかよくする
なんかよく
- モダンな開発環境を導入する
今のコードを洗練させる- 再利用するコードとできないコードを分別する
最初の失敗
失敗
- 編集画面を書き換えようとした
書き換えようとした理由
- コードが多いので置き換えればごっそり消せる
- 拡張の要望が多い
- エディタならKobitoの開発のノウハウを活かせる
破綻へ
- まずやっぱり分量が多い
- つつくと無限に知らない仕様が出てくる
- そもそもドメイン知識がなかった
- 判明する仕様を継ぎ接ぎするとコードが綺麗にならない…
進捗
- 「2週間ぐらいで終わらせるわ〜」
- 書き直したコードが読み易くならず辛い
- => 2ヶ月たっても終わらず
結果
- 一部のぞいてコードを破棄して中断
教訓
- 仕様を理解してないもののコードは書けない
- モジュールの境界面が明示されてないものは分解できない
- 「別実装で元の仕様を完全再現」はエネルギーの無駄
- 見積もりは失敗する
振り出しに戻る
決意
- エコシステムを整理しよう
- 現時点での負債と使える部分を認識しよう
ゴールの設定
- 新規モジュールを負債を引き継ぐことなく受け入れられる環境
- Turbolinks(PushState)が導入可能な初期化フローを作る(使うかはともかく)
Rails上のフロントエンドエンジニアの設定
- React.Component提供おじさん
やらないこと
- CoffeeScriptのコードはそのまま(段階的に破棄する)
- Backboneのコードはそのまま(段階的に破棄する)
足元を見直す
やったこと
- 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
- たまに変なキャッシュ握って更新されない
- なんか tmp/cache/browserify-rails 消すと動くぞ!
- ぶっちゃけwatchifyの方が速い
- babelの初期化の差
- いろんなトレードオフ考えてアリ
結果
すべてを browserifyのtransform で解決した
- ユニットテストの導入
状況
- browserifyによって各モジュールの依存が明示された
- jasmineが重いのでnodeでユニットテストしたい
- E2Eテストはまだ考えない
テスト対象
- 書きなおして分解された再利用性のコード
- 新規に書かれる react component
重要: Backbone.View/Backbone.Router はテストしない
テストを書く
- JavaScript - テストがないJS環境にモダンなテスト環境を導入していく - Qiita http://qiita.com/mizchi/items/bdf84f0b1c11c2870290
- node/mocha/isparta/jsdom
書き換え
+ 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のヘッダ
React化
- Fluxの選定面倒だったのでベタ書きした
- react-rails の prerender: false で書き出し
flumpt
- ヘッダみたいな独立した小さいコンポーネントで小さくFluxできるのが必要
- https://github.com/mizchi/flumpt 作った
- 本番でちょっとだけ使ってる
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 がおすすめ
まとめ
まとめ
- 必要なのは「仕様理解」と「勇気」
- フロントエンドに秩序を取り戻す方法 // 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