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

许可协议

Generator-生成器函数

Generator 函数

之前只是大概看了一眼,就是一种新的语法,解决异步问题,好多东西没有仔细了解,看了 ”JS高设第四版“,更加熟悉了一点。而且这个翻译太不友好了“生成器”。好变扭,不过对于我这种,每个字母分开都认识,组合在一起完全不认识的人没资格说,

但是这个新语法,还涉及到了另一个知识“迭代器”。先去了解什么 JS的迭代器会很有帮助

一些术语:

  • “生成器对象” 生成器函数返回的结果(对象)

语法

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
function * gen() {
}
let gen = function*(){
}
class Foo {
static *gen(){
}
*gen(){
}
}


// 那个 “星号” 和函数之间的空格可有可无,这三种写法都OK
function * gen1 (){
}
function *gen2 (){
}
function*gen3 (){
}


// 新增了一个关键词 yield

function *gen(){
yield "a"
yield "b"
}

注意 箭头函数能用与“生成器”

yield

这也配合 “生成器”的一些关键词 ,yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的 return 关键字,yield 关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误

生成器也是一个函数。 不同的是这个函数执行之后,返回一个对象, 而且这个对象实现了迭代器(Iterator)接口,那么有意思的来了,既然是实现了迭代器,那个就可以通过一些 ”遵循“ 迭代器语法去访问。当然可以了。

如果不熟悉迭代器,还是先去看看迭代器相关内容

可以用一个不规范的理解就是 ”把这个迭代内容分割病分配到next() 方法返回的结果中“。

1
2
3
4
5
6
7
8
9
function *gen(){
yield "a"
yield "b"
}

const g = gen()
console.log(g.next()) // {value: 'a', done: false}
console.log(g.next()) // {value: 'b', done: false}
console.log(g.next()) // {value: undefined, done: true}

从语法层面来看,就像是 yield 把函数的执行给中断了,到yield就停下,除非用 next() 方法恢复继续执行。例如:平时的”死循环“代码,在 生成器函数中,用起来却有不一样的效果。 这时候就可以理解 yield 中断作用了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* nums() {
let i = 0;
while (true) {
yield i++;
}
}
const obj = nums();
console.log(obj.next());
console.log(obj.next());
console.log(obj.next());
console.log(obj.next());

for (const v of obj) {
console.log(v);
if (v > 10) {
break; // 这里如果不终止的话,会一直循环下去
}
}

比较有意思的是,生成器对象 默认的跌代器 就是本身。

1
2
3
4
5
function *gen(){
}
const g = gen()
console.log(g === g[Symbol.iterator]()) // true

当然了,一般我们很少去直接 调用 “next()” 方法。利用迭代器语法,当做可迭代对象,用起来就爽歪歪了。

1
2
3
4
5
6
7
8
9
10
// 填充指定长度的数组
function *arrCreater(n){
let i =0
while(n--){
yield i
i++
}
}
const arr = Array.from(arrCreater(10))
console.log(arr)

另一个作用,使用 yield 实现输入和输出,啥意思呢,yield 可以作为参数使用,上一次的 next() 方法可以传一个参数进行, 那么下一次 yield 就会 接收这个值,不过第一次的参数 是偶无效的。第一次就是开始,他就是头,所以不会有“上一次的返回值”

1
2
3
4
5
6
7
8
9
10
11
12
function* genFn(initial) {
console.log(initial);
console.log(yield);
console.log(yield);
}

const obj = genFn("a");
obj.next("b");
obj.next("c");
obj.next("d");
// a c d
// 可以发看到第一次next的参数

yield 还有一个更有用的 语法:yield*, 继续迭代一个迭代对象(好拗口),来个例子你就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 一
function* gen() {
yield* [1, 2, 3];
}
for (const val of gen()) {
console.log(val);
}

// 二
function* gen() {
yield [1, 2, 3];
}
for (const val of gen()) {
console.log(val);
}
// 对比 一、二,两种情况的结果,就很清楚了

既然说了为了解决异步,那么js中最常见的异步就是 发送请求了,最吓人的还数“地狱回调”,一旦有多个请求有依赖关系,那么就很麻烦了。如果用 Generator 就可以优雅的处理了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 需要引入 axios
let it;
function getUser(url) {
axios.get(url).then((res) => {
it.next(res);
});
}
function* req() {
getUser("https://api.github.com/users/z");
const res1 = yield;
console.log(res1);
if (res1.status == 200) {
getUser("https://api.github.com/users/d");
const res2 = yield;
console.log(res2);
}
}
it = req();
it.next();

假设第二次依赖于第一次, 使用”生成器函数“就可以这样简单的实现。让代码是同步的。当然还有 async、await (Promise) 也可以.

提前终止

这个和迭代类似,也可以终止,生成器对象 实现了 “迭代接口” 方法,就会有一个可选的 “return” 方法,用于终止迭代器,生成器对象还有另一个方法 “throw()”.return()throw() 都会强制进入关闭状态

  • return
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function* gen() {
    yield "a";
    yield "b";
    yield "c";
    yield "d";
    }
    const g = gen();

    console.log(g.next());
    console.log(g.next());

    console.log(g.return(4));
    // return 之后就不再可以迭代了,无法恢复
    console.log(g.next());

    // 使用for-of等内置 语言迭代的话, return 返回值 会被忽略
    const g2 = gen();
    for (const v of g2) {
    if (v == "b") {
    g2.return(4);
    }
    console.log(v);
    }
  • throw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* gen2() {
for (const x of [1, 2, 3]) {
try {
yield x;
} catch (error) {}
}
}

// 如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被 捕获,因为这相当于在函数块外部抛出了错误。
const g2 = gen2();
// console.log(g2.next()); 请注意这里的区别,这一行代码注释与不注释的区别
g2.throw("stop2");
console.log(g2.next());

//

你经过测试对比对发现, 如果在没有开始执行 next(开始迭代)之前,抛出错误,后续就无法继续迭代, 但是在开始之后,抛出的话,则只是影响 当前的一次,不会影响下次迭代。

书中解释 :如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被 捕获,因为这相当于在函数块外部抛出了错误。

书中小节:
迭代是一种所有编程语言中都可以看到的模式。ECMAScript 6 正式支持迭代模式并引入了两个新的 语言特性:迭代器和生成器。

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象都有一个Symbol.iterator属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器 工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。

迭代器必须通过连续调用 next()方法才能连续取得值,这个方法返回一个 IteratorObject。这 个对象包含一个done属性和一个value属性。前者是一个布尔值,表示是否还有更多值可以访问;后 者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next()方法来消费,也可以通过原生消 费者,比如 for-of 循环来自动消费。

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口, 因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够 暂停执行生成器函数。使用yield关键字还可以通过next()方法接收输入和产生输出。在加上星号之 后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。

作者

Fat Dong

发布于

2022-08-03

更新于

2022-08-04

许可协议