依赖解析

当 Parcel 构建您的源代码时,它会发现依赖项,这允许代码被拆分为单独的文件并在多个地方重用。依赖项描述了在哪里找到您所依赖的代码所在的文件,以及关于如何构建它的元数据。

依赖说明符

#

依赖说明符是一个字符串,描述了相对于导入它的文件的依赖项位置。例如,在 JavaScript 中,import 语句或 require 函数可用于创建依赖项。在 CSS 中,@importurl() 可用于此目的。通常,这些依赖项不会指定完整的绝对路径,而是使用一个较短的说明符,由 Parcel 和其他工具解析为绝对路径。

Parcel 实现了 Node 模块解析算法 的增强版本。它负责将依赖说明符转换为可以从文件系统加载的绝对路径。除了许多工具支持的标准依赖说明符外,Parcel 还支持一些额外的说明符类型和特性。

相对说明符

#

相对说明符以 ... 开头,并相对于导入文件解析文件。

/path/to/project/src/client.js:
import "./utils.js";
import "../constants.js";

在上面的示例中,第一个导入将解析为 /path/to/project/src/utils.js,第二个将解析为 /path/to/project/constants.js

文件扩展名

#

建议在所有导入说明符中包含完整的文件扩展名。这既提高了依赖解析性能,又减少了歧义。

尽管如此,为了与 Node 中的 CommonJS 和 TypeScript 兼容,Parcel 允许对某些文件类型省略文件扩展名。可以省略的文件扩展名包括 .ts.tsx.mjs.js.jsx.cjs.json。导入所有其他文件类型都需要文件扩展名。

下面的示例解析的文件与上面相同。

/path/to/project/src/client.js:
import "./utils";
import "../constants";

请注意,这些仅在从 JavaScript 或 TypeScript 文件导入时可以省略。在 HTML 和 CSS 等其他文件类型中定义的依赖项始终需要文件扩展名。

在 TypeScript 文件中,Parcel 还将尝试将包括 .js.jsx.mjs.cjs 在内的 JavaScript 扩展名替换为其 TypeScript 等效项(.ts.tsx.mts.cts)。例如,对 ./foo.js 的依赖将解析为 ./foo.ts。这与 TSC 的行为相匹配。但是,与 TSC 不同的是,如果原始 ./foo.js 存在,它将被使用而不是 TS 版本,这与 Node 和其他打包器的行为相匹配。

目录索引文件

#

在 JavaScript、TypeScript 和其他基于 JS 的语言中,依赖说明符可以解析为目录而不是文件。如果目录包含 package.json 文件,则主入口将按照包入口部分中描述的方式解析。如果没有 package.json,它将尝试解析目录中的索引文件,如 index.jsindex.ts。上面列出的所有扩展名都支持索引文件。

/path/to/project/src/app.js:
import "./client";

例如,如果 /path/to/project/src/client 是一个目录,上面的说明符可以解析为 /path/to/project/src/client/index.js

裸说明符

#

裸说明符以除 ./~# 之外的任何字符开头。在 JavaScript、TypeScript 和其他基于 JS 的语言中,它们解析为 node_modules 中的包。对于 HTML 和 CSS 等其他类型的文件,裸说明符的处理方式与相对说明符相同。

/path/to/project/src/client/index.js:
import "react";

在上面的示例中,react 可能解析为类似 /path/to/project/node_modules/react/index.js 的路径。确切的位置将取决于 node_modules 目录的位置以及包中的配置。

从导入文件开始,node_modules 目录向上搜索。搜索在项目根目录处停止。例如,如果导入文件位于 /path/to/project/src/client/index.js,则将搜索以下位置:

找到模块目录后,将解析包入口。有关此过程的更多详细信息,请参见包入口

包子路径

#

裸说明符还可以指定包中的子路径。例如,一个包可能发布多个入口点而不仅仅是单个入口点。

import "lodash/clone";

上面的示例按照上面描述的方式在 node_modules 目录中解析 lodash,然后解析包中的 clone 模块,而不是其主入口点。例如,这可能是 node_modules/lodash/clone.js 文件。

内置模块

#

Parcel 包含许多内置 Node.js 模块的垫片,例如 pathurl。当依赖说明符引用这些模块名称之一时,内置模块优先于 node_modules 中安装的同名模块。在为 Node 环境构建时,内置模块从捆绑包中排除,否则将包含垫片。请参见 Node 文档以获取内置模块的完整列表。

在为 Electron 环境构建时,electron 模块也被视为内置模块并从捆绑包中排除。

绝对说明符

#

绝对说明符以 / 开头,并相对于项目根目录解析文件。项目根目录是项目的基本目录,通常包含包管理器锁定文件(例如 yarn.lockpackage-lock.json),或源代码控制目录(例如 .git)。绝对说明符可用于避免深度嵌套层次结构中非常长的相对路径。

