対象読者
Ruby on Rails 5.0以前のアプリケーションのフロントエンド開発で、npmによるモジュール管理やwebpackなどによるビルドを行いたいのにそれがなかなか実現できずに困っている方。またRailsを使っていなくとも、グローバル変数に依存したアーキテクチャを改善したい方。
技術トレンドの変化と共に成長したプロダクト
人事労務管理システムである「人事労務freee」は2014年から開発されています。当時のフロントエンド分野は技術トレンドの変化が非常に目まぐるしく、大ざっぱな印象としては、Backbone.jsが成熟しReactが流行の兆しを見せ始めた頃だったかと思います。
こうした時勢のまっただ中で、人事労務freeeというプロダクトは誕生し、成長してきました。それを表すように、このプロダクトにはさまざまな言語やライブラリやアーキテクチャで書かれたソースコードが存在しています。常に新しい技術を取り入れることでユーザへ、より大きな価値を少しでも早く届けることができないかを模索してきた結果でもあります。
しかし、新しい技術を取り入れる試みが徐々に難しくなってきました。原因はビルド環境の旧式化です。Ruby on Rails(5.0以前)には「Sprockets」というモジュールに、アセットパイプラインと呼ばれる仕組みが備わっており、これを用いるとCoffeeScriptからJavaScriptへの変換や、複数のJavaScriptファイルの結合が容易に実現できます。
フロントエンド黎明期から提供されていたこの仕組みは、Railsの「レール」の一部として浸透していました。freeeのプロダクトも当然のようにこのレールに乗って開発されていました。ところが、このSprocketsこそが、後にビルド環境の旧式化という問題の原因を作り、ビルド環境を刷新する上でのブロッカーとなる存在となってしまいました。
Sprocketsがもたらすグローバル変数依存アーキテクチャ
Sprocketsのアセットパイプラインで問題になるのは、グローバル変数依存のアーキテクチャを作ってしまうことです。
Sprocketsの機能として、「Sprocketsディレクティブ」と呼ばれるものがあります。これは//= require <FILE_PATH>
の記述をJavaScriptファイル内にすることで、指定されたファイルがサーバで結合されレスポンスとして返される、というものです。
これを使うとJavaScriptファイルが分割できるので、役割ごとにファイルを分けたくなりますね。例えばhello
関数を定義するファイルgreets.jsと、hello
関数を利用するmain.jsを分ける、といったことができます。
function hello() {...}
//= require greets hello()
function hello() {...} hello()
しかし greets.js を読み込み忘れた場合はどうでしょうか。当然、hello
関数は定義されていないので呼び出しエラーとなります。
function hello() {...}
hello()
hello() // ReferenceError: hello is not defined
つまり「先に読み込んだファイルで関数を定義」し、「後で読み込んだファイルで呼び出す」といった関係を死守しなくてはなりません。
ファイルが少ないうちはさほど混乱することはありませんが、プロダクトが成長したらどうでしょうか。次に載せるいくつかのコードリストは、ある時点でのfreeeのプロダクトに存在した、実際のファイルの中身です。
//= require ./common.js //= require ./lib //= require_tree ../templates //= require ./payroll/sub_router //= require_tree ./payroll/mixin //= require_tree ./payroll/components //= require_tree ./payroll/components/modal //= require_tree ./payroll/components/code_select //= require ./payroll/components_init //= require ./payroll/model //= require ./payroll/payroll_model //= require ./payroll/payroll_collection //= require ./models/company_payroll_statement //= require_tree ./models //= require_tree ./collections //= require ./components/shared/text_input //= require ./components/shared/time_string_input //= require ./components/shared/field_with_validation //= require ./components/shared/select //= require_tree ./components //= require ./components_init //= require ./payroll/payroll_modal //= require ./payroll/payroll_view //= require ./views/yearend/employee_base //= require_tree ./views //= require ./payroll/payroll_page //= require ./pages/settings/base //= require_tree ./pages //= stub ./routers/employees_v2 //= stub ./routers/paid_holiday //= stub ./routers/onboardings //= stub ./routers/monthly_standard_remuneration_reports //= require_tree ./routers //= require ./for_legacy //= require ./bootstrap $(document).ready(function() { window.router = new freee.GLOBAL.routers.Base({ role: window.userRole, movableRange: freee.data.get('movableRange') }); Backbone.history.start(); });
//= require freee-js-framework/for_payroll.js //= require ./session.js //= require ./heartbeat //= require ./initialize.js
//= require ./freee/mvc/vue_view //= require ./freee/mvc/vue_component //= require ./freee/ui/modal_vueview
3ファイルのみの引用でやめておきますが、ファイル読み込みはさらに連鎖し、結果として1000を超えるファイルが読み込まれることになります。この結果、「どこかのファイル」にある、views.Employees
やmodels.Settings
といったグローバル変数に定義された関数を、「どこかのファイル」で使用するという、大変複雑な状況になってしまっていました。
もはや人の脳のキャパシティを越えています。コードを実行するのは機械ですが、コードを書くのは人間です!
このように、Sprocketsがもたらすグローバル変数依存のアーキテクチャは、ファイルの読み込み漏れや読み込み順の誤りだけでなく、関数名の重複や循環参照など、さまざまなリスクを抱えてしまいやすいのです。
Sprocketsが扱える外部ライブラリ形式はgemだけ
さらに別の問題として、Sprocketsディレクティブで外部ライブラリを読み込むことができるのは、Rubyのパッケージ形式であるgemに限られ、npmのパッケージを読み込むことができません。例えば、jQueryをライブラリとして読み込む場合は、jQueryをgem形式にラップしたjquery-railsといったgemをインストールし、JavaScriptファイル内で//= require jquery
とすることでjQueryがグローバル変数$
にアサインされ使えるようになる、という仕組みです。
つまり、npmであればすぐに使えるパッケージでも、Sprocketsではgem化されたものが必要です。ややマイナーなパッケージの場合は、gem化されたライブラリが見つからなかったり、バージョンが最新版に追従していなかったり、途中でバージョンアップが放棄されていたりと、余計な問題をもたらします。