iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
Modern Web

用不到 30 天學會基本 TypeScript系列 第 24

模組 & 命名空間 ( Modules & Namespaces )

  • 分享至 

  • xImage
  •  

相信使用 ES6 的小夥伴們對模組都不陌生,現代前端框架也都使用模組在運作。而模組和命名空間是兩種組織和封裝程式碼的方式,雖然它們都有類似的目標,但它們在某些方面有不同的特性和用途。

模組 ( Module )

TypeScript 提供了不同種類的模組系統,包括 CommonJS、ES6、AMD 和 UMD 等。我們可以根據您的項目需求選擇最合適的模組系統,但通常前端開發還是建議使用 ES6 模組。以下將以 ES6 模組來說明:

模組允許將相關的變數、函數、類型和接口等封裝在一個單獨的檔案中,這有助於減少全局命名衝突,同時提供了導出 ( export )和導入 ( import )功能,提高程式碼的重用性、維護性和模組化。

  • 封裝性 模組允許將相關的代碼封裝在一個單獨的檔案中,並將其暴露給外部只有有限的訪問權限。

  • 可重用性 模組可以在不同的檔案中定義,並且可以在需要時進行導入和重複使用。

以下是使用 ES6 模組的範例:

// module.ts

// 導出
export const greet = "Hello, TypeScript!";

export const sayHello = (name: string): void => {
  console.log(`哈囉,${name}!`);
};
// app.ts

// 導入並使用模組
import { greet, sayHello } from "./module.js"; 

console.log(greet); // 輸出: Hello, TypeScript!
sayHello("威爾豬"); // 輸出: 哈囉,威爾豬!

在這個範例中,我們在 module.ts 檔案中使用 export 導出變數 greet 和 sayHello 函式。然後,我們在 app.ts 中使用 import 來導入這些模組。

注意: 為了方便演示,範例這邊導入模組最後加上 .js 副檔名,是為了讓編譯後的 app.js 自動帶入 .js 副檔名,不然會找不到檔案。( 原則上開發是不應該加上副檔名的。)

https://ithelp.ithome.com.tw/upload/images/20230924/20141250l6svcGVVZV.png

再看另一個例子,假設我們正在開發一個簡單的購物車系統,我們需要將商品和購物車分為不同的模組:

// product.ts

export interface IProduct {
  id: number;
  name: string;
  price: number;
}
// cart.ts

import { IProduct } from "./product.js";

export class ShoppingCart {
  private items: IProduct[] = [];

  addItem(item: IProduct) {
    this.items.push(item);
  }

  getItems(): IProduct[] {
    return this.items;
  }
}
  • product 模組定義了一個 IProduct 接口,表示商品的基本結構。
  • cart 模組導入了 IProduct 接口,並定義了一個 ShoppingCart 類別,用於管理購物車。

我們 app.ts 中使用這些模組:

// app.ts

// 導入模組
import { IProduct } from "./product.js";
import { ShoppingCart } from "./cart.js";

const apple: IProduct = { id: 1, name: "Apple", price: 44900 };
const samsung: IProduct = { id: 2, name: "Samsung", price: 43900 };

const cart = new ShoppingCart();
cart.addItem(apple);
cart.addItem(samsung);

const items = cart.getItems();
console.log("購物車內容:", items); //輸出: 購物車內容:[{id: 1, name: "Apple", price: 44900}, {id: 2, name: "Samsung", price: 43900}]

從這個範例我們可以看到,模組讓程式碼更有組織性,並且可以在不同的檔案中分開定義和導入功能。

命名空間 ( Namespace )

使用方式:namespace 名稱 {}
引入文件依順序建立相依性: /// <reference path="檔案路徑" />

命名空間提供了一種將相關功能和類別組織在一起的方法,以 避免在全域中發生命名衝突的問題。不過命名空間通常主要在具有 較舊的模組系統或非模組的 JavaScript 環境中使用

看以下範例:

我們創建一個名為 Shapes 的命名空間,其中包含了 Circle 和 Rect 兩個類別,並在 app.ts 中使用 Shapes 裡的 Circle 和 Rect 類別。

