time-geekbang

模块

★CommonJS 规范

◇为什么要了解它?

关于 Node.js 的模块规范,也就是CommonJS规范

模块规范是做一个大型 Node.js 应用程序的基础,所以这是非常关键的!

◇前端如何加载 JS ?

用script标签

在讲解它之前,先来回顾前端是如何加载 JS 的。

在浏览器里边我们都是用script标签去加载 JS 的

如果script标签里边有个src属性,那么浏览器就会从远端下载个 JS 脚本下来,并且执行它!

反之,咩有这个src属性,那么浏览器就会直接把script里边的代码给执行一遍哈!

而这就是我们以前唯一的一种加载 JS 的方法!

script标签的不足

那么用script标签去加载 JS 会有一个什么样的问题呢?

①当脚本变得很多的时候,我们需要手动去管理这个 JS 的加载顺序。

举个栗子来说,当你的业务代码使用了jQuery这个JS,那么你就需要把jQuery放在第一个script标签上去加载!然后再去运行的你业务脚本,如果就引入一两个库,这样做其实是没啥问题的,但是如果变得越来越多之后呢?如你不只是引入jQuery,还引入Underscore 等其它奇奇怪怪的函数库 ,而这样一来你需要管理的脚本数量就变得非常多了,而这也意味着有很大管理成本

②不同脚本之间,如果产生逻辑调用,那么这是需要一个全局变量作为桥梁去做通信的!

就拿刚才那个jQuery例子来说,jQuery的所有函数都是挂在一个全局变量 $上的,我们每一次调用jQuery都是访问 $变量,然后去找它里边的函数,然后去调用。简而言之,就是说每一个脚本的逻辑输出,都得输出到一个全局变量上才能被其它脚本所调用!如果你的脚本变得非常多,那么你要用的全局变量也就越来越多了!而全局变量多了,那么你的程序就很难管理了!因为你很难确定哪一天自己的全局变量就会被人所覆盖,说白了,某个全局变量被覆盖了,那么你的 Node.js 程序也就GG了。

③当一个 JS 的运行环境它咩有HTML,那该咋办呢?

而这个例子就是 Node.js ,我们运行 Node.js 其实咩有经过任何的HTML文件的,而这就意味着我们是没有地方去写一个script标签了啊!

所以 Node.js 是需要重新去搞一个模块管理的机制,来管理我们 JS 的加载

而这个机制就是 CommonJS 模块规范啦!

CommonJS 其实并不是一个模块规范的名字,它是一个非常庞大的东西,只是现在只有模块规范是留着的,因为它被 Node.js 所采用,并且推广,再加上它非常经典,后续也影响到了浏览器端 JS 的编写方式!

现在前端里边有很多构建工具,比如说webpack,它就是兼容 CommonJS 这种模块规范,可以让你用 CommonJS 规范来写前端代码!

接下来实战一下 CommonJS 规范,以及使用 CommonJS 规范改编、优化之前的小游戏!

◇代码 vs CommonJS

①创建一个入口 JS ,如 index.js

②由于我们是在讲一个模块规范,因此还得再创建一个被依赖的 JS ,如 lib.js,说白了就是其它模块要用到这个 lib.js,而这样才能测试 CommonJS 到底是个什么东东

③ CommonJS 规范是怎样的呢?

有几个关键的变量:

//index.js
require('./lib.js')

当你运行index.js的时候,就会加载lib.js,并且运行它!

验证是否如我们所想的这样:

1570682351508

require这个依赖默认是有一个输出的,在上边的代码里边,我们只在lib.js添加了这样的代码:

console.log('Hello!  CommonJS')

所以它的输出默认就是一个空对象 {}

那么如何指定被引用的这个 JS 的输出呢?即我不想要一个朴素的 {}

一个 JS 文件就是一个模块,lib这个模块里边,就有一个叫 exports变量,需要注意的是,它不是一个全局变量,而是一个不需要声明的关键字变量!(语法就这样,反正node会自己解析)总之,每个模块默认会注入一个 exports变量

