编写较早,支持node中的两种模块解决方案——CommonJS(cjs)和ECMAScript模块(esm)。随着ESModule的广泛使用,社区生态逐渐转向ESModule。相对于require的runtime执行,ESModule可以用来做一些treeshaking等静态代码分析来减少代码量,但是由于CommonJS拥有庞大的用户基数,对于第三方库作者来说,ESModule不能跨界使用board,而且必须要兼容CommonJS的使用场景,所以最合理的方式就是“haveitbothways”,即使用ESModule编写库代码,然后通过TypeScript、Babel等工具辅助生成相应的CommonJS格式代码,然后根据开发者参考方法动态替换为指定格式的代码。有了两个版本的代码,第三方库作者需要编写相应的入口文件来达到“动态”导入的目的(即导入时引用ESModule的代码,require导入时引用CommonJS的代码),并且也方便打包工具剔除无用代码,减少代码体积。本文主要关注如何正确配置入口文件。注:本文以node规范为准,打包工具支持的配置方式会另行标注。本文涉及的示例代码可以通过https://github.com/HomyeeKing/test-entrymainpackage查看和测试。json的main字段是最常见的指定入口文件的形式。{"name":"@homy/test-entry","version":"1.0.0","description":"","main":"index.js"}当开发者引入@homy/test-进入包的时候,可以确定npm包的入口文件@homy/test-entry指向的是index.js。constpkg=require('@homy/test-entry')但是index.js是cjs还是esm?一种方式是我们可以通过后缀名明确标明当前文件是cjs还是esm格式:cjs-->.cjsesm--->.mjs不同模块格式的文件如何相互引用?解释规则大致如下。导入CJS格式的文件,module.exports会等同于exportdefault,namedimport根据静态分析会兼容,但一般建议使用ESM中的defaultExport格式导入CJS文件。在CJS中,如果要导入ESM文件,由于ESM模块的异步执行机制,必须使用DynamicImport,即import()来引用//index.cjsconstpkg=require('./index.mjs')//?Errorconstpkg=awaitimport('./index.mjs')//?//index.mjsimport{someVar}from'./index.cjs'//??importpkgfrom依赖以下方式引入'./index.cjs'//?另一种方式package.json的type字段用于标识类型。package.json还提供了一个类型字段来标记用于执行.js文件的格式,{"name":"@homy/test-entry","version":"1.0.0","description":"","type":"commonjs",//or"module",默认是commonjs"main":"index.js"}如果手动设置type:module,index.js会被当作一个esmodule,否则视为CommonJStype:模块,只有在Node.js>=14且使用import时才能使用,不支持require注意:.js的详细解析策略,推荐阅读https://nodejs.org/api/modules.html#enabling通过类型和主要字段,我们可以指定入口文件和入口文件的类型,但是只指定了一个入口文件,还是不能满足我们“动态”导入的需求,所以node引入了一个新的字段exports作为对main更强大的替代与main字段相比,exports可以指定多个入口文件,优先级高于main{"name":"@homy/test-entry","main":"index.js","exports":{"import":"./index.mjs","require":"./index.cjs","default":"./index.mjs"//使用},}有效限制入口文件范围,即,如果引入指定入口文件范围之外的文件,会报错constpkg=require('@homy/test-entry/test.js');//错误!packagesubpath'./test.js'isnotdefinedby"exports"如果要指定子模块,我们可以这样写“exports”:{".":"./index.mjs","./mobile":"./mobile.mjs","./pc":"./pc.mjs"},//或更详细的配置"exports":{".":{"import":"./index.mjs","require":"./index.cjs","default":"./index.mjs"},"./mobile":{"import":"./mobile.mjs","require":"./mobile.cjs","default":"./mobile.mjs"}},然后就可以访问子模块文件importpkgfrom'pkg/mobile',还有一个imports字段,主要用来控制import的解析路径,和ImportMaps类似,只是node中指定的entry需要以#开头。如果您有兴趣,可以阅读子路径导入。对于前端日常开发,我们的运行环境主要是浏览器和各种webview。我们将使用各种打包工具来压缩和翻译我们的代码,除了上面提到的主要导出Section,被主流打包工具广泛支持,还有一个模块字段模块。很多时候我们在第三方库中也能看到module字段,用来指定esm的入口,但是这个提议并没有被node采纳(使用exports),但是大多数打包工具如webpack,rollup,而esbuild支持这个特性,方便treeshaking等优化策略。另外,TypeScript已经成为前端的主流开发方式,TypeScript也有自己的一套入口解析方法,但解析的是类型入口文件,有效辅助开发者进行类型检查和代码提示提升我们编码的效率和准确性。下面我们继续了解TypeScript是如何解析类型文件的。TypeScript的Kerry小入口文件TypeScript对Node有Native支持,所以会先检查main字段,然后在对应的文件中查找是否有类型声明文件。例如,如果main指向lib/index.js,TypeScript会检查是否有lib/index.d.ts文件。另外一种方式,开发者可以通过package.json中的types字段来指定类型文件,exports中也是如此。{"name":"my-package","type":"module","exports":{".":{//TypeScript解析的入口点-必须先出现!"types":"./types/index.d.ts",//ESM中`import"my-package"`的入口点"import":"./esm/index.js",//入口-pointfor`require("my-package")inCJS"require":"./commonjs/index.cjs",},},//CJSfall-backforolderversionsofNode.js"main":"./commonjs/index.cjs",//Fall-backforolderversionsofTypeScript"types":"./types/index.d.ts"}?TypeScript模块解析策略tsconfig.json包含一个moduleResolution字段,支持经典(默认)和节点解析策略,主要是相对路径导入和非相对路径导入,我们可以通过例子来理解?经典查找以.ts或.d.ts结尾的文件相对导入///root/src/folder/A.tsimport{b}from"./moduleB"//process:/root/src/folder/moduleB.ts/root/src/folder/moduleB.d.ts相对路径会找到.ts或.d.ts在当前目录文件中no-relativeimport///root/src/folder/A.tsimport{b}from"moduleB"//过程:/root/src/folder/moduleB.ts/root/src/folder/moduleB.d.ts/root/src/moduleB.ts/root/src/moduleB.d.ts/root/moduleB.ts/root/moduleB.d.ts/moduleB.ts会向上查找,直到找到moduleB相关的.ts或.d.ts文件?node使用类似node的解析策略进行查找,但是对应的查找范围是.ts.tsx.d.ts为后缀文件,会读取package.json中对应的types(或typings)字段relative/root/src/moduleAconstpkg=require('./moduleB')//process:/root/src/moduleB.js/root/src/package.json(查找/root/src下是否有package.json,如果指定了main字段,指向main字段对应的文件)/root/src/moduleB/index.js在node环境下,会依次解析.js当前package.json中main字段指向的文件以及是否有对应的index.js文件。TypeScript解析时,后缀名被ts特定的后缀.ts.tsx.d.ts替换,此时ts会读取types字段,而不是main/root/src/moduleB.ts/root/src/moduleBtime.tsx/root/src/moduleB.d.ts/root/src/moduleB/package.json(如果它指定了类型属性)/root/src/moduleB/index.ts/root/src/moduleB/index.tsx/root/src/moduleB/index.d.tsno-relativeno-relative直接查看指定node_modules下是否有对应文件/root/src/moduleAconstpkg=require('moduleB')//process:/root/src/node_modules/moduleB.js/root/src/node_modules/package.json/root/src/node_modules/moduleB/index.js/root/node_modules/moduleB.js/root/node_modules/moduleB/package.json(如果它指定一个“main”属性)/root/node_modules/moduleB/index.js/node_modules/moduleB.js/node_modules/moduleB/package.json(如果它指定一个“main”属性)/node_modules/moduleB/index.js类似TypeScript也会替换相应的后缀名,还有更多@types/root/src/node_modules/moduleB.ts/root/src/node_modules/moduleB.tsx/root/src/node_modules/moduleB.d.ts/root/src/node_modules/moduleB/package.json下的搜索类型(如果它指定类型属性)/root/src/node_modules/@types/moduleB.d.ts<-----查看@types/root/src/node_modules/moduleB/index.ts/root/src/node_modules/moduleB/index.tsx/root/src/node_modules/moduleB/index.d.ts....另外TypeScript支持版本选择映射不同的文件。有兴趣的可以阅读version-selection-with-typesversions(地址:https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions)摘要节点可以通过main和type指定入口文件及其模块类型:module|commonjs,和exports是更强大的替代品,配置方式也更灵活。webpackrollupesbuild等主流打包工具在此基础上增加了对顶层模块的支持。TypeScript会先检查package.json中是否有types字段,否则检查main字段指定的文件是否有对应的类型声明文件
