大家好,我是17。
moduleResolution 是 typescript 模块配置中最重要的一个配置,所以 17 单拿出来讲一下。如果你去看文档还是挺复杂的,但如果不去深究细节,只想知道如何配置还是很简单的。3 分钟就能学会。
moduleResolution 的作用
moduleResolution 是用来解决如何查找文件的。比如
import name from 'iam17'
这条语句首先要做的就是把 iam17 对应的文件找到。如何查找文件受到 moduleResolution 配置的景响。
配置 moduleResolution
为打包器量身定做的 Bundler 选项
前端主要的场景是使用 typescript 做类型检查,并不输出内容。把 ts 转换成 js 的工作是由打包工具来完成的。这种情况下,把 moduleResolution 设置为 Bundler 就行了。Bundler 的原意就是打包器的意思。现在流行的打包器 webpack,vite 等支持的特性,Bundler 选项都支持。Bundler 选项就是为打包工具量身定做的。
专为 node 开发的 node16 选项
还有一部分的前端开发场景是进行 nodejs 开发。最终的代码的运行环境是 node 环境。这个时候把 moduleResolution 设置为 node16 就行了。
其它的选项
之所以剩下的都归为其它,是因为其它的使用场景很少,了解即可。
如果不得不使用 node 老版本,只能用 Node10 选项了,也可以用 Node。 Node10 和 Node 是一个意思,可以认为是一个选项有两个名字。ts 5.0 中,把 Node rename 成了 Node10,为了让原来的配置文件有效,保留了 Node 选项。
NodeNext 目前等同于 Node16。如果之后有 Node17,那么 NodeNext 就等同于 Node17。NodeNext 总是指向 node 环境的最新配置。
还有一个选择 Classic。Classic 虽然还在出镜,但也仅限于名字本身了,几乎不会有使用的场景。ts 已经明确说明,在下个版本会移出 Classic 选项。各们同学就当他不存在就好。
到这里,如何配置 moduleResolution 就讲完了。这对于 99% 的同学来说已经够用了。但是作为 1% 的同学,还想有更高的追求,就需要了解一下配置的细节。比如 17 想写一个打包工具,不知道 ts 执行的细节 ,怎么和 ts 配合啊。就算没有写工具的需求,如果哪天 ts 报错了,总得知道怎么修改配置吧。还有一个理由,如果你去面试,问你 moduleResolution 的执行细节呢?虽然开发的时候用不到,但面试的时候不都是要求你会靠火箭的吗?其实呢,面试官也知道平时不用,这只是个加分项,可能是想知道你是不是爱学习。所以同学们,系好安全带,准备出发吧!
说到细节,首先要知道这些选项都是怎么来的,然后再理解这些选项都做了什么就水到渠成了。还要认清 TypeScript 的定位,TypeScript 是为了让 js 更加强大,更加好用,TypeScript 最终还是要编译成 js 才能用。所以在各个历史时期,ts 都是配合 js。对于解析文件路径,TypeScript 几乎总是从其输出JavaScript 文件的角度来考虑这些问题,而不是从其输入TypeScript(或 JavaScript!)文件的角度来考虑这些问题。
在解释这些查找细节之前,首先需要了解一下,ts 是如何自动补充文件名扩展名,自动解析文件夹模块的。
文件扩展名自动补充
import name from './iam17'
import name from './iam17.js'
两条语句一条带扩展名,一条不带,对于 ts 来说,.js
扩展名可以省略,
不带扩展名,和带 .js
扩展名, ts 查找相关文件策略是一样的。
ts 拿到文件名 iam17 后,会查找相关的文件。分两步
- 查找相关的类型文件,看有没有 iam17.ts,iam17.tsx,iam17.d.ts
- 查找相关的 js 文件,看有没有 iam17.js,iam17.jsx
ts 是会优先查找类型文件,如果没有类型文件才去查找 js 文件。
如果扩展名是 mjs
import name from './iam17.mjs'
ts 要查找的文件有所不同
- 查找相关的类型文件,看有没有 iam17.mts,iam17.d.mts
- 查找 iam17.mjs
如果扩展名是 cjs
import name from './iam17.cjs'
ts 要查找的文件为
- 查找相关的类型文件,看有没有 iam17.cts,iam17.d.cts
- 查找 iam17.cjs
如果只有类型文件,没有 js 文件,ts 会报错吗?答案是不会。
目录模块
还是这条语句
import name from './iam17'
如果不带后缀名,ts 除了查找相关的文件外,还会检查 iam17 是不是一个文件夹。
如果 iam17 是一个文件夹,会解析根目录下的 package.json 文件。
- 解析 package.json 中的 types
- 解析 package.json 中的 typings
- 解析 package.json 中的 main
如果上面 3 步都找不到,查找根目录下的 index.ts,index.d.ts,index.tsx,index.js。
总的来说,也是先查找类型文件,找不到类型文件,再查找 js 文件。
typings 是历史遗留,不要使用
types 和 typings 直接指定类型文件。 main 指定 js 文件,ts 找到 main 指定的 js 文件后还会根据补充文件扩展名的规则查找类型文件。
有了 文件扩展名补充 和 文件夹模块 这两个基础知识,再理解 ts 的文件查找逻辑就容易多了。
选项 Classic
Classic 是第一个 moduleResolution 选项。因为已经几乎没有使用的场景,所以直接忽略。
选项 Node
当 nodeJs 的 commonJs 模块标准逐渐成为主流标准的时候,ts 适时的给出 node 选项。听这个名字就知道,ts 查找文件的方式和 nodeJs 差不多。因为 ts 有类型,所以在查找上,ts 是优先查找类型文件,其次才是查找实际的 js 文件。查找逻辑分下面几种
- 绝对路径
- 相对路径
- 安装包路径
- node 核心模块
绝对路径
以 / 开头的就是绝对路径。绝对路径是磁盘上的绝对物理路径。
import name from '/root/home/iam17'
ts 会判断 iam17 是文件夹还是文件,如果是文件夹就按文件夹模块查找,如果是文件,就按文件扩展名补充查找。
如果后面有扩展名,不会执行文件模块查找。
相对路径
项目内的模块必须用 前导 './'
或 '../'
来指示相对路径。
import name from './iam17'
ts 会以当前文件为基准进行查找。ts 会判断 iam17 是文件夹还是文件,如果是文件夹就按文件夹模块查找,如果是文件,就按文件扩展名补充方案查找。
如果后面有扩展名,不会执行文件模块查找。
安装包路径
如果没有前导 '/'
、'./'
或 '../'
来指示文件,该模块会从 node_modules
文件夹加载。
import name from 'iam17'
ts 还是会优先查找 iam17 的类型声明,然后才是查找 js 文件。类型声明会先从模块中查,查不到,再到 @types/iam17 中查。
在模块中查找过程和 文件夹模块 查找逻辑一样。
和文件夹模块不同的是,ts 查找 node_modules 中的模块首先会在当前目录查,如果查不到,要到上级的 node_modules 目录查,直到根目录下的 node_modules。
node 选项还可以随意查找子路径 比如 import name from ‘iam17/work’
node 核心模块
ts 并不认识哪些是 node 核心模块。ts 会当普通模块处理,解决办法就是安装 node 类型声明
npm install @types/node
typesVersions
如果你需要在多个 ts 版本中进行开发,可能需要多个版本的声明文件,因为不同版本 ts 的声明文件可能是不兼容的。
{"name": "pkg","version": "1.0.0","types": "./index.d.ts","typesVersions": {">=3.1": {"*": ["ts3.1/*"]}}
}
了解下即可,一般不会遇到需要配置 typesVersions 的情况。这种只是用来应急的,如果把它当作常态,你需要反思下项目管理了。
选项 Node16,NodeNext
typeScript 4.7 增加了 Node16,NodeNext 这两个选项,因为这个时期 ESM 模块已经成为标准。和 Node 选项相比,在解析路径方面新增加两条规则,删除了两条规则。
增加了 package.json “exports”
当 moduleResolution 设置为 node16, nodenext, or bundler,并且 resolvePackageJsonExports(tsconfig.json 中的配置)没有设置为 false,ts 会解析 package.json 的 “exports” 信息。
当 moduleResolution 设置为 node16, nodenext, or bundler 的时候,resolvePackageJsonExports 默认是开启的。为什么 ts 要增加 resolvePackageJsonExports 这个配置。原因是为了照顾老项目。
resolvePackageJsonExports 了解一下即可,一般不需要手动修改它。
关于 exports 详细规范大家可以去 node 官网上去查,17 贴心的直接给出地址 点这里查看 export 详情
在 node exports 规范的基础上,ts 还会查找 types ,default 两个用户导出条件。关于用户导出条件可以看我写的上一篇文章 typescipt 配置精讲 | customConditions
增加了 package.json “imports” and self-name imports
当 moduleResolution 设置为 node16, nodenext, or bundler,并且 resolvePackageJsonImports(tsconfig.json 中的配置)没有设置为 false,ts 会解析 package.json 的 “exports” 信息。
当 moduleResolution 设置为 node16, nodenext, or bundler 的时候,resolvePackageJsonImports 默认是开启的。resolvePackageJsonImports 也是为了照顾老项目设置的。
resolvePackageJsonImports 了解一下即可,一般不需要手动修改它。
详情可以参阅 ts 文档,点这里
简单来说 #imports 相当于是给子路径起了一个别名。先查找 package.json,找到 imports,再找到相应的 key,根据 key 的内容去查找类型。了解下即可。
删除了文件扩展名自动补充
因为 在 node 的 ESM 模块中,引用模块是必须要写文件扩展名。
import name from './iam17'
这样写会报错,必须要补全后面的 .js
,这样写才行:./iam17.js
删除了目录模块的支持
设置为 node16 后,ts 不再进行文件夹模块的尝试。
node 在 ESM 模块下,找不到文件就会报错,不再做其它尝试。 ts 也只是查找相关的类型文件,找不到就报错。
17 觉得 ESM 要求明确写出扩展名是个好事,这样会大大简化查找文件的逻辑。但是,这只能是一个理想,因为目前已经存在的老项目都是没写扩展名的。这就导致 Node16 只能在新项目里用。很多同学都习惯了不写扩展名,现在让他们写扩展名,会觉得不习惯。最后的结果就是新项目也没人愿意用 Node16。
Bundle 选项
由于 Node16 让很多同学不习惯,所以 ts 在 5.0 又推出了 Bundle 选项。反正有打包工具打底,Bundle 选项索性支持所有特性,把文件扩展名自动补充,目录模块的规则又加了回来,也支持 “imports”,“exports"。
到这所有选项就讲完了,可是路径解析的故事还没有结束,小伙伴们再坚持一下,马上到终点了。
虽然本篇文章讲的是 moduleResolution 配置,但如果不讲 paths,baseUrl,typeRoots,types 和全局类型声明,ts 的路径解析逻辑是不完整的。
paths,全局路径映射
如果每次都写相对路径还是很不方便的,这时可以用 paths 简化。
{"compilerOptions": {"module": "esnext","moduleResolution": "bundler","paths": {"@/*": ["./src/*"]}}}
有了这个配置后,就可以这样写路径了,无论是在哪个文件里
import name from '@/iam17'
无论是哪个文件里,都会被解析成到项目根目录/src/iam17。这个配置还是挺常用的,实际项目里,一般都会加上这样的配置。
更多 paths 的详情 点这里查看
注意:如果不打算用打包工具,不要用 paths 配置,ts 不会对 path 设置的映射做转换
baseUrl
这是历史遗留的产物,不要使用,同学们当它不存在就好。
如果想了解一下详情,可以 看这里
typeRoots
ts 首先会查找类型文件,在模块里查不到会到 @types 目录去查,这个@types 就是默认的 typeRoots,一般不需要修改 typeRoots, 了解下就好。查看详情
types
types 相当于一个白名单,只有加入白名单的模块, ts 才会到 typeRoots 中查找。types 限制了 ts 在 typeRoots 中的查找范围。一般也不会用到这个配置,了解下就好。里查看详情
全局类型声明
在项目中,如果在 ts , .d.ts 文件中没有 import ,export 语句,文件中定义的类型就会成为全局的类型。以这种方式定义的模块全局可用。
用 declare module 来定义模块。这个定义是全局的。
在项目中添加 index.d.js
declare module 'iam17' {const name: stringexport default { name }
}
在项目中的任意 .ts 文件中都可以解析到 iam17 模块
本文到这里就结束了,撤花~
参考
- CommonJS 模块
- ECMAScript 模块
- TS Modules - Reference
番外
17 觉得,随着时间的推移,Node,Node10 两个选项可能会被移除。就和 Classic 的结局一样,它们都只是过渡值,最终肯定是 ESM 模块一统天下。
对于 ESM 规定必须写文件后缀名这事,17 是非常赞同的。只是使用者不会管你后面的逻辑有多复杂,用着爽就好。所以虽然规范不再支持,但打包工具依然会把这个逻辑补充上。如果一直这样,不知道后面规范会不会把自动补后缀名的逻辑再加回来。
如果不考虑历史原因,js 模块路径解析也并不复杂。 但 js 曾经的模块方案终究还是留下了自己的印记。
17 花了两天,周末都在写。本来打算半天就写好了,但实际上内容特别繁杂。如果你一直扎进文档,可能会越看越晕,所以 17 写这篇文章进行梳理。关于解析路径的内容很多,如果都放上,那就成一本书的规模了。多数内容文档上都有,没必要搬来搬去的。17 尽量压缩内容,但保证所有关键点都写到,让大家在最短的时间内对 ts 路径解析有一个整体的把握。