本文作者:刘观宇,360 奇舞团高级前端工程师、技术经理,W3C CSS 工作组成员。

孤山寺北贾亭西,水面初平云脚低。几处早莺争暖树,谁家新燕啄春泥。乱花渐欲迷人眼,浅草才能没马蹄。最爱湖东行不足,绿杨阴里白沙堤。 ———— 唐.白居易《钱塘湖春行》

自从 Node8.5 以后, Node 开始支持引入 ES 模块1。在新开的项目中,笔者尝试使用了这种方式。由于目前 NodeJS 对于 ES 模块尚属试验性支持,因此需要在启动时候加入参数:--experimental-modules,完整的命令如:node --experimental-modules index.mjs。这样程序就愉快地运行起来了。不过当笔者尝试加入一些新的依赖之后问题出现了:

这个问题在于,很多 NodeJS 依赖包,由于使用 CommonJS 的方式编写,对于 ES Module 的支持尚不完善。因此包在使用过程中,存在一定的兼容问题。

这篇文章我们就来谈谈,在 NodeJS 环境下,CommonJS 和 ES Module 区别和联系,以及互操作的方法。

在 NodeJS 诞生时候,ES Module 模块系统还没有诞生,因此 NodeJS 最初采用的是 CommonJS 模块系统。我们之前所使用的 NodeJS 包,大部分都是 CommonJS 模块。CommonJS 模块和 ES Module 之间有很大的区别。这也就是为什么ES Module使用.mjs区别对待的原因。 下面这张图阐述了 NodeJS 对于两种模块系统的解析流程。

那么它们在行为上有什么区别呢?

主要有两点:

一、引入模块时机的区别:CommonJS 模块是运行时加载,换句话说是在 NodeJS 脚本执行时才加载进来,而 ES Module 则是在静态分析时候就确定了引用关系。这有点像给目标模块建立了一个符号链接,或者说建立了一个指针。从这个意义上说,ES Module 的 import 的加载效率应该略高于 CommonJS。而 CommonJS 这种特性也给动态加载模块提供了可能。

二、CommonJS 模块加载实际上是把 module.exports 对象进行了一次拷贝。因此当原模块的存储在栈空间的值,也就是基本类型值在第一次加载过程中已经被缓存掉,当它发生了改变,不会影响引入这个模块的代码的值。读者可以尝试在加载后打印下require.cache看一看缓存的结构。下面看这个例子:

// cjs.js

var inner = 3;

let innerIncrease = () => {

 inner++;

}

module.exports = {

 inner,

 innerIncrease

};

// main.js

var mod = require('./cjs');

console.log(mod.inner);

mod.innerIncrease();

console.log(mod.inner);

我们如果执行 node main.js 会发现两次执行的结果都为 3。那是因为 inner 的值在第一次引入时候被缓存掉了。

如果上述代码使用 ES module。因为都是引用值,所以外部可以感知内部变化。

// mjs.mjs

export let inner = 3;

export let innerIncrease = () => {

 inner++;

}

// main.mjs

var mod = require('./mjs.mjs');

console.log(mod.inner);

mod.innerIncrease();

console.log(mod.inner);

这里因为"import"的都是引用,会内外同步,因此依次打印出 3 和 4。

如果你启用了--experimental-modules,则意味这几件事:

  1. 对于同样的文件名,当模块没有写明扩展名时,加入--experimental-modules的总是先去加载.mjs 扩展名的同名模块,如果没有才会去加载.js。

  2. 只要使用 import/export 命令的,必须使得扩展名为 .mjs。

  3. .mjs 文件中不能使用 require 函数,否则会报ReferenceError: require is not defined错误。

  4. import 命令是异步加载,意思是,后面的模块不会等待前面的加载完再去加载。如果有先后依赖关系的模块,不能在 import 中同级写,这个和 CommonJS 模块 不一样。

  5. 顶层文件,this指向undefined。arguments、require、module、exports、__filename和__dirname均为undefined

既然差异很难弥合,那么用扩展名来区分两套体系从目前来看是很必要的。但是有时候,互相操作又是不可避免的。

那么如何互操作呢?

如果 ES Module 需要加载 CommonJS 模块,NodeJS 将把 module.exports 当作一个对象赋给 default 对象,即相当于:export default {...module.exports 对象}

假设我们有 cjs 模块 cjs.js,如下:

// cjs.js

module.exports = {

 "Hello": "你好",

 "World": "世界"

}

我们可以在需要的 mjs 文件中这样使用:

// main.mjs

import cjsExport from './cjs.js'

// 以下两种也可以

/***

* import {default as cjsExport} from './cjs.js'

* import * as cjsExport from './cjs.js'

***/

const {Hello, World} = cjsExport

但是,这种方式是不允许的:import { someExports } from 'module'。

那么 CommonJS 能否加载 ES Module 呢?答案是肯定的,不过稍微有点麻烦。同时,使用这个特性需要在 NodeJS 9.7.0 版本以上,同时加入--experimental-modules,这里2是当时的 changelog

// es.mjs

export default {

 "a" : 1,

 "b" : 2

}

// main.mjs

(async _ => {

 const es = await import('./es.mjs');

 console.log(es);

})()

其它几种 export 语法也可以用这种方式导入,想要知晓所有 export 语法的,可以参考高峰老师的之前在周刊发表的这篇文章3

回到我们开头提出的问题,在 mjs 模块里,可以使用上面提到的相应的加载 CommonJS 模块的写法,就可以正常运行了。

