ES6模块,Node.js和Michael Jackson Solution

Posted 奇舞周刊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6模块,Node.js和Michael Jackson Solution相关的知识,希望对你有一定的参考价值。

编者按:本文由MVM在众成翻译平台上翻译。

javascript没有一个标准的方法,可以将一个功能从一个文件中导入或导出到另一个文件。好在有全局变量。例如:

<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script>

<script>

// `$` variable available here

</script>

这种方式远谈不上完美,因为它可能导致一些问题:

  • 如果你使用的其他库里面使用了相同的变量,你的代码可能会与之产生冲突。这就是为什么许多库都有一个noConflict()方法。

  • 你无法实现循环引用。如果模块A和模块B互相依赖,我们要以怎样的顺序来放置<script>标签呢?

  • 即使代码中不存在循环引用,你放置<script>标签的顺序也非常重要,并且这种书写方式会导致以后维护起来特别困难

CommonJs来帮忙

后来Node.js和其他服务器端的JavaScript解决方案开始出现,他们商定一个办法来解决这个问题。他们制定了一个叫做CommonJS的规范。为了解决导入和导出的问题,该规范定义了一个在运行时期被注入的require()函数,同时定义了一个exports变量来导出功能。

注: CommonJs并不是唯一的规范。除此之外还有一种不仅可以在前端还可以在后端使用的规范 UMD

时过境迁,前端工具大井喷,其中不乏针对单页应用(SPA)的构建。随着前端代码量与日俱增,加之前后端代码共享的趋势,前端代码缺乏模块化管理的劣势愈发明显,特别是在浏览器端。随之而来诞生了以browserify 和  webpack 为代表的工具。他们利用CJS的规范,巧妙的弥补了以上缺陷:平台(JS和浏览器)缺少一个好的模块系统。

这种方式是一种完完全全的hack。因为浏览器并没有实现require()或者exports,那些工具所做的只是通过某种方式把代码打包到一起。如果想了解更多,请移步 how JavaScript bundlers work

ES6模块是如何工作的,为什么Node.js还没有实现它?

随着JavaScript的发展,这个问题终于在ES6中得到了解决。这就是ES模块的由来,它在语法是和CJS类似。

现在让我们来比较两者。两者导入功能的方式分别如下:

const { helloWorld } = require('./b.js') // CommonJS

import { helloWorld } from './b.js' // ES modules

导出功能的方式如下:

// CommonJS

exports.helloWorld = () => {

  console.log('hello world')

}

// ES modules

export function helloWorld () {

  console.log('hello world')

}

很类似,不是吗?

Node已经实现了ECMAScript 2015(即ES6)99%的特性,但是对modules的支持,则预计要到2017年底才会完成,而且需要手动开启(译者注: 现已支持)。为什么在ES6模块与CJS如此类似的情况下,Node.js花了如此长的时间才支持ES6模块呢?

因为,关键问题出现在在细节方面。两个系统的语法非常类似,但是语义是完全不同的。在一些细节方面需要特殊的处理来实现100%的规范方面的兼容。

即使在Node.js没有支持ES模块的时候,一些浏览器已经实现了对于ES模块的支持。比如:你可以在Safari 10.1上进行测试。下面让我们来看一些例子。通过这些例子我们将会了解为什么语义十分重要。首先,创建以下三个文件。

// index.html

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

// a.js

console.log('executing a.js')

import { helloWorld } from './b.js'

helloWorld()

// b.js

console.log('executing b.js')

export function helloWorld () {

  console.log('hello world')

}

当文件执行的时候,我们会在浏览器的控制台看到以下结果:

executing b.js

executing a.js

hello world

然而,在Node.js中使用CJS语法执行相同的代码:

// a.js

console.log('executing a.js')

import { helloWorld } from './b.js'

helloWorld()

// b.js

console.log('executing b.js')

export function helloWorld () {

  console.log('hello world')

}

控制台显示的结果却是:

