参考文章:https://www.51cto.com/article/715402.html
在node中支持两种模块方案:CommonJS(cjs) 和 ECMAScript modules(esm)
小结
- node 中可以通过 main 和 type: module | commonjs 来指定入口文件及其模块类型, exports 则是更强大的替代品,拥有更灵活的配置方式。
- 主流打包工具如 webpack rollup esbuild 则在此基础上增加了对 top-level module 的支持。
- TypeScript 则会先查看 package.json 中有没有 types 字段,否则查看 main 字段指定的文件有没有对应的类型声明文件。
- 当这些字段同时存在时,
browser
=browser+mjs
>module
>browser+cjs
>main
,查看分析过程
main
package.json 的 main 字段是最常见的指定入口文件的形式。当我们引用一个包的时候,它的入口文件就是 main 字段指向的文件。
{
"name": "@std/map",
"version": "1.0.0",
"description": "",
"main": "index.js"
}
但我们如何得知这个 index.js 文件是 cjs 格式还是 esm 格式呢?
一种方式是看后缀名是 cjs 还是 mjs,另一种方式是看 type 字段。
type
package.json 里提供了一个type字段用于标注用什么格式来执行.js文件。
{
"name": "@std/map",
"version": "1.0.0",
"description": "",
"type": "commonjs", // commonjs | module, 默认commonjs
"main": "index.js"
}
那么不同模块格式的文件如何相互引用呢?解释规则大致如下
- import cjs文件,module.exports 等同于 export default, 具名导入会根据静态分析来兼容,但是一般推荐在ESM中使用 defaultExport 格式来引入CJS文件
// index.mjs
import { someVar } from './index.cjs' // 可用,但推荐下面👇的引入方式
import pkg from './index.cjs'
- 在 cjs 中,如果想要引入 esm 文件,由于 esm 模块是异步执行的机制,必须使用 Dynamic Import 即 import() 来引用。
const pkg = require('./index.mjs') // ❌ Error
const pkg = await import('./index.mjs')
通过 type 和 main 字段,可以指定入口文件以及入口文件是什么类型,但是指定的只是一个入口文件,仍然不能够满足“动态”引入的需求,所以 node 又引入 exports 这个新的字段作为 main 更强大的替代品。
exports
相比较于main字段,exports可以指定多个入口文件,且优先级高于main。而且还有效限制了入口文件的范围,即如果你引入指定入口文件范围之外的文件,则会报错。
{
"name": "@std/map",
"main": "index.js",
"exports":{
"import":"./index.mjs",
"require":"./index.cjs",
"default": "./index.mjs" // 兜底使用
},
}
const pkg = require('@std/map/test.js');
// 报错!Package subpath './test.js' is not defined by "exports"
如果想指定 submodule, 我们可以这样编写
"exports": {
"." : "./index.mjs",
"./mobile": "./mobile.mjs",
"./pc": "./pc.mjs"
},
// or 更详细的配置
"exports": {
".":{
"import":"./index.mjs",
"require":"./index.cjs",
"default": "./index.mjs"
},
"./mobile": {
"import":"./mobile.mjs",
"require":"./mobile.cjs",
"default": "./mobile.mjs"
}
},
然后通过如下方式可以访问到子模块文件:
import mobile from '@std/map/mobile'
除了上面提到的 main exports 字段处理 cjs mjs 格式问题,我们清楚 npm
包其实又分为:只允许在客户端使用的,只允许造服务端使用的,浏览器/服务端都可以使用。
如果我们需要开发一个 npm
包同时兼容支持 web端 和 server 端,需要在不同环境下加载npm包不同的入口文件,显然一个 main
字段已经不能够满足我们的需求,这就衍生出来了 module
与 browser
字段。
main
: 定义了npm
包的入口文件,browser 环境和 node 环境均可使用module
: 定义npm
包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用browser
: 定义npm
包在 browser 环境下的入口文件
module
用来指定 esm 的入口,browser 环境和 node 环境均可使用。
它原不是 node 支持的标准字段,但大多数打包工具比如 webpack、rollup 以及 esbuild 等支持了这一特性,方便进行 tree shaking 等优化策略。
browser
定义 npm
包在 browser 环境下的入口文件,只在 web 端使用,并且严禁在 server 端使用。
Type Script的入口文件
TypeScript已经成为前端的主流开发方式,同时TypeScript也有自己的一套入口解析方式,只不过解析的是类型的入口文件,有效辅助开发者进行类型检查和代码提示,来提高我们编码的效率和准确性,下面我们继续了解下TypeScript是怎么解析类型文件的。
TypeScript有着对 Node 的原生支持,所以会先检查 main 字段,然后找对应文件是否存在类型声明文件,比如 main 指向的是 lib/index.js, TypeScript 就会查找有没有 lib/index.d.ts 文件。
另外一种方式,开发者可以在 package.json 中通过 types 字段来指定类型文件,exports 中同理。
如果想指定 submodule, 我们可以这样编写
"exports": {
".":{
"types": "./index.d.ts",
"import":"./index.mjs",
"require":"./index.cjs",
"default": "./index.mjs"
}
},
"types": "./index.d.ts"
TypeScript模块解析策略
tsconfig.json 包含一个 moduleResolution 字段,支持 classic(默认)和 node 两种解析策略,主要针对相对路径引入和非相对路径引入两种方式:
- classic:查找以
.ts
或.d.ts
结尾的文件 - node:以类似于 node 的解析策略来查找,但是相应的查找的范围是以
.ts
.tsx
.d.ts
为后缀的文件,而且会读取 package.json 中对应的 types(或typings)字段。