iT邦幫忙

第 11 屆 iThome 鐵人賽

1
Modern Web

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

Day 37. 戰線擴張・第三方套件 X 支援的引入 - 3rd-Party Package & TypeScript Declaration File

https://ithelp.ithome.com.tw/upload/images/20191004/20120614X4ue0LkE7W.png

閱讀本篇文章前,仔細想想看

今天不用想,馬上看下去!

今天筆者要講本篇章系列比較重要的部分 —— TypeScript 的型別宣告檔 Declaration Files。

因此不用多說,正文開始

第三方套件與型別宣告檔 3rd-Party Package & TypeScript Declaration Files

原生 JavaScript + TypeScript?

事實上,要結合原生 JavaScript 與 TypeScript 協作 —— 理論上是可以的

畢竟 TypeScript 主要是從原生 JavaScript 並且採用大部分的 ECMAScript 標準的語法而出來的語言。

但是,如果直接使用原生 JS 與 TypeScript 結合起來的話,基本上:

少了 TypeScript 的型別系統 —— 根本殺雞焉用牛刀

使用 jQuery 為例

筆者以下就以 jQuery 為範例,示範一下 TypeScript 和原生 JS 會遇到的狀況。

首先,筆者將會從頭開始建構簡單的環境,讀者如果熟悉這些動作應該可以快速掠過:

// 前往測試的資料夾
$ cd PATH_TO_DIR

// 初始化 JS 專案
$ npm init -y

// 初始化 TypeScript 專案
$ tsc --init

// 建立 index.ts
$ touch index.ts

// 建立 index.html
$ touch index.html

另外,由於我們要使用 jQuery 作為範例,筆者建立 index.html 並且填入以下的程式碼。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614gXjZppq1CK.png

其中,除了引用 jQuery 的 CDN 外,以上的範例跟之前的計數器範例一模ㄧ樣:當按鈕被按下去時,應該要紀錄按按鈕的次數並且更新資訊。

因此,筆者在 index.ts 裡撰寫簡單的程式碼。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614pAF4pHnxe7.png

讀者應該也會覺得,這一定會被 TypeScript 警告,因為我們沒有宣告任何跟 $ 有關的型別或指派任何的值。(編輯器顯示的錯誤狀況如圖一)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614KiJCOocH99.png
圖一:編輯器上面每個 $ 都出現錯誤提示

但是這個檔案照樣可以被 TypeScript 編譯 —— 只要是原生 JS 的語法符合的情形下,再加上 tsconfig.json 裡的選項 noEmitOnErrorfalse 的前提,硬是要把檔案編譯出來是可以的!

但編譯結果會丟出一連串噁心的錯誤。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614iXcTkIQjHH.png
圖二:TypeScript 完全不知道 $ 這個東西到底是什麼,它似乎提示著我們要下載 jQuery 的型別宣告檔 Declaration Files,這點筆者後面會講到喔!

然而用瀏覽器打開 index.html,還真的可以執行。(如圖三)

https://i.imgur.com/7YHNfeC.gif
圖三:使用瀏覽器測試時,jQuery 照樣可以運作;然而,開發者工具右方的 Source 部分顯示編譯過後的結果,有編譯沒編譯似乎都沒差

型別定義與宣告檔的作用 Type Definition & Declaration Files

為了應對這種來自於第三方套件,想要從原生 JS —— 不需要重寫成 TypeScript 但依然能夠和 TypeScript 檔案協作的話 —— 型別定義(Type Declaration)就必須派上用場!

首先,最陽春的作法就是直接在 index.ts 裡,使用 declare 關鍵字將 $ 的型別宣告出來告訴 TypeScript 編譯器。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614DL4IKXuitj.png

讀者可以看到,只是多了一行 declare 就把型別資訊定義出來了 —— 另外,有 declare 跟沒 declare 的差別在於,儘管 declare 的變數沒有指派任何東西進去,TypeScript 依然認定為該會由某個開發者不需要知道的地方提供出來,而這裡指的某個地方就可能存在於外來套件模組等等。

