了解ES6模块和NodeJS模块

一句话总结:模块是一个JavaScript文件,文件名就是模块名,export定义了调用方能从模块中获得什么(变量,函数,类),调用方通过import(或require)引入要用到的内容。

翻译自Understanding ES6 Modules

本文探讨了ES6模块,展示了如何在转换器(transpiler)的帮助下使用它们。

几乎每种语言都有模块概念——将声明的功能包含在一个文件中的方法。通常,开发人员创建一个封装的代码库,负责处理相关任务。该库可以由应用程序或其他模块引用。

好处:

  1. 代码可以拆分为自包含功能的较小文件。
  2. 可以在任意数量的应用程序中共享相同的模块。
    3 .理想情况下,模块不需要由另一个开发人员进行检查,因为它们已被证明有效。
  3. 引用模块的代码理解它是一种依赖。如果更改或移动模块文件,问题立即显而易见。
  4. 模块代码(通常)有助于消除命名冲突。模块1中的函数x()不能与模块2中的函数x()发生冲突。使用命名空间等选项,将调用变为module1.x()和module2.x()。

JavaScript中的模块在哪里?

几年前开始进行Web开发的任何人都会惊讶地发现JavaScript中没有模块的概念。无法直接引用或包含一个JavaScript文件。因此,开发人员采用了替代方案。

多个HTML <script>标签

HTML可以使用多个<script>标记加载任意数量的JavaScript文件:

<script src="lib1.js"></script>
<script src="lib2.js"></script>
<script src="core.js"></script>
<script>
console.log('inline code');
</script>

2018年平均每个网页使用了25个单独的脚本,但它不是一个实用的解决方案:

  • 每个脚本都会启动一个新的HTTP请求,这会影响页面性能。HTTP/2在某种程度上缓解了这个问题,但它无法帮助其他域(如CDN)上引用的脚本。
  • 每个脚本在运行时都会停止进一步处理。
  • 依赖管理是一个手动过程。在上面的代码中,如果lib1.js引用了代码lib2.js,代码将因为尚未加载而失败。这可能会打破进一步的JavaScript处理。
  • 除非使用适当的模块模式,否则函数可以覆盖其早期的JavaScript库因使用全局函数名称或重写本机方法而臭名昭着。

脚本连接

解决多个<script>标签问题的一种方法是将所有JavaScript文件连接成一个大的文件。这解决了一些性能和依赖关系管理问题,但它可能会导致手动构建和测试步骤。

模块装载机

诸如RequireJSSystemJS之类的系统提供了一个库,用于在运行时加载和命名其他JavaScript库。需要时,使用Ajax方法加载模块。这些系统有所帮助,但对于较大的代码库或将标准<script>标签添加到组合中的站点而言可能会变得复杂。

模块捆绑器,预处理器和Transpilers

Bundlers引入了编译步骤,因此在构建时生成JavaScript代码。处理代码以包含依赖项并生成单个ES5跨浏览器兼容的连接文件。流行的选项包括BabelBrowserifywebpack和更多一般任务运行者,如GruntGulp

JavaScript构建过程需要一些投入,但也收获好处:

  • 处理是自动化的,因此人为错误的可能性较小。
  • 进一步处理可以lint代码,删除调试命令,缩小生成的文件等。
  • Transpiling允许你使用其他语法,如TypeScriptCoffeeScript

ES6模块

上述选项引入了各种竞争模块定义格式。广泛采用的语法包括:

  • CommonJS - Node.js中使用的语法module.exports和require语法
  • 异步模块定义(AMD)
  • 通用模块定义(UMD)

ES6(ES2015)中提出了单一的本机模块标准。

默认情况下,ES6模块中的所有内容都是私有的,并且以严格模式运行(不需要'use strict')。使用export公开变量,函数和类。例如:

// lib.js
export const PI = 3.1415926;

export function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

export function mult(...args) {
  log('mult', args);
  return args.reduce((num, tot) => tot * num);
}

// private function
function log(...msg) {
  console.log(...msg);
}

或者,用单条export语句的形式。例如:

// lib.js
const PI = 3.1415926;

function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

function mult(...args) {
  log('mult', args);
  return args.reduce((num, tot) => tot * num);
}

// private function
function log(...msg) {
  console.log(...msg);
}

export { PI, sum, mult };

然后用import将项目从模块拉到另一个脚本或模块(要用花括号):

// main.js
import { sum } from './lib.js';

console.log( sum(1,2,3,4) ); // 10

这个例子中,lib.js与main.js在同一个文件夹。也可以使用绝对文件引用(以...开头/),相对文件引用(起始./或../)或完整URL。

一次可以导入多个项目:

import { sum, mult } from './lib.js';

console.log( sum(1,2,3,4) );  // 10
console.log( mult(1,2,3,4) ); // 24

导入时设置别名以解决命名冲突:

import { sum as addAll, mult as multiplyAll } from './lib.js';

console.log( addAll(1,2,3,4) );      // 10
console.log( multiplyAll(1,2,3,4) ); // 24

可以通过提供命名空间来导入所有公共项:

import * as lib from './lib.js';

console.log( lib.PI );            // 3.1415926
console.log( lib.add(1,2,3,4) );  // 10
console.log( lib.mult(1,2,3,4) ); // 24

导出模块由命名和默认(default)两种方式,前面都是命名方式。命名方式要求引入的时候必须要通过相同的命名进行引入,但是默认的没有要求。

