iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

1
Modern Web

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

Day 38. 戰線擴張・模擬戰 — UBike 地圖 X Webpack 環境建構 - TypeScript Webpack Integration

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

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

  1. 什麼是宣告檔 Declaration Files?為何宣告檔很重要?
  2. 如何載入第三方套件在 TypeScript 專案裡?
  3. 如何執行 TypeScript 在 AMD 模式下的編譯成果?

如果還沒理解完畢的話,可以先翻看前一篇文章喔!

今天筆者又要講讀者應該會更常用的部分 —— 使用 Webpack 結合 TypeScript。

讀者可能覺得:“作者真是莫名其妙,為何一開始就不講這個?還要花很多篇時間講 Namespaces 或單純打包檔案等等東西。”

筆者之所以這麼做的原因是,要先讓讀者打好一些基礎 —— 像是宣告檔 Declaration Files 的目的與由來、自己如何建立簡單的 TypeScript 環境等等,畢竟在進行簡單的語法測試或學習時,你不太需要那麼大型的環境。

另外,筆者直接進入 Webpack 單元講也是可以,只不過讀者提前知道宣告檔的存在 —— 並且有能力善用工具 —— 可以獨立查詢套件的型別宣告規格的話,要開始跟一些專案應用層面的內容會比想像中簡單喔!

本篇文章就進入正文開始吧~

模擬戰 — UBike 地圖應用(一)

建構 Webpack 結合 TypeScript 環境

這應該是大部分讀者想要知道的東西,筆者也總算可以從前幾篇很痛苦的設定來設定去的泥淖中,準備從這一篇解脫。

筆者稍微簡介一下 Webpack,避免有些讀者可能還是對 Webpack 這東西有些疑惑。(圖一為官方網站對於 Webpack 的示意圖)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614sE1KTpxW6Q.png
圖一:解釋 Webpack 最簡單的架構圖

可以從圖一得知,事實上 Webpack 不僅僅只有打包純 JavaScript 相關的檔案而已,它連任何靜態資源,諸如:CSS、Sass、Image 等等相關的檔案都可以打包在一起。

而打包專案一切的方式就是用所謂的 Loader 進行,比如說:想要將 CSS 檔案打包進去,就會有所謂的 css-loader;想要編譯 Sass 相關的檔案,就會有 sass-loader 之類的東西 —— 而如果今天要將 TypeScript 的檔案編譯並且打包進去,官方就有出所謂的 ts-loader

因此,筆者今天就先示範如何建構單純的 Webpack 結合 TypeScript 的環境吧!當然,讀者可以點這裡檢視 Webpack 官方網站以及參考如何將 TypeScript 結合到 Webpack 的相關設定喔!

首先,在任何想要建構環境的資料夾中下一系列的指令:

// 到你想要建構環境的資料夾內
$ cd PATH_TO_DIR

// 初始化 package.json
$ npm init -y

// 初始化 tsconfig.json
$ tsc --init

下載 Webpack 相關套件

首先,筆者下載兩個跟 webpack 相關的套件:

$ npm install webpack webpack-cli --save-dev

另外,因為我們需要讓 TypeScript 與專案結合,必須要重新下載 typescript 與下載打包時需要的 Loader —— 也就是 ts-loader

$ npm install typescript ts-loader --save-dev

讀者可能覺得:“為何要重新下載 typescript 模組?這東西不是早在本系列第一天就已經下載過了,而且是用 npm 模組的 global 模式下載的?”

事實上 Webpack 認得的 TypeScript 編譯器並不會從 Global 的 NPM 模組參照,而是在專案內部參照。因此每一次建構 Webpack 結合 TypeScript 的環境時,必須額外下載 typescript 在專案內部

專案設定

習慣上,開發時我們會將主程式都放在名為 /src 的資料夾;打包專案時則會將結果輸出到 /build/dist 的資料夾(又或者是看讀者有什麼習慣)。

以下分別新增兩種專案資料夾,並且額外新增 index.ts/src 資料夾裡,代表主程式撰寫的地方喔~

// 建構 /src 與 /dist 這兩種不同的檔案資料夾
$ mkdir src
$ mkdir dist

// 新增 ./src/index.ts 檔案
$ touch ./src/index.ts

