俺、サービス売って家買うんだ

Swift, Kotlin, Vue.js, 統計, GCP / このペースで作ってればいつか2-3億で売れるのがポっと出来るんじゃなかろうか

jspmとSystemJSを利用したモジュール管理

f:id:ie-kau:20160217225213p:plain


さて、RequireJS、Browserify、webpackなど様々なクライアントサイドの依存管理ツールが出ていますが今日はjspmについて紹介します。

jspmとは

jspmとはSystemJSを利用したクライアントサイドのモジュールローダーのためのパッケージ管理システムです。不明な言葉が連続しますね、そもそものSystemJSとは何なのでしょうか?

jspm

SystemJSとは

SystemJSとはES6 modules,AMD,CommonJS,global scriptなどこれまでに登場してきた様々なJavaScriptのモジュール管理方法を普遍的な形で利用できるモジュールローダーです。また、Traceur CompilerとBabelなどのtranspilerを利用して使うことが前提とされています。

SystemJSは、Browserifyとは異なりビルドを必要とせず開発中は小分けしたファイルを読み込みつデバッグをすることができ、リリース時に全ファイルをまとめてビルドして配布することが可能になっています。この辺りに関してはRequireJSによく似ていますね。JavaScriptの開発規模が大きくなってきた際、ファイル更新のたびにビルドを行っていると反映まで若干のラグができブラウザを更新した時にまだビルドが終わっていないということもしばしば発生します。そのため、できるだけビルドの必要なく開発したいという機会は多くあると思われます。そんな時に便利です。

それでは利用方法を見て行きましょう。

SystemJS

インストール

まずはCLIをインストールします。

sudo npm install jspm -g

プロジェクトディレクトリに移動してバージョンを固定しましょう

cd [Project Name]
npm install jspm —save-dev

初期化すると色々聞かれます。 自分の設定は以下の通です。 * 自分が利用したいプロジェクトはRailsだったのでpublic/jsをbaseのURL * Traceur Compilerは利用したことがないのでtranslaterはBabel

 jspm init

Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:yes
Enter server baseURL (public folder path) [./]:public/js
Enter jspm packages folder [public/js/jspm_packages]:
Enter config file path [public/js/config.js]:
Configuration file public/js/config.js doesn’t exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:/js
Which ES6 transpiler would you like to use, Traceur or Babel? [traceur]:Babel

続いてプロジェクトで利用しそうなライブラリも入れておきましょう。 今回は適当にこんな感じですかね。インストールすると自動でpackage.jsonに保存されます。

jspm install jquery
jspm install lodash
jspm install vue

実際に呼ばれるコードを定義します。 最初なので各ライブラリの読み込みとバージョンの出力を行ってみましょう。

public/js/app.js

import $ from ‘jquery’;
import _ from ‘lodash’;
import Vue from ‘vue’;

console.log($);
console.log(_);
console.log(Vue);

app/viewlayouts/application.html.erb

<script src=“js/jspm_packages/system.js”></script>
<script src=“js/config.js”></script>
<script>                                                                                                                   
System.import(‘app’);
</script>

これだけでサーバーを起動してリロードすると、Chromeのconsoleに、以下が表示されるはずです。

app.js!eval:16 jQuery(selector, context)
app.js!eval:17 lodash(value)
app.js!eval:18 Vue(options)

プロダクション用ビルド

ではリリース時はどのようにすればよいのでしょうか? 現状のままでは大量のJavaScriptファイルを読み込んでいるのでロードに非常に時間がかかります。また、この記事の指定方法ではBabelのruntime用スクリプトも読み込まれており、IE9でこれが正しく動作しませんでした。

公式では以下のようなやり方で対応しています。
Production Workflows · jspm/jspm-cli Wiki · GitHub

まずはコマンドでバンドルしたファイルを作ります。

jspm bundle app public/js/bundle.js

そしてHTML側では以下のように書きます。

<script src=“js/jspm_packages/system.js”></script>               
<script src=“js/config.js”></script>
<script src=“js/bundle.js”></script>                             
<script>
  System.import(‘app’);                                          
</script>

本番ならこんな感じでしょうか?

<script src=“js/jspm_packages/system.js”></script>
<script src=“js/config.js”></script>
<% if is_production? %>                                                                                                          
<script src=“js/bundle.js”></script>
<% end %>
<script>
System.import(‘app’);
</script>

bundle.jsすら読み込みたくない場合は —injectオプションを付けます。

jspm bundle app public/js/bundle.js  —inject

この場合config.jsに以下の具合でバンドルされているファイルの一覧が記録されます。

  “bundles”: {
    “bundle”: [
      “github:components/jquery@2.1.4/jquery”,
      “npm:process@0.10.1/browser”,
      “npm:vue@0.12.1/src/util/lang”,
      “npm:vue@0.12.1/src/util/env”,
      “npm:vue@0.12.1/src/config”,
      “npm:vue@0.12.1/src/util/misc”,
      〜 略 〜
      “npm:vue@0.12.1/src/vue”,
      “npm:vue@0.12.1”,
      “app”
     ]
  }