随着 ES Module 越来越完善,我们可以期待,今后 ES module 会越来越多。我们看到在现在这个多种模块机制并存的时代,很多重要的库在新版本的源码中都做了适配,基本的方案是:利用扩展名引入的优先级,提供多种扩展名的文件。如.js 适应 CommonJS,.mjs 文件适配 ES module,这种方案也使得库的使用者,比较平滑地切换各种模块系统。当然,各种预编译器也可以在不得已的时候提供必要的帮助。

参考资料

  1. http://es6.ruanyifeng.com/#docs/module-loader

  2. https://nodejs.org/api/esm.html

  3. https://www.zcfy.cc/article/es-modules-and-node-js-hard-choices-477.html

文内链接

  1. https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V8.md#8.5.0

  2. https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V9.md#9.7.0

  3. https://mp.weixin.qq.com/s/bIU_FvesizFJ3D_6KWRPHA

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

nodejs html引用js_NodeJS与模块系统相关推荐

  1. nodejs html引用js_nodejs做出最简单的网页服务端。【501】

    一.先去官网下载nodejs,按自己的系统一步一步操作,基本很简单,这里就不多介绍了. 二.安装完成后,通过cmd验证是否安装成功,输入node -v可以查看版本号. 三.用开发工具创建一个js文件, ...

  2. jdk 11 模块系统_JDK 9:模块系统状态的重点

    jdk 11 模块系统 马克·雷因霍尔德 ( Mark Reinhold )的"模块系统状态 (SOMS)"已于本月初发布,它提供了信息丰富的可读性"对Jigsaw项目中 ...

  3. JDK 9:模块系统状态的重点

    马克·雷因霍尔德 ( Mark Reinhold )的"模块系统状态 (SOMS)"已于本月初发布,它提供了信息丰富的可读性"对项目Jigsaw中原型的Java SE平台 ...

  4. Nodejs的模块系统以及require的机制

    一.简介 Nodejs 有一个简单的模块加载系统.在 Nodejs 中,文件和模块是一一对应的(每个文件被视为一个独立的模块),这个文件可能是 JavaScript 代码,JSON 或编译过的C/C+ ...

  5. Nodejs中的模块系统

    一.模块化的定义 ①具有文件作用域 ②具有通信规则:加载和导出规则 二.CommonJS模块规范 1.nodejs中的模块系统,具有文件作用域,也具有通信规则,使用require方法加载模块,使用ex ...

  6. NodeJS与模块系统

    孤山寺北贾亭西,水面初平云脚低.几处早莺争暖树,谁家新燕啄春泥.乱花渐欲迷人眼,浅草才能没马蹄.最爱湖东行不足,绿杨阴里白沙堤. ---- 唐.白居易<钱塘湖春行> 自从 Node8.5 ...

  7. NodeJs 之模块系统

    前言 随着前端的发展,工程模块儿化已经是必不可少的一部分了,为了让NodeJs的文件可以相互调用,NodeJs提供了一个简单的模块系统:简单点说:一个 nodeJs 文件就是一个模块儿 . 通过代码来 ...

  8. 基于源码剖析nodejs模块系统

    nodejs模块系统 简介 为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统. 模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的.换言之, 一个 No ...

  9. nodejs中的模块系统:exports导出模块

    node中的模块系统 示例: 模块作用域 成功获取add: module.exports

最新文章

  1. Android读取短信和联系人
  2. KRPano JS 场景编辑器源码
  3. poj1743(后缀数组:最长不可重叠子串长度)
  4. php 配置(转载其他)
  5. python获取字典的值_python取出字典中的所有值的两种方法
  6. 【图像超分辨率】Perceptual Losses for Real-Time Style Transfer and Super-Resolution
  7. 头条 上传图片大小_【标签头条】北京市启用进口冷链食品追溯平台;全球包裹热潮助推标签业发展;数字水印实现大规模垃圾分类;安慕希的麻将酸奶包装好真实...
  8. keil运行c语言输入函数,keil 编译器V6 定义函数在ram中运行-和在指定地址定义常量,keil编译器...
  9. 不使用border-radius,实现一个可复用的高度和宽度都自适应的圆角矩形
  10. WGS-84坐标系转GCJ02坐标系
  11. Blender MMD 备忘
  12. 《计算机网络教程》(微课版 第五版)第三章 数据链路层 课后习题及答案
  13. win7 批处理文件默认以管理员身份运行及清除IE缓存脚本
  14. ROS2探索(一)Publisher-Subscriber的内部过程
  15. 新浪微博架构和FEED架构分析--人人架构
  16. 号码归属地及运营商查询工具
  17. PHP之实现 家谱树,子孙树
  18. 我的身体为什以会这样?如何诊治?
  19. 1504_AURIX_TC275参考手册_芯片介绍
  20. Java中tif转png,tif格式图片转换为gif、png、jpg格式(Java实战)

热门文章

  1. 学习 ---AJAX如何用于Web部件
  2. 《图解HTTP》读书笔记--第5章与HTTP协作的Web服务器
  3. mysql索引 钱缀_mysql字符串前缀索引
  4. 高校c语言程序设计比赛,分秒必争,力争上游,计算机学院举办第八届C语言程序设计挑战杯...
  5. 树莓派c语言访问mariadb,树莓派之MariaDB
  6. 行号 设置vim_Vim从小白到入门
  7. 服务器centos7系统更换网卡,Centos7更换网卡名称(示例代码)
  8. mysql存储过程遍历新增_MySQL存储过程:内部调用存储过程、存储过程实现遍历数据库建表以及修改字段...
  9. java中菜单分几级_JAVA构造多级菜单
  10. pandas的reindex功能