前端模块化——CommonJS
特点
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载运行,然后运行结果被缓存,以后的加载就直接读取缓存,要想模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
每个文件就是一个模块,有自己的作用域,在一个文件中的变量、函数、类都是私有的对其他文件不可见。
如果多个文件分享变量,必须定义为global
对象的属性。
1 | global.warning = true; |
上面代码的 warning 变量,可以被所有文件读取。当然,这样写法是不推荐的。
Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module
、exports
、require
、global
。
用 module.exports 定义当前模块对外输出的接口(不推荐直接用 exports),用 require 加载模块。
module 对象
Node 内部提供一个 Module 构建函数。所有模块都是 Module 的实例。
每个模块中都会有一个内置对象module
,表示当前模块。
在一个 node 文件中打印 module
1 | Module { |
属性
- id:模块的唯一标示,根文件为.,引入的子模块就是文件的绝对路径
- parent:对象,表示调用该模块的模块
- children:数组,表示该模块调用的其他模块
- filename:文件的绝对路径
- paths:模块的地址,与后面的模块查找机制有关
- loaded:布尔值,表示模块是否已经加载完成
- exports:模块对外输出的值,加载某个模块,就是加载
module.exports
属性
如果使用命令行调用模块,比如node a.js
,那么module.parent
就是 null,如果在其他脚本中调用,比如require('./a.js')
,那么module.parent
就是调用它的模块。利用这一点可以判断当前模块是否为入口脚本。
1 | if (!module.parent) { |
exports 变量
Node 为每个模块提供一个 exports 变量,指向 module.exports。这等同在每个模块头部,有一行这样的命令。
1 | var exports=module.exports |
可以为 exports 添加属性或方法
1 | exports.area = function (r) { |
不能为 exports 指向另一个值,这样 exports 不再与 module.exports 指向同一地址,同时 exports 也不会对外输出。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports
输出,只能使用 module.exports
输出。
1 | module.exports = function (x){ console.log(x);}; |
如果你觉得,exports
与module.exports
之间的区别很难分清,一个简单的处理方法,就是放弃使用exports
,只使用module.exports
。
require 命令
Node 使用 CommonJS 模块规范,内置的 require 命令用于加载模块文件。
功能:读取 JS 文件,然后返回该模块的module.exports
对象,如果没有发现指定模块。会报错。
加载规则
- 默认加载
.js
文件 - 参数
- 如果参数字符串以’/‘开头,表示一个绝对路径
- 如果参数字符串以’./‘开头,表示一个相对路径
- 如果参数字符串不以’/‘或’./‘开头,表示加载的是一个默认提供的核心模块(Node 的核心模块或 node_module 目录已安装的模块)。
- 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如 require(‘example-module/path/to/file’),则将先找到 example-module 的位置,然后再以它为参数,找到后续路径。
- 如果指定的模块文件没有发现,Node 会尝试为文件名添加.js、.json、.node 后,再去搜索。.js 件会以文本格式的 JavaScript 脚本文件解析,.json 文件会以 JSON 格式的文本文件解析,.node 文件会以编译后的二进制文件解析。
- 如果想得到 require 命令加载的确切文件名,使用 require.resolve()方法。
目录的加载规则
通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让 require 方法可以通过这个入口文件,加载整个目录。
例如,在目录中放置一个 package.json 文件,并且将入口文件写入 main 字段。
1 | // package.json |
require 发现参数字符串指向一个目录以后,会自动查看该目录的 package.json 文件,然后加载 main 字段指定的入口文件。如果 package.json 文件没有 main 字段,或者根本就没有 package.json 文件,则会加载该目录下的 index.js 文件或 index.node 文件。
模块的缓存
第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性。
1 | require("./test.js"); |
为模块添加的 message 依然存在,所以模块并没有重新加载。
所有缓存的模块保存在 require.cache 之中,如果想删除模块的缓存,可以像下面这样写。
1 | //删除指定模块的缓存 |
moduleName:”/Users/home/html5/node/test.js”
注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require 命令还是会重新加载该模块。
require.main
用来判断模块是直接执行,还是被调用执行。
直接执行的时候(node module.js),require.main 属性指向模块本身。
1 | require.main===module;//true |
调用执行的时候(通过 require 加载该脚本执行),上面的表达式返回 false。
模块的加载机制
输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
1 | //test.js |
上面代码说明,counter 输出以后,test.js 模块内部的变化就影响不到 counter 了。
require 的内部处理流程require
不是全局命令,而是指向当前模块的module.require
命令,而后者又调用 Node 的内部命令Module._load
。
1 | Module._load = function(request, parent, isMain) { |
上面的第4步,采用module.compile(),执行指定模块的脚本,逻辑如下。
1 | Module.prototype._compile = function(content, filename) { |
第 1 步和第 2 步,require 函数及其辅助方法主要如下。
- require(): 加载外部模块
- require.resolve():将模块名解析到一个绝对路径
- require.main:指向主模块
- require.cache:指向所有缓存的模块
- require.extensions:根据文件的后缀名,调用不同的执行函数
一旦 require 函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括 require、module、exports,以及其他一些参数。
1 | (function (exports, require, module, __filename, __dirname) { |
Module._compile 方法是同步执行的,所以 Module._load 要等它执行完成,才会向用户返回 module.exports 的值。
CommonJS 模块的循环加载
CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
1 | //a.js |
上面代码之中,a.js
脚本先输出一个 done 变量,然后加载另一个脚本文件b.js
。注意,此时a.js
代码就停在这里,等待b.js
执行完毕,再往下执行。
1 | //b.js |
上面代码之中,b.js 执行到第二行,就会去加载 a.js,这时,就发生了“循环加载”。系统会去 a.js 模块对应对象的 exports 属性取值,可是因为 a.js 还没有执行完,从 exports 属性只能取回已经执行的部分,而不是最后的值。
a.js 已经执行的部分,只有一行。exports.done = false;
因此,对于 b.js 来说,它从 a.js 只输入一个变量 done,值为 false。
然后,b.js 接着往下执行,等到全部执行完毕,再把执行权交还给 a.js。于是,a.js 接着往下执行,直到执行完毕。
1 | //main.js |
执行 main.js,运行结果如下。
1 | $ node main.js |
上面的代码证明了两件事。一是,在 b.js 之中,a.js 没有执行完毕,只执行了第一行。二是,main.js 执行到第二行时,不会再次执行 b.js,而是输出缓存的 b.js 的执行结果,即它的第四行exports.done = true;
。