阅读 262

`typescript`笔记-模块加载机制

前言

了解编译器是深入了解一门语言的敲门砖,因此,撰写本文以记录学习typescript 3.1的编译配置内容。

模块解析

模块

从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。

  • ts模块的一些基本常识

    1. 模块内容有自己的作用域,变量和函数不会存在于全局,外部也不可见
    2. 模块通过export导出,通过import导入,以此来建立两个文件之间的关系
    3. 模块是通过模块加载器来导入其他模块的,因此,这里会涉及到模块加载器的知识,比如commonjs、requirejs、es modules
    4. 包含顶级importexport的文件都会被当成模块,相反,如果不带有,则被视为全局可见(模块也可引用全局变量)。
  • 模块导入导出方式:

// 1. 导出声明
// 可以把interface替换为class、function、const、let等
export interface Man {
  name: string;
  speak(words: string): string;
}

// ->对应导入方式
import { Man } from 'xxx';


// 2. 导出语句
function Run(name: string): void {
  console.log(`${name} is runing`)
}
export { Run }; // import { Run } from 'xxx';
export { Run as RunMethod }; // import { RunMethod } from 'xxx';

// 3. 再导出。从一个文件导入,换个名称再导出
export { Run } from 'xxx'; // import { Run } from 'xxx';
export { Run as RunMethod} from 'xxx'; // import { RunMethod } from 'xxx';
export * from 'xxx'; // import methods from 'xxx';

// 4. 默认导出
export default 123; // import count from 'xxx';
export default {}; // import obj from 'xxx';
export default class Man {}; // import Man from 'xxx';
export default interface Run {}; // import Run from 'xxx';

// 5. 导入
import { Person } from 'xxx'; // 解构
import { Person as Man } from 'xxx'; // 解构别名
import * as xxx from 'xxx'; // 全量导入
import 'xxx.css'; // 副作用模块

// 6. 兼容性导出
// commonjs和amd环境都有一个`exports`变量,包含一个模块所有导出内容。
// ts提供了兼容导出的方法,支持`commonjs`和`amd`的`exports`
export = class Man {} // 只能使用这种方式导入 import Man = require('xxx')
复制代码
  • 编译目标 可以设定的编译目标有很多,支持平台有

    1. Node.js(Commonjs)
    2. Require.js(AMD)
    3. UMD
    4. SystemJs
    5. ECMAScript 2015 native modules(ES6)

    ts编译器会根据不同的平台来设定生成代码的模型。

  • 高级导入 ts编译器会检测是否每个模块都会在生成的JavaScript中用到。因此你如果一个模块只做了类型设定,在生成代码的时候是不会有文件引入的。

  • 使用第三方库 比如我们用到了一个js写的库,那怎么把ts用起来呢?就可以使用module关键字,并在一个.d.ts文件内用顶级export导出它,这样在ts类型系统内就可以看到这个类型。

    // 一个模块文件man.js的类型声明文件man.d.ts
    // 声明一个模块
    declare module "man" {
      export interface Human {
        name: string;
        speak(words: string): string;
      }
      // 模块内存在的类型: Man接口
      export interface Male extends Human {}
    
      // 模块内存在的类型: Man接口
      export interface Female extends Human {}
    }
    
    // 接下来就可以在你的文件内导入这个Man了
    // import something from 'module';
    import { Male, Female } from 'man';
    复制代码
    // 通配符模块 这个在声明图片、json、css什么的时候用处比较大
    declare module "*.css"; // 这样就可以在ts代码中引入任意css文件了,编译器不会报错
    declare module "*.json";
    
    复制代码

模块导入策略

第一步

在写了一个import {a} from "module";语句时编译器首先会根据预定的策略来定位导入模块的文件。

  • Classic: AMD | System | ES2015属于这个模式

    • 相对导入的文件是相对于导入它的文件进行解析的

      import a from './a';
      // 搜索策略
      // path/to/this/file/a.ts
      // path/to/this/file/a.d.ts
      复制代码
    • 非相对路径的会从包含导入文件的目录开始依次向上目录遍历

      import a from 'a';
      // 搜索策略
      // root/sub/a.ts
      // root/sub/a.d.ts
      // root/a.ts
      // root/a.d.ts
      // /a.ts
      // /a.d.ts
      复制代码
  • Node: 除了上述的模式,都属于Node模式。本模式是模仿Node.js的模块解析模式,根据require的路径是相对路径还是非相对路径,解析行为不一样。

    // 相对路径
    // /root/src/moduleA文件中
    const b = require('./moduleB')
    // 搜索策略
    // 1. root/src/moduleB.js是否存在
    // 2. root/src/moduleB是否是一个目录 如果是目录,是否包含package.json。如果包含package.json则导向main模块
    // 3. root/src/moduleB是否是一个目录,如果是目录,是否包含一个index.js文件,如果包含index.js,则作为模块
    
    // 非相对路径
    const c = require('moduleC')
    // 搜索策略
    // 1. /root/src/node_modules/moduleC 这里的小策略和上面的相对路径一样,都是moduleC.js -> moduleC/package.json -> moduleC/index.js
    // 2. /root/node_modules/moduleC 同上
    // 3. /node_modules/moduleC
    
    // 上述是Node.js的搜索路径,对typescript来说,就是变换了一下检测顺序
    // 1. *.ts -> *.tsx -> *.d.ts -> package.json -> module/*.ts -> module/*.tsx -> module/*.d.ts
    // 2. 从导入它的文件的路径开始,逐级向上遍历: /root/src/node_modules/moduleB.ts -> /root/node_modules/ -> /node_modules/
    复制代码