接着在exports变量旗下,挂一个 属性:

//libg.js
exports.xxx = 'Hello,World!'

以上就是我们定义一个模块的输出方式

说白了,这可以理解为一个模块就是一个函数,而exports的值就是该函数的返回值哈!

当然除了,挂字符串类型的数据以外,还可以挂其它的,如函数、对象等:

exports.add = function (a, b) {
  return a + b
}
exports.geekbang = {
  hello: 'world'
}

可见,exports是可以挂任何东西的!

总之,exports的值就是一个正常的对象字面量

接下来,分析一下,require('./lib.js')的返回值,和 lib.js 里边的 exports 值是不是同一个引用?

测试这个问题,也很简单:

// lib.js
console.log('Hello!  CommonJS')
exports.xxx = 'Hello,World!'
exports.add = function (a, b) {
  return a + b
}
exports.geekbang = {
  hello: 'world'
}

setTimeout(() => {
  console.log(exports)
}, 2000)

1570694641640

我们希望exports的值不是一个对象引用,而是一个字符串、函数等,那么这该如何实现呢?

这也很简单呀!

如果你这直接这样:exports = 'hi'

那么require的返回值还是一个对象,但如果你这样 module.exports = 'hi',那么它的返回就是字符串 hi

为啥会这样呢?

你可以理解为每个模块文件的第一行都有以下这行代码,只是这是node帮我们追加的罢了:

 var exports = module.exports

也就是说require的返回值是 module.exports 的引用,而不是 exports的值。这个东西你画一下内存图就很好理解了

1570695821055

当你 exports = 'hi' 的时候,module.exports的引用没啥变化,只是你 exports从此就成为了一个非常非常普通的变量,仅此而已!

因此,如果你想要require的返回值,不是默认的那个对象,那你就使用 module.exports = xxx 而不是 exports = xxx。如:

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

setTimeout(() => {
  console.log(exports)
}, 4000)

在这里需要注意的是,lib.js模块的exports的值是不会受到 index.js 模块的干扰的!你可以画个内存图理解一下。简单来说就是exports的值跟 module.exports的值,根本就不是同一个引用哈!那么这是何时改变的呢?

在我们写上module.exports = (a,b) => a-b 这一行代码的这一时刻!

另一种姿势解释 module.exportsexports 的区别:

之前说到,现在前端写 JS 的姿势也可以用 CommonJS 规范来写,即你用 CommonJS 规范写的 JS 代码,可以在浏览器上跑!

那么这是如何做到的呢?——引入webpack这个工具就可以做到!

那么webpack是怎么做到的呢?

把你用 CommonJS 规范写的代码,全都给分析一遍,然后打包成一个可以在浏览器跑的大的 JS ,然后,我们可以从这个打包结果来看出 CommonJS 的整个运行方式是怎样子的!

可以使用yarn全局安装webpack和webpack-cli,也可以本地安装

安装好之后,执行以下命令:

webpack --devtool none --mode development --target node index.js

注:这是全局安装的webpack,如果是局部安装的,你就在该命令的开头加上npx,或者把该命令放到package.json的scripts字段里边去,然后运行脚本

运行结果(这个 main.js 文件可以直接用 node运行 ):

/******/ (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 = "./index.js");
/******/ })
/************************************************************************/
/******/ ({

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

console.log('start require')
let lib = __webpack_require__(/*! ./lib */ "./lib.js")
console.log('end require')

lib.additional = 'test'
console.log(lib)


/***/ }),

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

console.log('我是lib.js模块')

exports.x = '我是x变量的字符串值'

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

setTimeout(() => {
  console.log(exports)
})

/***/ })

/******/ });

折叠代码分析该打包结果的大致结构:

1570704239911

④老师的解析

从中可以简单看出 __webpack_require__的返回值是 module.exports,而不是 exports;模块id即模块的路径 "./lib.js"(这最后应该拼装成绝对路径哈!)