所以編輯器此時的狀況是乾乾淨淨的,沒有任何錯誤。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20191004/201206149kVpVu5YFU.png
圖四:沒有出現任何跟 $ 相關的錯誤,因為已經告訴 TypeScript $ 這個東西為 any 型別

至於編譯過後的結果等等,讀者可以自行測試,照樣可以在瀏覽器上執行編譯結果喔~

想當然,這應該不是筆者與讀者們樂見的解法,但是筆者還沒講到 Declaration Files 的概念。

通常我們不會把 declare 相關的型別定義程式碼和主程式放在普通的 .ts 檔案 —— 而是會拆開來分成主程式的 .ts 檔外,其餘的型別定義程式碼部分會塞在 .d.ts 檔案 —— 這些就是所謂的 Declaration Files。

所以筆者可以選擇建立一個名為 jquery.d.ts 檔案並且將剛剛的 declare 表達式塞進去。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614FKxkAmgr7u.png
圖五:將 $ 的型別宣告放在 jquery.d.ts 裡時,index.ts 就算沒有宣告跟 $ 相關的變數或型別,TypeScript 依舊認得 $

重點 1. 宣告檔的目的 The Purpose of TypeScript Declaration Files

有些本身使用原生 JavaScript 撰寫出來的第三方套件,若不想要再額外寫 TypeScript 版本的話 —— 可以改採撰寫宣告檔(Declaration Files)—— 目的是要能夠讓 TypeScript 在原生 JavaScript 撰寫出來的程式碼,包裝其型別系統的定義,以利於 TypeScript 與原生 JavaScript 進行合作。

重點 2. 使用宣告檔與型別定義的宣告 Using Declaration Files & Declaring Type Definition

型別定義的宣告通常不會和主程式放在普通的 .ts 檔案,而是會額外放置在 .d.ts 檔案

可以使用 declare 關鍵字宣告型別的定義:

https://ithelp.ithome.com.tw/upload/images/20191004/20120614TgGpoWXZ4R.png

詳細資訊可以看官方 TypeScript Declaration Files 的 Doc

正確引入第三方套件

以下開始,筆者要介紹正確的方式引入第三方套件。通常熱門的套件,就像本篇重點 1. 所提及到的,由於直接改寫成 TypeScript 版本的成本太大,因此會改採用撰寫 Declaration Files 的方式供專案使用。

通常這些專案的 Declaration Files 都匯集在 GitHub 的 Definitely Typed 這個 Repo 裡面喔!筆者這邊提供 jQuery 的 Declaration File 供讀者參考。

另外,通常如果要下載具有 TypeScript Declaration File 版本的套件 —— 那些套件的名稱通常會有 @types 這個前綴字。

所以,如果我們想要下載 jQuery 以及其定義檔,必須要下這個指令:

$ npm install @types/jquery

https://ithelp.ithome.com.tw/upload/images/20191004/20120614KHjUyJLaRd.png
圖六:下載 @types/jquery 套件

其中,如果你翻看 node_modules 裡面的內容,你會發現 —— 只要任何套件含有 Declaration Files,都會在 node_modules 裡面多了一層 @types 這個檔案資料夾。(圖七)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614uxSO18xdch.png
圖七:node_modules 存放的是我們在專案下載的模組,其中 @types 資料夾存放的是 Declaration Files

不過這裡有一些使用 TypeScript 上必須注意的點:通常 @types 系列套件只有型別宣告檔而沒有實際上實作的程式碼

@types/jquery 似乎沒有包含原生 JS 版本的程式碼 —— 如果你仔細看圖七的檔案資料分布,裡面沒有任何 .ts.js 檔案,全部跟 JavaScript 相關的檔案都是 .d.ts 結尾,也就是 TypeScript 的宣告檔

