Skip to main content

Babel 中的 TC39 标准跟踪装饰器

· 16 min read

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

🌐 Babel 7.1.0 finally supports the new decorators proposal: you can try it out by using the @babel/plugin-proposal-decorators plugin 🎉.

一点历史​

🌐 A Bit of History

装饰器最早是由耶胡达·卡茨在三年前提出来的首次提出。TypeScript在1.5版本(2015年)中引入了对装饰器的支持,同时还带来了许多ES6特性。一些主要的框架,比如Angular和MobX,开始使用它们来提升开发者体验:这让装饰器变得流行,也给社区带来了错误的稳定感。

🌐 Decorators were first proposed by Yehuda Katz more than three years ago. TypeScript released support for decorators in version 1.5 (2015) alongside with many ES6 features. Some major frameworks, like Angular and MobX, started using them to enhance their developer experience: this made decorators popular and gave the community a false sense of stability.

Babel 在 版本 5 中首次实现了装饰器,但在 Babel 6 中移除了它们,因为该提案仍在不断变化中。Logan Smyth 创建了一个非官方插件(babel-plugin-transform-decorators-legacy),它复刻了 Babel 5 的行为;后来在 Babel 7 的第一个 alpha 版本发布时,它已经被移到了官方 Babel 仓库。这个插件仍然使用旧的装饰器语义,因为当时新提案的具体内容还不明确。

🌐 Babel first implemented decorators in version 5, but removed them in Babel 6 because the proposal was still in flux. Logan Smyth created an unofficial plugin (babel-plugin-transform-decorators-legacy) which replicated the Babel 5 behavior; it has since then been moved to the official Babel repository during the first Babel 7 alpha release. This plugin still used the old decorators semantics, because it wasn't clear yet what the new proposal would have been.

从那时起,Daniel Ehrenberg 和 Brian Terlson 成为了和 Yehuda Katz 一起的提案共同作者,它已经几乎被完全重写。并不是所有的事情都已经决定,而且到今天为止还没有符合规范的实现。

🌐 Since then, Daniel Ehrenberg and Brian Terlson become co-authors of the proposal along with Yehuda Katz, it has been almost completely rewritten. Not everything has been decided yet, and there isn't a compliant implementation as of today.

Babel 7.0.0 在 @babel/plugin-proposal-decorators 插件中引入了一个新的标志:legacy 选项,它唯一有效的值是 true。这个重大变动是为了提供从提案的第一阶段版本到当前版本的平滑过渡路径。

🌐 Babel 7.0.0 introduced a new flag to the @babel/plugin-proposal-decorators plugin: the legacy option, whose only valid value was true. This breaking change was needed to provide a smooth transition path from the Stage 1 version of the proposal to the current one.

在 Babel 7.1.0 中,我们引入了对这个新提案的支持,并且在使用 @babel/plugin-proposal-decorators 插件时默认启用。如果我们在 Babel 7.0.0 中没有引入 legacy: true 选项,就无法默认使用正确的语义(这将相当于 legacy: false)。

🌐 In Babel 7.1.0 we are introducing support for this new proposal, and it is enabled by default when using the @babel/plugin-proposal-decorators plugin. If we didn't introduce the legacy: true option in Babel 7.0.0, it wouldn't be possible to use the correct semantics by default (which would be equivalent to legacy: false).

新的提案也支持在私有字段和方法上使用装饰器。我们还没有在 Babel 中实现这个功能(每个类中,你可以使用装饰器或私有元素之一),但它很快就会到来。

🌐 The new proposal also supports decorators on private fields and methods. We haven't implemented this feature yet in Babel (for each class, you can use either decorators or private elements), but it will come very soon.

新提议有什么变化?​

🌐 What Changed In The New Proposal?

虽然这个新提案看起来和旧提案很相似,但有几个重要的不同点,让它们互不兼容。

🌐 Even though the new proposal looks very similar to the old one, there are several important differences that make them incompatible.

语法​

🌐 Syntax

旧的提案允许任何有效的左侧表达式(字面量、函数和类表达式、new 表达式和函数调用、简单和计算属性访问)被用作装饰器的主体。例如,这段代码就是有效的:

🌐 The old proposal allowed any valid left-hand side expression (literals, function and class expressions, new expressions and function calls, simple and computed property accesses) to be used as the body of a decorator. For example, this was valid code:

JavaScript
class MyClass {
@getDecorators().methods[name]
foo() {}

@decorator
[bar]() {}
}