进一步分析大的匿名函数 webpackBootstrap

忽视该匿名函数里边所声明的变量,找到它的return,发现这TM是在调用一个 叫__webpack_require__的函数

一个知识盲点:

let xxx = function(m) {
	console.log(m)
}
let p = {}
xxx(p.x=5) //居然log了一个5,我想这是赋值表达式返回5的结果哈!

总之,这种写法是在个xxx传参的过程中,顺便把一个对象的某个属性赋上了值!

因此,__webpack_require__(moduleId)moduleId的实参是 "./index.js"这个路径哈!

那么下边这行代码的意图就很明显了:

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

modules是传给大匿名函数的那个包含有两个匿名模块的实参对象!

接着就开始执行 index.js 这个模块,指定this的参数 index.js导出的模块,也就是它自己啦!

(function (module, exports, __webpack_require__) {
      console.log(this)
      console.log(module)
      console.log(exports)
      console.log('start require')
      let lib = __webpack_require__(/*! ./lib */ "./lib.js")
      console.log('end require')

      lib.additional = 'test'
      console.log(lib)


      /***/
})

this:{},来自于 module.exports

module:{ i: './index.js', l: false, exports: {} }

exports:{}

执行到 __webpack_require__(/*! ./lib */ "./lib.js")这一步,分析得知,该函数的返回值是 module.exports

所以 exportsmodule.exports的区别就很明显了呀!

其实看 webpack打包后的结果,并不那么好理解,我还是喜欢,在每个模块里边,添加一行 var exports = module.exports; 这种姿势来理解!

通过这个 main.js可以知道 CommonJS 模块规范到底是一个怎样的运行机制,然后webpack本身实现这些逻辑只要了100行代码左右,可见这是非常优秀的!

总之,本节课学习了 CommonJS 的一些机制,下一节课将会用到这节所学的 CommonJS 规范对石头剪刀布游戏进行一个改造哈!

★扩展阅读

➹:前端模块化一——规范详述 - 知乎

➹:CMD 规范是不是就是 commonJS 规范? - 知乎

➹:★最近做个电商项目,问下现在前端js模块化用什么库?seajs和requirejs过时了? - 知乎

➹:AMD / RequireJS 已经死了吗? - 知乎

➹:关于 JS 模块化的最佳实践总结 - 知乎

➹:带我彻底弄懂JavaScript模块化 - 知乎

➹:JS模块化加载之CommonJS、AMD、CMD、ES6 - 知乎

➹:AMD && CMD - 云+社区 - 腾讯云

➹:★JavaScript Modularization Journey

★总结

➹:小邵教你玩转nodejs之剖析/实现commonJs源码(2) - 掘金

★Q&A

① CommonJS ?

CommonJS是一个项目,目标是在Web浏览器之外为JavaScript建立模块生态系统的约定。创建它的主要原因是缺乏普遍接受的的JavaScript脚本模块单元,这些模块单元可以在不同于常规的Web浏览器提供的环境中重用,如运行 JS 脚本的web服务器或者是原生桌面端应用程序。

起初2009年一月份,它叫 ServerJS,在同年的八月份,它才正式叫做 CommonJS

为啥叫 CommonJS 呢?——因为要展示它的APIs有广泛地适用性啊!( Common,普遍的,常见的……)

规范是在一个被大众所认可的开放过程中才被创建的。一个规范的最终诞生,只有在通过多个实现完成之后才被认为是最终规范。

CommonJS不隶属于致力于EcmaScript的ECMA国际集团TC39,但TC39的一些成员参与了该项目

然而在2013年5月,Node.js的包管理器npm的作者Isaac Z. Schlueter说,CommonJS正在被Node.js淘汰,核心Node.js开发人员应该避免使用 CommonJS 规范。

虽然如此,但还是得了解它哈!

Node 应用由模块组成,采用 CommonJS 模块规范。

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。如

// example.js
var x = 5; //私有的
var addX = function (value) {
  return value + x;
};

