TypesSript 模块化浅谈

最近在学习 TS,一直在看官方的 handbook, 看到模块化这一章节,记录一下学习心得。其实大部分内容还是把文档的翻译直接拿过来,所以说最好的教程还是看官方的文档

什么是模块化

模块化是一种将代码按照一定规则组织成独立、可复用的单元的方式。模块化可以将一个复杂的系统分解为多个独立的模块,每个模块只关注自己的功能,与其他模块之间的耦合度低,从而提高代码的可维护性、复用性和扩展性。

其实重点就是:低耦合,高内聚, 高复用,高维护 , 通过将一些复杂的功能拆解,化整为零到多个模块(具体体现可能是分解成多个文件),尤其是最新的开发模式,借助于一些现代化的构建工具,可以很大程度提高项目的维护性。

在远古时期的浏览器端开发,大家可能都是在一个或多个文件中写代码,然后在引入到一个页面中。这个时期基本上谈不上模块化,在多人协作的时非常难。早期大家为了解决冲突问题,常见方式就是使用“命名空间模式(Namespace Pattern)”,通过创建全局对象,并将相关函数、变量添加到该对象中来实现模块化。

随着技术逐渐发展,出现了多种模块化的方案,例如: CommonJSAMDCMDUMDES6 Module 等。由于早期没有明确的规范,导致出现了多种模块化的方案, 关于每种方式的具体实现可自行查找资料了解。

JS 的模块化标准的推进比较缓慢,2015 年 6 月, ECMAScript6 标准正式发布,其中 ES 模块化规范的提出目标是整合 CommonJS、AMD 等已有模块方案,在语言标准层面实现模块化,成为浏览器和服务器通用的模块解决方案。到 2020 年,大多数 Web 浏览器和 JavaScript 运行时都得到了广泛支持。

TS 中的模块

在开始之前,了解 TypeScript 将模块视为什么非常重要。JavaScript 规范声明任何没有 import 声明、export 或顶级的 JavaScript 文件 await 都应被视为脚本而不是模块。

TypesScript 不仅支持 ES6 Module 也支持 CommonJS.

TS 中比较特别一点就是: 文件中没有 exportimport,又想将当前文件视为模块,可以添加 一行 export {} 那么当前文件不会导出任何内容,但会被认为是一个模块

还有一点就是,TS 还支持导出 类型声明. 可参照示下面例。

  • 通用语法示例(ES6 Module):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// a.ts
export default function helloWorld() {
console.log("Hello, world!");
}

// b.ts
import helloWorld from "./a.ts";

// 导出多个
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;

import { pi, phi, absolute } from "./a.ts";

// 重命名
import { pi as PI } from "./a.ts";

// 混合导出
import helloWorld, { pi } from "./a.ts";

// 使用命名空间导出
import * as A from "./a.ts";
  • TS 中特定(特殊)的语法,支持导出 Type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @filename: a.ts
export type A = { name: string };

export interface IA {
name: any;
}

// b.ts
import { A, IA } from "./a.ts";

// import type TS 独有
import type { A, IA } from "a.ts";

// 内联方式导入
// TypeScript 4.5 还允许单个导入添加前缀,type以指示导入的引用是一种类型:
import { type A, type IA } from "a.ts";
1
2
3
4
5
6
//  当然也可以使用commonJS的语法

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");

// 有关于commonJS的更多语法,可参考 Node 相关教程

关于 CommonJS 和 ES modules 的 互操作性,由于两者在导入、导出规则的区别,有一个配置项 esModuleInterop

TS 模块解析 配置项

主要是指 import 等语句引用的文件的时候,根据 import语句中的字符串,来确定是那个文件的过程
有两种策略: 经典模式 (Classic), Node 经典模式(Node Classic),

能够影响解析策略的配置项有好几个:moduleResolution, baseUrl, paths, rootDirs.

其实相对文件地址还好,主要是一些安装的第三方的库。

TS 模块输出 配置项

共有 2 个配置项,影响编译生成 JS,

  • target
    决定了那些 JS 功能会被降级(能够兼容以前的 JS 环境,保持正常运行),那些功能保持不变.
    这个很容易理解了,由于不同的浏览器厂商对规范实现的程度不一样(不同版本支持的 JS 版本不一样),如果当前浏览器版本较低,不支持一个新功能语法等,通过设置 target 可以把这类功能降低成支持的语法。
    例如:我们把 target 设置成 ES5, 以下代码就是被转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 之前
    class Animals {
    constructor() {}
    }

    // 之后
    ("use strict");
    var Animals = /** @class */ (function () {
    function Animals() {}
    return Animals;
    })();

    不过目前大部分浏览器基本上都支持了 ES6

  • module
    决定了模块之间用什么代码交互,听起来不太容易理解,简单的说,就是最终生成的代码是什么模块风格的。
    你可以自己尝试去修改 module 的值,然后生成 JS 文件,去对比区别。这里就不一一罗列了。

综上所述, target 和 module 都会影响最后生成 JS 代码的内容。

TS 的命名空间

这里说的命名空间,和之前讲的 JS 的命名空间不是同一个东西。虽然使用起来有一些相似之处。

TS 有自己的模块格式,成为命名空间,比 ES 的标准要早出现,目的也是:方便组织代码,跟踪类型,避免命名之间的冲突。类似与 JS 一样,把对象包装到一个命名空间,而不是放到全局中

这是官方的 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidValidationator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${
validators[name].isAcceptable(s) ? "matches" : "does not match"
} ${name}`
);
}
}

  • 注 1: 什么是互操作, 英文原文为:CommonJS and ES Modules interop, 直译过来就是 CommonJS 和 ES Modules 互操作。

    这个翻译很不好理解。

    其实所谓的互操作,其实简答的讲,就是可以在 CommonJS 模块导入 ES 模块, 或者 ES 模块导入 CommonJS, 他们之间相互导入的操作。

    通常 ES Modules 是 使用 import export 语法导入导出, CommonJS 使用 module.exports 语法导出,ES Modules 有 default 这个概念, 但是 CommonJS 没有,它是直接导出了一个“命名空间”。为了解决他们呢之间的相互导入的时候造成的不统一:
    TS 做了如下处理:

    • 对于 export default 的变量,TS 会将其放在 module.exports 的 default 属性上
    • 对于 export 的变量,TS 会将其放在 module.exports 对应变量名的属性上
    • 额外给 module.exports 增加一个 __esModule: true 的属性,用来告诉编译器,这本来是一个 esm 模块

    TS 配置文件中有一项 esModuleInterop 配置,设置成 true ,TS 的编译器会自动兼容两个模式的导入导出。也就是支持 export 方式导出。

作者

Fat Dong

发布于

2023-10-10

更新于

2023-10-11

许可协议