模块(Module Types) - TypeScript 模块类型
模块(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使用多个标签加载这些文件。
如果你有一个文件,现在没有任何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();
除了默认导出,你可以通过省略default
的export
语法导出不止一个变量和函数:
// @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这些可以在其他的文件通过
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 语法的工作原理也会帮助你调试更容易。
导出
通过设置全局
module
的exports
属性,导出标识符。function absolute(num: number) { if (num这些文件可以通过一个
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 的(发展)方向保持一致。
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!