CoffeeScriptで書かれたAngularJSアプリケーションのコード群を順次ES2015に置き換えた際、いくつかやらかしてしまった問題があったので、備忘録としてここにまとめます。
環境
- CoffeeScript 1系
- AngualrJS 1.3
- Grunt
- Browserify
移行手順
-
decaffeinateを使ってコード変換
- 一度に全部のコードを変換するのに耐えられるテストが無いため、細かく1ファイルずつ移行していく
- ESLintへの適応、ESModulesの対応
- ファイルの切り替え
陥った問題
二重定義とinstanceofによる問題
細かく移行していくため、jsに変換したあとも元のCoffeeScriptファイルを消せない場合が出てきます。例えば
class A
test: () ->
console.log(10)
export default class A {
test() {
console.log(10);
}
}
この変換に問題はありませんが、CoffeeScript側のクラスA
がほかの未変換のCoffeeScriptから参照されていた場合、それらがすべてjsに変換されるまでa.coffee
は消せず、クラスA
は二重に定義された状態になります。
ふたつのクラスA
は同じふるまいをするので多くの場合問題にはなりませんが、A
のインスタンスに対してinstanceof
している場合は致命的にマズイです。
import A from './a.js';
function test(obj) {
if (obj instanceof A) {
// ...
}
}
このコードでは、引数obj
にjs側のクラスA
インスタンスが渡ってくる場合はinstanceof A
はtrue
になりますが、CoffeeScript側のクラスA
インスタンスが渡ってきた場合はfalse
となり、意図しない動作の元となります。
@とグローバル変数問題
decaffeinateの変換によって不具合が発生する場合があります。以下がその例です。
class Test
constructor: (@hoge) ->
console.log(hoge)
このようなCoffeeScriptをdecaffeinateで変換すると、以下のようなjsが出力されます。
class Test {
constructor(hoge) {
this.hoge = hoge;
console.log(hoge);
}
}
しかしこの変換は正しくありません。
正しく動作させるには以下のように修正する必要があります。
class Test {
constructor(hoge) {
this.hoge = hoge;
console.log(window.hoge);
}
}
つまり、CoffeeScript側のconsole.log(hoge)
におけるhoge
はグローバル変数であり引数@hoge
とは別物であるるため、js側では引数hoge
とグローバル変数hoge
を明確に分けるためwindow
の明示が必要だったということです。
この変換による不具合は、一見して間違いが見えづらく、十分なテストが書かれていなければ防ぐのは難しいかもしれません。
$scopeとbind問題
class C
constructor: (scope) ->
scope.foo = @foo
foo: () =>
console.log(this)
C.$inject = ['$scope']
class C {
static $inject = ['$scope'];
constructor(scope) {
this.foo = this.foo.bind(this);
scope.foo = this.foo;
}
foo() {
console.log(this);
}
}
このthis.foo.bind()
の部分を「無くてもなんとかなるのでは?」と安易に考えて消してしまうと
<button ng-click="foo()" />
のようにscope経由でメソッドを触った時、メソッド内のthis
がオブジェクト自身ではなくscopeを指してしまい、意図しない動作の元となります。bind()
は残すか、Controller asでメソッドを使う必要があると思われます。
さいごに
当移行では、かなりのトラブルが発生してしまいました。それなりの規模のアプリケーションであることもあり、リリースには恐怖感すらありました。
なんとか大半の移行は完了しましたが、このような事態を避けるため日頃からテストを書いておくべきだと思うのでした。