JavaScriptのrequire/import

(2017-11-11)

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.jsentryにエントリーポイント、 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", ""]);