第二步

如果第一步的策略没法找到对应模块,并且模块名是非相对的(如'moduleA'),编译器会尝试去定位一个外部模块声明(上一章有讲到)。

第三步

上述都时报了,编译器就会报错error TS2307: Cannot find module 'moduleA'

解析策略

相对导入与非相对导入

  • 相对导入: / | ./ | ../开头的

    • 相对的是导入它的文件,并且不能解析为一个外部模块声明。
  • 非相对导入: 'vue' | 'react' | '@angular/core'等类型

    • 可以相对于baseUrl解析

      • baseUrlAMD模块常用做法,要求运行时模块都在一个文件夹里面(构建到一起)
      • 所有非相对模块导入都会被当做相对于baseUrl
      • 命令行中如果输入了baseUrl参数,就使用。如果给定的是相对路径,就相对于当前路径进行计算。
      • tsconfig.json配置的baseUrl。如果给定的是相对路径,就相对于tsconfig.json进行计算。
    • 可以根据路径映射来解析

      • 路径配置为compilerOptions.paths
      • 比如
      {
        "compilerOptions": {
          "paths": {
            "jquery": ["node_modules/jquery/dist/jquery"] // 相对于baseUrl来解析此路径
          },
          "*": [ // 匹配所有路径
            "*", // 不发生改变 还是使用baseUrl/moduleName查找
            "xxx/*" // 有xxx前缀的改为base/xxx/moduleName查找
          ]
        }
      }
      复制代码
    • 可以被解析为外部模块

路径配置

compilerOptions.paths在上一节已经讲了,本小节讲一下compilerOptions.rootDirs

{
  "compilerOptions": {
    "rootDirs": [ // 虚拟目录
      "src/views",
      "generated/templates/views" // 这两个实际目录被虚拟目录统一了,各自下面的文件就可以用import a from './xxx'来访问彼此的文件,因为他们都是在同一个虚拟目录下
    ]
  }
}
复制代码

官方文档中讲到了rootDirs的几个应用场景

  • 两个目录下的文件会被构建时拷贝到同一个目录

  • 特定的条件引入,比如国际化

    {
      "compilerOptions": {
        "rootDirs": [
          "src/zh",
          "src/de",
          "src/#{locale}" // 允许你import messages from './#{locale}/messages',就会被解析为对应的国际化路径,比如./zh/messages
        ]
      }
    }
    复制代码

编译选项

由于编译器大多数情况下都是在解决模块问题,因此上面花了很多时间来记录ts的模块加载机制,下面正式贴出编译选项。由于所有的编译选项可以在官网找到,这里只列常用的一些

选项 类型 默认值 描述 注释
target string "es3" 指定ECMAScript目标版本 "ES3"(默认), "ES5""ES6"/ "ES2015""ES2016""ES2017""ESNext" 表示编译器用什么版本来生成文件,一般都是生成"es5",来保证各个环境上都好用。也有的可以生成更高版本比如es6,再用其他工具(babel)来解析的。
module string target==="es6"? "es6": "commonjs" 生成代码哪个模块解析器系统的代码:"None""CommonJS""AMD""System""UMD""ES6""ES2015" "AMD""System"因为编译后文件都在同一个目录,因此只有这两个目标模块系统可以和outFile一起使用。(上文有讲到)
allowJs boolean false 允许编译js文件 比如你ts项目里面有的模块确实用的js,那就可以用这个设置为true
checkJs boolean false .js中报告错误,和allowJs一起使用
jsx string "Preserve" .tsx文件内支持jsx 有两个模式"React"或"Preserve"
baseUrl string 解析非相对模块时的基准目录 上文有讲到,比如配置为"."则表示以"tsconfig.json"所在目录为基准
paths Object 模块里面有讲到,再举个例子,比如你配置为{"@/*": "src/*"}那么你导入'@/components/vue'相当于导入'src/components/vue'。起到一个路径映射的作用,告诉编译器,找不到的路径可以通过这个路径试试
outDir string 重定向输出目录 如果不配置就是在你的ts文件所在目录生成js文件,设置了比如'./dist'(这里的'.'表示tsconfig所在目录)就会把所有生成文件定向到dist里面
rootDir string 内部计算出来 用来控制输出目录结构 用来定义rootDir里面的文件目录格式
moduleResolution string module === "AMD" or "System" or "ES6" ? "Classic" : "Node" 决定如何处理模块。或者是"Node"对于Node.js/io.js,或者是"Classic"(默认) 模块路径解决方案,前面几个小节已经讲到了。
types string[] 要包含的类型声明文件列表
typeRoots stringp[] 要包含的类型声明文件路径列表 支持*通配符,比如你项目用types/作为所有类型就可以配置["./src/**/*/types"]

其他配置

tsconfig的典型配置中,最主要的就是"compilerOptions",另外还有些配置也需要了解

{
  "compilerOptions": {},
  "include": [], // 表明哪些文件被包含在内,
  "exclude": [], // 表明哪些文件不被包含
}
复制代码