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

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

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

上一篇:nodejs基础-2


下一篇:def files ---EXPORTS