エンジニアの @suusan2go です。2017年の10月まではクラウド ワークスに社員として参画していましたが、現在はフリーランス のエンジニアとして、主にフロントエンド環境の改善・支援を行ったり、ちょっとだけRails のアップグレードを手伝ったりしています。
Rails のアップグレードを手伝っている様子
今回は、わかるエンジニアがいなくなり無人 化してしまいメンテナンス不能 になったフロントエンド環境を立て直す話です。
クラウド ワークスのフロントエンド事情
クラウド ワークスは最初のコミットが2011年です。当然その頃はVue.jsやReactなんていうものは登場もしておらず、2015年ころまではSprocketsを使ったアセットパイプライン、CoffeeScript 、 jQuery によりフロントエンド環境は記述されていました。
当時のCTOによる記念すべき最初のコミット
しかしながら、2015年の頃にはよく言われるように、大きくなったアプリケーションでjQuery で複雑な画面を記述して動作を変更するのは大変に辛い状況になっており、そのタイミングでReactやVue.js、ES6、Babelといったキーワードがバズりはじめたということもあって、当時在籍していたエンジニアによりSprocketsとは別のフロントエンドビルド環境が誕生しました。
2015年夏 frontend
ディレクト リの誕生
Rails アプリケーションと同じリポジトリ の中に frontend/
というディレクト リを切る形で、ES6(ES2015)・Vue.jsを記述するためのフロントエンドビルド環境が誕生しました。技術構成としては以下のようになっています。
gulp
Browserify
Babel (Babelify)
Vue.js (0.12.10)
├── app # Railsのコード
│ ├── assets
│ │ ├── javascripts
├── package.json
├── gulpfile.coffee
├── frontend # 新フロントエンド環境
│ ├── javascripts
│ │ ├── src
│ │ └── build
frontend/
ディレクト リでビルドされたJSをSprocketsのアセットパイプラインに組み込んで、Rails から配信するという形をとっていました。当時のRails プロジェクトとしては割とモダンな構成と言えるのではないでしょうか。
新しく作成されたfrontend/
ディレクト リにより、クラウド ワークスの中でも重要な機能がいくつか書き換えられることとなりました。
しかしながら、ES2015やVue.jsを使用するもののみ frontend/
ディレクト リを使うというルールになっていたこと、また後述するクラウド ワークス独自のRails とのインテグレーション機構が作り込まれていたこともあり参入障壁が高くなってしまい、 frontend/
ディレクト リは導入に関わったエンジニア + 数名のみしか触れない環境になってしまいました。
独自のルーティング、Rails からのデータ渡し機構
特にエンジニアを遠ざける原因となってしまったのは Rails との独自のインテグレーション機構です。Vue.jsを使用する画面はクラウド ワークスの中でも10くらいだったのですが、全画面で全てのVue.jsのコンポーネント とルーティング機構(どの画面でどのコンポーネント をrenderするか)がbundleされたJSを読み込んでおり、またRails からのデータの渡し方も独自の仕組みで行われていました。
import Base from './viewmodels/base' ;
import Dashboard from './viewmodels/dashboard/dashboard' ;
export default class Router {
constructor() {
this .routes = []
this .route("/user-dashboard" , Dashboard);
this .route("*" , Base);
}
route(path, vm) {
this .routes.push(new Route(path, vm));
}
exec() {
var pathName = location .pathname;
var first_match_route = _.find(this .routes, (route) => { return route.isMatch(pathName); } );
if (first_match_route != undefined ) {
global.CWViewModel = new first_match_route.ViewModel();
}
}
}
Vue.jsのコンポーネント は以下のようになっており、Rails のレンダリング するERBのbody配下を全てVue.jsのテンプレートとして扱っています。
import Vue from 'vue' ;
import MessageThread from '../components/message/thread.vue' ;
export default Vue.extend({
data: function () { return CW.getData(); } ,
el: function () { return "body" ; } ,
components: {
'cw-message-thread' : MessageThread,
} ,
} );
ここで、 CW.getData()
しているのが、独自のデータ渡し機構です。詳細は省きますが、以下のJSをERBに書くと、フロントエンド環境で定義されたモデルクラスに値を渡してインスタンス を生成するというような機構になっていました。
< script >
CW.assign ('some_model' , <%= raw @some_model.to_json ()) %> );
</ script >
Rails 側のテンプレートは以下のようになります。Rails のERBとVue.jsのテンプレートが混ざっており、読むのにかなり脳内メモリが必要になっています。
< input -text inline-template :ay-form-field= "hoge.fugas[<%= field.index - 1 %>].piyo" >
<%= text_area_tag "some_textT_area" ,
required: 'required' ,
class : 'hoge' ,
'v-model' => 'text',
'rows' => '{{rows}}'
':rows' => 'rows'
%>
< cw-keywords-counter :keywords= "hoge.keywords" : text = "text" ></ cw-keywords-counter>
</ input -text>
これらの機構はWebComponentsのようなものを志向して導入されたようなのですが、上記のコードを見てもわかるように以下のような問題があったと言えるでしょう。
Rails とフロントエンド環境との境目が曖昧でデータのフローがわかりにくい
ES2015、Vue.jsと合わせて、これらの独自機構を学ぶ必要があり不要に学習コストが高い
これによりVue.jsに苦手意識を持ってしまったエンジニアも多いです。在籍時はVue.jsは苦手と言っていたエンジニアが退職後にVue.jsいいじゃん!となっている事例を何度も見ました(ちなみに私もその一人です……)。またbody配下を全てVueのテンプレートとして扱ったせいか、あるChrome のバージョンから一時的に画面が真っ白になってしまうという現象が発生しており、chrome のみ画面ロード時に微妙にスクロールさせるという謎のハックが導入されたりしていました。また触る人間も少ないためライブラリのバージョンも古いままとなっており、Vue.jsは1.0のまま塩漬けにされているという状態でした。
エンジニアとしてフォローしておくと、2015年はまだWebpackerも存在しておらず、Vuex、vue-routerといった今ではデファクト となったツール・ライブラリの登場も未だだったと記憶しています。そんな中、手探りでRails とモダンフロントエンドの良い関係、Vue.jsでのアプリケーション開発にチャレンジしたことは称賛されるべきだろうと思います。
エンジニアの退職、そして無人 化へ
2017年の冬から2018年の夏にかけてfrontend/
環境を知るエンジニアが退職してしまい、とうとう frontend/
をメンテできるエンジニアはいなくなってしまいました。
私は2018年の春ごろから副業という形でクラウド ワークスのMihalyという社内CSS / JSフレームワーク 開発環境の整備・サービスへ適用するための技術的な支援を行っていましたが、作成したフレームワーク を実サービスへnpmのパッケージとして配布するためには、既存の無人 化した frontend/
をどうにかすることは避けられません。そこで社員時代には避けてきた frontend/
ディレクト リを何とかするという仕事を手が空いた時間を見つけては進めることにしたのでした。
フロントエンド環境をもう一度メンテ可能な状態にもっていくために
前述したとおり、誰も触れない状態ではあるものの、非常に重要な画面のインタラク ションを担っているJSなので、そのまま捨てる事はできません。またメンテ可能な状態にするには少なくとも以下のようなことは実施する必要がありそうです。
gulp + browserifyをやめてWebpackに移行する
Vue1.0 => 2.0にアップグレードする
オレオレデータフローをやめる
オレオレルーティングをやめる
Linter / テストを導入する
これらを一気に変更するのはリスクも高く、QAコストも高くつきます。またfrontendディレクト リ配下で徐々に新しいライブラリに移行していくことも以下のように難しい状況に見えました。
Vue.js 1.0と2.0を混在させた状態でビルド環境を作ることは vue-template-compiler
、webpack
等の依存関係により、不可能に見えた*1
後述する通りVue.js 2.0非互換なAPI を使っている箇所が大量にあった
そこで frontend/
ディレクト リの中身を一気に改善するのではなく、別のビルド環境を作って少しずつ移行していく方針をとりました。
frontendディレクト リからの 漸進的
な脱却
まずは全画面で読まれていたfrontendディレクト リのJSを、必要な画面だけで読み込む形にし、独自のルーティング機構が不要な状態に変更しました。
またアプリルートから package.json
や gulpfile
といったものを frontend
ディレクト リに移して frontend
ディレクト リでビルドが完結するようにし、完全別のパッケージの管理とビルドを導入できる状態にもっていきました。これにより、画面ごとに別のビルド環境で生成されたJSを読み込ませる事が可能になり、安全にフロントエンド環境の移行が行える準備が整いました。
Webpackerの導入
そしてfrontend/
ディレクト リとは別のビルド環境として選んだのはWebpackerです。
これを読んでいる人の中にはWebpackerではなく素のWebpackを使ったほうが良いのでは???と思ってる人も多いと思います。自分自身、過去に「webpackで作るSprockets無しのフロントエンド開発 - クラウドワークス エンジニアブログ 」というブログを書いたり、プロダクションでSprocketsではなくWebpackを使ってフロントエンドビルド環境を作ったこともあるので、自分がフルタイムで関わるプロジェクトなら素のWebpackを選んだでしょう。しかしながらクラウド ワークスにはフロントエンド専任のエンジニアがおらず、自分がいきなり1からビルド環境を作ってもまた無人 化してしまうのではという懸念があったので、今回はWebpackerを選んだのでした。
Webpacker2系までは大量のconfigファイルがアプリ上に生成される形式だったため、かなりアップグレードが難しい状況であったのは間違いないですが*2 、3系からはconfigファイルがアプリには殆ど入らない形式になっているため、Webpackのコンフィグをゴリゴリいじる必要がなく、Rails エンジニアが片手間に触るという環境なら良い妥協点なのではと考えています*3 。Webpacker脱却記事も多いので、フロントエンド人材が育ったら引き剥がすことがそこまで難しくなさそうなのもポイントです。
Webpackerをフロントエンド環境の補助輪と考えて、将来存在が足かせになったタイミングでWebpackerのレイヤは剥がしてもらえばよいと考えています。
この変更により、ローカル開発環境では Sprockets
、 gulp(frontendディレクトリ)
、Webpacker
と3つのフロントエンドビルド環境が一時的に生まれてしまったわけですが、幸いクラウド ワークスの開発環境は docker-compose up
で全てが立ち上がるように標準化されていたこともあり、他のエンジニアの開発環境に大きな迷惑はかけずに docker-compsoe.yml
の更新をするだけで済んだのでした。
Webpackerの導入にあたっては、本番環境のNodeのバージョンアップ、デプロイツールと関連するGemのアップデートなどフロントエンドに完結しないタスクも多くありましたが、話の本筋からは離れるので割愛します。
導入後はVue.jsを使っている画面ごとに以下のような変更を行っていき、少しずつ移行を実施していきました。
- <%= javascript_include_tag bundle.js # 旧フロントエンド環境のJS %>
+ <%= javascript_pack_tag 'hogefuga.js' # Webpackerによりビルドした新JS %>
Vue.js 1.0から2.0へのアップグレード
Vue.jsの公式ドキュメントには以下のように、Vue.js 1.x 系からの移行に関するドキュメントがあるのでこちらを参考にしつつ、公式が vue-migration-helper
という2.0非互換なAPI を使っている箇所を検出してくれるCLI を提供してくれていたので、それを元に少しずつ移行作業を進めていきました。
jp.vuejs.org
github.com
実際に vue-migration-helper
をかけてみると大体150件ほど警告が出る状態でしたが、殆どが name="{{ name }}" to v-bind:name="name"
のような警告で機械的 に修正が可能なものでした。
以下のように1画面ずつプルリクを出してリリースしていき、何か問題があっても複数の画面に影響がでないよう進めていったので、大きな問題を出さずに移行を進めていくことが出来ました。
Rails から素直にデータをフロントエンド環境にわたす
前述したとおり、従来のフロントエンド環境はRails からデータを渡すのに不必要に複雑な機構をもってしまっていました。しかしながらJSで扱うデータは全てAPI を作って対応するというのも、初期データしか必要のない場合には少し実装のオーバーヘッドが大きいように感じます。
そこで Webpackerにもドキュメントとして公開されており(webpacker/props.md at master · rails/webpacker · GitHub )、 react-rails などのライブラリが内部でやっているように、テンプレートにJSON を埋め込んでJSから読み出すという方法を取りました。
<%= content_tag :div,
id: "hello-vue",
data: {
message: "Hello!",
name: "David"
}.to_json do %>
<% end %>
document .addEventListener('DOMContentLoaded' , () => {
const node = document .getElementById('hello-vue' )
const props = JSON.parse(node.getAttribute('data' ))
new Vue({
render: h => h(App, { props } )
} ).$mount('#hello-vue' );
} )
これがベストなやり方かと言われると微妙かもしれませんが、globalにデータをセットしてJSから読み出すよりも、「データをどこでセットしているか」、「データをどこから読み出しているか」が明確になったので以前よりはデータフローが追いやすくなったと思います。
Vue2.0へのアップグレードと合わせて、このデータフローについても上記のような変更を行いました。
Linter / Formatter 及びテストの導入
当たり前の話ではありますが、ライブラリを現時点で最新までもっていっても、追随できなければまたメンテナンスは難しくなってしまいます。またライブラリを上げたときに全画面で手動で確認しなければいけないとなると、アップグレードしていくのはかなり辛い作業になってしまうでしょう。というわけで普通の話ではありますが、アップグレードと合わせてLintとテストをするようにしました。
特に特殊なことはやっておらず、以下のライブラリを使用しています。
Jest + vue-test-utils
ESLint / Prettier
既存のコンポーネント は設計もあまりよくなく、テストが書きにくいものが多かったのですが、スナップショットテストがあるだけでも、ライブラリのアップデート・変更の心理的 ハードルは下がるなーと実感しています。
そして現状
無事、frontendディレクト リは役目を終え、全てをWebpackerに載せることが完了しました。
また自分がプッシュしたわけでもなく、Vue.jsを使った施策を出すチームも現れてきています。もともとやっていた mihaly
という社内CSS フレームワーク についても、インタラク ションは自分が素のJS(TypeScript)で書いていたのですが、Vue.jsからも使いたいという声が出てきており、 vue-mihaly
爆誕 の機運が高まっています。
まだまだフロントエンドはCoffeeScript が支配的ではありますが、継続的にフロントエンドを改善していく下地は出来てきたのかなと感じています。今後はCoffeeScript の脱却、mihalyの導入などが進むといいな〜
おわりに
クラウド ワークスではフロントエンドエンジニアを募集していますし、私への仕事の依頼もお待ちしております!(DMください)
twitter.com
www.wantedly.com