如果想在多个文件分享变量,必须定义为global对象的属性

global.warning = true; //不推荐

上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的。

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

require方法用于加载模块。

CommonJS模块的特点如下:

module对象:

Node内部提供一个Module构建函数。所有模块都是Module的实例。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
}

总之,每个模块内部,都有一个module对象(你是看不见它的,执行模块时 nod e命令会知道),代表当前模块。它有以下属性。

最后一行输出module变量:

// 01-test.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);
console.log(exports);

module变量就是模块里边的隐式的全局变量啊!

执行这个文件,命令行会输出如下信息:

{
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: 'G:\\git-2019\\geek-nodejs\\02-commonJS\\01-test.js',
  loaded: false,
  children:
    [{
      id:
        'G:\\git-2019\\geek-nodejs\\node_modules\\jquery\\dist\\jquery.js',
      exports: [Function],
      parent: [Circular],
      filename:
        'G:\\git-2019\\geek-nodejs\\node_modules\\jquery\\dist\\jquery.js',
      loaded: true,
      children: [],
      paths: [Array]
    }],
  paths:
    ['G:\\git-2019\\geek-nodejs\\02-commonJS\\node_modules',
      'G:\\git-2019\\geek-nodejs\\node_modules',
      'G:\\git-2019\\node_modules',
      'G:\\node_modules']
}

//{ '$': [Function] }

我没想到module的pasht属性居然是这样获取模块的!即先看当前 JS 文件是否存在node_modules,不存在那就往上一级找!直到找到为止!

如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。如上边的 node 01-test.js01-test.js里边调用了 require('jquery'),那么该 jquery.js模块的父模块就是 01-test.js啦!即 .(Circular)

利用这一点,可以判断当前模块是否为入口脚本。因为入口脚本是咩有 parent的,即为null

if (!module.parent) {
    // ran with `node something.js`
    app.listen(8088, function() {
        console.log('app listening on port 8088');
    })
} else {
    // used with `require('/.something.js')`
    module.exports = app;
}

module.exports属性:

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

exports变量:

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

而这样做所造成的结果是,在对外输出模块接口时,可以向exports对象添加方法

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

注意,不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系。

exports = function(x) {console.log(x)};

上面这样的写法是无效的,因为exports不再指向module.exports

下面的写法也是无效的。

exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';

上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。

这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。

module.exports = function (x){ console.log(x);};

如果你觉得,exportsmodule.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports

分清它们方法很简单,你每次都假想 每个模块头部,有一行这样的命令 :

var exports = module.exports

AMD规范与CommonJS规范区别:

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

说白了就是一个AMD需要发送请求,而 CommonJS 则不需要!而这意味着浏览器端用AMD,而服务器端则用 CommonJS

不过现在,浏览器端用的是ES6模块规范,而不是AMD规范!而且服务端,也有逐步向着ES6模块规范靠近!

那么AMD规范长啥样呢?

AMD规范使用define方法定义模块,下面就是一个例子:

define(['package/lib'], function(lib){
  function foo(){
    lib.log('hello world!');
  }

  return {
    foo: foo
  };
});

当然,AMD规范是允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:

define(function (require, exports, module){
  var someModule = require("someModule");
  var anotherModule = require("anotherModule");

  someModule.doTehAwesome();
  anotherModule.doMoarAwesome();

  exports.asplode = function (){
    someModule.doTehAwesome();
    anotherModule.doMoarAwesome();
  };
});

函数参数里边的函数体代码就是 遵循CommonJS 规范书写的代码呀!

➹:CommonJS - Wikipedia

②require命令?

基本用法:

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

如果模块输出的是一个函数,那就不能定义在exports对象上面,而要定义在module.exports变量上面。


加载规则:

require命令用于加载文件,后缀名默认为.js

即你可以写后缀,也可以不写后缀,但在ES6里边是需要写的!

根据参数的不同格式,require命令去不同路径寻找模块文件。

