石器時代みたいなWebアプリ開発をしていたエンジニアがReact.jsに導かれて2016年にやってきた話

はじめに

社内向けのシンプルなWebアプリケーションを作成することになりました。 求められているものはサービスそのものであり、実装方式については全て自分で決めていいわけですが、この際なので2016年のモダンな開発方法で書こうと思い立ちました。そこでReact.jsを採用します。今回の記事はその日記のようなものです。

この記事のまとめ

React.jsを使い始めました。そのためにnpmとBabelを入れました。ついでにgulpとBrowerifyを入れました。快適かつモダンな開発環境を手に入れました。

f:id:nurenezumi:20161220160056j:plain

2016年を代表する猫

まずReact.jsについて

React.jsってなに

Facebookの偉大なエンジニアが作ったフロントエンド用のフレームワークです。特徴として、

  1. DOMを全てJavaScriptで構築・操作する
  2. (基本的には)データの流れが一方通行
  3. コードをReact.js独自の文法「JSX」で記述する

が挙げられます。

DOM

HTMLを書くのはやめます。DOMは全てJavaScript側で構築してHTMLに流し込みます。動的に操作するのももちろんJavaScriptです。したがって、HTMLは

<body>
<div id="app">読み込み中...</div>
<script src="./lib/app.js"></script>
</body>

こんな風に超シンプル(というより空っぽ)になります。React.jsが動き出すと、この<div id="app">読み込み中...</div>が丸ごとJavaScript側で生成したDOMに置き換わるわけです。

一方通行

jQueryを使ったDOM操作でWebアプリケーションを作るとき、たとえば画面の右側(例:アイテムの絞り込み条件)と左側(例:アイテムの一覧表示)で不整合が起きるようなバグに苦しむことがあります。これは画面の要素のあちこちが独自に状態を抱え込み、しかもその要素が複雑に絡み合って情報を受け渡しするため、その管理を正しく行うのが難しくなるからです。

これに対してReact.jsでは、

  1. 画面に表示すべき全てのデータを1個のオブジェクトにまとめてしまう
  2. そのオブジェクトを受け取って画面表示する(=DOMに変換する)処理を書く

という基本原則があります。画面を書き換えたい場合は、

  1. オブジェクトの値を更新する
  2. 全ての画面要素を一括で描画し直す

で済ませてしまいます。このルールにより、画面表示と内部データは常に一貫性を持っていることが保証されます。jQueryでこんなことをやっていたら重くてまともに操作できなくなりそうですが、React.jsはまず画面更新後のDOMを内部的に算出し、発生した差分のみに対して実際のDOM操作を行うというテクニックを採用しており(これはVirtual DOMと呼ばれています)、処理は高速に行われます。差分がなければDOM操作は発生しません。

JSX

ここからが本題です。

まず、React.jsのコードはJSXという文法で記述します。これはJavaScriptを拡張した独自の文法で、JavaScriptに似ていますがJavaScriptではありません(明らかに文法エラーになります)。簡単な例を示します。

function App() { return <span>hello world!</span>; }
ReactDOM.render(<App />, document.getElementById('app'));

JavaScriptのコード中にHTMLのタグが出現しています。これは、

という処理です。コンポーネントはいくらでも作ることができます。React.jsでは、Webアプリケーションを構成する要素をこのコンポーネントに分解し、それらを組み立てることでDOMを構築していきます。

JSX→JavaScriptの変換をしたい

JSXはFacebookの偉大なエンジニアが勝手に定義した言語ですので、ブラウザは解釈できません。したがって、JSX→素のJavaScriptへの変換を誰かが行う必要があります。

1つめの方法は手動で変換をかけるものです。オンラインの変換ツールがありますので、ここにJSXをコピペすれば通常のJavaScriptに変換してもらえます。実際にやることはないでしょうが、一度このリンクを開いておいてください。

2つめの方法は毎回動的に変換をかけるものです。HTML側で

<script src="https://fb.me/JSXTransformer-0.13.3.js"></script>
<script type="text/jsx" src="app.js"></script>

と書いておくと(type="text/jsx"に注目しましょう)、JSXで書かれたapp.jsをその場でJSXTransformerがJavaScriptに変換してくれます。これはお手軽なのですが、当然ながらパフォーマンス上の問題が生じますし、JSXTransformerは既にdeprecatedになっています

最後の方法は、何らかのツールでトランスパイル*1をかける方法です。

ここでちょっとnpmについて

.NETにNuGetがあるように、PHPPEARやComposerがあるように、またRubyにgemがあるように、JavaScript*2にもnpm (Node.js Package Manager)というパッケージマネージャーがあります。多数の人々により様々なパッケージが提供されているわけですが、そのパッケージをダウンロード・インストールしてくれたり、その際に依存関係を解決してくれたり、また新しいコマンドをインストールしてくれたりする優れものです。

