承接上文:前端工程化-模块化-初识模块化
commonJS 是由 Node实现的。且Node中也是靠CommonJS实现的各种依赖引用,是Node中极为重要的一个基础能力。
在commonJS中,模块是以文件为单位出现的。文件内的作用域是独立的,其中的各种变量也是私有的。
接下来先看看 commonJS 是怎么使用的,搞清楚使用方式,我们再探究原理。
模块暴露 module.exports
语法上,通过module.exports来实现模块暴露。
// 一个模块文件
var count = 1
function countAdd () {
count++
}
module.exports = { count, countAdd }
一般来说,module.exports 暴露一个对象,对象内的属性可以是模块的各种变量及方法。当然,想仅仅暴露一个基础类型值也没问题,很灵活,比如:
// 一个模块文件
var count = 1
module.exports = count
还有一种简写方式,即直接用 exports对象 来暴露,代码如下:
// 一个模块文件
var count = 1
function countAdd () {
count++
}
exports.count = count
exports.countAdd = countAdd
// 上面2行代码相当于
module.exports = { count, countAdd }
这里exports实际上是module.exports的一个引用,只能在其上添加属性,如果直接给exports赋值,那就不行了,会切断 exports与module.exports之间的联系。所以,简单的方式就是:
不要用exports!!!
接下来看看,模块的引入。
模块引入 require
语法上,commonJS通过require方法,引入模块文件。
require方法的基本功能是,读入并执行一个JavaScript文件,然后返回该文件暴露的模块的exports对象。
// 一个模块文件 module.js
var count = 1
function countAdd () {
count++
}
module.exports = { count, countAdd }
// 引用模块文件
var { count } = require('./module.js')
console.log(count) // 1
require方法接收的参数字符串稍微有点讲究,其实也就是node的文件索引方式。
可以接收一个文件路径,相对的或者绝对的,也可以只写一个字符串,只写一个字符串的话,会广度优先遍历 node_modules 文件夹中的依赖,找到后引用进来。
以上,就是commonJS的基础用法。接下来,说一下commonJS的特点:
commonJS的特点
- commonJS的模块引用是同步的,即代码执行到require方法时,才会进行引用
- 引用模块,即引用文件,文件中的所有代码,都会执行
- 如果在一个文件内重复引用同一个模块,该模块只会被引用1次,也就是执行1次
- 因其同步引用执行属性,该规范,仅适合用在Node场景下,在浏览器环境中,同步获取很影响加载性能(本质上浏览器也不支持commonJS语法)
commonJS原理
这部分内容比较硬核,了解了之后,以上所有所谓暴露方式、引用方式以及特点就都能够理解和记忆了。
首先我们先明确一下,commonJS的模块化语法,也是JS语法,没那么高深。
在Node环境下,每个文件,都会被内置一个module对象,这个module对象,也就是我们在模块暴露过程中 module.exports 的module对象,其实也是 Module构造函数的一个实例,而Module构造函数,来自于Node内部的 module模块。
上面这段“绕口令”,我们以代码的形式来展现:
// 接下来的代码,请配合注释逐行阅读
var ModuleSource = require('module') // module是Node的内置模块,提供模块化的能力,且往下看
// 我们看看ModuleSource里面都有什么
console.log(ModuleSource)
// 会打印出以下内容(格式做了些调整,保证内容更可读)
{
Module: function () {...}, // Module是用来实例化module对象的构造函数
_cache: { // 全局模块缓存对象,非常关键,且看里面有啥
'/Users/didi/practice/modules/commonJS/index.js': { // 我们看到_cache中,是以文件名为key,Module实例为value的一个对象,其中记录了,当前执行环境下,所有的模块信息
{
id: '.',
exports: {}, // 这个即为当前文件暴露出的模块内容,因为我没有在当前文件使用module.exports,所以这里exports是空对象
parent: null, // 用于记录依赖该模块的模块
filename: '/Users/didi/practice/modules/commonJS/index.js',
loaded: false,
children: [...], // 用于记录模块依赖的模块
paths: [...] }
}
},
_load: function () {...}, // node内置load方法,下面会提到
... // 剩下还有一大堆属性,和我们要讨论的内容无关,不去关注
}
了解了这个内置模块 module。接下来我们看看 module.export 和 require 背后都做了什么。
按执行顺序,我们先从模块引用开始
// index.js
var { a } = require('./a.js')
上面这段代码,实际上在node中,会按以下步骤执行:
在当前文件,调用require方法相当于,调用node内置对象module下的_load方法,_load方法逻辑如下:
- 检查 module._cache中 有没有当前引用的模块
- 如果缓存之中没有,就创建一个新的Module实例
- 将它保存到缓存
- 使用 module.load() 加载指定的模块文件, 读取文件内容之后,使用 module._compile() 执行文件代码
- 如果加载/解析过程报错,就从缓存删除该模块
- 返回该模块的 module.exports
这里,当执行到module.compile的时候,其实就是在执行模块文件的代码。模块文件我们举例如下:
// 一个模块文件 a.js
var a = 'a'
module.exports = { a }
上面这段代码,有看的见的部分和看不见的部分。我们先说看不见的部分,大致流程如下:
在当前作用域下,先实例化了一个 Module,得到一个module对象。然后再向module.export上绑定属性,补全后的代码如下:
// 一个模块文件 a.js
var { Module } = require('module')
// 下面这个module,仅在当前文件作用域下有效
module = new Module(option) // 这里的option是示意,传入的内容应该是初始化当前模块的一些默认内容,如文件名,文件路径等
// module的内容大致如下
{
id: '.',
exports: {},
parent: null,
filename: '/Users/didi/practice/modules/commonJS/a.js',
loaded: false,
children: [Array],
paths: [Array] },
}
// 接下来向module.exports上绑定属性
var a = 'a'
module.exports = { a }
具体逻辑印证可见node源码:github.com/nodejs/node…
如此依赖,就将module.js文件暴露的模块,添加到全局module对象的_cache中了,并且也在引用module.js的文件中执行了module.js文件,以及将 module.js 中暴露给export对象的内容拷贝了一份赋值给了index.js中的变量。
也就整体完成了一次模块化引用。
整理下流程图如下: