# 深入理解 TS 模块
模块有自己的作用域,除非进行某种形式的导出,否则,其中的变量,函数,类等都是对外不可见的。相应的,如果要在模块外使用其导出的成员,则需要进行相
应的导入。模块的相互导入需要使用 模块加载器 ,模块加载器在执行模块之前,会先定位并执行该模块的依赖项。JS
中主要使用Node.js
的CommonJs
模块
加载器和Web
应用程序中的AMD
模块的RequireJs
加载器。TS延用了ES2015
模块化方案,任何包含了顶层的import
或export
语句的文件,便是一个模块;
相反,没有在顶层包含这些语句的则是脚本,其内容在全局作用域中可见。
# 导入导出语法
export:规定模块对外接口
export default Test // 默认导出(导入时可指定模块任意名称,无需知晓内部真实名称)
export const name = "Bruce" // 单独导出
export { age, name, sex }(推荐) // 按需导出
export { name as newName } // 改名导出
export = // commonjs 导出模块 exports/module.exports导出模块, 针对这类模块的声明文件,需要使用export =导出
export type nameType=string // 明确是值还是类型
2
3
4
5
6
7
8
9
10
11
import:导入模块内部功能
import Test from "test" // 默认导入
import * as Test from "test" // 整体导入
import { age, name, sex } from "test" // 按需导入
import { name as newName } from "test" // 改名导入
import "test" // 自执导入
import Test, { name } from "test" // 复合导入
import type {nameType} from "test" // 引入类型(无法作为值来使用)
2
3
4
5
6
7
8
9
10
11
12
13
# 在其他 JS 库中使用模块
有些库不是用 TS 写的,我们声明它们需要暴露出的 API 来描述库的形状。未定义实现的声明称为环境 ambient ,通常写在 .d.ts 中。
# 环境模块
在 node.js 中,许多的任务都是通过各个模块来完成的,我们可以通过顶层的声明在各自的 .d.ts 文件中定义每一个模块,但一个更方便的方式是将它们都写在一个更大的 .d.ts 文件中。
我们可以通过环境命名空间来对它们进行改造,都是使用 module 关键字以及带引号的模块名,这些模块名用于后续的导入。
简化的node.d.ts
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
export function parse(
urlStr: string,
parseQueryString?,
slashesDenoteHost?
): Url;
}
declare module "path" {
export function normalize(p: string): string;
export function join(...paths: any[]): string;
export var sep: string;
}
declare module "@/utils/imgTools" { // 本地文件
export function pathToBase64(path: string): Promise<any>;
export function base64ToPath(base64: string): Promise<any>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
随后便可以使用三斜杠指令/// <reference path="node.d.ts"/>
来引入,并使用import url = require("url");
或import * as URL from "url"
来加载相应模块。
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("https://www.typescriptlang.org");
2
3
# 通配符 * 模块声明
SystemJs
和AMD
等,允许导入非JavaScript
的内容。这些内容通常会使用前缀或后缀来表示相关的语义。因此,使用通配符模块声明可以使你很方便地涵盖这些情况。
declare module "*!text" {
const content: string;
export default content;
}
// Some do it the other way around.
declare module "json!*" {
const value: any;
export default value;
}
2
3
4
5
6
7
8
9
上面的例子中使用了*!
,其中*
为通配符,表示任意字符内容,!
在此处用来分隔语义。之后,我们可以导入任何匹配*!text
或json!*
的内容
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
2
3
# UMD 模块
有些库在设计时兼顾了多种模块加载器,或作为全局变量以在没有模块加载器时使用。这些就是我们熟知的UMD
模块。这些库既可以通过某种形式的导入来使用,也可以直接通过其暴露的全局变量来使用
如下使用了export as namespace mathLib
来暴露一个全局变量mathLib
,在脚本中(注意,不是模块中)通过该变量可以访问模块成员。
// mathLib.d.ts
// 导出一个函数成员
export function isPrime(x: number): boolean;
// 定义一个全局变量mathLib
export as namespace mathLib;
// app1.ts
import {isPrime} from "math-lib";
isPrime(3);
// app2.ts
// 不导入,直接通过全局变量mathLib来使用
mathLib.isPrime(3);
2
3
4
5
6
7
8
9
10
11
12
13
# 模块结构化规范
# 导入导出规范
1.导出单个时,使用默认导出 export default。
2.如果导出多个成员,尽量放在顶层导出。
export class SomeType {
/* ... */
}
export function someFunc() {
/* ... */
}
2
3
4
5
6
3.如果导入少量的成员,应该显式的列出导入的名称。
import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();
2
3
4.导入大量的成员时,最好使用命名空间导入模式 import * as Name from "Module"
// largeModule.ts
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
// app.ts
import * as LM from "largeModule"
const Wangcai = new LM.Dog()
const Kitty = new LM.Cat()
const rose = new Tree()
2
3
4
5
6
7
8
9
10
# 通过重导出进行模块扩展
当我们需要对模块的功能进行扩展时,通常的做法是,不修改原有的模块的内容,而是重新导出一个具有新的功能的实体,然后通过 as 重命名为原模块名。
// car.ts
export class Car {
constructor(brand:string) {
this.carname = brand;
}
present(){
return 'I have a' + this.carname
}
}
2
3
4
5
6
7
8
9
// ProgrammerCar.ts
import {Car} from './car'
class ProgrammerCar extends Calculate { // 通过 extends 关键字来实现继承
constructor(brand:string,mod:string){
super()
this.modal = mod;
}
show(){
return this.present() + ',it is a' + this.modal
}
}
export { ProgrammerCar as Car }; // 导出继承后的新的实体类,并重命名为 car
2
3
4
5
6
7
8
9
10
11
12
13
# 不要在模块中使用命名空间
因为模块有它自己的作用域,只有导出的成员才具有对外的可见性。因此我们应该尽量避免在模块中使用命名空间。特别是以下两种情况:
1.模块唯一的顶层导出不能是命名空间:export namespace Foo {...}
, 模块文件本身已经是一个逻辑分组,并且它的名字是由导入这个模块的代码指定,所以没有必要为导出的对象增加额外的模块层。
2.多个文件导出了同名的命名空间:export namespace Foo {...}
,注意:同一模块下命名空间一致会合并,不同模块下同名命名空间不会合并。
# 模块解析
我们知道模块有两种导入方式,分别为 相对导入 和 非相对导入,
在我们导入模块时,根据模块路径的书写形式,分为 相对导入 和 非相对导入 。相对导入,顾名思义,就是使用相对路径来导入模块,包含./
,../
等表示相对路径的字符,如
import { getName } from "./Person"
;import Person from "./Person"
;import "./mod"
;
非相对导入,便是不包含./
,../
等表示路径的字符,如:
import { getName } from "Person"
;import Person from "Person"
;import "mod"
;
相对导入是相对于当前文件来解析,且不能解析为环境模块声明。我们自己的模块,应该使用相对导入。非相对导入基于baseUrl
或者路径映射来解析,可以解析为环境模块声明。导入外部依赖时,应使用非相对导入。