昨天提到了模組觀念以及撰寫語法,今天要來探討 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檔案
// greeter.ts
export function greet() {
return `Hello, world`;
}
// main.ts
import { greet } from "./greet";
let messsage = greet()
console.log(messsage);
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 要記住的兩個關鍵就是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);
// 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 模組。
模組解析就是指編譯器在查找導入模組內容時所遵循的流程。
假設有一導入程式碼 import { a } from "moduleA"
,為了要確認如何使用 a,編譯器需要確切的知道如何找,並檢查 moduleA 的定義。
相對導入指的是以相對路徑
開頭/,./或../方式表示模組檔案位置,例如
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
其他的方式則稱為非相對導入,例如:
import * as $ from "jQuery";
import { Component } from "@angular/core"
通常自己寫的程式碼會使用相對導入方式,而引入外部函式庫則常利用非相對導入。
TS 支援上面提到的兩種模組解析方式:Node 和 Classic。在編譯時,可以使用--moduleResolution 指令來指定要使用哪種方式。若沒指定,則編譯規範 AMD 、 System 和 ES2015 的預設值為 Classic,其他情況則是 Node。
針對相對導入的模組,會從導入該模組的檔案位置出發查找,因此 /root/src/folder/A.ts
檔案中的 import { b } from "./moduleB" 的查找流程為針對同層資料夾尋找:
針對非相對導入的模組,則會從導入模組的該檔案資料夾開始往上查找,假設在 /root/src/folder/A.ts 檔案中有 import { b } from "moduleB",則查找順序如下:
Node針對相對路徑和非相對路徑一樣會有不同查找方式。
針對相對路徑,假設/root/src/moduleA.js檔案包含了一個導入模組 var x = require("./moduleB"),則查找流程如下:
針對非相對路徑,Node會前往node_modules資料夾進行查找。
同樣的例子,架設/root/src/moduleA.js檔案使用了非相對路徑 var x = require("moduleB"),則查找流程如下:
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json (指定"main"屬性)
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json (指定"main"屬性)
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json (指定"main"屬性)
/node_modules/moduleB/index.js
TS 在解析模組時就是模仿 Node 方式進行,只是在查找檔案時會多查找 .ts,.tsx和.d.ts的檔案,而在 package.json 中使用"types"而非"main"。
針對相對路徑導入,假設在有./moduleB"在/root/src/moduleA.ts檔案中有一個導入模組 import { b } from "./moduleB" ,則查找流程如下:
同樣地,非相對路徑導入也是同樣的邏輯,就不多說了。
有些時候,我們會利用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
模組規範實在是多到不堪負荷
哇!是Maxwell大神,謝謝你的TypeScript系列文章幫助我釐清了好多觀念!真的 〒.〒 每一個感覺都是深抗,需要花蠻多時間研究的
不過基本上能夠用到就好,加油~~一起成長~~