import "/src/client.js";

上面的示例可以放置在项目目录结构中的任何文件的任何位置,并且将始终解析为 /path/to/project/src/client.js

波浪号说明符

#

波浪号说明符以 ~ 开头,并相对于导入文件的最近包根目录解析。包根目录是包含 package.json 文件的目录,通常位于 node_modules 中,或作为单一仓库中的包的根目录。波浪号说明符的用途类似于绝对说明符,但在您有多个包时更有用。

/path/to/project/packages/frontend/src/client/index.js:
import "~/src/utils.js";

上面的示例将解析为 /path/to/project/packages/frontend/src/utils.js

哈希说明符

#

哈希说明符以 # 字符开头,其行为取决于它们所在的文件类型。在 JavaScript 和 TypeScript 文件中,哈希说明符被视为内部包导入,下面将进行描述。在其他文件中,这些被视为相对 URL 哈希。

package.json 中的 "imports" 字段可用于定义应用于包内 JavaScript 或 TypeScript 文件的导入说明符的私有映射。这允许包根据环境定义条件导入,如下面和 Node.js 文档中记录的那样。

/path/to/project/package.json:
{
"imports": {
"#dep": {
"node": "dep-node",
"browser": "dep-browser"
}
}
}
/path/to/project/src/index.js:
import "#dep";

查询参数

#

依赖说明符还可以包含查询参数,这些参数为已解析文件指定转换选项。例如,在加载图像时,您可以指定宽度和高度以调整图像大小。

.logo {
background: url(logo.png?width=400&height=400);
}

有关图像的更多详细信息,请参见图像转换器文档。您还可以在自定义转换器插件中使用查询参数。

note:查询参数不支持 CommonJS 说明符(由 require 函数创建)。

URL 方案

#

依赖说明符可以使用 URL 方案来定位命名管道。这些允许您指定与默认管道不同的编译文件管道。例如,bundle-text: 方案可用于将编译后的捆绑包内联为文本。有关更多详细信息,请参见捆绑包内联

有一些保留的 URL 方案不能用于命名管道,并具有内置行为。

通配符说明符

#

Parcel 支持通过通配符一次性导入多个文件,但是,由于通配符导入是非标准的,因此它们不包含在默认的 Parcel 配置中。要启用它们,请在 .parcelrc 中添加 @parcel/resolver-glob

.parcelrc:
{
"extends": "@parcel/config-default",
"resolvers": ["@parcel/resolver-glob", "..."]
}

启用后,您可以使用 ./files/*.js 之类的说明符导入多个文件。这将返回一个对象,其键对应于文件名。

import * as files from "./files/*.js";

等效于:

import * as foo from "./files/foo.js";
import * as bar from "./files/bar.js";

let files = {
foo,
bar,
};

具体来说,通配符模式的动态部分成为对象的键。如果有多个动态部分,将返回一个嵌套对象。例如,如果存在 pages/profile/index.js 文件,以下将匹配它。

import * as pages from "./pages/*/*.js";

console.log(pages.profile.index);

这也适用于 bundle-text: 等 URL 方案,以及动态导入。使用动态导入时,结果对象将包含文件名到函数的映射。每个函数可以被调用以加载已解析的模块。这意味着每个文件是按需加载,而不是一次性全部加载。

let files = import("./files/*.js");

async function doSomething() {
let foo = await files.foo();
let bar = await files.bar();
return foo + bar;
}

Globs 也可以用于从 npm 包导入文件:

import * as locales from "@company/pkg/i18n/*.js";

console.log(locales.en.message);

Glob 导入也适用于 CSS:

@import "./components/*.css";

等效于:

@import "./components/button.css";
@import "./components/dropdown.css";

包入口

#

在解析包目录时,会查阅 package.json 文件以确定包的入口。Parcel 按以下顺序检查以下字段:

如果这些字段都未设置,或它们指向的文件不存在,则解析将回退到索引文件。有关更多详细信息,请参见目录索引文件

包导出

#

package.json 中的 "exports" 字段可用于定义包的公共可访问入口点。这些还可以根据环境定义条件行为,允许根据模块的导入位置(例如 node 或浏览器)更改解析。

启用包导出

#

默认情况下,包导出是禁用的,因为它们可能会破坏未考虑这一点的现有项目。您可以通过在项目根目录的 package.json 文件中添加以下内容来启用支持:

{
"@parcel/resolver-default": {
"packageExports": true
}
}

单一导出

#

如果一个包只有单个导出模块,"exports" 字段可以定义为字符串:

{
"name": "foo",
"exports": "./dist/index.js"
}

在上面的示例中,当用户导入 "foo" 包时,将解析 node_modules/foo/dist/index.js 模块。

多重导出

#