executing a.js

executing b.js

hello world

所以...相同的代码执行的顺序却不一样!这是因为ES模块首先解析代码(并不会直接执行),其次runtime查找imports并且加载他们,最后再执行代码。这种方式被称为异步加载。

另一方面,Node.js在执行代码的时候才会加载所需的依赖项(requires)。这两种执行方式不同。虽然在某些情况下没什么区别,但是在其他情况下表现是完全不同的。

Node.js和web浏览器需要以第一种方式来实现代码加载。但是它们如何确定使用哪种系统对应的方式呢?浏览器知道,因为你可以在<script>标签上指定,正如我们下面例子看到的type属性。

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

然而,Node.js是如何得知的呢?关于这个有许多讨论和建议(首先检查语法,然后决定是否将它视为一个模块?在package.json中直接定义它?...)。最终,决定的方案是:Michael Jackson Solution。基本上,如果你想一个文件作为ES6模块来加载,就使用一个不同的文件扩展名:.mjs来替代.js

这个扩展名(.mjs)就是为什么这个方案被称为Michael Jackson Solution的原因。

在一开始,这种方式看起来貌似是一个很差的决定,但是现在我认为它是一个很棒的解决方案。因为它非常简单并且其它工具(text editor,IDE,preprocessor)都可以很方便的知道是否一个文件需要被视为一个ES6模块。同时在加载工程方面,这种方式增加的开销最小。

如果你想了解更多Node.js中ES6模块的实现程度,你可以阅读this update

关于Babel的一个提示

Bable实现了ES6模块,但是准确上来说,它没有实现所有的规范。如果你正在使用Babel来转义一个原生的ES6模块的时候,请当心,这可能会有某些副作用。

为什么ES6模块是好的以及如何实现两全其美的效果呢

ES6模块有以下两个最主要的优点:

  • 它们是跨平台的,无论在浏览器还是Node.js中都可以正常执行。

  • import 和 export 都是静态方法,只有这么实现我们才能知道依赖载入是如何工作的。因为 runtime 会先载入文件,解析它然后我们需要在执行之前载入依赖,只有将它们实现成静态方法才能做到。意味着你不能使用import 'engine-' + browserVersion这种语法。这种方式有一个好处:工具可以静态分析代码,找出哪一部分代码确实被使用了然后按需加载这部分代码(tree shake it)。当在使用第三方库的时候这是非常有用的:你不可能使用它们提供的所有方法,所以你可以删除许多没有执行的代码。

但是,这意味这我们没有办法来异步引入某项功能了吗?对我来说,这种方式是很有用处的。许多情况下我都像下面这样来做一些事情:

const provider = process.env.EMAIL_PROVIDER

const emailClient = require(`./email-providers/${provider}`)

通过这种方式,我可以在改变配置的情况下获得相同的接口的不同实现,而不必加载所有实现的代码。

所以如果使用ES6模块会发生什么呢?不用担心,有一个处于stage-3(意味着它很可能在不久后获得批准)的提案,这个提案添加了一个import() function。这个方法接受一个路径然后会以promise的方式来导出功能。

所以通过ES6模块和import(),我们将实现两全其美的效果。

ES6模块是很棒的,但是接受它可能需要花些时间。希望这篇文章的内容能帮助你做好准备!


奇舞周刊

——————————————————

领略前端技术 阅读奇舞周刊


长按二维码,关注奇舞周刊


以上是关于ES6模块,Node.js和Michael Jackson Solution的主要内容,如果未能解决你的问题,请参考以下文章

如何欺骗 Node.js 将 .js 文件加载为 ES6 模块?

Node.js 的 commonJS 规范 ES6 导入 js 文件

Node.js 的 commonJS 规范 ES6 导入 js 文件

使用Node.js需要与ES6导入/导出

Node.js 全球化 es6 模块以像 ImportScripts 一样工作

使用 ES6 模块时 Node.js 中 __dirname 的替代方案