# 一、开始

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加载模块的流程:

  1. 找到需要加在的模块文件
  2. 判断是否缓存过,如果没有,就读取模块文件
  3. 把读取到的内容放到一个自执行函数中执行
(function (exports, require, module, __filename, __dirname) {
    //模块的代码实际上在这里
});
  1. 返回module.exports需要导出的内容

# (2)支持类型

NodeJS支持三种类型的文件,.js.json.node(C++扩展二进制模块)。

模块类型分为核心模块、文件模块、第三方模块,其中核心模块是编译二进制文件,加载速度最快,比如fs/path/http等。

当尝试加载一个不带后缀的文件时,比如require('./test'),会依次尝试test/test.js/test.json/test.node

# (3)缓存

再看一下模块的缓存,缓存可以通过require.cache查看,其返回一个对象,key是文件绝对路径,valueModule对象。

// console.log(require.cache)

{
  '/Users/mike/Documents/igame-web/test3.js': {
    // Module对象
  },
  '/Users/mike/Documents/igame-web/test2.js': {
    // Module对象
  }
}

最近遇到一个问题就是CommonJS的模块缓存机制导致的,起初想通过改变.env.local文件,对多个项目一起打包。由于缓存机制,新写入的环境变量,并不会被重新加载,也就导致了无法生效。

# (4)特点

  1. CommonJS加载的是一个对象(module.exports属性),只有脚本运行时该对象才会确定,所以CommonJS是运行时加载。
  2. CommonJS输出的是一个值的拷贝,也就是说模块输出一个值后,模块内部的变化就影响不到该值。
  3. 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语法的转化结果。

# 五、相关资料

  1. Module 的加载实现 (opens new window)
  2. import、require、export、module.exports 混合使用详解 (opens new window)
  3. require时,exports和module.exports的区别你真的懂吗? (opens new window)
  4. node.js中module模块的理解 (opens new window)