如果一个包导出多个模块,"exports" 字段可以提供映射,定义在哪里找到这些导出。"." 导出定义主入口点,其他条目定义为子路径。

{
"name": "foo",
"exports": {
".": "./dist/index.js",
"./bar": "./dist/bar.js"
}
}

使用上述包,用户可以导入 "foo",它解析为 node_modules/foo/dist/index.js,或 "foo/bar",它解析为 node_modules/foo/dist/bar.js

对于包含 "exports" 字段的任何包,未明确导出的任何子路径都将对使用者不可访问。例如,尝试在上述包中导入 "foo/abc" 将导致构建时错误。

* 字符可以在导出映射中用作通配符。在映射的左侧只能出现一个 *,匹配的字符串将替换右侧的每个实例。这允许您避免手动列出要导出的每个文件。

{
"name": "foo",
"exports": {
"./*": "./dist/*.js"
}
}

在上面的示例中,dist 目录中的所有 .js 文件都从包中导出,不带其扩展名。例如,导入 "foo/bar" 将解析为 node_modules/foo/dist/bar.js

条件导出

#

"exports" 字段还可以为同一说明符在不同环境或其他条件下定义不同的映射。例如,一个包可以为 ES 模块和 CommonJS,或为 Node 和浏览器提供不同的入口点。

{
"name": "foo",
"exports": {
"node": "./dist/node.js",
"default": "./dist/default.js"
}
}

在上面的示例中,如果使用者从 Node 环境导入 "foo" 模块,它将解析为 node_modules/foo/dist/node.js,否则将解析为 node_modules/foo/default.js

条件导出还可以嵌套在子路径映射中。

{
"name": "foo",
"exports": {
"./bar": {
"node": "./dist/bar-node.js",
"default": "./dist/bar-default.js"
}
}
}

这允许导入 "foo/bar" 在 Node 和其他环境中解析为不同的文件。

条件导出还可以相互嵌套以创建更复杂的逻辑。

{
"name": "foo",
"exports": {
"node": {
"import": "./dist/node.mjs",
"require": "./dist/node.cjs"
},
"default": "./dist/default.js"
}
}

上面的示例定义了模块的不同版本,具体取决于在 Node 环境中是通过 ESM import 还是 CommonJS require 加载包。

Parcel 支持以下导出条件:

导出条件的解析顺序遵循它们在 package.json 中定义的顺序,而不是上面列出的顺序。

更多示例

#

有关 package.json 导出的更多详细信息,请参见 Node.js 文档

别名

#

别名可用于覆盖依赖项的正常解析。例如,您可能希望用不同但 API 兼容的替代品覆盖模块,或将依赖项映射到由从 CDN 加载的库定义的全局变量。

别名通过 package.json 中的 alias 字段定义。它们可以在包含依赖项的源文件最近的 package.json 中本地定义,也可以在项目根目录的 package.json 中全局定义。全局别名适用于项目中的所有文件和包,包括 node_modules 中的包。

包别名

#

包别名将 node_modules 依赖项映射到不同的包或项目中的本地文件。例如,要在项目中的所有文件以及 node_modules 中的任何其他库中将 reactreact-dom 替换为 Preact,您可以在项目根目录的 package.json 中定义全局别名。

{
"alias": {
"react": "preact/compat",
"react-dom": "preact/compat"
}
}

您还可以通过使用定义别名的 package.json 中的相对路径将模块映射到项目中的文件。

{
"alias": {
"react": "./my-react.js"
}
}

还支持仅别名模块的某些子路径。此示例将 lodash/clone 别名为 tiny-clonelodash 包中的其他导入将不受影响。

{
"alias": {
"lodash/clone": "tiny-clone"
}
}

反过来也是如此:如果整个模块被别名,则该包的任何子路径导入都将在别名模块中解析。例如,如果您将 lodash 别名为 my-lodash 并导入 lodash/clone,这将解析为 my-lodash/clone

文件别名

#

别名还可以定义为相对路径,以将包中的特定文件替换为其他文件。这可以通过使用 alias 字段无条件替换文件,或使用 sourcebrowser 字段有条件地替换。有关这些字段的详细信息,请参见上面的包入口

例如,要用特定于浏览器的版本替换某个文件,您可以使用 browser 字段。

{
"browser": {
"./fs.js": "./fs-browser.js"
}
}

现在,如果在浏览器环境中导入 my-module/fs.js,他们实际上会得到 my-module/fs-browser.js。这适用于外部导入(例如包子路径),以及模块内部。

通配符别名

#

文件别名还可以使用通配符定义,这允许使用单个模式替换多个文件。替换可以包含 $1 等模式以访问捕获的通配符匹配。这可以通过使用 alias 字段无条件替换文件,或使用 sourcebrowser 字段有条件地替换来完成。有关这些字段的详细信息,请参见上面的包入口

