freeeでエンジニアをやっています@tohashiです。FF15はまだ3章でうろうろしています。
freee Engineers Advent Calendar 2016の18日目いきます。
早いと言われたり実際は早くないと言われたりするWebフロントエンド界隈ですが、ここでは実際のプロダクトでこの1年間に起きたフロントエンド周りの変化を振り返ってみました。
プロダクトについて
2012年から開発が進められているRuby on Rails製のモノリシックなアプリケーションです。フロントエンドの技術スタックは当初CoffeeScript,Backbone.js,jQuery,eco,Bowerなどを使っていましたが、昨年よりBabel,React,Webpackといった構成に移行しつつあります。
DBのテーブル数は500弱、フロントエンドの規模としては下記通りです。
Files | Lines | |
---|---|---|
.js |
1191 | 122182 |
.coffee |
608 | 56276 |
.scss |
297 | 50109 |
.eco |
468 | 18478 |
プロダクトに関わるエンジニアは40人近くいて、弊社ではフロントエンド/サーバーサイドといった明確な線引きがないため全員がフロントエンドに触れる機会が有りえます。開発チーム・コード共にそれなりに大規模と言えるのではないでしょうか。
やったこと
モジュール間の依存解決
もともとRailsのSprocketsに沿ってjsを書いていたため、classは全て一つのグローバル変数に格納され、全てのjsが結合された巨大なapplication.jsをロードしている状態で、メンテナビリティやパフォーマンスに大きな問題を抱えていました。そこで去年よりWebpackを導入し、各モジュールの依存関係を整理してjsファイルを適切な単位に分割するようにしました。ファイル数が多いため段階的に作業をつづけ、今年ようやく全てのファイルの依存解決が完了することができました。
過渡期はWebpackとSprockets両方から参照されるモジュールはこのようなつらい書き方をしていましたが、それももう必要なくなりました。
# Sprockets環境では何もしない
require = -> unless require
module = {} unless module
# Webpack環境では必要なモジュールのロード
views =
Foo:
BazView: require('views/foo/baz')
# Sprockets環境なら上の宣言をグローバル変数で上書き
if window.views
views = window.views
module.exports = class views.Foo.BarView extends views.Foo.BazView
initialize: ->
#…
この顛末の詳細は以前勉強会でも発表させていただいたので、こちらもご参照ください。
フロントエンドのモダン化とJavaScriptモジュールの依存解決
ビルド高速化
開発環境でフロントエンドのビルドプロセスはRailsから完全に独立していて、jsは全てwebpack-dev-serverを使用しています。しかし上述の通りその対象となるファイルが1700以上あり、一時期は開発環境の立ち上げ(初回ビルド)にjsだけで2分近くかかっている状態でした。このままでは全体の生産性にも影響を及ぼしかねないため、開発合宿の時間を使って色々とビルド高速化のための施策を試みました。
ライブラリの別バンドル化
Reactなどほぼ全てのページで使うモジュールを独立したバンドルにまとめ、また頻繁に変更されることもないのでwatch対象から外しました。ファイルサイズ削減、キャッシュ効率化にも繋がります。外部化したモジュールはexternalsに含め二重でバンドルされないようにしておきます。
module.exprots = {
//...
externals: {
'moment': 'moment',
'react': 'React',
'react-dom': 'ReactDOM'
}
};
今回は手動でやってしまいましたが、同様のことはWebpackのDllPluginを使ってもできるようです。
HappyPackの導入
HappyPackはマルチスレッドによりWebpackのビルドを高速化するためのプラグインです。
実行するとキャッシュ用に .happypack
ディレクトリが作られます。
const HappyPack = require('happypack');
const threadPool = HappyPack.ThreadPool({ size: 4 });
module.exports = {
//...
plugins: [
new HappyPack({
id: 'js',
threadPool
})
],
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: excludes,
query: {
cacheDirectory: true,
comments: false
},
happy: { id: 'js' }
}
]
};
結果
jsの初回ビルドを107872ms
-> 55091ms
まで縮めることができました。とはいえまだまだ遅いので、引き続き改修は続けていきます。そもそもこの規模でWebpackでやっていくという話もあまり聞かないので、中長期的にはリポジトリの分割やアーキテクチャの変更など抜本的な対策を考えていきたいところですね。
こちらも勉強会の資料があるので、よければご覧ください。
大規模プロダクト Webpack やっていく気持ち
flowtypeの導入
flowtypeによる静的型チェックが導入され、大規模なチーム開発でもメンテナビリティを損なわないコードが実現可能になりました。当アドベントカレンダー5日目の@joe-reの記事でより詳しく触れられているのでこちらもぜひご覧ください。
Babelアップデート
Babel5系から6系へのアップデートを行いました。対応が必要だったのは以下の2点です。
IIFE without wrap
Babel5では() => {}()
というシンタックスが動くのですが、実は(これが動くのは)バグでArrow funcitonの場合は(() => {})()
でないとダメなようです。Babel6のes2015-presetsではエラーになるので修正します。
参考: https://github.com/babel/babel/issues/2118
module.exports
Babel6からCommonJSのサポートがなくなり、CommonJSのファイルをインポートした際のエントリーポイントが変わっています。
module.exports = 'foo';
import foo from 'foo'; // "foo"
import foo from 'foo'; // { default: "foo" }
書き換えない場合は、add-module-exportsプラグインを使うことでこの問題を回避できます。
Node.jsアップデート
Node.jsを4系からLTSとなった6系にアップデートしました。あくまでビルド周りに使っているだけなのであまり大きな影響はありませんが、npmも3系にバージョンアップされnode_modulesがフラットになった結果npm-shrinkwrap.json
が半分ほどの長さ(15000行 -> 7000行)になったのは嬉しいポイントでした。(とはいえshrinkwrapもう少しなんとかならないかなという気持ちがあります)
(主に時間的な都合などで)できなかったこと
古い技術スタックの撲滅
CoffeeScript
基本的にはES2015で書いていますが、最初にあるとおり1/3ほどのコードは依然としてCoffeeScriptであり、既存機能の改修やバグ修正などで触る時のコンテキストスイッチのコストはそれなりに大きいものです。decafeかdecaffeinateで一気にやってしまいたいところ。
Backbone.js
CoffeeScript同様、Reactに移行していく中でコンテキストスイッチの問題に加えコンポーネントの互換性もほぼありません。
import React from 'react';
import ReactDOM from 'react-dom';
import OldView from './views/old_view';
class MyComponent extends React.Component {
componentDidMount() {
this.oldView = new OldView();
this.oldView.render(ReactDOM.findDOMNode(this.refs.oldViewWrapper));
}
render() {
<div>
<p>foo</p>
<div ref="oldViewWrapper" />
</div>
}
}
書き換えのコストがそれなりに大きいため、現状は業務上必要になった箇所から随時やっていっています。
Underscore.js
多くの機能がES5,ES2015で提供されているためもういらないのでは?という意見も有りましたが、_.omit
や_.throttle
など使いたいメソッドもあるのも事実なのでメソッドごとに使ってOKかNGかのガイドラインを用意するに留めています。
Bootstrap
ローンチ当初は使っていましたが、今は社内製CSSフレームワークを使う方針に切り替えています。
こいつが残っていると.button
といった汎用的なクラス名や要素そのものに詳細度の高いCSSを当ててくるなどして非常に行儀が悪いので早めに消したいところですが、適用範囲が広い上にスタイルが崩れていたところで特にエラーを吐くわけでもないで確認に少し手間取っています。
jQueryUI
こちらも古いコードにわずかに残っています。撲滅系の作業は作業コストと同じくらい確認のコストが大きい(最終的には手動リグレッションテストになりがち)のが悩みどころですね。
Immutable.jsの導入
データ操作時のObject.assign地獄の回避やshouldComponentUpdateによるReactコンポーネントのパフォーマンスチューニングなどのために、導入の機運が高まりつつあります。チーム開発の場合、flowtypeによる型アノテーションがないと通常のオブジェクトなのか、MapなのかListなのかRecordなのか混乱を招く恐れがあるのでそこは必須ではないかと思います。
React.jsアップデート
React v0.14.xからv15.x.xへのアップデートを試みています。
変更が必要なポイントとしては主に
- 不要なpropsを渡している
- styleのpxなどの単位がない
- 独自attributesを渡している
などがあります。
<MyComponent
{...this.props}
style={
left: 12,
top: 36
}
foo-id={1}
/>
<MyComponent
foo={this.props.foo}
bar={this.props.bar}
style={
left: '12px',
top: '36px'
}
fooId={1}
/>
gulpやめる
現状scssはgulpでビルドしていますが、Webpackと別々にプロセスを立ち上げていて非効率的なので統一したいなと。ただgulpfile内でasset_path
の解決などややトリッキーなことをしているのでそこはWebpack用のloaderにしてやる必要が有ります。
ちなみにWebpack=CSS in JSみたいなイメージですが普通に.css
ファイルとして出力することも可能です。
https://webpack.github.io/docs/stylesheets.html#styles-from-initial-chunks-into-separate-css-output-file
yarnpkg
npm-shrinkwrapに悩まされる我々としては非常に魅力的なのですが、もうしばらく様子見で...リプレイス作業自体は簡単にできました。
進め方
Slackに#frontend
というチャンネルがあり、普段はそこで話してシュッとやったりやらなかったりします。中長期的な展望や大きな施策に関しては有志でフロントエンド委員会というMTGを作りそこで決めるなどしています。内容によってはクォーターごとの開発チームのObjectiveに発展することもあります。
ちなみに、こうした技術系社内コミュニティには他に共通コンポーネント委員会,CSS標準化委員会,ISU会といったものがあったりします。
さいごに
というわけで2016年の1年間で起きた変化を振り返ってみました。
2~3年で大きく変化するのはWebフロントエンドの環境だけでなく、会社やプロダクトを取り巻く状況もです。その時できる限りの価値をお客様に届けられるために、これらの変化は行われてきました。例えば、先日リリースしたとある機能は会計業務における記帳作業をより効率的に行うためのものですが、そうした作業は従来デスクトップアプリケーション上で行われてきました。UIやパフォーマンスは必然的にそれらと比較されることになるため、その中で上述のImmutable.jsを使用したパフォーマンスチューニングなどが重要なファクターとなってきます。
昨年から進めているアーキテクチャの刷新はだいぶ進んできたので、個人的には来年以降はより品質を上げるための施策をやりたいなと考えています。
freeeではエンジニアを絶賛募集中です。これを読んで「踏み込みが足りないな」「もっといいやり方があるんじゃない?」と思った方もそうでない方も、ぜひ一度遊びに来てください。
明日は気象情報からプロダクトKPIまで分析するデータサイエンティスト @ami_o がお届けします。