1 前言

不同时期的的 JS 发展,诞生了不同的模块化机制;近些年,随着前端模块化的标准落地,不同端的 JS 对此也都做了各自的实现。今天我们就来聊聊这个话题。

本文我们将主要探讨以下四个方面:

  • JavaScript 模块化机制概览;

  • 如何在 Node 中使用 ES Modules;

  • CommonJS 和 ES Modules 的模块机制的异同;

  • CommonJS 和 ES Modules 的模块文件相互引用;

2 JavaScript 模块化机制概览

JavaScript 常见的模块化机制主要有以下三种:

  • AMD (Asynchronous Module Definition): 在浏览器中使用,并用 define 函数定义模块;

  • CJS (CommonJS): 在 NodeJS 中使用,用requiremodule.exports引入和导出模块;

  • ESM (ES Modules): JavaScript 从 ES6(ES2015) 开始支持的原生模块机制,使用importexport引入和导出模块;

3 Node 对 ES Modules 支持

Node verison 13.2.0 开始正式支持 ES Modules 特性。

注:虽然移除了 --experimental-modules 启动参数,但是由于 ESM loader 还是实验性的,所以运行 ES Modules 代码依然会有警告:

(node:47324) ExperimentalWarning: The ESM module loader is experimental.
come from speak )。

4 在 NodeJS 中使用 ES Modules

1)在 package.json 中,增加 type: "module" 配置;

文件目录结构:

.
├── index.js
├── package.json
└── utils└── speak.js
// utils/speak.js
export function speak() {console.log('Come from speak.')
}// index.js
import { speak } from './utils/speak.js';
speak(); //come from speak

2)在 .mjs 文件可以直接使用 import 和 export

文件目录结构:

.
├── index.mjs
├── package.json
└── utils└── sing.mjs
// utils/sing.mjs
export function sing() {console.log('Come from sing')
}// index.mjs
import { sing } from './utils/sing.mjs';
sing(); //come from sing

注意:

  • 若不添加上述两项中任一项,直接在 Node 中使用 ES Modules,则会抛出警告:

Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
  • 根据 ESM 规范,使用 import 关键字并不会像 CommonJS 模块那样,在默认情况下以文件扩展名填充文件路径。因此,ES Modules 必须明确文件扩展名。

模块作用域:

一个模块的作用域,由父级中有type: "module"的 package.json 文件路径定义。而使用.mjs扩展文件加载模块,则不受限于包的作用域。

同理,package.json中没有type标志的文件都会默认采用 CommonJS 模块机制,.cjs类型的扩展文件使用 CommonJS 方式加载模块同样不受限于包的作用域。

包的入口

定义包的入口有两种方式,在 package.json 中定义main字段或者exports字段:

{"main": "./main.js","exports": "./main.js"
}

需要注意的是,当exports字段被定义后,包的所有子路径都将被封装,子路径的文件不可再被导入。

例如: require('pkg/subpath.js') 将会报错:

ERR_PACKAGE_PATH_NOT_EXPORTED error.`

5. 两个模块机制在执行时机上的区别

  • ES Modules 导入的模块会被预解析,以便在代码运行前导入;

  • 在 CommonJS 中,模块将在运行时解析;

举一个简单的例子来直观的对比下二者的差别:

// ES Modules// a.js
console.log('Come from a.js.');
import { hello } from './b.js';
console.log(hello);// b.js
console.log('Come from b.js.');
export const hello = 'Hello from b.js';

输出:

Come from b.js.
Come from a.js.
Hello from b.js

同样的代码使用 CommonJS 机制:

// CommonJS// a.js
console.log('Come from a.js.');
const hello = require('./b.js');
console.log(hello);// b.js
console.log('Come from b.js.');
module.exports = 'Hello from b.js';

输出:

Come from a.js.
Come from b.js.
Hello from b.js

可以看到 ES Modules 预先解析了模块代码,而 CommonJS 是代码运行的时候解析的。

需要注意的是,根据 EMS 规范 import / export 必须位于模块顶级,不能位于作用域内;其次模块内的 import/export 会提升到模块顶部,这是在编译阶段完成的。

6.两个模块机制在原理上的区别

  1. CommonJS

Node 将每个文件都视为独立的模块,它定义了一个 Module 构造函数,代表模块自身:

function Module(id = '', parent) {this.id = id;this.path = path.dirname(id);this.exports = {};this.parent = parent;this.filename = null;this.loaded = false;this.children = [];
};

而 require 函数接收一个代表模块ID或者路径的值作为参数,它返回的是用module.exports导出的对象。在执行代码模块之前,NodeJs 将使用一个包装器对模块中的代码对其进行封装:

(function(exports, require, module, __filename, __dirname) { // Module code actually lives in here
});

通过这样做,Node.js 实现了以下几点:

  • 它保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象。

  • 它有助于提供一些看似全局的但实际上是模块特定的变量,例如:

    • 实现者可以用于从模块中导出值的 module 和 exports 对象。

    • 包含模块绝对文件名和目录路径的快捷变量 __filename 和 __dirname 。

简言之,每个模块都有自己的函数包装器, Node 通过此种方式确保模块内的代码对它是私有的。

在包装器执行之前,模块内的导出内容是不确定的。除此之外,第一次加载的模块会被缓存到 Module._cache中。

一个完整的加载周期大致如下:

  Resolution (解析) –> Loading (加载) –> Wrapping (私有化) –> Evaluation (执行) –> Caching (缓存)
  1. ES Modules

在 ESM 中,import 语句用于在解析代码时导入模块依赖的静态链接。文件的依赖关系在编译阶段就确定了。对于 ESM,模块的加载大致分为三步:

  Construction (解析) -> Instantiation (实例化、建立链接) -> Evaluation (执行)

这些步骤是异步执行的,每一步都可以看作是相互独立的。这一点跟 CommonJS 有很大不同,对于 CommonJS 来说,每一步都是同步进行的。

7. 两种模块间的相互引用

CommonJS 和 ES Modules 都支持Dynamic import(),它可以支持两种模块机制的导入。

在 CommonJS 文件中导入 ES Modules 模块

由于 ES Modules 的加载、解析和执行都是异步的,而 require() 的过程是同步的、所以不能通过 require() 来引用一个 ES6 模块。ES6 提议的 import() 函数将会返回一个 Promise,它在 ES Modules 加载后标记完成。借助于此,我们可以在 CommonJS 中使用异步的方式导入 ES Modules:

// 使用 then() 来进行模块导入后的操作
import(“es6-modules.mjs”).then((module)=>{/*…*/}).catch((err)=>{/**…*/})
// 或者使用 async 函数
(async () => {await import('./es6-modules.mjs');
})();
在 ES Modules 文件中导入 CommonJS 模块

在 ES6 模块里可以很方便地使用 import 来引用一个 CommonJS 模块,因为在 ES6 模块里异步加载并非是必须的:

import { default as cjs } from 'cjs';// The following import statement is "syntax sugar" (equivalent but sweeter)
// for `{ default as cjsSugar }` in the above import statement:
import cjsSugar from 'cjs';console.log(cjs);
console.log(cjs === cjsSugar);

8. Node 中 ES Modules 的现状和未来

在引入 ES6 标准之前,服务器端 JavaScript 代码都是依赖 CommonJS 模块机制进行包管理的。如今,随着 ES Modules 的引入,开发人员可以享受到与发布规范相关的许多好处。但需要注意的是,截止至本文发布时间,在最新版 Node v15.1.0 中,该特性依然是实验性的(Stability: 1),不建议在生产环境中使用该功能。

最后,由于两种模块格式之间存在不兼容问题,将当前项目从 CommonJS 到 ES Modules 转换将是一个挑战。可以借助 Babel 相关插件(plugin-transform-modules-commonjs, babel-plugin-transform-commonjs)实现 CommonJS 和 ES Modules 间的相互转换。

参考

  • Node Documentation

  • Node version13+ release log

  • Node version14+ release log

  • Node modules wrapper

  • Node Source code: cjs

  • ECMA262 Modules

  • TC39 Proposal Dynamic import



2020年我们可以在Node中使用ES Modules了吗相关推荐

  1. 二十五、Node中的Buffer缓冲器和EventEmitter事件触发器

    @Author:Runsen @Date:2020/6/5 作者介绍:Runsen目前大三下学期,专业化学工程与工艺,大学沉迷日语,Python, Java和一系列数据分析软件.导致翘课严重,专业排名 ...

  2. 长连接及在Node中的应用——HTTP/1.1 keep-alive

    HTTP请求都要经过TCP三次握手建立连接,四次分手断开连,如果每个HTTP请求都要建立TCP连接的话是极其费时的,因此HTTP/1.1中浏览器默认开启了Connection: keep-alive. ...

  3. 打开浏览器的包 node_如何发布可在浏览器和Node中使用的软件包

    打开浏览器的包 node When you create a package for others to use, you have to consider where your user will ...

  4. node中模块、AMD与CMD、ES6模块,node中使用ES6

    1.Nodejs 中的模块 在node环境中一个js文件就是一个模块(module) 我们采用的是CommonJS规范,使用require引入模块,使用module.exports导出接口 node的 ...

  5. node 进阶 | 通过node中如何捕获异常阐述express的特点

    node如何捕获异常 node基于js的单线程,有了非阻塞异步回调的概念,但是在处理多个并发连接时,并发环境要求高,最重要的是单线程,单核CPU,一个进程crash则web服务都crash,但是为什么 ...

  6. Node中同步与异步的方式读取文件

    场景 Node.js最大的特点就是异步式I/O(或者非阻塞I/O)与事件紧密结合的编程模式.这种模式与传统的同步式I/O线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻 ...

  7. node中异步IO的理解

    解释性语言和编译型语言的区别: 计算器不能直接的理解高级语言,只能理解机器语言,所以必须把高级语言翻译为机器语言,翻译的方式有两种,一个是编译,一个是解释. 解释性语言的程序不需要编译,它是在运行程序 ...

  8. node --- 在node中使用mongoosemongoDB的安装

    *首先确保,你的电脑安装了mongodb,网址: mongodb官网 *使用npm安装 mongoose: mongoose官网 ps:mongoose是Node中操作mongoDB的第三方插件.用于 ...

  9. 什么流读取MultipartFile_深入理解并运用Node中的IO模型流

    在 NodeJs 中,流随处可见,读/写文件流,HTTP请求/返回流,stdin/stdout流.理解并运用好流会让你的Node更具力量. Stream lib/_stream_readable.js ...

最新文章

  1. 交换友链的几个技巧!
  2. python项目ImportError: Plotly express requires pandas to be installed.解决方案
  3. 使用WTMPlus快速搭建发卡网
  4. ROADS POJ - 1724(限制条件的最短路)【邻接表+深搜】
  5. python生成list的时候 可以用lamda也可以不用_python 可迭代对象,迭代器和生成器,lambda表达式...
  6. linux yum的用法【ZT】
  7. C++ Coding潜意识(-ing)
  8. 国家开放大学英语和计算机考试答案,最新电大国家开放大学《人文英语》网络核心课形考网考作业试题及答案...
  9. 解决SVN汉化不成功问题,下载历史版本
  10. 数字电路基础知识(二) 复位设计-亚稳态的产生与影响
  11. Jdom的安装和使用
  12. Axure RP7.0学习记录
  13. 什么是“个人商业模式”?就是一个人出售自己时间的方式
  14. Word一行有空白格,却无法输入新的文字
  15. python中e怎么计算_蒙特卡洛法计算自然常数e——python编程及可视化
  16. 永洪bi日志清理配置
  17. c++left right 和 setw() 函数的用法Alignment of Code
  18. 计算机二级黑板板书书写,清华老师的板书惊艳朋友圈 8个技巧让黑板亮起来
  19. outlook express 邮件附件丢失
  20. 个人网站使用github登录

热门文章

  1. 正规现货黄金中的MACD技术
  2. 高二学生爆肝10个月!自学数电在《我的世界》里打造理论最快计算器,5天涨粉1万...
  3. 使用border属性绘制三角形、五角星、心形
  4. 如何参与开源社区_我的世界如何让我参与开源社区
  5. 二分查找法-函数形式
  6. UE4网络部分(1)——网络构架
  7. 期货投资最低需要多少资金
  8. 虚拟网络技术:Bond技术
  9. 【校招VIP】产品项目分析之功能分析
  10. 在JMeter中提取token值并传递给其它接口使用