【翻译】babel对TC39装饰器草案的实现
原文地址:https://babeljs.io/blog/2018/...
原文作者:Nicolò Ribaudo
Babel 7.1.0最终支持新的装饰器提案,可以通过@babel/plugin-proposal-decorators
插件使用。
历史
装饰器这个概念三年多前被Yehuda Katz首次提出。TypeScript在版本1.5(2015年)中发布了对装饰器的支持以及许多ES6特性。很多主流框架,像Angular和MobX,为了提高开发体验也开始使用装饰器。这些使装饰器变得很流行,并且给了社区一种很稳定的错觉。
Babel在版本5里面首次实现了装饰器,但在版本6的时候移除了,因为提案在不断的变化。Logan Smyth创建了一个非官方的插件(babel-plugin-transform-decorators-legacy
)来代替Babel5里面的装饰器,在第一个Babel7 alpha版本发布的时候的时候,它被移到了Babel官方的存储库。这个插件还是使用老版本的插件语法,因为还不清楚新的提案会变成什么样。
从那个时候开始,Daniel Ehrenberg和Brain Terlson和Yehuda Katz一起成为了提案的作者,提案几乎完全被重写了。并非所有的事情都已经确定,而且目前也没有合规实施的方案。
Babel7.0.0为@babel/plugin-proposal-decorators
插件介绍了一个新的标志:配置项legacy的唯一有效值为true。为了从提案的第一阶段平滑过渡到当前版本,需要有这种重大的改变。
在Babel7.1.0,我们引入了对这个新提案的支持,并且在使用@babel/plugin-proposal-decorators
插件的时候会默认启用。如果我们不在Babel7.0.0里引入配置项legacy为true的话,在默认情况下就不可能使用正确的语义(也就意味着配置项legacy的值为false)
新的提案还支持私有字段和方法上的装饰器。我们还没有在Babel中实现这个功能(对于每个类而言,你可以使用装饰器或者私有元素),但很快就会实现的。
新的提案的改变点
尽管新的提案看上去跟旧的很相似,但还是有一些重要的不同点。
语法
旧的提案允许任何有效的左侧表达式(文字、函数和类表达式,new表达式和函数调用,简单和计算属性访问)作为装饰器的主体:
class MyClass { @getDecorators().methods[name] foo() {} @decorator [bar]() {} }
这个语法有一个问题:[...]这个符号在装饰器里进行属性访问以及定义计算属性名字的时候也会被用到。为了消除这个歧义,新的提案值允许用点符号来进行属性访问(foo.bar),也可以在最后加上一个括号(foo.bar())。如果你需要更多复杂的表达式,你可以用括号括起来:
class MyClass{ @decorator @dec(arg1, arg2) @namespace.decorator @(complex ? dec1 : dec2) method() {} }
对象装饰器
旧版本的提案允许出现除了类和类元素装饰器之外的对象成员装饰器:const myObj = { @dec1 foo: 3, @dec2 bar() {}, };
由于跟当前对象的一些表达语法的不兼容性,在提案中被移除了。如果你在你的代码中使用了对象成员装饰器,继续关注因为它们可能会在后续提案中被引入。
- 函数装饰器的参数
新提案引入的第三个重要变化是关于传递给装饰器函数的参数。在第一版提案中,类元素装饰器接受一个目标类(对象),一个变量,和一个属性描述符-类似于传递给
Object.defineProperty
的参数。类装饰器将目标构造函数作为唯一的参数。新的提案的装饰器更强大一些:元素装饰器接受一个对象,该对象除了更改属性操作符之外,还允许更改变量值,位置(
static
、prototype
或者own
)以及元素的种类(field
或method
)。他们还可以创建其他的属性并定义运行在类装饰器里的函数。
类装饰器接受一个包含每个类元素的描述符的对象,从而保证可以在创建类之前修改它们。 升级
由于这些不兼容性,不能在现有的装饰器上使用新的提案:这会让升级特别慢,因为现有的库(MobX,Angular等)不能再没有引入重大改变的情况下进行升级。为了解决这个问题,我们已经发布了一个实用程序包,它将装饰器包装在你的代码里。运行这个之后,你可以安全地修改你的Babel配置以使用新的提案。
你可以使用下面这行代码去升级文件:npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write
如果你的代码只在Node中运行,或者你用Webpack或Rollup打包你的代码,你可以使用外部依赖来避免在每个文件中都注入包装函数:
npm install --save decorators-compat npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --external-helpers --write
开放问题
并非所有的事情都已经确定:装饰器是一个非常大的功能,而且要以最好的方式定义它们是非常复杂的。
导出类的装饰器应该放在哪里
这个问题在装饰器的提案里反复出现:装饰器应该在export这个关键字的前面还是后面?export @decorator class MyClass {} // or @decorator export class MyClass {}
根本问题是export关键字是否是类声明的一部分,还是只是一个“包装器”。如果是前一种情况,它应该放在装饰器的后面,因为装饰器出现在声明的开头;在第二种情况下,它应该在装饰器前面,因为装饰器是类装饰器的一部分。
如何让装饰器和私有元素安全地互动?
装饰器引起了重要的安全问题:如果可以装饰私有元素,那么私有名称(也可以称为私有属性的变量名)可能会被泄漏。有不同的安全级别需要考虑:
1) 装饰器不应该意外泄漏私有名称。恶意代码不应该以任何方式从其他装饰器中“窃取”私有名称。
2) 只有直接应用于私有元素的装饰器才能被视为可信任:类装饰器应该无法读写私有元素?
3) 硬私有(类字段提案的目标之一)意味着私有元素应该只能有类的内部访问:任何装饰器是否可以访问私有名称?装饰器只能装饰公共元素么?
这些问题需要进一步讨论才能解决,这也是Babel的用武之地。Babel的作用
随着What's Happening With the Pipeline(|>) Proposal?这篇文章里的趋势,随着Babel7的发布,我们开始利用我们在JS生态系统中的位置,通过让开发人员测试和反馈有关提案的不同版本的体验来帮助提案的提出者们。
由于这个原因,在@babel/plugin-proposal-decorators更新的同时,我们也引入了一个新的属性:decoratorsBeforeExport
,允许用户同时使用export @decorator class C {}
和@decorator export default class
。
我们也将引入一个属性来自定义私有属性装饰器的隐私约束。在TC39人员做出决定之前,这些属性是必需的,这样我们可以让默认行为成为最终提案知道的内容。
如果你直接使用我们的解析器(@babel/parse,以前的babylon),你已经可以在版本7.0.0里使用decoratorsBeforeExport属性:const ast = babylon.parse(code, { plugins: [ ["decorators", { decoratorsBeforeExport: true }] ] })
用法
Babel用法:
shell版本:npm install @babel/plugin-proposal-decorators --save-dev
JSON版本:
{ "plugins": ["@babel/plugin-proposal-decorators", {"decoratorsBeforeExport": true }] }
查看@babel/plugin-proposal-decorators文档了解更多属性。
你的作用
作为JavaScript的开发人员,你可以帮助概述该语言的未来。你可以测试装饰器的新语法,并向提案的作者们提出反馈意见。我们需要知道你在现实生活的项目中是怎么使用它们的。你也可以通过阅读问题中的讨论和proposal's repository中的笔记中发现为什么要这样设计。
如果你想要立即尝试装饰器,你可以在我们的repl里使用不同的预设属性值。