TS学习笔记七:模块与模块解析

时间:2024-01-22 18:41:51

本节介绍ts中模块及模块解析相关知识,模块限制了作用域,代码在其作用域里执行,而不是在全局作用域里,可以使用导出及导入进行暴露成员及引入成员。

模块解析介绍编译器所要依据的一个流程,通过解析规则来导入操作所引用的具体值。

  1. B站视频:https://www.bilibili.com/video/BV1ek4y1Q7C8/
  2. 西瓜视频:https://www.ixigua.com/7323028072310145575

一、模块

模块在其自身作用域里执行,不是在全局作用域中,即模块中的变量、函数、类等成员模块外部是不可见的,除非明确的使用export导出,在使用的时候使用import导入。

模块是自声明的,两个模块之间的关系是通过文件级别上使用export和import来组织的。

模块使用模块加载器会根据规则导入其它模块,在运行时,模块加载器在执行此模块代码前去查找并执行这个模块的所有依赖,常见的模块加载器包括:CommonJS和Require.js。ts中任何包含import和export的文件都被当成一个模块。

1.导出

任何声明,如变量、函数、类、类型别名或接口等都能通过添加export关键字来导出,如下:

a.ts
export interface A{}
export class B{};
export const c=1;

导出语句:

导出语句可以对导出的部分进行重命名,如下:

class A{
}
export {A}
export {A as a}

重新导出:

扩展其它模块只导出那个模块的部分内容时,可以使用重新导出功能实现,重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量:

export class A{}
export {A as B} from ‘./a’;//导出原先的并进行重命名

或者一个模块可以包裹多个模块,并且把他们联合在一起进行导出:

export * form “./a”;
export * form “./b”;
export * form “./c”;

2.导入

模块导入通过import实现。

  • 导入一个模块中的某个导出内容:
import { A} from "./a";
let myValidator = new A();

可以对导入内容进行重命名:

import { A as a} from "./a";
let myValidator = new a();
  • 将整个模块导入到一个变量:
import * as vfrom "./a";
let myValidator = new v.A();
  • 具有副作用的导入模块: 一些模块会设置一些全局状态个供其他模块使用,这些模块可能没有任何的导出或根本不关注它的导出,如:
import "./my-module.js";

3.默认导出

每个模块都可以指定默认导出,默认导出就是指定在导入的时候不指定具体的成员名的时候的默认成员。默认导出使用default关键字标记,且一个模块只能有一个default导出,需要使用特殊的导入形式来导入default,如: JQuery.d.ts

declare let $: JQuery;
export default $;
App.ts
import $ from "JQuery";

$("button.continue").html( "Next Step..." );

类和函数声明可以直接被标记为默认导出,标记为默认导出的类和函数名字是可以省略的,如下: Validate.ts

export default class Validator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && Validator.numberRegexp.test(s);
    }
}

Test.ts

import validator from "./Validate";
let myValidator = new validator();

BValidate.ts

const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}

Test.ts

import validate from "./BValidate";
let strings = ["Hello", "98052", "101"];
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});

default 也可导出一个值,如:

a.ts:export default “ffds”;
test.ts:import num form “./a”;

export = 和 import = require(),CommonJS和AMD都有一个exports对象的概念,exports包含了一个模块的所有导出内容,也支持把exports替换为一个自定义对象,ts模块支持export = 语法以支持传统的CommonJS和AMD的工作流模型。export = 语法定义一个模块的导出对象,可以是类、接口、命名空间、函数或枚举。若要导入一个使用了export = 的模块时,必须使用ts提供的特定语法import let = require(“module”)。 Valida.ts

let numberRegexp = /^[0-9]+$/;
class Validator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = Validator;

Test.ts

import zip = require("./Validator");
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validator = new zip();
// Show whether each string passed each validator
strings.forEach(s => {
  console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

4.生成模块代码

编译时指定的模块的目标参数,编译器会生成相应的Node.js(CommonJS)、Require.js(AMD)、isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。以下是不同模块转换后的代码: SimpleModule.ts

import m = require("mod");
export let t = m.something + 1;

AMD / RequireJS SimpleModule.js

define(["require", "exports", "./mod"], function (require, exports, mod_1) {
    exports.t = mod_1.something + 1;
});

CommonJS / Node SimpleModule.js

let mod_1 = require("./mod");
exports.t = mod_1.something + 1;

UMD SimpleModule.js

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        let v = factory(require, exports); if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./mod"], factory);
    }
})(function (require, exports) {
    let mod_1 = require("./mod");
    exports.t = mod_1.something + 1;
});