インストール

npmはsudo apt install npmで簡単に入ります。すみませんがAmazon LinuxなどFedora系では入れるのが地味に面倒くさいので自分で調べてください。

プロジェクト作成

npmでは、まず最初にプロジェクトを作成する必要があります。空のディレクトリに対して

npm init

を実行すると、プロジェクト名などの基本情報を入力するように要求され*3、これが終わるとこのディレクトリはプロジェクトとして管理対象になります。管理対象と言っても、単に管理用ファイルpackage.jsonが新規作成されるだけです。

パッケージ取得・インストール

これができたら次のコマンドでパッケージを取得・インストールすることができます。

npm intall --save <パッケージ名>
npm intall --save-dev <パッケージ名>

ここで--saveと書いているのは、package.jsonの中に「このプロジェクトが依存するパッケージ」としてインストールしたパッケージを記録しておくことを示します。開発時にのみ必要なパッケージをインストールする際は--save-devオプションを指定します。他の人は後からpackage.jsonを見ればそのプロジェクトのために必要なパッケージが分かりますし、package.jsonだけ手元にあれば、npm installでその中に記載されたパッケージを一括でダウンロード・インストールすることができます。

さてインストールを行うと、プロジェクトのディレクトリ直下にnode_modulesディレクトリが作成され、ダウンロードしたファイル(多くの場合はJavaScriptのコード)が入ります。これらのコードは、

var hoge = require("MODULE_NAME");

などの文法で参照することができます。これについては後述します。

また、インストールと同時にnode_modules/.bin/以下に実行可能ファイルが置かれるものもあります。これはプロジェクトごとにインストールされるものですが、プロジェクトによらず、OS全体で(グローバルに)利用したいこともあるでしょう。後述のgulpなどがそれにあたります。この場合は、

sudo npm install --global <パッケージ名>

でインストールができます。OS全体に影響が及ぶのでsudoが必要なことがあります。

Babel導入

さて、上の方で試しに利用したオンライン変換ツールの名前はBabelとありました。そもそもBabelってなんでしょう?

Babelの名前は、(おそらく)聖書のバベルの塔の記述、「全ての地は、同じ言葉と同じ言語を用いていた」から来ています。JavaScript*4にはES5, ES2015(ES6), ES2017(ES7)といくつかのバージョンがあり、また正式採用前ながらも開発者コミュニティによく使われている文法もあり、さらにはJSXのような方言も存在します。そのサポート状況はブラウザによって異なります。先進的なバージョンの文法で書かれたJavaScriptはどれ一つとしてブラウザが対応していないことだって考えられますし、現時点ではJSXを理解できるブラウザは存在しません(これから先もないでしょう)。このようなJavaScriptを古いバージョンのJavaScriptに変換し、どのようなブラウザでも解釈できるようにしてくれるのがBabelです。「同じ言葉と同じ言語」が実現するわけですね。

Babelはオンラインで使うこともできますが、普通はコマンドとして使いたいでしょう。そこでnpmを使います。このような流れで実行します。

  1. 空のプロジェクトを作る
  2. このプロジェクトにnpmでBabelをインストールする(babelコマンドも入る)
  3. このプロジェクトにnpmでBabelのreactプリセット(プラグインのようなもの)をインストールする
  4. 準備完了

順に実行します。まず空のディレクトリを作り、そこに空のプロジェクトを作成します。

mkdir helloworld
cd helloworld
npm init

次にBabelとBabelのプリセットをインストールします。

npm install --save-dev babel-cli babel-preset-react

変換が終わってしまえば稼働時には必要ないので--save-devを指定しています。これで準備は完了です。次のコマンドでJSXのファイルを読み込み、JavaScriptに変換して標準出力に吐き出します。

function App() { return <span>hello world!</span>; }
ReactDOM.render(<App />, document.getElementById('app'));
neko@ubuntu:/workdir$ node_modules/.bin/babel --presets react app.js
function App() {
  return React.createElement(
    'span',
    null,
    'hello world!'
  );
}
ReactDOM.render(React.createElement(App, null), document.getElementById('app'));

先ほどオンラインで試したものと同じコードが出力されました。

ES2015? ES2016???

上の方で出てきた「ES2015」「ES2016」について説明します。

まず、JavaScriptECMAScriptという規格に沿った原語です。ECMAScriptには、

  • ECMAScript 5th Edition (ES5)
    • IE6とかのいにしえの時代はこれ準拠のJavaScriptが使われていた
  • ECMAScript 2015 (ES2015)
    • ES6とも呼ばれる
  • ECMAScript 2016 (ES2016)
    • ES7とも呼ばれる

