webpackメモ
まとめ
- webpackはESM形式をCJS形式にして、require/exportsはwebpackが実装する。
- webpackはバンドル対象がCJS形式でも問題ない。
minipack
minipackをもとに、実装を確認する。
大事な点
- minipackはESModuleをCommonJS形式にトランスパイルしてバンドルする
- ブラウザ環境で解釈できない、require/exportsはminipackが実装してる。
実装
エントリーとなるentry.js
をパースしてimport
を拾う。
そこからimport
するファイルもパースして依存関係を記録して〜と繰り返し、最終的に依存関係グラフを作成する。
// 結果以下のような情報に整理される
const graph = [
{
id: 0, // moduleを識別するためのID
filename: './example/entry.js', // module名
dependencies: [ './message.js' ], // entry.jsはmessage.jsをimportしてる
code: '' // codeは後述
},
{
id: 1,
filename: 'example/message.js',
dependencies: [ './name.js' ],
code: '' // 割愛
},
{
id: 2,
filename: 'example/name.js',
dependencies: [ './name.js' ],
code: '' // 割愛
},
]
ここで興味深いのが、割愛したcode
の中身。
以下は、entry.js
のcode
なんだけど、もともとのentry.js
の内容とは異なる。
というのも、冒頭のパース処理は、パースするだけではなくbabelをつかって、transformも行っている。
babelの結果
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_message.default);
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _name = require("./name.js");
var _default = "hello ".concat(_name.name, "!");
exports.default = _default;
transform後のコードで気になるのが、require
,exports
。
これはCommonJS(cjs)のモジュール形式なので、ブラウザのjsエンジンでは実行できない。
ではどうするのかというと、minipackがrequire
, exports
を実装してた
以下は、最終成果物を見やすいように整理したもの。
バンドルされたモジュールの情報が吐かれる。
さきほどのrequrie
、export
が関数の引数としてもらうようになってる。
// モジュール情報
const modules = {
// モジュールid:0 = entry.jsの情報
0: [
// babelでcommonjsにtransformされたcode
function(require, module, exports) {
const _message = require('./message.js');
const _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {default: obj};
}
console.log(_message2.default);
},
// 依存するモジュール名 + id
{'./message.js': 1},
],
// モジュールid:1 = message.jsの情報
1: [
// babelでcommonjsにtransformされたcode
function(require, module, exports) {
Object.defineProperty(exports, '__esModule', {
value: true,
});
const _name = require('./name.js');
exports.default = `hello ${_name.name}!`;
},
// 依存するモジュール名 + id
{'./name.js': 2},
],
2: //割愛
こちらがrequrie
、export
の実装。
function main(modules) {
function require(id) {
// 3. モジュールid:0の関数本体(code)と依存してるモジュール情報を取得
const [fn, mapping] = modules[id];
// 5. 関数本体のrequireから呼ばれる
function localRequire(name) {
// 関数本体でrequire('./message.js')とした場合
// ここでは、require(1)となる。
return require(mapping[name]);
}
const module = {exports: {}};
// 4. 関数本体を実行
// 関数本体のrequireには、localRequrie関数を渡す
// 関数の実行結果は、module.exportsに格納される。
fn(localRequire, module, module.exports);
// 6. 実行結果を返却
return module.exports;
}
// 2. モジュールid:0 = entry.jsをrequireする
require(0);
}
// 1. モジュール情報を渡して、実行
main(modules)
項番降ったはいいけど、とても難しい。
webpack
webpack5で検証。
流れは、minipackと同じようなものなってることが確認できた。
ただ、関数本体のトランスパイル後のコードがminipackと異なる。
__webpack_require__
含め、誰がトランスパイル後してるんだろう。babelのオプションなのか、webpackがやってるのかはわからなかった。
var __webpack_modules__ = ({
"./example/message.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
var _name_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./name.js */ "./example/name.js");
const __WEBPACK_DEFAULT_EXPORT__ = (`hello ${_name_js__WEBPACK_IMPORTED_MODULE_0__.name}!`);
}),
"./example/name.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"name": () => (/* binding */ name)
});
const name = 'world';
})
});
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
webpack追い焚き
ここまでの理解。
webpackはESM形式をCJS形式にして、require/exportsはwebpackが実装する。
最初からCJS形式の場合はどうなんだっけ。
module.exports = 'cjs child'
const cjsChildText = require('./cjsChild')
module.exports = `require ${cjsChildText} and export`
import message from './message.js';
// 追加
import text from './cjs'
console.log(message);
// 追加
console.log(text)
これは問題なくいけた。
トランスパイル後の本体のコードはこんな感じ。
誰がトランスパイルしてるのが問題はさておき、esm形式よりシンプルになってることがわかった。
{
"./example/cjsChild.js":
((module) => {
module.exports = 'cjs child'
}),
"./example/cjsParent.js":
((module, __unused_webpack_exports, __webpack_require__) => {
const cjsChildText = __webpack_require__(/*! ./cjsChild */ "./example/cjsChild.js")
module.exports = `require ${cjsChildText} and export`
}),
}