index.ts 裡隨便寫一行 console.log

https://ithelp.ithome.com.tw/upload/images/20191005/20120614YipM7G1OQq.png

另外,我們也需要設定 TypeScript 編譯器的檔案:

{
  "compilerOptions": {
    /* 略... */
    "target": "es5",
    "module": "es6",
    "outDir": "./dist/",
    "rootDir": "./src/",
    "strict": "true",
    "noImplicitAny": true,
    "strictNullChecks": true
    /* 略... */
  }
}

以上是筆者設定過的東西,其他如果有一開始預設的設定被啟動可以放著。

另外,建立 webpack.config.js 並且填入以下內容:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614RHMq4jogGl.png

以下筆者稍微解釋 webpack 的設定檔到底寫了些什麼:

  • entry 部分代表的是 Webpack 要打包的檔案輸入位置,也就是 ./src/index.ts
  • module 裡面通常都是放置 Loader 相關設定 —— 其中 TypeScript 相關檔案都會經由 ts-loader 進行編譯處理的動作
  • output 則是設定 Webpack 打包過後的專案輸出點 —— 以上面的設定來說,它會把檔案打包到 ./dist 資料夾內,並且取名為 bundle.js

編譯到測試開發流程

接之前筆者有提過我們可以使用 lite-server 簡單地 Host HTML 檔案並且監控靜態檔案的變更 —— 如果該 HTML 檔引入的 JavaScript 檔案有被變動時,就會自動幫我們刷新頁面。

首先,我們當然得先要有 index.html 來測試我們編譯過後的 JavaScript 檔案,以下是 index.html 的程式碼內容。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614jy4EAqeEoW.png

另外,我們除了想要使用 lite-server 外,通常使用 webpack 打包專案時,可以使用 webpack -w 開啟 Watch 模式 —— 只要專案一有變動,Webpack 就會自動重新打包並更新輸出結果。

不過這樣子又會遇到必須同時開兩個終端機,分別執行 lite-serverwebpack -w 這兩個指令,於是我們也可以使用筆者之前提到的 concurrently 這個套件 —— 協助我們同時執行這兩種指令。

首先,下載 lite-serverconcurrenly 這兩個套件:

$ npm install lite-server concurrently --save-dev

並且將 package.json 裡的 scripts 修改成:

{
  /* 略... */
  "scripts": {
    "start:watch": "webpack -w",
    "start:serve": "lite-server",
    "start": "concurrently npm:start:*"
  },
  /* 略... */
}

我們就可以使用 npm start 同時執行 webpack -wlite-server 這兩種指令。

以下是下達 npm start 指令時,VSCode 編譯器的結果。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614B81JFr9bgl.png
圖二:一連串執行的過程並且成功地將專案打包出來

