2015年10月29日,Sebastian McKenzie、James Kyle和Babel团队的其他成员为各地的前端开发者发布了一个巨大的主要版本。Babel 6.0.0.它非常棒。它不再仅仅是一个转码器,现在是一个超级可插拔的JavaScript工具平台。作为一个社区,我们只是触及了它的能力的表面,我对JavaScript工具的未来感到兴奋(和谨慎的乐观)。
综上所述,Babel 6.0.0是一个巨大的突破性变化。它的开始有点坎坷。它的升级并不完全是直接的,需要一些学习。这篇文章并不打算讨论如何升级Babel。我只是想谈谈当Babel修复了一个我非常依赖的错误时,我对自己的代码所了解的情况...在您尝试将您的东西从Babel 5升级到Babel 6之前,我推荐您查看一些资源。
ES6模块
如果我正确理解了ES6模块的规范,升级对我来说就不是那么困难了。Babel 5允许滥用导出和导入语句,Babel 6修复了这个问题。起初我以为这可能是一个bug。 我在Stack Overflow上问了这个问题,Logan Smyth告诉我,我从根本上误解了ES6模块,而Babel 5则促成了这种误解(写转码器很难)。
近乎中年的危机
起初,我不太明白罗根的意思,但当我有时间专门升级我的应用程序时,发生了这样的事情
Tyler McGinnis、Josh Manders和我在这个话题上来回讨论了很久。这可能很难理解,但这时我意识到问题不在于把对象作为默认值导出,而是我如何预期我可以导入该对象。
我一直以为我可以把一个对象导出为默认值,然后从该对象中解构出我需要的部分,就像这样。
// foo.js
const foo = {baz: 42, bar: false}
export default foo
// bar.js
import {baz} from './foo'
然而,根据规范,这在技术上是不正确的,这就是为什么Babel 6(正确地)取消了这一功能,并有效地破坏了我工作中应用的200多个模块。
当我看了Nicolás Bevacqua的博文后,我终于明白了事情的真正运作方式。
当我读到Axel Rauschmayer的博文时,我发现了为什么我一直在做的事情无法进行。
基本的想法是这样的。ES6模块应该是可静态分析的(运行时不能改变出口/进口),所以它不能是动态的。在上面的例子中,我可以在运行时改变foo对象的属性,然后我的导入语句可以导入该动态属性,像这样。
// foo.js
const foo = {}
export default foo
somethingAsync().then(result => (foo[result.key] = result.value))
// bar.js
import {foobar} from './foo'
我们假设result.key是 'foobar'。在CommonJS中,这可以很好地工作,因为require语句是在运行时发生的(当它们被要求时)。
// foo.js
const foo = {}
module.exports = foo
somethingAsync().then(result => result => (foo[result.key] = result.value))
// bar.js
const {foobar} = require('./foo')
然而,由于ES6规范规定,导入和导出必须是可静态分析的,你不能在ES6中完成这种动态行为。
所以这就是Babel改变的原因。现在不可能再这样做了,这是件好事。
这意味着什么呢?
事实证明,想出一个好的方法用散文来描述这个问题是很困难的,所以我希望一堆的代码例子和比较会有启发作用
我遇到的问题是,我把ES6的输出与CommonJS的需求结合起来。我想做的事情是这样的。
// add.js
export default (x, y) => x + y
// bar.js
const three = require('./add')(1, 2)
随着Babel的改变,我有三个选择。
**选项1:**使用默认的require
// add.js
export default (x, y) => x + y
// bar.js
const three = require('./add').default(1, 2)
**选项2:**100%的ES6模块
// add.js
export default (x, y) => x + y
// bar.js
import add from './add'
const three = add(1, 2)
**选项3:**100%的CommonJS
// add.js
module.exports = (x, y) => x + y
// bar.js
const three = require('./add')(1, 2)
我是怎么解决的呢?
几个小时后,我得到了构建运行和测试通过。我有两种不同的方法来应对不同的情况。
- 我把导出改为CommonJS*(module.exports*),而不是ES6*(export default*),这样我就可以继续要求它,因为我一直在做。
- 我做了一个花哨的 regex 查找和替换(应该用 codemod),将其他 require 语句从require('./thing')改为require('./thing').default。
这样做的效果很好。最大的挑战是理解ES6模块规范是如何工作的,以及Babel是如何将其移植到CommonJS中以实现互操作的。一旦我明白了这一点,更新我的代码以遵循这个惯例就很简单了。
建议
尽量避免混合ES6模块和CommonJS。我个人认为,所有的东西都用ES6模块就可以了。我把它们混在一起的原因之一是,我可以做一个单行的require,并立即使用所需的模块(比如require('./add')(1, 2))。但这真的不是一个足够大的好处,IMO。
如果你觉得你必须结合它们,你可以考虑使用以下babel插件/预设之一。
babel-plugin-add-module-exports
总结
从这一切中得到的真正教训是,我们应该学习事情应该如何运作。如果我能够理解ES6模块规范的实际工作原理,我就可以为自己节省大量的时间。
你可能会从我做的这个 egghead.io 课程中受益,该课程演示了如何从 Babel 5 升级到 Babel 6。
另外,请记住,人无完人,我们都在学习:-)在Twitter上见!
附录...
更多的例子。
在使用Babel的变化之前,require语句类似于。
import add from './add'
const three = add(1, 2)
但在Babel的改变之后,require语句现在变得更像。
import * as add from './add'
const three = add.default(1, 2)
给我造成问题的是,现在add变量不再是默认出口,而是一个拥有所有命名出口和默认export (在默认键下)的对象。
命名的出口。
值得注意的是,你也可以使用命名的导出,我建议在使用实用模块时这样做。这将允许你在import 语句中进行类似于析构的语法**(警告,尽管它看起来像,但由于前面提到的静态分析的原因,它实际上不是析构**)。所以你可以这样做。
// math.js
const add = (x, y) => x + y
const subtract = (x, y) => x - y
const multiply = (x, y) => x \* y
export {add, subtract, multiply}
// foo.js
import {subtract, multiply} from './math'
这在树的摇动下变得非常棒/令人兴奋。
就个人而言,我一般建议,对于一个组件(如React组件或Angular服务),你会想使用默认导出(你正在导入一个特定的东西,单一文件,单一组件,你知道😀)。但对于实用模块,你一般都有各种纯函数,可以独立使用。这就是命名导出的一个很好的用例。