// shapes.ts

namespace Shapes {
  abstract class Shape {
    constructor(protected a: number, protected b?: number) {}
  }

  export class Circle extends Shape {
    calculateArea(): number {
      return Math.round(Math.PI * this.a ** 2);
    }
  }

  export class Rect extends Shape {
    calculateArea(): number {
      return this.a * this.b;
    }
  }
}
// app.ts

// 建立相依性
/// <reference path="shapes.ts" />

const circle = new Shapes.Circle(10);
const rect = new Shapes.Rect(4, 6);

console.log("圓形面積:", circle.calculateArea()); // 輸出: 圓形面積: 314
console.log("方形面積:", rect.calculateArea()); // 輸出: 方形面積: 24

當然我們也可以修改成使用別名的方式:

// app.ts

// 使用別名
import circleAlias = Shapes.Circle;
import rectAlias = Shapes.Rect;

const circle = new circleAlias(10);
const rect = new rectAlias(4, 6);

console.log("圓形面積:", circle.calculateArea()); // 輸出: 圓形面積: 314
console.log("方形面積:", rect.calculateArea()); // 輸出: 方形面積: 24

編譯後,記得要在 html 加入 shapes.js,不然會出錯誤。

https://ithelp.ithome.com.tw/upload/images/20230924/20141250XFnwVtboBe.png

<!-- dist/index.html -->

<!DOCTYPE html>
  <!-- ... -->
  <body>
    <script type="module" src="./app.js"></script>
    <script src="shapes.js"></script> <!-- 加入 -->
  </body>
</html>

因為目前威爾豬只單純使用 tsc 環境的關係,編譯後還要自己手動加上檔案,有點麻煩。除非我們特別去修改,讓它最後只編譯出一隻 app.js,這樣就不需要再去 html 加入 shapes.js 了。不過這種方式比較不推薦,因為它只支持 AMD 和 SystemJS 模組系统,不適用其他的模組 ( ex: ES6 )。

// tsconfig.json

{
 "compilerOptions": {
    // ...
    "module": "AMD", // AMD 或 System
    "outFile": "./dist/app.js",  // 指定要編譯出的檔案位置和名稱
    // ...
  }
}

所以威爾豬改成在 Namespace 使用 Module 的方式 ( ❌ 不要用 ):

// shapes.ts

export namespace Shapes {
  abstract class Shape {
    constructor(protected a: number, protected b?: number) {}
  }
  export class Circle extends Shape {
    calculateArea(): number {
      return Math.round(Math.PI * this.a ** 2);
    }
  }

  export class Rect extends Shape {
    calculateArea(): number {
      return this.a * this.b;
    }
  }
}
// app.ts

import { Shapes } from "./shapes.js";

const circle = new Shapes.Circle(10);
const rect = new Shapes.Rect(4, 6);

console.log("圓形面積:", circle.calculateArea()); // 輸出: 圓形面積: 314
console.log("方形面積:", rect.calculateArea()); // 輸出: 方形面積: 24

雖然最後在模組上使用命名空間解決了問題,但官方有明確的跟我們說:不要在模組中使用命名空間,因為命名空間在使用模組時提供的價值非常少 ( 如果有的話 ) 。

https://ithelp.ithome.com.tw/upload/images/20230924/20141250a6psjotuqN.png

https://ithelp.ithome.com.tw/upload/images/20230924/20141250DvLY1gWNtJ.png


所以當我們決定使用模組或命名空間時,應該根據專案來選擇。以下是一些參考因素:

  • 如果專案是一個複雜的大型應用程式,且需要管理多個模組,則 Module 是更好的選擇。
  • 如果專案比較小且不需要太多模組化結構,或者是一個非模組的 JavaScript 環境,則使用 Namespace。

雖然威爾豬自己沒用過 Namespace 的方式,不過除非有特殊需求,否則在新的 TypeScript 專案中 ( 不管大小專案 ),使用 Module 可能會是更好的選擇。


上一篇
類別封裝 ( Class Encapsulation )
下一篇
型別縮小 ( Narrowing )
系列文
用不到 30 天學會基本 TypeScript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言