那个语法有个问题:[...] 表示法既被用在装饰器体内访问属性,又被用来定义计算属性名。为了防止这种歧义,新提案只允许点语法访问属性(foo.bar),末尾可以可选地加上参数(foo.bar())。如果你需要更复杂的表达式,可以用括号包起来:

🌐 That syntax had a problem: the [...] notation was used as both property access inside the decorator body and to define computed names. To prevent such ambiguity, the new proposal only allows dot property access (foo.bar), optionally with arguments at the end (foo.bar()). If you need more complex expressions, you can wrap them in parentheses:

JavaScript
class MyClass {
@decorator
@dec(arg1, arg2)
@namespace.decorator
@(complex ? dec1 : dec2)
method() {}
}

对象装饰器​

🌐 Object Decorators

旧版本的提案除了允许类和类元素的装饰器之外,还允许对象成员的装饰器:

🌐 The old version of the proposal allowed, in addition to class and class elements decorators, object members decorators:

JavaScript
const myObj = {
@dec1 foo: 3,
@dec2 bar() {},
};

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

🌐 Due to some incompatibilities with the current object literal semantics, they have been removed from the proposal. If you are using them in your code, stay tuned because they might be re-introduced in a follow-on proposal (tc39/proposal-decorators#119).

装饰器函数参数​

🌐 Decorator Functions Arguments

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

🌐 The third important change introduced by the new proposal is about the arguments passed to the decorator functions.

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

🌐 In the first version of the proposal, class elements decorators received a target class (or object), a key, and a property descriptor — similar in shape to what you would pass to Object.defineProperty. Class decorators took as their only argument a target constructor.

新的装饰器提案功能更强大:元素装饰器接收一个对象,除了可以修改属性描述符外,还可以改变元素的 key、位置(static、prototype 或 own)和类型(field 或 method)。它们还可以创建额外的属性,并定义一个在被装饰类上运行的函数(finisher)。

🌐 The new decorators proposal is much more powerful: element decorators take an object which, other than changing the property descriptor, allows changing the key, the placement (static, prototype or own), and the kind (field or method) of the element. They can also create additional properties and define a function (a finisher) which is run on the decorated class.

类装饰器接受一个包含每个类元素描述符的对象,从而可以在创建类之前修改它们。

🌐 Class decorators take an object which contains the descriptors of every single class element, making it possible to modify them before creating the class.

升级中​

🌐 Upgrading

鉴于这些不兼容性,现有的装饰器无法与新的提案一起使用:这会导致迁移非常缓慢,因为现有的库(如 MobX、Angular 等)在不引入破坏性更改的情况下无法升级。为了解决这个问题,我们发布了一个实用工具包,可以在你的代码中封装这些装饰器。运行它之后,你就可以安全地更改 Babel 配置,使用新的提案 🎉。

🌐 Given these incompatibilities, it is not possible to use existing decorators with the new proposal: this would make the migration very slow, since existing libraries (MobX, Angular, etc.) can't be upgraded without introducing breaking changes. To workaround this issue, we have published an utility package which wraps the decorators in your code. After running it, you can safely change your Babel config to use the new proposal 🎉.

你可以用一行命令来升级你的文件:

🌐 You can upgrade your files using a one-liner:

npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write

如果你的代码只在 Node 上运行,或者你正在用 Webpack 或 Rollup 打包代码,你可以通过使用外部依赖来避免在每个文件中注入封装函数:

🌐 If your code only runs in Node, or if you are bundling your code with Webpack or Rollup, you can avoid injecting the wrapper function in each file by using an external dependency:

npm install --save decorators-compat
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --external-helpers --write

想了解更多信息,你可以阅读软件包文档。

🌐 For more information, you can read the package documentation.

开放性问题​

🌐 Open Questions

还没有一切都定下来:装饰器是一个非常大的特性,要以最好的方式定义它们很复杂。

🌐 Not everything has been decided yet: decorators are a very big feature and defining them in the best possible way is complex.

导出类的装饰器应该放哪里?​

🌐 Where Should Decorators on Exported Classes Go?

tc39/proposal-decorators#69

关于这个问题,装饰器提案一直在反复讨论:装饰器应该放在 export 关键字前面还是后面?

🌐 The decorator proposal has gone back and forth on this question: should decorators come before or after the export keyword?

JavaScript
export @decorator class MyClass {}

// or

@decorator
export class MyClass {}

潜在的问题是 export 关键字是否是类声明的一部分,还是一个“封装器”。在第一种情况下,它应该放在装饰器 之后,因为装饰器位于声明的开头;在第二种情况下,它应该放在 之前,因为装饰器是类声明的一部分。

🌐 The underlying question is whether or not the export keyword is part of the class declaration or it is a "wrapper". In the first case it should come after decorators, since decorators come at the beginning of the declaration; in the second one it should come before, because decorators are part of the class declaration.

如何让装饰器安全地与私有元素互动?​

🌐 How to Make Decorators Securely Interact with Private Elements?

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

装饰器引发了一个重要的安全问题:如果可以装饰私有元素,那么私有名称(可以被看作是私有元素的“键”)可能会泄露。关于安全性,有几个不同的层次需要考虑:

  1. 装饰器不应该意外泄露私有名称。恶意代码不应该以任何方式从其他装饰器“偷取”私有名称。
  2. 只有直接应用于私有元素的装饰器才可能被认为是可信的:类装饰器是否不应该能够读取和写入私有元素?
  3. 严格隐私(类字段提案的目标之一)意味着私有元素只能在类内部访问:是否应该允许任何装饰器访问私有名称?是否应该只允许装饰公共元素?

🌐 Decorators give rise to an important security concern: if it is possible to decorate private elements, then private names (which can be considered as the "keys" of private elements) could be leaked. There are different safety levels to be thought of:

  1. Decorators should not accidentally leak private names. Malicious code should not be able to "steal" private names from other decorators, in any way.
  2. Only decorators directly applied to private elements might be considered trusted: should class decorators not be able to read and write private elements?
  3. Hard privacy (one of the goals of the class fields proposal) means that private elements should only be accessible from inside the class: should any decorator have access to private names? Should it be only possible to decorate public elements?

这些问题在解决之前需要进一步讨论,这就是 Babel 出场的地方。

🌐 These questions need further discussion before being resolved, and that's where Babel comes in.

Babel的角色​

🌐 The Role of Babel

延续《管道提案发生了什么(|>)》文章中的趋势,随着 Babel 7 的发布,我们开始利用自己在 JS 生态系统中的地位,进一步帮助提案作者,让开发者能够测试并反馈不同版本的提案。

🌐 Following the trend in the What's Happening With the Pipeline (|>) Proposal? article, with the Babel 7 release we are starting to use our position in the JS ecosystem to help proposal authors even more, by giving developers the ability to test and give feedback about different variations of the proposals.

因此,随着 @babel/plugin-proposal-decorators 的更新,我们引入了一个新选项:decoratorsBeforeExport,它允许用户同时体验 export @decorator class C {} 和 @decorator export default class。

🌐 For this reason, alongside with the update of @babel/plugin-proposal-decorators we introduced a new option: decoratorsBeforeExport, which allows users to try both export @decorator class C {} and @decorator export default class.

我们还将引入一个选项,允许自定义被装饰的私有元素的隐私限制。在 TC39 的成员对其做出决定之前,这些选项将是必需的,这样我们就可以让默认行为符合最终提案规定的内容。

🌐 We will also introduce an option to customize the privacy constraint of decorated private elements. These options will be required until TC39 folks make a decision about them, so that we can let the default behavior be whatever the final proposal will specify.

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

🌐 If you are directly using our parser (@babel/parser, formerly babylon) you can already use the decoratorsBeforeExport option in version 7.0.0:

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

用法​

🌐 Usage

在 Babel 本身中的用法:

🌐 For usage in Babel itself:

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

查看 @babel/plugin-proposal-decorators 文档以获取更多选项。

🌐 Check the @babel/plugin-proposal-decorators docs for more options.

你的角色​

🌐 Your Role

作为一名 JavaScript 开发者,你可以帮助勾勒这门语言的未来。你可以测试正在考虑用于装饰器的各种语义,并向提案作者提供反馈。我们需要知道你在真实项目中是如何使用它们的!你还可以通过阅读提案仓库中的问题讨论和会议记录,了解一些设计决策是如何做出的。

🌐 As a JavaScript developer, you can help outline the future of the language. You can test the various semantics which are being considered for decorators, and give feedback to the proposal authors. We need to know how you are using them in real-life projects! You can also find out why some design decisions were taken by reading the discussions in the issues and the meeting notes in the proposal's repository.

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

🌐 If you want to try out decorators right now, you can play with the different presets options in our repl!