4月からForSchool事業部の石上です。以前は、主にStudyplusのウェブ版を担当していました。現在ForSchool事業部では、Studyplus for Schoolというプロダクトをエンジニア2人で開発しています。2人ともサーバーサイドとフロントエンドの両方を担当しています。
今回、Studyplus for Schoolのフロントエンド周りに必要なツールを入れるなどして、フロントエンドのコードをふつうに書いていくための下準備をしました。
背景
Studyplus for SchoolはRailsで作られたアプリケーションです。極力Railsのレールに乗るように作られています。
フロントエンドにはWebpacker、Babel、一部にReactをすでに利用していました。 多くの部分にはRailsのビューと、自前のベースJSコンポーネントを継承したコンポーネントが設置されています。
今後、アプリケーションのReact化と適切なコンポーネント分けを進めるために、下準備として型とLintとテストの導入に取り組みました。
私自身作業が遅いという問題はありますが、やはりこの辺のboilerplateを作ったことがない人であれば調べごとが多くなるのは避けられません。2日くらいでぺろっとやろうというよりは1週間以上はかけてやったほうがいいと思います。もしくは、一気に整えようとせずに徐々に環境を整えていくなりしたほうが良いです。今回は、ちゃんと時間をいただけて良かったです。
型
型があることで、
- コンポーネントのpropsとstateが見やすくなる
- エディタの補完を使えるようになる
などのメリットがあります。
JSに型を入れるツールとしては、TypeScriptとFlowが有名です。 今回はこの2つを比較して、結果としてTypeScriptを選択しました。
どちらもシンタックス的にそれほど大きな違いはありませんでしたが、使用方法や周辺環境に以下のような違いがありました。
Flowは静的型チェッカー、TypeScriptは言語
いずれもソースコードに型を書いていくのでJSへの変換が必要ですが、エラーの確認方法は違います。
Flowの場合、拡張子は.js
のままファイルの行頭に// @flow
と書くことで型チェック対象とします。トランスパイル時のエラーにはなりません。型チェックするときはLintを走らせるように、flow
コマンドを実行すればエラーが見れます。
一方、TypeScriptはJSのスーパーセットとはいえ、別言語です。拡張子は.ts
にする慣習があり、ビルド時にコンパイルエラーが出ます。
DefinitelyTyped vs flow-typed
TypeScriptでコードを書いていて、なるべく外部ライブラリの型定義で困りたくはありません。なので、外部ライブラリの型定義ファイルを見つけられる可能性の高い方がいいです。
量だけで見ると、TypeScriptの型定義ファイルの置き場所であるDefinitelyTypedの方が多いです。今の所TypeScriptを使ったほうが困ることは少ないと推測しました。
なお、Studyplus for Schoolではreact-on-rails
というライブラリを使っていて、こちらの型定義ファイルは存在しなかったので自前で書く必要がありました。他にも、いくつかの型定義ファイルは自分たちで書く必要が出てくるかもしれません。
Lint
TypeScriptのLintには、tslint が使えます。
tslint.json
にtslint:recommended
を指定すると、TSLintのおすすめのルールが設定されます。
また、React用には、tslint-react というパッケージがtslintを開発しているpalantir社から公開されているので、それがそのまま使えます。
中身を見てみると、Reactのアンチパターン的な書き方は大体含まれているようです。
今回は tslint:recommended
、tslint-react
をベースに、カスタマイズしていく形にしました。
{ "extends": ["tslint:recommended", "tslint-react"], "rules": { "arrow-parens": false, ... } }
Lintがアプリケーションの改善につながるところ
Lint設定するにあたって、コードの書き方を定めて無駄な迷いをなくして生産性向上しようという意図がありました。Reactアプリケーションの場合はこれに加えて、書き方によってパフォーマンスに影響が出るようなところを見つけて改善することにも役立ちました。
たとえば以下のようなものです。
jsx-no-bind
Binds are forbidden in JSX attributes due to their rendering performance impact
このルールはtslint-reactのv2.6.0からのもので、以下のような書き方を禁止しています。
export Parent extends React.Component<Props, State> { onClickItem() { // ... } render() { return <Child onClick={this.onClickItem.bind(this)}> } }
render時に毎回新しい関数をつくることになるので、パフォーマンスに影響があります。以下のように直したほうが良く、そうすることでLintも通るようになります。
export Parent extends React.Component<Props, State> { - onClickItem() { /*...*/ } + onClickItem = () => { /*...*/ } render() { - return <Child onClick={this.onClickItem.bind(this)}> + return <Child onClick={this.onClickItem}> } }
Reactのドキュメントにも、注釈に書かれています。
Using Function.prototype.bind in render creates a new function each time the component renders, which may have performance implications (see below).
テスト
今回、Reactコンポーネントに対してユニットテストが行えるところまでを設定しておきました。
Reactコンポーネントでやりたいことは、ユーザーのアクションやイベントを受け取って状態を変えて、見た目に反映することです。なので、
- イベントに対して意図した通りに状態が変わっていること
- 入力に対して正しい見た目を出力していること
をテストすれば十分ではないでしょうか。これができれば、ライブラリは何でもいいと思います。
テストには、基本的なテストライブラリに加えて、Reactのコンポーネントを扱うためのライブラリが必要です。今回は、同様のライブラリの中でも設定が楽そうなJestとEnzyme を選択しました。
Jest
Jestは、Facebookによって開発されている「ゼロ・コンフィギュレーションのテストプラットフォーム」です。楽に設定できるのが特徴なので、ここには実際どんな設定が必要だったのかを簡単に書いておきます。
インストール
$ yarn add -D jest @types/jest ts-jest
JSのソースをテストする分にはjest
さえ入れればテストを書き始められます。今回はTypeScriptなので、jestの型定義ファイルと、プリプロセッサのts-jest
を入れました。
TypeScript固有の設定については、ts-jest
のREADMEの通りにすれば大丈夫です。Jestの設定をpackage.json
に書き足します。
{ "scripts": { "test": "jest" }, "jest": { "transform": { "^.+\\.tsx?$": "ts-jest" }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", "moduleFileExtensions": [ "ts", "tsx", "js", "jsx", "json", "node" ] } }
これでテストが書けるようになりました。
Enzyme
Enzymeは、テストコードでReactコンポーネントを扱うために入れました。
インストール
$ yarn add -D enzyme enzyme-adapter-react-16
shallow
か mount
でコンポーネントを初期化して、テストで行いたい操作ができます。
shallow
では浅いレンダリングが行われます。最終的に出力されるDOM要素をすべて確認することはできませんが、そのコンポーネントの子のコンポーネントまでは確認することができます。
mount
では子以下のコンポーネントも含めてすべてrenderするので、DOM要素を確認することができます。
ユニットテストとしてはなるべくmountは使わず、shallowを使う意識でテストを書いたほうが良いと思います。親のコンポーネントが子のコンポーネントが管理しているDOMを確認するより、子のコンポーネントのテストとして書いた方が、関心を分離できるからです。
// よい const wrapper = shallow(<MyParentComponent hoge={hoge} />) expect(wrapper.find("ChildComponent")).toHaveLength(1); // よい const wrapper = shallow(<MyChildComponent hoge={hoge} />); expect(wrapper.find(".child-component__label")).text().toBe("ほげ") // よくない const wrapper = mount(<MyParentComponent hoge={hoge} />); expect(wrapper.find(".child-component__label")).text().toBe("ほげ");
その他
Webpackerどうする問題
現在、Studyplus for SchoolではWebpackerを利用しています。しかしいくつか問題があり、これが正しい選択かはあまり自信がないため上では扱いませんでした。現状として、問題になった部分だけ設定を書き換えて使っています。たとえばUglifyJSがデフォルトの設定ではproductionでソースマップを吐いたり、同じくUglifyJSの設定が原因のIE11で起きる不具合を踏んだりなどしました。
とはいえ、やはりWebpackerを使うことで、Webpack設定に割く労力を節約できます。現段階で脱Webpackerは考えていません。
まとめ
今回、フロントエンド環境の下準備として、型とLintとテストの設定をしました。
今後は、以下のようなことをやっていきたいと考えています。
- Reactで作られていないコンポーネントをReact化する
- Railsのビュー(slim)に書かれた要素をフロントエンド側に持ってくる
- Atomic Designによってコンポーネントを分ける
- Storybookでコンポーネントごとの見た目確認を行えるようにする
ForSchool事業部では、フロントエンド以外にも、サーバーサイド、デザイン、カスタマーサポート、企画レベルでもそれぞれやりたいことはたくさんあります。手が足りてません。手伝ってくれるRailsエンジニアを募集しております。よろしくお願いします。