[譯]在 Babel 中支援 TC39 標準的裝飾器

QCLee發表於2019-01-09

Babel7.jpg

歡迎拍磚,協助我們更好的建設中文社群。

Babel 7.1.0 最終支援了新的裝飾器提案:你可以使用 @babel/plugin-proposal-decorators 外掛來提前嘗試此功能 ?。

相關歷史

三年多以前,Yehuda Katz 首先提出了裝飾器的概念。TypeScript 在 1.5 版本(2015)中釋出了對裝飾器的支援以及許多 ES6 的相關特性。 一些主流框架,如 Angular 和 MobX 等開始使用它們來增加開發者體驗:這使得裝飾器非常受歡迎,並給社群帶來了一種已經穩定的錯覺。

Babel 第一次實現裝飾器是在 v5 版本中,但由於該提案仍在不斷變化,則在 Babel v6 中移除了它們。Logan Smyth 建立了一個非官方的外掛(babel-plugin-transform-decorators-legacy),它延用了 Babel 5 中裝飾器的行為;在 Babel 7 的 alpha 版本釋出期間該庫被移至 Babel 官方的倉庫中。當時該外掛仍使用舊的裝飾器語法,因為新提案尚未明確。

自那時起,Daniel EhrenbergBrian Terlson 以及 Yehuda 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)。

新提案同時支援使用裝飾器實現私有欄位(private fields)和私有方法(private methods)。我們尚未在 Babel 中實現此功能(在每個 class 中使用裝飾器或私有元素),但我們會很快去出現它。

新提案有何變化?

儘管新提案看起來與舊提案非常相似,但還是有幾個重要的差異使得它們互不相容。

語法

舊提案允許任何有效的左表示式(字面量,函式,類表示式,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

裝飾器函式相關引數

新提案提出的第三個重要變化與傳遞給裝飾器函式引數相關。

在提案的第一個版本中,類元素裝飾器接收的引數分別為目標類(或物件),key 以及屬性描述符 - 與傳遞給 Object.defineProperty 的形式類似。類裝飾器將目標建構函式(constructor)作為唯一引數。

新的裝飾器提案更加強大:元素裝飾器會接收一個物件,該物件除更改屬性描述符外,還允許更改 key 值,可以賦值(staticprototype 或者 own),以及元素的型別(fieldmethod)。它們還可以建立其他屬性並在裝飾類上定義執行函式(完成器)。

類裝飾器接收一個包含類描述符的物件,使得類在建立之前修改它們成為可能。

升級

鑑於這些不相容性問題,新提案中不可能使用現有的裝飾器:這將使得遷移變得緩慢,因為現有庫(MobX,Angular等)無法在不引入這些突破性變化的情況下進行升級。 為解決此問題,我們釋出了實用工具包,它將裝飾器包裝在你的程式碼當中。執行後, 你可以安心的更改你的 Babel 配置以便使用新提案 ?。

使用如下命令來升級你的檔案:

npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write
複製程式碼

如果你的程式碼僅在 Node 中執行,或者你會使用 webpack 或 rollup 構建你的程式碼,則需要使用外部依賴項(external dependency),避免在每個檔案中注入包裝函式:

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

欲瞭解更多資訊,請參閱工具包(package)相關文件.

開放問題

並非所有內容都已確定:裝飾器是一個非常強大的功能,想要將裝飾器定義為最好的表現形式,是相當複雜的。

匯出類(exported class)裝飾器如何放置?

tc39/proposal-decorators#69

該問題在裝飾器提案中反覆被問到:裝飾器應該放置在關鍵字 export 前還是關鍵字 export 後?

export @decorator class MyClass {}

// 或者

@decorator
export class MyClass {}
複製程式碼

根本問題是 export 關鍵字是否是類宣告的一部分,或者它是否是一個"包裝器"。第一種情況下,它應該在裝飾器之後,因為裝飾器出現在宣告的起始位置;第二種情況下,它應該在裝飾器之前,因為裝飾器是類宣告的一部分。

如何讓裝飾器與私有元素安全地互動?

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

裝飾器引發了一個重要的安全隱患:如果裝飾私有元素,那麼私有名稱(可以視為私有元素的 "key")可能會被洩露。有不同的安全級別需要考慮:

  1. 裝飾器有意外洩露私有名稱的風險。惡意程式碼不應該以任何方式從其他裝飾器中"竊取"私有名稱。
  2. 只有直接應用於私有元素的裝飾器才被視為可信任:類裝飾器是不是不應該讀寫私有元素?
  3. 高度隱私 (class fields 提案的目標之一) 意味著私有元素只能從類內部訪問:是否需要讓任何裝飾器都可以訪問私有名稱?是否應該只裝飾公共元素?

這些問題需要在解決之前進一步討論,這正是 Babel 所存在的意義。

Babel 的作用

遵循 What's Happening With the Pipeline (|>) Proposal? 文章中的走向,隨著 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-decorators 文件以獲取更多相關選項。

你的作用

作為 JavaScript 開發者,你可以幫助規劃改語言的未來。你可以為裝飾器考慮各種語義環境同時進行測試,並向提案的作者提出反饋。我們需要知道你在真實專案環境中是如何使用它們的!你還可以通過閱讀提案倉庫中的 issues 討論及會議記錄來找出為什麼最終做出這樣的設計決策。

如果想立即嘗試裝飾器,可以使用我們的 repl 配置不同的 preset 選項進行試用!

關注我們

掃碼關注我們的公眾號,我們會定期推送一下社群相關的文章和動態。

印記中文

相關文章