持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
誓要拿下 CommonJS
start
- 今天来说说 CommonJS 规范
背景
早期的 JS 是没有模块化规范的。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
常见的使用 CommonJS 规范的主要有:
- NodeJS
- webpack
- 正是由于 NodeJS 使用的是 CommonJS 规范,导致与之相关的 npm 依赖包大多数都采用了 CommonJS 规范。
- 所以想要和 npm 依赖包 “打好交道”,熟练掌握 CommonJS 规范,是非常有必要的。
开始
1. CommonJS 规范初体验
/* main.js */
var obj = require('./a')
console.log(obj)
/* a.js */
exports.a = '123'
/* shell node ./main.js */
// { a: '123' }
2. 模块包装器
提一个问题: 为什么上述示例的代码可以使用 require 、 exports 、 module.export不会报错?
2.1 模块包装器
在执行模块的代码之前,Node.js 将使用如下所示的函数包装器来包装它:
;(function (exports, require, module, __filename, __dirname) {
// Module code actually lives in here
// 模块代码实际上就在这里
})
通过这样做,NodeJs 实现了一些目标:
- 用 var,const 或 let 声明的顶级变量,作用域在模块内,而不是全局对象的范围内。
- 它有助于给模块提供特定的全局变量,例如:
- 模块和导出对象,实现者可以使用这些对象从模块导出值。
- 方便的变量
__filename和__dirname,包含模块的绝对文件名和目录路径。
官方文档原文:
可以看到上述的模块包装器,传入了五个变量
exports, require, module, __filename, __dirname这就是为什么我们可以直接在 js 文件中使用exports, require, module的原因。因为 NodeJs 执行模块代码之前,会在外层包装一层函数。
__filename,__dirname这里稍微解释一下:
__filename当前模块的文件名(绝对路径), 例如q:\growth\blog\a.js__dirname当前模块的目录名(绝对路径) 例如q:\growth\blog
3. exports or module.export
CommonJS 的导出,可以通过 exports 或者 module.export;
3.1 exports 和 module.export是什么
直接打印一下
/* main.js */
console.log(module)
console.log(exports)
console.log(module.exports === exports)
/* 执行 */
// node main.js
输出
// Module {
// id: '.',
// path: 'q:\\growth\\blog',
// exports: {},
// parent: null,
// filename: 'q:\\growth\\blog\\main.js',
// loaded: false,
// children: [],
// paths: [
// 'q:\\growth\\blog\\node_modules',
// 'q:\\growth\\node_modules',
// 'q:\\node_modules'
// ]
// }
// {}
// true
总结:
由第 2 小节的知识,我们知道:exports 和 module 是 nodejs 执行我们代码的时候,在模块包装器传入的。
为了了解这两者是什么,我特意打印了一下它们。
- module 是一个对象,该对象中有
id,path,exports等属性; - exports 也是一个对象;
- 默认状态下
module.exports === exports为true;
3.2 为什么默认状态下 module.exports === exports
其实本质上 我们导出数据,是利用 module.exports 来实现的,而 exports 存储的是 module.exports 的引用地址
验证第 4 点
/* main.js */
exports.a = '123'
exports.b = '222'
module.exports = {
name: '新对象',
}
/* a.js */
var obj = require('./a')
console.log(obj)
/* shell node ./main.js */
// { name: '新对象' }
// 这里就解释了为什么,使用exports导出数据的时候,不能这样使用exports:`exports = { name:"新对象"}`
3.3 exports 带 s 结尾
我们看到 exports 带 s结尾。这里需要和 ES6 的 export 做好区分.
说个我自己用来记忆理解:CommonJS 的输出,默认都是放在一个对象中输出, 对象存储的信息比较多,所以需要加
s后缀。
4. require
4.1 require 是什么
直接打印一下require
/* main.js */
console.log(require)
console.log(typeof require) // function
/* 执行 */
// node main.js
输出
[Function: require] {
resolve: [Function: resolve] { paths: [Function: paths] },
main: Module {
id: '.',
path: 'q:\\growth\\blog',
exports: {},
parent: null,
filename: 'q:\\growth\\blog\\main.js',
loaded: false,
children: [],
paths: [
'q:\\growth\\blog\\node_modules',
'q:\\growth\\node_modules',
'q:\\node_modules'
]
},
extensions: [Object: null prototype] {
'.js': [Function],
'.json': [Function],
'.node': [Function]
},
cache: [Object: null prototype] {
'q:\\growth\\blog\\main.js': Module {
id: '.',
path: 'q:\\growth\\blog',
exports: {},
parent: null,
filename: 'q:\\growth\\blog\\main.js',
loaded: false,
children: [],
paths: [Array]
}
}
}
function
总结
由上述的代码可以了解到,require 本质是一个函数,其次这个函数上还有一些属性:resolve , main , extensions , cache
这里简单说一下这几个属性的作用:
resolve使用内部 require()机制来查找模块的位置(不是加载模块,只返回解析的文件名)。mainmodule 表示 Node.js 进程启动时加载的入口脚本的对象。extensions指导 require 如何处理某些文件扩展名。cache缓存已经导入的内容。
4.2 require 有什么用
作用:用于导入模块、JSON 和本地文件
4.3 require 文件加载流程
主要有三种
- 核心模块
- 文件模块
- 第三方模块
4.3.1 核心模块
// 例如导入 NodeJS内置的核心模块: fs
var fs = require('fs')
4.3.2 文件模块
// 相对路径
var a = require('./a.js')
// 绝对路径
var b = require('q:/growth/blog/.js')
4.3.3 第三方模块
nodejs 是如何寻找我们的第三方模块依赖呢
例如:
var express = require('express')
/* 1. 首先在当前文件目录下寻找node_modules */
/* 2. 如果当前目录没有找到,在父级目录的 node_modules 查找 */
/* 4. 如果父级也没有找到,就寻找父级的父级,沿着路径向上递归,直到根目录下的 node_modules 目录 */
/* 4. 在查找过程中,会找 package.json 下 main 属性指向的文件,如果没有 package.json ,在 node 环境下会以此查找 index.js ,index.json ,index.node */
4.4 require 加载文件的顺序
main.js
console.log('main开始执行啦')
var say = require('./a.js')
var edit = require('./b.js')
exports.a = 1
console.log('main结束了')
a.js
console.log('a开始执行啦')
module.exports = function say() {
console.log('开始说话')
}
var edit = require('./b.js')
console.log(edit)
console.log('a结束了')
b.js
console.log('b开始执行啦')
exports.edit = function () {
console.log('开始编辑')
}
console.log('b结束了')
输出
/*
main开始执行啦
a开始执行啦
b开始执行啦
b结束了
{ edit: [Function] }
a结束了
main结束了
*/
解释
- 从上向下依次加载;
- 执行到 require,优先加载引入的文件;
按顺序加载,深度优先加载,重复引入会跳过。
4.5 重复加载
4.4 案例有体现,当重复 require('./b.js'),只会执行一次。
4.6 动态加载
/* main.js */
if (true) {
var obj = require('./a.js')
console.log(obj)
}
/* a.js */
module.exports = function say() {
console.log('开始说话')
}
/* node ./main.js */
// [Function: say]
可以按条件加载
4.7 导入对象和导出对象之间的关系
先来一个案例
/* main.js */
var obj = require('./a.js')
console.log(obj)
/*
步骤一. 首先打印引入的obj
{
num: 2,
outObj: { count: 6 },
say: [Function: say],
add: [Function: add]
}
*/
obj.num = 80
obj.outObj.count = 80
console.log(obj)
/*
步骤二. 手动修改obj的值,再打印一下obj
{
num: 80,
outObj: { count: 80 },
say: [Function: say],
add: [Function: add]
}
*/
obj.say()
/*
步骤三. 第二步修改了导入的obj,查看一下原本的obj,发现基础类型的没有更改,复杂类型的发生了改变
2 { count: 80 }
*/
/* a.js */
let num = 2
let outObj = {
count: 6,
}
function say() {
console.log(num, outObj)
}
function add() {
num++
outObj.count++
}
module.exports = {
num,
outObj,
say,
add,
}
看上述的案例我们发现,模块导出的对象 A 和模块导入的对象 B,是有一定的关系的。
具体的关系是什么?
对象 A 和对象 B 两者, 简单理解:
1.基础类型的数据是完全复制一份。 2.复杂类型的数据是复制的引用地址。
可以理解为浅拷贝。 这里顺便提一下 ES Module 的导入导出数据之间的关系,ES Module 无论是基础类型还是复杂类型的数据,都是链接引用。也就是说,导出模块的数据发生改变,导入的数据也会发生改变。
end
- 写到这里基本就写完啦,加油!