Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
メインコンテンツまでスキップ

Modern JavaScript概観、そしてElectronへ

Sato Taichi

この一か月分の学習成果を整理したリポジトリを作ったので、その成果についてまとめておく。

作ったサンプルプロジェクトだけを手軽に欲しければ、このリポジトリを clone してほしい。

  • taichi/js-boilerplate
    • master ブランチには、ミニマムな JavaScript 開発環境がサンプルコード付きで入っている
    • frontend ブランチには、React/Redux/webpackなウェブアプリケーション用の開発環境が入っている
    • デフォルトブランチにしてある electron ブランチには、frontend ブランチの内容に加えてElectronでアプリケーションを開発するための環境が入っている

はじめに#

最近の JavaScript について#

僕は仕事として JavaScript を書いている訳ではないけども、この半年くらいの間にちょっとしたツールならいくつか作った。どちらも便利なので是非使ってみてほしい。

そういうわけで、僕は最先端の JavaScript を追いかけられている訳ではないけども、全然知らないってわけでもない。

今の JavaScript は、色んなライブラリやツールが出たり消えたりしているから動きがあるように感じるだけで、やりたい事や、やれるようになった事はそれほど増えていない、と言うのが僕の考えだ。

ここに書いた内容は、過去二年から三年分くらいの動きをできるだけ拾ったので、量としてはかなりある。流石に三年あれば色々と変わるよね。

JavaScript における主な問題領域#

これから、僕が最近学習した JavaScript に関する知識を説明していくけども、話題にする領域を列挙しておく。

これがさっき書いた「やりたい事や、やれるようになった事」のリストだ。

  • 言語を中心とした話題
    • 型システムの導入
    • 静的解析の発展
  • テストに関する話題
  • 高度な UI に関する話題
  • ビルドに関する話題
    • 依存モジュールの管理
    • モジュールバンドル
  • Electron に関する話題

対象読者について#

この文書の対象読者として想定しているのは、十分に使える第一言語があるプログラマ。その言語は Java や C#のようなコンパイルプロセスのある言語が望ましい。

加えて、最近の JavaScript に興味があるけども、効率よく学習するために指針となる文書が欲しい人。

あまりコードが書けない人や JavaScript に興味が無い人に対する配慮のある文章にはなっていない。

最先端の JavaScript をバリバリ使いこなしている人は対象読者では無いけども、目を通して何かおかしな部分や誤解し易い表現があったら適宜 Twitter やはてブで指摘してくれると嬉しい。

開発環境#

僕は OS として Windows10 を使っている。

今回の話にプラットフォーム依存性のある部分は少ないから、どんな OS を使っていても関係ないけど念のため。

Node とYarnscoopでインストールするのがいいよ。

エディタとしてはVS Codeを使っていることを前提に環境を作り込んだ。 ただし、VS Codeにしか無いような拡張は使ってないので、他のエディタで同様の環境を再現するのは難しくないはずだ。

プロジェクト直下の.vscode/extensions.jsonってファイルに VS Code 拡張の ID を並べてある。 リポジトリをgit cloneしたディレクトリを VS Code で開いて、確認ダイアログを承認すれば必要な拡張は全部自動的にインストールされる。

アプリケーションを動作確認するためのブラウザとしては、Chrome を使っている。

ハードウェアスペック#

僕が使ってるマシンのスペックは以下の通り。

  • Thinkpad X1 Carbon 2016 年モデル
  • CPU i7 6600U @ 2.6GHz
  • メモリ 16GB
  • ストレージ SAMSUNG NVMe SSD 950 PRO 512GB

少し高級かもしれないけど開発者用のマシンとしては普通だよね。

僕のバックグラウンドについて#

僕のキャリアは SIer で受託のシステムを Java でゴリゴリ作るソルジャーとして 10 年くらい過ごした所から始まっている。 だから、Java に対する知識が多く、一番うまく使えるのは Java に関連するサーバサイド技術だ。

今はそれなりに色んな言語を使ってプログラミングできるけども、結局は脳内で Java によく似た謎のオレオレ言語を Ruby なり Python なりに変換して出力している。

そういうわけで Java 以外の環境を学ぶ際には、どうしてもそのオレオレ Java に変換し易い環境で作業したいと考える傾向がある。

加えて、SIer でキャリアを積んで今も SIer で働いているので、受託のシステム開発や、パッケージ開発において当該技術をどのように使うかに強い関心がある。

つまり、僕は技術とその改善が収益にはほどんど結びつかない環境を前提に、新しい技術をどう適用していくのか考える傾向がある。

このエントリには、そういうバイアスがあるってことを理解してほしい。

言語の話#

JavaScript の実行環境は色々あるけども、ほとんどのブラウザで動くのは ES5 だ。 そもそも ES5 で出来る限り頑張るなら、ややこしいトランスパイラがどうのって話、つまりBabelとは無縁になれる。

JavaScript には学ぶべき知識が多いのだから、まずBabelと付き合わないという選択肢は最初に考えよう。

僕にはBabelと付き合わないって選択肢を取るのは無理だった。理由は単純で、もう function って沢山書きたくないから。みんなはどうかな?

付け加えるなら、Java から来た僕はコンパイルって作業に対する忌避感がないのは事実だけどね。

Babelはみんなの砂場#

プログラミング言語のデザインは、ある程度孤独な作業だ。今までは少数の言語デザイナがある程度形になるまで丁寧に育ててからリリースしてきた。

ただ、少なくとも JavaScript に関しては、そういうやり方じゃ上手くいかないってことが ECMAScript4 の失敗で分かっている。

そこで、実験的な言語機能をプラグインという形でカジュアルに実装できるBabelだ。新しい言語機能をガシガシ使いながら議論して次のバージョンの JavaScript に取り込む機能を決める。

ユーザはちょっと.babelrcを書くだけでクールな新機能を試せる。その機能が思った程よく無かったり、動作が不安定だったらサクっと使うのを止めればいいだけのことだ。

言語機能を自分の需要に応じて足したり引いたりするのは結構楽しいよ。

Babel関連の理解すべきモジュール#

Babel関連のモジュールは沢山あるけども、キチンと理解した方がよいものは意外と少ない。

前半の二つは環境をセットアップするためのモジュールで、残りの二つは他のツールと連携するためのモジュールだ。

babel-preset-env#

babel-preset-env は、Babelで変換した JavaScript が動作する環境に併せて、利用するBabelプラグインを自動的に選択してくれる便利なモジュールだ。

JavaScript は、ブラウザやNode.jsなどの実行環境がどんどん改善しているので、それに併せてコンパイル時に必要なBabelのプラグインが変化していく。 無駄なプラグインが有効化されたところでコンパイル時間が微妙に伸びるだけなので、あまり害は無いけどね。

ただ、意識しないと最新状態に追いつけないというのはあまり健康的でないだろう。

babel-preset-env を使うと、そういう億劫になり易い作業から開放される。定期的にモジュールをアップデートするだけで最新環境が付いてくるってわけだ。素晴らしい。

babel-register#

babel-register は Node の require をフックして、Babelの処理を差しはさむハッキングモジュールだ。

プロダクションコードでモジュールをロードする時に使うのではなく、主にテストコードを実行する際に使う。

JavaScript におけるユニットテストコードの実行では、ファイルの変更検知からテストを実行するといったことがよく行われている。

この時テストコードをちょっと修正しただけで、プロジェクトのコードを全部コンパイルしてたら全く仕事にならないだろう。

そこで変更したファイルと、それに関係があるファイルだけコンパイルするプロセスを、完全に自動化するなら require なり import なりをフックすればいいのだ。

