成果物
はじめに
「ES6」とタイトルに書きながら TypeScript です。
まあES6スーパーセットだし、設定ファイルをちょっと書き換えれば traceur も babel も使えるんで。
動機
- ぼくのかんがえたさいきょうのElectron を読んでから、Electron をさわってみたくなった
- 仕事で
main-bower-files
で取ってきたモジュール群をgulp-inject
し、production 向けにはuseref
して js をuglify
、scss/less をcsso
などとしていたが、こんな自らのラビリンスエンジニアっぷりが心底嫌になった
つまりこういうことだ。
gulp を使う際は、
- gulp.src で対象ソースを選択
- pipe でコンパイルや minify など、やりたいことをやる
- pipe で gulp.dest で配置
のようなタスクを作ると思うが、これに依存関係解決が入ると相当な複雑さになる。
ぼくのかんがえたさいきょうのElectron では、bower は専ら bootstrap などのスタイル系のものの管理だけで利用していて、開発時・パッケージング時ともに main-bower-files
で集められて html に inject される。
js は、開発時は babel
で transpile しながら アウトプットを watch、パッケージングの際は packages.json の dependencies に書かれているものを自前の gulp タスクで集めてきて browserify
で minify ということをやっている。
この中で、
- main-bower-files や自前のタスクで依存ファイルを集めてくる
- 依存ファイル を html に inject
- アウトプットを watch
の部分をどうにかしたい。なぜなら、それこそが gulp タスク群が迷宮化する原因だからだ。
そこで jspm
元々 Angular2 は今回の Electron とは関係なく興味を持っていた。Electron で遊ぶために前述の「さいきょうのElectron」を、View のみをターゲットとする React から Angular1.x or Angular2 にカスタムする予定だったが、本家の 5 Min Quickstart に出てくる jspm ってなんだろうと調べていくうちに、es6 や TypeScript をランタイムでトランスパイルしながら表示できるということを知り、「これだ!」と感じて手を出すことになる。
メリットは、以下のように思われる
- gulp でパズルを一切作ることなく、LiveReload 可能な開発環境を作ることができる
- production 向けには、
jspm bundle-sfx
という、予めアプリで import している(つまり利用している)ものを芋づるで引っ張ってきて 1 ファイルにバンドルしてくれるという機能1がある
ボイラープレート詳解
開発時
今回作成したボイラープレートには個人的な使い勝手からいくつかタスクを足しているので数は多くなっているが、jspm + Angular2 に限れば、実際に開発を行うにあたって必要最小限のタスクは以下のみ。
gulp.task('serve', function() {
// start http server
gulp.src(conf.paths.src)
.pipe($.webserver({
host: '0.0.0.0',
port: 3000,
livereload: true,
open: 'http://localhost:3000/index.dev.html'
}));
});
つまり gulp からは webserver
だけ扱えばよい。あとは jspm がよしなにやってくれる。
gulp でのコンパイル不要、ソースディレクトリそのものである conf.paths.src
をドキュメントルートにしているなど、TypeScript を利用しながらも驚異のシンプルさ。
electron-connect
も使う場合はこちら。
/**
* Transpile scripts for Electron
* src: js(es6) files for Electron
* dest: serve dir
*/
gulp.task('transpile:electron', function () {
return gulp.src(conf.paths.srcElectron + "/**/*.js")
.pipe($.plumber(conf.errorHandler))
.pipe($.sourcemaps.init())
.pipe($.babel({stage: 2}))
.pipe($.sourcemaps.write('.'))
.pipe(gulp.dest(conf.paths.serve))
;
});
/**
* privides an electron server that loads an angular app run by JSPM
* root: serve dir
* approot: src dir for application
* watching:
* - src files for Electron
* - all ts,html,less files under src dir
*/
gulp.task('serve:electron', ['transpile:electron'], function () {
// switch the pathToApp for BrowserWindow.loadUrl(url) according to the value of NODE_ENV
$.env({
vars: {
APP_RELATIVE_PATH: path.relative(conf.paths.serve, conf.files.indexFileDev)
}
});
var electron = electronServer.create({
path: conf.paths.serve + "/main.js"
});
electron.start();
// watch electron src and re-transpile
gulp.watch([conf.paths.srcElectron + '/**/*.js'], ['transpile:electron']);
// watch serve dir and restart electron
gulp.watch([conf.paths.serve + '/**/*.js'], electron.restart);
// watch app src and reload electron
gulp.watch([
conf.paths.src + '/*.ts',
conf.paths.src + '/*.html',
conf.paths.src + '/*.less',
conf.paths.src + '/!(jspm_packages)/**/*.ts',
conf.paths.src + '/!(jspm_packages)/**/*.html',
conf.paths.src + '/!(jspm_packages)/**/*.less',
], electron.reload);
});
後述するが、Electron 関連のソースは ぼくのかんがえたさいきょうのElectron と同様の手法で取り扱っているので、ここは babel
が走る(transpile:electron
タスク)。conf.paths.serve
という、テンポラリなディレクトリを watch しなければいけないのも、今は慣れつつあるがラビリンスの滲出。
Production 向け
以下は、gulp/build.js
からの抜粋。
/**
* create Self-Executing (SFX) Bundles
*/
gulp.task('build:jspm:bundle-sfx', $.shell.task([
// create command string with array.join()
['jspm bundle-sfx',
paths.jspmBundleTargetModule,
paths.jspmBundleOutFile,
(process.env.JSPM_SFXOPTS_SKIP_SOURCE_MAPS == true) ? '--skip-source-maps' : '',
(process.env.JSPM_SFXOPTS_MINIFY == true) ? '--minify' : ''
].join(' ')
]));
/**
* build, minify and locate html files
* src: html files for application
* dest: dist dir
*/
gulp.task('build:html', function() {
return gulp.src([
// select all html files
conf.paths.src + "/**/*.html",
// exclude
"!" + conf.files.indexFileDev
])
.pipe($.plumber(conf.errorHandler))
.pipe($.minifyHtml({
empty: true,
spare: true,
quotes: true,
conditionals: true
}))
.pipe(gulp.dest(conf.paths.dist));
});
/**
* build, minify and locate style files
* src: less for application
* dest: dist dir
*/
gulp.task('build:style', function() {
return gulp.src(conf.paths.src + "/index.less")
.pipe($.plumber(conf.errorHandler))
.pipe($.less())
.pipe($.cssmin())
.pipe(gulp.dest(conf.paths.dist));
});
build:jspm:bundle-sfx
タスクは、少し見にくいが jspm コマンドの文字列を join で生成して shell に投げているだけ。
html, less はコンパイルして conf.paths.dist
に配置しているだけで、inject や usemin は行っていないので比較的見やすいのではないかと思う。これらは、別途用意した production 用の index.html
から静的なパスで参照されている。
asset に関するタスクはいらなかったので作らなかったが、もし画像ファイルを使いたいという場合も
import 'some/image.png!image'
みたいにイメージを js に bundle できる。
惜しいのは build:style
を作ってしまったことで、これは現在の jspm には、preprocess が必要なスタイルファイル(scss とか less とか stylus とか)用の loader がないことに起因する。css だけならイメージと同じように import 可能。
そのため今回作成したボイラープレートでは、開発環境では less.js
を読み込んでクライアントサイドで transpile、production 向けには断腸の思いで上述の build:style
を書いた。
この問題は、[QUEST] Less Plugin #241 にあるように、コミュニティによるプラグインの開発が待たれる。webpack くらい loader が豊富になれば更にタスクが減るだろう。
その他
serve:*
タスクについて
gulp/serve.js
を見ていただければわかると思うが、serve:
系のタスクをいくつか作っている。
serve
は、素の jspm で動くので、ブラウザリロードの度に読み込むファイル数がとにかく多い。
逆に serve:dist
は、production 用の構成を作っているので、ブラウザリロードは高速だが TypeScript の変更をトリガに jspm bundle-sfx
が走るとかなり待たされる。ただし、html, less の編集だけならすこぶる早い。
という特徴を利用して、TypeScript の編集メインの作業なら serve
、html や lessの編集メインなら serve:dist
を使えばよい。
serve:electron
は serve
と同じ戦略で、electron によってラップされる。
serve:dist:electron
は最終確認用。watch 自体していない。
serve:dist
タスクで今後やるかも?
まだ本格的なアプリ開発を行っていないため jspm で動的ロードする serve
タスクでも十分な速度で LiveReload できてはいるが、今後規模が大きくなれば「部分的な再コンパイル」が必要になると思われる。
理想は、変更検知とバンドルファイルを3つほどにわけること。
- Angular2 や moment のような外部のライブラリは
vendor.js
として、watch の対象から外す - アプリの core 部分は、単体で bundle する
- アプリの core 部分以外(Angular2 ならコンポーネント類?)も、単体で bundle する
こうするとおそらく serve:dist
しながら快適に TypeScript も編集できる。
ただ当然タスクのシンプルさを犠牲にすることになるが、そこは jspm の力をフルに使いたい。
$ jspm bundle-sfx vendor + vendorDev ./dist/vendor.js
$ jspm bundle-sfx appcore ./dist/vendor.js
$ jspm bundle-sfx app - appcore - vendor ./dist/vendor.js
のように arithmetic なバンドリングが可能なので、bower と inject を駆使するよりはずっとシンプルに書けるはず。
気が向いたら挑戦しようと思う。
-
正確には、jspm が依存している systemjs の機能 ↩