iT邦幫忙

第 11 屆 iThome 鐵人賽

1
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 31 篇

Day 31. 戰線擴張・專案監控 X 編譯設定 - TypeScript Compiler Compile Configurations

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20190929/20120614MaeJz9jmfg.png

恩... 照常 Day 31. 繼續。

《戰線擴張》篇章概要

本系列進入到第三部分:《戰線擴張》篇(The Front Line Expansion)

筆者就開始進行算是專屬於本系列的延長賽篇章,ㄧ樣會帶領讀者遍歷 TypeScript 各種很有趣的功能與應用~

以下大概筆者粗估會在本篇章提到的內容:

  • 使用 tsconfig.json 與 tsc 指令更多的功能
  • TypeScript Namespaces
  • 使用 RequireJS 執行不同編譯模式下的檔案
  • 引入第三方套件協作 與 Definition File

看起來很少對不對~然而,筆者難免會延伸出東西,照樣不太確定會出多少篇章。(原本預估五、六篇左右應該就可以結束掉這個篇章?不過可能會寫到十篇也不知道~那就算了~寫就寫吧~)

貼心小提示

就在前幾天,筆者確定第三篇章總共會有 11 篇,延伸出來的東西還有:

  • Webpack 環境的建立
  • 外觀模式 Façade Pattern

另外,筆者脫稿演出的機會很大(它 X 是有多喜歡寫?),不一定會按照以上的順序講解,反而是漸進式的介紹功能 —— 介紹到哪就往哪裡跑的概念。

那麼我們就正式進入《戰線擴張》系列~

正文開始!

熟悉 TypeScript 編譯器的設定

重溫編譯器的設定檔 TypeScript Configuration File tsconfig.json Revisited

讀者應該看過本系列後,每一次建構簡單的小專案時,就會需要呼叫:

$ tsc --init

它就會幫我們建立一個名為 tsconfig.json 的檔案。其中,筆者就把 tsconfig.json 大致上長的樣子截下來給大家看。

https://ithelp.ithome.com.tw/upload/images/20190928/20120614c8wrGOtRfI.png

讀者一定覺得:“tsconfig.json 密密麻麻的,筆者該不會是要全講?”

當然是不太可能,但筆者會特別挑選必須具備的編譯器選項功能,其他可以參考 TypeScript 官方的文件:Compiler Options —— 對於編譯器的任何設定寫得非常詳盡,筆者認為本身就很足夠。

另外,還有這一篇 —— 【Day 03】 TypeScript 編譯設定 - tsconfig.json by Kira 大大 已經將常見的 tsconfig.json 裡的 compilerOptions 甚至是也有把筆者沒有寫到關於 tsconfig.json 的東西也補足了,特地推薦給讀者服用。

本系列筆者應該只會講自己會用到的東西,因此筆者將所有的編譯器設定粗略地分成四類:

  1. 專案相關設定(Project Related Config.):跟專案的編譯流程、版本以及除錯等等相關的設定
  2. 語法檢測相關設定(Syntatic Checks Related Config.):跟進階的 TypeScript 語法偵測相關的設定
  3. 實驗性功能設定(Experimental Feature Related Config.):與 TypeScript 實驗性語法相關的設定
  4. 其他(Other Config.):筆者認為的雜項,沒意外應該不會講到(因為太雜,你也不需要一下子就鑽所有的專案設定)

貼心小提示

這只是筆者為了學習方便而自己定的分類方式,事實上在 tsconfig.json 裡的註解部分就有更進階的分類,但筆者在此不贅述 —— 本系列目標就是:能夠理解、靈活應用就好了,不需要背一些沒用到的東西,除非自己未來要用時,再查就 OK 囉。

專案相關設定 Project Related Configurations

1. 編譯語言版本 —— 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 編譯結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614D4JRwJHFr4.png

https://ithelp.ithome.com.tw/upload/images/20190928/20120614hI2Qc8iShs.png
圖二:以上是 es5 編譯之結果,有興趣的讀者可以試試看使用 es3 版本編譯結果 —— 會跟 es5 結果差不多ㄧ樣