というバージョンがあり、更新されるたびに便利な(あるいは複雑な)機能が追加されていっています。テンプレートリテラル(文字列の組み立て)などはもうES5の時代には戻れません。

var name = "neko";

// ES5
console.log("my name is " + name + ".");

// ES2015
console.log(`my name is ${name}.`);

主要なブラウザが搭載しているJavaScriptエンジンはES2015に対応していますが、ES2016は新しすぎるためまったくと言っていいほど未対応です。

gulp導入

さて話を戻して、ここではgulpというツールを投入します。

gulpとは「ビルドシステム」で、あらかじめ定義したタスクをまとめてコマンド一発で実行してくれるツールです。たとえばJSXをJavaScriptに変換するだけではなく、ついでに圧縮とかもしたいですよね? それもコマンド一発でやって欲しいですよね? さらにはファイルの更新を監視し、それらの処理を自動で済ませたりしてほしいですよね? gulpがやってくれます。

試しに使ってみる

まず、グローバルでgulp-cliをインストールします。どこのディレクトリで実行しても構いません。

sudo npm install --global gulp-cli

これでどこからでもgulpコマンドが実行できるようになりました。次に、プロジェクトのディレクトリに移動してgulpとgulpが利用するパッケージをインストールします。ここではJavaScriptのコードを圧縮するgulp-minifyを入れてみます。

npm install --save-dev gulp gulp-minify

gulpは「あらかじめ定義したタスク」を自動で行うものだと言いました。この定義はgulpfile.jsというファイルで行います。プロジェクトディレクトリの直下にgulpfile.jsを作成し、以下のように書きます。

var gulp = require('gulp');
var minify = require('gulp-minify');

gulp.task("neko", function() {
  gulp.src("src/*.js")
    .pipe(minify())
    .pipe(gulp.dest("lib"));
});

gulp.task("default", ["neko"]);

ここではnekoという名前のタスクを定義していて、その内容はsrcディレクトリ以下の*.jsを対象にminify()して、結果をlibディレクトリに出力するというものになっています。また、タスク名が指定されなかった場合のdefaultタスクの内容としてはnekoを使ってね、ということも書いてあります。

試しにsrc/app.jsとしてこんなJavaScriptを書いて、

// tama-san is kawaii
var name = "tama";
console.log("hello, my name is " + name + " desu.");

コマンドラインgulpとだけ実行すると(明示的にgulp nekoと実行しても構いません)、libディレクトリ(存在しなければ自動的に作成されます)にオリジナルのapp.js、圧縮済みのapp-min.jsが出力されます。中身を見てみます。

var name="tama";console.log("hello, my name is "+name+".");

確かにコメントや空白が削除されて圧縮されています。

gulp + Babel

これでgulpが何者かは分かりました。gulpからBabelを使えるようにしましょう。まずgulpが利用するパッケージをインストールします。

npm install --save-dev gulp babel-cli babelify browserify vinyl-source-stream babel-preset-react babel-preset-es2015

見知らぬ名前がたくさん出てきました。

  • gulp : gulp本体
  • babel-cli : Babel CLI
  • babelify : Babel
  • browserify : (後述)
  • vinyl-source-stream : gulpが利用する
  • babel-preset-react : BabelのReact.js用プリセット
  • babel-preset-es2015 : BabelのES2015用プリセット

分かってしまえばどうと言うことはありませんね!*5

gulpfile.jsの内容はこうなります。

var gulp = require("gulp");
var babelify = require("babelify");
var browserify = require("browserify");
var source = require("vinyl-source-stream");

gulp.task("neko", function() {
  browserify({entries: "src/app.js"})
    .transform(babelify)
    .bundle()
    .on("error", function (err) { console.log("Error : " + err.message); })
    .pipe(source("app.js")) 
    .pipe(gulp.dest("./lib/"))
});

gulp.task("default", ["neko"]);

gulp.task("watch", function() {
  gulp.watch("src/*.js", ["neko"]);
});

さらにプロジェクトディレクトリの直下に.babelrcというファイルを新規作成します。これはBabelの動作内容を決める設定ファイルです。ReactのJSXとES2015を使いたいのでこのように書きます。

{"presets": ["react", "es2015"]}

これで下準備が全て整いました。srcディレクトリにapp.jsを配置します。

const App = props => <span>hello world!!!!</span>; // ES2015の新文法
ReactDOM.render(<App />, document.getElementById('app'));

これでgulpを実行すると、libディレクトリに変換されたJavaScriptが出力されます。これを次のHTMLで読み込めば動作します。

<!doctype html>
<html>
<head>
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
</head>
<body>
  <div id="app">読み込み中...</div>
  <script src="./lib/app.js"></script>
</body>
</html>

ファイル監視

