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 = "";
图例
最后,我根据个人的理解做了一张图。基于上面的分析过程,如果和上面模式一样,我们可以想象一下,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个人目前能力有限,并没有自主构建一个社区的能力,如有任何问题或想法与我沟通,请通过上述某个平台联系我,谢谢!!!