閱讀本篇文章前,仔細想想看
- 你會如何善用型別推論與註記的機制呢?
- 什麼情形可能會出現
any
型別推論出來的行為?如果出現了,要如何處理這類型的案例?另外,本篇文承接上一篇文,因此如果是跳到這篇的話可以先從上一篇看起喔~
貼心小提示
想要本範例的程式碼 —— 可以參見 Maxwell-Alexius/Iron-Man-Competition 這個 Repo 喔!
以下正文開始!
今天主要是先把功能給完成,但不代表今天就會結束掉本篇章系列,最後一篇會進行程式碼重構的動作 —— 不過在重構之前,沒有一個完整的功能前,基本上要談到重構的議題,也是沒辦法的。
貼心小提示
另外有一個名詞叫做 Premature Optimization —— 過早優化。儘管對一些比較資深的讀者,可能已經早就聽過它了,不過初入軟體設計的讀者可能還沒聽過,在此就補充一下。(
筆者本身也是沒多少經驗)軟體設計比較麻煩的地方應該是 —— 判斷程式是否該優化或者是重構的時間點,過早地將程式碼進行優化的動作反而會導致程式設計上的彈性被緊縮掉。
在還未將功能設計好的前提下,重構程式碼可能會導致功能還沒實踐完畢,就要面對種種設計上的限制,導致破壞掉原來重構出的架構的機率上升。另一種情形可能是在一開始設計軟體的規格時,可能沒有想到後續會出現什麼樣種類的問題等等,也就造成整個軟體的規格又必須改掉,於是有重構沒重構反而沒差,導致浪費時間。
讀者有空可以搜搜看並理解這句話的含義 —— “Premature optimization is the root of all evil.” by Donald Knuth
上一篇筆者已經把資料處理部分結束:
SourceUBikeInfo
這個型別對應 UBike 源頭資料的格式UBikeInfo
為筆者在這個應用程式裡需要用到的格式Districts
這個複合型別將所有台北市行政區的名稱字串 union
起來./src/data.d.ts
這個檔案fetchData
這個函式 —— 負責從台北市政府開放資料中的 UBike 即時資料系統抓出資料筆者在 UBike 地圖想要實踐的是:
由於筆者想要動態將選項從 districts
(行政區的陣列,為 Districts[]
型別)轉換成 HTML 表單相關的元素,必定會需要用到 document.createElement
相關的功能。
貼心小提示
相信會看本系列的讀者們(除非你是真的只有接觸 NodeJS 而沒前端經驗),普遍來說都應該熟悉原生 JS 操作 DOM 的流程。因此真的不清楚 DOM 的操作基礎可以上網查查看 DOM Manipulation 相關的資源啊!
首先,筆者更新一下 index.html
的內容 —— 使用 CSS Absolute Position 將選擇行政區域的 UI 放置在網頁的右上方,並且使用 <select>
與 <option>
這兩個元素做為行政區表單選項。
這是目前瀏覽器應該會出現的畫面,筆者刻意將顯示的倍率放大。(如圖一)
以下筆者就簡單地在 index.ts
裡,利用 districts
將所有的行政區選項轉換成 DOM。另外,如果讀者正在跟本篇文章的內容,記得要把 index.html
裡的 <option name="信義區">信義區</option>
砍掉,因為我們要改採動態方式新增 <option>
標籤喔!
這裡也會出現 TypeScript 的警告訊息喔!(如圖二)
圖二:$selectDistrict
可能為 null
ㄧ樣,我們可以使用 Type Guard 解決這個問題:
我們的問題就解決掉了,TypeScript 判斷我們的 $selectDistrict
這個變數存取的確定是 HTMLElement
型別。(如圖三)
圖三:沒有任何錯誤訊息,經過型別檢測就限縮掉 $selectDistrict
之型別為 HTMLElement
!
筆者在模擬戰 — UBike 地圖的第一篇提到,如果臨時對於 LeafletJS 的功能有疑惑的話,除了可以參考官網的 Doc 外,還有另一招就是翻閱 Leaflet 套件的 Declaration File 喔!
技巧到現在還不清楚的讀者,強烈建議看前幾篇文章(已經被 Reference 至少三次囉),後續筆者不會再重複講~
首先,由於會需要在不同的行政區畫一連串的 UBike 點位,因此筆者習慣會將所有產生的點位 —— 也就是 Leaflet 的 Markers 匯集在一起,這時候會需要一個變數 —— 儲存聚集所有點位的 LayerGroup
。
於是筆者先定義一個變數 markerLayer
並且積極註記為 LayerGroup
這個屬於 LeafletJS 提供的型別。這裡使用遲滯性指派(Delayed Initialization) —— 因此一定要積極註記避免 any
型別的出現。
接下來,就是實踐主要的功能。
貼心小提示
這邊的實作自由性很高,讀者當然也可以選擇不要使用筆者的做法達到相同的功能,這裡舉一個例子:實踐產生一個 Marker 的函式,並且在外面就可以用
for
迴圈對 UBike 點位進行迭代、匯集、然後再轉換為LayerGroup
也是可以的。
以下筆者直接使用 fetchData
(記得要從 fetchData.ts
載入進來)開始寫下轉換點位到 Leaflet Marker 的動作:
不過,我們必須先將行政區名稱從 <select>
元素取出來 —— 通常是使用 <select>
這個 DOM 的 value
屬性:
但這會出現一個問題:HTMLElement
並沒有 value
屬性。(如圖四)
圖四:HTMLElement
型別並沒有 value
屬性
這裡就要考驗讀者對於 DOM 本身的認識 —— 事實上並不是 HTMLElement
提供 value
這個屬性,而是 <select>
元素的 HTMLSelectElement
型別才會提供 value
這個屬性。
也就是說,之前在宣告 $selectDistrict
這個變數時,更好的做法是 —— 積極註記為 HTMLSelectElement
。
但是這裡要注意一點:$selectDistrict
原本的型別推論結果為 HTMLElement | null
代表 TypeScript 認為這裡有潛在 Bug 提醒開發者說:“你可能並沒有真正地選擇到元素,getElementById
裡的 ID 可能打錯字所以才會沒選到”。
因此,我們不僅僅要把 $selectDistrict
註記為 HTMLSelectElement
型別,另外還要將它和 null
進行複合,這應該才是正確流程,而不是使用 HTMLSelectElement
進行型別壟斷(此為筆者自創名詞,Type Monopolization)的動作:
因此,編輯器也沒有出現警告訊息了。(如圖五)
重點 1. 型別註記 V.S. 型別壟斷 Type Annotation V.S. Type Monopolization
型別壟斷為筆者自創名稱 —— 目的是描述型別註記誤用的一種情形,絕對不是官方或其他文章會出現的觀念。
通常,一個型別如果出現了複合的情形,又以
union
為主,如果進行型別註記時 —— 除非你可以抱持 100% 信心可以對型別進行積極註記而取代使用 Type Guard 進行型別限縮,否則貿然地對型別積極註記為複合型別中的其中一種情形而忽略掉其他狀況,此現象被筆者稱之為型別壟斷(Type Monopolization)—— 為強烈建議禁止的行為。以下以變數
A
為範例,若A
被 TypeScript 判定,型別推論為T | U
:若
A
被強行註記為T
或者是U
,而非經過 Type Guard 等安全性處理,此時就犯了型別壟斷的行為。
回過頭來,筆者繼續實作 fetchData
以後,將 UBike 資料轉換成 Leaflet Marker 的程式碼內容。
首先我們必須根據目前選到的行政區對 UBike 資料進行過濾的動作 —— 使用 Array.prototype.filter
這個方法。
筆者再次強調 —— 儘管讀者沒有看到以上的程式碼做任何型別註記,但筆者之前就有探討過某些情形是不需要對函式的參數進行註記的,而這些機制也會和泛用型別的機制有關,如果忘記的讀者請記得要好好看一下本系列函式型別篇章以及函式與陣列的相關篇章。
filter
裡面的回呼函式的 info
參數會被自動推論為 UBikeInfo
,所以並不是說筆者沒有應用到 TypeScript 的型別系統,而是 TypeScript 的型別系統在為筆者工作,確保筆者 100% 不會寫出會出現潛在 Bug 的程式碼。(如圖六)
圖六:就算參數 info
沒有被註記,此時的型別推論會自動鎖定 UBikeInfo
型別
第二個步驟就是將過濾出的點位轉換成 Leaflet Marker,這一次筆者使用 Array.prototype.map
方法。
這裡提醒一下,如果臨時忘記套件提供的 API 的功能,記得 TypeScript 提供給你的優勢,也就是型別系統;而 TypeScript 會根據 Leaflet 套件的 Declaration File 告訴你 new L.Marker
裡面的參數該填入什麼。(如圖七)
圖七:new L.Marker
裡面的細節
L.Marker
顯示的內容為:
constructor Marker<any>(
latlng: L.LatLngExpression,
options?: L.MarkerOptions | undefined
): L.Marker<any>
代表它除了是 Marker
的建構子函式外,第一個參數必須為 L.LatLngExpression
,正好是筆者的範例程式碼 UBikeInfo
的 latlng
對應的型別;第二個參數為 L.MarkerOptions
,如果讀者好奇或者臨時忘記有哪些參數可以帶進去,可以用筆者教過的方式,直接查找 Declaration File。(圖八為 L.Marker
的定義;圖九為 L.Marker
第二個參數 —— MarkerOptions
的定義)
圖八:藉由筆者之前教過的技巧,可以在 VSCode 裡從 L.Marker
找到原本的定義
圖九:此為 MarkerOptions
的介面定義
如果你會這個技巧,就可以節省時間不用上網查 Doc,直接在編輯器裡就可以查到 Declaration 以及該套件的 API 規格。
除了將 UBikeInfo
轉換成 Marker
外,還需要對 Marker 新增 Tooltip 顯示 UBike 站點的資料與自行車借用狀態。
貼心小提示
Tooltip 是一種提示性視窗,通常就是滑鼠滑到某個功能時,會跳出來的視窗。
marker.bindTooltip
以及一些細節就留給讀者去探索,跟剛剛筆者在解說 new L.Marker
的過程差不多。
以上的程式碼除了對每一個 Marker 新增 Tooltip 外,Leaflet Marker 還提供事件註冊的 API —— 因此筆者註冊兩個分別為 mouseover
與 mouseleave
事件,代表顯示或關閉 Tooltip。
最後,將 Marker 使用 L.layerGroup
包在一起後丟進一開始有宣告過的 markerLayer
變數(為 LayerGroup
型別),並且將其加到地圖裡。
這樣子,我們就完成基本的程序囉!打開瀏覽器來看,出現預設的區域 中正區
以及該區的 UBike 站點,甚至還可以用滑鼠檢視該地區的站點資訊喔!(如圖十)
圖十:UBike 地圖站點的結果
儘管已經做好了初步的準備,但筆者還沒有將 $selectDistrict
這個 <select>
元素註冊事件 —— 如果行政區被改變的話,應該要更新地圖的。
首先,由於建立 Leaflet Marker 的程式碼要被重複使用,因此筆者先把它包成函式並先呼叫一次,為的是要初始化地圖的狀態:
由於 updateUBikeMap
參數需要為 Districts
型別,因此除了要記得將 Districts
從 data.d.ts
載入進去外,currentDistrict
記得要註記為 Districts
。
另外,將 $selectDistrict
註冊一個 change
事件並嘗試更新地圖資訊:
以上的程式碼完成了!打開瀏覽器測試結果如圖十一。
圖十一:切換行政區時,可以更新整體的地圖狀況
本來筆者還想要多寫一個功能,就是當使用者選擇行政區時,要藉由上一篇宣告過的 districtLatLngMap
取的經緯度後,再讓地圖聚焦到那個座標,這樣子也會比較 User Friendly —— 不過筆者認為,讀者可以先試試看如何實踐這個功能。(提示:map.flyTo
這個方法)
下一篇筆者就先切入程式碼重構部分,敬請期待~