如何在前端项目中以原生方式配置路径别名
我们将深入了解该imports字段package.json及其在路径别名配置中的应用。此外,我们还将探讨常用开发工具如何支持此字段,并确定各种使用场景下的最佳配置。
内容
- 关于路径别名
- 进口领域
- 配置路径别名
- Node.js 的局限性
- TypeScript 对子路径导入的支持
- 代码打包器对子路径导入的支持
- 测试运行器中对子路径导入的支持
- 代码编辑器对子路径导入的支持
- 推荐配置
- 结论
- 实用链接
关于路径别名
项目通常会发展成复杂的嵌套目录结构。因此,导入路径可能会变得更长、更混乱,这不仅会影响代码的美观,还会使人难以理解导入代码的来源。
使用路径别名可以解决这个问题,它允许定义相对于预定义目录的导入语句。这种方法不仅解决了理解导入路径的问题,还简化了重构过程中代码的移动。
// Without Aliases
import { apiClient } from '../../../../shared/api';
import { ProductView } from '../../../../entities/product/components/ProductView';
import { addProductToCart } from '../../../add-to-cart/actions';
// With Aliases
import { apiClient } from '#shared/api';
import { ProductView } from '#entities/product/components/ProductView';
import { addProductToCart } from '#features/add-to-cart/actions';
Node.js 中有很多库可用于配置路径别名,例如alias-hq和tsconfig-paths。然而,在查阅 Node.js 文档时,我发现了一种无需依赖第三方库即可配置路径别名的方法。此外,这种方法无需构建步骤即可使用别名。
本文将探讨Node.js 子路径导入以及如何使用它来配置路径别名。我们还将探讨它们在前端生态系统中的支持情况。
进口领域
从 Node.js v12.19.0 开始,开发者可以使用子路径导入 (Subpath Imports)在 npm 包中声明路径别名。这可以通过文件imports中的相应字段来实现package.json。无需将包发布到 npm,package.json只需在任何目录中创建一个文件即可。因此,这种方法也适用于私有项目。
💡 这里有个有趣的事实:Node.js
imports早在 2020 年就通过名为“ Node.js 中的裸模块规范解析”的 RFC 引入了对该字段的支持。虽然该 RFC 主要针对的exports是允许声明 npm 包入口点的字段,但 `<module>`exports和 `imports<module>` 字段的功能完全不同,即使它们的名称和语法相似。
理论上,原生支持路径别名具有以下优点:
- 无需安装任何第三方库。
- 运行代码时无需预先构建或动态处理导入。
- 任何使用标准导入解析机制的基于 Node.js 的工具都支持别名。
- 代码导航和自动完成功能应该在代码编辑器中正常工作,无需任何额外设置。
我尝试在我的项目中配置路径别名,并在实践中测试了这些语句。
配置路径别名
例如,我们考虑一个具有以下目录结构的项目:
my-awesome-project
├── src/
│ ├── entities/
│ │ └── product/
│ │ └── components/
│ │ └── ProductView.js
│ ├── features/
│ │ └── add-to-cart/
│ │ └── actions/
│ │ └── index.js
│ └── shared/
│ └── api/
│ └── index.js
└── package.json
要配置路径别名,您可以按照文档中的说明添加几行代码package.json。例如,如果您想允许相对于src目录的导入,请将以下imports字段添加到package.json:
{
"name": "my-awesome-project",
"imports": {
"#*": "./src/*"
}
}
要使用已配置的别名,导入语句可以这样写:
import { apiClient } from '#shared/api';
import { ProductView } from '#entities/product/components/ProductView';
import { addProductToCart } from '#features/add-to-cart/actions';
从设置阶段开始,我们就面临第一个限制:字段中的条目imports必须以#符号开头。这确保它们与包说明符(例如)区分开来@。我认为这个限制很有用,因为它允许开发人员快速确定导入中何时使用了路径别名,以及别名配置的位置。
要为常用模块添加更多路径别名,imports可以按如下方式修改该字段:
{
"name": "my-awesome-project",
"imports": {
"#modules/*": "./path/to/modules/*",
"#logger": "./src/shared/lib/logger.js",
"#*": "./src/*"
}
}
文章结尾如果以“其他一切都能开箱即用”这句话来结尾就非常理想了。然而,实际上,如果你打算使用这个imports字段,可能会遇到一些困难。
Node.js 的局限性
如果您计划将路径别名与CommonJS 模块一起使用,那么我有个坏消息要告诉您:以下代码将无法正常工作。
const { apiClient } = require('#shared/api');
const { ProductView } = require('#entities/product/components/ProductView');
const { addProductToCart } = require('#features/add-to-cart/actions');
在 Node.js 中使用路径别名时,必须遵循 ESM 世界的模块解析规则。这适用于 ES 模块和 CommonJS 模块,并由此产生了两个必须满足的新要求:
- 必须指定文件的完整路径,包括文件扩展名。
- 不允许指定目录路径并期望导入文件。必须指定文件
index.js的完整路径。index.js
为了使 Node.js 能够正确解析模块,应按如下方式更正导入语句:
const { apiClient } = require('#shared/api/index.js');
const { ProductView } = require('#entities/product/components/ProductView.js');
const { addProductToCart } = require('#features/add-to-cart/actions/index.js');
在包含大量 CommonJS 模块的项目中配置字段时,这些限制可能会导致问题imports。但是,如果您已经在使用 ES 模块,那么您的代码就满足所有要求。此外,如果您使用打包工具构建代码,则可以绕过这些限制。我们将在下文中讨论如何做到这一点。
TypeScript 对子路径导入的支持
为了正确解析导入的模块以进行类型检查,TypeScript 需要支持该imports字段。此功能从 4.8.1 版本开始支持,但前提是必须满足上述 Node.js 的限制。
要使用该imports字段进行模块解析,需要在文件中配置一些选项tsconfig.json。
{
"compilerOptions": {
/* Specify what module code is generated. */
"module": "esnext",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "nodenext"
}
}
此配置使该imports字段的功能与在 Node.js 中相同。这意味着,如果您忘记在模块导入中包含文件扩展名,TypeScript 将生成错误警告。
// OK
import { apiClient } from '#shared/api/index.js';
// Error: Cannot find module '#src/shared/api/index' or its corresponding type declarations.
import { apiClient } from '#shared/api/index';
// Error: Cannot find module '#src/shared/api' or its corresponding type declarations.
import { apiClient } from '#shared/api';
// Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'?
import { foo } from './relative';
我不想重写所有的导入语句,因为我的大多数项目都使用打包工具来构建代码,而且我在导入模块时从不添加文件扩展名。为了绕过这个限制,我找到了一种方法,将项目配置如下:
{
"name": "my-awesome-project",
"imports": {
"#*": [
"./src/*",
"./src/*.ts",
"./src/*.tsx",
"./src/*.js",
"./src/*.jsx",
"./src/*/index.ts",
"./src/*/index.tsx",
"./src/*/index.js",
"./src/*/index.jsx"
]
}
}
这种配置允许以通常的方式导入模块,而无需指定扩展名。即使导入路径指向目录,这种方法也有效。
// OK
import { apiClient } from '#shared/api/index.js';
// OK
import { apiClient } from '#shared/api/index';
// OK
import { apiClient } from '#shared/api';
// Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'?
import { foo } from './relative';
我们还有一个关于使用相对路径导入的问题。这个问题与路径别名无关。TypeScript 会抛出错误,因为我们将模块解析配置为使用相对路径模式。幸运的是,最新的TypeScript 5.0 版本nodenext新增了一种模块解析模式,无需在导入语句中指定完整路径。要启用此模式,需要在配置文件中配置一些选项。tsconfig.json
{
"compilerOptions": {
/* Specify what module code is generated. */
"module": "esnext",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "bundler"
}
}
设置完成后,相对路径的导入功能将照常运行。
// OK
import { apiClient } from '#shared/api/index.js';
// OK
import { apiClient } from '#shared/api/index';
// OK
import { apiClient } from '#shared/api';
// OK
import { foo } from './relative';
现在,我们可以通过该字段充分利用路径别名,imports而无需对如何编写导入路径施加任何其他限制。
使用 TypeScript 编写代码
使用tsc编译器构建源代码时,可能需要进行额外的配置。TypeScript 的一个限制是,使用该imports字段时,代码无法构建为 CommonJS 模块格式。因此,代码必须编译为 ESM 格式,并且type必须将该字段添加到package.jsonNode.js 中才能运行编译后的代码。
{
"name": "my-awesome-project",
"type": "module",
"imports": {
"#*": "./src/*"
}
}
如果你的代码被编译到一个单独的目录(例如 `/usr/bin`)中build/,Node.js 可能找不到该模块,因为路径别名会指向原始位置(例如 `/usr/bin`)src/。为了解决这个问题,可以在 `.js` 文件中使用条件导入路径。这样就可以从 ` /usr/bin` 目录而不是 `/ usr/bin` 目录package.json导入已编译的代码。build/src/
{
"name": "my-awesome-project",
"type": "module",
"imports": {
"#*": {
"default": "./src/*",
"production": "./build/*"
}
}
}
要使用特定的导入条件,应该使用该--conditions标志启动 Node.js。
node --conditions=production build/index.js
代码打包器对子路径导入的支持
代码打包工具通常使用它们自己的模块解析实现,而不是 Node.js 内置的模块解析。因此,对它们来说,实现对该字段的支持至关重要imports。我已经在我的项目中使用 Webpack、Rollup 和 Vite 测试了路径别名,现在可以分享我的测试结果。
这是我用来测试打包工具的路径别名配置。我使用了与 TypeScript 相同的技巧,避免在导入语句中指定文件的完整路径。
{
"name": "my-awesome-project",
"type": "module",
"imports": {
"#*": [
"./src/*",
"./src/*.ts",
"./src/*.tsx",
"./src/*.js",
"./src/*.jsx",
"./src/*/index.ts",
"./src/*/index.tsx",
"./src/*/index.js",
"./src/*/index.jsx"
]
}
}
Webpack
Webpack从 v5.0 版本开始支持该imports字段。路径别名无需任何额外配置即可正常工作。以下是我用于构建 TypeScript 测试项目的 Webpack 配置:
const config = {
mode: 'development',
devtool: false,
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-typescript'],
},
},
},
],
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
};
export default config;
维特
Vite 4.2.0 版本新增了对该imports字段的支持。然而,4.3.3 版本修复了一个重要的 bug,因此建议至少使用此版本。在 Vite 中,路径别名在两种模式下均无需额外配置即可正常工作。因此,我创建了一个配置完全为空的测试项目。devbuild
Rollup
虽然 Vite 内部使用了 Rollup,但该imports字段并非开箱即用。要启用它,您需要安装@rollup/plugin-node-resolve11.1.0 或更高版本的插件。以下是一个配置示例:
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { babel } from '@rollup/plugin-babel';
export default [
{
input: 'src/index.ts',
output: {
name: 'mylib',
file: 'build.js',
format: 'es',
},
plugins: [
nodeResolve({
extensions: ['.ts', '.tsx', '.js', '.jsx'],
}),
babel({
presets: ['@babel/preset-typescript'],
extensions: ['.ts', '.tsx', '.js', '.jsx'],
}),
],
},
];
遗憾的是,在这种配置下,路径别名仅在 Node.js 的限制范围内有效。这意味着您必须指定完整的文件路径,包括文件扩展名。在字段中指定数组imports并不能绕过此限制,因为 Rollup 只会使用数组中的第一个路径。
我认为可以使用 Rollup 插件解决这个问题,但我没有尝试过,因为我主要用 Rollup 开发小型库。对我来说,重写整个项目的导入路径更容易些。
测试运行器中对子路径导入的支持
测试运行器是另一类严重依赖模块解析机制的开发工具。它们通常使用自己的模块解析实现,类似于代码打包器。因此,该imports字段可能无法按预期工作。
幸运的是,我测试过的工具都运行良好。我分别使用 Jest v29.5.0 和 Vite v0.30.1 测试了路径别名。在这两种情况下,路径别名都能无缝运行,无需任何额外设置或限制。Jest 自 v29.4.0 版本起就支持该imports字段。而 Vitest 的支持程度完全取决于 Vite 的版本,Vite 的版本必须至少为 v4.2.0。
代码编辑器对子路径导入的支持
imports目前,常用库对这一功能的支持相当完善。但是,代码编辑器的情况如何呢?我在一个使用了路径别名的项目中测试了代码导航,特别是“跳转到定义”功能。结果发现,代码编辑器对这一功能的支持存在一些问题。
VS Code
对于 VS Code 而言,TypeScript 的版本至关重要。TypeScript 语言服务器负责分析和处理 JavaScript 和 TypeScript 代码。根据您的设置,VS Code 将使用内置的 TypeScript 版本或项目中安装的版本。我测试了importsVS Code v1.77.3 与 TypeScript v5.0.4 结合使用时的字段支持情况。
VS Code 在路径别名方面存在以下问题:
importsTypeScript只有在模块解析设置设为 `true`nodenext或 `false`时才会使用该字段bundler。因此,要在 VS Code 中使用它,您需要在项目中指定模块解析。- IntelliSense 目前不支持使用该字段建议导入路径。此问题
imports已提交待解决的 issue 。
为了绕过这两个问题,你可以在文件中复制路径别名配置tsconfig.json。如果你不使用 TypeScript,也可以在其他地方执行相同的操作jsconfig.json。
// tsconfig.json OR jsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"#*": ["./src/*"]
}
}
}
// package.json
{
"name": "my-awesome-project",
"imports": {
"#*": "./src/*"
}
}
WebStorm
自 2021.3 版本起(我测试的是 2022.3.4 版本),WebStorm支持该imports字段。此功能与 TypeScript 版本无关,因为 WebStorm 使用的是自己的代码分析器。但是,WebStorm 在支持路径别名方面存在一些其他问题:
- 编辑器严格遵守 Node.js 对路径别名使用的限制。如果未显式指定文件扩展名,代码导航将无法正常工作。导入目录时也存在同样的问题
index.js。 - WebStorm 存在一个 bug,会导致无法在字段中使用路径数组
imports。在这种情况下,代码导航将完全停止工作。
{
"name": "my-awesome-project",
// OK
"imports": {
"#*": "./src/*"
},
// This breaks code navigation
"imports": {
"#*": ["./src/*", "./src/*.ts", "./src/*.tsx"]
}
}
幸运的是,我们可以使用与解决 VS Code 中所有问题相同的技巧。具体来说,我们可以在配置tsconfig.json文件中复制路径别名配置jsconfig.json。这样就可以不受限制地使用路径别名了。
推荐配置
根据我imports在各种项目中使用该字段的实验和经验,我已经确定了不同类型项目的最佳路径别名配置。
如果没有 TypeScript 或 Bundler
此配置适用于源代码在 Node.js 中运行且无需额外构建步骤的项目。要使用此配置,请按照以下步骤操作:
imports在文件中配置该字段package.json。本例中只需进行非常基本的配置即可。- 为了使代码编辑器中的代码导航功能正常工作,需要在
jsconfig.json文件中配置路径别名。
// jsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"#*": ["./src/*"]
}
}
}
// package.json
{
"name": "my-awesome-project",
"imports": {
"#*": "./src/*"
}
}
使用 TypeScript 构建代码
此配置适用于源代码使用 TypeScript 编写并使用tsc编译器构建的项目。在此配置中,务必配置以下内容:
imports文件中的字段。package.json在这种情况下,需要添加条件路径别名,以确保 Node.js 能够正确解析已编译的代码。- 在文件中启用 ESM 包格式
package.json是必要的,因为 TypeScript 只有在使用该字段时才能编译 ESM 格式的代码imports。 - 在
tsconfig.json文件中设置 ESM 模块格式moduleResolution。这将允许 TypeScript 在导入时提示遗漏的文件扩展名。如果未指定文件扩展名,代码在编译后将无法在 Node.js 中运行。 - 要修复代码编辑器中的代码导航问题,必须在文件中重复路径别名
tsconfig.json。
// tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "nodenext",
"baseUrl": "./",
"paths": {
"#*": ["./src/*"]
},
"outDir": "./build"
}
}
// package.json
{
"name": "my-awesome-project",
"type": "module",
"imports": {
"#*": {
"default": "./src/*",
"production": "./build/*"
}
}
}
使用捆绑器构建代码
此配置适用于源代码已打包的项目。在这种情况下,无需使用 TypeScript。如果未使用 TypeScript,所有设置都可以在一个jsconfig.json文件中完成。此配置的主要特点是允许您绕过 Node.js 在导入时指定文件扩展名的限制。
请务必配置以下内容:
imports在文件中配置该字段package.json。在这种情况下,您需要为每个别名添加一个路径数组。这样,打包工具无需指定文件扩展名即可找到导入的模块。- 要修复代码编辑器中的代码导航问题,需要在 a
tsconfig.json或jsconfig.json文件中重复路径别名。
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"#*": ["./src/*"]
}
}
}
// package.json
{
"name": "my-awesome-project",
"imports": {
"#*": [
"./src/*",
"./src/*.ts",
"./src/*.tsx",
"./src/*.js",
"./src/*.jsx",
"./src/*/index.ts",
"./src/*/index.tsx",
"./src/*/index.js",
"./src/*/index.jsx"
]
}
}
结论
imports与通过第三方库配置路径别名相比,直接在字段中配置路径别名有利有弊。虽然这种方法已被常用开发工具支持(截至 2023 年 4 月),但它也存在一些局限性。
这种方法具有以下优点:
-
无需“即时”编译或转译代码即可使用路径别名。
-
大多数流行的开发工具都支持路径别名,无需任何额外配置。这一点已在 Webpack、Vite、Jest 和 Vitest 中得到验证。
-
package.json这种方法提倡在一个可预测的位置(文件)配置路径别名。 -
配置路径别名不需要安装第三方库。
然而,目前存在一些暂时的缺点,但随着开发工具的不断发展,这些缺点将会消除:
-
即使是常用的代码编辑器在支持该
imports字段时也存在问题。为了避免这些问题,您可以使用该jsconfig.json文件。但是,这会导致路径别名配置在两个文件中重复。 -
某些开发工具可能无法
imports直接与该字段兼容。例如,Rollup 需要安装额外的插件。 -
在 Node.js 中使用该
imports字段会给导入路径添加新的约束。这些约束与 ES 模块的约束相同,但可能会使该imports字段的使用变得更加困难。 -
Node.js 的约束可能会导致 Node.js 与其他开发工具在实现上存在差异。例如,代码打包工具可能会忽略 Node.js 的约束。这些差异有时会使配置变得复杂,尤其是在设置 TypeScript 时。
那么,使用字段来配置路径别名是否值得呢imports?我认为对于新项目来说,是的,这种方法比使用第三方库更值得。
在未来几年,该imports字段很有可能成为许多开发人员配置路径别名的标准方法,因为它与传统的配置方法相比具有显著优势。
但是,如果您已经有一个配置了路径别名的项目,切换到该imports字段不会带来显著的好处。
希望这篇文章对您有所帮助,您能从中有所收获。感谢阅读!