https://ithelp.ithome.com.tw/upload/images/20190928/20120614d7iJ3Nns9q.png
圖三:es6 版本的編譯結果,有編譯沒編譯差不了多少的感覺

讀者也可以發現,TypeScript 經過編譯成原生的 JS 時,會把型別系統的註記拔除。(這句應該是廢話,不過筆者姑且提醒一下)

2. 引入功能層面支援 —— lib 設定

有些初期使用 TypeScript 的讀者會發現一件很莫名其妙的事情。(以下程式碼的警告訊息如圖四)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614msVlvLmUyP.png

https://ithelp.ithome.com.tw/upload/images/20190929/20120614v2golt6zp9.png
圖四:Object.assign 竟然不能用?

首先,Object.assign 屬於 ECMAScript 2015(ES6)的語法之一。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614nlzRfrremm.png
圖五:Object.assign 為 ES6 的語法,可以參考 caniuse.com 這個網站查詢

所以這時候 lib 設定就派上用場啦!如果讀者去查詢官方的 Compiler Options,會發現 lib 可以接收的值特別多。(如圖六)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614jBR6Td8vlp.png
圖六:筆者的電腦螢幕太小,整個畫面沒辦法截,由此可知 lib 裡面選項多到炸

我們必須告訴 TypeScript 專案 —— 我們需要 ES6 的 Feature,於是在 tsconfig.json 裡面,lib 選項更改為 ["es2015"],然後錯誤訊息就會不見。(更改前如圖七;更改後如圖八)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614sNb7PYstGb.png
圖七:讀者仔細看,沒有將 "lib" 選項開啟,Object.assign 部分會出現警告訊息

https://ithelp.ithome.com.tw/upload/images/20190928/20120614bIgiTPIP0q.png
圖八:新增 "lib": ["es2015"] 後,Object.assign 的警告訊息立馬消失

筆者再舉另外一個常見案例。首先我們先讓 "lib" 選項被註解掉 —— 其中,window 這個瀏覽器很常見的物件是可以被使用的。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614bnej3Duj2I.png
圖九:window 物件可以被 TypeScript 認得

由於剛剛為了使 TypeScript 專案支援 ES6 的語法,因此我們必須將 "lib" 設定為 ["es2015"] 才能使用;然而,如果真的使 "lib" 開啟並設定為 ["es2015"] 的話,又會發生錯誤!(如圖十)

https://ithelp.ithome.com.tw/upload/images/20190928/201206146ka1gwnAsP.png
圖十:window 被 TypeScript 視為 any,根本不存在

什麼!?剛剛 TypeScript 不是認得 window 這個東西嗎?

其實,window 的預設型別定義部分是被放在名為 dom 的選項,也是屬於 lib 設定當中的其中一項。所以如果改成,"lib": ["es2015", "dom"] —— window 物件又會再度被 TypeScript Recognized!(圖十一)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614zDnGXs5wXl.png
圖十一:原來 window 物件是由 dom 選項控制的啊

其中,lib 選項提供的功能,就是負責補足 —— 在 TypeScript 裡所謂的 Declaration Group —— 宣告性群組的東西。

讀者可以理解成:dom 裡面含有的是 window 或 document 那一類的宣告性註記;如果 lib 裡面的設定包含 dom 這個宣告性群組,它會在專案裡幫你自動宣告好:

https://ithelp.ithome.com.tw/upload/images/20190928/20120614w3GQqnC1kX.png

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
  • 解構式 Destructuring
  • 箭頭函式 Arrow Function
  • 類別宣告 Class Declaration
  • 屬性導出 Computed Properties
  • 匯集-散布運算子 Rest-Spread Operators
  • 迭代器產生函式的語法 Generators
  • 更多 ES6 以後的語法等

但是像 Promise、Symbol、Array 的擴充方法(例如:Array.prototype.findIndex),這些都是屬於可以經過 Polyfill 就出來的東西 —— 並不是語法,而是功能層面(Utility Aspect)的擴展 —— 也就是說,這些 ES6 以後所擴展的各種型別功能都會群聚在 lib 設定裡的 es2015 選項部分,但 TypeScript 本身就有涵蓋某部分的 ECMAScript 語法標準(根據 Compatibility Table 而定)!

