跳到主要內容
黯羽輕揚每天積累一點點

命名空間_TypeScript 筆記 15

免費2019-04-13#TypeScript#TypeScript namespace vs module#TypeScript module keyword#TypeScript /// <reference#TypeScript internal module vs external module#TypeScript内部模块与外部模块

命名空間源自 JavaScript 中的模塊模式,是另一種代碼組織方式

一、由來

命名空間源自 JavaScript 中的 [模塊模式](/articles/模塊模式-javascript 設計模式 2/):

var MyModule = {};
(function(exports) {
  // 私有變量
  var s = "hello";
  // 公開函數
  function f() {
    return s;
  }
  exports.f = f;
})(MyModule);

MyModule.f();
// 錯誤 MyModule.s is not a function
MyModule.s();

由兩部分組成:

  • 模塊閉包(module closure):封裝模塊實現,隔離作用域
  • 模塊對象(module object):該模塊暴露出去的變量和函數

後來在此基礎上擴展出模塊動態加載,拆分到多文件等支持

TypeScript 結合模塊模式和類模式實現了一種模塊機制,即命名空間:

namespace MyModule {
  var s = "hello";
  export function f() {
    return s;
  }
}

MyModule.f();
// 錯誤 Property 's' does not exist on type 'typeof MyModule'.
MyModule.s;

編譯產物就是經典的模塊模式:

var MyModule;
(function (MyModule) {
  var s = "hello";
  function f() {
    return s;
  }
  MyModule.f = f;
})(MyModule || (MyModule = {}));
MyModule.f();
MyModule.s;

二、作用

類似於模塊,命名空間也是一種組織代碼的方式:

namespace Page {
  export interface IPage {
    render(data: object): string;
  }
  export class IndexPage implements IPage {
    render(data: object): string {
      return '<div>Index page content is here.</div>';
    }
  }
}

// 編譯結果為
var Page;
(function(Page) {
  var IndexPage = /** @class */ (function() {
    function IndexPage() {}
    IndexPage.prototype.render = function(data) {
      return '<div>Index page content is here.</div>';
    };
    return IndexPage;
  })();
  Page.IndexPage = IndexPage;
})(Page || (Page = {}));

同樣具有作用域隔離(上例僅暴露出 Page 一個全局變量),也支持按文件拆分模塊:

// IPage.ts
namespace Page {
  export interface IPage {
    render(data: object): string;
  }
}

// IndexPage.ts
/// <reference path="./IPage.ts" />
namespace Page {
  export class IndexPage implements IPage {
    render(data: object): string {
      return '<div>Index page content is here.</div>';
    }
  }
}

// App.ts
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
let indexPageContent = new Page.IndexPage().render({value: 'index'})

編譯結果為:

// IPage.js
空

// IndexPage.js
/// <reference path="./IPage.ts" />
var Page;
(function (Page) {
    var IndexPage = /** @class */ (function () {
        function IndexPage() {
        }
        IndexPage.prototype.render = function (data) {
            return '<div>Index page content is here.</div>';
        };
        return IndexPage;
    }());
    Page.IndexPage = IndexPage;
})(Page || (Page = {}));


// App.js
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
var indexPageContent = new Page.IndexPage().render({ value: 'index' });

注意到這裡通過三斜線指令引入被拆分出去的"namespace 模塊"(而不是像 module 一樣 import),仍用 import 的話,會得到報錯:

// 錯誤 File '/path/to/IndexPage.ts' is not a module.ts(2306)
import IndexPage from "./IndexPage";

P.S. 另外,可以通過 --outFile 選項生成一個 JS Bundle(默認編譯生成對應的同名 JS 散文件)

三、三斜線指令

支持 6 種指令:

  • 描述文件間依賴:/// <reference path="./myFile.ts" />,引用當前目錄下的 myFile.ts
  • 描述(類型)聲明依賴:/// <reference types="node" />,引用 @types/node/index.d.ts 類型聲明,對應 --types 選項
  • 顯式引用內置(類型)庫文件:/// <reference lib="es2015" />/// <reference lib="es2017.string" />,引用內置(類型)庫 lib.es2015.d.tslib.es2017.string.d.ts,對應 --lib 編譯選項
  • 禁用默認庫:/// <reference no-default-lib="true"/>,編譯過程中不加載默認庫,對應 --noLib 編譯選項,同時標記當前文件為默認庫(以致於 --skipDefaultLibCheck 選項能夠跳過檢查該文件)
  • 指定當前模塊的 AMD 模塊名:///<amd-module name="NamedModule"/>,指定 AMD 模塊名為 NamedModule
  • 指定 AMD 模塊依賴(已廢棄):/// <amd-dependency path="legacy/moduleA" name="moduleA"/>,依賴 legacy/moduleA,並指定引入模塊名為 moduleAname 屬性可選)

P.S. 更多示例,見 Triple-Slash Directives

形式上以 3 條斜線開頭,因此稱為三斜線指令(triple-slash directives),其中 XML 標籤用來表達編譯指令。其它注意事項如下:

  • 必須出現在文件首行(注釋除外)才有效。也就是說,一條三斜線指令前面只能出現單行注釋、多行注釋或其它三斜線指令
  • /// <amd-dependency /> 指令已廢棄,用 import "moduleName"; 代替
  • 指定 --noResolve 選項時,忽略掉所有 /// <reference path="..." /> 指令(不引入這些模塊)

