TC39标准跟踪巴别尔的装饰者

205 阅读7分钟

TC39标准跟踪Babel中的装饰器

Babel 7.1.0终于支持新的装饰器提案:你可以通过使用 @babel/plugin-proposal-decorators插件🎉。

一段历史

装饰器是三年多前由Yehuda Katz 首次提出的。TypeScript在1.5版本(2015年)发布了对装饰器的支持,同时也发布了许多ES6功能。一些主要的框架,如Angular和MobX,开始使用它们来增强他们的开发者体验:这使得装饰器流行起来,并给社区一种错误的稳定感。

Babel在第5版中首次实现了decorators,但在Babel 6中删除了它们,因为该提议仍在变化中。Logan Smyth创建了一个非官方的插件(babel-plugin-transform-decorators-legacy),它复制了Babel 5的行为;此后,在Babel 7的第一个alpha版本中,它被移到了Babel的官方仓库。这个插件仍然使用旧的装饰器语义,因为当时还不清楚新的建议会是什么。

从那时起,Daniel EhrenbergBrian TerlsonYehuda Katz一起成为该提案的共同作者,它几乎被完全重写了。不是所有的事情都已经决定了,而且到今天为止还没有一个符合要求的实现。

Babel 7.0.0 为@babel/plugin-proposal-decorators 插件引入了一个新的标志:legacy 选项,其唯一有效的值是true 。为了提供一个从提案的第一阶段版本到当前版本的平稳过渡路径,需要做出这一突破性的改变。

在Babel 7.1.0中,我们引入了对这个新建议的支持,在使用@babel/plugin-proposal-decorators 插件时,它被默认为启用。如果我们没有在Babel 7.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() {},
};

由于与当前的对象字面语义有一些不兼容,它们已经从提案中删除。如果你在你的代码中使用它们,请继续关注,因为它们可能会在后续的提案中重新引入(tc39/proposal-decorators#119)。

装饰器函数参数

新提案引入的第三个重要变化是关于传递给装饰器函数的参数。

在提案的第一个版本中,类元素装饰器接收一个目标类(或对象)、一个键和一个属性描述符--类似于你传递给Object.defineProperty 的形状。类的装饰器只接受一个目标构造函数作为其参数。

新的装饰器建议更加强大:元素装饰器接受一个对象,除了改变属性描述符之外,还允许改变元素的键、位置(static,prototypeown )和种类(fieldmethod )。他们还可以创建额外的属性,并定义一个函数(终结者),在被装饰的类上运行。

类装饰器采取一个对象,其中包含每个单一的类元素的描述符,使得在创建类之前修改它们成为可能。

升级

鉴于这些不兼容性,不可能在新方案中使用现有的装饰器:这将使迁移非常缓慢,因为现有的库(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

更多信息,你可以阅读包的文档

开放的问题

不是所有的事情都已经决定了:装饰器是一个非常大的功能,以最好的方式定义它们是很复杂的。

出口类上的装饰器应该去哪里?

tc39/proposal-decorators#69

装饰器提案在这个问题上反反复复:装饰器应该放在导出关键字之前还是之后?

export @decorator class MyClass {}

// or

@decorator
export class MyClass {}

基本问题是export 关键字是否是类声明的一部分,或者它是一个 "包装器"。在第一种情况下,它应该装饰器之后,因为装饰器是在声明的开头;在第二种情况下,它应该在前面,因为装饰器是类声明的一部分。

如何使装饰器安全地与私有元素交互?

tc39/proposal-decorators#129,tc39/proposal-decorators#133

装饰器引起了一个重要的安全问题:如果有可能装饰私有元素,那么私有名称(可被视为私有元素的 "钥匙")就可能被泄露。有不同的安全级别需要考虑。

  1. 装饰者不应该意外地泄露私有名称。恶意代码不应该能够以任何方式从其他装饰器中 "偷 "出私有名称。
  2. 只有直接应用于私有元素的装饰器才可能被认为是可信的:类装饰器不应该能够读写私有元素吗?
  3. 硬隐私(类领域提案的目标之一)意味着私有元素应该只能从类内部访问:任何装饰者都应该能访问私有名称吗?是否应该只对公共元素进行装饰?

这些问题在解决之前需要进一步讨论,而这正是Babel的作用。

巴别尔的作用

顺着 "管道(|>)提案发生了什么?"一文的趋势,随着Babel 7的发布,我们开始利用我们在JS生态系统中的地位来帮助提案作者,让开发者能够测试和反馈提案的不同变化。

出于这个原因,在更新@babel/plugin-proposal-decorators 的同时,我们引入了一个新的选项。decoratorsBeforeExport,允许用户同时尝试export @decorator class C {}@decorator export default class

我们还将引入一个选项来定制装饰过的私有元素的隐私约束。这些选项将是必需的,直到TC39的人们对它们做出决定,这样我们就可以让默认行为成为最终提案将指定的任何内容。

如果您直接使用我们的解析器(@babel/parser, 以前是babylon),你已经可以在7.0.0版本中使用decoratorsBeforeExport 选项了。

const ast = babylon.parse(code, {
  plugins: [
    ["decorators", { decoratorsBeforeExport: true }]
  ]
})

使用方法

用于Babel本身的使用。

npm install @babel/plugin-proposal-decorators --save-dev
{
  "plugins": ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
}

查看 @babel/plugin-proposal-decoratorsdocs了解更多选项。

你的角色

作为一个JavaScript开发者,你可以帮助勾勒语言的未来。您可以测试各种正在考虑的装饰器的语义,并向提案作者提供反馈。我们需要知道你是如何在现实生活中使用它们的你也可以通过阅读问题中的讨论和提案库中的会议记录来了解为什么要做出一些设计决定。

如果你现在就想尝试装饰器,你可以在我们的副本中玩不同的预设选项