2020.04.30

モダンJavaScript概論 − Node, npm, ECMAScript, Babel, Webpack


この記事では、モダンな JavaScript を書くための前提として知っておくべき、いくつかのキーワードを紹介します。想定読者は、初心者 〜 jQuery なら書けるレベルの開発者です。これから React や Vue.js を学びたい方、または Web 開発の世界に飛び込む新人さんなどの学習の助けになれば、と思います。

「モダン」な JavaScript

ここで言う「モダン」とはどういう意味でしょうか?

「モダン JavaScript」という表現は数年前から目にしますが、大抵 ES2015(JavaScript の 2015 年バージョン)以降の JavaScript を指しているようです。確かに、const / let やアロー関数などの文法的な改善、API の拡充は JavaScript をより便利な言語にしたかもしれません。

ただ、私が思うに、モジュール機能の獲得こそが JavaScript のモダン化にとって最も重要な要素です。

モジュール機能というのは、他のファイルで定義した関数やクラスなどを読み込む機能です。Java や Python、Ruby、PHP、C# など大抵のプログラミング言語には備わっています。しかし JavaScript には、長く高級プログラミングとしては普通のこの機能がありませんでした。

コードのモジュール化はプログラミングにとって非常に重要な要素です。画像を左右に動かすスクリプトを書く程度であれば必要ないかもしれませんが、より複雑なアプリケーションを構築するためには欠かせません。

JavaScript は、モジュール機能の獲得によって、本格的なアプリケーションを書くのに足る言語に成長し、時代が求める複雑なフロントエンド開発が可能になりました。

さて、ここからは、「Node.js」「npm」「ECMAScript」「Babel」「Webpack」という、現代の JavaScript に欠かせない5つのキーワード(ツール)について説明していきます。いずれも JavaScript の学習や開発を進めるうえで、なんとなく目にする、耳にすると思いますが、それぞれ何者なのか、できるだけ平易に説明してみたいと思います。

Node.js

Node.js とは、JavaScript の実行環境の一つです。ブラウザだけでなく、より汎用的に、サーバーサイドで JavaScript を利用するために開発されました。

JavaScript の実行環境は、プログラミング言語を読み込んで動作させるための JavaScript エンジンと、各環境に必要な追加機能で構成されています。

Node.js

JavaScript はウェブブラウザ上で実行されることが多いと思いますが、ブラウザにも各ブラウザベンダーによって開発された JavaScript 実行環境が組み込まれています。

実行されるのがブラウザであろうとサーバーであろうと、同じ JavaScript なのですから、もちろん言語仕様は共通です。要するに if とか for とか、Array とか…そういった基本の言語機能は使えるべきで、その部分を解釈してくれるのが JavaScript エンジンです。

その JavaScript エンジンに、さらに環境特有の追加機能が搭載されます。

ブラウザでいうと、DOM を操作する getElementById であったり、音声や動画を再生する機能などです。これらの機能は、ブラウザ上で実行するからこそ必要な機能です。

これと同じように、Node.js には、サーバー上で実行するからこそ必要になるような追加機能(Node.js Code Modules)が組み込まれています。たとえば、サーバー内のファイルの読み込み機能などが特徴的でしょう。ブラウザの実行環境には、セキュリティ上の理由からユーザーのコンピュータ内のファイルを読み込む機能はありません。

もう一つ重要な機能として、外部モジュールの読み込み(require)が挙げられます。

モジュール読み込みについては、その後、JavaScript そのものの共通仕様として import / export が提案されました。現在は Node.js の require とどちらも使えます。

Node.js によって、Java や PHP などのように JavaScript でも Web サーバーアプリが書けるわけですが、Node.js の恩恵はそれだけでなく、フロントエンド開発にも大きな飛躍をもたらしました。JavaScript がブラウザ以外で汎用的に動かせるということは、開発者の PC 上でも動かせるということです。Babel や Webpack、Rollup、Browserify、Gulp など、Node.js 環境で動作する多くの開発ツールが生まれ、フロントエンド開発のモダン化が推し進められました。

npm

npm(Node Package Manager)とは、JavaScript 向けのパッケージマネージャです。

パッケージとは、インストールして利用可能な外部モジュール、ライブラリのことです。そのパッケージを集約するリポジトリ(データベース)、およびパッケージの管理を行うコマンドラインツールをひっくるめて npm です。

パッケージの管理は、リポジトリへのパッケージのアップロード〜公開と、リポジトリからのパッケージのダウンロードなどがその内容です。

npm