單純只有宣告檔但沒有實際的程式碼的實作來源是沒辦法讓專案動作的,就好比今天你下載的東西是只有一個黑盒子的外殼,但卻沒有實作的細節內容

因此,筆者還是得下載原生 JS 版本的 jQuery 模組:

$ npm install jquery

那讀者可能會問:“恩... 難道所有的套件都得這麼做嗎?必須下載 TypeScript 版本的型別宣告檔外,還需要原生 JS 的 Copy?”

事實上是不一定喔!

就以 RxJS 這個套件來說,它已經全面轉戰到 TypeScript 這個語言進行開發了 —— 想當然,你可以只下 npm install rxjs,它就已經附贈了 .d.ts 相關的 TypeScript 型別宣告檔。

圖八、圖九為筆者臨時下載 rxjs 這個套件給讀者看裡面的內容,讀者不需要在本範例執行此步驟。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614PBlIHb0Sdd.png
圖八:筆者下載 RxJS 這個套件

https://ithelp.ithome.com.tw/upload/images/20191004/20120614LFCslTmbiw.png
圖九:RxJS 套件就算沒有出現在 @types 資料夾下,它本身就附帶了 .d.ts 檔案!

因此,要不要下載原生的套件與對應的 @types 宣告檔是不一定的,要看該套件的情形

而 jQuery 的案例就是除了基本的原生 JS 版本需要下載外,連同 @types/jquery 必須得下載。所以筆者還是得多下:

$ npm install jquery

好的,筆者接下來可以使用 jQuery 套件裡面的功能。在 index.ts 檔案裡面直接進行載入的動作:

https://ithelp.ithome.com.tw/upload/images/20191004/20120614RvrLE9Pgvi.png

另外,Declaration Files 的主要意義就是讓原生 JS 包裝一層 TypeScript 的型別系統,因此如果你使用 jQuery 的 $ 時,TypeScript 就會根據 @types/jquery 提供的宣告檔,顯示你可以使用的功能!(如圖十~十二)

https://ithelp.ithome.com.tw/upload/images/20191004/201206141JDqlBxyrZ.png
圖十:$ 被型別推論的結果

https://ithelp.ithome.com.tw/upload/images/20191004/20120614sVO3qXFtFX.png
圖十一:$() 裡面告訴你,你可以填入 HTMLElement 型別的值

https://ithelp.ithome.com.tw/upload/images/20191004/20120614A3IDSuRnKR.png
圖十二:使用 $(document) 時,可以接的方法內容,為編輯器的 Auto-Complete 功能

另外,如果你想要查詢宣告那些型別的實際程式碼位置 —— 以查詢 $ 之型別宣告位置為例,首先將滑鼠指到 $(這是廢話),然後:

  • 如果是 Windows 系統可以按下 Ctrl
  • 如果是 Mac 系統可以按下 Command(⌘)

ㄧ樣會在 $ 底下出現底線,直接點下去之後就會 Pop 出該型別被宣告的地方。(如圖十三)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614f63U7VTAth.png
圖十三:從我們撰寫的程式碼,查詢套件原本型別被宣告的地方是可以如此地輕鬆

這個功能筆者特別強調的原因是 —— 可以藉由這個方式查詢套件本身的型別結構外,通常 Declaration Files 上面都會有套件的 Documentation —— 也就是說,你可以直接下載完套件、使用裡面提供的功能同時,如果遇到臨時不知道到底套件的功能要接收的參數為何,你可以直接查詢該功能的 Declaration 而不需要再打開瀏覽器上網查它們的文件

這個技巧如果會的話,除了可以大大節省上網查 Doc 的時間,甚至還可以一窺套件內部的結構長相呢

筆者記得這個技巧本來就在選用屬性篇章有提到,這裡又再使用了一次,因此這個技巧可是很重要的呢~

