[转]如何正确地配置入口文件?

第三方库作者就需要编写相应的入口文件,来达到“动态”引入的目的,同时也方便于打包工具对于无用代码的剔除,减少代码体积,本篇文章主要聚焦于前端工程如何正确地配置入口文件。

写在前面

在node中支持两种模块方案——CommonJS(cjs) 和 ECMAScript modules(esm)。随着ESModule的广泛使用,社区生态也在逐渐转向ESModule,ESModule相比于require的运行时执行,可以用来做一些静态代码分析如tree shaking等来减小代码体积,但是由于CommonJS已有庞大的用户基础,对于第三方库作者来说,不能完全一刀切只用ESModule,还要兼容CommonJS场景的使用,所以最合理的方式就是“鱼和熊掌兼得”,即使用ESModule编写库代码,然后通过TypeScript、Babel等工具辅助生成对应的CommonJS格式的代码,然后根据开发者的引用方式来动态替换为指定格式的代码。有了两种版本的代码,第三方库作者就需要编写相应的入口文件,来达到“动态”引入的目的(即import引用的时候指向ESModule的代码,require引入则指向CommonJS的代码),同时也方便于打包工具对于无用代码的剔除,减少代码体积,本篇文章主要聚焦于如何正确地配置入口文件。

注:本篇文章以node规范为准,对于打包工具额外支持的配置方式会进行额外标注
本文的涉及的示例代码可以通过 https://github.com/HomyeeKing/test-entry 进行查看、测试

main

package.json的 main字段是最常见的指定入口文件的形式

{
  "name": "@homy/test-entry",
  "version": "1.0.0",
  "description": "",
  "main": "index.js"
}

当开发者引入@homy/test-entry这个包的时候,可以确定@homy/test-entry 这个npm包的入口文件指向的是 index.js

const pkg = require('@homy/test-entry')

但是index.js究竟是cjs or esm?

一种方式是我们可以通过后缀名来显示地标注出当前文件是cjs还是esm格式的:

  1. cjs ---> .cjs

  2. esm ---> .mjs

那么不同模块格式的文件如何相互引用呢?解释规则大致如下

  1. import了CJS格式的文件,module.exports会等同于export default, 具名导入会根据静态分析来兼容,但是一般推荐在ESM中使用defaultExport格式来引入CJS文件

  2. 在CJS中,如果想要引入ESM文件,因为ESM模块异步执行的机制,必须使用Dynamic Import即import()来引用


// index.cjs
const pkg = require('./index.mjs')  // ❌ Error
const pkg = await import('./index.mjs')  // ✅

// index.mjs
import { someVar } from './index.cjs' //  ⚠️ it dependens 推荐下边方式引入
import pkg from './index.cjs'  //  ✅

另一种方式是通过package.json的 type字段来标识

typepackage.json 里也提供了一个type字段 用于标注用什么格式来执行.js文件,


{
  "name": "@homy/test-entry",
  "version": "1.0.0",
  "description": "",
  "type": "commonjs", // or "module", 默认是 commonjs
  "main": "index.js"
}

如果手动设置type: module, 则将index.js当做esmodule处理,否则视为CommonJS

type: module ,只有Node.js >= 14 且使用import才能使用,不支持require引入

注:关于.js的详细解析策略推荐阅读 https://nodejs.org/api/modules.html#enabling

通过type和main字段,我们可以指定入口文件以及入口文件是什么类型,但是指定的只是一个入口文件,仍然不能够满足我们“动态”引入的需求,所以node又引入exports这个新的字段作为main更强大的替代品。

exports

相比较于main字段,exports可以指定多个入口文件,且优先级高于main


{
 "name": "@homy/test-entry",
  "main": "index.js",
  "exports":{
    "import":"./index.mjs",
    "require":"./index.cjs",
    "default": "./index.mjs"  // 兜底使用 
  },
}

而且还有效限制了入口文件的范围,即如果你引入指定入口文件范围之外的文件,则会报错


const pkg = require('@homy/test-entry/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 pkg from 'pkg/mobile'

另外还有一个imports 字段,主要用于控制import的解析路径,类似于Import Maps, 不过在node中指定的入口需要以#开头,感兴趣的可以阅读subpath-imports对于前端日常开发来说,我们的运行环境主要还是浏览器和各种webview,我们会使用各种打包工具来压缩、转译我们的代码,除了上面提到的main exports字段,被主流打包工具广泛支持的还有一个module字段

module

大部分时候 我们也能在第三方库中看到module这个字段,用来指定esm的入口,但是这个提案没有被node采纳(使用exports)但是大多数打包工具比如webpack、rollup以及esbuild等支持了这一特性,方便进行tree shaking等优化策略

另外,TypeScript已经成为前端的主流开发方式,同时TypeScript也有自己的一套入口解析方式,只不过解析的是类型的入口文件,有效辅助开发者进行类型检查和代码提示,来提高我们编码的效率和准确性,下面我们继续了解下TypeScript是怎么解析类型文件的。

Type Script

TypeScript有着对Node的原生支持,所以会先检查main字段,然后找对应文件是否存在类型声明文件,比如main指向的是lib/index.js, TypeScript就会查找有没有lib/index.d.ts文件。 另外一种方式,开发者可以在package.json中通过types字段来指定类型文件,exports中同理。

{
  "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for TypeScript resolution - must occur first!
            "types": "./types/index.d.ts",
            // Entry-point for `import "my-package"` in ESM
            "import": "./esm/index.js",
            // Entry-point for `require("my-package") in CJS
            "require": "./commonjs/index.cjs",
        },
    },
    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs",
    // Fall-back for older versions of TypeScript
    "types": "./types/index.d.ts"
}

TypeScript模块解析策略

tsconfig.json包含一个moduleResolution字段,支持classic(默认)和node两种解析策略,主要针对相对路径引入和非相对路径引入两种方式,我们可以通过示例来理解下

classic

查找以.ts.d.ts结尾的文件

  • relative import

//  /root/src/folder/A.ts
import { b } from "./moduleB"


// process:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts

相对路径会找当前目录下的.ts 或.d.ts的文件

  • no-relative import


//  /root/src/folder/A.ts
import { b } from "moduleB"


// process:
/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/moduleA
const pkg = 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/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (if it specifies a types property)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
  • no-relative

no-relative就直接查看指定node_modules下有没有对应文件

/root/src/moduleA
const pkg = 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 (if it specifies a "main" property)
/root/node_modules/moduleB/index.js

/node_modules/moduleB.js
/node_modules/moduleB/package.json (if it specifies a "main" property)
/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 (if it specifies a types property)
/root/src/node_modules/@types/moduleB.d.ts     <----- check out @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)总结

  1. node中可以通过maintype: module | commonjs 来指定入口文件及其模块类型, exports 则是更强大的替代品,拥有更灵活的配置方式

  2. 主流打包工具如webpack rollup esbuild 则在此基础上增加了对top-level module的支持

  3. TypeScript 则会先查看package.json中有没有types字段,否则查看main字段指定的文件有没有对应的类型声明文件

参考

  1. https://webpack.js.org/guides/package-exports/
  2. https://nodejs.org/api/packages.html#packages_package_entry_points
  3. https://esbuild.github.io/api/#main-fields
  4. https://www.typescriptlang.org/docs/handbook/module-resolution.html#relative-vs-non-relative-module-imports
  5. https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions
  6. https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容