参数字符串以什么开头?

  1. /:表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js
  2. ./:表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js
  3. 以上二者都不是:表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。

第三点,举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件。

/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

说白了就是,先看node提供的API模块,接着就是从同级的 node_modules下的模块文件开始逐渐上一步台阶往上找:

1570554989414

为啥要这样设计呢?或者说这样设计的目的是啥?

这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。

  1. 不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。

    我的测试 require('03-test/a.js')

    直接去找相对于当前 JS 文件的 node_modules/03-test/a.js

  2. 如果指定的模块文件没有发现,Node会尝试为文件名添加.js.json.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

  3. 如果想得到require命令加载的确切文件名,使用require.resolve()方法。


目录的加载规则:

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。

在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。

// package.json
{ 
  "name" : "some-library",
  "main" : "./lib/some-library.js" 
}

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

测试:

require('jquery')

没有 /./,意味着需要在 Node 里边找,或者在node_modules里边找

  1. Node里边没有找到,那就到node_modules找

  2. 如果一个jquery目录和一个jquery.js同级,那就选择后者

    1570587450429

  3. 如果不存在jquery.js,那么会进到jquery目录里边,然后找 main字段

  4. 如果main字段咩有东西,或者咩有package.json这个文件,那就加载该目录下的index.js或者index.node,如果都没有,那就报找不到该模块的错误!

  5. 当然,你还可以这样: require('jquery/jquery')自行设定要找到模块!

    1570587703536

总之,优先级是jquery文件,其次才是目录!


模块的缓存:

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"

上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。

说白了,在同一个模块里边,require了同一个模块,那么除了第一次require的这个模块,其余的都是一次的 module.exports 结果

当然,如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。

如果是函数,就可以忽视缓存所带来的影响了,毕竟我们要的是一个输入,会有怎样的一个输出!

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

测试cache是个数组还是个对象:

let $ = require('jquery')
console.log(require.cache) //[Object: null prototype]

结果:

{
  'G:\\git-2019\\geek-nodejs\\02-commonJS\\04-module-cache.js':
  {
    id: '.',
    exports: {},
    parent: null,
    filename: 'G:\\git-2019\\geek-nodejs\\02-commonJS\\04-module-cache.js',
    loaded: false,
    children: [[Module]],
    paths:
      ['G:\\git-2019\\geek-nodejs\\02-commonJS\\node_modules',
        'G:\\git-2019\\geek-nodejs\\node_modules',
        'G:\\git-2019\\node_modules',
        'G:\\node_modules']
  },
  'G:\\git-2019\\geek-nodejs\\node_modules\\jquery.js':
  {
    id: 'G:\\git-2019\\geek-nodejs\\node_modules\\jquery.js',
    exports: {},
    parent:
    {
      id: '.',
      exports: {},
      parent: null,
      filename: 'G:\\git-2019\\geek-nodejs\\02-commonJS\\04-module-cache.js',
      loaded: false,
      children: [Array],
      paths: [Array]
    },
    filename: 'G:\\git-2019\\geek-nodejs\\node_modules\\jquery.js',
    loaded: true,
    children: [],
    paths:
      ['G:\\git-2019\\geek-nodejs\\node_modules',
        'G:\\git-2019\\node_modules',
        'G:\\node_modules']
  }
}

cache是个对象,它的key是一个绝对路径,value则是一个模块对象哈!

因此 delete require.cache[moduleName]moduleName就是那个require的参数值,而不是require的返回值!

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块

测试:

let $ = require('./04-cache')
$.name = 'frank'
console.log(require('./04-cache').name) //有缓存,输出frank

delete require.cache[require.resolve('./04-cache.js')]
console.log(require('./04-cache').name) //undefined

注意,一定要有 resolve,不然是无法删除缓存的!

➹:为什么我反对清除node.js中require的cache? - 岳知涵’s Blog

关于对 resolve这个API的认识:

