scriptタグを並べる
<body>
<script src="a.js"></script>
<script src="b.js"></script>
</body>
先に書かれた a.js で定義された内容は b.js で読むことができる。
$ cat a.js
const a = 'a is defined';
const divA = document.createElement('div');
divA.textContent = (typeof b !== 'undefined') ? b : 'b is undefined';
document.body.appendChild(divA);
$ cat b.js
const b = 'b is defined';
const divB = document.createElement('div');
divB.textContent = (typeof a !== 'undefined') ? a : 'a is undefined';
document.body.appendChild(divB);
依存が増えてくると順番を考えるのが大変。さらにグローバルな名前空間を汚染してしまう。
b is undefined
a is defined
AMDとCommonJS
というのも、かつてのJSにはモジュールを読み込む仕組みがなかった。 そこで考えられたのがAMDやCommonJSというフォーマット。 AMD(Asynchronous module definition)はRequireJSによって提供される require() で動的にscriptタグを埋める。CommonJSはNodeでもおなじみの require() で、これにWebpackを通して一つのファイルにまとめておく。同じ関数名が使われているが全くの別物。
ES Modules
今は言語仕様にECMAScript Modulesが追加され、普通に import でモジュールを読み込めるようになったが、 対応ブラウザがまだ少ないこともあり基本的にはWebpackをかけることになる。 Nodeにも実装されつつあるがStableになるのはまだ先のようだ。
RequireJS
define() でモジュールを定義し、require() で読み込む。 エントリーポイントは data-main に指定する。
$ cat src/b.js
require(['a'], (a) => {
const divB = document.createElement('div');
divB.textContent = a.a();
document.body.appendChild(divB);
});
$ cat src/a.js
define({
a: () => 'a is defined'
});
$ cat src/index.html
<body>
<script data-main="b.js" src="require.js"></script>
</body>
CommonJSのモジュールを読み込もうとするとエラーになる。
$ cat src/c.js
const d = require('d');
exports.c = () => {
return d.d();
};
$ cat src/d.js
exports.d = () => 'd is defined';
Uncaught Error: Module name "d" has not been loaded yet for context: _. Use require([])
RequireJSのNode版、r.jsでCommonJSのモジュールをAMDに変換することができる。
$ npm install -g requirejs
$ r.js -convert src out
$ cat c.js
define(function (require, exports, module) {const d = require('d');
exports.c = () => {
return d.d();
};
});
$ cat d.js
define(function (require, exports, module) {exports.d = () => 'd is defined';
また、Webpackのようにコードを一つのjsファイルにbundleすることもできる。
$ r.js -o baseUrl=out name=b out=bundle.js optimize=none
$ cat bundle.js
define('a',{
a: () => 'a is defined'
});
define('d',['require','exports','module'],function (require, exports, module) {exports.d = () => 'd is defined';
});
define('c',['require','exports','module','d'],function (require, exports, module) {const d = require('d');
exports.c = () => {
return d.d();
};
});
require(['a','c'], (a,c) => {
const divB = document.createElement('div');
divB.textContent = a.a();
document.body.appendChild(divB);
const divB2 = document.createElement('div');
divB2.textContent = c.c();
document.body.appendChild(divB2);
});
define("b", function(){});
ちなみに optimize=none を付けているのはES6のコードに対応していないため。
If the source uses ES2015 or later syntax, please pass "optimize: 'none'" to r.js and use an ES2015+ compatible minifier after running r.js. The included UglifyJS only understands ES5 or earlier syntax.
guybedford/require-cssを使うと cssも依存に含めることができ、scriptタグと同様にstyleタグが動的に入る。
Webpack
webpack.config.js の entry にエントリーポイント、output に出力場所、module にJS以外のファイルをbundleするloader、plugins に全体を処理するpluginの設定を書く。
$ yarn add --dev webpack html-webpack-plugin
$ cat webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
const config = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {},
plugins: [
// new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
module.exports = config;
ES Modulesの記法を使っている。CommonJSにも対応しているが今はこちらが推奨。
$ cat src/main.js
import { bar } from './foo';
const div = document.createElement('div');
div.textContent = bar();
document.body.appendChild(div);
$ cat src/foo.js
export function bar() {
return 'bar';
};
実行するとこんな感じにbundleされる。実際はUglifyJsPluginによってもう少しサイズが小さくなる。
$ node_modules/.bin/webpack
$ cat dist/index.html
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<script type="text/javascript" src="bundle.js"></script></body>
</html>
$ z$ cat dist/bundle.js
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
...
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(1);
const div = document.createElement('div');
div.textContent = Object(__WEBPACK_IMPORTED_MODULE_0__foo__["a" /* bar */])();
document.body.appendChild(div);
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = bar;
function bar() {
return 'bar';
};
/***/ })
/******/ ]);
css-loaderでCSSをbundleする。
$ yarn add --dev style-loader css-loader
CSS Moduleを有効にして、ほかの同名のクラスに影響を及ぼさないようにする。
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
}
}
]
}
]
},
importするとCSSに書かれたクラスと変換後の対応が取れる。
$ cat src/main.js
import { bar } from './foo';
import css from './style.css';
const div = document.createElement('div');
div.textContent = bar();
div.className = css['bg']; /* {"bg":"_2T2hBh3FkCro4-BOuqaGg5"} */
document.body.appendChild(div);
$ cat src/style.css
.bg {
background-color: #22ee22;
}
こんな感じでbundleされている。動的にstyleタグが入るのは同じ。
$ cat dist/bundle.js | grep "#22ee22"
exports.push([module.i, "._2T2hBh3FkCro4-BOuqaGg5 {\n background-color: #22ee22;\n}\n", ""]);