ES6 引進 Module(模組化) 概念,每個 Module 自成獨立 Scope,各 Module 可自由定義變數、型別,要開放外界存取的項目再透過 export 開放。當需要引用其他 Module 時,則必須明確使用 import 匯入才能使用。如此各 Module 可獨立開發維護而不彼此干擾,甚至能實現需要時再動態載入,大幅提升開發及應用彈性。

TypeScript 也支援 Module,我目前的專案沒用到這麼高級的技巧,原本並不打算深入了解,但發現苗頭不對。開源專案如 Angular 2、Vue 早就 Module 滿天飛,不懂 Module 就看不懂範例程式及原始碼,遇到狀況也不知從何查起,感覺自己很廢,好吧,硬著頭皮也要學會。

開始之前推薦幾篇先修知識文章:

先簡單歸納幾則重點:

  1. TypeScript 程式碼只要出現 import 或 export,就會被視為 Module,編譯結果與一般 TypeScript 大不相同。
  2. TypeScript Module 編譯產生的 js 不能單純用 <script src="/img/loading.svg" data-src=".."> 載入,需依賴載入機制,載入機制分兩種:
    * 靜態載入: 編譯時將 Module js 檔打包成單一檔案,例如 Node.js 使用的 CommonJS
    * 動態載入: 網頁執行時視需要下載 Module js,例如: AMD 與 RequireJS
  3. 由於瀏覽器對 ES6 支援度不足,故需要額外的 Module 系統輔助, 目前還在百家爭鳴: AMD、CMD、closure、CommonJS、ES6。Visual Studio 的 TypeScript 編譯設定也可指定要用哪一種系統。
  4. 如果不想動用 Node.js,選擇 AMD Module 系統,網頁端使用 ReuqireJS 載入是最簡單的做法。

有了初步認識,來寫一個簡單範例當作練習。

在 ASP.NET MVC 專案 Scripts 目錄開一個 lab 資料夾,放入多個 ts 檔:

Module System 選擇 AMD:

在四個 ts ,我分別練習了不同的 export/import 寫法。首先是 common.ts,class 及 interface 前方加上 export 開放 Message 類別及 IOutput 介面:

//直接在const、function、class、interface前加上export關鍵字
export class Message {
    Time: Date = new Date();
    Text: string;
    constructor(text: string) {
        this.Text = text;
    }
}
 
export interface IOutput {
    Write(msg: Message);
}

console.ts 先從 common.ts 引用 IOutput 及 Message,最後將自己定義的新類別 ConsoleOutput 跟來自 common 的 Message export 出去(註: 如果自己不用純分享,可以寫成 export { Blah } from "./blah")。

import { IOutput, Message } from "./common";
 
class ConsoleOutput implements IOutput {
    Write(msg: Message) {
        console.log(`${msg.Time.toLocaleTimeString()} ${msg.Text}`);
    }
}
 
//各模組可export相同名稱項目
export const Version = "ConsoleOutput 1.0";
 
export { ConsoleOutput, Message };

另一個 dom.ts,import 的做法不太一樣。 * as com 會將 common 所有 export 項目包入名為 com 的變數,使用時需寫成 com.IOutput、com.Message,有點像 Namespace 的觀念。dom.ts 跟 console.ts 都匯出了名為 Version 的 const,由於引用方會用 import 明確宣告,我們不用擔心名稱衝突。最後 export 時加上 default 關鍵字,引用方可以直接寫 import 名稱 from "./dom" 取得 DomOutput。

//取得模組所有匯出項目,包成變數com的成員
import * as com from "./common";
 
class DomOutput implements com.IOutput {
    Write(msg: com.Message) {
        var div = $("<div></div>");
        div.text(`${msg.Time.toLocaleTimeString()} ${msg.Text }`);
        div.appendTo("body");
    }
}
 
//各模組可export相同名稱項目
export const Version = "DomOutput 1.0";
 
//export為預設項目,import時可直接引用
export default DomOutput;

測試程式 main.ts 如下,分別由 dom.ts/console.ts import 取得 DomOutput、ConsoleOutput、Message,餘下的寫法跟一般 TypeScript 無異。

import DomOutput from "./dom";
import { ConsoleOutput, Message } from "./console";
 
var c = new ConsoleOutput();
c.Write(new Message("console test"));
var d = new DomOutput();
d.Write(new Message("dom test"));

編譯出來的 main.js 如下: (注意: TypeScript Module 編譯成的 js 不能以 <script src="/img/loading.svg" data-src="scripts/lab/main.js"> 直接載入,會出現 "Mismatched anonymous define() modules" 錯誤,必須改用 require.js require() 函式載入。)

define(["require", "exports", "./dom", "./console"], function (require, exports, dom_1, console_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var c = new console_1.ConsoleOutput();
    c.Write(new console_1.Message("console test"));
    var d = new dom_1.default();
    d.Write(new console_1.Message("dom test"));
});
//# sourceMappingURL=main.js.map

測試網頁如下: (require.js 可使用 NuGet 安裝或從官網下載,接著用 require(["scripts/lab/main"]) 就能順利載入 main.js 並執行之。)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>TypeScript Module Test</title>
</head>
<body>
    <script src="Scripts/jquery-3.2.1.min.js"></script>
    <script src="Scripts/require.js"></script>
    <script>
        require(["scripts/lab/main"]);
    </script>
</body>
</html>

測試 OK,網頁與 Console 都成功印出訊息。而我們也能觀察到 RequireJS 先載入 main.js 再陸續載入 dom.js/console.js/common.js 的過程,代表它能解析相依性自動載入所需模組。

以上就是簡單的 TypeScript Module 演練,謝謝收看。


Comments

# by Ho.Chun

請問,我可以這樣理解嗎 Typescript 的 module 在 tsc 指令編譯之前,跟 ESModule / CommonJS 沒什麼關係 在 tsc 指令編譯時,可以選擇要編譯成哪一種的模組系統 ex. ESModule / CommonJS

# by Jeffrey

to Ho.Chun,對,同樣的 TypeScript 模組程式,可以編譯成搭配不同模組系統的 JavaScript。參考: https://www.typescriptlang.org/tsconfig#module

# by Ho.Chun

感謝 👏

# by 小克

TypeScript 之 Module - 點燈坊的文章連結換了 現在在這裡 https://old-oomusou.goodjack.tw/typescript/module/

# by Jeffrey

to 小克,已更新,謝謝通報。

# by ChrisTorng

突然看到這篇。最近我在用 <script type="module"> 方式引用 module,遇到一個老問題是 HTML 中不支援 TypeScript。而另外 import 來源的 js 是可以寫 TypeScript,但遇到 Visual Studio 無法透過 tsconfig.json 指定版本的問題,它預設使用相當舊的版本,導致一堆 Visual Studio 告警問題。但其實瀏覽器下載 ts 轉成的 js 是都正常。如果黑大有興趣研究的話,再開示一下?

# by ChrisTorng

忘了補充 <script type="module"> 可參考這篇 https://usefulangle.com/post/252/script-type-module

# by Jeffrey

to ChrisTorng,開發 TypeScript 用 VSCode 應該會比較輕鬆愉快,我個人覺得 VS 已經棄守進階前端開發這塊,而前端開發人員也多以 VSCode 為主,相關套件、資源豐富很多倍。我自己這幾年程式開發多採輕前端,以 cshtml 或 html 為主體,以 script src 載入 .js 寫 MVVM,JavaScript 程式偏小偏短,就沒再用 TypeScript 了。

Post a comment