欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

从javascript的角度简单理解nodejs中commonjs模块规范的导入导出

程序员文章站 2022-04-25 14:31:14
...

foreword(前言)

最近在看极客时间的“nodejs开发实战”,其中有个nodejs中commonjs模块规范的例子,例子中可以得出的结论是如果以module.exports作为导出,那么其优先级是最高的。

我对此非常好奇,它的运行机制是怎样的?

作者在最后给出了一个小引导,通过webpack将nodejs代码打包成js代码,并以js的角度来作为一个窗口。

所以,本篇文章,我想要做的是将这打包好的js代码进行拆解并逐个分析,以此大概理解它背后的运行模式。

example(案例)

首先,我们给出nodejs的代码案例:

/commonjs/lib.js:

exports.hello = 'world'

module.exports = function minus (a, b) {
  return a - b
}

exports.add = function (a, b) {
  return a + b
}

/commonjs/index.js:

var lib = require('./lib.js')

console.log(lib)

我们通过node commonjs/index.js,终端输出minus这个函数。

然后我们再执行脚本:webpack --devtool none --mode=development --target node commonjs/index.js

会在我们的根目录下生成一个dist文件夹,并在里面生成一个main.js文件:

main.js:

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./commonjs/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./commonjs/index.js":
/*!***************************!*\
  !*** ./commonjs/index.js ***!
  \***************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

var lib = __webpack_require__(/*! ./lib.js */ "./commonjs/lib.js")

console.log(lib)



/***/ }),

/***/ "./commonjs/lib.js":
/*!*************************!*\
  !*** ./commonjs/lib.js ***!
  \*************************/
/*! no static exports found */
/***/ (function(module, exports) {

exports.hello = 'world'

module.exports = function minus (a, b) {
  return a - b
}

exports.add = function (a, b) {
  return a + b
}


/***/ })

/******/ });

总共也就124行的代码(注释和空行很多,应该不到100行代码,或者只有五六七八十行),读完它不需要花费很长时间,但我们得理解它。

module split(模块拆分)

整段代码是一个匿名函数的自调,所以我们首先将代码分成两部分:函数体参数

参数

我们首先从参数开始看,主要的逻辑在函数体中,参数相对于比较简单:

{
  "./commonjs/index.js": (function (module, exports, __webpack_require__) {
      var lib = __webpack_require__("./commonjs/lib.js")
      console.log(lib)
  }),
  "./commonjs/lib.js": (function (module, exports) {
      exports.hello = 'world'
      module.exports = function minus(a, b) {
          return a - b
      }
      exports.add = function (a, b) {
          return a + b
      }
  })
}

我将参数中多余的注释去掉,即上述代码。这是一个对象,对象中有两个函数类型的属性,很容易我们可以看出代表的就是我们在打包之前的两个脚本文件——commonjs/index.js和commonjs/lib.js,两个函数的参数结构基本一样,前两个参数为module和exports,index.js中因为引入了lib.js,所以我们需要额外的一个require参数(代码中为__webpack_require__),接着函数体内就是我们之前两个脚本文件中的完全一样的脚本。

函数体

var a = function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {

      // Check if module is in cache
      if (installedModules[moduleId]) {
          return installedModules[moduleId].exports;
      }
      // Create a new module (and put it into the cache)
      var module = installedModules[moduleId] = {
          i: moduleId,
          l: false,
          exports: {}
      };

      // Execute the module function
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

      // Flag the module as loaded
      module.l = true;

      // Return the exports of the module
      return module.exports;
  }


  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  __webpack_require__.d = function (exports, name, getter) {
      if (!__webpack_require__.o(exports, name)) {
          Object.defineProperty(exports, name, {
              enumerable: true,
              get: getter
          });
      }
  };

  // define __esModule on exports
  __webpack_require__.r = function (exports) {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
          Object.defineProperty(exports, Symbol.toStringTag, {
              value: 'Module'
          });
      }
      Object.defineProperty(exports, '__esModule', {
          value: true
      });
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
      if (mode & 1) value = __webpack_require__(value);
      if (mode & 8) return value;
      if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
      var ns = Object.create(null);
      __webpack_require__.r(ns);
      Object.defineProperty(ns, 'default', {
          enumerable: true,
          value: value
      });
      if (mode & 2 && typeof value != 'string')
          for (var key in value) __webpack_require__.d(ns, key, function (key) {
              return value[key];
          }.bind(null, key));
      return ns;
  };

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function (module) {
      var getter = module && module.__esModule ?
          function getDefault() {
              return module['default'];
          } :
          function getModuleExports() {
              return module;
          };
      __webpack_require__.d(getter, 'a', getter);
      return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function (object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
  };

  // __webpack_public_path__
  __webpack_require__.p = "";


  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./commonjs/index.js");
}

这一块,我们可以将整体逻辑分成三块:__webpack_require__函数__webpack_require__函数额外的属性函数的return

1.__webpack_require__函数