另外,lite-server 會自動打開瀏覽器,並且鎖定 localhost:3000 這個 Port。(如果本來的 3000 Port 被佔據了,則會打開 3001,依此類推)(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614PAsUMLN73r.png
圖三:打包後清楚地印出 Hello World! 字串

以上就是完整的 Webpack 結合 TypeScript 基礎的環境開發流程!

本篇唯一重點. 使用 Webpack 結合 TypeScript 建立環境

除了 Webpack 基本的套件與設定外,最主要需要注意的是:

  • 必須要下載 typescript 模組到專案裡,因為 Webpack 的 ts-loader 必須仰賴專案內部的編譯器,而非全域
  • TypeScript 檔案對應的 Loader 是 ts-loader
  • 通常使用 Webpack 時,就不太管 tsconfig.json 裡面的 outFile 這個選項,因為 Webpack 會幫你處理好整個打包流程,你也不需要再為其他模組規範擔心來擔心去的
  • 記得啟動 tsconfig.json 裡跟語法或型別監測相關的設定 —— 例如 noImplicitAnystrictNullChecks

UBike 地圖範例簡介

既然環境都設定完畢了,筆者想順便簡單應用目前所學到的東西,示範小專案如何應用 TypeScript 的各種 Feature:諸如型別、類別、介面等等語法的協作。

本範例筆者想要示範的是使用 LeafletJS 打造台北市的 UBike 即時地圖 —— 使用的自然是台北市開放資料中的 UBike 台北市公共自行車即時資訊

如果對 LeafletJS 不熟悉的讀者們,可以將它想成類似於 Google Map 的套件(但不是 Google Map XD)。(圖四為 LeafletJS 官方的網站截圖)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614YfdmolK4Yv.png
圖四:LeafletJS 官方網站

另外,由於本篇內容已經花了一半的篇幅在解說 Webpack + TypeScript 的環境建構流程,因此下半篇只能先把進度推到先把主要地圖建構出來。不過光是這樣的簡單過程,筆者就可以扯到關於套件的型別宣告檔以及一些技巧,這都是在前一篇都討論過的東西,因此這裡會再次示範,不失為一個練習的好機會。

下載 LeafletJS

由於官方的 LeafletJS 的套件 leaflet 是用原生 JavaScript 的程式碼實作的,因此我們也必須同時下載 @types/leaflet

$ npm install leaflet @types/leaflet

由於 LeafletJS 的地圖也有官方的 CSS 檔案需要載入,這裡筆者選擇用它們的 CDN 載入到 HTML 檔案裡。此外,我們也會需要提供一個標籤負責作為地圖的容器,因此在 HTML 檔案裡筆者選擇使用 <div id="map"></div> 作為地圖的容器。以下是 index.html 目前的結果。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614QUJwxywLzg.png

基本地圖的建構

當然,如果對於 LeafletJS 提供的功能有任何問題可以參考官方網站的 Doc。這裡筆者就按照自己寫程式的過程一一記錄下來。

首先,筆者在 index.ts 寫下產生地圖的程式。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614wNGFIMVCq9.png

不過一開始就碰到問題了,TypeScript 對於 taiwanCoord 變數的使用有些警告。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191005/201206140zmVwPFoyt.png
圖五:很明顯地,setView 方法要求第一個參數的型別為 LatLngExpression 而非 number[]

讀者可能覺得疑惑,明明沒有任何 LatLngExpression 的蹤跡為何會出現這種奇怪的訊息。

其實重點在下一句話:Type 'number[]' is missing the following properties from type '[number, number]': 0, 1

其中,不覺得 [number, number] 這個格式很熟悉嗎?這不就是在很久以前討論過的元組型別嗎?

如果讀者還記得的話,筆者當時有說過:

元組型別一定要進行積極註記,不然會被 TypeScript 自動推論為陣列相關的型別

因此筆者將 index.ts 中的 taiwanCoord 變數宣告的時候註記 LatLngExpression,不過這時候 VSCode 提供的 Auto-Complete 功能非常好用(如圖六)。

https://ithelp.ithome.com.tw/upload/images/20191005/201206146FPKIXbDtV.png
圖六:VSCode 會自動彈出開發者可以註記的型別

其中最重要的一點在後面的提示字:Auto import from 'leaflet' —— 也就是說,當筆者選擇這項功能的時候,VSCode 會自動將 LatLngExpression 自動幫我們載入進去,這是非常好用的功能!

https://ithelp.ithome.com.tw/upload/images/20191005/20120614CnLRmCt9r8.png

此時編輯器內部不會出現任何錯誤訊息(如圖七),此外瀏覽器可以顯示出地圖的結果(如圖八)!

https://ithelp.ithome.com.tw/upload/images/20191005/20120614YeIwu17P9f.png
圖七:編輯器內部除了沒有錯誤訊息外,Webpack 正在幫我們打包檔案呢!

https://ithelp.ithome.com.tw/upload/images/20191005/20120614gSTdcSBNRA.png
圖八:地圖出來了~

建立設定檔並隔離型別註記 Type Annotation Isolated From Main Program

首先,筆者必須說,因為太多亂七八糟的設定摻雜在一起,包含:

  • taipeiCoord 代表的是地圖初始的聚焦點
  • zoom 代表預設的地圖縮放等級
  • 'map' 字串地圖裝載的容器之標籤的 ID
  • tileLayer 方法裡有一個是代表地圖的背景 URL

因此筆者額外建立 map.config.ts 這個檔案在 /src 資料夾裡並填入這些資訊:

https://ithelp.ithome.com.tw/upload/images/20191005/20120614vGfPOukaKv.png

並且在 index.ts 裡引入 map.config.ts(當然,讀者可能也會想要用 JSON 格式,不過後面筆者會說明為何採用 .ts)。

https://ithelp.ithome.com.tw/upload/images/20191005/2012061490MN8KZw6T.png

不過!這裡ㄧ樣會遇到剛剛的問題 —— coordinate 會被推論為 number[] 而非 LatLngExpression,因此我們可以選擇註記 coordinateLatLngExpression;又或者,乾脆在 map.config.ts 宣告該設定檔的 JSON 物件格式並積極註記它,以下是 map.config.ts 的結果。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614iMcEwQk3WZ.png

這樣子,原本的 index.ts 檔案裡的 coordinate 就會被自動推論為 LatLngExpression 了!(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614jBsijFmMdS.png
圖九:將型別註記整理到其他地方,主程式就不會特別繁雜,而且 TypeScript 還會自動幫我們載入進來的變數之型別推論結果。

以上就是筆者選擇用 map.config.ts 而非 .json 格式的原因 —— 主程式檔案不會充滿繁雜的註記,但是仍舊在 TypeScript 型別系統的監控下

注意,這一句話是重點:

必須要讓 TypeScript 專案掌控在型別系統之下

很多人以為使用 TypeScript 就是一股腦兒地要讓全部變數型別等等都要被註記下去,但事實上 —— 光是善用 TypeScript 的型別推論有很多優點:

  1. 可以省掉我們的開發時間,不需要每次宣告東西就必須積極註記東西
  2. 程式碼不會滿滿都是型別註記 —— 剛入門 TypeScript 的人看到滿滿的註記,就算看得懂可能心裏也會有壓力感到怕怕的(筆者看到型別註記充滿在檔案裡,除非型別註記是簡潔地,否則也是會感到混亂)
  3. 不用特別註記,TypeScript 就會自動監控專案

這是要善用 TypeScript 的優勢,而不是被工具逼迫一定要每次宣告新的東西就得進行註記

不用打開瀏覽器上網就可以查到文件內容

如果讀者有 Follow 前一篇文章講到的技巧 —— 可以藉由型別推斷出來的結果,使用 VSCode 就可以追溯型別被宣告的原始碼所在位置

首先,假設我們想要知道 Leaflet Map 的個體到底可以使用什麼功能,我們可以將滑鼠指向 L.map 那個方法。

再來是:

  • Mac 系統就按下 Command 鍵
  • Windows 系統就按下 Ctrl 鍵

底下出現底線。(如圖十)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614JXvQ7BEFul.png
圖十:L.map 被顯示出來額外資訊

點下去就會出現很精彩的 —— LeafletJS 套件的型別宣告檔(Declaration File)。(如圖十一)

https://ithelp.ithome.com.tw/upload/images/20191005/201206143qy5ZfcZlC.png
圖十一:這是 L.map 的型別宣告

這裡讀者就可以不用上網查資料就可以知道 —— 原來 L.map 後面還有 MapOptions

重複剛剛的步驟,檢索 MapOptions。(如圖十二)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614S27jP8s2bF.png
圖十二:檢索 MapOptions 的所有功能

哇噻~你不需要上網查,你就知道 MapOptions 裡面可以填入什麼樣的資料,以下筆者列舉幾個:

  • preferCanvas? —— 筆者猜測可能是將 Container 繪出地圖的過程改採用 HTML5 Canvas,而非 SVG 模式
  • zoomControl? —— 如果為 false 代表該地圖不能被縮放
  • dragging? —— 如果為 false 則該地圖不能被滑鼠拖曳
  • 還有更多其他選項

所以筆者要再強調一次:

你如果會使用型別宣告檔查詢功能的話,花在瀏覽器查詢 Doc 的時間會大大減少

這就是筆者強調 Declaration Files 重要的地方 —— 它們可是套件的規格所在地呢!

小結

筆者今天光是講簡單的地圖建構就花了另外五千字講解 —— 善用工具的技巧是筆者首要推廣的目標

下一篇我們就緊接著開始開發更多 UBike 地圖相關的功能喔~


上一篇
Day 37. 戰線擴張・第三方套件 X 支援的引入 - 3rd-Party Package & TypeScript Declaration File
下一篇
Day 39. 戰線擴張・模擬戰 — UBike 地圖 X 資料處理 - Data Processing using Type Alias
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言