TypeScript 完全指南:从基础到高级类型系统(七)「模块」

在TypeScript的世界里,模块是构建大型应用的关键拼图🧩。它就像一个个独立的小盒子,将代码封装起来,让代码更有条理、更易于维护。今天,就让我们一起深入探索TypeScript模块的奥秘吧!

1. 模块基础:开启代码封装之旅🔑

在TypeScript中,只要文件里出现了import或者export语句,它就摇身一变成为了一个模块。反之,如果文件里没有export语句,那它就是一个全局脚本文件。

模块有自己独立的作用域,就像一个封闭的小世界🌐,里面的变量、函数、类,在模块外面是看不到的。要是想把模块里的内容暴露给外界使用,就得用export命令声明;而其他文件想要使用这个模块的内容,就必须通过import命令引入。

有时候,我们希望一个文件虽然没有export语句,但也能当作模块,让内部变量对外不可见,这时候在脚本头部加上export {};就可以啦。这行代码就像一个神奇的开关🚀,虽然它本身不产生实际作用,但能让文件变成模块,里面的代码都变成内部代码。

TypeScript模块不仅支持ES模块的所有语法,还多了个厉害的功能——可以输出和输入类型。比如下面这样:

export type Bool = true | false;

上面的代码输出了一个类型别名Bool,也可以把类型定义和接口输出分开写:

type Bool = true | false;
export { Bool };

假设这个模块文件叫a.ts,另一个文件b.ts就可以用import语句引入这个类型:

import { Bool } from './a';
let foo:Bool = true;

注意哦,加载文件时可以省略.ts后缀名,TypeScript会自动帮我们定位到对应的文件。编译的时候,可以同时编译两个脚本:

$ tsc a.ts b.ts

也可以只编译入口脚本b.ts,因为TypeScript会自动找到它依赖的a.ts并一起编译:

$ tsc b.ts

2. import type语句:精准区分类型与接口🧐

在一条import语句里,我们可以同时输入类型和正常接口,就像这样:

// a.ts
export interface A {
  foo: string;
export let a = 123;
// b.ts
import { A, a } from './a';

但是这样很容易混淆类型和正常接口,为了解决这个问题,TypeScript给出了两个好办法。

第一个办法是在输入的类型前面加上type关键字:

import { type A, a } from './a';

第二个办法是使用import type语句,它专门用来输入类型,不能输入正常接口:

// 正确
import type { A } from './a';
let b:A = 'hello';
// 报错
import type { a } from './a';
let b = a;

import type语句还可以输入默认类型:

import type DefaultType from 'moduleA';

如果想在一个名称空间下输入所有类型,可以这样写:

import type * as TypeNS from 'moduleA';

同样的,export语句也有两种方式表示输出的是类型:

type A = 'a';
type B = 'b';
// 方法一
export {type A, type B};
// 方法二
export type {A, B};

export type把类作为类型输出时,输出的不是类本身,而是类的实例类型:

class Point {
  x: number;
  y: number;
export type { Point };

在其他文件导入时,只能当作类型使用:

import type { Point } from './module';
const p:Point = { x: 0, y: 0 };

3. importsNotUsedAsValues编译设置:掌控编译细节⚙️

TypeScript里输入类型的import语句,编译成JavaScript时会怎么处理呢?这就要用到importsNotUsedAsValues编译设置项啦,它有三个可选值:

  • remove:这是默认值,会自动删除输入类型的import语句。🚫
  • preserve:保留输入类型的import语句,但会删掉其中涉及类型的部分。🗄️
  • error:保留输入类型的import语句(和preserve一样),不过必须写成import type的形式,否则会报错。⚠️

比如下面这个输入类型的import语句:

import { TypeA } from './a';

remove的编译结果会把这行语句删掉;preserve的编译结果会变成这样:

import './a';

error的编译结果和preserve相同,但如果不写成import type的形式,编译过程中就会报错,改成import type { TypeA } from './a';就没问题啦。

4. CommonJS模块:TypeScript与Node.js的桥梁🌉

CommonJS是Node.js专用的模块格式,和ES模块不兼容。在TypeScript中,我们可以用import =语句来输入CommonJS模块:

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

也可以用import * as [接口名] from "模块文件"这种方式输入:

import * as fs from 'fs';
// 等同于
import fs = require('fs');

输出CommonJS模块的对象时,要用export =语句,它等同于CommonJS的module.exports对象:

let obj = { foo: 123 };
export = obj;

export =语句输出的对象,只能用import =语句加载:

import obj = require('./a');
console.log(obj.foo); // 123

5. 模块定位:寻找模块的宝藏地图🗺️

模块定位是一种算法,用来确定importexport语句里模块文件的位置。比如下面这两个例子:

// 相对模块
import { TypeA } from './a';
// 非相对模块
import * as $ from "jquery";

TypeScript要确定./ajquery具体指哪个模块、在哪里,就靠模块定位算法啦。

编译参数moduleResolution可以指定使用哪种定位算法,常用的有ClassicNode两种。如果没有指定moduleResolution,它的默认值和编译参数module有关。当module设为commonjs时,moduleResolution默认值为Node;其他情况(比如module设为es2015esnextamdsystemumd等),就采用Classic定位算法。

5.1 相对模块与非相对模块:模块的不同路径标识🚧

加载模块时,模块分为相对模块和非相对模块。

