iT邦幫忙

第 11 屆 iThome 鐵人賽

2
Modern Web

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

Day 39. 戰線擴張・模擬戰 — UBike 地圖 X 資料處理 - Data Processing using Type Alias

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

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

是否會使用 Webpack 建立 TypeScript 專案的環境呢?

另外,本篇文承接上一篇文,因此如果是跳到這篇的話可以先從上一篇看起喔~

貼心小提示

想要本範例的程式碼 —— 可以參見 Maxwell-Alexius/Iron-Man-Competition 這個 Repo 喔!

直接進入正文開始

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

在進入 LeafletJS 的使用之前,上一篇已經將簡單的地圖給實踐出來了,筆者快速把上一篇的主程式碼帶過。

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

其中,筆者已經將 mapConfig 的內容與型別註記整理到 map.config.ts 這個檔案,這樣子主程式不會塞太多型別註記的東西外,如果之後要做調整,就可以直接更改 map.config.ts 裡面的值。

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

快速看過後我們趕快進到下一關!

資料處理 —— 善用型別化名 Data Processing with Type Alias

通常遇到要視覺化的應用,一定會經過痛苦的資料處理過程。

題外話,這裡附上筆者資料視覺化的作品集,但對筆者而言還不算完整,因為筆者個人目標是把所有的 Chart 都畫出來 XD,但也要看筆者個人時間上的安排。(糟糕偏題了)

今天的目標就是要把台北市的 UBike 資料簡化到我們需要的格式 —— 當然,原本的資料名稱實在是很難看!(如圖一)

https://ithelp.ithome.com.tw/upload/images/20191005/201206140YgJuB2XEJ.png
圖一:台北市政府的開放資料,對於 UBike 的自行車即時資料的描述內容

光是看欄位名稱:sbi 代表目前的自行車數量、sarea 為行政區域等等,實在是太抽象。今天筆者來 Run 一次自己認為如何用 TypeScript 進行資料處理。

先把請求管道建立起來 Data Request

當然,第一步驟就是確保我們能夠接收到資料,以下筆者新增 fetchData.ts/src 資料夾內,裡面內容為:

https://ithelp.ithome.com.tw/upload/images/20191005/201206141OKdMPRhhg.png

首先 URL 自然而然是台北市開放 UBike 資料的接口連結,另外 fetchUBikeData 的函式參數,筆者建議不要寫死,因為有可能請求的來源不同。

以下是在 index.ts 裡面引入該函式後,並且進行測試時,檢視資料的狀況。

https://ithelp.ithome.com.tw/upload/images/20191005/2012061468jqYMNARF.png

敏銳的讀者如果讀過編譯器設定篇章 —— 由於我們使用 ES6 Promise 這個物件 —— 此為功能層面(Utility Aspect)而非語法層面的東西,我們必須要在 tsconfig.json 裡的 "lib" 選項改成:

{
  "compilerOptions": {
    /* 略... */
    "lib": ["dom", "es2015"]
    /* 略... */
  }
}

方才可使用 Promise 這些 ES6 以後的功能。

貼心小提示

Promise 物件的詳細推論機制,由於牽扯到 Generics 泛用型別機制,因此會在第四篇章講到喔!

儲存之後打開瀏覽器可以發現我們成功地將資料接出來了。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614kiyKjAEPkP.png
圖二:至少我們先確定資料可以出來了

善用 TypeScript 型別系統並且練習寫 Doc

通常筆者習慣遇到這種亂糟糟的資料,一定會把來源資料的格式,以文件的方式寫出來(省去日後還要上網查詢的時間)。

所以筆者在 fetchData.ts 裡,按照台北市的 UBike 即時資料格式 —— 運用型別化名(Type Alias)寫下來,將它化名成 SourceUBikeInfo

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

讀者可能認為以上的格式有寫沒寫都沒差,因為輸出的值全部都是 string 型別 —— 但是筆者建議儘量敘述源頭資料的狀況 —— 筆者這裡告訴 TypeScript 在資料還未處理前,全部的資料都是 string 型別,圖二的開發者工具中的 Console 裡面的內容確實全部都是對應到 string 型別。

第二個步驟就是定義我們的 App 想要轉換成的資料格式,筆者於是再次宣告另一種理想的格式型別化名 UBikeInfo 如下:

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