注意しておくと、テストコードがテスト対象コードを直接的に参照しないタイプのテスト、例えば E2E テストみたいなものではあまり上手く動かないこともあるよ。

babel-eslint#

babel-eslintBabelで拡張された JavaScript のコードをESLintで扱うためのパーザだ。

ESLintESLintでちゃんと進化しているから、普通に ES と JSX でコードを書くだけなら babel-eslint は必要ない。

何故これが必要なのかと言うと、Flowという JavaScript に型を宣言して検証するためのツールを使うからだ。

FlowESLintについては、後でちゃんと説明するから安心してほしい。

babel-plugin-transform-flow-strip-types#

babel-plugin-transform-flow-strip-types は名前の通り、Flowのために宣言した型に関する情報を自動的に除去するモジュールだ。

これで型に関する情報を削除した後に、書いたコードを対象のランタイムで実行可能な JavaScript へ変換する。

BabelFlowを使わずに固まった言語仕様だけで動かすなら、ほとんど何の設定もいらない。 だから、そんなにBabelを忌避することも無いんじゃないかなぁ。

型システムについて#

型と言っても、何もそんなに難しい話をしようってわけじゃない。JavaScript に足りない味をちょっとだけ足そうって話だ。

そもそも JavaScript には型宣言がほぼ全くないので、単純な話として関数の引数に何が渡ってくるのかを呼び出される側のコードだけ見てても絶対に分からない。

呼び出す側と呼び出される側の両方を、短期間のうちに自分ひとりで書くなら、引数としてどんなものを渡すのかなんて全部分かってるだろう。 それなら、わざわざ宣言することも無いだろって話はある。

でも、冷静に考えてほしい。一週間前の自分は、今の自分か?僕は違うと言える。先週の僕が何考えてコード書いてたのかなんて大まかには分かっても、詳細には分からないってのが正直なところだ。

コードにコメントを書くことで、将来の自分を含む他人に情報提供するってのは悪くないアイディアなんだけど、コメントは動かないし、自動的に検証しようがない。 それに対して、ちょっとしたメモ書きとして型を書いておけば、自動的に検証できて幸せになれる。

JavaScript における型宣言の派閥#

ここで少し JavaScript 界隈ではどんな方法で型を宣言する方法があるのか確認しておこう。

Google Closure Compiler#

僕が JavaScript を使う中で型宣言に初めて取り組んだのは、Google Closure Compiler の型システムだ。

こいつはドキュメントコメントの中にせっせと型宣言を書くと、コンパイラがアレコレチェックしてくれるってやつだ。

/** * Some class, initialized with an optional value. * @param {!Object=} opt_value Some value (optional). * @constructor */function MyClass(opt_value) {  /**   * Some value.   * @private {!Object|undefined}   */  this.myValue_ = opt_value;}

このアプローチは何が素晴らしいって、型宣言はコメントの中にあるので、ソースコードの動作に全く影響を与えずにコンパイラが必要な情報をプログラマが提供できるってこと。

一方で、コメントの中にある宣言がないがしろにされ易いって問題がある。分かり易いかわりに表現力が極端に低いって問題もある。

Google Closure CompilerADVANCED_OPTIMIZATIONSはコードがまるで動かなくなるリスクがあるけど、マジで小さくなるから本当に驚いた。

TypeScript#

TypeScriptは、発展的な JavaScript として、新しい言語を作ってしまおうという沢山の取り組みの中の一つとして現れた。

僕にとっては、Delphi や C#をデザインしたAnders Hejlsberg先生の最新作という認識で、TypeScriptは神からの恩寵の一種だと考えている。

TypeScriptのコードを少しだけ見てみよう。

interface Person {  firstName: string;  lastName: string;}
function greeter(person: Person) {  return "Hello, " + person.firstName + " " + person.lastName;}
var user = { firstName: "Jane", lastName: "User" };
document.body.innerHTML = greeter(user);

僕の脳内にあるオレオレ Java に本来の Java よりも似ているのがTypeScriptだ。

TypeScriptの型宣言は、動作するコードの中に型アノテーションを混ぜ込むやり方と、型宣言のための専用のファイルを作るやり方が提供されている。 例えば専用ファイルを作るなら、こう宣言する。

declare namespace myLib {  function makeGreeting(s: string): string;  let numberOfGreetings: number;}

型宣言のためのd.tsというファイルがあれば、既存の型宣言が全くないライブラリでも型があるかのようにコードを書ける。

型宣言が無い既存のライブラリに対して必要に応じて後付けで型を付けられるってのは、既存資産を活用する上で非常に重要な機能だ。

OSS のモジュールに対する型宣言ファイルを単一のリポジトリに集めようと活動していたDefinitelyTypedプロジェクトは、型宣言ファイルの数が多くなり過ぎて破滅した。

今は、typesという Organization で、1 モジュールに対して 1 リポジトリを割り当てるという真っ当な方法で管理している。 とはいえ、DefinitelyTypedが参照されなくなった訳ではない。

ちなみに、npm リポジトリから型宣言ファイルを取り出すには npm install -D @types/lodash 等とすればよい。専用のツールじゃなくて、npm で取れるって所が素晴らしいよね。

Flow#

そしてFlowだ。こいつは OCaml で作られた静的解析ツールで、JavaScript 内に混ぜ込まれた型宣言をみてアレコレチェックしてくれる。

Flowで型付けされた JavaScript のコードを少しだけ見てみよう。

// @flowfunction total(numbers: Array<number>) {  var result = 0;  for (var i = 0; i < numbers.length; i++) {    result += numbers[i];  }  return result;}
total([1, 2, 3, 4]);

ぱっと見、TypeScript の型アノテーションと似ているよね。でも、FlowTypeScriptと明確に違う点がある。 Flowは、JavaScript に付与された型アノテーションを評価するなどの静的解析はやるけど、新しいプログラミング言語を作ったわけではないことだ。

Google Closure Compilerにより近いアプローチと言えなくもない。でも、Google Closure Compilerが作られた頃と違って今はBabelがあるので、言語の構文を一次的に拡張して用が済んだらFlow専用の記述だけを安全に取り除くという実装が可能になった。

コメントの中で行う型宣言は、表現力がどうしても限定的になってしまう。Flowはそれを乗り越えた。とは言え、Haskell や OCaml とは比べるまでも無く、Java と比較しても、それ程豊かな表現力があるわけではない。

Flowが型を推論してくれる範囲について理解するのは、それ程難しくない。というかFlowは凄く簡単な型推論しかやってくれない。

FlowにもTypeScriptと同様に型宣言を共有するためのflow-typedというリポジトリがある。 何故DefinitelyTypedと同じ轍を踏むのかはよく分からないが、単一リポジトリでやっている。

ただ、こちらは PR を投げると結構厳しくレビューされるようで、格納されている型定義ファイルの中身はしっかりしている代わりに、絶対量が少ない。

型宣言ファイルを取り出す時には、flow-typedというコマンドを使うと package.json の中にある依存モジュールの型宣言ファイルをflow-typedのリポジトリから自動的にダウンロードしてくれる。 型宣言の無いものに関しては、全部の型が any だと宣言した型宣言ファイルを雑に作ってくれる。

何で npm コマンドで型宣言ファイルを取れるようにしないんだろうね?雑な型宣言ファイルを作るコマンドは別に分けておけばいいんじゃないなぁ。

どの型システムを使うか#

流石に今更Google Closure Compilerは無いよね。

TypeScriptFlowは目的が違うので単純に星取表で評価できるようなものでもない。星取表で比べるならTypeScriptが圧勝だ。

じゃあ、どちらを使うのが望ましいのか?

