TypeScript 模块那些事儿

2,215 阅读4分钟

哈喽,大家好,我是 SuperYing。今天我们来聊聊 TypeScript 模块那些事儿。

关于术语的一点说明:
TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015 里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

什么是模块

TypeScript 沿用了 ES6 的模块概念。模块只能在其自身的作用域内执行,而不是在全局作用域里。模块内的变量,函数,类等可以通过 export 导出,也可以通过 import 导入所需的其他模块内容。
模块更形象的叫法应该是“文件模块”,任何包含 import 或 export 的文件,都会被识别为模块。相反,若一个文件中没有 importexport,则该文件内容全局可见。

用法

  • 导出
    • 导出声明
    // 导出变量
    export const foo = '123'
    // 导出类型
    export type FooType = {
        foo: string
    }
    // 导出函数
    export function bar() {
        console.log(123)
    }
    
    • 导出语句
    // 导出变量
    const foo = '123'
    // 导出类型
    type FooType = {
        foo: string
    }
    // 导出函数
    function bar() {
        console.log(123)
    }
    export { foo, FooType, bar }
    
    • 重新导出,可以通过 as 重命名变量并导出
    const foo = '123'
    export { foo as renamedFoo }
    
    • 默认导出
      每个模块仅能有一个默认导出,使用 default 关键字标记。
    export default '123'
    export default function Foo() {}
    export default class Foo {}
    
  • 导入
    • 导入模块的一个变量或类型
    import { foo } from './foo'
    
    • 重命名导入的变量或类型
    import { foo as renamedFoo } from './foo'
    
    • 导入整个模块,使用 * as 指定一个对象,导入模块的所有输出值都赋值给该对象
    import * as Foo from './foo'
    
    • 只导入模块,有些模块内部可能会设置某些全局状态,供其他模块使用,在模块没有任何导出或者用户不关心其导出内容时,可以使用如下方式导入:
    import 'core-js'; // 一个普通的 polyfill 库
    
    • 默认导入
    import defaultFoo from './foo'
    

查找策略

模块查找策略相关的 TypeScript 配置:
开启 moduleResolution: node 选项,启用 Node 模式;如果使用了 module: commonjsmoduleResolution: node 会默认开启。

模块查找场景主要分为以下两种:

  • 相对路径模块(以 . 开头,例如 ./foo, ../foo 等)
  • 其他动态查找模块(如 vue, react, reactDOM 等)

相对路径模块

仅按照相对路径规则查找即可:

  • 如果文件 bar.ts 中含有 import * as foo from './foo',那么 foo 文件必须与 bar.ts 文件存在于相同的文件夹下。
  • 如果文件 bar.ts 中含有 import * as foo from '../foo',那么 foo 文件所存在的地方必须是 bar.ts 的上一级目录。
  • 如果文件 bar.ts 中含有 import * as foo from '../someFolder/foo',那么 foo 文件所在的文件夹 someFolder 必须与 bar.ts 文件所在文件夹在相同的目录下。

其他动态查找模块

若导入路径不是相对路径,则模块查找与 Node 模块解析策略相似。

  • 当你使用 import * as foo from 'foo',将会按如下顺序查找:

    • ./node_modules/foo
    • ../node_modules/foo
    • ../../node_modules/foo
    • 直到系统的根目录
  • 当你使用 import * as foo from 'something/foo',将会按照如下顺序查找:

    • ./node_modules/something/foo
    • ../node_modules/something/foo
    • ../../node_modules/something/foo
    • 直到系统的根目录

导入模块解析

仍然以 import * as foo from 'foo' 为例:

  • 若 foo 是一个文件,匹配。
  • 否则,若 foo 是一个文件夹,且存在 foo/index.ts, 匹配。
  • 否则,若 foo 是一个文件夹,且存在 package.json 文件,在该文件中指定了 types 属性且对应的文件存在,匹配。
  • 否则,若 foo 是一个文件夹,且存在 package.json 文件,在该文件中指定了 main 属性且对应的文件存在,匹配。

模块编译

通过设置 tsconfig 编译选项 module 的值,可以把 TavaScript 模块编译成不同 JavsScript 模块类型。

  • "CommonJS":NodeJs 模块。
  • "AMD":Require.js 模块。
  • "System":SystemJs 模块。
  • "UMD":兼容多个模块加载器,或者不使用模块加载器(全局变量)。
  • "ES6""ES2015":ES6 模块。 目前比较常用的是 CommonJSUMDES6,大部分框架或库会同时编译为这三种模式,如 Element-Plus。

*.d.ts

我们在使用 TypeScript 开发的时候,有时会用到非 TypeScript 类库,它们没有自己的类型,我们在引入的时候,往往会收到 TypeScript 的报错信息,类似于 ‘无法解析对应的模块’
那么如何搞定这种类库的类型呢,这时候就需要我们来手动声明类库暴露出来的 API 了。这类声明通常在 .d.ts 文件中定义。(这里使用 module 关键字,并且名称使用引号包裹)

declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

有时候我们只是想快速使用某个类库模块,并不想去一一声明其 API 类型。可以使用如下简写形式

declare module "url"

简写形式下所有导出的类型都是 any

好啦!以上便是「 TypeScript 模块 」的全部内容,感谢阅读。

欢迎各路大佬讨论、批评、指正,共同进步才是硬道理!