System SimpleModule.js

System.register(["./mod"], function(exports_1) {
    let mod_1;
    let t;
    return {
        setters:[
            function (mod_1_1) {
                mod_1 = mod_1_1;
            }],
        execute: function() {
            exports_1("t", t = mod_1.something + 1);
        }
    }
});

Native ECMAScript 2015 modules SimpleModule.js

import { something } from "./mod";
export let t = something + 1;

每个模块只有一个命名的导出,为了编译,必需要在命令行上指定一个模块目标,对于Node.js来说,使用--module commonjs; 对于Require.js来说,使用``--module amd`。比如:

tsc --module commonjs Test.ts

编译完成后,每个模块会生成一个单独的.js文件。 好比使用了reference标签,编译器会根据 import语句编译相应的文件。

可选模块加载:

有时候需要再某个特定条件下加载某个模块,在ts中可以直接调用模块加载器并且可以保证类型安全。

编译器会检测是否每个模块都会在生成的js中用到,如果一个模块代码只在类型注解部分使用,并且完全没有在表达式中使用,就不会生成require这个模块的代码,这样就可以省略掉没有用到的引用,提升性能,并提供选择性加载模块的能力。

import id = require(”……”)语句可以访问模块导出的类型,模块加载器会被动态调用,import定义的标识符只能在表示类型处使用。为了确保类型安全,可以使用typeof关键字,typeof关键字在表示类型的地方使用时,会得出一个类型值。 nodejs里的动态模块加载:

declare function require(moduleName: string): any;
import { Validator as Zip } from "./Validator";
if (needValidation) {
    let Validator: typeof Zip = require("./Validator");
    let validator = new Validator();
    if (validator.isAcceptable("...")) { /* ... */ }
}

require.js里的动态模块加载:

declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;
import  * as Zip from "./Validator";
if (needValidation) {
    require(["./Validator"], (Validator: typeof Zip) => {
        let validator = new Validator.Validator();
        if (validator.isAcceptable("...")) { /* ... */ }
    });
}

System.js里的动态模块加载:

declare const System: any;
import { Validator as Zip } from "./Validator";
if (needValidation) {
    System.import("./Validator").then((Validator: typeof Zip) => {
        var x = new Validator();
        if (x.isAcceptable("...")) { /* ... */ }
    });
}

5.使用其它js库

想要使用非ts编写的类库的类型,需要声明类库所暴露处的API,此声明不是外部程序的具体实现,通常定义在.d.ts中,类型c语言中的.h文件。

  • 外部模块

Node.js里会通过加载一个或多个模块实现,可以使用*的export声明来为每个模块都定义一个.d.ts文件,可以使用与构造一个外部命名空间相似的方法,使用module关键字并把模块名称用引号括起来,如:

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 let sep: string;
}

可以使用/// node.d.ts并使用import url = require("url");加载模块。

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
  • 外部模块简写

若不想再使用一个新模块之前花时间去编写声明,可以采用声明的简写形式以便能够快速使用它。 test.d.ts

declare module “module”

简写模块里所有导出类型都将是any:

import x,{y} from “module”;
x(y)
  • 模块声明通配符

某些加载器SystemJS和AMD支持导入非js的内容,会使用一个前缀和后缀来表示特殊的加载语法,模块声明通配符可以用来表示:

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;
}

定义上述声明了之后,就可以导入匹配"!text"或"json!"的内容了:

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
  • UMD模块

有些模块被设计成兼容多个模块加载器,或者不适用模块加载器直接使用全局变量,如UMD库,可以通过导入形式或全局变量的形式访问: math-lib.d.ts

export const isPrime(x: number): boolean;
export as namespace mathLib;

之后,这个库可以在某个模块里通过导入来使用:

import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module

也可以通过全局变量的形式使用,但只能在某个脚本里:mathLib.isPrime(2);

  • 创建模块结构

模块导出尽可能在顶层,可以更容易的使用模块导出的内容,如果仅导出单个class和function时,使用export default直接导出默认值,如: A.ts

export default class A{}

B.ts

export default function get();

C.ts

import a from ‘./A’
import b from ‘./B’
let a1 = new a();
b();

可以随意命名导入模块的类型,并且不需要多余的.来找到相关的对象。

如果想要导出多个对象,放到顶层进行导出:

A.ts

export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

导入的时候明确的列出导入的名字:

C.ts

import { SomeType, SomeFunc } from "./A";
let x = new SomeType();
let y = someFunc();

当要导出大量内容的时候,可以使用命名空间导入模式,如下:

A.ts

export class A{}
export class B{}
export class C{}
export class D{}
export class E{}
export class F{}

C.ts

import * as a from “./A”
a.A
  • 使用重新导出进行扩展

需要扩展一个模块的功能时,因为模块不会像命名空间那样去合并,可以导出一个新的实体来提供新的功能,而不是去改变原来的对象。

A.ts

export class A{}

B.ts

import {A} form “./A”;
class C extends A{}
export {C as A}
export {a} form “./A”;

模块B中导出的API和原先的A模块相似,但却没有改变原模块里的对象。

  • 模块里不要使用命名空间

模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见,命名空间在使用模块时几乎没有价值。

命名空间对全局作用域内的对象进行封装,模块本身已经 存在于文件系统中,必须使用路径和文件名来查找,通过文件系统来组织对应的解构,不需要用命名空间去再组织机构。

命名空间对于解决命名冲突比较有用,可以通过命名空间对变量进行分组隔离,但是模块本身已经是隔离的,在同一个模块中是不会存在两个同名的变量的。

  1. 文件顶层声明:export namespace Foo{},不需要namespace,直接向上移一层即可。
  2. 若文件只有一个export class或export function,尽量直接使用export default。
  3. 多个文件顶层具有同样的export namespace Foo,ts并不会合并这些定义。

二、模块解析

模块解析就是指编译器编译的时候依据的流程,按照指定的流程来找出某个导入操作所引用的具体值。

编译器定位导入的模块文件时,会遵循下列策略之一:Classic或Node,对应策略会确定从哪里去查找引入的模块。

如果定位失败了,并且模块名是非相对的,编译器会尝试定位一个外部模块声明,如果还不能解析这个模块,会记录一个错误。

1.相对模块及非相对模块导入

相对导入是以/、./、../开头的,如下:

import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";

所有其它形式的导入都是非相对的,如下:

import * as $ from "jQuery";
import { Component } from "angular2/core";

相对导入解析时是相对于导入它的文件来说的,并且不能解析为一个外部模块声明。

2.模块解析策略

模块解析策略可以使用--moduleResolution标记为指定使用哪个。 默认值为 Node。

  • Classic策略:

旧版本中ts默认的解析策略,相对导入的模式是相对于导入它的文件进行解析的,如/root/serc/A.ts中引入了import {b} from “./B”,此时的查找流程如下:

  1. /root/src/folder/B.ts
  2. /root/src/folder/B.d.ts

对于非相对模块的导入,编译器会从包含导入文件的目录开始依次向上级目录遍历定位匹配的声明文件,如/root/src/folder/A.ts文件中引入了import {b} from “B”,此时的查找流程如下:

  1. /root/src/folder/B.ts
  2. /root/src/folder/B.d.ts
  3. /root/src/B.ts
  4. /root/src/B.d.ts
  5. /root/B.ts
  6. /root/B.d.ts
  7. /B.ts
  8. /B.d.ts
  • Node策略:

模仿Node.js模块解析机制,Node.js里导入是通过require函数调用进行的,Node.js会根据require引入的是相对路径还是非相对路径做出不同的行为,相对路径按照引入文件的位置进行查找,如引入一个路径为:/root/src/A.js,A.js中包含了一个导入var x = require(‘./B’),具体的解析顺序如下:

  1. 检查/root/src/B.js文件是否存在。
  2. 将/root/src/B视为目录,检查次目录下是否包含package.json文件并且在package.json中指定了一个"main"模块。如果Node.js发现文件 /root/src/B/package.json包含了{ "main": "lib/mainModule.js" },那么Node.js会引用/root/src/B/lib/mainModule.js。
  3. 将/root/src/B视为目录,检查它是否包含index.js文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。

对于非相对模块名的解析过程是完全不同的过程,Node会在特殊的文件夹node_modules中查找对应的模块,node_modules可能与当前文件在同一级目录,或者在上层目录,Node会向上级目录遍历,查找每个node_modules直到它找到要加载的模块。如文件/root/src/A.js中使用的是非相对路径导入import x = require(“B”),此时的解析顺序如下:

  1. /root/src/node_modules/B.js
  2. /root/src/node_modules/B/package.json (如果指定了"main"属性)
  3. /root/src/node_modules/B/index.js
  4. /root/node_modules/B.js
  5. /root/node_modules/B/package.json (如果指定了"main"属性)
  6. /root/node_modules/B/index.js
  7. /node_modules/B.js
  8. /node_modules/B/package.json (如果指定了"main"属性)
  9. /node_modules/B/index.js Node.js在步骤4和7向上跳一级目录。
  • TS解析模块:

TS是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件,TS在Node解析逻辑基础上增加了Ts源文件的扩展名(.ts、.tsx、.d.ts)。在package.json里使用字段”typing”来表示类似”main”的意义,编译器会使用它来找到要使用的”main”定义文件。

如一个文件”/root/src/A.ts”中有个导入语句”import {b} from ./B”,具体的解析流程如下:

  1. /root/src/B.ts
  2. /root/src/B.tsx
  3. /root/src/B.d.ts
  4. /root/src/B/package.json (如果指定了"typings"属性)
  5. /root/src/B/index.ts
  6. /root/src/B/index.tsx
  7. /root/src/B/index.d.ts

非相对导入会遵循Node.js的解析逻辑,首先查找文件,其次是合适的文件夹,如”/root/src/A.ts”中有个”import {b} from B”会以以下顺序解析:

  1. /root/src/node_modules/B.ts
  2. /root/src/node_modules/B.tsx
  3. /root/src/node_modules/B.d.ts
  4. /root/src/node_modules/B/package.json (如果指定了"typings"属性)
  5. /root/src/node_modules/B/index.ts
  6. /root/src/node_modules/B/index.tsx
  7. /root/src/node_modules/B/index.d.ts
  8. /root/node_modules/B.ts
  9. /root/node_modules/B.tsx
  10. /root/node_modules/B.d.ts
  11. /root/node_modules/B/package.json (如果指定了"typings"属性)
  12. /root/node_modules/B/index.ts
  13. /root/node_modules/B/index.tsx
  14. /root/node_modules/B/index.d.ts
  15. /node_modules/B.ts
  16. /node_modules/B.tsx
  17. /node_modules/B.d.ts
  18. /node_modules/B/package.json (如果指定了"typings"属性)
  19. /node_modules/B/index.ts
  20. /node_modules/B/index.tsx
  21. /node_modules/B/index.d.ts

ts会在步骤8/15向上跳两次目录。

使用--noResolve:

编译器会开始编译之前解析模块的导入,每当解析了对一个文件import,这个文件会被加载到文件列表里,以供编译器后续处理。

--noResolve编译选项会告诉编译器不要添加任何不是在命令行上传入的文件到编译列表,编译器任然会尝试解析模块,但是只要没有指定这个文件,就不会被包含在内,如: a.ts

import * as A from "A" // OK, A passed on the command-line
import * as B from "B" // Error TS2307: Cannot find module B.
tsc app.ts a.ts --noResolve

使用--noResolve编译a.ts:

  • 可能正确找到A,因为它在命令行上指定了。
  • 找不到B,因为没有在命令行上传递。

在exclude列表中的模块还会被编译的问题解释:

tsconfig.json将文件夹转变一个“工程” 如果不指定任何 “exclude”或“files”,文件夹里的所有文件包括tsconfig.json和所有的子目录都会在编译列表里。 如果你想利用 “exclude”排除某些文件,甚至你想指定所有要编译的文件列表,请使用“files”。

有些文件是被tsconfig.json自动加入的,不会涉及模块解析规则,如果编译器识别出一个文件是模块导入目标,就会被加到编译列表中,不管是否被排除。

若要想从编译列表中排除一个文件,需要在标记排除它的同时,还要排除所有对它进行import或使用了///指令的文件。