使いたいライブラリやフレームワークの対応状況を見て決めるといいんじゃないかな。

少なくともAngularを使うならTypeScriptだろうし、React使うならFlowで間違いない。

Vue.js には公式の TypeScript 用型定義ファイルがある。でも、これはTypeScriptの利用を推奨しているわけではなく、一部のユーザによる貢献がマージされた結果らしい。

素の JavaScript を使うならFlowを使うのが良いし、使おうと思ってるライブラリの型定義ファイルがしっかり揃ってるならTypeScriptはかなり良い選択肢になる。 特にVS Codeは型定義ファイルが揃っていると入力補完の精度が明確に改善するからね。

一応補足しておくとFlow Language Supportには入力補完を強化する機能もあるよ。

型システムに関する積み残し課題#

このエントリ公開後に識者から貰った指摘を補足しておく。

静的解析でベストプラクティスを共有しよう#

JavaScript には多くの罠がある。ここでいう罠と言うのは多くのプログラマが誤解し易い言語仕様や、ある種のライブラリの動作のことだ。 JavaScript は本当に柔軟な言語なので、新しい言語仕様のほとんどは古い言語に直接変換できる。だからこそ、罠も多い。

プログラマがミスした結果記述されるコードや、おおよそ仕様を誤解していない限り書かれないようなコード。大抵の状況でバグだとみなせるコード。 言語仕様には即しているけども、アプリケーションとして動作させても期待通りの結果を得られないようなコードを自動的に見つけるためのツールが Lint だ。

もう少しアグレッシブに Lint を使うと、インデントのやり方や、変数名の付け方みたいにコードの動作とは関係ないけど、コードの読み易さには関係があるものも検査できる。

JavaScript における Lint の歴史#

JavaScript はちょっと調べると沢山の Lint がある。僕が使ったことがあるもので最も古いのはJSLintだ。 こいつは、伝説のDouglas Crockford先生が作った Lint で凄く厳しい。一切の甘えを許さないストロングスタイルだ。 JavaScript の奇妙な動作には絶対に惑わされない、奇妙な動作をしうるコードは奇妙な見た目になるはずだという強い意志を感じるツールなのでヘタレには辛い。

ちなみに、JSLint のコードは読むとすごく勉強になる。コンパクトで読み易いよ。

僕のようなヘタレにとって福音となった Lint はJSHintだ。これは余り厳しくない。設定ファイルを切り出したり出来るんで凄く使いやすい。 ただ、JSHint は組み込まれた機能を有効化したり無効化するのは簡単だけど、新しいルールを追加するのはちょっと面倒だ。

それに誰かのオススメルールを自分のプロジェクトに取り込むには、設定をコピー&ペーストしないといけない。 Lint のルールを作り込むなんて、僕みたいな一部のマニアがやる作業で大抵の人は誰かのオススメセットをパッと使いたい。 そのオススメセットがメンテナンスされているなら、面倒なことをせずにその最終成果だけを使いたいよね。

そういうわけで、拡張し易く、また設定ファイルを作り込みやすいESLintが今はオススメなのだ。

ESLintはプラグインシステムがよく出来ているので、沢山のプラグインがあるし、オススメのルールセットを npm モジュールとして公開できる。

Airbnb が公開しているeslint-config-airbnbは、そういうオススメルールセットの一種だ。

Airbnb のは僕の考えと合わないものが多く含まれているので、僕は採用しなかったけどね。

ESlint関連の理解すべきモジュール#

ESLint関連のモジュールと言えば、おおむねプラグインのことになる。

他にも沢山あるとは思うけど、僕が見つけて使ってみたものの中で特に有用だと感じたものをここでは列挙しておく。

もしあなたがESLintマニアで、僕が知らない便利なプラグインを知っているなら是非教えてほしい。

eslint-plugin-ava#

僕のプロジェクトではAVAというテスティングフレームワークを使っている。AVAについては後で説明する。

AVAは凄くシンプルで使い易いけども、シンプル過ぎてどうテストコードを書けばいいのか慣れるまでは分かりづらい。

そこで、このプラグインを使うとAVAを明らかに間違った使い方をしている時に警告して貰えるようになる。

eslint-plugin-import#

eslint-plugin-importimport文に関するミスを見つけてくれる。

具体的にはプロジェクト内には存在しないモジュールをimportしようとすると警告がでる。モジュール名を typo することくらい誰にでもあるよね。

他にも require に対してリテラル以外の文字列を渡しているとエラーになったりする。

eslint-import-resolver-node#

こいつだけは他のモジュールと違った役割を持っている。

eslint-import-resolver-nodeeslint-plugin-import と協調動作して import するモジュールの探し方をカスタマイズできる。

どういうことかと言うと、僕の作ったプロジェクトでは、アプリケーションコードとテストコードをそれぞれsrctestという別なディレクトリに格納している。

このままだとテストコード側で、アプリケーションコードを import するときに、import world from "../../src/hello/world";とか書かなくちゃいけなくて凄く辛い。 それを避けるために、テストコードを実行するときに NODE_PATH=src とすることで、その ../../src/ 部分を記述しなくても良いようにしている。

このことをeslint-plugin-importに知らせるためにeslint-import-resolver-nodeを使っているのだ。

eslint-plugin-promise#

JavaScript のPromiseは慣れが必要なタイプのライブラリだ。しかも、正しい使い方が凄く分かりづらい。 ちなみに、JavaScript Promise の本は大変素晴らしいドキュメントなので何度でも読める。

Promiseに慣れるまでの間は誤ったPromiseの使い方をしている可能性が高い。 近くに JavaScript のエキスパートがいてコードレビューして貰えるなら、それは大変素晴らしいけど、趣味でちょっと JavaScript 書いてるだけなのに、そんなエキスパートに声かけたりは出来ないよね。

そこで、このプラグインを使うことで、明らかにダメなPromiseのコードにはエラーを出して貰おうってわけだ。 このプラグインのルール詳細を読むだけでも、避けるべきPromiseの使い方を理解できるので、是非丁寧にルールを設定してみてほしい。

ところで、ES に async/await が入ったらPromiseを直接使うことは無くなるんだろうか?

僕は C#の async/await を利用した経験から async/awaitPromiseは普通に共存すると考えているが、実際はどうだろうね。 少なくとも、AVAではasync/awaitを使うと、テストコードが分かり易くなる印象はある。

eslint-plugin-security#

あなたはコード書きながらセキュリティについて気にしているだろうか? 僕は静的解析ツールを使ったセキュリティレビューを仕事としてやっているので、それなりに気にしているつもりだ。 でも、常に頭の中にセキュリティに対する考慮があるかと言われると、そんなことはない。

このプラグインでチェックできる脆弱性は、それ程多くは無いけども、ついウッカリ作り込んでしまうような脆弱性をパッと見つけてくれる。

普通にコードを書いてるぶんには、このプラグインによるエラーをみることは余り無いだろうけども、こいつに怒られた時は、ちょっと慎重になってほしい。

eslint-plugin-flowtype#

Flowを使って型宣言をするのはいいんだけども、しょうもないミスをすることはある。

また、Flowによる型宣言は通常の JavaScript ではないのでESLintにある標準のルールは全く適用されない。 つまり、インデントやら、行末にセミコロン付けろとか、ケツカンマを殺せだのは、Flow用に再実装する必要がある。

Flow自体にもそれなりに罠はあるし、一貫性のある型宣言をするなら、少々 Lint にかけておいた方が良いだろうね。

Promiseのところでも同じ話をしたけども、Flowも新しい技術だから慣れるまでの養成ギブスとしてeslint-plugin-flowtypeを使うのが良いんじゃないかな。 ちゃんと慣れれば、エラーが完全に出なくなるだろうし。

