October 24, 2023

Как сборщики взаимодействуют с модулями в Node.js

На примере WebPack’а. Интересно как webpack резолвит ESM модули

Бандл из входной точки cjs, которая импортирует esm модуль.

Сперва обратим своё внимание на IIFE(самовызывающаяся анонимная функция)

💡 Почему это было сделано? Так как это бандл, то означает, что вся кодовая база, в виде совокупности отдельных функциональных модулей, сосредоточенная в одном файле, то вспомогательные функции, переменные, такие как, например, webpack__require, должны быть изолированные от других файлов в общей среде под названием браузер. Коротко говоря, всё, что внутри функции, находиться в области видимости только этой функции, извне не получиться ни к чему получить доступ.

var __webpack_modules__ = {
    123: (
      __unused_webpack___webpack_module__,
      __webpack_exports__,
      __webpack_require__
    ) => {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__,
        substract: () => /* binding */ substract,
      });
      function substract(a, b) {
        return a + b;
      }

      const __WEBPACK_DEFAULT_EXPORT__ = substract.bind(null, 2);
    },
  };

💡 Далее webpack генерирует объект, где ключи - id модуля, а значение - функция, аргументы которой следующие: __unused_webpack__webpack_module__ (это объект модуля), __webpack__exports (это объект exports), __webpack__require__ (вспомогательная функция для загрузки содержимого модуля)

❓ Но что же такое **webpack_require**.r, **webpack_require**.d, **__webpack_require__**.o ?

☝🏻 Сперва посмотрим на последнюю часть это большой функции IIFE. Тут мы можем заметить содержимое entry файла (index.cjs). Вместо require(’./modules.mjs’) используется __webpack__require(123) , где 123 - id модуля, который webpack сгенерировал. И тут мы обращаемся к следующему куску кода - не посредственно к самому function __webpack__require(moduleId)

// The require function
function __webpack_require__(moduleId) {
  // Проверяет, есть ли модуль в кеше
  var cachedModule = __webpack_module_cache__[moduleId];
  // Если есть, возвращаем из кеша полученный объект module, а у него - объект exports
	if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  // Если нет в кеше, создаем объект module со свойством exports и записываем в кеш
  var module = (__webpack_module_cache__[moduleId] = {
    exports: {},
  });

  // Выполняем функцию модуля из **__webpack_modules__**
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  // Возвращаем объект exports полученного модуля
  return module.exports;
}

☝🏻 А теперь начнём по порядку: webpack_require.r

/* webpack/runtime/make namespace object */
  (() => {
    // Определяем признак через свойство __esModule для exports-объекта модуля
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })();

💡 Symbol.toStringTag - это символ для геттера объекта, который возвращает всем известную нам строку, когда мы пытаемся привести объекта в строку посредством toString().

То есть вот это создаст для объекта exports свойство в виде символа Symbol.toStringTag и задаст значение value = “Module”. Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); Тогда получим такое поведение данного выражения: Object.prototype.toString.call(exports) // “[object Module]”

Если в среде нет поддержки этого символа, тогда для объекта назначается свойство __esModule: true , это нужно для определения того, что экспортируемый модуль является ESM.

☝🏻 А теперь давайте про webpack_require.o

/* webpack/runtime/hasOwnProperty сокращенная версия Object.prototype.hasOwnProperty(obj, prop) */
(() => {
  __webpack_require__.o = (obj, prop) =>
		// Проверяет, является ли prop собственным свойством объекта obj, вот и всё
    Object.prototype.hasOwnProperty.call(obj, prop);
})();

😈 Ну и самое тяжелое webpack_require.d

/* webpack/runtime/define property getters */
(() => {
  // define getter functions for harmony exports
  __webpack_require__.d = (exports, definition) => {
    for (var key in definition) {
      if (
        __webpack_require__.o(definition, key) &&
        !__webpack_require__.o(exports, key)
      ) {
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };
})();

💡 Что бы хорошо понять, давайте подготовим контекст. Посмотри ещё раз как вызывается эта функция

__webpack_require__.d(__webpack_exports__, {
    default: () => __WEBPACK_DEFAULT_EXPORT__,
    substract: () => /* binding */ substract,
  });
//                             вот это **__webpack__exports__**
//                                        \\/
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

👉🏻 Получается что exports параметр - webpack__exports, который в свою очередь modules.exports из webpack__require функции:

__webpack_require__.d(__webpack_exports__, {
    default: () => __WEBPACK_DEFAULT_EXPORT__,
    substract: () => /* binding */ substract,
  });

👉🏻 Вернемся обратно к функции d. Мы поняли, что такое webpack__exports, а вот definition - объект с содержимым модуля, который webpack распарсил при загрузке модуля module.mjs { default: () => __WEBPACK_DEFAULT_EXPORT__, substract: () => /* binding */ substract, }

/* webpack/runtime/define property getters */
(() => {
  // Определяем геттеры для объекта exports
  __webpack_require__.d = (exports, definition) => {
		// Проходимся по ключам объекта definition
    for (var key in definition) {
      // если ключ есть в объекте definition и если его нет в exports, а его там нет
			if (
        __webpack_require__.o(definition, key) &&
        !__webpack_require__.o(exports, key)
      ) {
				// то определяем для объекта exports геттер key(default | substract),
				// как перечисляемое и возврающее значение по ключу key из definition
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };
})();