默认导出
对于开发一个 JavaScript 三方库供外部使用而言,package.json
是其中不可缺少的一部分
一般而言,对于库开发者来说,我们会在package.json
中指定我们的导出入口。一般而言会涉及两个字段main
和export
,它们会涉及到当前模块在被导入的行为。通常我们会将main
字段指向 cjs 产物,module
字段指向 ES 产物
main
main
字段指定了该模块的主入口文件,即 require 该模块时加载的文件。该字段的值应为相对于模块根目录的路径或者是一个模块名(如index.js
或lib/mymodule.js
,如果是模块名,则需要保证在该模块根目录下存在该模块)。主入口文件可以是 JavaScript 代码、JSON 数据或者是 Node.js C++扩展
module
module
字段是 ES 模块规范下的入口文件,它被用于支持 import 语法。当使用 esm 或 webpack 等工具打包时,会优先采用 module 字段指定的入口文件。如果没有指定 module 字段,则会使用 main 字段指定的入口文件作为默认的 ES 模块入口文件
指定导出
一般情况下,我们使用main
和module
在大部分场景下对于开发一个库来说已经足够。但是如果想实现更精细化的导出控制就无法满足
当我们一个库本身同时包含运行时和编译时的导出时,如果我们导出的模块在编译时(node 环境)包含副作用,如果运行时模块也从同一入口导出就会出现问题
// 例如编译时入口存在以下编译时副作用
// buildtime.ts
console.log(process.env.xxx)
export const buildLog = () => console.log("build time")
// runtime.ts
export const runLog = () => console.log("run time")
// index.ts
export * from "./buildtime.ts"
export * from "./runtime.ts"
当前,可以通过解决掉副作用规避这个问题,但是很可能我们依赖的第三方模块也是有复作用的这个时候就无解了。此时最好的办法是将这个库的运行时和编译时从两个入口进行导出,这样子就不存在某一方影响到另一方。库使用者也不需关心从统一入口导入的方法到底是编译时方法还是运行时方法
这个时候就可以利用package.json
的exports
字段进行导出,当存在该字段时会忽略main
和module
字段。该字段在 Node.js 12 版本中引入,可用来大幅简化模块的导出方式,支持同时支持多个环境下的导出方式,提供了更好的可读性和可维护性
支持以下用法
- 多文件导出
"name": "pkg",
"exports": {
".": "./dist/index.js",
"./runtime": "./dist/runtime.js",
"./buildtime": "./dist/buildtime.js"
}
这样当运行require('pkg')
时会加载dist/index.js
,而当运行 require('pkg/runtime')
时会加载dist/runtime.js
,require('pkg/buildtime')
则会加载 dist/buildtime.js
- 多条件导出
{
"name": "pkg",
"version": "1.0.0",
"main": "dist/index.js",
"exports": {
".": {
"require": "./dist/index.cjs",
"import": "./dist/index.mjs",
"node": "./dist/index.cjs",
"default": "./dist/index.js"
},
"./runtime": {
"require": "./dist/runtime.cjs",
"import": "./dist/runtime.mjs",
"node": "./dist/runtime.cjs",
"default": "./dist/runtime.js"
},
"./buildtime": {
"require": "./dist/buildtime.cjs",
"import": "./dist/buildtime.mjs",
"node": "./dist/buildtime.cjs",
"default": "./dist/buildtime.js"
}
}
}
对于条件,目前 node 支持import
、require
、 node
、node-addons
和default
。同时社区对于其它环境也定义了如types
、deno
、browser
等供不同环境使用。具体规范可见
- 目录导出
支持目录的整体导出
{
"exports": {
"./lib/*": "./lib/*.js"
}
}
类型
按照上述操作完成后,打包就能符合相关预期,但是对于 typescript 文件的导入如果使用runtime
路径是会找不到相应的类型文件,typescript 并不会去识别该字段,已有的讨论issues
此时需要借助package.json
的typeVersions
字段进行声明供 ts 识别
对于这个例子,我们在库的package.json
中增加如下,表示各路径分别导出的类型文件路径
"typesVersions": {
"*": {
".": ["./dist/index.d.ts"],
"runtime": ["dist/runtime.d.ts"],
"buildtime": ["dist/dist/runtime.d.ts"]
}
},
此时我们就能看见能正确找到相应的类型提示
实现
目前 Node.js 12+和主流的打包工具都已经支持exports
字段的解析,下面来简单看下webpack的实现
Webpack
webpack已经内置支持对于exports
的解析,它的解析由enhance-resolve
实现
createResolver
是enhance-resolve
导出的create
函数,用法如下
// https://github.com/webpack/enhanced-resolve/blob/main/README.md
const fs = require("fs");
const { CachedInputFileSystem, ResolverFactory } = require("enhanced-resolve");
// create a resolver
const myResolver = ResolverFactory.createResolver({
// Typical usage will consume the `fs` + `CachedInputFileSystem`, which wraps Node.js `fs` to add caching.
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".js", ".json"]
/* any other resolver options here. Options/defaults can be seen below */
});
// resolve a file with the new resolver
const context = {};
const lookupStartPath = "/Users/webpack/some/root/dir";
const request = "./path/to-look-up.js";
const resolveContext = {};
myResolver.resolve(context, lookupStartPath, request, resolveContext, (
err /*Error*/,
filepath /*string*/
) => {
// Do something with the path
});
通过创建一个自定义resolver函数后可调用resolve
函数根据当前的模块路径和一些配置查找一个模块的绝对路径
相关自定义resolver选项含义
extensions
查找的文件扩展名conditionNames
对应package.json
中的exports
条件exportsFields
指定从package.json哪个字段读取exports
条件fullySpecified
为 true 时,解析器会优先尝试使用完全指定的路径来解析模块请求,而忽略其他任何条件。如果找到了对应的模块文件,则直接返回该路径;否则抛出错误
通过相关上述代码我们可以知道
- 对于解析
es
导入,webpack会尝试读取exports
字段的导出,依次读取import
和node
字段。并且这里也是直接配置了fullySpecified
。即处理相对路径的导入如import foo from './foo';
时,Webpack在解析模块请求时会直接将 ./foo.js 当作完整路径来处理,而不进行路径的拼接和解析 - 对于解析
cjs
导入,webpack会尝试读取exports
字段的导出,依次读取require
和node
字段。并且会尝试使用各种解析策略来解析该路径
由于enhance-resolve
是一个完全独立于webpack的模块,当我们自己实现一个三方打包器或者插件时,如果想实现类似的模块解析能力,也可以完全独立使用enhance-resolve
来实现
总结
为了实现一个库更友好的导出,我们可以借助 package.json 的exports
字段指定多条件的导出方式,主流打包工具以及 Node.js 都已经支持;对于 ts 类型,我们可以结合typeVersions
进行配置