JavaScript のモジュールを理解する


おはこんばんちは。JavaScript を利用している皆さん、お元気していますか。 JavaScript について触れていると目にする「モジュール」という言葉について、モヤモヤしていませんか。

特に JavaScript を触り始めた頃、みなさんは「require と import って何が違うの?」や「export と module.exportsって何が違うの?」といった疑問を持ったことはないでしょうか。 あるいは、適当に Web 上で拾ってきた誰が書いたかわからないコードをコピペして利用しようとしたときに、import 文でエラーが出たことはないでしょうか。コピペエンジニアの私には、その経験がたくさんあります。

さて、そのようにファイルレベルで情報を受け渡ししたい時に利用する import や require や export について、少しまとめていきたいと思います。

JavaScript モジュール とは

そもそも JavaScript 自体は Web ブラウザにおいてユーザーの操作に対して反応したり動的に表示をカスタマイズする目的で1995年頃に生まれました。 そのため、他の言語では当たり前に存在する外部ファイルを読み込んだりスコープという概念が存在しませんでした。

一方で、時代が進むにつれて「JavaScript をサーバー側でも利用したい」といったブラウザ以外での活躍の場を求める声があがりました。 そこで大きな問題になるのが、上記のような外部ファイルを読みこむようなモジュールに関する機能(機能といっていいのだろうか)が JavsScript には存在しないことでした。

そのような背景があり、 CommonJS というモジュールの仕様が誕生しました。 そして、CommonJS に加えて AMD や ECMAScript Modules といったモジュールの仕様が次々に爆誕し、JavaScript のモジュール仕様が複数存在することになって現在のモジュール事情は混沌としているわけです。

また、JavaScript におけるモジュールとは飽くまでも仕様であり、それ自体が機能を提供するわけではありません。モジュールの仕様自体は複数存在し、そのプログラムがどの仕様に則って書かれているかを解釈するのは読み込み側の責務となります。 そのため、ECMAScript Modules の仕様に則って書かれている import を利用したプログラムを Web 上で拾ってきてコピペしても自分の環境は CommonJS で書かれていると読み込んでしまい動かなかった、というようなことが起きるわけです。

様々な JavaScript のモジュール仕様とトレンド

さて、そんな JavaScript のモジュール仕様ですが、実際にそれぞれの仕様に則って書かれたコードで見ていきましょう。

CommonJS

Node.js は CommonJS のモジュール仕様を採用しているので、Node.js を利用したことがあれば馴染み深い書き方でしょう。 このままだとブラウザでは利用できないので、モジュールシステムである Browserify を利用してバンドリングする必要があります。( Browserify によってバンドルされた成果物をブラウザ上で利用することになる)

また、CommonJS における require は同期的に読み込みを行います。ちなみに、require.ensure として非同期的に読み込む Proposal が立っていましたが、実装されていません。Webpack にある require.ensure はここから来てるのかな(教えて欲しい)

// Import 
const sample = require('./sample.js'); 

// Export
module.exports = function example() {
    return "example"
}

AMD

RequireJSのドキュメントみると良いです。(実際に書いた経験はないので、あまり深くは語れません) Asynchronous Module Difinition こと AMD ですが、名前の通り非同期的に読み込みます。CommonJS がサーバー向けであるのに対して、 AMD はブラウザ向けです。

// Import
define(["sample"], function (sample) {
    sample();
});

// Export
define(function () {
    return function example(){
        return "example"
    };
});

UMD

USJ は Universal Studio Japan ですが、 UMD は Universal Module Definition の略です。AMD と似ているので注意です。

自前で書くのが少々面倒だった(というより書けない)ので雑に TypeScript のファイルをトランスパイルして生成しました。書こうと思えば書けますから!

少し読みづらいですが着目すべきは最初の if 文で、コードからもわかる通り、UMD は CommonJS と AMD の両対応、すなわちサーバーでもブラウザでも動きます。ユニバーサル。 サーバー/ブラウザ問わず動くということから、ライブラリなどの成果物として利用されることがあります。

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    function example() {
        return "example";
    }
    exports.default = example;
});

ECMAScript Modules

ようやくやってきました大本命。最近 JavaScript を始めた人には馴染み深い書き方かと思います。ESModules は2015年にリリースされた ES6 で策定され、今後は ESModules が主流になっていくでしょう。

IEを除いた主要ブラウザにおいては ES6 の対応が既に済んでいるので、トランスパイルなどをしなくてもそのまま動きます。( IE の利用率は 5 %を切ってるはずなので、もはや主要とは言えないけど)

上述の通り Node.js は CommonJS を採用しており、長らくの間 Node.js 上で ESModules を使う際には引数として --experimental-modules を与える必要がありましたが、Node v13.2.0 でようやく正式にサポートされました(対象のPR) ですので、今後はより一層 ESModules が主流になっていくと思われます。

// Import
import sample from "./sample.js"

// Export
export function example1(){
    return "example1"
}

モジュールバンドラー

上述の通り、モジュールについては飽くまでも仕様であるため、それらのコードを任意の環境に向けてバンドリングしていく必要があります。 その時に利用するのがモジュールバンドラーであり、今回はブラウザにおいて、なぜバンドリングする必要があるのかを考えていきます。

例えば HTML において script タグを用いて A, B, C の 3 つの JavaScript ファイルを読み込む際に、 B のファイルは A のファイルを読み込んで処理を行うにも関わらず先に B を読み込んでしまうと、動かないわけです。

<script src="./B.js"></script> <!-- 内部的に A.js を読み込んでいる -->
<script src="./A.js"></script>
<script src="./C.js"></script>

この読み込み順序を人類がメンテナンスしていくのは非常に難易度が高く、このあたりの依存解決をモジュールバンドラーで行う必要があります。 であれば、1つのファイルにしてしまえばええんちゃう?って思うでしょう。そうです。正解です。モジュールバンドラーは依存解決と合わせて、1 つの JavaScript ファイルを生み出してくれるのです。

そのようにして、人類は Webpack の設定に躓きながらもパフォーマンスと開発体験の両方を手に入れているというわけです。ナム。

おわりに

JavaScript に関する技術的な側面を除いたストーリーに関しては、こちらの記事を読むと面白いかもしれません。英文 PDF で 189 ページになるのでとんでもない量ですが、序盤にサマリーもあるのでサラっと理解できるでしょう。

Node.js が正式に ESModules をサポートしたことによってスタンダードが CommonJS から ESModules へ移行が一層進むでしょうから、フロントエンドとバックエンドで共に JavaScript (TypeScript) を採用して開発するっていうシーンが今後ますます増えていくんじゃなかろうかと思っています。

特にフロントエンドの初学者においては Babel や TypeScript の知識だけでなく React (Vue) のようなテンプレートエンジンの知識も必要になり、結果的に闇の webpack.config.js が出来上がったりするので、このあたりも JavaScript のエコシステムが整うことによって解決されるとよいですね。