作用上,/// <reference path="..." /> 類似於 CSS 中的 @import(在指定 --outFile 選項時,模塊整合順序與 path reference 指令順序一致)

實現上,在預處理階段會深度優先解析所有三斜線指令,將指定的文件添加到編譯��程中

P.S. 出現在其它位置的三斜線指令會被當做普通單行注釋,不報錯,但無效(編譯器不認)

四、別名

命名空間支持嵌套,因此可能會出現深層嵌套的情況:

namespace Shapes {
  export namespace Polygons {
    export class Triangle { }
    export class Square { }
  }
}

此時可以通過別名來簡化模塊引用:

import P = Shapes.Polygons;
import Triangle = Shapes.Polygons.Triangle;
let sq = new P.Square();
let triangle = new Triangle();

// 編譯後
var P = Shapes.Polygons;
var Triangle = Shapes.Polygons.Triangle;
var sq = new P.Square();
var triangle = new Triangle();

不難發現,這裡的 importvar 的語法糖:

This is similar to using var, but also works on the type and namespace meanings of the imported symbol.

因此在給一個值起別名時會創建一個新的引用

Importantly, for values, import is a distinct reference from the original symbol, so changes to an aliased var will not be reflected in the original variable.

例如:

namespace NS {
  export let x = 1;
}
import x = NS.x;
import y = NS.x;
(x as any) = 2;
y === 1;    // true

// 編譯後
var NS;
(function (NS) {
    NS.x = 1;
})(NS || (NS = {}));
var x = NS.x;
var y = NS.x;
x = 2;
y === 1; // true

P.S. import q = x.y.z 別名語法僅適用於命名空間,要求右側必須是 namespace 訪問

五、namespacemodule

TypeScript 1.5 之前只有 module 關鍵字,不區分內部模塊(internal modules)與外部模塊(external modules),二者都通過 module 關鍵字來定義。後來清晰起見,新增 namespace 關鍵字表示內部模塊

(摘自 namespace keyword

簡言之,關鍵字 modulenamespace 在語法上完全等價,例如:

namespace Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

module Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

的編譯結果都是:

var Shape;
(function (Shape) {
  var Rectangle;
  (function (Rectangle) {
    function getArea() { return Rectangle.a * Rectangle.b; }
    Rectangle.getArea = getArea;
  })(Rectangle = Shape.Rectangle || (Shape.Rectangle = {}));
})(Shape || (Shape = {}));

namespace 代替舊的 module 只是為了避免混淆,或者說是給 ES Module、AMD、UMD 等 Module 概念(所謂的外部模塊)讓道。因為如果霸佔著 module 關鍵字,實際上定義的不是 Module 而是 Namespace 的話,是很讓人迷惑的一件事

六、模塊與命名空間

內部模塊與外部模塊

也就是說:

  • 內部模塊:即命名空間,通過 namespacemodule 關鍵字聲明
  • 外部模塊:即模塊(如 ES Module、CommonJS、AMD、UMD 等),不用特別聲明,(含有 importexport 的)文件即模塊

外部模塊可以簡單理解為外部文件中的模塊,因為可以在同一文件中定義多個不同的 namespacemodule(即內部模塊),而無法定義多個 ES Module

P.S. 畢竟命名空間實質上是 IIFE,與模塊加載器無關,不存在文件即模塊的加載機制約束

概念差異

概念上,TypeScript 遵從 ES Module 規範(文件即模塊),通過編譯輸出 CommonJS、AMD、UMD 等模塊形式

而命名空間源自 JavaScript 中的 [模塊模式](/articles/模塊模式-javascript 設計模式 2/),算是舊時代的產物,不建議使用(用來 [聲明模塊類型](/articles/模塊-typescript 筆記 13/#articleHeader7) 除外)

加載機制差異

模塊引入機制上,命名空間需要通過三斜線指令引入,相當於源碼嵌入(類似於 CSS 中的 @import),會引入額外的變量到當前作用域中

P.S. 如果不打包成單文件 Bundle,就需要在運行時引入這些通過三斜線指令引入的依賴(例如通過 <script> 標籤)

而模塊則通過 import/require 等方式引入,由調用方決定是否通過變量去引用它,而不會主動影響當前作用域

P.S. import "module-name"; 語法就只引入模塊(的副作用),不引用並訪問模塊,具體見 import

最佳實踐

在模塊與命名空間的使用上,有一些實踐經驗:

  • 減少了命名空間嵌套層級,比如只含有靜態方法的 class 通常是不必要的,模塊名足夠表達語義
  • 模塊僅暴露一個 API 時,用 export default 更合適,引入更方便,而且調用方不必關注 API 名
  • 要暴露出多個 API 的話,都直接 export(數量過多的話,[引入模塊對象](/articles/module-es6 筆記 13/#articleHeader5),如 import * as largeModule from 'SoLargeModule'
  • 通過 re-export 擴展現有模塊,例如 export as
  • 不要在模塊裡使用命名空間,因為模塊本就具有邏輯結構(文件目錄結構)和模塊作用域,命名空間提供不了更多好處了

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論