ブラウザのES6サポートが急速に良くなってきています。社内ツールとかElectronとか、ブラウザの普及率を気にしなくていい環境ならそろそろ使えるのではないかと思って調べたり試してみたりしています。
更新
これを見るともうほぼ実装は完了していますね。Node.jsも対応していますし使えるブラウザが限定できるならもはや変換なんかしなくても大丈夫。注意点としては以下の2つ。
- IE11は渋い
- ES6 modulesはまだまだ
ソースをES6で書いて、結果もそのままES6という手抜き開発に使えるツールのメモです。手抜きなので、おそらく経年変化の影響はほとんどないはずです。対象としてはブラウザだったり、ElectronでのSPA開発です。
タイトルの「最弱」というのは、なるべく素のといった風味です。普段、毎日JSを書くようなことはしてなくて、たまに書くぐらいなので、ライブラリの変更をおいかけたり、非互換のアップデートを吸収したりはなるべくしたくない。ぱっと書いて他の人に引き継ぎということもありえる。そもそも、1年に一回書くぐらいだと、JSライブラリの場合はLTSサポートが2周切れるとか・・・
この記事は次のことに重点を置いてツール選定をしています:
- ツールはなるべく減らす。ツール間の依存関係も極力なくして完全に独立させる
- なるべく、「このオプションが必要!」みたいな小難しいツールや、複雑な設定ファイルは減らす
- トランスパイラやgulpなどのジョブランナーは使わない
紹介しているツールは増えてきていますが、LintもMinifyも監視してビルドも必要はないです。テストとConcatぐらいは優先度高いと思いますが、それ以外はすべてオプショナルです。小さいコードならConcatも外してもいいかもしれません。
Concat
更新
Uglify.jsのES6対応が入ったのでWebPackも使えるようになりました
node.js上で動くコードを書くのであれば不要です。そうでなければ、1-2画面に収まる短いコードしかかかないのでなければ、構造化のためには現時点ではこのツールは必須です。
なるべくツール依存を減らしてES6で書くと言ってもファイル分割ぐらいはしたいですよね。Browserify、WebPackあたりが主流です。ただ、WebPackはBrowserifyと違って、設定ファイル必須なので、最弱環境一押しはBrowserifyです。ただ、WebPackの方がAMD形式にも対応していたりと少し高機能だったりはします。
Browserifyなので当然common.jsスタイルになります。ES6 modules形式ではなくて、こちらの形式の良いところは主に2つ。
- 次のユニットテストとの相性の良さですね。何も頑張らなくてもnode.jsでテストが動く。ツールの依存が掛け算になるとつらみが倍増していきますが、本番環境のConcatツールとユニットテストで依存関係がまったくないので、そういう心配はなしです。
- 使う環境がElectronだったら、そもそもこのツールすら使わなくても良い。
こんな感じで実行すればOK
$ browserify src/app.js -o js/app.js
WebPackの方がもろもろエコシステムが育ってきたり、ツリーシェイキングでサイズを落とすとか、もろもろの知見が溜まってきています。以前は標準バンドルのuglify.jsがES5しか通さなかったので、TypeScriptかBabelかでトランスパイルしないと使えなかったのですが、今後は大丈夫です。
ES6 Modulesについて: 5/8/2017補足
だいぶES6 modulesの足音が近づいてきました。Safariは10.3で実装済み。Edge/Firefox/Chromeはオプションで有効にすると使えるようになります。この勢いだと半年もするとデフォルトで使えるようになりそうです。
そうなるとconcat関係のツールは不要になるか?というと、不要にはならない気がします。現状のJavaScriptは大量の依存関係を持っています。SPAで逆に低速になりそうなケースとして、データ取得がバラバラに発生して表示に必要なRTT数が増えてしまうことがあります。同じことがES6 modulesでも発生しうると思います。1ファイルにバンドルする必要はなくても、React、Reduxみたいな粒度でいくつかの.jsファイルにまとめつつ、エントリーポイントとなる.jsファイルの先頭に必要な全ライブラリのimport文を記述していく(2RTTで全部簡潔する)といったファイル改変をする必要性はありそう。
あと、最近のnpmパッケージ類はカジュアルに依存を増やしガチなので、Tree Shaking的な必要なコードの抽出機能がないと、アップロードのファイルサイズも大変なことになりそうです。でもって、そういう効率的なES6 modules対応のコード改変を最初にサポートするのはおそらくWebPackやRollupとかのプラグイン式のconcatツールになると思われます。
あと、node.jsのサポートはまだ半年以上かかりそう。
ということで、とりあえずES6 Modulesはnode.jsがサポートし、concatツールが面倒みてくれるまでは脳みそから追い出しておいて良さそうです。concatツールを作って名前を上げたい人はチャンスです。
あるいは、prepackのような最適化対応でやっぱり1ファイルにしようぜというのが主流になる可能性も無きにしもあらず。
トランスパイラ(というかBabel)
Mithrilであれば別にBabelを使わないで開発もできますが、React/JSX、Vue.js、Riot.jsではBabelが必須でしょう。特に、Vue.jsとRiot.jsではランタイムでテンプレート機能を使おうとすると、new Function呼び出しに変換されるので、Content Security Policy違反になってしまいます。事前に変換しておくのが必須となります。
僕は使ってないですが、トランスパイラの呼び出しの今時の呼び出し方はBrowserifyとかWebPackから呼び出す方法でしょう。Gulp/Grunt依存を増やす必要はありません。
BrowserifyからBabelを呼び出すには、Babelifyが、WebPackからBabelを呼び出すにはBabel-loaderというBabel公式なやつがあります。Babelifyならこんな感じですね。
$ browserify src/main.js -t babelify
TypeScriptもtsify、ts-loaderがあります。
ユニットテスト
ユニットテストはまあみんなやりますよね。
フレームワークとしては最近はMochaが人気かと思います。Mochaはテストの構造部分だけで、テストに使うassert機能は別のライブラリを使います。Chaiあたりが海外では人気ですが、日本ではPowerAssertの方が人気です。Mocha + PowerAssertで特にES5時代から困っておらず、ES6でも問題ありませんでした。APIとかも安定しているし、PowerAssertは特にAPIは1つだけ、という世界観なので、頑張らない人にはうってつけです。こんな感じで実行します。MochaでPower-Assertを使う時はintelli-espower-loaderというのを使う必要があります。
$ mocha --require intelli-espower-loader
依存ライブラリがシンプルな方が良ければJasmineの方がシンプルです。assert機能も入っています。assert文の書き方は多少冗長になりますが、Mochaとほぼおなじです(beforeがbeforeAllとかになるぐらい)。explicit is better than implicitを徹底したい人はJasmineも悪くないです。
Linter
軟弱なので、型がない動的言語だと少し不安なのでエラーチェックは欲しいところ。ESLintがES6サポートも充実してて良い感じです。envにcommonjsを追加すれば、common.jsのモジュールでエラーになることはなくなります。設定ファイルは次の通り。
{
"extends": ["eslint:recommended"],
"plugins": [],
"parserOptions": {},
"env": {
"browser": true,
"es6": true,
"commonjs": true
},
"globals": {},
"rules": {}
}
実行はこう
$ eslint src
Minify
更新2
uglify-jsのharmonyブランチが、uglify-esという別パッケージで公開されました。すでにBroserifyのuglifyfyもこれを使うようになったので、ES6構文が解釈できないのでBabel通さないと・・・というのはなくなっているはずです。
$ broserify -t uglifyify src/main.js
ES6のままminifyするbabiliというツールを以前紹介しましたが、これをインストールするとせっかく除外したBabel関連のツールが大量にインストールされてしまってうれしくなかったのですが、これで大分楽になりそうです。
WebPackも使えるようになりますね。
テストサーバ
今までは python3 -m http.server とかやってたんですが、まあJavaScriptで統一した方がいいかな、と思って調べました。ES6だからといって特別な機能はあまりいらないので、なるべくシンプルで実績があるのを探した所、 http-server というのがよく使われているようでしたのでこれにしました。
現在のパスをルートにして8080番ポートでHTTPサーバを起動するには次のようにタイプします。
$ http-server . -p 8080
SPAのフレームワーク
今回は慣れているのでMithrilで。
モノリシックで相性問題とか気にしなくていいSPAとしては、他にもいくつかあります。Riot.jsでも、2.0になったVue.jsも最弱開発環境の選択肢にしていいと思います。時間があったらVue.jsは試したかったです。
Angular.js 2.0はもろもろツールとか制約されそう。Polymerも1ファイルにまとめるあたりでちょっと癖がある感じなので今回はパス。後者はとうとう仮想DOMとの相性問題が解決しそうで、Shadow DOMの実装が進んだら使ってみたいです。Mithril本の時はPolymerの章を書いてたけど、1.0が出たらShady DOMとやらが出てきて、仮想DOMとの相性が破滅して章を泣く泣く削ったのでリベンジしたい。
React一強なら、Reactでもいいかなと思ったんですが、結局Router周りとか、Fluxとかでフレームワーク闘争がいまだ続いている感じなので、もう少し収まるまでは手を出すのはやめようと思ってます。
UI周りはMaterial Designにしようと思ったんですが、使おうと思ったPolytheneがビルドツールとかもろもろ縛りがあって使えなかったのでmithril-mdlにしました。慣れてるのもありますが。
CSSのビルド
ページ数が多くなってきたので、CSSも分割したいと思いました。sass記法、SCSS記法(sassの3.0から追加)、LESS記法、Stylus記法、PostCSS記法などの記法があり、いろんなmixinを詰め込んだconpassがあり、なおかつ処理系もいろいろあります。例えばStylusはsassとLESSも対応していたり。記法とツールといろいろあって難しいですよね。
今まで避けてきたのですが、 @wozozo @ymotongpoo @mopemope に教えてもらってSCSSにしました。
- イケイケなのはPostCSSらしいのですが、プラグインを大量にいれて設定しないといけなくて、プラグインの安定性とか将来性まで管理するのはツライ
- SCSSは書き方がCSSに近い
- ユーザは多くて安定している
- オリジナルのsassはRubyだったが、node-sassでnode.jsで一式揃えられる
scssフォルダにソースを入れてビルドします。
$ node-sass --include-path scss scss/main.scss assets/app.css
まあ基本的にはファイルを分割しておきたいぐらいなので、そんなにハードには使い込まないかも。
監視して自動ビルド
nodemon
JSだけなら別に手でビルドしてもいいんですが、SCSSもビルドするので、自動ビルドを入れてみます。nodemonというのがスタンドアローンで実行できて、特定の拡張子の変更に対してコマンド実行するというシンプルな感じです。
ディレクトリを指定したり、ファイルを指定したり、いろいろできますが、 -e
で拡張子を指定しています。実際に実行するコマンドはOSのコマンドとかなんでもできるんですが、次の項目でpackage.jsonにコマンド書くので、それを実行するようにします。
$ nodemon -e scss -x "npm run build-css"
今回はソースと出力先の拡張子が一緒で、このままだと無限ループしてしまうので、出力先フォルダを--ignore
(-i
)で設定します。
$ nodemon -e js -i js -x "npm run build"
goemon
...というのをやってたんですが、どうもnodemonはCPU消費が多くて、バッテリーがガンガン減る。知り合いに相談したところ、goemonがいいということを聞きました。Go製です。
(from from scratch)
node.jsのファイルI/O周りはスレッドでやっていて、システムコールによる非同期は使ってないようです。ファイル監視系もどうもそんな感じらしい。そのため、監視対象のファイルが増えるとどうしてもCPUを食う。Goの方はfsnotifyを使っているので、ポーリングはしてないはず。なるべくnode.jsで固めてたのはポータビリティのためでしたが、安心と信頼のmattn-wareなので、Windowsも問題ないでしょう。
ESDoc
ES6クラス形式で書いておけば特に苦労せずに使えるのがESDocです。JSDocはパッチを送ったりもしたことはあるんですが、元の言語がprototypeベースだけど、クラス指向のドキュメントを生成しようとするために、たくさんの追加タグを打たないとうまくドキュメントが出てくれない。ESDocなら、 /**
で始まるコメント+ @param
タグと @return
タグぐらいでほぼ書ける感じです。
/**
* クラスだよ
*/
class MyClass {
/** @return {String[]} 文字列の配列を返すよ */
method() {
return ["Hello", "World"];
}
}
ただし、ESDocはES6 modules形式しか対応してません。common.jsスタイルの対応はしないと明言されています。このコメントに書かれているコードをローカルに書いておきます。
また、export対象がオブジェクトで、オブジェクト内にクラスやら関数がある場合は現在はドキュメント化はされないので、 /* @export */
と書いておくとエクスポートされるようなコメントも追加しておきます。
exports.onHandleCode = function (ev) {
ev.data.code = ev.data.code
.replace(/module\.exports = /g, 'export default ')
.replace(/\/\*\s*@export\s*\*\/ /g, 'export ');
};
次のような設定ファイルを書いておきます。必須項目はsource, destinationですが、今回はCommon.jsモジュール対応も必要なのでpluginsも追加します。その他はお好きなように。testがあるとカバレッジ機能が使えるようになります。
{
"source": "./src",
"destination": "./esdoc",
"plugins": [
{"name": "./contrib/esdoc_node_helper"}
],
"test": {
"type": "mocha",
"source": "./test"
},
"title": "MyProject"
}
実行はこんな感じ
$ esdoc -c ./.esdoc.json
Mithrilのコンポーネントは、
{
controller: コントローラ関数(もしくはクラスコンストラクタ関数、クラス),
view: ビュー関数
}
という定義になっています。ESDocとの相性を考えて、次のような構成にすることにしました。
/*@export*/ class MyComponent {
constructor() {
//コンストラクタ
}
// イベントハンドラはインスタンスメソッドで。
eventHandler() {
}
// ビューはクラスメソッドで
static view(ctrl) {
}
}
module.exports = {
controller: MyComponent,
view: MyComponent.view
};
package.json
ジョブランナーは使わない方針なので、すべてnpmコマンドで完結させます。これまで紹介したツールやライブラリのインストールも含めて、このpackage.jsonにすべてふくまれてます。
結果的にはconcatツールがWebPackからBrowserifyになっただけで、僕のES5開発環境とはほぼおなじになりました。
追記
lint/minifyはbuildに付随して実行されるように変更しました。
{
"name": "myproject",
"version": "1.0.0",
"description": "",
"main": "js/index.js",
"scripts": {
"test": "mocha --require intelli-espower-loader",
"prebuild": "eslint src",
"build": "browserify src/app.js -o js/app.js",
"postbuild": "babili -o js/app.min.js js/app.js",
"start": "http-server . -p 8080",
"build-css": "node-sass --include-path scss scss/main.scss assets/app.css",
"watch-css": "nodemon -e scss -x \"npm run build-css\"",
"watch-js": "nodemon -e js -i js -x \"npm run build\"",
"esdoc": "esdoc -c ./.esdoc.json"
},
"author": "Yoshiki Shibukawa",
"license": "なにか",
"devDependencies": {
"babili": "0.0.6",
"browserify": "^13.1.0",
"esdoc": "^0.4.8",
"eslint": "^3.7.1",
"http-server": "^0.9.0",
"intelli-espower-loader": "^1.0.1",
"mithril": "^0.2.5",
"mithril-mdl": "^1.1.0",
"mocha": "^3.1.0",
"node-sass": "^3.10.1",
"power-assert": "^1.4.1"
}
}
scripts以下が充実しました
-
npm start
: HTTPサーバを実行 -
npm test
: テスト実行 -
npm run build
: JSのLint、ビルド、minifyを実行 -
npm run build-css
: CSSのビルド -
npm run watch-js
: JSを監視して自動ビルド -
npm run watch-css
: CSSを監視して自動ビルド -
npm run esdoc
: ソースコードからドキュメント生成
だんだん、ジョブ同士の依存関係が大変になってきたり、並列実行したい、という時はnpm-run-allが良いです。npm scriptsベースで書かれたジョブを逐次実行、並列実行するというシンプルなツールです。こういうタスクの効率的な実行というとすぐDSL化みたいなことを言い出す人がいます。達なんとかプログラマーを読んで麻疹にかかった人だと思うんですが、見た目の短さの裏に暗黙の知識みたいなのが埋もれてしまって、実際に挙動を理解するのに時間がかかるという欠点があります。npm-run-allは裏の仕組みは平易な既存の仕組みで、それをバッチ化するだけなので、良いです。
{
"lint": "eslint src",
"concat": "browserify src/app.js -o js/app.js",
"minify": "babili -o js/app.min.js js/app.js",
"build": "npm-run-all -s lint concat minify"
}
-s
で逐次実行、 -p
で並列実行です。pre/postだと複雑になってくると見通しが悪くなってきます。具体的には、lintなのにprebuild、みたいに仕事の名前と実際の処理がずれてきちゃう点が問題です。そこは解決できます。