jspmを使ってシンプルな構成でES6アプリをつくる(ついでにElectron)

成果物
======

https://github.com/yuki-takei/electron-angular2-boilerplate/

![ss_20150825.png](https://qiita-image-store.s3.amazonaws.com/0/41678/fa9cc8ba-713d-1b49-4e0b-c0ec69194284.png)

はじめに
=======

「ES6」とタイトルに書きながら TypeScript です。
まあES6スーパーセットだし、設定ファイルをちょっと書き換えれば traceur も babel も使えるんで。

動機
====

* [ぼくのかんがえたさいきょうのElectron](http://qiita.com/Quramy/items/90d61ff37ca1b95a7f6d) を読んでから、Electron をさわってみたくなった
* 仕事で `main-bower-files` で取ってきたモジュール群を `gulp-inject` し、production 向けには `useref` して js を `uglify`、scss/less を `csso` などとしていたが、**こんな自らのラビリンスエンジニアっぷりが心底嫌になった**

つまりこういうことだ。

gulp を使う際は、

1. gulp.src で対象ソースを選択
2. pipe でコンパイルや minify など、やりたいことをやる
3. pipe で gulp.dest で配置

のようなタスクを作ると思うが、これに依存関係解決が入ると相当な複雑さになる。

[ぼくのかんがえたさいきょうのElectron](http://qiita.com/Quramy/items/90d61ff37ca1b95a7f6d) では、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](https://angular.io/docs/js/latest/quickstart.html) に出てくる jspm ってなんだろうと調べていくうちに、es6 や TypeScript をランタイムでトランスパイルしながら表示できるということを知り、「これだ！」と感じて手を出すことになる。

メリットは、以下のように思われる

* gulp でパズルを一切作ることなく、LiveReload 可能な開発環境を作ることができる
* production 向けには、`jspm bundle-sfx` という、予めアプリで import している(つまり利用している)ものを芋づるで引っ張ってきて 1 ファイルにバンドルしてくれるという機能[^1]がある

ボイラープレート詳解
==================

開発時
------

今回作成したボイラープレートには個人的な使い勝手からいくつかタスクを足しているので数は多くなっているが、jspm + Angular2 に限れば、実際に開発を行うにあたって必要最小限のタスクは以下のみ。

```gulp/serve.js
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` も使う場合はこちら。

```gulp/build.js
/**
 * 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))
  ;
});
```

```gulp/serve.js
/**
 * 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](http://qiita.com/Quramy/items/90d61ff37ca1b95a7f6d) と同様の手法で取り扱っているので、ここは `babel` が走る(`transpile:electron` タスク)。`conf.paths.serve` という、テンポラリなディレクトリを watch しなければいけないのも、今は慣れつつあるがラビリンスの滲出。


Production 向け
---------------

以下は、`gulp/build.js` からの抜粋。

```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 に関するタスクはいらなかったので作らなかったが、もし画像ファイルを使いたいという場合も

```hoge.ts
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](https://github.com/systemjs/systemjs/issues/241#issuecomment-70551841) にあるように、コミュニティによるプラグインの開発が待たれる。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 を駆使するよりはずっとシンプルに書けるはず。
気が向いたら挑戦しようと思う。


[^1]: 正確には、jspm が依存している systemjs の機能