// file test.js
let k; export default k = 12;
// some other file
import m from './test'; // 用变量m替代了变量k
console.log(m);  

在浏览器中使用ES6模块

在撰写本文时,基于Chromium的浏览器(v63 +),Safari 11+和Edge 16+ 支持ES6模块。Firefox支持将在版本60中支持(它about:config在v58 + 的标志后面)。

必须通过type="module"<script>标记中设置属性来加载使用模块的脚本。例如:

<script type="module" src="./main.js"></script>

或内联:

<script type="module">
  import { something } from './somewhere.js';
  // ...
</script>

无论它们在页面或其他模块中引用了多少次,模块只被解析一次。

服务器端注意事项

必须使用MIME类型提供模块application/javascript。大多数服务器会自动执行此操作,但要警惕动态生成的脚本或.mjs文件(请参阅下面的Node.js部分)。

常规<script>标记可以在其他域上获取脚本,但使用跨源资源共享(CORS)获取模块。因此,不同域上的模块必须设置适当的HTTP标头,例如Access-Control-Allow-Origin: *

最后,除非将crossorigin="use-credentials"属性添加到<script>标记并且响应包含标题,否则模块将不会发送cookie或其他标头凭据Access-Control-Allow-Credentials: true

模块执行推迟

<script defer>属性会延迟脚本执行,直到文档加载并解析为止。模块(包括内联脚本)默认延迟。例:

<!-- runs SECOND -->
<script type="module">
  // do something...
</script>

<!-- runs THIRD -->
<script defer src="c.js"></script>

<!-- runs FIRST -->
<script src="a.js"></script>

<!-- runs FOURTH -->
<script type="module" src="b.js"></script>

模块后备

不支持模块的浏览器不会运行type="module"脚本。可以通过浏览器的nomodule属性给模块提供后备脚本。例如:

<script type="module" src="runs-if-module-supported.js"></script>
<script nomodule src="runs-if-module-not-supported.js"></script>

现在应该在浏览器中使用模块吗?(文章是18年4月写的)

浏览器支持正在增长,但转换到ES6模块可能还为时过早。目前,使用模块捆绑器创建一个可在任何地方工作的脚本可能更好。

在Node.js中使用ES6模块

当Node.js在2009年发布时,不能提供模块是不可想象的。它采用了CommonJS,这意味着可以开发Node包管理器npm。从那时起,使用量呈指数级增长。

CommonJS模块的编码方式与ES2015模块类似。使用module.exports而不是export

// lib.js
const PI = 3.1415926;

function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

function mult(...args) {
  log('mult', args);
  return args.reduce((num, tot) => tot * num);
}

// private function
function log(...msg) {
  console.log(...msg);
}

module.exports = { PI, sum, mult };

require(而不是import)将模块拉入另一个脚本或模块:

const { sum, mult } = require('./lib.js');

console.log( sum(1,2,3,4) );  // 10
console.log( mult(1,2,3,4) ); // 24

require还可以导入所有项目:

const lib = require('./lib.js');

console.log( lib.PI );            // 3.1415926
console.log( lib.add(1,2,3,4) );  // 10
console.log( lib.mult(1,2,3,4) ); // 24

ES6模块很容易在Node.js中实现吗?不。

ES6模块位于Node.js 9.8.0+中的标志后面,并且至少在版本10之前不会完全实现。虽然CommonJS和ES6模块具有相似的语法,但它们的工作方式基本不同:

  • ES6模块已预先解析,以便在执行代码之前解析进一步的导入。
  • CommonJS模块在执行代码时根据需要加载依赖项。

它在上面的示例中没有任何区别,但请考虑以下ES2015模块代码:

// ES2015 modules

// ---------------------------------
// one.js
console.log('running one.js');
import { hello } from './two.js';
console.log(hello);

// ---------------------------------
// two.js
console.log('running two.js');
export const hello = 'Hello from two.js';

ES2015的输出:

running two.js
running one.js
hello from two.js

使用CommonJS编写的类似代码:

// CommonJS modules

// ---------------------------------
// one.js
console.log('running one.js');
const hello = require('./two.js');
console.log(hello);

// ---------------------------------
// two.js
console.log('running two.js');
module.exports = 'Hello from two.js';

CommonJS的输出:

running one.js
running two.js
hello from two.js

执行顺序在某些应用程序中可能至关重要,如果ES2015和CommonJS模块混合在同一个文件中会发生什么?要解决此问题,Node.js将仅允许具有扩展名.mjs文件中的ES6模块。带.js扩展名的文件默认为CommonJS。这是一个简单的选项,它消除了很多复杂性,应该有助于编码和编辑。

你应该在Node.js中使用ES6模块吗?

ES6模块仅适用于Node.js v10以后(2018年4月发布)。转换现有项目不太可能带来任何好处,并且会使应用程序与早期版本的Node.js不兼容。

对于新项目,ES6模块提供了CommonJS的替代方案。语法与客户端编码相同,可以提供更容易的同构JavaScript路径,可以在浏览器或服务器上运行。

模块混战

标准化的JavaScript模块系统需要很多年才能到达,甚至更长时间才能实现,但问题已得到纠正。所有主流浏览器和2018年中期的Node.js都支持ES6模块,尽管在每个人都升级时应该会出现切换延迟。

立即学习ES6模块,为明天的JavaScript开发做好准备。

参考

MDN JavaScript模块
MDN export
MDN import

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容