Sprocketsの切り崩し方
Sprocketsは非常に基盤的な位置でアプリケーションのアーキテクチャを支えています。そのため、これを切り崩していくためには大量のファイルに変更を加えなくてはなりません。
その際に無視できないのは、プロダクトは現在も成長を続けていることです。大量のファイルに変更を加えれば、あちこちのファイルでコンフリクトが発生しやすくなります。またグローバル変数依存であるがゆえに、変更内容の影響範囲もgrep頼りで確認せざるを得ないため、レビュー作業にも時間がかかります。そうこうしているうちにさらにコンフリクトは増えていくばかり……。
つまり、どうしても力技で一気に解決することはできません。コンフリクトを避けつつ着実に前進するための段取りが重要になります。
とはいえ、やること自体は単純です。
-
各ファイル内で、関数やクラスを定義している箇所について、CommonJS形式で
module.exportsを付与し、外部ファイルから読み込めるようにする。 -
他ファイル内で定義されている関数やクラスを使用しているすべての箇所に対して、1のステップで公開した関数やクラスを
requireで読み込む。 - これらの変更を加えたJavaScriptをwebpackなどでビルドする。
この段取りをいかにして安全かつ着実に進めるか、ここが腕の見せどころです。
レビュー可能な変更を作る
1回の変更でいろいろなことをやりすぎるのは厳禁です。ミスが含まれていてもレビューで見落とされやすくなりますし、そもそもレビューに時間がかかるのでレビューが終わる頃にはまたコンフリクトが発生してしまいます!
とはいっても、編集しなくてはならないファイル数は1000を超えており、変更量はどうしても多くなってしまいます。そこでポイントとなるのは「小さなコンテキストで大きな横断的変更を作る」ことです。
具体的には、段取りをより細分化した変更内容を、大量のファイルに一気に施します。例えば次の内容です。
- 特定のネームスペースをそぐだけの変更
-
特定のディレクトリ以下のファイル内の関数に
module.exportsを付けてまわるだけの変更 -
特定の関数の使用箇所に
requireを付けてまわるだけの変更
これなら、100ファイル程度の変更であれば机上レビューで素早く機械的にチェックできるので、コンフリクト前にレビューを終えてマージにこぎつけやすくなります(図1)。
正しさを検証しつつ変更を作る
横断的変更には作業漏れのリスクがあります。例えば次のようなhello関数をCommonJS形式化する場合は、
- function hello() {...}
+ module.exports = function hello() {...}
とした上で、次に全ファイル内から元のhello関数を呼び出している箇所をすべて探し当て、const hello = require('./hello')のように書き換えていかなくてはなりません。書き換える箇所を1つでも漏らしたらそれはバグです!
この作業はESLintなどのコードチェックツールを活用することで、正しさを検証しながら安全に進めることができます。
具体的には、まずESLintのno-undefルールを有効にし、ファイル内での未定義変数(つまりグローバル変数として解決している変数)の使用箇所を検知するようにします。
module.exports = {
rules: {
'no-undef': 'error'
}
}
この段階でESLintを実行すると、大量の未定義変数が検知されてエラーまみれになるはずです。次に、検知されたすべての未定義変数をいったん見逃してもらうよう、設定ファイルのglobalsプロパティを定義していきます。
module.exports = {
// すべてのグローバル変数を定義
globals: {
hello: false,
any: false,
other: false,
variables: false
},
rules: {
'no-undef': 'error'
}
}
これでESLintを実行して未定義変数が検知されないことを確認したら、最後に今回のCommonJS化対象の変数helloを設定ファイルのglobals定義から削除します。
module.exports = {
// すべてのグローバル変数を定義
globals: {
// hello: false,
any: false,
other: false,
variables: false
},
rules: {
'no-undef': 'error'
}
}
hello関数の呼び出し箇所をすべてCommonJS形式に書き換えていれば、ESLintでエラーが検知されなくなります。エラーがでなくなるまでESLintの実行と書き換えを繰り返すことで、作業漏れなく横断的変更が作られていることをツールに保証させることができ、レビューコストを肩代わりさせることができます。
再現可能な変更を作る
コンフリクト解消作業は、コスト以外のなにものでもありません。極力発生させたくない作業とはいえ、いくらレビューコストを下げて短期間でのマージを目指しても、コンフリクトするときはしてしまいます。
そこで発想を変えて、コンフリクトする前提で、同じ変更をすぐに再現できるようにしておくと効率的かつ気持ちを楽に保つことができ、中長期的なリファクタリングも着実に前進させられます。
具体的な手段としては、正規表現置換です。「本気か?」と思われた方もいるかもしれませんが本気です。もっと理論的に正しく攻めるならjscodeshiftなどのAST(抽象構文木)操作によるリファクタリングツールを使うこともできそうですが、AST操作に慣れていないと使いこなすのは難しいかもしれません。
正規表現の精度を上げるためには、あらかじめprettierなどのコードフォーマッタをかけておくことで、マッチできないパターンはほぼなくせるはずです。
# ネームスペースをそぐ
perl -i -pe 's/models\.(?=[A-Z])//g' **/*.{js,coffee}
# class 定義に module.exports を付与する
perl -i -pe 's/^class/module.exports = class/g' **/*.{js,coffee}
スクリプトによる変更はむしろ人為的なミスを防げるため、正しさをツールに保証させてレビューコストを肩代わりすることにもつながります。
フロントエンド用gemの置き換え
フロントエンド用のライブラリを読み込むためにgemを使っていた箇所があれば、それも書き換える必要があります。
OSSとして公開されているフロントエンド用gemであれば、ほぼ確実にnpmにオリジナルのパッケージが公開されているはずなので、そちらを利用すればよいでしょう。
またfreeeでは、メインリポジトリとは別のリポジトリで、gemフォーマットでメンテナンスされている社内独自のJSフレームワークがあります。こうしたものは、先述した変更方法でCommonJS形式化し、プライベートnpmパッケージ化してメンテナンスするように変更するのが理想的ではありますが、すでに機能的にもバグ的にも枯れていたため、思い切ってメインリポジトリ内にソースコードをそのまま取り込み、その上で不要なファイルを削除したり、必要な部分だけをCommonJS形式化したりしました。
その際、YarnのWorkspaces機能を使うことで、ソースコードをメインリポジトリ内に取り込みつつも、通常のnpmパッケージかのように扱えるようにしました。これにより、依存関係の方向性を制御したり、機能境界を明確にしたりすることが可能になります。
Sprocketsがなくなったその後
こうして着実にリファクタリングを進めていった結果、ついにすべてのモジュールがCommonJS/ES Modules形式で管理されるようになり、Sprocketsディレクティブが全廃されました。
これまでもESLintなどの静的解析ツールを部分的に使用してはいましたが、すべてのモジュールの依存性が明確化し、webpackなどのビルドやその上でBabelによるコード変換を経るようになったことで、より強力で正確な静的解析が可能になりました。
具体的には、現在はFlowによる型付けをどんどん導入していっています。
また、webpackのプラグインのcircular-dependency-pluginを用いて循環参照を検知したり、unused-files-webpack-pluginを用いて未使用コードを発見したりすることも可能になります。
このように、これまでのグローバル変数依存のアーキテクチャでは運用することが困難だった静的解析ツールを導入することで、今度は一変して、将来にわたってアーキテクチャを健全に保つ仕組みが導入できるようになった、というわけです。
まとめ
Rails5.0以前、「レール」の一部であったSprocketsアセットパイプライン機能は、フロントエンド黎明期を支えてきた反面で、グローバル変数依存のアーキテクチャを生み出してしまう問題がありました。Nodeエコシステムが発展した現在の基準からすれば、こうしたアーキテクチャによるデメリットは大きく、大規模な開発に耐えられなかったり、堅牢な開発を支援する静的解析ツールやビルドツールの導入障壁となっていたりしていました。
成長中のプロダクトにおいて、モジュール管理のアーキテクチャを改善することは、一朝一夕で処理できるタスクではありません。安全かつ着実に進めるための段取りが重要です。しかし、それを達成した先には希望があふれる世界が待っています。
同じような状況でお困りの方にとって、本記事で紹介したfreeeでの事例が参考になれば幸いです。