第二種 UBikeInfo 是我們希望輸出的型別:

  • availableBikes 必須從 SourceUBikeInfo 裡的 sbi 欄位取用,並且將 string 轉成 number
  • totalBikes 必須從 SourceUBikeInfo 裡的 tot 欄位取用,並且將 string 轉成 number
  • latLng 使用 leaflet 內建的 LatLngExpression 元組型別,必須從 SourceUBikeInfolatlng 兩個值 —— 除了轉換成 number 型別外,還要組織成 [number, number] 格式
  • regionName 則是對應 SourceUBikeInfo 裡的 sarea 欄位,不需進行型別轉換
  • stopName 則是對應 SourceUBikeInfo 裡的 sna 欄位,也不需進行型別轉換

於是筆者修改 fetchUBikeData 這個函式的內容:

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

看起來很複雜,筆者分兩段講。

首先,第一個部分:由於本來台北市 UBike 資料長得很奇怪,是 { retVal: [...], retCode: number } 格式,而我們只需要 retVal 這個屬性。

此外,retVal 裡面的值是這種格式:

{
  0001: SourceUBikeInfo,
  0002: SourceUBikeInfo,
  /* 以下略... */
}

筆者希望轉換成 SourceUBikeInfo[] 這種陣列,於是需要使用 Object.keys 結合 map 方法產生成 SourceUBikeInfo[] 的陣列形式。

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

另外,由以上的程式碼,筆者刻意註記為 retVal[key] as SourceUBikeInfo,理由是 —— 還記得之前筆者強調過:出現 any 型別的情形,其中就以 I/O 相關的機制常見,而 TypeScript 哪會知道你 fetch 到的外部資料格式為何,它當然會自動推論為 any 型別

因此這並不是在 TypeScript 專案裡樂見的行為。

我們必須在資料轉換的過程中,開始進行型別的積極註記動作喔

第二步驟:開始進行 SourceUBikeInfoUBikeInfo 的資料格式轉換。

https://ithelp.ithome.com.tw/upload/images/20191005/201206142McRA3b8Xd.png

讀者可能覺得這段程式碼好亂,但是這是資料格式轉換必經的過程 —— 不過這裡可以藉由型別的積極註記為 UBikeInfo 的話,不管過程再亂,我們只需要關注資料被轉換的結果有沒有被 TypeScript 監測

另外,多虧函式的推論機制,我們的 fetchUBikeData 函式的輸出推論結果自動被變成 Promise<UBikeInfo[]>。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191005/20120614ZMw6Nmri5s.png
圖三:推論為 Promise<UBikeInfo[]>

有些看到這裡的讀者冒出問號:“恩恩恩!?什麼是 Promise<UBikeInfo[]>?難道不是 UBikeInfo[] 而已嗎?”

事實上這是所謂的泛用型別(Generic Type)—— 這會在第四篇章《通用武裝》被筆者討論到

筆者這邊不負責任先提示:Promise<UBikeInfo[]> 代表的意思是,只要使用 fetchUBikeData 輸出的 Promise 物件,並且使用 then 方法:

fetchUBikeData()
  .then((A) => ...)

參數 A 之型別會被自動推論為 UBikeInfo[]

貼心小提示

這裡讀者若不知道 ES6 Promise 物件可以暫時看一下社群上的資源。

由於本篇討論的是應用,而非 Promise 物件的主題,所以並不會在本篇介紹過多跟 Promise 甚至是泛用型別相關的東西。

然而,Promise 物件將會在第四篇章《通用武裝》篇章介紹,目的是要讓讀者知道 Generic Types 的威力以及常見性 —— 儘管泛用型別是進階主題,但是這個東西會比讀者想像中還要常遇到。

所以我們可以確定,資料轉換過後的型別註記的部分也在 fetchData.ts 全部處理完畢。

因此在 index.ts 裡:

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

你可以發現我們的 data 參數被推論結果為 UBikeInfo[],如圖四。

https://ithelp.ithome.com.tw/upload/images/20191005/20120614D60pQN9lIr.png
圖四:data 自動被推論為 UBikeInfo[]

