Node.js入门 03:模块化规范 CommonJS 与 ES Module

Posted Naisu Xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node.js入门 03:模块化规范 CommonJS 与 ES Module相关的知识,希望对你有一定的参考价值。

目的

传统的用在网页中的javascript代码文件与文件之中的内容都是全局相互可见的,这对于大型项目特别是多人合作的项目来说挺不好的,容易互相影响,出现各种问题。

大多数编程语言都有类库、模块等概念来处理这个问题,在Node.js中早期是使用CommonJS规范来处理这个问题。后来JavaScript的ES6标准中制定了ES module规范,所以Node.js也渐渐的开始支持ES module规范了。

这篇文章将对这两种模块化方式做个简单的使用说明。

CommonJS

基础使用

CommonJS规范模块使用上主要就三点:

  • 每个文件就是一个模块,有自己的作用域,其中的变量、函数等都是私有的,对其他文件不可见;
  • 可以使用 module.exports 导出当前模块中的变量、函数等,供其它文件使用;
  • 其它文件中使用 require() 方法引入模块;

下面是个简单的例子:

// common.js
let name = 'naisu'
module.exports.name = name // 导出name变量
module.exports.fun = function(){ // 导出函数
    console.log('233~');
}
// test.js
const cm = require('./common.js') // 导入模块

console.log(cm.name) 
cm.fun()


上面例子可以看到CommonJS规范模块导入导出使用都很简单。对于模块而言所有要导出的东西整体就是一个对象,挂在module.exports上;其它文件中用 require() 方法导入模块其实就是导入了模块的module.exports上的对象。

module 对象

CommonJS规范中每个模块内部,都有一个module对象代表当前模块,它有以下属性:

module.id 模块的识别符,通常是带有绝对路径的模块文件名;
module.filename 模块的文件名,带有绝对路径;
module.loaded 返回一个布尔值,表示模块是否已经完成加载;
module.parent 返回一个对象,表示调用该模块的模块;
module.children 返回一个数组,表示该模块要用到的其他模块;
module.exports 表示模块对外输出的值;

上面属性中最重要的就是 module.exports 了,就像前面所描述的所有要导出的东西整体就是一个对象,挂在module.exports上。

在Node.js中提供了一个 exports 变量,该变量指向 module.exports ,也就是你可以使用 exports 来代替 module.exports ,但要注意的是不能使用 exports = ... 这种方式赋值,因为 exports 的内部实现其实可以理解为 var exports = module.exports

require() 方法

CommonJS规范中 require() 方法用于导入模块,导入放入可以向前面例子中 require('./common.js') 也可以写成 require('./common') ,两者效果是一样的。

require() 导入时可以使用相对路、绝对路径,或者也可以不带路径。在不带路径的情况下,程序运行时会从Node.js安装目录、项目的node_modules文件夹、用户的node_modules文件夹、系统node_modules文件夹等依次寻找该模块。

要注意的是 require() 导入模块生成的是一份拷贝,一旦一个值导出后模块内部的变化就不会影响到这个值了:

使用 require() 导入模块后会缓存该模块,下次再加载该模块会直接从缓存中取出。所有缓存保存在 require.cache 中,可以使用 delete require.cache[moduleName] 方法删除缓存:

上面的 require.resolve() 方法用于将模块解析到绝对路径。

ES Module

在JavaScript的ES6标准中加入了模块功能,通常被成为ES Module(或ES6 Module),这和前面的CommonJS有一个非常大的不同点:CommonJS中的模块只是从代码层面来实现,本质其实是个对象,ES Module是从语言层面制定的,原生的功能在性能、调试、编译等方面都有优势。

目前Node.js(v14.17.6)中虽然已经支持ES Module,但是使用起来还不是很舒服。想要使用ES Module的话得稍作处理,主要有两种方式(二选一):

  • ES模块和使用ES模块模块的文件名后缀都改为 .mjs
  • 项目文件夹中建立 package.json 文件,设置其中type字段 { "type": "module" } ,注意使用该方式后CommonJS模块文件名后缀就得改为 .cjs 了;

ES Module使用上和CommonJS差不多:

  • 每个文件就是一个模块;
  • 使用 export 导出;
  • 使用 import 导入;

下面是个简单的例子:

// module.mjs
let name = 'naisu'
function fun() {
    console.log('fun 233~')
}
class MC {
    constructor() {
        console.log('MC 233~')
    }
}
export { name, fun, MC }

// 下面代码等同于上面
// export let name = 'naisu'
// export function fun() {
//     console.log('fun 233~')
// }
// export class MC {
//     constructor() {
//         console.log('MC 233~')
//     }
// }
// test.mjs
import { name, fun, MC } from './module.mjs'

console.log(name)
fun()
let mc = new MC()

ES Module中 import 可以使用 * 代表模块整体、使用 as 来重命名,所以可以有下面用法:

// module.mjs
let name = 'naisu'
function fun() {
    console.log('fun 233~')
}
export { name, fun}
// test.mjs
import * as esm from './module.mjs'
console.log(esm.name)
esm.fun()


模块在输出的时候可以有一个默认输出,使用 export default 修饰,可以用来修饰匿名函数。在import这个默认输出的时候需要指定一个名字。参考下面例子:

// module.mjs
let name = 'naisu'
function fun() {
    console.log('fun 233~')
}
export { name }
export default fun
// test.mjs
import ff, {name} from './module.mjs'
console.log(name)
ff()


ES Module中导出与导入是动态绑定的关系,通过相关的接口还是可以修改内部的值的。这点和 CommonJS 有区别。

上面用 import 导入模块是静态的,会在程序运行之前导入,其实还有一个动态导入的方法 import() ,这个是异步的方法,调用时将返回一个 Promise 。使用方式可以如下:

import('./module.mjs').then((module) => {
    // TODO
});

// 也可以使用await关键字
let module = await import('./module.mjs');

混合使用

CommonJS 中使用 ES Module 可以使用 import() 方法导入:

ES Module 中使用 CommonJS 也只能用 import 方式整体导入:

更多内容可以可以参考下面文章:
阮一峰的网络日志 《Node.js 如何处理 ES6 模块》

总结

总的来说目前Node.js中的模块使用 CommonJS 规范还是最方便的,ES Module 虽然是JavaScript原生标准,但现在在Node.js中使用并不是非常顺畅,不过可以预见的是原生的 ES Module 将来大概率会完全替代现有的 CommonJS 的。

以上是关于Node.js入门 03:模块化规范 CommonJS 与 ES Module的主要内容,如果未能解决你的问题,请参考以下文章

node.js入门学习--Demo模块化改造

Node.js 入门教程 :模块

node.js的模块引用

Node.js开发实战

Nodejs的模块化

es6和node.js模块的区别