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

大規模プロダクトにおけるフロントエンドの1年間の変化

More than 3 years have passed since last update.

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に含め二重でバンドルされないようにしておきます。

webpack.config.js
module.exprots = {
  //...
  externals: {
    'moment': 'moment',
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

今回は手動でやってしまいましたが、同様のことはWebpackのDllPluginを使ってもできるようです。

HappyPackの導入

HappyPackはマルチスレッドによりWebpackのビルドを高速化するためのプラグインです。
実行するとキャッシュ用に .happypack ディレクトリが作られます。

webpack.config.js
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の記事でより詳しく触れられているのでこちらもぜひご覧ください。

flowtypeによりFluxにおいて型安全を手に入れる

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のファイルをインポートした際のエントリーポイントが変わっています。

foo.js
module.exports = 'foo';
before
import foo from 'foo'; // "foo"
after
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であり、既存機能の改修やバグ修正などで触る時のコンテキストスイッチのコストはそれなりに大きいものです。decafedecaffeinateで一気にやってしまいたいところ。

Backbone.js

CoffeeScript同様、Reactに移行していく中でコンテキストスイッチの問題に加えコンポーネントの互換性もほぼありません。

Reactの中でBackbone.Viewを使うつらい例
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かのガイドラインを用意するに留めています。

スクリーンショット 2016-12-18 22.26.12.png

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を渡している

などがあります。

before
<MyComponent
  {...this.props}
  style={
    left: 12,
    top: 36
  }
  foo-id={1}
/>
after
<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に発展することもあります。

スクリーンショット 2016-12-18 18.56.25.png

ちなみに、こうした技術系社内コミュニティには他に共通コンポーネント委員会,CSS標準化委員会,ISU会といったものがあったりします。

さいごに

というわけで2016年の1年間で起きた変化を振り返ってみました。

2~3年で大きく変化するのはWebフロントエンドの環境だけでなく、会社やプロダクトを取り巻く状況もです。その時できる限りの価値をお客様に届けられるために、これらの変化は行われてきました。例えば、先日リリースしたとある機能は会計業務における記帳作業をより効率的に行うためのものですが、そうした作業は従来デスクトップアプリケーション上で行われてきました。UIやパフォーマンスは必然的にそれらと比較されることになるため、その中で上述のImmutable.jsを使用したパフォーマンスチューニングなどが重要なファクターとなってきます。

昨年から進めているアーキテクチャの刷新はだいぶ進んできたので、個人的には来年以降はより品質を上げるための施策をやりたいなと考えています。

freeeではエンジニアを絶賛募集中です。これを読んで「踏み込みが足りないな」「もっといいやり方があるんじゃない?」と思った方もそうでない方も、ぜひ一度遊びに来てください。

(そういえばこんなこともやりました)
recruiting

明日は気象情報からプロダクトKPIまで分析するデータサイエンティスト @ami_o がお届けします。

freee
スモールビジネスのバックオフィス業務をテクノロジーで自動化し、日本のスモールビジネスを元気にする
http://www.freee.co.jp/
Why not register and get more from Qiita?
  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