iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 26
0
自我挑戰組

Typescript 初心者手札系列 第 26

【Day 26】在 React 專案中使用 TypeScript - 模組(Module)觀念(下)

昨天提到了模組觀念以及撰寫語法,今天要來探討 TS 模組編譯成 JS 的機制了!

模組編譯設定

我們無法直接使用 .ts 檔案中的模組,一定需要編譯成 JS 檔案,而模組的編譯會取決於設定的目標環境,在 TS 中可以自行決定如何編譯,在tsconfig.json檔案中的 compilerOptions 的 module 中進行設定。

{
    "compilerOptions": {
        "target": "es5",  //指定編譯生成的JS版本
        "module": "commonjs", //指定生成哪種模組
    }
}

模組編譯規範

模組編譯目前有各種不同的標準規範,TS 可以選擇 None、AMD、CMD、System、closure、CommonJS、ES6、ES2015 或 ESNext 等(關於 tsconfig.json 檔的設定可見Day03文章)。

針對模組規範,來個小小的比較吧!

  • AWD:RequireJS 定義的標準,動態加載(異步),主要針對瀏覽器運行JS。
  • CWD: seaJS 定義的標準
  • closure:google 出的編譯規範
  • CommonJS: Node.js 使用的模組化標準,browserify 讓使用 CommonJS 模組化規範的程式碼可以運行在 client 端,靜態加載(同步)

如何選擇呢? 選擇模組編譯標準會取決於專案性質模組加載器(module loader)和 模組打包工具(module bundlers)。模組加載器的作用是在執行模組前去查找並執行這個模組的相關檔案。最常見的 JS 模組加載器就是 Node.js 的 CommonJS 和 Web 應用程式的Require.js。在實際開發上,假設設定編譯規範為 AMD ,你可能必須使用RequireJS; 如果是 CommonJS,則可能需要使用 webpack 來打包程式碼才能在瀏覽器上看。

來個範例,看看較常見的模組規範 AMD、CommonJS 和 ES6 究竟會編譯成什麼呢?

模組編譯指令

要進行編譯,我們必須要輸出指令告知編譯器要使用哪種規範。

tsc --module 模組規範 檔案名稱

編譯完成後,每個模組會產生相對應的JS檔案

欲編譯的 TS 檔案
// greeter.ts
export function greet() {
    return `Hello, world`;
}

// main.ts

import { greet } from "./greet";
let messsage = greet()
console.log(messsage);
使用 AMD / RequireJS 編譯出來的 JS 檔案

AMD要記住的關鍵概念是定義函式和require函式。定義函式定義相依的模組,而require函式則撰寫主要程式碼。

// generated greet.js
define(["require", "exports"], function (require, exports) {
    "use strict";
    exports.__esModule = true;
    function greet() {
        return "Hello, world";
    }
    exports.greet = greet;
});

// generated main.js

define(["require", "exports", "./greet"], function (require, exports, greet_1) {
    "use strict";
    exports.__esModule = true;
    var messsage = greet_1.greet();
    console.log(messsage);
});
使用 CommonJS 編譯出來的 JS 檔案

CommonJS 要記住的兩個關鍵就是module.exports / exports和require函式。

// generated greet.js
"use strict";
exports.__esModule = true;
function greet() {
    return "Hello, world";
}
exports.greet = greet;

// generated main.js
"use strict";
exports.__esModule = true;
var greet_1 = require("./greet");
var messsage = greet_1.greet();
console.log(messsage);
使用 ES6 編譯出來的 JS 檔案
// generated greet.js
export function greet() {
    return "Hello, world";
}
// generated main.js

import { greet } from "./greet";
var messsage = greet();
console.log(messsage);

編譯完會注意到,當 module 設定為 ES6 時,編譯出來的 JS 和 TS 原本的程式碼很類似,因為 TS 本質上使用的就是 ES 模組。

模組解析(module resolution)

模組解析就是指編譯器在查找導入模組內容時所遵循的流程。
假設有一導入程式碼 import { a } from "moduleA",為了要確認如何使用 a,編譯器需要確切的知道如何找,並檢查 moduleA 的定義。

  1. 首先,編譯器會嘗試定位導入模組的檔案,會遵循兩種方式之一:Classic 或 Node。
  2. 倘若解析失敗,且模組是非相對導入,則編譯器會嘗試定位外部宣告的模組。
  3. 倘若還是找不到,就會報錯 Error : Cannot find module 'moduleA'

相對v.s 非相對模組導入

相對導入指的是以相對路徑 開頭/,./或../方式表示模組檔案位置,例如

import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";