npm は、Node.js をインストールすると一緒に付いてきます。その名の通り、Node.js とは切り離せない存在です。Node.js がモジュール読み込み機能(require)を導入したことにより、第三者が開発したパッケージを利用する開発スタイルが広まり、それらのパッケージを管理する npm が生まれたというわけです。

一般論としてなぜパッケージマネージャが必要なのかというと、本格的なアプリケーションを構築しようとすると、様々なパッケージを導入することが普通で、それらの管理は手作業では実質無理だからです(*注1)

yarn という同様のパッケージマネージャもあり、こちらもよく使われています。リポジトリは npm と一緒で、コマンドラインツールが置き換わっている、というイメージで OK でしょう。

ECMAScript

ECMAScript とは、Ecma インターナショナルという国際標準化団体が定めた JavaScript の言語仕様(言語標準)です。

JavaScript の言語機能について、どんな機能があったら便利か、どのように動作するべきか、既存の機能との整合性を保つためには…など、JavaScript をアップデートするためにいろいろ考える人たちが Ecma で、彼らが策定した言語仕様が ECMAScript と呼ばれます。その後、各ブラウザベンダーが、策定された仕様を、自らのブラウザに組み込まれた JavaScript エンジンに実装していく、という流れです。

言語仕様が発案されてから実際に策定されるまでの工程は、以下のステージに分かれています。各段階でレビューや議論を経て、ステップアップしていくのですね。

  • Stage 0(アイデア)
  • Stage 1(提案)
  • Stage 2(ドラフト)
  • Stage 3(仕様完成)
  • Stage 4(策定完了)

2015年から ECMAScript は毎年新機能の策定がなされています。各年度のバージョンは、西暦をつけて ES2015、ES2016…と呼ばれ、最新は ES2019 です。

ポイントは、仕様の策定と実装が別団体によって行われる、という点です。

ブラウザもいろいろありますよね。MicroSoft の Edge や IE、Google Chrome、Firefox、Safari、Opera などなど。ECMAScript で新しい仕様が策定、発表されても、各ベンダーが足並み揃えて実装するわけありません。いくつかは商用でもあります。自社ブラウザの優位性や先鋭性のために、提案ステージの仕様を実装するブラウザもあるかもしれません。

プログラマとしては最新鋭の便利な機能を使いたい、だけどブラウザによってはまだ実装されていない、というギャップが生まれるわけです。

開発する際は、このギャップを意識する必要があります。ある機能が動作するブラウザを調べられる Can I Use という有名なサイトもあります。

対応ブラウザすべてで、書きたいコードが動くのか?という問いは、フロントエンド開発特有でしょう。サーバサイド開発では、実行環境を開発側がコントロールできるからです。

このギャップには、さらに一歩進んだ解決策もあります。次に紹介する Babel です。

Babel

Babel とは、ある構文の JavaScript を、別の構文の JavaScript に変換するツールです。

ECMAScript の項で、仕様と実装のギャップについて触れましたが、Babel はその解決策の一つです。つまり、最新の構文で書かれたコードを、指定されたブラウザで動作する構文に変換してくれます。とても賢いツールですよね。

Babel

変換前
const func = a => a * 2;
変換後
"use strict";

var func = function func(a) {
  return a * 2;
};

この変換処理はトランスパイルと呼ばれます。高級言語からマシン語への変換をコンパイルと言いますが、この場合は高級言語から高級言語への変換なので、別の名前がついています。

Babel は、React でおなじみの JSX などの標準外の構文を実行可能な JavaScript に変換するためにも使われます。変換を試せるページもあるので遊んでみてはいかがでしょうか。

コードの変換が行われるのは、for 〜 of 文やアロー関数、クラス記法などの「構文」だけで、Object.assign のようなメソッドについては、Babel だけでは解決しません。未実装の(存在しない)メソッドの補完を行いたい場合は、ポリフィルと呼ばれる補完コードを別途入れる必要があります。ただ、Babel にはポリフィルを助ける機能も含まれています。

Webpack

Webpack とは、複数ファイル間の依存関係を解決して、ブラウザで実行可能な JavaScript ファイルを生成するツールです。

言い換えると、モジュール機能をフロントエンドにもたらすツールです。

前述したように、Node.js および近年の ECMAScript 標準仕様により、JavaScript にもモジュール読み込み機能が導入されました。

// Node.js 独自仕様
const myModule = require('./my-module.js');

// EcmaScript 標準
import myModule from './my-module.js';

問題は、モジュール機能をブラウザ上で動作させるための標準的な開発手法が確立されていないことです(*注2)