  • 相对模块的路径以/./../开头,比如:
    • import Entry from "./components/Entry";
    • import { DefaultHeaders } from "../constants/http";
    • import "/mod";
      相对模块的位置是根据当前脚本的位置来计算的,一般用于项目目录里的模块脚本。📍
  • 非相对模块没有路径信息,比如:
    • import * as $ from "jquery";
    • import { Component } from "@angular/core";
      非相对模块的位置由baseUrl属性或模块映射来确定,通常用于加载外部模块。🌐

5.2 Classic方法:以当前脚本为起点的探索🧭

Classic方法把当前脚本的路径当作“基准路径”来计算相对模块的位置。比如脚本a.ts里有import { b } from "./b",TypeScript就会在a.ts所在的目录查找b.tsb.d.ts

对于非相对模块,也是从当前脚本路径出发,一层层查找上级目录。比如a.ts里有import { b } from "b",就会在每一级上层目录查找b.tsb.d.ts

5.3 Node方法:模拟Node.js的模块加载之路🚶

Node方法模拟的是Node.js的模块加载方法,也就是require()的实现方式。

相对模块同样以当前脚本路径为“基准路径”。比如a.ts里有let x = require("./b");,TypeScript会按以下顺序查找:

  1. 当前目录有没有b.tsb.tsxb.d.ts。没有就执行下一步。🔍
  2. 当前目录有没有子目录b,子目录里的package.json文件有没有types字段指定模块入口文件。没有就执行下一步。📄
  3. 当前目录的子目录b有没有index.tsindex.tsxindex.d.ts。没有就报错。🚫

非相对模块以当前脚本路径为起点,逐级向上查找node_modules子目录。比如a.js里有let x = require("b");,TypeScript会这样查找:

  1. 当前目录的子目录node_modules有没有b.tsb.tsxb.d.ts。🔍
  2. 当前目录的子目录node_modules里有没有package.json文件,它的types字段有没有指定入口文件,有的话就加载该文件。📄
  3. 当前目录的子目录node_modules里有没有子目录@types,在里面查找b.d.ts。🔍
  4. 当前目录的子目录node_modules里有没有子目录b,在里面查找index.tsindex.tsxindex.d.ts。🔍
  5. 进入上一层目录,重复上面4步,直到找到为止。🚶

5.4 路径映射:自定义模块的定位规则✍️

tsconfig.json文件里,我们可以手动指定脚本模块的路径。

  • baseUrl字段可以指定脚本模块的基准目录:
  "compilerOptions": {
    "baseUrl": "."
  }

上面的例子里,baseUrl.,表示基准目录就是tsconfig.json所在的目录。📍

  • paths字段可以指定非相对路径的模块与实际脚本的映射:
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"]
    }
  }

这样,加载jquery模块时,实际加载的脚本就是node_modules/jquery/dist/jquery,它的位置是根据baseUrl字段计算出来的。注意,jquery属性的值是个数组,可以指定多个路径,第一个路径不存在就加载第二个,以此类推。📑

  • rootDirs字段指定模块定位时必须查找的其他目录:
  "compilerOptions": {
    "rootDirs": ["src/zh", "src/de", "src/#{locale}"]
  }

这里指定了模块定位时要查找的不同国际化目录。🌍

5.5 tsc的--traceResolution参数:查看模块定位过程👀

模块定位过程比较复杂,tsc命令有个--traceResolution参数,编译时可以在命令行显示模块定位的每一步:

$ tsc --traceResolution

这样就能清楚地看到模块定位的判断过程啦。

5.6 tsc的--noResolve参数:限定模块定位范围🚫

