恩... 照常 Day 31. 繼續。
本系列進入到第三部分:《戰線擴張》篇(The Front Line Expansion)
筆者就開始進行算是專屬於本系列的延長賽篇章,ㄧ樣會帶領讀者遍歷 TypeScript 各種很有趣的功能與應用~
以下大概筆者粗估會在本篇章提到的內容:
tsconfig.json
與 tsc
指令更多的功能看起來很少對不對~然而,筆者難免會延伸出東西,照樣不太確定會出多少篇章。(原本預估五、六篇左右應該就可以結束掉這個篇章?不過可能會寫到十篇也不知道~那就算了~寫就寫吧~)
貼心小提示
就在前幾天,筆者確定第三篇章總共會有 11 篇,延伸出來的東西還有:
- Webpack 環境的建立
- 外觀模式 Façade Pattern
另外,筆者脫稿演出的機會很大(它 X 是有多喜歡寫?),不一定會按照以上的順序講解,反而是漸進式的介紹功能 —— 介紹到哪就往哪裡跑的概念。
那麼我們就正式進入《戰線擴張》系列~
正文開始!
tsconfig.json
Revisited讀者應該看過本系列後,每一次建構簡單的小專案時,就會需要呼叫:
$ tsc --init
它就會幫我們建立一個名為 tsconfig.json
的檔案。其中,筆者就把 tsconfig.json
大致上長的樣子截下來給大家看。
讀者一定覺得:“tsconfig.json
密密麻麻的,筆者該不會是要全講?”
當然是不太可能,但筆者會特別挑選必須具備的編譯器選項功能,其他可以參考 TypeScript 官方的文件:Compiler Options —— 對於編譯器的任何設定寫得非常詳盡,筆者認為本身就很足夠。
另外,還有這一篇 —— 【Day 03】 TypeScript 編譯設定 - tsconfig.json
by Kira 大大 已經將常見的 tsconfig.json
裡的 compilerOptions
甚至是也有把筆者沒有寫到關於 tsconfig.json
的東西也補足了,特地推薦給讀者服用。
本系列筆者應該只會講自己會用到的東西,因此筆者將所有的編譯器設定粗略地分成四類:
貼心小提示
這只是筆者為了學習方便而自己定的分類方式,事實上在
tsconfig.json
裡的註解部分就有更進階的分類,但筆者在此不贅述 —— 本系列目標就是:能夠理解、靈活應用就好了,不需要背一些沒用到的東西,除非自己未來要用時,再查就 OK 囉。
target
設定筆者首先要介紹的是 target
這個屬性 —— 代表 TypeScript 專案要被編譯到的目標 JS 版本,包含 ES3
、ES5
、ES6
(或 ES2015
)以及 ES2016
以上。
其中,官方預設的編譯結果為 ES3
版本,至於 ES5
與 ES3
版本的差別,可以參考這篇 StackOverflow,筆者實際上也不太清楚差別,但鑿於本人真的覺得沒必要理解(除非是興趣使然吧),於是就跳過了。
話說,如果讀者想要用 tsc
並且註明要編譯的設定,格式如下:
$ tsc --<config-1> <config-1-value> --<config-2> <config-2-value> // ...
以 target
屬性為例,可以這樣使用:
$ tsc --target ES5
以下筆者編譯出不同版本的程式碼給讀者看看。(es5
編譯結果如圖二;es6
編譯結果如圖三)
圖二:以上是 es5
編譯之結果,有興趣的讀者可以試試看使用 es3
版本編譯結果 —— 會跟 es5
結果差不多ㄧ樣
圖三:es6
版本的編譯結果,有編譯沒編譯差不了多少的感覺
讀者也可以發現,TypeScript 經過編譯成原生的 JS 時,會把型別系統的註記拔除。(這句應該是廢話,不過筆者姑且提醒一下)
lib
設定有些初期使用 TypeScript 的讀者會發現一件很莫名其妙的事情。(以下程式碼的警告訊息如圖四)
圖四:Object.assign
竟然不能用?
首先,Object.assign
屬於 ECMAScript 2015(ES6)的語法之一。(如圖五)
圖五:Object.assign
為 ES6 的語法,可以參考 caniuse.com 這個網站查詢
所以這時候 lib
設定就派上用場啦!如果讀者去查詢官方的 Compiler Options,會發現 lib
可以接收的值特別多。(如圖六)
圖六:筆者的電腦螢幕太小,整個畫面沒辦法截,由此可知 lib
裡面選項多到炸
我們必須告訴 TypeScript 專案 —— 我們需要 ES6 的 Feature,於是在 tsconfig.json
裡面,lib
選項更改為 ["es2015"]
,然後錯誤訊息就會不見。(更改前如圖七;更改後如圖八)
圖七:讀者仔細看,沒有將 "lib"
選項開啟,Object.assign
部分會出現警告訊息
圖八:新增 "lib": ["es2015"]
後,Object.assign
的警告訊息立馬消失
筆者再舉另外一個常見案例。首先我們先讓 "lib"
選項被註解掉 —— 其中,window
這個瀏覽器很常見的物件是可以被使用的。(如圖九)
圖九:window
物件可以被 TypeScript 認得
由於剛剛為了使 TypeScript 專案支援 ES6
的語法,因此我們必須將 "lib"
設定為 ["es2015"]
才能使用;然而,如果真的使 "lib"
開啟並設定為 ["es2015"]
的話,又會發生錯誤!(如圖十)
圖十:window
被 TypeScript 視為 any
,根本不存在
什麼!?剛剛 TypeScript 不是認得 window
這個東西嗎?
其實,window
的預設型別定義部分是被放在名為 dom
的選項,也是屬於 lib
設定當中的其中一項。所以如果改成,"lib": ["es2015", "dom"]
—— window
物件又會再度被 TypeScript Recognized!(圖十一)
圖十一:原來 window
物件是由 dom
選項控制的啊
其中,lib
選項提供的功能,就是負責補足 —— 在 TypeScript 裡所謂的 Declaration Group —— 宣告性群組的東西。
讀者可以理解成:dom
裡面含有的是 window
或 document
那一類的宣告性註記;如果 lib
裡面的設定包含 dom
這個宣告性群組,它會在專案裡幫你自動宣告好:
Declaration Group 或者是宣告性註記的意義在於 —— 將一些預設就有的 JavaScript 物件,比如:瀏覽器裡的 window
、document
物件;NodeJS 裡的 global
;ECMAScript 裡的 Promise
物件等等,提前告訴 TypeScript 它們的型別架構為何。否則 TypeScript 會視這些沒有經由提前的型別宣告性的群組的任意變數為 any
型別。
貼心小提示
declare
關鍵字會在後續篇章提及 —— 這會跟 Declaration File 相關的內容一起講解~
這裡讀者可能會問的問題是:
“像是
let
、const
或一些 Arrow Function —— 明明都是 ES6 以後的語法,那為何不用lib
裡設定es2015
也可以動作呢?”
筆者的推測是這樣:
TypeScript 是可以接受任何語法層面(Syntatic Aspect)相關的東西
let
& const
但是像 Promise
、Symbol
、Array
的擴充方法(例如:Array.prototype.findIndex
),這些都是屬於可以經過 Polyfill 就出來的東西 —— 並不是語法,而是功能層面(Utility Aspect)的擴展 —— 也就是說,這些 ES6 以後所擴展的各種型別功能都會群聚在 lib
設定裡的 es2015
選項部分,但 TypeScript 本身就有涵蓋某部分的 ECMAScript 語法標準(根據 Compatibility Table 而定)!
以下筆者就進行簡單驗證。(圖十二~圖十四)
圖十二:語法層面上,不需要引入 es2015
的宣告性群組就可以使用!
圖十三:功能層面上,沒有引入 es2015
宣告性群組會無效!
圖十四:功能層面上,必須引入 es2015
作為編譯器 lib
設定之一,使得這些 ES6 延伸出來的功能性的物件能夠被使用
以上的驗證確認成立!因此筆者正式向讀者宣告:
TypeScript 本身內建語法層面(Syntatic Aspect)相關的東西,因此 ECMAScript 規定的語法層面的東西大部分可以使用(參照 Compatibility Table)
如果在功能層面上沒有引入 es2015
的宣告性群組,TypeScript 會出現貼心的提示喔!(如圖十五)
圖十五:TypeScript 主動告訴你 Promise
本身是一個型別,但被誤用成一個物件,會建議你將 es2015
的選項加入編譯器裡的 lib
設定喔!
另外要注意一件事情:因為 Generators 的語法回傳值會產生類似 Iterator 的物件,而 Iterator 本身是一種物件,隸屬於功能層面的擴展 —— 因此推得就算可以使用 Generator 語法,如果在 lib
裡面沒有新增 es2015
選項,TypeScript 依然會跟你作對。(圖十六與圖十七)
圖十六:就算 Generator Function 屬於語法層面,但因為該函式會回傳一個類似 Iterator 介面的物件,因此牽扯到了功能層面,所以 TypeScript 會跟你作對呢!
圖十七:將 es2015
加入了 lib
選項就變正常了~(真是說變就變)
貼心小提示
本篇章結尾時,筆者會為這兩個名詞:語法層面與功能層面進行慎重定義的動作
module
筆者認為, JavaScript 這個領域出現的模組系統規範實在是有夠多、有夠麻煩,可能是筆者見識不廣,但是模組系統可以分成這麼多種,筆者實在是覺得很崩潰。(筆者的吶喊)筆者大致上是參考這篇文章的敘述 —— 以下的程式碼範例就是節選自它。
require
與 exports
這兩種關鍵字module.exports
作為 exports
相關功能,但又比 CommonJS 的規則稍微複雜些;通常 NodeJS 的開發者應該會習慣看到這樣的語法。define
關鍵字;如其名,在瀏覽器載入模組時是非同步載入,而 RequireJS
就是對於 AMD 的實踐。以上是常見的模組系統的語法規範與特徵。(其實還有 UMD
、System
更多方式,但筆者認為讀者有用到再去特別查詢就可以囉...)
貼心小提示
通常
amd
或system
那一類選項被編譯過後的成果,需要用特定的 Module Loader 執行。
然後筆者直接示範,兩種不同 module
的設定下 —— TypeScript 編譯的結果。(圖十八與十九為 commonjs
規範下的編譯成果;圖二十則是 es6
語法規範)
圖十八:可以看到 commonjs
的編譯成果
圖十九:commonjs
模式下的編譯結果,仔細一看有這麼一段 —— require('./module')
圖二十:結果使用 es6
模式編譯過後,有編譯沒編譯好像都沒差?
事實上,es6
模式並不是編譯過後沒差,原因是我們還未設定將所有檔案進行打包(Bundle)成一個檔案的動作。所以編譯結果事實上會有兩個檔案,一個是 index.js
、另一個是 module.js
。(如圖二十一)
圖二十一:事實上筆者還沒有將專案設定成打包單一檔案的模式
貼心小提示
打包檔案的設定會在下一篇討論 —— 但是打包檔案並且執行就可能要等到使用 RequireJS 篇章再說。
然而,讀者若覺得自己並沒有需要打包成單一檔案自行執行,而是選擇使用 Webpack 的話,可以參考網路上的資源外,筆者也會有一篇專門在講解 Webpack + TypeScript 環境建構。
所以第三篇章基本上在環境建構或者是編譯器設定部分算是供讀者自由學習的概念 —— 但是講到 Namespace 以及模擬戰時,就比較算是重頭戲囉。
所以 TypeScript 預設的編譯模式是 —— 你有幾個檔案,就會編譯成幾個檔案的概念。
重點 1.
tsconfig.js
專案相關
target
選項:主要目標是設定專案被編譯過後的語言版本,預設值為ES3
版本(官方指定的);可以選擇其他版本諸如:ES5
、ES6
(或es2015
以上)甚至還有ESNext
lib
選項:可以指定要載入的宣告性群組,通常是指功能層面(Utility Aspect)的宣告,不是語法層面。選項眾多,常見的是dom
、es2015
等等選項。
module
選項:代表被編譯過後,應該要採用的模組語法之規範,比如commonjs
、amd
或es6
等
重點 2. 語法層面與功能層面的差異
語法層面(Syntatic Aspect)泛指單純語法解析上的特點,通常語法是自然內建在編譯器的
功能層面(Utility Aspect)泛指可以藉由語法實踐的功能,並非內建在編譯器,通常會以函式庫(Library)的方式載入,或以 ECMAScript 的觀點來看 —— 以 Polyfill 的方式載入功能。
而 TypeScript 編譯器裡的
lib
選項就是對於 TypeScript 專案的功能層面的擴充,比如可以使用 ES6 規範的Promise
或更多 ES6 以後的特殊Array
相關的成員方法
筆者眼看字數已經破萬,決定先將本章 End 到這裡,但是筆者還沒把專案設定部分完全 Cover 啊!
因此下一篇也會是跟專案編譯有關的設定~
不過像是 lib
與 module
這兩個設定,光是要分析就可以講出很多概念了,而這通常也是初學 TypeScript 的人可能稍微碰過但沒有仔細理解內在機制的部分,然而如果可以對編譯過程與設定瞭若指掌的話,只會對管理任何專案或客製化編譯流程有加分的作用呢!