例如,您可以使用 source 字段提供包中编译代码和原始源代码之间的映射。当模块被符号链接或位于单一仓库中时,这将允许 Parcel 从源代码编译模块,而不是使用预编译版本。

{
"source": {
"./dist/*": "./src/$1"
}
}

现在,每当导入 dist 目录中的文件时,都会加载 src 文件夹中的相应文件。

垫片别名

#

文件或包可以别名为 false 以从构建中排除,并替换为空模块。例如,这可用于从浏览器构建中排除仅在 Node.js 中工作的某些模块。

{
"alias": {
"fs": false
}
}

全局别名

#

文件或包还可以别名为在运行时定义的全局变量。例如,特定库可能从 CDN 加载。与其捆绑它,不如在每次解析该库的依赖项时,将其替换为对该全局变量的引用,而不是捆绑。

这可以通过创建一个带有 global 属性的对象的别名来完成。以下示例将 jquery 依赖说明符别名为全局变量 $

{
"alias": {
"jquery": {
"global": "$"
}
}
}

TSConfig

#

Parcel 支持 TypeScript 的 tsconfig.json 配置文件中定义的一些设置,包括 baseUrlpathsmoduleSuffixes。Parcel 从包含依赖项的文件向上搜索以找到最近的 tsconfig.json 文件。它还支持使用 extends 选项将多个 tsconfig 的设置合并在一起。有关更多详细信息,请参见 TypeScript 文档

baseUrl

#

baseUrl 字段定义了解析裸说明符的基本目录。

{
"compilerOptions": {
"baseUrl": "./src"
}
}

在上面的示例中,如果存在,Home 将解析为 src/Home.js。否则,它将回退到 node_modules/Home

paths

#

paths 字段可用于定义从裸说明符到文件路径的映射。您还可以使用 * 字符定义通配符模式。

paths 字段中引用的文件路径相对于 baseUrl(如果已定义),否则相对于包含 tsconfig.json 文件的目录。

{
"compilerOptions": {
"paths": {
"jquery": ["./vendor/jquery/dist/jquery"],
"app/*": ["./src/app/*"]
}
}
}

在上面的示例中,jquery 解析为 ./vendor/jquery/dist/jquery.jsapp/foo 解析为 ./src/app/foo.js

moduleSuffixes

#

moduleSuffixes 字段允许您指定解析模块时要搜索的文件名后缀。

{
"compilerOptions": {
"moduleSuffixes": [".ios", ".native", ""]
}
}

在上面的示例中,Parcel 将查找 ./foo.ios.ts./foo.native.ts./foo.ts(除了其他扩展名如 .tsx.js 等)。

性能考虑

#

缓存

#

Parcel 缓存依赖解析结果以提高性能。这意味着后续构建将更快,尤其是在大型项目中。

并行解析

#

Parcel 使用并行算法解析依赖项,这显著提高了大型项目的构建速度。

常见问题与陷阱

#

循环依赖

#

虽然 Parcel 支持循环依赖,但仍建议尽量避免,因为它们可能导致意外的行为和性能问题。

大小写敏感性

#

在某些操作系统(如 macOS 和 Linux)上,文件路径是大小写敏感的。始终使用正确的文件名大小写以避免跨平台问题。

结论

#

Parcel 的依赖解析系统提供了灵活且强大的方式来管理项目依赖。通过理解其工作原理,您可以更有效地组织和构建代码。

包别名

#

您可以使用 alias 字段为整个包创建别名。这允许您将一个包替换为另一个包。

{
"alias": {
"old-package": "new-package"
}
}

在上面的示例中,对 old-package 的所有导入都将解析为 new-package

文件别名

#

您还可以为特定文件创建别名,这对于替换单个模块非常有用。

{
"alias": {
"old-package/file.js": "new-package/file.js"
}
}

通配符别名

#

通配符可用于创建更复杂的别名映射。

{
"alias": {
"old-package/*": "new-package/dist/*"
}
}

这将把 old-package 的所有子模块映射到 new-package/dist 目录中的相应模块。

绝对路径别名

#

别名还可以映射到绝对路径,这对于将依赖项映射到本地文件非常有用。

{
"alias": {
"package": "/absolute/path/to/module.js"
}
}

全局变量别名

#

您可以将模块别名映射到全局变量,这对于使用从 CDN 加载的库很有用。

{
"alias": {
"jquery": "window.jQuery"
}
}

条件别名

#

别名还可以根据构建目标或环境进行条件定义。

{
"alias": {
"package": {
"browser": "package-browser",
"node": "package-node"
}
}
}

结论

#

Parcel 的依赖解析和别名系统提供了极大的灵活性,使您能够精确控制如何解析和替换依赖项。通过理解这些机制,您可以更有效地管理项目依赖关系,并轻松适应不同的开发场景。