重點 3. 引入第三方套件 Integrating 3rd Party Packages

引入第三方套件的原則不一定,通常熱門的套件都會有相對應的 Declaration Files,而這些宣告檔都匯集在 Definitely Typed 這個 GitHub Repo. 裡。

第三方套件的型別宣告檔,通常都會以 @types/<package> 的格式作為名稱。然而,@types/<package> 不會包含原套件的 JS 程式碼,因此有可能必須同時下載套件的原始碼。

另外,有些本來就有使用 TypeScript 作為開發的套件,本來就會有對應的型別宣告檔,此時就不需要再額外下載 @types/<package> 相關得檔案。

因此,引入第三方套件都是得看該套件本身狀況來決定下載的

重點 4. 查詢套件提供的功能對應到的型別宣告

若臨時想要查詢第三方套件提供的功能之型別宣告所在的地方,有可能是因為:

  • 你想要查看該功能必須填入的參數格式或型別
  • 你想要查看該功能的型別推論與註記的機制為何

你可以使用滑鼠指向該功能的同時:

  • 如果是 Windows 系統,按下 Ctrl
  • 如果是 Mac 系統,按下 Command(⌘)

並且按下滑鼠,此時 VSCode 編輯器就會打開該型別宣告的內容。

這個技巧超重要,甚至到下一個《通用武裝》篇章,會一而再、再而三地使用!

打包程式碼並且使用 RequireJS 執行它

最後筆者要示範 —— 將檔案進行打包後並且在瀏覽器上執行

筆者將 tsconfig.jsonmodule 設定為 system 模式外,並且將 outFile 設定為 index.js

{
  "compileOptions": {
    /* 略... */
    "module": "amd",
    "outFile": "index.js"
    /* 略... */
  }
}

另外,使用 tsc 將我們檔案進行編譯的動作。(編譯結果如圖十四)

https://ithelp.ithome.com.tw/upload/images/20191004/20120614qQYd4jTFTU.png
圖十四:amd 模式下編譯出來的結果

通常選擇哪一種 module 編譯模式,必須要有相對應的 Module Loader 協助我們執行檔案。而 amd 模組模式可以對應的 Module Loader 就是 RequireJS

首先,下載 RequireJS 的檔案,並且筆者將它命名為 require.js。圖十五為目前的檔案資料夾狀態。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614eL358kKnjm.png
圖十五:從 RequireJS 官方下載了檔案

另外,我們必須更改 index.html 的內容。

https://ithelp.ithome.com.tw/upload/images/20191004/20120614KPidG0iUUn.png

主要要注意的是:

  • 必須先載入 require.js,否則瀏覽器會噴出錯誤訊息,因為它會找不到 define 這個東西的定義,而 define 就是 require.js 宣告的東西
  • 載入完 require.js 與編譯過後的 index.js 後,必須要先設定 RequireJS —— 將 jquery 的路徑指向 node_modules 裡的 jQuery 檔案,那是因為 RequireJS 必須知道 index.js 用到 jquery 所在的位置才能夠載入進去
  • 最後才執行 index.js 檔案 —— require(['index'])

打開瀏覽器後可以正常執行。(如圖十六)

https://i.imgur.com/2mnUq6G.gif
圖十六:使用 RequireJS 執行 AMD 模式編譯過後的結果

小結

筆者今天講到的東西感覺特別多,最主要是在強調 TypeScript Declaration Files 的使用以及優勢

另外,執行打包過後的專案也是很重要的課題,因此筆者今天也示範如何簡單使用 RequireJS 執行打包過後的專案。

下一篇筆者就來講簡單的小專案實作喔~


上一篇
Day 36. 戰線擴張・戰線分散 X 組織集中 - TypeScript Namespaces Import/Export Mechanism
下一篇
Day 38. 戰線擴張・模擬戰 — UBike 地圖 X Webpack 環境建構 - TypeScript Webpack Integration
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言