Webpack の働きをざっくり言うと、開発マシン上であらかじめモジュールの読み込みを解決して、importrequire のないファイルにまとめることです。まとめる処理をバンドルと呼びます。

たとえば、a.jsb.js をインポートしているとしたら、a.jsb.js の内容をまとめた別ファイルが作られます。こうしてできたファイルだけを配布すれば、ブラウザでも問題なく動作しますよね。

ちなみに開発マシン上でこのような処理が行えるのは、Node.js のおかげです。

さてここから、なぜ Webpack や、そもそもモジュール読み込み機能を使うとより高度な開発ができるのか、について説明してみたいと思います。

a.jsb.js に依存しているとしましょう。「依存している」とは、具体的には、b.js で定義された関数やオブジェクトなどを a.js で使用する、ということです。

Webpack

requireimport などのモジュール機能をあえて使わずに、b.js での定義内容を a.js でも参照することも可能です。b.js の関数などが、グローバルスコープに定義されている前提ですが、以下のように、順番に読み込めば良いです。

<script src="/js/b.js"></script>
<script src="/js/a.js"></script>

jQuery 時代の JavaScript 読み込みは、まさにそのようになっていたかと思います。

<script src="/js/jquery.min.js"></script>
<script src="/js/jquery-plugin-aaa.js"></script>
<script src="/js/jquery-plugin-bbb.js"></script>
<script src="/js/app.js"></script>

このような書き方は、<script> タグの順番を間違えると動かなくなります。

もう少し複雑な例を見てみましょう。

Webpack

この依存関係をモジュール機能なしで解決するには、以下の順番で読み込みます。

<script src="/js/c.js"></script>
<script src="/js/d.js"></script>
<script src="/js/b.js"></script>
<script src="/js/e.js"></script>
<script src="/js/a.js"></script>

上記からも推測されるように、ファイル数が増え、依存関係が複雑になるほど、人力での管理は困難になります。

たとえば、c.jsd.js は順番が逆でもOKですが、d.jsb.js が逆になると動きません。b.jsd.js に依存しているためです。このことは HTML ファイルだけを見ても判断ができません。

依存関係の途中に別のファイルを追加する場合も、慎重に読み込む箇所を判断する必要があります。依存関係が変われば、現在の順番では動かなくなるかもしれません。

モジュール機能と Webpack を使えばこの課題を解決できます。Webpack は、自動的に依存関係を解決して、各 JavaScript ファイルを読み込み、記載内容をまとめて、ブラウザでそのまま実行可能なファイルを出力します。

Webpack

そのおかげで、開発者自身が依存関係を管理する必要はなくなります。

<script src="/js/main.js"></script>

上の図は1ファイルにまとめる絵になっていますし、簡単な設定では実際1ファイルになりますが、その場合はファイルサイズが大きくなり、パフォーマンスが悪化する恐れが出てきます。

この問題に対処するため、Webpack には Code Splitting の機能も備わっています。発展的な内容なのでここでは詳しく触れられませんが、1ファイルに何もかも突っ込んで大きなファイルを作るのではなく、アウトプットをいくつかのファイルに分割するテクニックです。分割したファイルはそれぞれ <script> で読み込んだり、もしくは実行時に、必要なタイミングで必要なファイルを非同期通信で取得したりします。

Webpack は JavaScript の他にも、CSS や画像ファイルまでバンドルすることができます。

まとめ

最後に、今回紹介したキーワードやツールの関連を示した図を作りました。

まとめの図

以上、この記事では、今時の JavaScript に欠かせないキーワードを紹介しました。

文法だけ学んでも JavaScript 開発者とは言えない時代になっているので、初学者さんのステップアップの一助になれば幸いです。


  1. たとえば、あるライブラリAのバージョン1.2.3を導入したとしましょう。このライブラリは別のライブラリBのバージョン4.4.4とライブラリCのバージョン9.8.7に依存しています。「依存している」とは、内部で利用しているという意味です。さて、Aをバージョン2.0.0にアップデートすることになりました。そのためには、Bもv5.0.0にアップデートする必要があります。さらに、Bも別ライブラリD、E、Fに依存しているとしたら…。実際は管理対象のライブラリもその依存関係ももっとたくさんあります。これらの管理を手作業で行うのは非現実的です。

  2. 実は <script> 要素でモジュールを読み込む仕様type="module")は策定されていて、IE 以外のブラウザでは2017〜18年頃にすでに実装されているのですが、実際の開発シーンでどのように適用するか、という観点ではいまだに実験的だと思います。