こうすることにより、System.import()が呼ばれるタイミングで必要なbundleファイルをロードしてくるのでプログラム側でどのモジュールを呼んでくるか制御しやすくなります。

これだけでbundle.jsが呼ばれる。

<script src=“js/jspm_packages/system.js”></script>
<script src=“js/config.js”></script>                                                                                              
<script>
System.import(‘app’);
</script>

バンドルをやめる場合は。

jspm unbundle

1ファイルにする
System.js, config.jsもまとめて1ファイルにするときは以下のコマンドです。最後の紹介となりましたがこの方法が一番現実的ですよね。

jspm bundle-sfx app public/js/dist/app.js

※sfxはself-executingの略。
※デフォルトではglobalでの依存管理になっている。 https://github.com/systemjs/builder/blob/10091795272d6429660f993b1833acfcc792d6f0/lib/builder.js#L31

オプションは

  • —skip-source-maps
    • ソースマップを作らない
  • —minify
    • ミニファイする

Gulpで管理する
ここまで来たらGulpやGruntで管理したいですね。ということで、jspm-cliが内部的に利用しいるsystemjs-builderというのを使おうとおもったのですが。。。。

※参考 How to start writing apps with ES6, Angular 1.x and JSPM

インストール

npm install systemjs-builder
npm install babel-core

gulp.coffee

# jspm
gulp.task ‘bundle’, ->
  Builder = require ‘systemjs-builder’
  builder = new Builder()
  builder.loadConfig(‘./public/js/config.js’)
    .then ->
      builder.buildSFX ‘app’, ‘./public/dist/app.js’, {sourceMaps: false,  minify: true}

エラー

Press ENTER or type command to continue
[16:41:42] Requiring external module coffee-script/register
[16:41:44] Using gulpfile ~/git/[Project Name]/gulpfile.coffee
[16:41:44] Starting ‘bundle’…
[16:41:52] Finished ‘bundle’ after 8.51 s
[16:41:52] ‘bundle’ errored after 8.51 s
[16:41:52] Error: task completion callback called too many times
  at finish (/home/vagrant/git/[Project Name]/node_modules/gulp/node_modules/orchestrator/lib/runTask.js:15:10)
  at /home/vagrant/git/[Project Name]/node_modules/gulp/node_modules/orchestrator/lib/runTask.js:43:4
  at lib$rsvp$$internal$$tryCatch (/home/vagrant/git/[Project Name]/node_modules/systemjs-builder/node_modules/rsvp/dist/rsvp.js:489:16)
  at lib$rsvp$$internal$$invokeCallback (/home/vagrant/git/[Project Name]/node_modules/systemjs-builder/node_modules/rsvp/dist/rsvp.js:501:17)
  at lib$rsvp$$internal$$publish (/home/vagrant/git/[Project Name]/node_modules/systemjs-builder/node_modules/rsvp/dist/rsvp.js:472:11)
  at lib$rsvp$asap$$flush (/home/vagrant/git/[Project Name]/node_modules/systemjs-builder/node_modules/rsvp/dist/rsvp.js:1290:9)
  at process._tickCallback (node.js:355:11)

とまぁ、このエラーが解決できずこれ以上深追いしないことに決めました。 また時間あるときにちゃんと読もうと思います。

ということで面倒になったのでシェルで全てを解決することに。 逆にこれはこれでシンプルになったので良かったのではと。。

gulp.coffee

gulp.task ‘bundle’, shell.task([‘jspm bundle-sfx app public/js/dist/app.js —skip-source-maps —minify’]) 

その他、今のところよくわかってない問題

  • bundleしたライブラリ群のライセンスどうするんだろか?
    • minifyオプションが自動で保存してくれてるわけじゃないっぽいのでまさか手動?

使って見てる感想

  • ある程度の規模のSPAとかとなってくるとビルドに時間がかかり始めるのでビルドが必要ないRequireJSの方が扱いやすいと思っていたこともあり、未来志向+扱いやすさといった点で、今後はこれを使っていきたいなーと思います。(細々問題を解決しつつ)
  • 1ファイルにしてしまえばローディングもあまり気にならない。
  • bundle-sfxしないとIE9で動作しない。ちょくちょくbuildしないと本当に動いているかわからないのが超怖い。
  • 本題から逸れますがtranspilerについて、本当はCoffeeScriptが好きなのですが、ES6、7のtranspilerと比較されると将来のために今後そっち使っていこうかなって気がする。IE8を切れる案件やスマホオンリーな案件も増えてきたし。

オラッ!オラオラッ!

関連書籍

いまから始めるWebフロントエンド開発

いまから始めるWebフロントエンド開発