みなさまこんにちは。
ベーシックアドベントカレンダー13日目です。
担当は@tkhrです。6日ぶり2回目となります。
今回はJSやCSS、PHPのコードが直に書かれたせいで肥大化しているViewをスリムにして、管理しやすい形にしようという計画です。
まずはViewにベタ書きのJSコードを引き剥がして、webpackによってモジュール化することで実現します。
before/after
before
スクショの右側、L159-163の約20行のJSコードです。
スクショの左側、15このファイル全てに記述されていました。
しかも、.ctp
からもわかるようにViewファイルです。
after
1行になりました。
JSファイルはこんな感じ。
admin_uril.js
単純計算で300行近い重複コードを削減することができました。
(20行x15ファイル=約300行)
なぜこうなるのか
原因は?
このコードが書かれた頃は、複数人の外注さんに開発を委託していました。
各外注さんはアサインされたタスクをこなす(受注した機能が動く)ためにコードを書いていきます。
この時、開発を委託していたのはディレクターなどプログラミング技術に詳しくない人です。
そして外注さんは求められた機能が動くようにできる限り早く実装します。
となれば、外注さんはすでにあるコードをコピペすれば早く実装でき、委託したディレクターは求めた機能が手に入ります。
どちらもHappy!ですね。
ですが、水面下では先ほどのように、いわゆる保守性が悪い&属人化したコードが生まれて技術的負債となってしまいました。
他部署のエンジニアでもこれに共感してくださる方がいました。
やはり同じように、ディレクターと外注さんとのやりとりの末に起こってしまった悲劇とのことでした。
何が悪いのか
これは個人的な考えですが、1番の原因はコード全体を見渡す人がいなかったことだと思います。
コードの重複に気づき、それを指摘できる人間が1人いればこのような状態は避けられたのではと思います。
先のような状況では、ディレクターがこの立場に立てれば、ベターなのかもしれません。
そしてこのコード全体を見渡す人が率先してコードの質を追求できると、将来的な負債の量も軽減できるのではないかと思います。
未来を見据えて全体最適と部分最適をしていくのがいいと何となく想像できます(言いたかっただけ)。
コードレビューのように全体を俯瞰できなくても複数人で補完しあって全体像を把握できればそれもまた解決できると思います。
重要視されていますが、実際にはこういうケースで役立つのですね。
問題点
ということで、今回の問題はこのViewに書かれた大量の重複コードです。
問題点は2つ
- ViewにJSのコードがべた書きされている
- 多くの場所でコードに重複が存在している
この2つの問題を解決するためにwebpackを選びました。
webpackとは
webpackは「モダンなJSアプリのためのモダンなモジュールバンドラー」だそうです。
https://webpack.js.org/concepts/
ざっくり理解しているところだと、
- JSにモジュールの概念が来る(commonJSとかES6を使える)
- モジュールの依存関係をよしなにしてくれる
(gulp系のタスクランナーやrails標準装備のsprocketsは依存関係を気にしないといけない) - JS以外のassetもまとめて扱える(CSS,画像,htmlテンプレートetc)
- タスクランナーと連携できる
やったこと
あまり関係ないですが、一応今回の環境です。
- cakePHP
- node 6.4
ポイント
今回のポイント
- すでに稼動しているプロダクトに導入する
- モダンなJSアプリではない
すでに稼動しているプロダクトに導入する
すでに稼動しているのである程度体系だったもの、依存ライブラリがあります。
フロント(SCSS)のコンパイルにはgulpを使っています。
なので、$ gulp
でまとめて出来ちゃえばいいなぁと、webpackをgulpと連携させることにしました。
また、依存ライブラリに関しては可能な限りnpm管理に寄せます。
jQueryやbootstrapなど、サーバに手動で配置する形になっていましたが、npmで管理してコストを下げ、webpackとの連携をしやすくします。
npmを選んだ理由としては、webpackはライブラリ管理にnpmを推奨しているのでnpmです。
が、bowerとの連携もできます。
usage with bower
webpackでbower使って外部ライブラリの依存解決する - Qiita
モダンなJSアプリではない
モダンなJSアプリ(ReactとかAngularとか)を触ったことがないのでなんとも言えませんが、
当方はcakePHPで作られたJSがなくてもなんとかなりそうなウェブサイトです。
何が言いたいかというと、たくさんのJSを1つにまとめてブラウザにキャッシュさせるというのはそこまで求められていない(はず)です。
むしろ、たくさんのページがあるのでどのページで何を使っているのかをはっきりさせたかったです。
なので、1Viewファイルに対して1JSファイルという構成にしました。
Viewで読み込むJSは原則1つとして、JSコードを書くときはJSファイルだけ気にすればいいという状態です。
(レイアウト用にもう1ファイルあってもいいかもしれませんね。)
導入
npmのインストールは前提とします。
nodeのバージョン管理にはn
が便利でした。
nodeのバージョンをnで管理する - Qiita
今回はgulp連携しますが、gulpは導入済みとします。
npm から gulp インストールまでの手順 - Qiita
npm install -s webpack@1.3 # webpackインストール
npm install -s webpack-stream # webpackとgulp連携
npm install -s vinyl-named # ディレクトリ構成を維持する
webpackの設定を書きます。
いくつか注意点があります。
- modulesDirectories:npmやbower以外のパッケージ管理を使う時は、エンドポイントを指定します。
npmとbowerはデフォルトで指定されています。
だからと言って今回のように併用する場合、modulesDirectories: [vendorPath]
としてしまうとmodulesDirectoriesが上書きされて、npmのライブラリが認識されなくなります。 - devtool:ソースマップの方式を指定します。詳細はこちらdevtool
- webpack.ProvidePlugin:jQueryプラグインなど、ある環境が前提となる場合に、プラグインが参照する値を指定できます。
var path = require('path');
var webpack = require('webpack');
var vendorPath = path.join(__dirname, 'vendor'); // npmにない外部ライブラリ置き場
module.exports = {
resolve: {
modulesDirectories: ['node_modules', vendorPath], // モジュールの保存ディレクトリを指定
alias: {
prettyLoader: path.join(vendorPath, 'jquery.prettyLoader.js'), // npm管理外ライブラリのrequireの時のalias
'bootstrap-datetime-picker-ja': 'bootstrap-datetime-picker/js/locales/bootstrap-datetimepicker.ja.js' // npm管理下だけどrequireで素直に取れないjsのalias
}
},
devtool: '#source-map', // ソースマップの方式 inline-source-mapでインライン出力
plugins: [
new webpack.optimize.UglifyJsPlugin({ // minify
compress: {
warnings: false
}
}),
new webpack.ProvidePlugin({ // jQueryプラグインのための設定
$: 'jquery',
jquery: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
})
]
};
gulpの設定を追記します。
webroot/module/
以下にcakePHPのViewディレクトリを踏襲した構造を用意して、Viewファイルに対応するJSファイルを配置しています。
View/AdminMail/index.ctp
に対応するJSは
webroot/module/admin_mail/index.js
という感じ。
コンパイルされたJSは webroot/js/admin_mail/index.js
ソースマップはwebroot/js/admin_mail/index.js.map
に出力します。
var webpack = require('webpack-stream');
var named = require("vinyl-named");
gulp.task('webpack', function() {
return gulp.src("webroot/module/**/*.js", {base: "webroot/module"}) // コンパイル対象のディレクトリ
.pipe(named(function(file) { // ディレクトリ構成を維持する
return file.relative.replace(/\.[^\.]+$/, '');
}))
.pipe(webpack(require("./webpack.config.js"))) // webpackの設定ファイルをrequireする
.pipe(gulp.dest("webroot/js/")); // 最終成果物の出力先
});
gulp.task('default', ['webpack']); // ここはよしなに
ここまできたらJSを書きます。
先ほどのルールに従い、webroot/module/
以下に配置していきます。
webpackが扱えるJSのバージョン(?)はES6、CommonJs、AMDがメインのようです。
今回のwebpack v1.13ではES6を読み込むためにはloaderが必要になる(&ES6を使いこなせない)のでCommonJsで書きます。
ES6使うならこの辺が良さげ。webpack+babel環境でフロントエンドもES6開発 - Qiita
最後にコンパイル。
gulp
まとめ
webpack導入によって、レガシーシステムが一歩モダンシステムに近づきました。
今回のケースはルールの整備から入りましたが、僕はあまり長期的な開発経験がないのであまりいいものではないかもしれません。
もっと細かくしっかりと考察された記事があるので貼っておきます。
レガシーシステム上のJavaScriptをモダンビルドにしていくwebpack利用実例 - Qiita
今回のようなJSの話に限りませんが、開発する時のルール決めって大事だなと改めて感じました。
次はCSSの環境改善に取り組もうと思っています。
情報求む
1つどはまりしている問題があります。
ソースマップを別ファイルにした時にchromeでソースマップの読み込みができません。firefoxでは読み込めます。
devtool: '#source-map'
です。'inline-source-map'
や'eval'
など、インラインのものは表示してくれます。
X-SourceMapヘッダを使ったやり方も試しました。が、同じ状況です。
主にこちらの記事を参考にさせていただいています。
SourcemapをRequest Headerから指定させて捗らせる話 - Qiita
ソースマップファイルのURLを直接ブラウザからアクセスすると何の問題もなく表示されます。
webpackのバージョンを1.12にしてもダメでした。
今回の記事を書いた理由の50%はこれです。
詳しい方お願いします。