# 一、开始
require/module.exports
属于CommonJS
规范,import/export
属于ES6规范。我们一起来看下其中的不同。
# 二、NodeJS模块
先介绍下NodeJS中模块的相关概念。
# 1. Module
在NodeJS中,一个文件就是一个单独的模块。
CommonJS规范规定,每个模块内部,module
变量代表当前模块。这个变量是一个对象,
在一个NodeJS模块内部打印下module
:
// console.log(module)
{
id: '.',
path: '/Users/mike/Documents/igame-web',
exports: {},
parent: null,
filename: '/Users/mike/Documents/igame-web/test3.js',
loaded: false,
children: [
Module {
id: '/Users/mike/Documents/igame-web/test2.js',
path: '/Users/mike/Documents/igame-web',
exports: [Object],
parent: [Circular *1],
filename: '/Users/mike/Documents/igame-web/test2.js',
loaded: true,
children: [],
paths: [Array]
}
],
paths: [
'/Users/mike/Documents/igame-web/node_modules',
'/Users/mike/Documents/node_modules',
'/Users/mike/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
module
上有一些属性:
id
,模块的标识符exports
,模块的导出对象parent
,表示当前模块的父模块,当前模块是谁加载的filename
,模块的绝对路径loaded
,表示是否加载完成children
,表示当前模块加载了哪些模块paths
,表示模块的搜索路径,路径的多少取决于目录的深度,寻找第三方模块时会用到
在模块的内部,this
指向的是当前模块的导出对象:
console.log(this === module.exports); // true
console.log(this === exports); // true
# 2. require
# (1)运行机制
模块的exports
属性(即module.exports
)是对外的接口。加载某个模块,其实是加载该模块的module.exports
属性。
通过require
加载模块的流程:
- 找到需要加在的模块文件
- 判断是否缓存过,如果没有,就读取模块文件
- 把读取到的内容放到一个自执行函数中执行
(function (exports, require, module, __filename, __dirname) {
//模块的代码实际上在这里
});
- 返回
module.exports
需要导出的内容
# (2)支持类型
NodeJS支持三种类型的文件,.js
、.json
、.node
(C++扩展二进制模块)。
模块类型分为核心模块、文件模块、第三方模块,其中核心模块是编译二进制文件,加载速度最快,比如fs/path/http
等。
当尝试加载一个不带后缀的文件时,比如require('./test')
,会依次尝试test/test.js/test.json/test.node
。
# (3)缓存
再看一下模块的缓存,缓存可以通过require.cache
查看,其返回一个对象,key
是文件绝对路径,value
是Module
对象。
// console.log(require.cache)
{
'/Users/mike/Documents/igame-web/test3.js': {
// Module对象
},
'/Users/mike/Documents/igame-web/test2.js': {
// Module对象
}
}
最近遇到一个问题就是CommonJS的模块缓存机制导致的,起初想通过改变.env.local
文件,对多个项目一起打包。由于缓存机制,新写入的环境变量,并不会被重新加载,也就导致了无法生效。
# (4)特点
- CommonJS加载的是一个对象(
module.exports
属性),只有脚本运行时该对象才会确定,所以CommonJS是运行时加载。 - CommonJS输出的是一个值的拷贝,也就是说模块输出一个值后,模块内部的变化就影响不到该值。
- CommonJS模块是同步加载,由于是运行时加载,且从写法可以看出来,没有回调或
promise.then
方法,所以是同步的。
对比一下ES6的import
语法,ES6是编译时输出接口,输出的是值的引用,且ES6模块是异步加载,有独立的模块依赖的解析阶段。
# 3. exports和module.exports
模块的exports
属性(即module.exports
)是对外的接口。加载某个模块,其实是加载该模块的module.exports
属性。
为了方便,Node为每个模块提供一个exports
变量,指向module.exports
。这等同在每个模块头部,有一行这样的命令:
var exports = module.exports;
如果对exports
直接赋值,也就是给它一个新的引用地址,会切断其与module.exports
的引用关系,也就会导致require
加载不到。
// a.js
// 错误,加载不到
exports = {
a: 1
}
// 正确
exports.a = 1
// 正确
module.exports = {
a: 1
}
// b.js
const { a } = require('./a.js')
# 三、babel转化
我们在用webpack
做项目的时候,经常CommonJS和ES模块混用,这是因为babel
做了转化,将它们都转为CommonJS。
# 1. 转化导出
ES6的导出类型有:
export default 123;
export const a = 123;
const b = 3;
const c = 4;
export { b, c };
babel
会把它们转化为:
exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;
就是将ES6模块内容赋值给exports
,然后加上__esModule
属性。
# 2. 转化导入
ES6的导入大概有这么几种:
import a from './a.js';
import * as b from './b.js';
import { c } from './c.js';
对于第一种导入方式,本意是想倒入模块的default
属性,所以会被转化为:
const a = require('./a.js').default;
所以如果你使用了babel
,想要require
方法导入ES6模块的default
输出,可以用上述方式。
第二种导入方式,就是导入b.js
文件中所有输出,其实就是exports
对象,所以会被转为:
const b = require('./b.js');
第三种方式会被转为:
const { c } = require('./c.js');
# 四、总结
本文重点讲解了NodeJS中的模块加载原理,和babel
转化ESM的机制。CJS和ESM加载原理上几乎针锋相对,掌握了它们可以在开发过程中规避一些坑。
关于babel
转化,可以自己动手试一下,看一下对其他ES6语法的转化结果。
# 五、相关资料
← V8引擎 React Fiber →