eslint-plugin-react#

僕のプロジェクトでは UI を構築するためのフレームワークとしてReactを使っている。

Reactは非常に良く使われているので、多くのベストプラクティスと共に、望ましくないコーディングスタイルが発見されている。

ルールとして定義されているもの全てが自分たちのプロジェクトにとって望ましいとは限らないけれども、ルール化されているものについて是非を議論するのは、その知識を生み出すよりもはるかに容易だ。

実際、このプラグインで定義されているようなルールに関する知識を自力で得るには小さなプロジェクトで簡単に使ってみるだけでは足りないだろう。

デファクトスタンダードなフレームワークを使うことのメリットはこういうところにある。

eslint-plugin-jsx-a11y#

自分たちが作っているシステムの Accessibility にはどれくらい気を使っているだろうか?

僕は、おろそかになりがちな Security よりもさらに優先順位が低いのが Accessibility じゃないかなと感じている。

リッチな UI にするのは、より多くのユーザにより良い体験を提供するためなのだから、その延長線上にハンディキャップのあるユーザへの配慮があっても良いんじゃないかな。

とは言え、WAI-ARIA1.1や、MDN の ARIA に関するページを端から端まで読んでガッツリやるぞと言っても無理があるよね。

HTML5 Accessibilityくらいの使い易いリファレンス片手に最小限の手間で最大限の価値を提供したいものだ。

そういうわけで、まずは、eslint-plugin-jsx-a11y のオススメルールを使ってプロジェクト内の JSX が Accessibility に関する対応が妥当なのか確認しよう。

テストの話#

一口にテストと言っても色々ある。ここではディベロッパーテスティングについて話をする。

僕は、アプリケーション作る上でプログラマが最も熟達すべき技術はテストだと考えている。 あるべき姿が何なのかをテストという形で表現した上で、対象のソフトウェアを実装することで、より良いものが作れるはずだと信じているからだ。 つまり、本物のテストエンジニアがやってるようなことを出来るようになるべきって話をしたいわけではない。

品質保証には膨大な体系化された知識があり、アプリケーション作るための知識と両方を備えられれば素晴らしいけど、時間は有限だ。 家族がいたり、やりたいゲームがあったりする。長生きするために一日十時間は寝たいし、ゆっくり旨い飯を食いたい。

何をテストすべきか、どんなテストすべきかを学ぶことに時間を使うべきであって、テスティングフレームワークのクールな使い方を学ぶことに時間を割いてはいけない。

そういうわけで、テスティングフレームワークは API が出来るだけ簡素なものを選ぼう。

つまり、AVAだ。

AVAのススメ#

AVAで書いたテストコードはこうなる。

import test from "ava";
test("foo", (t) => {  t.pass();});
test("bar", async (t) => {  const bar = Promise.resolve("bar");
  t.is(await bar, "bar");});

僕が何故AVAを気に入っているのか簡単に列挙しておこう。

  • API が小さい
  • power-assertが標準で組み込み済
  • 実行速度が速い
  • =>演算子、Promiseasync/awaitobservableへのサポートが完璧

AVAの API は小さい#

import test from 'ava';で import したら、test変数に定義されている関数を呼び出せばやりたいことが全部できる。

そして、このtestがデフォルト関数になっていることを含めても関数が 11 個しかない。列挙してみよう

  • test([title], implementation)
  • test.serial([title], implementation)
  • test.cb([title], implementation)
  • test.only([title], implementation)
  • test.skip([title], implementation)
  • test.todo(title)
  • test.failing([title], implementation)
  • test.before([title], implementation)
  • test.after([title], implementation)
  • test.beforeEach([title], implementation)
  • test.afterEach([title], implementation)

この中で覚えておくべきは、強調した 4 つの API だけだ。それ以外は必要になった時に、マニュアルを参照すればいい。

AVAは global 空間や暗黙のthisに配置された関数を前提に動作しないってことも大事なことだ。

power-assertが標準で組み込まれている#

テストを書く時には、何も考えずに使えば最高にリッチなアサーションエラーをはいてくれるpower-assert使おう。

こんなテストが…

test((t) => {  const a = /foo/;  const b = "bar";  const c = "baz";  t.true(a.test(b) || b === c);});

こんなエラーになる。

t.true(a.test(b) || b === c)       |      |     |     |       |      "bar" "bar" "baz"       false

マジ、最高。

power-assertはこのアサーションエラーを出すためにコードを書き換えるんだけども、そのためのセットアップが少々面倒だ。やれば簡単だしマニュアルも揃ってるんだけど、まぁ、面倒は面倒だ。

でも、AVAを使っておけば、そのちょっとした面倒すらない。

実行速度が速い#

テストの実行速度が問題になるのは、本物のアプリケーションをちゃんとリリースして、それがメンテナンスフェーズに入ってからだ。

ちょっとお試しで動かしてるうちは、実行速度が 5 秒だろうが、1 秒だろうが変わりはない。でも、テストコードの量が 1000 件とか 10000 件とかなったらどうだろうか?

最早、自分のローカルマシンでは、ちょっとコードを変更するたびにテストを全件走らせるのは無理だ。

自分が変えた部分と直接関係ありそうなテストコードをサッと実行して、後は Jenkins なり CircleCI なりといった CI サーバにブン投げて祈るだけだ。知らんところがコケませんように…。が、コケる。

どうせテストは定期的にコケるんだから、変更 → テスト → デバッグ → 変更……のサイクルは短い方が良い。

AVAはテストの実行速度を確保するために、あまり複雑な構造のテストコードを敢えて書けないようにしてある。 Mochaみたいなテスティングフレームワークで複雑な構造のテストを書いてきた人からすると、奇妙に見えるかもしれないが慣れだ。

テストコードの変数スコープが何重にも積み重なってると、書いてる時はいいけど、どうせ後で苦労することになる。

各種サポートが完璧#

これはAVAの API が簡素だから完璧にしうるって話でしかない。

thisを下手に書き換えないから=>演算子が快適に動く。

テストメソッドの戻り値はAVAの責任範囲だって決まってるから、Promiseだろうが、observableだろうが返しておけばAVAがよろしくやっといてくれる。

テストコードのファイルに並ぶ関数呼び出しは入れ子にならない。

ちなみに、test.beforeEachで作った変数をtestで参照するには、こうする。

test.beforeEach((t) => {  t.context = "unicorn";});
test((t) => {  t.is(t.context, "unicorn");});

contextは、複数のtest間では共有されていないので、存分に触ってよろしい。 ただし、複数のtesttest.serialを使わない限り順序性が保証されないので、testの中でcontextを書き換えてはいけない。 そもそも、test.serialは何か特別な理由がない限り使ってはいけない。

尚、テストメソッド間で何らかの情報を共有したり、テストの実行順序に意味があるような書き方をするのは悪手なのでやらないように。

テストに関する積み残し課題#

僕が学習すべきだけど、まだ手を付けられていない課題について、誰かが補完してくれることを願って少し書いておく。

如何にして UI を作るか#

ここからは、JavaScript でユーザインターフェースを構築する方法について説明していこう。

道具の選び方について#

10 年くらい前に Gmail や GoogleMaps を初めて使った時、こんなにもハイパフォーマンスで豊かな画面表現が JavaScript だけで可能なのかって、本当に驚いたよね。

DHTML とか言って JavaScript を使ってウェブサイトに動きを付けるみたいなことに取り組んでる人々は、それ以前からいたけども大体が忌み嫌われていた。

リッチなユーザインターフェースを持つアプリケーションをブラウザ内で動かすなら Flash を使うしかなかった。他にもブラウザプラグインみたいなテクノロジはあったけども、結局ほとんど使われなかった。