這樣的好處就是,如果我們隨便從該陣列資料拉出一個值,VSCode 就會自動幫我們偵測可以使用的屬性呢!(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191005/201206143ko1wLIIB4.png
圖五:VSCode 自動幫我們判斷我們可以使用的屬性

除了利用型別系統的註記機制外,然後在主程式能夠不靠註記而只靠型別推論就得知可以使用的功能 —— 這就是筆者想要從 TypeScript 開發過程中想要達到的效率。

打開瀏覽器,我們的資料已經被轉換成好讀的格式。(圖六)

https://ithelp.ithome.com.tw/upload/images/20191005/201206142Us3BGj98R.png
圖六:格式變得美美的~

台北市行政區資料與座標轉換

另外,筆者認為我們還需要把所有台北市的行政區資料提供出來 —— 就稱之為 districtData.ts,並且將其放置在 /src 這個檔案資料夾位置。以下是 ./src/districtData.ts 的內容:

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

第一個 districts 很簡單,就是一連串的所有行政區。

第二個就比較麻煩一些,但也不會麻煩到哪裡去。districtLatLngMap 使用的是 ES6 Map 這個資料結構 —— 筆者依然在這裡使用類似 ES6 Promise 的型別格式,也是使用泛用型別。

但 Map 可以接受的泛用型別有兩個 —— 第一個代表鍵(key)、第二個則是值(value),它可以這樣被使用:

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

貼心小提示

同理,ES6 Map 的推論註記行為會在第四篇章討論到!

不過呢,更精確的型別註記應該要先宣告 Districts 這個型別化名為所有行政區的名稱的 union

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

貼心小提示

當然,如果覺得這樣很麻煩,讀者也覺得沒有必要將 Districts 寫得這麼制式化,可以選擇退化為 string 型別也 OK。

然後將 districtsdistrictLatLngMap 改成:

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

另外,因為我們將台北市行政區所有可能的值設定為 Districts 這個型別,這也代表剛剛的 UBikeInfo 裡的 regionName 可能也必須從 string 改成 Disticts

可是這樣又要再把 Districts 這個型別額外建立一個檔案,然後再把該化名載入到 fetchData.ts 檔案,不如乾脆我們把所有的型別定義都匯聚在一個宣告檔(Declaration File)好了。

於是筆者在 /src 裡面額外新增 data.d.ts 這個檔案 —— 專門宣告型別的定義。以下是 data.d.ts 的內容:

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

於是你可以將 DistrictsSourceUBikeInfoUBikeInfo 的型別宣告從 fetchData.tsdistrictData.ts 拔除,並且從 data.d.ts 載入進去。

不過,讀者也可以選擇不要用 data.d.ts 檔案,而是普通的 data.ts 檔案,但改成 export type ... 的方式將型別化名(Type Alias)輸出出去也是可以的

以下是目前 fetchData.ts 的程式碼大致上的狀況。

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

以下是目前 districtData.ts 的程式碼大致上的狀況。

https://ithelp.ithome.com.tw/upload/images/20191005/201206146QADWEgZ1C.png

這樣子我們就可以把型別化名宣告部分整理到其他的檔案,若讀者想要查詢 UBikeInfo 等等實際上的內容,可以ㄧ樣按照筆者教過的技巧:

  1. 滑鼠滑到 UBikeInfo
  2. 按下 Command(Mac 系統)或 Ctrl(Windows 系統)
  3. 滑鼠點下去

就會立馬導到你所定義型別化名的地方喔。

最後還是在提醒一下:想要參考完整的程式碼,可以點擊這邊

小結

今天主要把資料處理的動作都交代清楚了,讀者應該可以很輕易地體會到 TypeScript 型別化名的好處,協助我們確保型別不會弄錯外,我們還可以藉由型別系統寫出 Documentation —— 創造出屬於專案的 Declaration Files 呢!

下一篇我們繼續本案例,將進度推展下去~!


上一篇
Day 38. 戰線擴張・模擬戰 — UBike 地圖 X Webpack 環境建構 - TypeScript Webpack Integration
下一篇
Day 40. 戰線擴張・模擬戰 — UBike 地圖 X 使用 LeafletJS - Using LeafletJS with TypeScript
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
戴均民
iT邦新手 4 級 ‧ 2019-10-20 16:35:14

我最近在公司的專案也做過類似的事情耶,這是我做的地圖

哇噻,這是全台灣的Bike地圖嗎,酷耶!/images/emoticon/emoticon12.gif

我要留言

立即登入留言