
之前有轉(zhuǎn)載過(guò)一篇 JavaScript 中的模塊導(dǎo)入和導(dǎo)出 ,但是沒(méi)有系統(tǒng)的進(jìn)行說(shuō)明,只是提到了模塊該如何使用,這里結(jié)合 TypeScript 來(lái)系統(tǒng)的啰嗦一番。
藍(lán)貓?zhí)詺馊?wèn):
- 前端的模塊是怎么工作的?
- TypeScript 中的模塊如何查找的,為什么會(huì)隱式查找到
index.ts、index.js,為什么會(huì)到node_modules中去找模塊? - 如何定義一個(gè)全局變量供所有代碼共享?
-
tsconfig.json文件有什么用,自定義模塊別名@/*是如何映射到指定目錄的?
帶著這些問(wèn)題,我們開(kāi)始今天的探索之旅!
什么是模塊
引用一段百度百科對(duì)模塊的解釋:
在程序設(shè)計(jì)中,為完成某一功能所需的一段程序或子程序,或指能由編譯程序、裝配程序等處理的獨(dú)立程序單位;或指大型軟件系統(tǒng)的一部分
模塊可以和大多數(shù)編程語(yǔ)言中的 命名空間、package 等概念進(jìn)行關(guān)聯(lián),在模塊中定義的變量、函數(shù)、類(lèi),如果不經(jīng)過(guò)特殊處理,一般只有模塊內(nèi)能夠訪問(wèn),這樣可以避免與其他模塊沖突。由此可見(jiàn)模塊的功能是很重要的。
早期 JavaScript 并沒(méi)有模塊的概念,當(dāng) Node.js 被推出之后,JavaScript 才逐漸引入了模塊的概念,而TypeScript也沿用這個(gè)概念。有興趣的可以查看前端模塊化的歷程。
在 CommonJS && ES6 模塊化方案中, 一個(gè)模塊里的變量,函數(shù),類(lèi)等等在模塊外部是不可見(jiàn)的,除非明確地使用 export 導(dǎo)出它們。 相反,如果想使用其它模塊導(dǎo)出的變量,函數(shù),類(lèi),接口等的時(shí)候,你必須使用import導(dǎo)入它們。
如何創(chuàng)建模塊
JavaScript 的模塊是自聲明的,事實(shí)上我們?cè)趯?xiě)代碼的時(shí)候一直在不知不覺(jué)中以模塊的形式進(jìn)行書(shū)寫(xiě)。
文件模塊
只要一個(gè) JavaScript 文件中包含 imports 導(dǎo)入模塊 或者 exports 導(dǎo)出模塊 的聲明,那它就是一個(gè)模塊,嚴(yán)謹(jǐn)點(diǎn)應(yīng)該叫文件模塊。
在前端模塊實(shí)際上是通過(guò)閉包來(lái)實(shí)現(xiàn)的,一個(gè)模塊就是一個(gè)閉包,類(lèi)似下面這樣:
編譯前:
// 1、依賴導(dǎo)入、變量聲明
export class module {
// 2、模塊內(nèi)部實(shí)現(xiàn)
}
編譯后:
const module = (function(){
// 1、依賴導(dǎo)入、變量聲明
// 2、模塊內(nèi)部實(shí)現(xiàn)
})();
這樣就能夠?qū)⒏鱾€(gè)文件的實(shí)現(xiàn)給隔離開(kāi),達(dá)到模塊化的目的。
全局模塊
如果一個(gè)文件沒(méi)有包含imports或exports呢,根據(jù)上面的描述這個(gè)文件不是一個(gè)模塊,那它是什么?
實(shí)際上,它是一種特殊的模塊,我們稱之為“全局模塊”,這個(gè)模塊里面的任何定義都是全局共享的!毋庸置疑,使用全局模塊是危險(xiǎn)的,因?yàn)樗鼤?huì)與文件內(nèi)的其他代碼命名沖突。但是全局模塊可以用在一些特殊的場(chǎng)景,比如使用頻繁的一些變量或方法,可以放在全局模塊進(jìn)行聲明,避免每次使用都需要導(dǎo)入。
模塊的導(dǎo)出
導(dǎo)出聲明
任何聲明(比如變量,函數(shù),類(lèi),類(lèi)型別名或接口)都能夠通過(guò)添加 export 關(guān)鍵字來(lái)導(dǎo)出。
export const a = "123" // 導(dǎo)出變量
export class CotpButton {} // 導(dǎo)出類(lèi)
export interface ConfigInfo {}; // 導(dǎo)出接口
導(dǎo)出語(yǔ)句
導(dǎo)出語(yǔ)句支持將需要導(dǎo)出的模塊包裝到一個(gè)對(duì)象中,并且支持對(duì)導(dǎo)出的部分重命名:
import BaseComponent from "./src/base/BaseComponent"
import CotpButton from "./src/components/button/CotpButton"
export {
BaseComponent,
CotpButton as Button
}
重新導(dǎo)出
我們經(jīng)常會(huì)去擴(kuò)展其它模塊,有時(shí)可能會(huì)合并之后重新導(dǎo)出供外部使用:
// 重新導(dǎo)出部分模塊
export { pushContants } from "./lib/constants"
// 重新導(dǎo)出部分模塊并且重命名
export { pushContants as sfPushContants } from "./lib/constants"
// 重新導(dǎo)出全部模塊
export * from "./lib/constants"
默認(rèn)導(dǎo)出
每個(gè)模塊都可以有一個(gè)default導(dǎo)出。 默認(rèn)導(dǎo)出使用default關(guān)鍵字標(biāo)記;并且一個(gè)模塊只能夠有一個(gè)default導(dǎo)出。
export default 可以理解為等價(jià)于 const 任意變量名 =(這里的“任意變量名”是用來(lái)給其他模塊導(dǎo)入這個(gè)默認(rèn)模塊時(shí)候使用的),導(dǎo)出類(lèi)和函數(shù)的名字可以省略,也可以導(dǎo)出一個(gè)值。
export default class {} // 導(dǎo)出一個(gè)匿名類(lèi)
export default function () {} // 導(dǎo)出一個(gè)匿名函數(shù)
export default "123" // 導(dǎo)出一個(gè)值
模塊的導(dǎo)入
部分導(dǎo)入
import { BasePage } from "@/common";
export default class HomePage extends BasePage {}
導(dǎo)出重命名
import { BasePage as XMFBasePage } from "@/common";
export default class HomePage extends XMFBasePage {}
全部導(dǎo)入
將整個(gè)模塊導(dǎo)入到一個(gè)變量,并通過(guò)它來(lái)訪問(wèn)模塊的導(dǎo)出部分
import * as common from "@/common";
export default class HomePage extends common.BasePage {}
導(dǎo)入默認(rèn)模塊
在前面導(dǎo)出默認(rèn)模塊的時(shí)候提到了默認(rèn)導(dǎo)出相當(dāng)于 const 任意變量名 =,所以導(dǎo)入默認(rèn)模塊就是用“任意變量名”來(lái)接默認(rèn)模塊,如下:
import 任意變量名 from "./my-module.js";
具有副作用的導(dǎo)入模塊
偶爾會(huì)存在這種場(chǎng)景,我只想導(dǎo)入模塊,而不像要這個(gè)模塊內(nèi)的具體導(dǎo)出,那么可以像下面這樣進(jìn)行導(dǎo)入:
import "./my-module.js";
要注意的是如果./my-module.js是一個(gè)全局模塊,很容易產(chǎn)生變量沖突,所以說(shuō)這種導(dǎo)入是具有副作用的。
模塊分類(lèi)
從大類(lèi)來(lái)講模塊可以分為 全局模塊 和 文件模塊
全局模塊
全局模塊的作用域是全局。一個(gè) JavaScript 文件如果沒(méi)有export import,那么這個(gè)文件被引入后,則會(huì)是一個(gè)全局模塊,其中的任何聲明也都是全局共享的。
文件模塊
文件模塊的作用域被限定在文件內(nèi),且至少含有 export import 中的任何一個(gè)關(guān)鍵字。文件模塊按照導(dǎo)入方式又可分 相對(duì)導(dǎo)入 和 非相對(duì)導(dǎo)入
- 相對(duì)導(dǎo)入
相對(duì)導(dǎo)入是以/,./或../開(kāi)頭的
import Button from "./components/Button";
import HttpConstants from "../constants/HttpConstants";
import "/mod";
- 非相對(duì)導(dǎo)入
所有其它形式的導(dǎo)入被當(dāng)作非相對(duì)導(dǎo)入
import { View } from "react-native";
import { BasePage } from "@/common";
模塊解析
Typescript 模塊解析就是指導(dǎo) ts 編譯器查找 import 導(dǎo)入內(nèi)容的流程。TypeScript 共有兩種可用的模塊解析策略: Classic 和 Node 。
先縱觀一下各種方式的解析流程,不需要牢記,主要是幫助快速對(duì)整個(gè)解析策略的理解:

Classic
這種策略以前是TypeScript默認(rèn)的解析策略。 現(xiàn)在,它存在的理由主要是為了向后兼容。
- 相對(duì)路徑
相對(duì)路徑導(dǎo)入的模塊是相對(duì)于導(dǎo)入它的文件進(jìn)行解析的。

例如:
// /root/src/folder/A.ts
import { b } from "./moduleB"
查找流程:
1、/root/src/folder/moduleB.ts
2、/root/src/folder/moduleB.d.ts
可以發(fā)現(xiàn)當(dāng)解析 import 導(dǎo)入的的時(shí)候,TypeScript 會(huì)優(yōu)先選擇 .ts 文件而不是 .d.ts 文件
- 非相對(duì)路徑
非相對(duì)模塊的導(dǎo)入,編譯器則會(huì)從包含導(dǎo)入文件的目錄開(kāi)始依次向上級(jí)目錄遍歷,嘗試定位匹配的聲明文件。

例如:
//路徑: /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
這個(gè)解析策略試圖在運(yùn)行時(shí)模仿 Node.js 模塊解析機(jī)制, 完整的 Node.js 解析算法可以在Node.js module documentation找到
Node.js 如何解析模塊
為了理解TypeScript編譯依照的解析步驟,先弄明白Node.js模塊是非常重要的。 通常,在Node.js里導(dǎo)入是通過(guò)require函數(shù)調(diào)用進(jìn)行的。 Node.js會(huì)根據(jù)require的是相對(duì)路徑還是非相對(duì)路徑做出不同的行為。
- 相對(duì)路徑
相對(duì)路徑的解析比較簡(jiǎn)單,先以文件的模式查找,如果沒(méi)找到,再以目錄的形式進(jìn)行查找。

例如:
// /root/src/moduleA.js
const b = require("./moduleB")
查找流程如下:
1、/root/src/moduleB.js
2、/root/src/moduleB/package.json (如果指定了"main"屬性) 。
3、/root/src/moduleB/index.js(這個(gè)文件會(huì)被隱式地當(dāng)作那個(gè)文件夾下的main模塊)
- 非相對(duì)路徑
非相對(duì)路徑的解析是個(gè)完全不同的過(guò)程。Node會(huì)在一個(gè)特殊的文件夾node_modules里查找你的模塊。 node_modules可能與當(dāng)前文件在同一級(jí)目錄下,或者在上層目錄里。 Node會(huì)向上級(jí)目錄遍歷,查找每個(gè)node_modules直到它找到要加載的模塊。

例如:
// /root/src/moduleA.js
const b = 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 // 向上級(jí)目錄查找
5、/root/node_modules/moduleB/package.json (如果指定了"main"屬性)
6、/root/node_modules/moduleB/index.js
7、/node_modules/moduleB.js // 向上級(jí)目錄查找
8、/node_modules/moduleB/package.json (如果指定了"main"屬性)
9、/node_modules/moduleB/index.js
...
TypeScript 的 Node 模塊解析和 Node.js 有何區(qū)別
當(dāng)使用 Node 模塊解析策略是,TypeScript 是模仿 Node.js 運(yùn)行時(shí)的解析策略來(lái)在編譯階段定位模塊定義文件。 因此,TypeScript 在 Node.js 解析邏輯基礎(chǔ)上增加了 TypeScript 源文件的擴(kuò)展名(.ts、.tsx、.d.ts)。 同時(shí),TypeScript在package.json里使用字段types來(lái)表示類(lèi)似main的意義,編譯器會(huì)使用它來(lái)找到要使用的main定義文件。
- 相對(duì)模塊

例如:
// /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
可以發(fā)現(xiàn)文件查找的優(yōu)先級(jí)依次是:.ts->.tsx->.d.ts,如果是 TypeScript 和 JavaScript 的混合項(xiàng)目(在 tsconfig.json 中配置 "allowJs": true,關(guān)于 tsconfig.json 文件會(huì)在下面提到),在 d.ts 之后還會(huì)去查找 .js 文件,由于查找鏈會(huì)很長(zhǎng),所以這里暫且不討論這種情況。
- 非相對(duì)模塊

例如:
// /root/src/moduleA.ts
import { b } from "moduleB"
查找流程如下:
1、/root/src/node_modules/moduleB.ts
2、/root/src/node_modules/moduleB.tsx
3、/root/src/node_modules/moduleB.d.ts
4、/root/src/node_modules/moduleB/package.json (如果指定了"types"屬性)
5、/root/src/node_modules/moduleB/index.ts
6、/root/src/node_modules/moduleB/index.tsx
7、/root/src/node_modules/moduleB/index.d.ts
8、/root/node_modules/moduleB.ts
9、/root/node_modules/moduleB.tsx
10、/root/node_modules/moduleB.d.ts
11、/root/node_modules/moduleB/package.json (如果指定了"types"屬性)
12、/root/node_modules/moduleB/index.ts
13、/root/node_modules/moduleB/index.tsx
14、/root/node_modules/moduleB/index.d.ts
15、/node_modules/moduleB.ts
16、/node_modules/moduleB.tsx
17、/node_modules/moduleB.d.ts
18、/node_modules/moduleB/package.json (如果指定了"types"屬性)
19、/node_modules/moduleB/index.ts
20、/node_modules/moduleB/index.tsx
21、/node_modules/moduleB/index.d.ts
不要被這里步驟的數(shù)量嚇到,TypeScript只是在步驟(8)和(15)向上跳了兩次目錄。 這并不比 Node.js 里的流程復(fù)雜。
TypeScript 模塊解析配置
為了讓 TypeScript 能夠滿足工程化的需求,靈活配置類(lèi)型檢查和編譯參數(shù),特意提供了一個(gè) tsconfig.json 配置文件。我們可以通過(guò) tsconfig.json 來(lái)自定義模塊的解析策略。
tsconfig.json 文件
TypeScript 使用 tsconfig.json 文件作為其配置文件,當(dāng)一個(gè)目錄中存在 tsconfig.json 文件,則認(rèn)為該目錄為 TypeScript 項(xiàng)目的根目錄。熟悉移動(dòng)端開(kāi)發(fā)的可能會(huì)聯(lián)想到 Android 中的 build.gradle,iOS 中的 xcodeproj。
通常 tsconfig.json 文件主要包含兩部分內(nèi)容:指定待編譯文件和定義編譯選項(xiàng)。
tsconfig.json 的配置項(xiàng)可以用一張圖來(lái)簡(jiǎn)單進(jìn)行說(shuō)明:

詳細(xì)說(shuō)明可以查看這里
自定義模塊解析策略
tsconfig.json 中的 compilerOptions 是我們用的最多,也是最復(fù)雜的配置。其中有兩種方式來(lái)自定義模塊解析策略。
路徑映射
第一種是路徑別名映射,顧名思義是給路徑取個(gè)簡(jiǎn)稱,通過(guò)這個(gè)簡(jiǎn)稱我們就能夠定位到這個(gè)路徑。涉及到下面兩個(gè)配置項(xiàng):
- baseUrl:解析非相對(duì)模塊的根地址,默認(rèn)是當(dāng)前目錄
- paths:路徑映射別名,相對(duì)于baseUrl
比如我們項(xiàng)目中的基礎(chǔ)模塊,由于和業(yè)務(wù)模塊是獨(dú)立的,如果使用相對(duì)路徑進(jìn)行引用,無(wú)疑會(huì)產(chǎn)生很多 ../../../../../ 的引用方式,不僅很冗長(zhǎng),而且增加了代碼閱讀成本。這個(gè)時(shí)候就可以用路徑別名的方式進(jìn)行映射。來(lái)看下下面這個(gè)例子:
項(xiàng)目目錄結(jié)構(gòu)是這樣的:
├── node_modules
├── src
│ ├── common # 基礎(chǔ)模塊
│ │ ├── index.ts
│ ├── modules # 業(yè)務(wù)模塊
│ │ ├── xxx
│ │ │ ├── a.ts
└── tsconfig.json
tsconfig.json 配置如下:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/common": ["src/common"]
}
}
baseUrl 是相對(duì)于 tscofnig.json 的目錄,上面配置的是 .,說(shuō)明 baseUrl 就是 tsconfig.json 所在的目錄,也就是項(xiàng)目根目錄。
上面的配置指定了 @/common 等價(jià) <baseUrl>/src/common,這樣我們就可以直接用 @/common 來(lái)代替在 a.ts 中 ../../common 引用基礎(chǔ)庫(kù)的方式,最重要的是其中的層級(jí)不限,不管業(yè)務(wù)代碼所屬層級(jí)有多深,最終都會(huì)到項(xiàng)目根目錄下的 ./src/common 中查找模塊。
虛擬目錄
有時(shí)多個(gè)目錄下的工程源文件在編譯時(shí)會(huì)進(jìn)行合并放在某個(gè)輸出目錄下。 這可以看做一些源目錄創(chuàng)建了一個(gè)虛擬目錄。
比如,有下面的工程結(jié)構(gòu):
src
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
src/views 里的文件是用于控制UI的用戶代碼。 generated/templates是UI模版,在構(gòu)建時(shí)通過(guò)模版生成器自動(dòng)生成。 構(gòu)建中的一步會(huì)將/src/views和/generated/templates/views的輸出拷貝到同一個(gè)目錄下。 在運(yùn)行時(shí),視圖可以假設(shè)它的模版與它同在一個(gè)目錄下,因此可以使用相對(duì)導(dǎo)入"./template"。
利用配置項(xiàng) rootDirs,可以告訴編譯器生成這個(gè)虛擬目錄的 roots; 因此編譯器可以在“虛擬”目錄下解析相對(duì)模塊導(dǎo)入,就好像它們被合并在了一起一樣。。 因此,針對(duì)這個(gè)例子,tsconfig.json 如下:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
每當(dāng)編譯器在某一rootDirs的子目錄下發(fā)現(xiàn)了相對(duì)模塊導(dǎo)入,它就會(huì)嘗試從rootDirs的所有子目錄中導(dǎo)入。
自定義模塊解析只是一種標(biāo)記
當(dāng)你按照上面的配置完成自定義模塊解析之后,你會(huì)發(fā)現(xiàn)代碼運(yùn)行起來(lái)之后依然找不到對(duì)應(yīng)的模塊,這是為什么?
事實(shí)上,通過(guò) tsconfig.json 定義的解析策略,只是一種騙過(guò)編譯器的手段,編譯器并不會(huì)進(jìn)行對(duì)應(yīng)的路徑轉(zhuǎn)換。
虛擬目錄目錄需要在編譯時(shí)將代碼按照約定拷貝到指定目錄;
路徑映射則需要使用 babel 在編譯階段進(jìn)行轉(zhuǎn)換,babel 有提供現(xiàn)成的插件來(lái)完成路徑映射的轉(zhuǎn)換,如下:
安裝插件
npm install babel-plugin-root-import --save-dev
在 babel.config.js 或者 .babelrc 進(jìn)行相應(yīng)的配置
module.exports = {
plugins: [
[
"babel-plugin-root-import",
{
paths: [
{
rootPathPrefix: "@/common",
rootPathSuffix: "./src/common"
}
]
}
]
]
}
跟蹤模塊解析
模塊解析是一個(gè)很復(fù)雜的流程,編譯器在解析模塊時(shí)可能訪問(wèn)當(dāng)前文件夾外的文件,這會(huì)導(dǎo)致很難診斷模塊為什么沒(méi)有被解析,或解析到了錯(cuò)誤的位置。 通過(guò)--traceResolution啟用編譯器的模塊解析跟蹤,它會(huì)告訴我們?cè)谀K解析過(guò)程中發(fā)生了什么。
假設(shè)我們有一個(gè)使用了 typescript 模塊的簡(jiǎn)單應(yīng)用。 app.ts里有一個(gè)這樣的導(dǎo)入import * as ts from "typescript"。
├─── tsconfig.json
├─── node_modules
│ └─── typescript
│ └─── lib
│ └─── typescript.d.ts
└─── src
└─── app.ts
使用--traceResolution調(diào)用編譯器。,或者在 tsconfig.json 中添加該配置
tsc app.ts --traceResolution
輸出結(jié)果如下:
======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to
總結(jié)
這篇文章講述了 TypeScript 模塊的概念及使用方式,知道了怎么定義一個(gè)全局模塊和一個(gè)文件模塊。并且詳細(xì)描述了 TypeScript 模塊解析的流程,解析過(guò)程中文件的優(yōu)先級(jí)策略等等,讓大家對(duì) TypeScript 模塊有了一個(gè)全面的認(rèn)識(shí)。