tsc命令的--noResolve参数表示模块定位时,只考虑命令行传入的模块。

比如app.ts里有这两行代码:

import * as A from "moduleA";
import * as B from "moduleB";

用下面的命令编译:

$ tsc app.ts moduleA.ts --noResolve

因为用了--noResolve参数,所以能定位到moduleA.ts(因为命令行传入了),但无法定位到moduleB(因为没传入),就会报错。

6. 模块与命名空间:功能相似,用法有别🤔

在TypeScript里,命名空间和模块都是组织代码的好帮手,但它们有不少区别哦。

6.1 语法和定义方式:各有各的语法规则📜

  • 命名空间:用namespace关键字定义,可以在一个文件里定义多个命名空间:
namespace MyNamespace1 {
    export function func1() {
        console.log('Function in namespace 1');
    }
namespace MyNamespace2 {
    export function func2() {
        console.log('Function in namespace 2');
    }
}
  • 模块:一个文件就是一个模块,用export导出成员,用import导入其他模块的成员:
// module1.ts
export function func1() {
    console.log('Function in module 1');
}
// module2.ts
import { func1 } from './module1';
func1();

6.2 作用域和可见性:访问权限的差异🔒

  • 命名空间:在同一个文件里,命名空间内的成员默认是私有的,要在外部访问,得用export关键字:
namespace MyNamespace {
    function privateFunc() {
        console.log('This is a private function');
    }
    export function publicFunc() {
        privateFunc();
    }
}
MyNamespace.publicFunc(); // 可以调用
// MyNamespace.privateFunc(); // 报错,无法访问私有函数
  • 模块:模块成员默认也是私有的,只有用export导出的成员才能在其他模块被访问:
// module.ts
function privateFunc() {
    console.log('This is a private function');
}
export function publicFunc() {
    privateFunc();
}
// main.ts
import { publicFunc } from './module';
publicFunc(); 
// privateFunc 无法被导入和调用

6.3 兼容性和使用场景:适用场景大不同🎯

  • 命名空间:在旧项目或者需要全局作用域的情况下比较常用。早期JavaScript项目里,命名空间能避免全局变量冲突,而且它支持合并,多个同名命名空间会合并成一个:
namespace MyNamespace {
    export function func1() {
        console.log('Function 1');
    }
}
namespace MyNamespace {
    export function func2() {
        console.log('Function 2');
    }
}
MyNamespace.func1();
MyNamespace.func2();
  • 模块:是ES6及后续版本的标准机制,兼容性和可移植性更好。在现代JavaScript和TypeScript项目里,模块是组织代码的首选,它能更好地管理依赖关系,方便代码分割和复用。

6.4 编译结果:编译后的代码差异📄

  • 命名空间:编译后的JavaScript代码会创建一个全局对象来包含命名空间的成员,可能会污染全局作用域:
namespace MyNamespace {
    export function func() {
        console.log('Function in namespace');
    }
}

编译后的代码:

var MyNamespace;
(function (MyNamespace) {
    function func() {
        console.log('Function in namespace');
    }
    MyNamespace.func = func;
})(MyNamespace || (MyNamespace = {}));
  • 模块:编译后的代码会根据使用的模块系统(如CommonJS、ES模块等)生成对应的模块代码,不会污染全局作用域。比如用ES模块语法编写的代码:
export function func() {
    console.log('Function in module');
}

编译成ES模块后的代码:

export function func() {
    console.log('Function in module');
}

6.5 引用方式:引入方式不一样🔗

  • 命名空间:如果命名空间定义在不同文件中,需要用三斜杠引用(/// <reference path="path/to/file.ts" />)来引入:
// file1.ts
namespace MyNamespace {
    export function func() {
        console.log('Function in namespace');
    }
}
// file2.ts
/// <reference path="file1.ts" />
MyNamespace.func();
  • 模块:使用importexport关键字进行导入和导出,这是现代项目中更常用的方式:
// module1.ts
export function func() {
    console.log('Function in module');
}
// module2.ts
import { func } from './module1';
func();

总的来说,模块在现代TypeScript项目里更推荐使用,它的兼容性、可维护性和可移植性都更好。

7. 模块的适用场景:模块的最佳舞台🎇

7.1 大型项目开发:化繁为简的利器🗡️

大型项目代码量大、功能复杂,模块可以把代码分割成一个个独立单元,方便管理和维护。每个模块专注于特定功能,不同模块通过importexport交互,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值