豆腐とコンソメ

豆腐とコンソメ

もろもろのプログラム勉強記録

webpackのメモ

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.jscodeなんだけど、もともとの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を実装してた

以下は、最終成果物を見やすいように整理したもの。

バンドルされたモジュールの情報が吐かれる。 さきほどのrequrieexportが関数の引数としてもらうようになってる。

// モジュール情報
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: //割愛

こちらがrequrieexportの実装。

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`
}),
}