javascriptのバンドルツールで詰まった

フロントエンドを簡単に作成しようとフレームワーク無しで簡単なサイト構築をしていたら、
js のバンドルで詰まったのでメモ。

目次

何で詰まったか

例えば以下のようにボタンに onclick に関数を割り当てた html ファイルを作成したとする。

index.html
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<!--省略-->
<script type="text/javascript" src="app.js"></script>
</head>
<body>
<!--省略-->
<button id="test" onclick="test()">TEST</button>
<!--省略-->
</body>
</html>

この時onclickに割り当てたtest()が src.js に定義されている

src.js
1
2
3
4
5
6
7
8
9
"use strict";

const Lib = require("./lib.js");
let lib = new Lib();

function test() {
console.log(`test`);
lib.func();
}

このsrc.jsは、Webpack + babel で app.js にバンドルする。

実行

動かない!

コンソールを見たら
「Uncaught ReferenceError: test is not defined」

えっ、なんで?!!


というわけで調べた

webpack + babel でバンドルされた app.js の中を見ると、確かにtest()の定義自体はある。
しかし、test()の定義が、即時関数に包まれて evel()で実行される状態に

app.js(webpack + babel)
1
2
3
4
5
6
7
8
9
(function (modules) {
// webpackBootstrap
/*省略*/
eval(
'\n\nvar Lib = __webpack_require__(/*! ./lib.js */ "./src/lib.js");\n\nvar lib = new Lib();\n\nfunction test() {\n console.log("test");\n lib.func();\n}\n\n//# sourceURL=webpack:///./src/index.js?'
);

/*省略*/
});

Browserify でバンドルした app.js だと次のようになる。
こっちも即時関数に包まれる

app.js(Browserify)
1
2
3
4
5
6
7
8
9
10
(function () {
/*省略*/

function test() {
console.log(`test`)
lib.func()
}
/*省略*/
);

この点について説明されているサイトが有った。
ブックマークし忘れて、見つけられなくなってしまった。

この状態だと、グローバル空間に関数の定義が無いので html 側で、関数の割り当てしても見つからないそうだ。
グローバル空間に関数が露出しない(この表現でいいのか?)らしい。

実際、html で src.js を参照させて実行すると関数自体は見つかる。(もちろんそのままではエラーだけど)

こういう場合以下のように変更する必要があった。

index.html
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<!--省略-->
</head>
<body>
<!--省略-->
<button id="test">TEST</button>
<!--省略-->
<script type="text/javascript" src="app.js"></script>
</body>
</html>
src.js
1
2
3
4
5
6
7
8
9
10
11
"use strict";

const Lib = require("./lib.js");
let lib = new Lib();

function test() {
console.log(`test`);
lib.func();
}

document.getElementById("test").onclick = test;

javascript の処理として、イベント割り当てをすればうまくいった。
この場合は、変換後に即時関数に包まれていても大丈夫。

それでも html 側で関数割り当てをしようと思ったら

バンドルをやめる方法しか見つからなかった。
もう一件方法を見つけたのでそれは後述。

今回やりたかったのは、class とかアロー関数とかの新しい構文の利用だったから、 babel だけ使うことにした。
バンドルをやめたので、別ファイルに書いてインポートしていたクラス定義も src.js に書くことになった。

src.js は以下のように書き変えた。

src.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use strict";

class Lib {
constructor() {}
func() {
console.log("Lib.js");
}
}
let lib = new Lib();

function test() {
console.log(`test`);
lib.func();
}

これが、babel で変換すると次のようになった。

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"use strict";

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}

function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}

var Lib =
/*#__PURE__*/
(function () {
function Lib() {
_classCallCheck(this, Lib);
}

_createClass(Lib, [
{
key: "func",
value: function func() {
console.log("Lib.js");
},
},
]);

return Lib;
})();

var lib = new Lib();

function test() {
console.log("test");
lib.func();
}

この babel で変換しただけの app.js を変更前の index.html で読み込むと、
ボタンを押して関数test()が実行できる。

今回の件で js のバンドルについて自身の理解が足りていないことに気がつけた。
vue-cli での webpack 利用とか、使ってる気で使われているなと・・・。
反省

今回 webpack もしくは Browserify でのバンドルをやめるという方法しか取れなかったけど、
他に方法がないのが気になった。

2/10 追記 もう一つ方法を見つけた

LCL Engineers’ Blog - ES5 から Webpack 管理下の関数を呼び出す方法
に記載があった。

グローバル空間に関数登録をしてしまう方法になる。

以下のように作った関数 test()を、window.test = testでグローバル空間に関連付けする。

src.js
1
2
3
4
5
6
7
8
9
10
11
"use strict";

const Lib = require("./lib.js");
let lib = new Lib();

function test() {
console.log(`test`);
lib.func();
}

window.test = test;

この src.js を webpack + babel で処理した app.js なら、
以下の html 側でイベントへの関数割り当てした場合も動作する。

index.html
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<!--省略-->
<script type="text/javascript" src="app.js"></script>
</head>
<body>
<!--省略-->
<button id="test" onclick="test()">TEST</button>
<!--省略-->
</body>
</html>

LCL Engineers’ Blog - ES5 から Webpack 管理下の関数を呼び出す方法
にも

グローバルに露出させるのであまりよくないかもしれませんが

とあるけど変数・関数の名前がぶつかる危険性など考えると、
安全性が担保できないのだろうなと。
少なくとも test とか使ってそうな名前でやるのは避けたほうがよさそうだ。