モダンなクライアントサイドの開発環境を整えたいと思い、とりあえず名前をよく聞くGulpとBrowserifyを検討してみました。が、結論を言うと、僕のプロジェクトの場合、MakefileとWebpackだけで十分そうでした。
開発スタイルを向上したくて検討することにしました
僕のプロジェクトでは、今まで素のJSとjQueryとの組み合わせだけでクライアントサイドを作っていましたが、SPAにしたいReact使いたいというモチベーションが出てきて、クライアントサイドの開発スタイルを整備する必要性が出てきました。
いいきっかけなので、次の点を整備をしたいと思いました。
- type-safeなAltJSを使いたい ← これからはクライアントサイドでもロジックがガリガリ書かれそうなので、コンパイルがあってかつ型安全な言語を採用したいと思いました。
-
コマンド一発でbundle.jsが作れる ← 今まではHTMLに
<script>
タグを沢山並べていたのですが、JS同士の依存関係が難しくモジュール化を諦めていました。その結果コピペコードがあります。今回はモジュール化をちゃんとしてコピペコードを減らし、モジュールの依存関係も気にせずビルドできるようにしたいと思います。 - コードは静的解析したい ← 今までJSで書いたコードは、コードレビューがなかった時期もあり、変数名がsnake_caseだったりcamelCaseだったり開発者によってばらつきがあったり、使ってない変数が放置されていたりと、キレイとはいえないコードになっていました。今後は、コードを静的解析して、きれいなコードが常に作られるようにしたいと考えています。
TypeScript + ???
先にAltJSを選定しました。第一候補はScala.jsでした。バックエンドをScalaで書いているので、UIでもScalaが使えたら、言語脳のスイッチがないので開発効率が上がりそうだと思ったからです。しかし、Scala.jsの情報の少なさがネックになり、今回は不採用になりました。
次に検討したのはTypeScriptでした。JavaScriptと構文がよく似ているTypeScriptは、今まで普通にJavaScriptを書いたチームがScalaの次に馴染めそうな言語ということ、情報が十分にあることが決め手となり採用することにしました。
TypeScript + Gulp + Browserify
AltJSをTypeScriptにすることに決めたので、次にやるべきことはビルド管理ツールの選定です。
これまで「GruntよりGulpがシンプル!」「Browserify便利」といった情報を目にしていたので、特に競合を調べることもなく、GulpとBrowserifyを使ってみようと思いました。
GulpもBrowserifyも今回初めて試しました。試行錯誤しながら作ったgulpfile.jsがこれです。たぶん、今後使うことはないと思いますが、せっかくなので載せておきます。
'use strict';
var gulp = require("gulp");
var typescript = require("gulp-typescript");
var uglify = require("gulp-uglify");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var buffer = require("vinyl-buffer");
var tslint = require("gulp-tslint");
var sourcemaps = require("gulp-sourcemaps");
// Lint TypeScript code
gulp.task("tslint", function() {
gulp.src(["./app/assets/typescripts/*.ts"])
.pipe(tslint())
.pipe(tslint.report("verbose"));
});
// TypeScript compile
gulp.task("typescript", function() {
gulp.src(["./app/assets/typescripts/*.ts"])
.pipe(typescript({
noImplicitAny : true,
target: "ES5",
module: "commonjs"
}))
.pipe(gulp.dest("./target/javascripts"));
});
// Browserify concat
gulp.task("browserify", function() {
browserify({
entries: ["./target/javascripts/auth.js"]
})
.bundle()
.pipe(source("bundle.js"))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest("./public/javascripts"));
});
// Browserify with minify
gulp.task("browserify-minify", function() {
browserify({
entries: ["./target/javascripts/auth.js"]
})
.bundle()
.pipe(source("bundle.min.js"))
.pipe(buffer())
.pipe(uglify())
.pipe(gulp.dest("./public/javascripts"));
});
// auto reloading
gulp.task("watch", function() {
gulp.watch("./app/assets/typescripts/*.ts", ["dev"]);
});
gulp.task("dev", ["tslint", "typescript", "browserify"]);
gulp.task("build", ["tslint", "typescript", "browserify-minify"]);
gulp.task("default", ["dev"]);
このgulpfileでは、次のことができるようになっています。
- TypeScriptのコンパイル
- TypeScriptの静的解析
- 本番用JSのminify
- bundle.jsの生成
- source mapの生成(これはうまくできてないかも)
欲しいと思った仕組みはひと通りそろったのですが、いくつか難点がありました。
- テンプレートのHTMLファイルを
require
する方法が分からなかった - 複数のbundle.jsを生成する方法が分からなかった
- watchタスクが一回failすると自動ではリスポーンしない
TypeScript + Webpack
Browserifyを調べていて偶然見つけたのがWebpackでした。WebpackはBrowerifyの競合のようですが、僕がGulpでやりたかった「静的解析」や「source map」の生成などもひと通りできることが分かり試してみることにしました。
下が、Webpackでやりたいことを実現したコードです。Webpackはwatchコマンドがデフォルトであったり、特にプラグインを入れなくてもsource mapが作れることもあり、gulpfileに書いた半分くらいの量で、ビルドの方法を定義することができました。
module.exports = {
entry: "./assets/javascripts/entry.js",
output: {
path: "./public/javascripts",
filename: "bundle.js"
},
devtool: "source-map", // source-mapを生成する
module: {
preLoaders: [
// TypeScriptで使ってない変数などを警告してくれる
{ test: /\.ts$/, loader: "tslint" }
],
loaders: [
// requireでcssが読み込める
{ test: /\.css$/, loader: "css?minimize"},
// jadeファイルを読み込んだ場合にjade-loaderを使用する。
{ test: /\.jade$/, loader: 'jade' },
// requireでtsが読み込める
{ test: /\.ts$/, loader: "awesome-typescript-loader?emitRequireType=false&library=es6" }
]
}
};
また、Webpackのwatchはfailしても自動的に再開されるので、Gulpで悩んでいた課題の1つが自然に解決しました。
Webpackは「なんでもrequire
してロードすることができる」をひとつのアピールポイントにしているようで、HTMLテンプレートをbundle.jsに含めて使えるようにするということも達成できました。もしかしたらBrowserifyもこれと同じことができるのかもしれません。
TypeScript + Webpack + Gulp?
やりたいことがひと通りできると分かったWebpackですが、それでもタスク管理ツールではないようで、GruntやGulpのようにタスクを定義してシンプルなコマンドで実行するには向いてないと思います。
例えば、本番用のJSはminifyしたいですが、minifyは時間がかかる処理なので、開発時はオフにしたいです。これをWebpackだけでやろうとすると、設定ファイルを複数作って、使う設定ファイルを切り替えることになるかと思います。
本番用JSを生成するためのwebpack設定ファイル
var webpack = require('webpack');
var config = require("./webpack.config.js");
config.plugins = [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
// デバッグコードに使われているconsole.logを取り除く
drop_console: true
}
})
];
module.exports = config;
開発時は次のコマンドを使います。
./node_modules/.bin/webpack --watch
一方、本番用JSを生成するときは次のコマンドになります。
./node_modules/.bin/webpack --config webpack-build.config.js -p
思いのほかコマンドが長くなったので、ここはGulpと組み合わせてタスクにしようかと思いました。GulpとWebpackを組み合わせるプラグインもあるようです。
TypeScript + Webpack + Makefile!
しかし、ここまでコマンドになっているなら、GulpよりもMakefileのほうがシンプルだと思ったので、Makefileを作ることにしました。そのMakefileがこちらになります。
watch:
./node_modules/.bin/webpack --watch
bootstrap:
npm install
./node_modules/.bin/tsd reinstall -so
build:
./node_modules/.bin/webpack --config webpack-build.config.js -p
clean:
rm public/javascripts/bundle.*
destroy:
rm -rf node_modules typings
このMakefileでは、npmでモジュールのインストールや、TypeScriptのtsdをインストールもできるようにしています。
Gulpを使えば、JavaScriptの複雑なコードを実行できたり、ライブラリを使ってアーティファクトを加工するなどやれることは多そうですが、今回僕がやりたかったことには、そういったことがなかったので、Makefileに収めてしまうことにしました。今後、Gulpを使いたくなったらMakefileからGulpを呼び出せばいいかなとも思います。
ちなみに今回検証用に作った、TypeScript + Webpack のサンプルプロジェクトはsuin/webpack-typescript-exampleで公開してます。