こんにちは。D&Aチームです。
クラウドワークスではユーザーの皆様により良い体験をしていただけるよう、クラウドワークス 安心安全宣言の取り組みを進めています!
そんな中、私たちのチームでは、ほぼ初めてのフロントエンドの開発に関わることになりました。 新規での開発ではなく、既存の実装からの更新という形となるのですが、それは古の時代のCoffeeScriptで書かれたものでした。
今回のブログはフロントエンドモダン化までの流れについて書いていこうと思います。
目次
クラウドワークスのフロントエンドの事情
クラウドワークスのフロントエンドはいくつかの地層が存在します。
詳しい内容は過去の投稿に譲りますが、今回の作業対象は、
- Sprockets層のCoffeeScriptとjQuery : 4画面
- Webpacker層のVue : 1画面
となります。
Sprockets層は以下の理由からモダンJS化をし、Webpacker層への引っ越しを合わせて実施することにしました。
- 負債を少しでも返したい
- 社内にCoffeeScriptエンジニア少ない(いない?)
- 今後のエンジニア採用のしやすさ
- 今後の開発のしやすさ
- 正直CoffeeScriptニガテ...(重要)
- TypeScript使いたい!(超重要)
リリースまでの流れ
モダンJS化をすることにしましたが、サービス中のコードを入れ替えるのはとても不安です。 そこで、ページ単位でかつ段階を分けて少しずつリリースをすることにしました。
- CoffeeScriptをJavaScript(ES6)に移植しWebpackerにお引越し
- 1をTypeScript化
- 関連するjQueryで書かれたDOM操作の共通処理をTypeScript化
- DOM要素の検索性の向上
- Vue内のJavaScriptをTypeScript化
- ページデザインの適用
これと並行して
- フロントエンド用のRollbar(エラーモニタリング)の整備
をしていきます。 フロントエンド用のRollbar整備については別の記事にまとめています。
デカフェ(CoffeeScriptをJavaScriptに変換)
CoffeeScriptをいきなりTypeScript化はせず、decaffeinateトランスパイラを利用して、一度JavaScriptに変換します。
トランスパイルされたコードはコメントを含めて機械的に生成されるため、無駄なコードが発生しますが、当初は動作させることを優先し、TypeScript で型付けをするときにリファクタリングしていきます。 これにより CoffeeScript に対する知識は最小ですませることができました。
class WithholdingTaxCalculator constructor: (@tax_rate)-> @tax_rate = .1021 unless @tax_rate? plus: (amount) -> amount = parseInt(amount, 10) if isNaN(amount) 0 else tax(amount) + amount tax: (amount) -> amount = parseInt(amount, 10) if isNaN(amount) 0 else if amount < 1000000 Math.floor(amount * @tax_rate) else # 100万円を超える部分は20.42% Math.floor((amount - 1000000) * @tax_rate * 2) + 102100 @WithholdingTaxCalculator = WithholdingTaxCalculator @withholding_tax_calculator = new WithholdingTaxCalculator
decaffeinateを実施後
/* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * DS208: Avoid top-level this * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ class WithholdingTaxCalculator { constructor(tax_rate){ this.tax_rate = tax_rate; if (this.tax_rate == null) { this.tax_rate = .1021; } } plus(amount) { amount = parseInt(amount, 10); if (isNaN(amount)) { return 0; } else { return tax(amount) + amount; } } tax(amount) { amount = parseInt(amount, 10); if (isNaN(amount)) { return 0; } else if (amount < 1000000) { return Math.floor(amount * this.tax_rate); } else { // 100万円を超える部分は20.42% return Math.floor((amount - 1000000) * this.tax_rate * 2) + 102100; } } } this.WithholdingTaxCalculator = WithholdingTaxCalculator; this.withholding_tax_calculator = new WithholdingTaxCalculator;
TypeScriptへ
次にTypeScript化をします。 TypeScriptは静的型付けが可能で、コンパイル時点で不正なコードの大部分を検出することが可能です。 また、最終的に出力されるJavaScriptのコードがCoffeeScriptより想像しやすく、また、Webpackerとの組み合わせでChromeでのデバッグもしやすいので、開発の生産性を向上させるという狙いもあります。
JS→TS変換では、主に次のことを行いました。
- 変数に型をつける
- 変数に適切な修飾子をつける
- メソッド引数をinterface化する
- jQueryで書かれたロジックを変換
Vueに書かれているJavaScriptも同様に対応していきます。
以下の例は、decaffeinateで出力されたJavaScriptに、変数の型と修飾子をつけて整理したものです。メソッド引数を number 型と宣言し、呼び出し側で number に制約をつけることで、処理がだいぶスッキリしました。
class WithholdingTaxCalculator { private readonly taxRate: number; public constructor(taxRate?: number) { this.taxRate = taxRate || 0.1021; } public plus(amount: number): number { return this.tax(amount) + amount; } public tax(amount: number): number { if (amount < 1000000) { return Math.floor(amount * this.taxRate); } else { // 100万円を超える部分は20.42% return Math.floor((amount - 1000000) * this.taxRate * 2) + 102100; } } }
また、引数が多いメソッドなどには、インターフェースを型注釈として利用します。 インターフェースが要求するプロパティとその型を持つかをコンパイラがチェックしてくれます。 次のCoffeeScriptとTypeScriptの例を見ると、両者の特徴がよく分かるかと思います。
class Item constructor: (@name, @description, @maker, @price, @discount_price) -> item = new Item "pen", "red", "crowdworks", 100, 0
TypeScript
interface ItemParam { name: string; description: string; maker: string; price: number; discountPrice: number; } class Item { public readonly name: string; public readonly description: string; public readonly maker: string; public readonly price: number; public readonly discountPrice: number; public constructor(param: ItemParam) { this.name = param.name; this.description = param.description; this.maker = param.maker; this.price = param.price; this.discountPrice = param.discountPrice; } } const item = new Item({ name: "pen", description: "red", maker: "crowdworks", price: 100, discountPrice: 0 });
リファクタリング
最後にDOM要素の検索性の改善を行います。 現状のコードでは、あるDOM要素を取得する場合にDOMの階層を辿っていくような実装になってしまっています。
$parent = $(@).closest('div') $parent.find('div.XXX span.TARGET').text("UPDATE!")
これでは、デザインを変更したいときにJavaScriptの実装も変更する必要が出てきてしまいます。 なので、class名, id での大まかなグループと詳細な class 名に分けDOMの細かな構造に依存しないようにするなど、コーディングだけに集中できるようにリファクタリングを行いました。
テスト
これが一番大変だったように思います。 まず、ブラウザの種類が沢山あるという点です。
これらにバージョンまで絡んでくる場合もあります。 今回は対応する画面が5画面と少ないので、各ブラウザでの動作確認を行っていきました。
- Chrome/FireFox
- OSに依存しないのでインストールするだけです
- Safari
- 開発環境はmacOSなので問題なし
- IE/Edge
- 同じネットワーク内にいるWindows機を借りる
- スマホ/タブレット
とはいえ、クラウドワークスのサービス全体でみるとたくさんの画面が存在します。 それを都度、
- 各ブラウザを
- 手動で操作し
- 目視で確認
をするのはとても現実的ではありません。 jestでのユニットテストやCypress, capybaraといったE2Eテストをやれば本当に十分なのかという不安もあります。 これはまだ課題として残っており、良いアイディアを模索している最中です。 「我こそは!」という方は、ぜひご連絡をください!!!
工夫したこと
計算ロジックの共有化
当初の jQuery 関連のコードでは、クラス名により宣言的に機能を実現し、解読が困難な jQuery Plugin が多用されていました。
同じ計算結果を出力するために、時期により 「jQuery Plugin による宣言的記述」 もしくは、「Vue.js の計算モデルクラス」と画面により複数の実装が行われていました。これを、「Vue.js の計算モデルクラス」を jQuery 側でも利用し、サーバーからの出力やユーザの入力からの手続き的なシンプルなデータフローに書き直しました。
複数ブラウザ対応
当チームはフロントエンドへの知識が浅いため弊社でのサポートブラウザである IE11 で動作しないコードを書いてしまうことがありました。随時修正していましたが、便利なプラグイン eslint-plugin-compat を導入することで、後手に回ることはなくなり、既存のコードの問題も同時に修正できました。
さいごに
クラウドワークスのフロントエンドはどうあるべきか、というのは数年前から抱えている課題です。
少しずつではありますが、レガシーなビルド環境からの脱却や、CoffeeScriptからTypeScript, Vueへの移植を進めています。
また、社内でもフロントエンド開発への意識も再び高まってきています。
フロントエンドはユーザーの皆さまとの大きな接点なので、これからも力を入れていけたらと思います。