Gmail や GoogleMaps のように現実的なパフォーマンスで動作する GUI アプリケーションをブラウザ上で JavaScript だけを使って実装するという取り組みは、あの頃から活発化したけど、ここ数年に至るまで、その取り組みはそれ程上手くいっていなかった。

確かにYUI LibraryExt.jsClosure Libraryといったライブラリが次々と出てきたけれども、 それまで VB や Delphi で出来てたような UI を作るのはとてつもなく難しかった。

例えば、データを 1000 件程度表示した上で、各セルがそれなりに複雑な動作をするようなグリッドコントロールを作るのは現実味が無かった。

結果的に、CSS に加えて jQuery で少々頑張るというのが定番になった。少し動きのあるウェブサイトが作りたいだけなら jQuery でやればいい。 大人数で作業したり、長期間メンテナンスしたり、物凄い作り込みをしたりしなければ、おおむね上手くいく。 大抵のウェブサイトは GUI アプリケーションでは無いのだから、jQuery とそのプラグインを便利に使えばさっと作れる。

ところで、高度に作り込まれたウェブサイトと、ブラウザ上で動作する GUI アプリケーションに明確な境界線はあるのだろうか?

定量的な判断基準を見たことは無いが、一人か二人のウェブデザイナーが片手間に jQuery を使って動きを付けるだけで破たんせずに作れる範囲は少なくともウェブサイトだろう。

一方で、サーバアプリケーションが存在し、きちんとした JavaScript が書けるプログラマが 3 人も 4 人も UI を作るために必要で、 UI デザインとその実装は役割として分担しなければ成立しないようなものは、ブラウザ上で動作する GUI アプリケーションだろう。

実際には、こんなに分かり易く区別できないだろうけどね。

この区別が何故重要なのかと言うと、使うべきツールキットや揃えるべきチームメンバーのスキルセットが違うからだ。

ReactAngularのように高度な GUI アプリケーションを実装するためのフレームワークを使ってウェブサイトを作ろうとするなら、工数を無駄に大量消費することになるだろう。

でも、jQuery プラグインを駆使して高度な GUI アプリケーションを実装しようとするなら、その先には破滅が待っているだろう。

道具は適した場所で適切に使うべきなのだ。

Virtual DOM というブレイクスルー#

ブラウザ上で動作する GUI アプリケーションを作るにあたって確実に解決すべき課題はパフォーマンスと操作性だ。

特にパフォーマンスの問題は非常に解決が難しい。ブラウザ上で動作する以上、全ての UI 要素は何らかの形で DOM に落とし込まれるからだ。

まず、DOM は汎用的な木構造をしているのでメモリ効率が恐ろしく悪い。 加えて、この木構造は中間の内部ノードの数を限定できず、末端ノードまでの深さも限定できないので走査するコストが高くなり易い。

ブラウザが DOM を画面に描画するためには、全てのノードを何らかの形で走査する必要があるので、この効率の悪さは非常に大きな問題となる。

加えて、CSS という DOM 構造を完全に無視して描画要素をレイアウトする仕組みが、パフォーマンスの悪さに拍車をかける。

末端のノードに現れた class 属性一つで画面全体を再描画しなければならなくなることなど、ブラウザの世界では当たり前なのだ。 つまり、描画済みの DOM に対して新しく作ったノードをappendChildをするだけで画面全体のロックをとる必要があるのだ。

とは言え、その新しいノードが画面全体に与える影響が十分に小さければロックを確保し続けなければいけない時間は十分に短くなる。

ある種のコンポーネントを画面内で一つ更新しただけで、画面全体に再描画がかかる可能性があり、そうならないよう細心の注意を払う必要がある。 そんなことでは、一部の例外的な組織を除いて、効率よく大規模な GUI アプリケーションを作ることなど出来ないのだ。

この問題への解答となる技術が Virtual DOM だ。

まず、画面を描画するために必要なデータ(モデル)と、そのデータを流し込むテンプレート(ビュー)に分けて考える。 これにユーザ入力からモデルを更新する役割を持つコントローラを追加すれば古典的な MVC モデルとなる。

原始的なやり方では、モデルの構成要素のうちどれが変更されたかに応じて、ビューのどの部分を変更するのかをプログラマが一々決めていた。ごくごく単純化した例はこうだ。

前半のuserという変数を、後半部分の HTML っぽいテンプレートに流し込むと考えてほしい。

// モデルの例var user = {  name: "John Doe",  age: 22,};// ビューの例<html>  <body>    <div class="name">Name: ${user.name}</div>    <div class="age">Age: ${user.age}</div>  </body></html>;

モデルを更新するというのはつまり、userという変数のメンバであるnameageの内容を変えるということだ。 原始的なやり方では、どのメンバ変数を変えたかによって、対応する DOM の要素を探すコードをプログラマが書かなければならない。 確かにこれくらい単純な例では、jQuery で$(".name")とか書けば対応する要素を見つけ出すことはできる。

考えてみてほしい、画面内に変更可能性のある要素が 1000 個単位であったらどうだろうか? ある要素を変更すると、それに影響を受けて推移的に変更が必要になる要素があったらどうだろうか? 自分がコンポーネントを画面に組込んだ時には存在しなかった要素の影響を受けて、自分のコンポーネントが変更されるべきだったら、どうだろうか?

悪夢だ。

雑に要約すると Virtual DOM は現在の DOM と、次に出力される DOM の差分を取って、その差分だけをブラウザのレンダリングエンジンに渡してくれる仕組みだ。 以前は人間が気合いで部分更新するコードを書いてたのを、自動的にやってくれる仕組みなのだ。 当然、職人が一つずつ更新すべき要素を選定する実装に比べれば動作が遅くなりうる。

現実的には、そういう職人はごく稀にしかいないので、自動的に差分を取ってもらった方が、おおむね高速に動作する。

似たような議論は、C 言語とアセンブラの頃、もしくはその前からあった。 アプリケーションを全部ハンドアセンブルしたら、普通のコンパイラが出したバイナリよりも高速なバイナリを作れるとか、そういう話だ。 ハンドアセンブルする対象を小さく限定するとか、アプリケーションが十分に小さくなければ成り立たない話だけどね。

どのフレームワークを使うか#

ざっくりと、僕が選定候補にしたものを挙げるとこうだ。どれも、内部では Virtual DOM を使っている。

React#

現状Reactがデファクトスタンダードと言えるので、Reactを選択し学ぶのが最も妥当であろう。 コミュニティが十分に大きく周辺ツールも充実している。 SurviveJSのように質の良いチュートリアルも沢山ある。

そういうわけで、僕のプロジェクトではReactを選んだ。

InfernoRaxといったReactと互換性のあるフレームワークもあるが、 彼らのやっている事に妥当性があるなら、それはいずれReactに取り込まれていくだろう。

Angular#

TypeScriptが第一言語として指定されているAngularは僕にとってかなり魅力的な選択肢だ。 ただ、僕が観測する限りReactに比べてコミュニティが十分に大きいとは言えない。 これは安定的に動作するものがリリースされてからの期間が相対的に短いせいだ。

現行のAngularは 2.4 だけど、Angularの 1 系は僕の好きなタイプのフレームワークだ。 何せ、まず 2way binding があり、ゴミ置き場としてあらゆる黒魔術的な行為が許されている Directive があり、DI コンテナがあった。 digest ループは極めて分かり辛い概念だったが、画面を作り易くするためには、その複雑さは受け入れられるべきだと考えた。

