• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 模块(Module Types)

    JavaScript 有一个很长的处理模块化代码的历史,TypeScript 从 2012 年开始跟进,现在已经实现支持了很多格式。不过随着时间流逝,社区和 JavaScript 规范已经收敛为名为 ES 模块(或者 ES6 模块)的格式,这也就是我们所知的import/export语法。

    ES 模块在 2015 年被添加到 JavaScript 规范中,到 2020 年,大部分的 web 浏览器和 JavaScript 运行环境都已经广泛支持。

    本章将覆盖讲解 ES 模块和和它之前流行的前身 CommonJS module.exports =语法。


    JavaScript 模块是如何被定义的

    在 TypeScript 中,就像在 ECMAScript 2015 中,任何包含了一个顶层import或者export的文件会被认为是一个模块。

    相对应的,一个没有顶层导入和导出声明的文件会被认为是一个脚本,它的内容会在全局范围内可用。

    模块会在它自己的作用域,而不是在全局作用域里执行。这意味着,在一个模块中声明的变量、函数、类等,对于模块之外的代码都是不可见的,除非你显示的导出这些值。

    相对应的,要消费一个从另一个的模块导出的值、函数、类、接口等,也需要使用导入的格式先被导入。


    非模块

    在我们开始之前,我们需要先理解 TypeScript 认为什么是一个模块。JavaScript 规范声明任何没有export或者顶层await的 JavaScript 文件都应该被认为是一个脚本,而非一个模块。

    在一个脚本文件中,变量和类型会被声明在共享的全局作用域,它会被假定你或者使用 outFile 编译选项,将多个输入文件合并成一个输出文件,或者在 HTML使用多个<script>标签加载这些文件。

    如果你有一个文件,现在没有任何import或者export,但是你希望它被作为模块处理,添加这行代码:

    export {};
    

    这会把文件改成一个没有导出任何内容的模块,这个语法可以生效,无论你的模块目标是什么。


    TypeScript 中的模块

    在 TypeScript 中,当写一个基于模块的代码时,有三个主要的事情需要考虑:

    • 语法:我想导出或者导入该用什么语法?
    • 模块解析:模块名字(或路径)和硬盘文件之间的关系是什么样的?
    • 模块导出目标:导出的 JavaScript 模块长什么样?


    ES 模块语法

    一个文件可以通过export default声明一个主要的导出:

    // @filename: hello.ts
    export default function helloWorld() {
      console.log("Hello, world!");
    }
    

    然后用这种方式导入:

    import hello from "./hello.js";
    hello();
    

    除了默认导出,你可以通过省略defaultexport语法导出不止一个变量和函数:

    // @filename: maths.ts
    export var pi = 3.14;
    export let squareTwo = 1.41;
    export const phi = 1.61;
    
    export class RandomNumberGenerator {}
    
    export function absolute(num: number) {
      if (num < 0) return num * -1;
      return num;
    }
    

    这些可以在其他的文件通过import语法引入:

    import { pi, phi, absolute } from "./maths.js";
    
    console.log(pi);
    const absPhi = absolute(phi);
    // const absPhi: number
    


    附加导入语法

    一个导入也可以使用类似于import{old as new}的格式被重命名:

    import { pi as π } from "./maths.js";
    
    console.log(π);
    /*
    (alias) var π: number
    import π
    */
    

    你可以混合使用上面的语法,写成一个单独的import

    // @filename: maths.ts
    export const pi = 3.14;
    export default class RandomNumberGenerator {}
    
    // @filename: app.ts
    import RNGen, { pi as π } from "./maths.js";
    
    RNGen;
    /*
    (alias) class RNGen
    import RNGen
    */
    
    console.log(π);
    /*
    (alias) const π: 3.14
    import π
    */
    

    你可以接受所有的导出对象,然后使用* as name把它们放入一个单独的命名空间:

    // @filename: app.ts
    import * as math from "./maths.js";
    
    console.log(math.pi);
    const positivePhi = math.absolute(math.phi);
    
    // const positivePhi: number
    

    你可以通过import "./file"导入一个文件,这不会引用任何变量到你当前模块:

    // @filename: app.ts
    import "./maths.js";
    
    console.log("3.14");
    

    在这个例子中,import什么也没干,然而,math.ts的所有代码都会执行,触发一些影响其他对象的副作用(side-effects)。


    TypeScript 具体的 ES 模块语法

    类型可以像 JavaScript 值那样,使用相同的语法被导出和导入:

    // @filename: animal.ts
    export type Cat = { breed: string; yearOfBirth: number };
    
    export interface Dog {
      breeds: string[];
      yearOfBirth: number;
    }
    
    // @filename: app.ts
    import { Cat, Dog } from "./animal.js";
    type Animals = Cat | Dog;
    

    TypeScript 已经在两个方面拓展了import语法,方便类型导入:


    导入类型

    // @filename: animal.ts
    export type Cat = { breed: string; yearOfBirth: number };
    // 'createCatName' cannot be used as a value because it was imported using 'import type'.
    export type Dog = { breeds: string[]; yearOfBirth: number };
    export const createCatName = () => "fluffy";
    
    // @filename: valid.ts
    import type { Cat, Dog } from "./animal.js";
    export type Animals = Cat | Dog;
    
    // @filename: app.ts
    import type { createCatName } from "./animal.js";
    const name = createCatName();
    


    内置类型导入

    TypeScript 4.5 也允许单独的导入,你需要使用type前缀,表明被导入的是一个类型:

    // @filename: app.ts
    import { createCatName, type Cat, type Dog } from "./animal.js";
    
    export type Animals = Cat | Dog;
    const name = createCatName();
    

    这些可以让一个非 TypeScript 编译器比如 Babel、swc 或者 esbuild 知道什么样的导入可以被安全移除。

    导入类型和内置类型导入的区别在于一个是导入语法,一个是仅仅导入类型。


    有 CommonJS 行为的 ES 模块语法

    TypeScript 之所以有 ES 模块语法跟 CommonJS 和 AMD 的required有很大的关系。使用 ES 模块语法的导入跟require一样都可以处理绝大部分的情况,但是这个语法能确保你在有 CommonJS 输出的 TypeScript 文件里,有一个 1 对 1 的匹配:

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

    你可以在模块引用页面了解到关于这个语法更多的信息。


    CommonJS 语法

    CommonJS 是 npm 大部分模块的格式。即使你正在写 ES 模块语法,了解一下 CommonJS 语法的工作原理也会帮助你调试更容易。


    导出

    通过设置全局moduleexports属性,导出标识符。

    function absolute(num: number) {
      if (num < 0) return num * -1;
      return num;
    }
    
    module.exports = {
      pi: 3.14,
      squareTwo: 1.41,
      phi: 1.61,
      absolute,
    };
    

    这些文件可以通过一个require语句导入:

    const maths = require("maths");
    maths.pi;
    // any
    

    你可以使用 JavaScript 的解构语法简化一点代码:

    const { squareTwo } = require("maths");
    squareTwo;
    // const squareTwo: any
    


    CommonJS 和 ES 模块互操作

    因为默认导出和模块声明空间对象导出的差异,CommonJS 和 ES 模块不是很合适一起使用。TypeScript 有一个 esModuleInterop 编译选项可以减少两种规范之间的冲突。


    TypeScript 模块解析选项

    模块解析是从import或者require语句中取出字符串,然后决定字符指向的是哪个文件的过程。

    TypeScript 包含两个解析策略:Classic 和 Node。Classic,当编译选项 module 不是commonjs时的默认选择,包含了向后兼容。Node 策略则复制了 CommonJS 模式下 Nodejs 的运行方式,会对.ts.d.ts有额外的检查。

    这里有很多 TSConfig 标志可以影响 TypeScript 的模块策略:moduleResolution , baseUrl , paths , rootDirs 。


    TypeScript 模块输出选项

    有两个选项可以影响 JavaScript 输出的文件:

    • target 决定了哪些 JS 特性会被降级(被转换成可以在更老的 JavaScript 运行环境使用),哪些则完整保留。
    • module 决定了转换后代码采用的模块规范

    你使用哪个 target 取决于你期望代码运行的环境。这些可以是:你需要支持的最老的浏览器,你期望代码运行的最老的 Nodejs 版本,或者一些独特的运行环境比如 Electron 等。

    编译选项 module 决定了模块之间通信使用哪一种规范。在运行时,模块加载器会在执行模块之前,查找并执行这个模块所有的依赖。

    举个例子,这是一个使用 ES Module 语法的 TypeScript 文件,展示了 module 选项不同导致的编译结果:

    import { valueOfPi } from "./constants.js";
    
    export const twoPi = valueOfPi * 2;
    


    ES2020

    import { valueOfPi } from "./constants.js";
    export const twoPi = valueOfPi * 2;
    


    CommonJS

    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.twoPi = void 0;
    const constants_js_1 = require("./constants.js");
    exports.twoPi = constants_js_1.valueOfPi * 2;
    


    UMD

    (function (factory) {
        if (typeof module === "object" && typeof module.exports === "object") {
            var v = factory(require, exports);
            if (v !== undefined) module.exports = v;
        }
        else if (typeof define === "function" && define.amd) {
            define(["require", "exports", "./constants.js"], factory);
        }
    })(function (require, exports) {
        "use strict";
        Object.defineProperty(exports, "__esModule", { value: true });
        exports.twoPi = void 0;
        const constants_js_1 = require("./constants.js");
        exports.twoPi = constants_js_1.valueOfPi * 2;
    });
    
    注意 ES2020 已经跟原始的 index.ts 文件相同了。

    你可以在 TSConfig 模块页面看到所有可用的选项和它们对应编译后的 JavaScript 代码长什么样。


    TypeScript 命名空间

    TypeScript 有它自己的模块格式,名为namespaces。它在 ES 模块标准之前出现。这个语法有一系列的特性,可以用来创建复杂的定义文件,现在依然可以在 DefinitelyTyped 看到。虽然命名空间没有被废弃,但是由于 ES 模块已经拥有了命名空间的大部分特性,因此更推荐使用 ES 模块,这样才能与 JavaScript 的(发展)方向保持一致。

    上篇:模块

    下篇:模块 nodejs