gulpとだけ実行すると、gulpはタスクを1回だけ行って終了します。gulp watchとすると、gulpはファイルの更新を監視し、更新されたらその場その場でタスクを繰り返し実行してくれます。つまり、もう手動でコマンドを実行する必要はなくなるのです。

Browserify

次に、上の項で唐突に出てきたBrowserifyの解説をします。

古き良き時代

ここでjQuery UIのことを思い出してみましょう。ドキュメントを読むと「jQuery UIはjQueryに依存している」と書いてあります。したがって、HTMLに

<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>

のように書かないといけないのでした。このような依存関係は人間がドキュメントを読んだり書いたりして管理しないといけません。また、jQueryグローバル変数$を利用するため、同じ変数を利用するライブラリとは同時に利用できません*6。これも利用者が気を配ってやる必要があります。

Node.jsではどうやっているか

このような牧歌的な時代は終わりました。現代のJavaScriptではこんなのどかなことを言っていてはいけません。

サーバーサイドJavaScript実行環境であるNode.jsには他モジュールのインポート機能があります。たとえばAWS APIを使いたいのであれば、まずnpmを使ってaws-sdkパッケージをインストールします。

npm install --save aws-sdk

プログラムコード中でaws-sdkをインポートするにはrequire()文を使います。

var AWS = require("aws-sdk"); // aws-sdkという名前のモジュールを読み込む
var ec2 = new AWS.EC2();
ec2.startInstances(...); // EC2インスタンスを起動

ブラウザでも同じことがしたいと誰もが思います。しかし(現在の)ブラウザに許されるのは<script>タグでHTTPリクエストを発生させてソースコードを読み込むことだけ。このような仕事はブラウザの領分を超えています。

そこでBrowserify

Browserifyは、npmと組み合わせてこのインポート構文をブラウザ用JavaScriptにも持ち込むためのツールです。このように使います。

  1. npm install --save <パッケージ名>で必要なパッケージをインストールする
  2. JavaScriptのコードの中にrequire()文を書く
  3. Browserifyを通すと、require()が消滅し、代わりに参照したパッケージが埋め込まれる

この結果、ユーザーは参照しているライブラリ(パッケージ)が全て結合された1つの大きな.jsファイルを得ます。名前空間の汚染がないような工夫も行われています。

例1

JavaScriptで日次を任意の書式(たとえばYYYY/mm/dd HH:mmとか)で出力したいというとき、整数のフォーマットが面倒でいつも頭を悩ませていました。従来はそのためのjQueryプラグインなどに頼っていたわけです。

我々はもうjQueryと手を切ると決めたので、ここでnpmパッケージを探すとdateformatというものがあります。便利そうなので使わせてもらいましょう。インストールは簡単です。

npm install --save dateformat

あとはrequire()で読み込むだけです。

var dateFormat = require("dateformat"); // これ!

var timestamp = dateFormat(new Date(), "yyyy/mm/dd HH:MM");
console.log(timestamp); // "2016/12/21 12:34"

これをgulpでビルドすると、require()部分が消えてdateformatが取り込まれ、単体で動作するJavaScriptソースコードが出てきます。

例2

最初の方の例では、react.js, react-dom.jsを直接CDNから読み込んでいました。これを統合してみましょう。

まず、npmでreact, react-domを追加します。

npm install --save react react-dom

次にapp.jsを修正します(上2行を追加します)。

var React = require("react");
var ReactDOM = require("react-dom");

const App = props => <span>hello world!!!!</span>; // ES6の文法
ReactDOM.render(<App />, document.getElementById('app'));

そしてgulpでこのapp.jsをビルドすると、生成されたlib/app.jsはライブラリを取り込んで700KBになりました*7。これならもうスクリプトは1個で済みます。

<!doctype html>
<html>
<head>
</head>
<body>
  <div id="app">読み込み中...</div>
  <script src="./lib/app.js"></script>
</body>
</html>

終わりに

ここまで準備してしまえば、その他のライブラリを導入するのも、別のトランスパイラを利用するのもぐっと障壁が低くなります。特にJavaScriptの開発環境は異様なほど流行廃りが速いのですが、なるべく便利なものをどんどん取り入れて快適に開発を進めていきたいものです。

*1:コンパイルのようなものですが、コンパイルのようにバイナリを吐くわけではないのでこう呼ばれることがあります

*2:正確にはNode.js

*3:全てEnter空打ちでOKですが、プロジェクト名に大文字は含められないため、ディレクトリ名に大文字が入っている場合だけは手で入力する必要があります

*4:正しくはECMAScript

*5:こういうところでは気になってもとりあえず先に進み、しかし後から戻ってきてちゃんと理解するのが惑わずにいるコツです

*6:近年ではあまり名前を見なくなりましたが、prototype.jsが該当します

*7:圧縮すればもっと縮みます