Angularの 1 系は、業務アプリケーションを作るために設計されたとしか思えないような機能群で SIer のためにあるかのようなフレームワークだった。

Angularの 2 系に大きな学習コストを投下するのは、Google 内部の本格的な採用事例が公開されてからでも遅くは無いんじゃないかと考えている。

Vue.js#

Vue.jsは今まさに話題のフレームワークだ。Angularの 1 系が好きだった僕が選ぶべきはVue.jsかもしれない。

でも、僕にはメインの開発者が一人しかないものを基盤となる部分には採用できない。 Evan You一人がほんの少し熱意を失ったり、何らかの政治的取引によって翻意するだけで壊滅的な状況になりうる。 それは恐ろしいことだし、僕には耐えがたいリスクだ。

ざっと読む限りコードベースはそれなりに妥当な印象を受けるし、かなり丁寧なドキュメントがある。 少し使ってみた感じでは機能に不足は無いようだ。 ただ、それなりに大きなアプリケーションを作ろうと思った時に、思いがけないところで根源的な問題を引いてしまい、それに上手く対応できない可能性を考えてしまう。

大きなコミュニティがあり、実稼働しているアプリケーションでキチンと使われているものは、細かい問題の多くは発見され丁寧に対処されている。 一見すると無意味な複雑さを持っているように見えるコードも、ある種の状況では重要な意味があるのかもしれない。

これは僕の粗悪な印象論に過ぎないが、Angularの 2 系とVue.jsは本気のプロダクション環境で使われているようには見えないコードベースの綺麗さがある。

ちなみに、Angularの 1 系はプロダクション環境で使われているように感じられるコードベースだった。

Mithril#

Mithrilの良さは小さいこと。フレームワークの内部詳細を完全に理解した上で、足りないものに自分で気が付いて、自分で足せる人が使う道具だ。

小さいからこそ、それができる。

GUI アプリケーションの開発経験が十分にあるエンジニアだけで構成された、少人数のチームで使うなら合うかもしれない。

アプリケーションを作りながら足りないものに気が付いたとして、その足りないものに関する仕様を適切に定めて、適切に実装することは本当に難しい。 特にプロジェクト進行中のコスト圧力が強いとき、上手くやれるだろうか?少なくとも、僕には出来ないだろう。

Reactの使い方について#

Reactの本体はビューの部分に対してのみ役割を持つので、アプリケーションを作る上では色々と不足がある。

Router については余り悩まずに、react-routerを選んだ。僕自身が Router を選定するための基準を持っていないせいだとも言える。

テスト用のユーティリティとして、enzymeは外せないだろう。 フレームワークを使い込んだユーザが作りこんだ品質の高いモジュールを利用できることが、デファクトスタンダードなライブラリを使うことの最大のメリットだ。

CSS Modulesについて#

CSS というのはつまり、グローバル変数しかないプログラミング言語みたいなもので、デフォルトのスコープがローカル変数な世界に住んでいるプログラマからすると、ありえない環境だ。 コードの量に比例して複雑さが上がっていく環境で完全に整合性のとれた仕事なんて出来るわけがないと考えてしまう。

この問題に今まではどう対応してきたのかというと、命名規則を人間が明示的に守ることによって名前空間を分割してきた。 例えば、メジャーな命名規則にはOOCSSBEMSMACSSがある。

また、Sass(SCSS)lessと言ったメタ言語を使って、より安全に CSS を記述するという取組みもなされてきた。 これらのメタ言語が持つ機能のうち最も重要なのはスタイルの定義を入れ子にすることで影響範囲を限定する機能だ。

メタ言語は CSS 自体の複雑性を制御する仕組みとして優れているけども、そもそも CSS は DOM 構造に対して、どのような見た目を与えるのか決定する仕組みなので、DOM 構造の設計と CSS の設計は切り離せない。

そう考えると、Reactコンポーネントの設計と CSS の設計は整合性が取れていることが望ましい。それぞれを完全に独立した事象として扱おうとするのは無理があるだろう。

Reactコンポーネントの設計と CSS の設計の整合性をとるには、どちらかのやり方を優先するしかない。それなら、CSS にローカル変数を持ち込み、それをデフォルトの挙動とするのが望ましい。

これを実現するために考え出された概念と取組みがCSS Modulesだ。

CSS ModulesReactのコミュニティから発生した概念だけど、似た機能はVue.js にもあるし、 Angular2 にも同様の機能はあるようだ。

CSS Modulesが解決する CSS の問題に関する詳細な情報はCSS Modules チームのメンバーによるブログエントリがあるので、そちらを参照してほしい。

要約しておくと、CSS Modulesは単に新しい命名規則に基づく CSS の設計方法論の一種ではない。また、CSS Modulesを使ったからといって、CSS のメタ言語を使わなくていいわけでもない。 既存の命名規則に慣れた Web デザイナであっても、大人数で作業分担するようなプロジェクトで UI デザインをするならCSS Modulesを学ぶべきだろう。

尚、CSS Modulesを使ったとしても、アプリケーション全体に対して UI の一貫性を持たせるために設計する CSS は、以前と変わらずグローバルな空間に対して定義していくので、既存の CSS に関する設計知識が不要になるわけでもない。

ReactにおけるCSS Modules#

ReactCSS Modulesを導入するにはwebpackcss-loaderを使って CSS を処理した上で専用の記述をReactコンポーネント側に行う。webpackについては、後で説明する。

CSS Modulesの導入#

一番簡単にCSS Modulesを使うコードはこうだ。

import React from "react";import styles from "./table.css";
export default class Table extends React.Component {  render() {    return (      <div className={styles.table}>        <div className={styles.row}>          <div className={styles.cell}>A0</div>          <div className={styles.cell}>B0</div>        </div>      </div>    );  }}

これがレンダリングされると、大体こういう HTML になる。css-loaderによって自動的に重複が発生しないような css のクラス名が命名されるので、奇妙な名前が class 属性に設定されている。

<div class="table__table___32osj">  <div class="table__row___2w27N">    <div class="table__cell___1oVw5">A0</div>    <div class="table__cell___1oVw5">B0</div>  </div></div>

このやり方は、CSS を import したうえで JavaScript のオブジェクトであるかのように扱っていることが、問題だ。

この場合、スタイルの状態を全く評価する必要のないユニットテスト時にも CSS がコンポーネントに対して適用されなければならない。 このままナイーブな方法で import を処理すると import 先の CSS を JavaScript としてパーズできないのでエラーになってしまう。

中括弧が多いのもコードとして書きづらいので出来れば避けたい。 Shift が要るので中括弧はタイプしづらいのだ。

react-css-modulesによる改善#

一番簡単な方法でCSS Modulesを使う際に発生する問題を改善するモジュールとして、react-css-modulesがある。

これを使うとこんなコードになる。

import React from "react";import CSSModules from "react-css-modules";import styles from "./table.css";
class Table extends React.Component {  render() {    return (      <div styleName="table">        <div styleName="row">          <div styleName="cell">A0</div>          <div styleName="cell">B0</div>        </div>      </div>    );  }}
export default CSSModules(Table, styles);

中括弧が無くなった代わりに、react-css-modulesを import するようになった。

レンダリングされると class 属性に追加される、styleNameという特別なプロパティが導入されている。

コードの書きやすさは改善したけれども、コードの難しさは余り改善していない。

特に最後の行を見ればわかるように、import した CSS を JavaScript オブジェクトとして取り扱っているという問題は解決していない。

babel-plugin-react-css-modulesによる改善#

react-css-modulesの問題を改善するBabelのプラグインとしてbabel-plugin-react-css-modulesがある。

ちなみに、react-css-modulesbabel-plugin-react-css-modulesの開発者は同じだ。