以下筆者就進行簡單驗證。(圖十二~圖十四)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614sKuk8l8ZCA.png
圖十二:語法層面上,不需要引入 es2015 的宣告性群組就可以使用!

https://ithelp.ithome.com.tw/upload/images/20190928/20120614H8RweUk2vR.png
圖十三:功能層面上,沒有引入 es2015 宣告性群組會無效!

https://ithelp.ithome.com.tw/upload/images/20190928/201206140sepYgOTkA.png
圖十四:功能層面上,必須引入 es2015 作為編譯器 lib 設定之一,使得這些 ES6 延伸出來的功能性的物件能夠被使用

以上的驗證確認成立!因此筆者正式向讀者宣告:

TypeScript 本身內建語法層面(Syntatic Aspect)相關的東西,因此 ECMAScript 規定的語法層面的東西大部分可以使用(參照 Compatibility Table)

如果在功能層面上沒有引入 es2015 的宣告性群組,TypeScript 會出現貼心的提示喔!(如圖十五)

https://ithelp.ithome.com.tw/upload/images/20190928/20120614h15f4s5krh.png
圖十五:TypeScript 主動告訴你 Promise 本身是一個型別,但被誤用成一個物件,會建議你將 es2015 的選項加入編譯器裡的 lib 設定喔!

另外要注意一件事情:因為 Generators 的語法回傳值會產生類似 Iterator 的物件,而 Iterator 本身是一種物件,隸屬於功能層面的擴展 —— 因此推得就算可以使用 Generator 語法,如果在 lib 裡面沒有新增 es2015 選項,TypeScript 依然會跟你作對。(圖十六與圖十七)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614sOWihQQlJo.png
圖十六:就算 Generator Function 屬於語法層面,但因為該函式會回傳一個類似 Iterator 介面的物件,因此牽扯到了功能層面,所以 TypeScript 會跟你作對呢!

https://ithelp.ithome.com.tw/upload/images/20190929/20120614E5W8M12mzI.png
圖十七:將 es2015 加入了 lib 選項就變正常了~(真是說變就變)

貼心小提示

本篇章結尾時,筆者會為這兩個名詞:語法層面與功能層面進行慎重定義的動作

3. 編譯出不同的模組系統 —— module

筆者認為, JavaScript 這個領域出現的模組系統規範實在是有夠多、有夠麻煩,可能是筆者見識不廣,但是模組系統可以分成這麼多種,筆者實在是覺得很崩潰。(筆者的吶喊)筆者大致上是參考這篇文章的敘述 —— 以下的程式碼範例就是節選自它。

  1. CommonJS 規範:特點是使用 require 與 exports 這兩種關鍵字

https://ithelp.ithome.com.tw/upload/images/20190929/20120614Fos1DuT06k.png

  1. NodeJS 實踐 CommonJS 規範:跟 CommonJS 差別在於 —— NodeJS 是 module.exports 作為 exports 相關功能,但又比 CommonJS 的規則稍微複雜些;通常 NodeJS 的開發者應該會習慣看到這樣的語法。

https://ithelp.ithome.com.tw/upload/images/20190929/20120614BcgxSsQTKh.png

  1. 非同步模組定義檔 Asynchronous Module Definition (AMD):由 CommonJS 延伸出來的,用的是 define 關鍵字;如其名,在瀏覽器載入模組時是非同步載入,而 RequireJS 就是對於 AMD 的實踐。

https://ithelp.ithome.com.tw/upload/images/20190929/20120614Fz9fqOa5Cu.png

  1. ES6 模組載入/輸出語法(ECMAScript Import & Export):大致上寫過各種前端東西的讀者應該熟悉這種模式。

https://ithelp.ithome.com.tw/upload/images/20190929/20120614wqj9ubpX6k.png

以上是常見的模組系統的語法規範與特徵。(其實還有 UMD、System 更多方式,但筆者認為讀者有用到再去特別查詢就可以囉...)

貼心小提示