其他的方式則稱為非相對導入,例如:

import * as $ from "jQuery";
import { Component } from "@angular/core"

通常自己寫的程式碼會使用相對導入方式,而引入外部函式庫則常利用非相對導入。

模組解析方式:Classic 和 Node

TS 支援上面提到的兩種模組解析方式:Node 和 Classic。在編譯時,可以使用--moduleResolution 指令來指定要使用哪種方式。若沒指定,則編譯規範 AMD 、 System 和 ES2015 的預設值為 Classic,其他情況則是 Node。

Classic解析

針對相對導入的模組,會從導入該模組的檔案位置出發查找,因此 /root/src/folder/A.ts檔案中的 import { b } from "./moduleB" 的查找流程為針對同層資料夾尋找:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

針對非相對導入的模組,則會從導入模組的該檔案資料夾開始往上查找,假設在 /root/src/folder/A.ts 檔案中有 import { b } from "moduleB",則查找順序如下:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

Node解析

Node針對相對路徑和非相對路徑一樣會有不同查找方式。

針對相對路徑,假設/root/src/moduleA.js檔案包含了一個導入模組 var x = require("./moduleB"),則查找流程如下:

  1. 檢查 /root/src/moduleB.js檔案是否存在
  2. 檢查 /root/src/moduleB 資料夾是否有 package.json檔案,且package.json 檔案指定了一個"main"模組:{ "main": "lib/mainModule.js" },如果有,就會使用這個檔案引入
  3. 檢查 /root/src/moduleB資料夾是否有 index.js 檔案,若有,這個檔案會視為"main"模組

針對非相對路徑,Node會前往node_modules資料夾進行查找。
同樣的例子,架設/root/src/moduleA.js檔案使用了非相對路徑 var x = require("moduleB"),則查找流程如下:

  1. /root/src/node_modules/moduleB.js

  2. /root/src/node_modules/moduleB/package.json (指定"main"屬性)

  3. /root/src/node_modules/moduleB/index.js

  4. /root/node_modules/moduleB.js

  5. /root/node_modules/moduleB/package.json (指定"main"屬性)

  6. /root/node_modules/moduleB/index.js

  7. /node_modules/moduleB.js

  8. /node_modules/moduleB/package.json (指定"main"屬性)

  9. /node_modules/moduleB/index.js

TS 在解析模組時就是模仿 Node 方式進行,只是在查找檔案時會多查找 .ts,.tsx和.d.ts的檔案,而在 package.json 中使用"types"而非"main"。

針對相對路徑導入,假設在有./moduleB"在/root/src/moduleA.ts檔案中有一個導入模組 import { b } from "./moduleB" ,則查找流程如下:

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (如果指定"types"屬性)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

同樣地,非相對路徑導入也是同樣的邏輯,就不多說了。

tsconfig.json設定模組解析

有些時候,我們會利用tsconfig.json設定檔中的 baseUrl 和 path 來標記模組解析路徑

{
  "compilerOptions": {
    "baseUrl": ".", // 查找根目錄
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // 相對於baseUrl的路徑"
    }
  }
}

如果baseUrl改變,則paths也會跟著改,舉例來說,如果上面範例改成 "baseUrl": "./src",則 jquery 的paths 應該改成 "../node_modules/jquery/dist/jquery"。

參考資料:
TypeScript官網- module resolution, module
Typescript Deep Dive


上一篇
【Day 25】在 React 專案中使用 TypeScript - 模組(Module)觀念(上)
下一篇
【Day 27】在 React 專案中使用 TypeScript - 命名空間(namespace)
系列文
Typescript 初心者手札30

2 則留言

0
Maxwell Alexius
iT邦新手 5 級 ‧ 2019-10-11 10:58:26

模組規範實在是多到不堪負荷 /images/emoticon/emoticon33.gif

Kira iT邦新手 5 級‧ 2019-10-11 13:04:58 檢舉

哇!是Maxwell大神,謝謝你的TypeScript系列文章幫助我釐清了好多觀念!真的 〒.〒 每一個感覺都是深抗,需要花蠻多時間研究的/images/emoticon/emoticon06.gif

不過基本上能夠用到就好,加油~~一起成長~~ /images/emoticon/emoticon37.gif

0
Chris
iT邦新手 5 級 ‧ 2019-10-12 14:44:59

邊學 TS 邊吃臭豆腐特別有共鳴囉?

Kira iT邦新手 5 級‧ 2019-10-13 14:23:59 檢舉

這是什麼Chris梗XD 不懂為什麼是臭豆腐呢?/images/emoticon/emoticon19.gif

我要留言

立即登入留言