これを使うとこんなコードになる。

import React from "react";import "./table.css";
export default class Table extends React.Component {  render() {    return (      <div styleName="table">        <div styleName="row">          <div styleName="cell">A0</div>          <div styleName="cell">B0</div>        </div>      </div>    );  }}

これで CSS を JavaScript オブジェクトとして扱わずに済むようになった。CSS とコンポーネントとの関連を表すためだけに import 文を使っている。

babel-plugin-react-css-modulesは、CSS Modulesとして必要な処理をコンパイル時に行う。 そのおかげで、コンポーネントのコードがCSS Modulesのためだけに増えることがない。

こういう形で、コードがスッキリするのはいいことだ。

CSS の import がコード上でコンポーネントに影響を与えていないので、ignore-stylesのようなモジュールで当該部分を外しても悪影響がない。

というわけで、僕のプロジェクトではCSS Modulesを実装するのに、css-loaderbabel-plugin-react-css-modulesの組み合わせを使うことにした。

Flux実装について#

アプリケーションの構造を規定するアーキテクチャとしてはFluxを採用するのが良いだろう。 Fluxを実装しているライブラリは数多くあるが、人気があり扱い易いのでReduxを使う。

Reduxを使う上で、Action や ActionCreator にどれだけの役割と責任を持たせるのかは、アプリケーションの規模感によって最適解が変わる。

redux-thunkredux-promiseを使うと動作モデルとしては非常に分かり易いが、ActionCreator のコードが大きくなり易くなる。 非同期処理に関するコードが Action の中に現れるので、コードベースが大きくなる過程で、何らかの基準を設定して Action からコードを分離する施策が必要になる。

ごく単純化して説明すると、redux-thunkはコールバックモデルで Action を実装する。redux-promisePromiseモデルで Action を実装する。

この単純なやり方の問題点は、タスクのキャンセルが出来ないことだ。 サーバとクライアントの間で何らかの不整合があったり、レスポンスの遅さに苛立ったユーザが処理を中断したくなることは、GUI アプリケーションではよくある。 そういう時に、ユーザ操作を起点に処理中のタスクをキャンセルする方法が無いというのは望ましくない。

redux-sagaredux-observableは、動作モデルを理解するのがかなり難しいモジュールだ。 代わりに ActionCreator のコードは単純になり、Action は発生したイベントの内容を格納するかなり単純なオブジェクトになる。

redux-sagaredux-observableは、それぞれ名前は違うが同様の役割をもつ新しいレイヤーをReduxに追加する。

ここでも、単純化して説明すると、redux-sagaは GeneratorFunction を使って追加のレイヤーを実装し、redux-observableRxJSを使って追加のレイヤーを実装する。

redux-sagaredux-observableは、どちらもタスクをキャンセルする方法がある。

エラーハンドリングするコードを書く時、redux-sagaredux-observableの違いは明確になる。 詳細については、それぞれのドキュメントを見てほしいが、要はtry/catchを使うか、ライブラリ定義のイベントハンドラを使うかが違う。

どんなに言いつくろったところで、合法的なgotoでしかないtry/catch構文が僕は嫌いだ。仕方なく書くことはあっても、積極的には書きたくない。

ここまでの議論をまとめるとこうだ。

名前実装技術難易度Action追加レイヤキャンセルエラー処理
redux-thunkcallback不要×try/catch
redux-promisePromise不要×イベントハンドラ
redux-sagaGeneratortry/catch
redux-observableRxイベントハンドラ

そういうわけで、僕のプロジェクトではredux-observableを選んだ。

UI に関する積み残し課題#

僕が学習すべきだけど、まだ手を付けられていない課題について、誰かが補完してくれることを願って少し書いておく。

ビルドの話#

基本的な言語の話と、テストのやり方、UI の作り方について整理したので、次はビルドの話だ。

現代的な開発プロセスにおいて、Continuous Integration(CI)することは必須だ。 つまり、CLI でビルド作業を自動化しなければならないということだ。それは、Windows 環境で開発するとしても変わらない。

僕が利用している CI の SaaS はCircleCIAppVeyorだ。

CircleCIは Linux 環境でビルドするために使っている。特にビルドが失敗した時にSSH ログインできるのでデバッグしたりログを集めたりできる。

環境がビルドのたびに綺麗になる CI サーバ上でのビルド結果と、色々なものが積み重なっているローカル環境のビルド結果は、一致しないことはよくあるからね。

特に、僕の場合は Windows 環境で開発しているので、Linux 環境の CircleCI とは環境の差分が発生し易い。つまり、SSH ログインは極めて重要な機能だ。

AppVeyorはクリーンな Windows 環境でビルドするために使っている。少し設定ファイルを細工するだけで、RDP 接続できるのでデバッグしたりログを集めたりできる。

OS は、Windows Server 2012 R2 (x64)だけど、サーバ OS とクライアント OS の差分が問題になるような使い方をしてはいない。

依存モジュールの管理について#

Node にはプロジェクトに関するメタ情報が格納されている package.json に基づいて、様々なタスクを実行できる npm というツールが付属している。

npm から実行するタスクの中で特に重要なのは依存ライブラリの自動的なダウンロードだ。

npm は非常によく出来ているけど、依存ライブラリのバージョンを固定するためのnpm-shrinkwrapが非常に使いづらい。 そもそもnpm shrinkwrapコマンドを明示的に実行しなければ、依存ライブラリの状態を保存するファイルが作成されない。 更に、productionモードが事実上機能しないバグが直される気配が無い。

そこで、僕のプロジェクトでは、npm に代わるツールとしてYarnを使うことにした。 Yarnはコマンドラインオプションで明示的に指定しない限り、依存ライブラリの状態を保存するファイルが作成される。 勿論、productionモードは適切に動作する。

Yarnで依存ライブラリのバージョンを固定した後は、僕のci-yarn-upgradeを使って欲しい。 これは、依存ライブラリの状態を定期的に監視して必要があれば、package.json と yarn.lock を更新するプルリクエストを自動的に生成してくれる。

タスクランナーについて#

複数のプラットフォームで動作するビルドスクリプトを書くには、基本的にシェルスクリプトを使えないという制限が発生する。

僕が Linux バイナリのあるPowerShellならできるんじゃね?とか考えるのは Windows ユーザだからだ。

プロジェクトで利用している言語は JavaScript だし、Node は複数のプラットフォームで適切に動作するので、ビルドスクリプトは JavaScript で記述するのが望ましい。

JavaScript でビルドスクリプトを書くためのモジュールは沢山あるが、僕の考え方は単純だ。

gulpを使うか、使わないか?

確かに、Gruntbroccoliのようなタスクランナーはある。

これらが考慮対象になり得ない理由は簡単で、npm script を大きく超える利便性を提供してくれないからだ。 ただでさえ学習すべき対象が多いのでタスクランナーには、出来るだけ学習コストをかけたくない。

それでも、gulpが考慮対象になりえる理由はパフォーマンスだ。gulpは Node の Stream API をタスク構築の中心に置いたタスクランナーで高速に動作する。

数十万行レベルのコードベースになれば、テストの実行時間が短いほうがいいのと同様に、ビルドの実行時間も出来る限り短縮することが期待される。

ところで、パフォーマンスに関する格言で僕が好きなものに「予測するな、計測せよ」というものがある。 つまり、gulpの採用理由が本当にパフォーマンスだけなら、その採用はコードベースが十分に大きくなり、ビルド時間が十分に長くなってからでも遅くは無いということだ。

というわけで、僕のプロジェクトでは、とりあえず npm script で出来る限り頑張ることにした。

npm script で使える便利なモジュール#