我们主要关注这几句代码:

  • 这是一个模块的数据结构:
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {}
};

  • 结合一开始匿名函数的参数结构 ——(module, exports, require),exports是module的子集对象,如果给module.exports赋予一个新的对象,exports已经被抛弃,所以从这里,我们知道了为什么如果我们用到了module.exports,那么module.export会将exports覆盖了。
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  • 这一句,我们关注的是最后的return,输出的是module.exports,这其实在前面两段代码中我们就可以预料到了。
// Flag the module as loaded
module.l = true;

// Return the exports of the module
return module.exports;

我们再接着看。

2.函数的return

其实整段代码的执行就只有最后大那一句return,它只调用了__webpack_require__,所以我们只要看这一句就行了。

它传了一个路径字符串,刚好是匿名函数参数中index.js脚本函数对应的属性名,根据__webpack_require__函数的声明,首先会创建一个module对象,然后执行以下语句:

var lib = __webpack_require__("./commonjs/lib.js")
console.log(lib)

其中的__webpack_require__("./commonjs/lib.js")也会创建一个module对象,并通过lib.js中的脚本给module进行操作,然后将module.exports返回给上面代码的lib。

到这里,模块的导入导出运行机制我们已经有了稍微清晰的轮廓,按照这样的方式,对于模块间的导出引入,基本上已经满足了我们的需求。

3.__webpack_require__函数额外的属性

这部分代码和本次demo其实没有任何关系,不过出于一探究竟的心态,还是阅读了一下。

这两句分别用一个属性暴露了modules(匿名函数中传的参,我这里姑且称他为模块队列好了)和 installedModules(这个是代码开头声明的一个用来缓存模块数据的变量,主要由一个模块标示moduleId和导出的数据组成)

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules; 

// expose the module cache
__webpack_require__.c = installedModules;

下面这个函数是一个工具函数,用于给某个对象的某个属性添加getter。

// define getter function for harmony exports
  __webpack_require__.d = function (exports, name, getter) {
      if (!__webpack_require__.o(exports, name)) {
          Object.defineProperty(exports, name, {
              enumerable: true,
              get: getter
          });
      }
  };

下面这句用来在导出对象中设置esModule标示,具体怎么用,由于本次demo中没有用到,不清楚。(大概的思考方向觉得应该是在脚本中用了EsModule中的export时会用到这个方法,由于时间问题,本次文章就不再继续进行验证了)

// define __esModule on exports
  __webpack_require__.r = function (exports) {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
          Object.defineProperty(exports, Symbol.toStringTag, {
              value: 'Module'
          });
      }
      Object.defineProperty(exports, '__esModule', {
          value: true
      });
  };

下面这句个人猜测是声明一个基于EsModule的导出模块数据结构。

  // create a fake namespace object(声明一个假的命名空间对象)
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
      if (mode & 1) value = __webpack_require__(value);
      if (mode & 8) return value;
      if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
      var ns = Object.create(null);
      __webpack_require__.r(ns);
      Object.defineProperty(ns, 'default', {
          enumerable: true,
          value: value // module.exports
      });
      if (mode & 2 && typeof value != 'string')
          for (var key in value) __webpack_require__.d(ns, key, function (key) {
              return value[key];
          }.bind(null, key));
      return ns;
  };

这句由于看到了module.__esModule和return module[‘default’],大致可以判定是根据模块规范类型,选择不同的获取导出模块数据的方法,其中的getDefault大概用于获取EsModule中的export default导出的数据。

// getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function (module) {
      var getter = module && module.__esModule ?
          function getDefault() {
              return module['default'];
          } :
          function getModuleExports() {
              return module;
          };
      __webpack_require__.d(getter, 'a', getter);
      return getter;
  };

判断对象中是否包含某个属性。

// Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function (object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
};

这是个路径,具体这个public path代表着什么不清楚。

// __webpack_public_path__
  __webpack_require__.p = "";

图例

从javascript的角度简单理解nodejs中commonjs模块规范的导入导出
最后,我根据个人的理解做了一张图。基于上面的分析过程,如果和上面模式一样,我们可以想象一下,nodejs在处理的时候或许首先有个脚本去获取一个模块集ModuleSet(这个模块集是基于入口脚本开始,所有有关联的模块的集合),我在图中用Input来表示,然后Input脚本再将这个ModuleSet传给一个脚本Script,我们假设它是一个函数,函数中通过require方法从入口点开始不断扩展。

last(最后)
非常感谢您能阅读完这篇文章,您的阅读是我不断前进的动力。

对于上面所述,有什么新的观点或发现有什么错误,希望您能指出。

最后,附上个人常逛的社交平台:
知乎:https://www.zhihu.com/people/bi-an-yao-91/activities
csdn:https://blog.csdn.net/YaoDeBiAn
github: https://github.com/yaodebian

个人目前能力有限,并没有自主构建一个社区的能力,如有任何问题或想法与我沟通,请通过上述某个平台联系我,谢谢!!!