当前位置:网站首页>如何正确地配置入口文件?
如何正确地配置入口文件?
2022-08-02 23:46:00 【阿里巴巴淘系技术团队官网博客】
第三方库作者就需要编写相应的入口文件,来达到“动态”引入的目的,同时也方便于打包工具对于无用代码的剔除,减少代码体积,本篇文章主要聚焦于前端工程如何正确地配置入口文件。
写在前面
在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格式的:
cjs --->
.cjs
esm --->
.mjs
那么不同模块格式的文件如何相互引用呢?解释规则大致如下
import了CJS格式的文件,module.exports会等同于export default, 具名导入会根据静态分析来兼容,但是一般推荐在ESM中使用defaultExport格式来引入CJS文件
在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
字段来标识
type
package.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)
总结
node中可以通过
main
和type: module | commonjs
来指定入口文件及其模块类型,exports
则是更强大的替代品,拥有更灵活的配置方式主流打包工具如webpack rollup esbuild 则在此基础上增加了对top-level
module
的支持TypeScript 则会先查看package.json中有没有
types
字段,否则查看main字段指定的文件有没有对应的类型声明文件
参考
https://webpack.js.org/guides/package-exports/
https://nodejs.org/api/packages.html#packages_package_entry_points
https://esbuild.github.io/api/#main-fields
https://www.typescriptlang.org/docs/handbook/module-resolution.html#relative-vs-non-relative-module-imports
https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions
https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
团队介绍
我们是大淘宝技术行业与商家技术前端团队,主要服务的业务包括电商运营工作台,商家千牛平台,服务市场以及淘系的垂直行业。团队致力于通过技术创新建设阿里运营、商家商业操作系统,打通新品的全周期运营,促进行业垂直化运营能力的升级。
* 拓展阅读
作者|王宏业(莽原)
编辑|橙子君
边栏推荐
猜你喜欢
随机推荐
如何使用vlookup+excel数组公式 完成逆向查找?
令人心动的AI综述(1)
js基础知识整理之 —— Math
用了 TCP 协议,数据一定不会丢吗?
MySQL最大建议行数2000w, 靠谱吗?
What is the matter that programmers often say "the left hand is knuckled and the right hand is hot"?
RollBack Rx Professional RMC 安装教程
稳压电源: 电路图及类型
【多线程】线程与进程、以及线程进程的调度
Nacos配置中心之事件订阅
flutter 每个要注意的点
Find My技术|智能防丢还得看苹果Find My技术
d实验新异常
DB2数据库-获取表结构异常:[jcc][t4][1065][12306][4.26.14]CharConvertionException ERRORCODE=-4220,SQLSTATE=null
D experimental new anomaly
js基础知识整理之 —— 字符串
Servlet——请求(request)与响应(response)
为了面试阿里,熬夜肝完这份软件测试笔记后,Offer终于到手了
Database auditing - an essential part of network security
合并两个excel表格工具