通常 amd 或 system 那一類選項被編譯過後的成果,需要用特定的 Module Loader 執行。

AMD 的標準可以用 RequireJS;而 system 設定下編譯結果可以用 SystemJS 去執行

然後筆者直接示範,兩種不同 module 的設定下 —— TypeScript 編譯的結果。(圖十八與十九為 commonjs 規範下的編譯成果;圖二十則是 es6 語法規範)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614ADpBhovrpM.png
圖十八:可以看到 commonjs 的編譯成果

https://ithelp.ithome.com.tw/upload/images/20190929/20120614frx9SwrEdr.png
圖十九:commonjs 模式下的編譯結果,仔細一看有這麼一段 —— require('./module')

https://ithelp.ithome.com.tw/upload/images/20190929/20120614pRpk0EkcXJ.png
圖二十:結果使用 es6 模式編譯過後,有編譯沒編譯好像都沒差?

事實上,es6 模式並不是編譯過後沒差,原因是我們還未設定將所有檔案進行打包(Bundle)成一個檔案的動作。所以編譯結果事實上會有兩個檔案,一個是 index.js、另一個是 module.js。(如圖二十一)

https://ithelp.ithome.com.tw/upload/images/20190929/20120614t9KOZFMcnT.png
圖二十一:事實上筆者還沒有將專案設定成打包單一檔案的模式

貼心小提示

打包檔案的設定會在下一篇討論 —— 但是打包檔案並且執行就可能要等到使用 RequireJS 篇章再說。

然而,讀者若覺得自己並沒有需要打包成單一檔案自行執行,而是選擇使用 Webpack 的話,可以參考網路上的資源外,筆者也會有一篇專門在講解 Webpack + TypeScript 環境建構。

所以第三篇章基本上在環境建構或者是編譯器設定部分算是供讀者自由學習的概念 —— 但是講到 Namespace 以及模擬戰時,就比較算是重頭戲囉。

所以 TypeScript 預設的編譯模式是 —— 你有幾個檔案,就會編譯成幾個檔案的概念。

重點 1. tsconfig.js 專案相關

  1. target 選項:主要目標是設定專案被編譯過後的語言版本,預設值為 ES3 版本(官方指定的);可以選擇其他版本諸如:ES5、ES6(或 es2015 以上)甚至還有 ESNext

  2. lib 選項:可以指定要載入的宣告性群組,通常是指功能層面(Utility Aspect)的宣告,不是語法層面。選項眾多,常見的是 dom、es2015 等等選項。

  3. module 選項:代表被編譯過後,應該要採用的模組語法之規範,比如 commonjs、amd 或 es6 等

重點 2. 語法層面與功能層面的差異

語法層面(Syntatic Aspect)泛指單純語法解析上的特點,通常語法是自然內建在編譯器的

功能層面(Utility Aspect)泛指可以藉由語法實踐的功能,並非內建在編譯器,通常會以函式庫(Library)的方式載入,或以 ECMAScript 的觀點來看 —— 以 Polyfill 的方式載入功能。

而 TypeScript 編譯器裡的 lib 選項就是對於 TypeScript 專案的功能層面的擴充,比如可以使用 ES6 規範的 Promise 或更多 ES6 以後的特殊 Array 相關的成員方法

小結

筆者眼看字數已經破萬,決定先將本章 End 到這裡,但是筆者還沒把專案設定部分完全 Cover 啊!

因此下一篇也會是跟專案編譯有關的設定~

不過像是 lib 與 module 這兩個設定,光是要分析就可以講出很多概念了,而這通常也是初學 TypeScript 的人可能稍微碰過但沒有仔細理解內在機制的部分,然而如果可以對編譯過程與設定瞭若指掌的話,只會對管理任何專案或客製化編譯流程有加分的作用呢!


上一篇
Day 30. 機動藍圖・流言終結者 X 重新認識物件的複合 - Favour Object Composition Over Class Inheritance
下一篇
Day 32. 戰線擴張・專案輸出 X 輸出設定 - TypeScript Compiler Output Configurations
系列文
讓 TypeScript 成為你全端開發的 ACE! 共 51 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言