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
(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
(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
'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とか使ってそうな名前でやるのは避けたほうがよさそうだ。