iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

1
Modern Web

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

Day 36. 戰線擴張・戰線分散 X 組織集中 - TypeScript Namespaces Import/Export Mechanism

https://ithelp.ithome.com.tw/upload/images/20191016/201206140V5nmbYmhw.png

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

  1. 命名空間的用意是什麼?
  2. 如何運用 TypeScript Namespaces 組織不同區塊的程式碼?
  3. 命名空間融合(Namespaces Merging)有沒有需要注意到的點?

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

筆者認為今天的文章應該會很噁心(至少對筆者來說)。

事實上,讀者應該使用到本篇提到的使用情形,應該很少,不過為了完整性,筆者還是補齊一下。(畢竟,ES6 Import/Export 還是常用)

不過筆者免不了地想大吼一下:“好想趕快進入《通用武裝》篇章啊 —— 泛用型別的東西真的精彩好多啊!啊!啊!啊!”

但是這邊東西不講又不行(本篇一切都是 TypeScript 版本歷史過程下遺留的塵埃,煙蒂掉了一大堆髒兮兮的,有些讀者可能讀完本篇後會和筆者有類似的體會),反正就冷靜地進入正文~

正文開始

命名空間載入/輸出的機制 Namespaces Import/Export Mechanism

引用命名空間的方式 Importing Namespaces From Another File

還記得在上一篇,筆者講到命名空間是可以被重複宣告的 —— 另外,如果複數個宣告為同一個命名空間的結果會融合成一個,這在官方被稱為 Declaration Merging

今天筆者ㄧ樣用簡單的範例來說明。其中,筆者先建立一個名為 Circle.ts 的檔案,內容如下:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614uRKRsW9CJ3.png

這應該對讀者來說挺簡單的,裡面有 PI 代表圓周率,以及計算圓的面積與周長的功能。

再來,如果我們要引用外部的命名空間 —— 根據 TypeScript 官方說明使用 Namespace 的方式 —— 筆者把官方在 Namespace 裡的 Splitting Across Files 某部分說明截下來:

Once there are multiple files involved, we’ll need to make sure all of the compiled code gets loaded. There are two ways of doing this.

First, we can use concatenated output using the --outFile flag to compile all of the input files into a single JavaScript output file.

(...中間略)

Alternatively, we can use per-file compilation (the default) to emit one JavaScript file for each input file. If multiple JS files get produced, we’ll need to use <script> tags on our webpage to load each emitted file in the appropriate order.

(以下是筆者覺得很麻煩的翻譯時間)

有兩種方式可以使用:

  1. 使用 outFile 這個編譯器設定將所有檔案打包在一起,也就是在本系列前幾天談到的一些內容。
  2. 如果是前端的話,可以直接把所有的檔案使用 <script> 引用進去,但順序很重要呢!

貼心小提示

理解 TypeScript 編譯器的某些設定是很重要的~除非有經驗的讀者早已熟悉,若跳過編譯器設定系列的內容建議趕快補齊呢!

前端引用法 Front-End Import Technique

筆者就先從比較簡單(但實際上不太好用?)的方式介紹,也就是官方所講的第二種方式 —— 在 HTML 檔案使用 <script> 引入模組。

1. 普通引用情形 Normal Case

以下筆者簡單地寫出測試的程式碼:

https://ithelp.ithome.com.tw/upload/images/20191002/20120614p9ZNZ33OEC.png