在Node.js中,可以使用require.resolve函数来查询某个模块文件的带有完整绝对路径的文件名:

console.log(require.resolve('./04-cache.js')) //可以不加js后缀

//g:\git-2019\geek-nodejs\02-commonJS\04-cache.js

在这行代码中,我们使用require.resolve函数来查询当前目录下04-cache.js模块文件的带有完整绝对路径的模块文件名。

注意:使用require.resolve函数查询模块文件名时并不会加载该模块。

我之前对resolve的理解是需不需要加后缀呀!没想到这是在拿到某个模块的绝对路径啊!那么这样一来,就很好的解释,为啥删除模块缓存的时候,需要加 resolve了,因为cache对象的键值对就是以 路径:模块名这样存在的啊!只要cache没有某个模块的路径,那么就不会有缓存了!

再次强调绝对路径的概念,要么是url,要么是从盘符开始的路径!

所以,delete require.cache[moduleName];这样的概述是很low的,因为我误以为是 delete require.cache['./04-cache.js']这样


环境变量NODE_PATH:

Node执行一个脚本时,会先查看环境变量NODE_PATH。它是一组以冒号分隔的绝对路径。在其他位置找不到指定模块时,Node会去这些路径查找。

NODE_PATH是历史遗留下来的一个路径解决方案,通常不应该使用,而应该使用node_modules目录机制。

➹:nodejs 中的 NODE_PATH - 东来 - SegmentFault 思否

③模块的加载机制?

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。

下面是一个模块文件05-lib.js

var counter = 3;
function incCounter() {
  counter++;
}
setTimeout(() => {
  console.log(counter) //4
}, 2000)
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter

然后,加载上面的模块。

//main.js
var counter = require('./05-lib').counter;
var incCounter = require('./05-lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

上面代码说明,counter输出以后,05-lib.js模块内部的变化就影响不到counter

我不是很赞同阮一峰这个解释啊!

因为 incCounter这个函数是在 05-lib.js里边声明定义的,而且该函数里边的 counter变量时在定义时确定的,而不是在运行时确定的!

因此,incCounter的执行,只会改变 05-lib.jscounter变量!


require的内部处理流程:

➹:CommonJS规范 – JavaScript 标准参考教程(alpha)

➹:Node.js方式-require()实际上如何工作

④老师的解析:打包后的main.js?

模块代码都被打包在一个对象里边,这个对象的key是脚本的路径,脚本的内容则被打包在一个个匿名函数里里边,为啥要这样做呢?——因为每个函数都是一个独立的作用域哈!而 JS 创建作用域的方式就是通过函数呀!require被webpack编译成了一个叫 __webpack_require__的函数,而 module.exports都是通过函数参数输入进来的!接下来就看看 JS 是如何运行的,说白了就是看看这个大大的匿名函数,你可以把它看做是一个主函数!这个主函数的return部分会调用 ` webpack_require,并把我们 node index.js这个 入口模块index.js`放进去!

重复require会用到缓存,如果没有缓存,那就新生成一个结果!即一个模块 module 哈!而每个模块都会有一个 exports的属性哈!而这也是我们最需要关注的属性!创建好后这个模块,就会执行我们的模块代码:

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

这里的 __webpack_require__实际上就是我们的 require函数呀!这也就是为啥我们在每个模块里边都能用 require的原因所在了

而其它的 modulemodule.exports就是刚刚创建的模块!而这也解释了每个模块为啥有 module这个变量,以及为啥 module.exports的初始值为 {}

ps:我自己忽视了的一个点

1570726506052

我理解错了,index.js这个模块执行完毕后才会让 l(loaded)为true,我尝试这样写:

1570726915426

一个模块里边有异步API,那么这是不会等待异步代码里边的callback执行的,而这意味着模块执行到最后一行代码就加载完毕了,当执行callback的时候,打印该模块的 module值,你会发现其旗下的 l为true哈!

跟之前的理解对上了:

1570728152970

加深理解 exports变量的一如既往:

1570728709598