CoffeeScriptからJavaScript(ES2015以降)への移行

はじめに

株式会社リゾーム 企画・開発部第4グループの平松です。
現在、自社サービスの一つである交渉管理ware*1の改修を行っています。

現時点の交渉管理wareのRailsのバージョンは5.2.6です。最新版にバージョンアップしていこうと思っているのですが、その改修の一環として今回はCoffeeScriptからES2015以降のJavaScriptへの移行を行いました。

弊社のプロジェクトでは逐次CoffeeScriptからJavaScriptへの移行が進んでおり、当サービスにおいてもJavaScriptの方が他のライブラリやフレームワークを使う際など開発や保守が行いやすいと判断し、移行することにしました。

今回の移行ですがWebpacker等を使った移行ではなくSprocketsを使っているため、Sprockets用の作業内容が含まれています。ご注意ください。

移行の準備

JavaScriptに移行していきますがファイル一つ一つ手作業で直していくのは時間が掛かります。 そのため、decaffeinateというツールを使ってJavaScriptに変換していきます。

decaffeinateのGitHubのページに変換ガイドがあるのでそれに沿って進めていきます。
まずは、ツールのインストールから始めます。

npm install -g bulk-decaffeinate decaffeinate eslint

インストールできたら次は変換していくのですがその前に、変換できるかどうか確認します。 問題なければコマンドを打って変換していきます。

bulk-decaffeinate check

JavaScriptに一括変換

下記のコマンドでCoffeeScriptからJavaScriptに一括変換します。
-d オプションでディレクトリの指定をしています。
-f オプションでファイルの指定が可能です。

bulk-decaffeinate convert -d app/assets/javascripts

これでJavaScriptに変換できました。 しかし、単純に変換をかけただけではエラーが発生し、動きませんでした。 まずは正しく動くように修正し、その後、リファクタリングをしていきます。
参考: decaffeinate/conversion-guide.md at master · decaffeinate/decaffeinate · GitHub

動作するように修正

thisの前にsuperを配置

クラスが親クラスを継承している場合、thisを呼ぶ前にsuperを呼ぶ必要があります。 superを呼び出すことで親クラスのコンストラクターを呼ぶことができます。
先にthisを呼んだ場合、ReferenceErrorになります。

// before
class foo extends bar {
  constructor(hoge) {
    this.hoge = hogehoge;
    super(hoge); // エラーになります
  }
}
// after
class foo extends bar {
  constructor(hoge) {
    super(hoge); // superを先に呼ぶ
    this.hoge = hogehoge;
  }
}

参考: super - JavaScript | MDN

ES6(ES2015)に対応

元々のCoffeeScriptの方でES6の構文で書かれている箇所もあるため、それに対応していきます。 交渉管理wareではアセットファイルの管理にSprocketsを使用しています。
GitHubのページにES6サポートの項目があるのでそれに沿って進めます。

babel-transpilerをインストールする

Gemfileに追記してbundle installをします。

gem 'babel-transpiler'
manifest.jsのコンパイル対象に「.es6」を追加する

ES6に対応したファイルがコンパイル対象になるようにmanifest.jsに追記します。

//= link_directory ../javascripts .es6
拡張子を「es6」に変更する

拡張子を変更してES6ファイルとして扱われるようにします。

hogehoge.js => hogehoge.es6

ここまで行ったところ、正常に動作することが確認できました。

参考:
GitHub - rails/sprockets: Rack-based asset packaging system
Sprockets 4 なら ES6書けるよ - Qiita

リファクタリング

正常に動作するところまでできたので次はリファクタリングをしていきます。

varをlet,constに置き換える

varは再宣言でき、またスコープ外から参照できるため扱いづらいです。
そのため、letもしくはconstをできるだけ使うようにします。

let hoge = hogehoge;
// もしくは
const hoge = hogehoge;

constructorの引数をデフォルト引数にする

decaffeinateで変換した結果、元々デフォルト引数だったものがif文でデフォルト値を定義するように変換されていたため、元に戻しました。

// before
class Foo {
  constructor(hoge) {
    if (hoge == null) { hoge = 'hoge'; }
  }
}
// after
class Foo {
  constructor(hoge = 'hoge') {

  }
}

アロー関数を使用する

function()と書かれている箇所を() =>のように修正します。 アロー関数自体はthisを保持しないため、アロー関数によってthisの参照先が変わることはありません。
そのため、selfにthisを代入する必要がなくなります。
※ 元々の処理がthisの値が変わること前提の場合、アロー関数にしてしまうと動作が変わってしまうので注意が必要です。

// before
var self = this;
this.hogehoge = function() {
  self.foo = "baz";
};
// after
this.hogehoge = () => {
  this.foo = "baz";
};

var self = this;を削除する

前述のアロー関数を使用することでselfを使う必要がなくなるので下記のコードを削除し、selfをthisに置換します。

var self = this; // この行を削除

配列をforからforEachで処理する

decaffeinateで変換した結果、forで配列を処理をしていた箇所があったため、forEachで簡潔にします。

// before
for (let index = 0; index < hoges; index++) {
  hoge = hoges[index];
  // 何らかの処理
}
// after
hoges.forEach(hoge => {
  // 何らかの処理
});
// または
hoges.forEach(hoge => "何らかの処理");

まとめ

今回はdecaffeinateを使用してCoffeeScriptJavaScriptに一括変換しました。decaffeinateは便利ですが、既存の処理が変わっていたり、そもそも正しく動かなくなってしまうこともあるため、JSファイルの数によっては手動で直したほうが手間がかからないかもしれません。

移行作業の中で難しかったところはアロー関数に変更していった部分です。thisの値によってアロー関数を使わない方がいいところもあったため、都度確認が必要でした。
また、作業自体は動作確認をしながら進めていき、漏れがないように注意してましたがテストを流した際に何個か落ちてしまいました。 各機能の処理内容の理解とともにテストの重要性が感じられた作業となりました。

今後も改修を続けていくので内容がまとまったら記事を出していこうと思っています。

*1: デベロッパーとショップとの交渉内容をシンプルに効率的に一元管理できるクラウドサービス(ホームページ商品説明引用)