另外,筆者忘記提到一點,就算你把命名空間隔離到其他檔案,TypeScript 還是會認得那些命名空間,這應該算是 TypeScript Declaration 的 Magic 吧?(筆者如是說

所以圖一是 index.ts 的結果 —— 使用 Circle.ts 裡的 Circle 命名空間還是認得出來;圖二則是你在使用命名空間時,TypeScript 會很貼心地提示 —— Circle 有哪些可以用的屬性或方法,算是所謂的編輯器 Auto-Complete 的 Feature。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614WwCIvHriIS.png
圖一:左方為 index.ts、右方為 Circle.ts —— 就算在別的檔案使用 Circle,TypeScript 還是認得出來,因此不會出現警告喔

https://ithelp.ithome.com.tw/upload/images/20191002/20120614RwNSKvopbF.png
圖二:筆者打上 Circle 幾個字,後面就會出現提示性的功能,顯示 Circle 命名空間有的屬性與功能

筆者接下來按照 TypeScript 官方的說明,直接用預設模式進行編譯(使用 tsc),所以編譯出來應該會是分開的兩個檔案喔。(如圖三所示)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614hGZBOmDWR2.png
圖三:預設的 TypeScript 編譯器,編譯出來的結果

並且建立一個 index.html 檔案,將兩個 JavaScript 檔案引用進去。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614zpGeySXd6B.png

打開 index.html 可以看到結果如圖四。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614K3rt77IcPQ.png
圖四:瀏覽器的 Console 輸出結果

其中,如果檢視 Circle.js 編譯過後的結果(如圖五),你會發現它就是簡單的 IIFE 函式包裝起來的 —— 一般 JavaScript 在宣告一個簡單模組時會用到小技巧,只是被 TypeScript 用一行 namespace 的宣告進行包裝而已。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614ZM54N6jkNR.png
圖五:檢視瀏覽器的 Source,其中 Circle.js 被編譯過後的結果純粹只是簡單的 IIFE

貼心小提示

筆者當初學習原生 JavaScript 的時候,個人 naively 以為 IIFE 是不太會用到的東西 —— 因為無法想像會在什麼情況需要用到 Anonymous Function 然後馬上呼叫(Invoke)它。(簡單來說,當時的筆者菜味很重卻不自知

事實上,IIFE 好用的地方在於 —— 它可以將變數作用域進行隔離的動作,因此用到的時機比想像中多很多,想要組出更多更複雜的系統 —— 使用原生 JavaScript 無可避免地(Inevitably)會遇到 IIFE 這種東西

不過這也要怪當初設計語言的人將變數作用域設計成只有分全域(Global Scope)與函式作用域(Functional Scope),所以才會需要使用 IIFE 建構出乾淨的變數作用域。筆者沒有想要婊誰,但想要耍 P、婊一下當初設計 JavaScript 的人 XDDDDDD

所以學過原生 JavaScript 卻和(過往的)筆者抱持 “反正 IIFE 我也不會用到” 的心態的讀者,筆者誠心建議至少要了解一下 IIFE 的功用。

然後你就會涅槃到另一個境界請筆者停止講 P 話

2. 多組不同命名空間 Multiple Different Namespaces Distributed Among Different Files

另外,我們當然可以再新增更多檔案分佈不同的命名空間 —— 筆者另外新增名為 Rectangle.ts 的檔案,裡面的內容也是宣告一個名為 Rectangle 的命名空間。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614bu66yYW4BH.png

以下筆者更改一下 index.ts 裡的測試程式碼。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614HUF9L8feEp.png

想當然,index.html 勢必得更新。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614vlqJZU9L8H.png

不過別忘了,記得先下 tsc 編譯出結果再打開 index.html 喔!(測試結果如圖六)

https://ithelp.ithome.com.tw/upload/images/20191002/20120614QKACfjX4FQ.png
圖六:完全執行到正確的結果

3. 相同命名空間的融合 Identical Namespace Distributed Among Different Files

另外,命名空間融合的案例也是可以的

這裡筆者建立一個名為 MyMath 的檔案資料夾 —— 將原本的 Circle.tsRectangle.ts 丟進去,並且額外再包裝一層命名空間 MyMath

https://ithelp.ithome.com.tw/upload/images/20191002/20120614pvlvCaMpp8.png

https://ithelp.ithome.com.tw/upload/images/20191002/20120614G3bhtMVgVf.png

圖七為目前的檔案資料夾的分布狀態。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614yzHpL2Chd1.png
圖七:Circle.tsRectangle.ts 裡的命名空間都被包覆一層 MyMath,而兩個檔案都被放在 MyName 這個資料夾

最後,筆者附上 index.ts 的內容。

https://ithelp.ithome.com.tw/upload/images/20191002/20120614T67Jf27NHV.png

讀者如果測試本範例,一定會發現就算檔案被塞到更裡面的資料夾,TypeScript 依舊會記得所有的命名空間被宣告的結果 —— 並且也會根據上一篇提到的命名空間融合(Namespaces Merging)的規則,將重複宣告的命名空間進行融合。

當然,同類型的命名空間可以交互使用輸出的功能外,其他的功能(包含變數、函式、類別、型別的宣告)都不能互相覆寫 —— 除非是介面,因為介面也有介面融合的規則

前一篇重點沒看完至少也要看一下!

另外,index.html 也因此必須更新。(筆者知道更新內容超多,但這是為了展示給讀者看不同的案例下使用命名空間的標準流程)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614nvDNxcktZm.png

以下圖八是檢測的結果。(筆者強烈提醒:記得要先用 tsc 編譯才測試啊!!!)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614K7tpKE9OAe.png
圖八:檢測結果符合!

重點 1. 前端引用命名空間模組 Front-End Namespaces Import

若命名空間散佈在不同的檔案裡,且每個命名空間若有重複宣告時,符合命名空間融合的原則,則在普通編譯模式下 —— 編譯結果為每個 TypeScript 的檔案會對應出一個原生的 JavaScript 檔案。

其中,前端部分可以使用 <script> 標籤引入 Namespaces,但必須注意順序,通常會先把所有的命名空間相關的檔案載入後再載入主程式。

不過通常我們不會這麼麻煩地一個個將命名空間的檔案使用 <script> 標籤載入進去 —— 如果十個檔案必需載入進去,除非你可能會寫 Shell Script 自動化程序去遍歷所有跟 Namespaces 相關的檔案(但依然還是有順序搞錯的風險),否則你也只能手動將十個檔案硬刻進去。

所以這個方法才會被筆者說不太可行的辦法 —— 小型專案可能還可以接受

另外,這個方法也僅僅只能用在前端,後端如果硬用 NodeJS 執行當然會出錯,因為找不到模組!(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614WrwEdgpnK3.png
圖九:使用 NodeJS 後端執行一定會出錯

專案打包法與參照指令 Bundle Technique & Reference Directives

打包的方式應該是大家普遍會用的手法,不過讀者可能還是會問:“為何筆者還要花大篇幅講解很沒用的前端引用法?”

理由是:純粹比較好講解 XD,又可以快速驗證多個不同檔案拆開來時,命名空間的規則會不會跟著改變 —— 從剛剛驗證的過程得知的結論自然是不會有任何變化

很明顯地,outFile 這個編譯器設定又被派上用場。認真閱讀本系列的讀者一定知道,outFile 有使用上的限制:

必須要在模組規範為 amdsystem 下才能進行打包成單一檔案的動作,也就是說,編譯器設定裡的 module 選項只能為 'amd''system',預設的 commonjs 是錯誤的喔!

因此筆者將 tsconfig.json 裡面得編譯器設定為:

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

但是這還不夠

今天還要介紹另一個很邪門的東西 —— Triple Slash Directive,但筆者實在不喜歡直翻成中文,所以才另起一個中文名稱:參照指令(Reference Directives)。

其實用久了,你也自然而然不會覺得這個東西邪門

首先,由於想要在 index.ts 載入 MyMath 這個命名空間,你必須使用參照指令。

修改過後的 index.ts 如下。

https://ithelp.ithome.com.tw/upload/images/20191003/20120614TKAlB2UXIo.png

讀者可能覺得奇怪,還要一個個載入 Namespace,而且是用很奇怪的語法:

/// <reference path="<path-to-file>" />

筆者不開玩笑,而且多一條或少一條斜線就會視為不見,這是 Triple Slash Directive 的特性

另外,如果想要詳細看 Triple Slash Directive 的其他應用,可以直接點本連結,因為本系列會用到應該也就只有少數幾篇。不過筆者還是提醒,它還有其他功能,有興趣可以去翻看~

回過頭來,如果你嘗試使用 tsc 去編譯並且使用 node 執行。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614eqdznOxL4H.png
圖九:成功地使用 node 執行了檔案

另外,以下展示實際上 result.js 被編譯出來的結果。(程式碼有些長,不過請大致上看好結構就可以了!)

https://ithelp.ithome.com.tw/upload/images/20191003/20120614ZS6518vW67.png

可以看得出來,編譯的結果就只是把 CircleRectangle 用 IIFE 的方式載入進去。

讀者試試看

這邊讀者可以試試看:把 index.ts 的參照指令拿掉後,直接編譯並且執行 result.js 會發生什麼事情。

另外,如果 module 換成 system 模式,編譯出來的結果會不會動作?

另外,筆者就不示範將 result.js 載入前端 HTML 檔案測試囉~ 光是看到它是按照 IIFE 的格式產出就可以知道它一定可以在前端使用。

重點 2. 專案打包法載入命名空間 Bundle Technique to Import Namespaces

除了在前端一個個使用 <script> 方式引入命名空間外,可以選擇啟動編譯器設定 outFile 並且調整 moduleamdsystem 模式,將專案打包成一個檔案,同時可以在前端執行外,後端 NodeJS 也可以執行。

然而想要打包成單一個檔案,必須使用參照指令(Triple Slash Directives)引入命名空間模組,其語法如下:

https://ithelp.ithome.com.tw/upload/images/20191003/20120614ZOTVaRmK9c.png

多或者少一個斜線,亦或者是結尾的 /> 斜線少掉也不行

你真的會需要用到 Namespace 嗎?

事實上,普通的專案裡,我們不太需要用到 TypeScript Namespaces,因為 JavaScript 的解法就是用 IIFE 的方式解決外,ECMAScript 的 Import/Export 語法也解決了很多跟模組相關的問題。(請參照這篇 StackOverlow

你可能還會問說:“那 TypeScript 為何還要出什麼 Namespaces 呢?”

其實回答這個問題之前,事實上筆者沒有講到 —— TypeScript Modules 這一個東西。

你沒聽錯!

TypeScript Module 與 TypeScript Namespace 這兩者是完全不同的東西,在很久很久以前的時候(約 2014 年時灰姑娘沒換玻璃鞋、白雪公主還沒吃毒蘋果、愛麗絲還沒夢遊仙境時),那時候 Module 被稱為 External Module、Namespace 則是 Internal Modules。

筆者看到也是覺得 WTF,不過你想想看:2014 年那時候 ES2015 的標準還沒出來,所以 TypeScript 必須要有特定的解法去實踐模組的載入與輸出系統,於是 TypeScript Module 與 Namespaces 出來了。

所以對於現在的專案開發來說 Namespace 對於 TypeScript 或現在的 JS 開發而言是一種 Old Fashion 的手法,幾乎沒有出現了。

筆者還是想抱怨:“哎... 歷史遺留下來的 Legacy Feature。”

那麼筆者為何要讓大家知道有 TypeScript Namespace 這個東西呢~ 因為第三方套件 —— 有些的定義檔 Definition Files 就使用 Namespace 包裝一系列型別宣告相關的程式碼,然後就沒有再改,因為怕改了之後造成連鎖反應(Chain Reaction),讓相依的其他套件壞光光

如果這時想看原始碼還看不懂 Namespace 語法的話,想當然就尷尬了。

鐵人所見略同

引用自 —— 【 Day 27 】在 React 專案中使用 TypeScript - 命名空間(namespace) by Kira - TypeScript 初心者手札系列(精華版系列)

“換句話說,目前實際開發上似乎很少使用命名空間,大部分使用模組來組織程式碼結構,事實上真是如此嗎?可能要有實際開發經驗的前輩們來解答了!”

筆者卡這一關也卡很久,不過產出了這篇文之後,讀者讀過能夠回答這個問題了嗎?

小結

筆者總算把 TypeScript Namespace 的內容基本上涵蓋完畢了!

下一篇要講到很重要的 TypeScript 定義檔 —— Definition Files 外,還會簡單地示範如何引入第三方套件喔~。

這又比 TypeScript Namespace 的重要程度高幾百倍也不為過!敬請期待~


上一篇
Day 35. 戰線擴張・命名空間 X 組織分明 - TypeScript Namespaces Introduction
下一篇
Day 37. 戰線擴張・第三方套件 X 支援的引入 - 3rd-Party Package & TypeScript Declaration File
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言