npm script を使って複数のプラットフォームで動作するビルドスクリプトを書くにあたって便利なモジュールがいくつかある。

cross-env#

これは、簡単に環境変数を設定するためのモジュールだ。

環境変数の定義方法は、Linux と Windows で微妙に違う。これは、その差分を吸収するために使う。 とは言え、NODE_ENVNODE_PATHくらいしか設定するものは無いけどね。

npm-run-all#

これは、npm script 内で他の npm script をまとめて呼び出したり、複数の npm script をそれぞれ別なプロセスとして並列に動かしたりできるモジュールだ。

このモジュールがあるから、npm script で頑張れると言っても過言ではない。

例えば、こんな使い方ができる。

{  "name": "sample",  "scripts": {    "compile": "run-p compile:*",    "compile:main": "cross-env NODE_ENV=production webpack --profile --config config/webpack.main.js",    "compile:renderer": "cross-env NODE_ENV=production webpack --profile --config config/webpack.renderer.js"  }}

これを、yarn compile で実行すると、compileタスクには、run-p compile:*とあるので、compile:maincompile:rendererが同時に実行される。

rimraf#

これは、指定したディレクトリの中にあるファイルやディレクトリの全てを消せるモジュールだ。

これも、Linux と Windows の差分を吸収するために使っている。

モジュールバンドラについて#

タスクランナーは使わないがモジュールバンドラは使う。JavaScript におけるモジュールバンドラは、C コンパイラに対するリンカみたいなものだ。

役割と責任範囲に応じてバラバラに分割された JavaScript がBabelにコンパイルされて ES5 で動くように変換される。 この時点で、それぞれのファイルサイズは少々大きくなったりするが、ファイル数に変化はない。 出来たファイル群を単に結合すれば動くと思うかもしれないが、そうではない。

コンパイルしなければ動作しないファイルは他にも CSS のメタ言語がある。Sass や Less といったメタ言語も同様に CSS へコンパイルする。 CSS のメタ言語はコンパイル時点でファイルが一つに結合されることが多い。

CSS Modulesを使うなら、CSS と JavaScript の整合性を取りながらコンパイルする必要もある。

これらのコンパイル済みリソースをブートストラップエントリとなる HTML に統合することで、JavaScript のアプリケーションは動作するモジュールバンドルになるのだ。

モジュールバンドラの選び方#

候補になるモジュールバンドラはBrowserifywebpackRollupBackpackなど、 いくつかある。でも、webpack以外の選択肢はない。

何故なら僕のプロジェクトではCSS Modulesを使うからだ。

加えて、webpack-dev-serverに強く依存して実装されているReact Hot Loaderを使うからだ。

webpack2.2 が正式リリースされたが、 extract-text-webpack-pluginの β が取れていないので、まだ使っていない。

かなり丁寧な移行ドキュメントが用意されているので、その気になればすぐに移行できるだろう。

webpack関連の理解すべきモジュール#

そもそもwebpackは、かなり大きなアプリケーションで丁寧に学習する必要がある。だから、あまり多くを追加しないようにと考えている。

webpack-dev-server#

これはwebpackのビルド生成物を簡易的に動かすためのサーバだ。

webpack用の設定ファイルをもとに動作するので、Expressなんかで簡易サーバ作るよりもさらに簡単にアプリケーションを動かせる。

プロキシとして動かしたりもできるなど機能は豊富なので、ドキュメントを一通り読んでおくと便利に使える。

ちなみに、ウェブブラウザは file://localhost を特別扱いするので、ブラウザの新しい機能をふんだんに使うアプリケーションを書いている時には、ちゃんとしたホスト名を割当てて動かすこと。

webpack-merge#

webpackの設定ファイルは NODE_ENV 毎にオブジェクトを作ることになる。 複数の環境で同一の部分と、環境ごとに違う部分を分けて記述するなら、設定オブジェクトを合成する必要がある。

その時に使えるのが、このモジュールだ。特に高度なことをしているわけではないので使わなくても良いけどね。

webpack-validator#

webpackの設定ファイルが正しく記述されているのかを確認できるモジュールだ。

webpack2 では、同等の機能が実装されているので、こういうモジュールは必要ない。

Electronの話#

Electronは複数の OS で動作する GUI アプリケーションを作るためのフレームワークだ。内部的にはNode.jsChromiumが動く。

Electronは、Chrome 由来の富豪モデルで動作する。どういうことかと言うと、スレッドが無く、そういうことをしたいならNode.jsのプロセスを起動する。タブというか Window 一つに一つにも、それぞれプロセスが起動する。

プロセスをどんどん起動するモデルでは、状態を共有するためにプロセス間通信するしかない。つまり、スレッド競合のようなややこしいバグは起きない。その代わり、メモリを大量に消費する。

この手のツールキットは昔から色々ある。僕がそれなりに知っているものだと、JavaFXQtなんかがそうだ。

これらに対して、Electronが優れている点はウェブアプリケーションを書くための知識セットで GUI アプリケーションを作れることだ。

何故 Electron に取り組むのか#

僕にとっては、VS CodeElectronで実装されているというのが大きい。

僕の 20 代を捧げた eclipse のプラグインフレームワークを設計したErich Gamma先生の最新作がVS Codeだ。 十年以上の間、彼を全能なる神の一種だと考えていたが、vscode-tslintのダメなコードを読んで少し親近感が沸いた。

Erich Gamma先生は、アーキテクトとしては素晴らしい業績を残しているが、プログラマとしては凡人かそれ以下だ。 コードはその辺から質の悪いのをコピペしてるし、まともなユニットテストも無い。規模が小さいし、ほぼ一人で作業してるんだから、少々雑でも良いだろって?まぁ、そうだよね。

こういうことがカジュアルに見えるようになったのは、GitHub 時代の素晴らしさだと思う。

話を戻すと、他に普段使いしているElectronで実装されているアプリケーションとしては、GitKrakenや、CurseInsomniaMarpがある。 Slack の Windows クライアントもElectronだけど、特に使い勝手が良くないので使っていない。蛇足だけど、Kindle の Windows クライアントは Qt だ。

昔は UI が OS ネイティブなものとかけ離れているアプリケーションは嫌われたものだ。 最近は OS ネイティブな UI とウェブアプリケーションの UI が徐々に近づいているので、Electronベースのアプリケーションが放つ違和感は小さい。

Electronに取り組む最後の理由は、各 OS で動作する実行バイナリを作るためのビルドプロセスが簡単だからだ。

electron-builderなら、package.json に少々設定を書いて CI サーバでちょいちょいとビルドすれば、インストーラ付きの実行バイナリがパッと出てくる。

まとめ#

これでやっと、作りたかったアプリケーションの開発に取り掛かれる。

本来なら正月休みに最初のプロトタイプが出来てるはずだったんだけども、腕が鈍ってるのか見積もりに失敗して、今やっと開発環境が整ったのだ。

余りにも沢山のことを学習したし、何かと理由を付けて切り捨てたものもある。

アプリケーションを作ってると、生みの苦しみみたいなものがあって、それから逃れるために取らなかった選択肢に意識が向いたりする。 切り捨てたものとその理由を忘れて、時間を浪費したくなるのだ。

そういう精神状態に自分がなった時に、ちょっとググるだけで、自分のエントリが出てくれば思い直せるだろう。そういう目的で、このエントリを書いた。

僕が JavaScript によるアプリケーション開発の専門家と議論する際にも、このエントリがあれば状況認識を合わせるための基準にできるだろう。

こんなに長いエントリを最後まで読んでくれたなら、あなたには感謝の言葉しかない。ありがとうございます、そして、お疲れさまでした。