こんにちは、オリジナル新作マンガの配信サービス、GANMA!のチームに所属する@paralleltoです。
最近はSwift 3を使ってiOSのクラアントアプリを開発していることが多かったのですが、
ひさしぶりにブラウザ上で動作するフロントエンドの開発環境をいじる機会があったので書いてみます。
前提として、フロントエンドの開発環境ではGruntというタスクランナーを介して
コンパイルやユニットテストを行っています。
今回、Gruntをnpm-scriptsに置き換えるにあたって感じたことをまとめています。
Gruntとは
圧縮、コンパイル、ユニットテスト、lintなどの反復タスクを最小限のコストで自動化するもの。
https://gruntjs.com/
npm-scriptsとは
package.jsonのscriptsフィールドにシェルコマンドやnpm runコマンドを記述し、
コマンドラインからnpm run <script名>で呼び出すことができるもの。
一部の予約されたscript名はrunを省略してnpm <script名>で呼び出すことができる。
予約されたscript名の例:start、restart、stop、test
https://docs.npmjs.com/misc/scripts
なぜGruntをnpm-scriptsに置き換えるのか
フロントエンドの開発環境のレガシー化を防ぐため、TypeScriptを2.xにアップデートしたかった。
現状、TypeScriptのビルドにはgrunt-tsの5.5.1を利用しているが、TypeScriptのサポート状況は1.8となっている。
TypeScript 2.x系にアップデートするためには、grunt-tsの6.0.0-beta.16を利用する必要があるが安定版ではない。
いい機会なのでgruntをnpm-scriptsに置き換える研究をやってみた。
メリット
- Gruntプラグイン作者への依存がなくなる
- Gruntの抽象レイヤーがなくなるためデバッグしやすい
- ツールとGruntプラグインの両方のドキュメントを行き来する必要がなくなる
- タスクランナーの知識を必要としない(npmの基礎だけ知っていれば読める)
デメリット
Gruntをnpm-scriptsに置き換える
ここから具体例を交えてGruntをnpm-scriptsに置き換えていきます。
まずpackage.jsonを見て、依存してるGruntプラグインを確認します。
package.json
{ "devDependencies": { "grunt": "1.0.1", "grunt-chmod": "1.1.1", "grunt-contrib-clean": "1.0.0", "grunt-contrib-compass": "1.1.1", "grunt-contrib-copy": "1.0.0", "grunt-contrib-jasmine": "1.1.0", "grunt-contrib-sass": "1.0.0", "grunt-contrib-symlink": "1.0.0", "grunt-contrib-watch": "1.0.0", "grunt-karma": "2.0.0", "grunt-text-replace": "0.4.0", "grunt-ts": "5.5.1" } }
Gruntが実際にどのプラグインを使っているかはGruntfile.jsを見るとわかります。
Gruntfile.js
module.exports = function(grunt) { grunt.loadNpmTasks('grunt-chmod') grunt.loadNpmTasks('grunt-contrib-clean') grunt.loadNpmTasks('grunt-contrib-compass') grunt.loadNpmTasks('grunt-contrib-copy') grunt.loadNpmTasks('grunt-contrib-sass') grunt.loadNpmTasks('grunt-contrib-symlink') grunt.loadNpmTasks('grunt-contrib-watch') grunt.loadNpmTasks('grunt-text-replace') grunt.loadNpmTasks('grunt-ts') }
あれれ~?おかしいぞ~?次の2つの依存は使われていないようです。削除してしまいます。
- grunt-contrib-jasmine
- grunt-karma
※余談ですがテストはkarmaをコマンドラインから直接呼び出して行っていました。
メンテナンスする人が変わるとこういった消し損ないも出てきてしまうので、
今回のように定期的に見直すことは、開発環境をクリアに保つ上で大切だと思います!
シェルコマンドの置き換え
次に置き換えられそうなプラグインはこれらです。
- grunt-chmod
- grunt-contrib-clean
- grunt-contrib-copy
- grunt-contrib-symlink
ファイルの権限変更、削除、コピー、シンボリックリンク、どれもシェルコマンドで行うことができます。
- chmodコマンド
- rmコマンド
- cpコマンド
- lnコマンド
ではなぜGruntプラグインで行っていたかですが、大きく分けて2つの理由があると思います。
- クロスプラットフォームで動作する(Windows、Linux、Max OS Xなど)
- 用途に特化した簡潔なJavaScriptで記述できる
例えば、globのパターンマッチング(*/.tsなど)を使った複数ファイルの繰り返し処理は、 シェルコマンドで書くよりも、JavaScriptで書いた方がメンテナンス性が高いと思います。
これらを必要としない小さなタスクであればシェルコマンドへ、
そうでなければ同等のNPM packagesに置き換える必要があります。
置き換え例:
- “grunt-chmod" → "shelljs”
- “grunt-contrib-clean” → “rimraf”
- “grunt-contrib-copy” → “cpx”
- “grunt-contrib-symlink" → "shelljs”
そして上記の依存を使ったJavaScriptファイルをそれぞれ用意し、
scripts/chmod.js
const shell = require('shelljs') const files = require('./files.json') // いろいろ処理 files.forEach(file => shell.chmod(444, file)) ...
npm-scriptsからnodeを経由して呼び出せるようにします。
package.json
scripts: { "chmod": "node scripts/chmod.js" }
コマンドラインから実行します。
npm run chmod
watchの置き換え
特定のファイルが変更されたとき、自動的に関連付けられたタスクを実行するのがwatchです。
複雑なことをやっていなければ、TypeScriptなどのコマンドに標準で付属するwatchで何ら問題ないと思います。
ソースファイルに変更があったら再コンパイルする
tsc --watch
今回はgrunt-contrib-watchプラグインを使って複数のGruntタスクを実行していたため、 grunt-contrib-watchをchokidar-cliに置き換えて、以下のようなnpm-scriptsを書きました。
package.json
scripts: { "process1": "何か処理", "process2": "何か処理", "watch": "chokidar 'src/**/*.ts' -c 'npm run process1 && npm run process2'" }
これでsrc配下の.tsファイルに変更があったときに、process1とprocess2が逐次実行されます。
grunt-text-replaceの置き換え
ファイルの内容を書き換えるものでした。
私はnodeのfsを使って同等の機能をJavaScriptで実装しましたが、
おそらく需要薄な内容なので割愛させていただきます。
Sass、Compassの置き換え
SassとはCSSの拡張言語のことです。
CSSと完全互換に加え、変数、ミックスイン、継承などの機能が追加されています。
CompassはSassで構築されたフレームワークで、Webの再利用可能なパターンを利用したり、
スプライトを簡単に作成することができます。
詳しくは本家をご覧ください。
http://sass-lang.com/
http://compass-style.org/
grunt-contrib-sassは非常に便利なプラグインです。
コマンドラインに比べて、複数ファイルをソースファイルに指定でき、
また、出力先の親ディレクトリがない場合は自動的に作成してくれます。
なんとかsassコマンドから同等のことが行えないか挑戦してみましたが、長くなりそうなので断念しました。
断念したモノ
find src -name '*.scss' | sed 's/.scss$//' | xargs -I {} -P 16 sass {}.scss:dist/{}.css -compass --style nested
ここではnode-sass(compass使っている場合はcompass-mixinsを忘れずに)に置き換えて対応しました。
scripts/sass.js
const sass = require('node-sass') const compile = input => { sass.render({ file: input, outputStyle: "nested", includePaths: ["node_modules/compass-mixins/lib/"] }, (err, result) => { // ファイル出力処理 }) }
grunt-contrib-compassは、単にcompassコマンドに置き換えられます。
※compassはconfig.rbに設定をまとめられるのでコマンドラインではシンプルになります。
package.json
scripts: { "css:sass": "node scripts/sass", "css:compass": "compass compass compile --force" }
TypeScriptの置き換え
TypeScript 2.x系を使っている人は、何も考えずにtscコマンドに置き換えられます。
--
はnpm-scriptsを実行する際にカスタムオプションを渡すものです。
https://docs.npmjs.com/cli/run-script
package.json
scripts: { "tsc": "tsc", "tsc:dev": "npm run tsc -- -p ./tsconfig.dev.json", "tsc:prod": "npm run tsc -- -p ./tsconfig.prod.json", "tsc:test": "npm run tsc -- -p ./tsconfig.test.json" }
ただTypeScript 1.x系を使っている人は注意が必要です。
grunt-tsにはreferenceという本家にはない便利なオプションが存在します。
このオプションを使用しているかどうかで置き換えのコストが大きく変わります。
TypeScript 1.x系では外部ファイルを参照する際に、referenceタグを書く必要がありました。
///<reference path="外部ファイルA.ts"> ///<reference path="外部ファイルB.ts"> ///<reference path="外部ファイルC.ts">
これを各ソースファイルに一つ一つ書いていくのは大変なコストだと思います。
grunt-tsのreferenceオプションはこの手間を補うためのオプションになります。
referenceオプションに下記のようなファイルを渡すことで、 各ソースファイルにreferenceタグを書かずともコンパイル時に参照を解決してくれます。
reference.js
///<reference path="外部ファイルA.ts"> ///<reference path="外部ファイルB.ts"> ///<reference path="外部ファイルC.ts">
まとめ
いかがでしたでしょうか。
最終的に、package.jsonのdevDependenciesは以下のようになりました。
パッケージ間の依存がなくなり、パッケージの更新や取捨がやりやすくなりました。
package.json
{ "devDependencies": { "chokidar-cli": "1.2.0", "compass-mixins": "0.12.10", "cpx": "1.5.0", "node-sass": "4.5.3", "rimraf": "2.6.1", "shelljs": "0.7.8", "typescript": "2.4.2" } }
また、今まではGruntfile.jsという大きなJavaScriptに書かれていた処理が、 npm-scriptsの小さなシェルコマンド、またはJavaScriptに分割されたことにより、 Gruntの知識をもたないメンバーでも各タスクの目的と処理がわかりやすくなったかと思います。
package.json
{ "scripts": { "chmod": "node scripts/chmod.js", "process1": "何か処理", "process2": "何か処理", "watch": "chokidar 'src/**/*.ts' -c 'npm run process1 && npm run process2'", "css:sass": "node scripts/sass", "css:compass": "compass compass compile --force", "tsc": "tsc", "tsc:dev": "npm run tsc -- -p ./tsconfig.dev.json", "tsc:prod": "npm run tsc -- -p ./tsconfig.prod.json", "tsc:test": "npm run tsc -- -p ./tsconfig.test.json" } }
タスクランナーは知識を持つ人が、ある程度固定化された環境で使うには強力ですが、 開発環境をモダンに保ちたい、開発環境をさわれるメンバーを増やしたい場合は、npm-scriptsも十分選択肢に入りますね!
参考ページ
今時のフロントエンド開発2017
http://qiita.com/Lyude/items/783a972bace33002455c[意訳]私がGulpとGruntを手放した理由
http://qiita.com/chuck0523/items/dafdbd19c12efd40e2deGrunt/Gulpで憔悴したおっさんの話
https://t32k.me/mol/log/npm-run-script/