建構 Angular library 注意 ES2022 與 useDefineForClassFields
最近某個 Library 在升級至 Angular 15 後,開發階段正常,但建置(Build)後就會特定的變數初始化失敗,且連 constructor 都未被執行。
分析後其實很單純,倒是一連串誤會形成迷霧,過程中的發現還是有些說明與提醒的意義,所以整理了一下。
範例
為了方便說明,以下的簡單範例以最單純的方式呈現,模擬一個 Component
注入一個 Service
。
class Service {
public getName(): string {
return 'User';
}
}
class Component {
private greeting = 'Hi,' + this.service.getName();
constructor(
private service: Service,
) {}
}
敏感的讀者,也許已經覺得 greeting
這樣的宣告與賦值有些怪味道,確實是如此,但在我的案例中,升上 Anglar 15 前是可以運作;而升版之後,service
則不存在,無法從它身上執行 getName()
。
為什麼以前可以?
本文所謂的「以前」,即是未升上 Angular 15 前,當時 tsconfig.json
當中的 target
為 ES2020
。
我們可以直接在 TypeScript Playground 進行實驗,選擇 TypeScript 版本為 v.4.8.4,並且在 TS Config 設定 target 為 ES2020。(TS 版本其實可以用最新的 5.1.x,因為關鍵在於 target,這邊只是盡量還原過去)
在右側 .JS 區塊可看到 tsc 轉譯後的 JavaScript ,變數賦值都移到 constructor 內部來處理,十分和諧。
為什麼現在不行?
升上 Angular 15 後,Angular CLI 會將 tsconfig.json
的 target
更改至 ES2022
,並設定 useDefineForClassFields
為 false
(ref.)。依樣畫胡蘆,在 TS Playground 選擇 TypeScript 5.1.3,並設定 target
為 ES2022
。
然後,你就得到一樣的結果 (咦)。
嗯… 其實你還要再進到 TS Config 當中,把 Language and Environment 當中的 useDefineForClassFields
勾起來 (註)!
之後可以發現:
Class 直接存在兩個成員(屬性)的宣告,一是 service
,二是 greeting
,相信這邊可以看出問題所在,也能夠體會所謂的怪味道。
顯然 gretting
在初始化的當下就會遇到問題,因為 service 其實還不存在,就算我們還沒反應過來,編輯器 (如 VS Code、TS Playground) 也老早發出警告,會直接給予提示:Property ‘service’ is used before its initialization.(2729)。
註:依照官方文件的說明,設定欄位 useDefineForClassFields
在 target 為 ES2022
時,預設是 true
。不排除是 TS Playground 以 Checkbox 來設計,可能造成沒勾為 false;有勾為 true。但總之要透過它實驗與實現時,記得先調整。
如何解決?
可以在 tsconfig 當中,於 compilerOptions
將 useDefineForClassFields
設定 為 false
。
或是調整程式的寫法,這也有合理性,畢竟其他語言不見得像 TypeScript 有提供 Parameter Properties 特性,宣告變數並在 Constructor
當中賦值的寫法也是能理解的。
例如,若不使用 Parameter Properties,會寫成這樣:
class Component {
private greeting; // 宣告
constructor(
private service: Service,
) {
this.greeting = 'Hi,' + this.service.getName(); // 賦值
}
}
迷霧呢?
其實看完上面,倒沒什麼迷霧,因實際上產生迷霧的根源是「設定不一致」。
由於 Repo 起草的早,早在手動引入 ng-packagr 作為打包工具就開始運作著,隨時間產生設定不一致,且升版過程也不容易被察覺。若是以 Angular CLI 推出的新方式,即透過 ng g library
的方式在同一個 repo 當中管理多個 libs,或許能迴避此問題。
是怎麼樣的不一致呢?ng-packagr 是可以透過 -- config
參數傳入指定的 tsconfig,若沒有指定則使用它的預設。在過去是不需要特別傳入,但如今 ng-packagr 的預設值已變成 ES2022,並且無特別指定 useDefineForClassFields
(採預設 true
)。同時,我們的 Repo 卻將 useDefineForClassFields
設定為 false
,也因此:開發時沒有感覺、ng serve
沒感覺,建置 library 卻出錯了,就是因為如此--開發與建置的設定是不一致的。
小結
- 若您的 target 為
ES2022
,關心一下useDefineForClassFields
的值為何、留意開發工具給您的提醒。 - 若您使用 ng-packagr v16 以上(含),留意您是否需要傳遞 tsconfig 給它,因為它的
target
為ES2022
,並且沒有useDefineForClassFields
=false。