JavaScriptの Module Loading の仕組みを学ぶ。
プログラムを複数の機能に分割した単位のこと。ここでは特にファイル単位で分割したものを指す。
例えばRubyでは、
# lib.rb
def print_hoge(n)
n.times do
puts 'hoge'
end
end
# main.rb
require 'lib'
print_hoge(100)
モジュールに分割することによって、ソフトウェアの設計原則である関心の分離やカプセル化を達成することができる。
JavaScriptは本来ブラウザ上でちょっとした処理を行うためのスクリプト言語であったため、長らくモジュールシステムが存在しなかった。
JavaScriptが発展するにつれて、様々なモジュールシステムが提案、実装されてきた。
- AMD
- CommonJS
- ES2015 Module
JavaScriptの大きな特徴の一つである、非同期処理を最大限活用したモジュールシステム。
基本的な使い方は以下の通り。
// lib.js
define(() => {
return (n) => {
console.log('hoge\n'.repeat(n));
}
})
// main.js
define(['./lib.js'], (printHoge) => {
printHoge(100);
});
AMDはおもにブラウザ上で動かすためのモジュールシステムとして普及した。ブラウザではこのような機能は実装されていないため、使用するにはRequireJSなどのライブラリを読み込む必要がある。
また、AMDはDojoなどのライブラリによって拡張され、パラメーターなどを渡すこともできる。
// lib.js
define({
load: (name, require, load) => {
const n = parseInt(name);
return () => {
console.log('hoge\n'.repeat(n));
};
}
})
// main.js
define(['./lib.js!100'], (printHoge) => {
printHoge();
});
後述するCommonJSの仕様に似せたAPIも存在する。
厳密にはCommonJSとはモジュールシステムを含むJS共通基盤仕様のことだが、現在ではモジュールシステム以外の仕様はほとんど使われない。
おもにサーバーサイドでの仕様を想定して策定されたモジュール仕様。AMDと異なり、同期的である。
// lib.js
module.exports = (n) => {
console.log('hoge\n'.repeat(n));
};
// main.js
const printHoge = require('./lib.js');
printHoge(100);
Node.jsがこれを採用したということもあり、サーバーサイドではこの手法が一般的である。
Node.jsではこれに加えてビルトインライブラリやnode_modules
ディレクトリにインストールされたnpmモジュールなどを読み込むことができる。
また、読み込むファイルを動的に指定することができる。
const modules = {};
['hoge', 'fuga', 'piyo'].forEach((file) => {
modules[file] = require(`./${file}.js`);
});
ES2015というJavaScriptの最新仕様に言語仕様として組み込まれる予定だったモジュールシステム。
ES2015 Module という名前だが文法以外はまだ仕様が確定していない。が、多分こんな感じになる。
// lib.js
export default = (n) => {
console.log('hoge\n'.repeat(n));
};
// main.js
import printHoge from './lib.js';
printHoge(100);
特徴として、動的なモジュール指定ができなくなる予定。よって実装によっては静的解析によって非同期読み込みが可能である。
TypeScriptやBabelによる先行実装により使用することできるが、未だに仕様の方針で揉めているので、手を出すときは注意が必要。
それまでNode.js上でしか使われていなかったCommonJSのモジュール読み込みの仕様を、ブラウザでも利用できるようにした仕組み。
具体的には、ソースコードを静的解析することにより、読み込まれているモジュールを抽出し、それらの内容をすべて含む巨大なJSファイルをビルドする。
先ほどのCommonJSのコードをBrowserifyを用いてコンパイルすると、次のようになる。
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = (n) => {
console.log('hoge\n'.repeat(n));
};
},{}],2:[function(require,module,exports){
const printHoge = require('./lib.js');
printHoge(100);
},{"./lib.js":1}]},{},[2]);
Browserifyには次のような利点がある。
- ビルドされた一つの大きなJavaScriptファイルを作成することにより、リソースの読み込み回数を削減することができる。
- npmモジュールなど、Node.jsで培われたコード資源をブラウザ上で再利用することができる。
Browserifyにはいくつかの固有の仕組みがある。
モジュールをビルドされるJSファイルにバンドルする前に、ファイルに対して行う変換処理。
これによって、
- JSファイルをminifyしたり、
- AltJSで書かれたファイルをそのまま読み込んだり、
- スクリプト以外のファイルを読み込んだり
することができるようになる。
基本的にBrowserifyと同じ仕組みで、モジュール化された複数のJSファイルをビルドしてくれる。
Browserifyと違うのは、
- JS以外にもCSSや画像ファイルなどをバンドルすることができる。
- 単一ファイルだけでなく、複数のファイルをターゲットにすることができる。
- AMDや ES2015 Module にデフォルトで対応している。(ただし非推奨)
先ほどの例をWebpackでビルドすると、以下のようになる。
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
const printHoge = __webpack_require__(1);
printHoge(100);
/***/ },
/* 1 */
/***/ function(module, exports) {
module.exports = (n) => {
console.log('hoge\n'.repeat(n));
};
/***/ }
/******/ ]);
画像をバンドルする機能などがWebアプリに向いているため、近年はこれが使われる場合も多い。
Node.jsの普及とBrowwserifyの登場によって現在ではAMDはほとんど使用されない。ただし、DojoやRequireJSなどAMDの上に培われたシステムを使う勢力も根強く残っており、どちらが優れているかは議論の余地がある。
ES2015 Module は仕様が策定されたら忌憚なく使えばよいと思う。
ブラウザ向けのバンドルシステムはBrowserifyからWebpackへの過渡期にある。
今日は特に演習することがないので、代わりにBrowserifyを使っているプロジェクト、MNEMOのプロジェクト解説をします。