项目通常会演变为复杂的嵌套目录结构。因此,导入路径可能变得更长且更混乱,这会对代码的外观产生负面影响,并使理解导入代码的来源变得更加困难。
使用路径别名可以通过允许定义与预定义目录相关的导入来解决问题。这种方法不仅解决了理解导入路径的问题,而且还简化了重构期间代码移动的过程。
// 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 包中声明路径别名。这可以通过package.json
文件中的imports
字段来完成。不需要在 npm 上发布包。
在任何目录中创建一个package.json
文件就足够了。因此,这种方法也适用于私人项目。
这是一个有趣的事实:Node.js 早在 2020 年就通过名为“ node.js 中的裸模块说明符解析”的 RFC 引入了对imports
字段的支持。虽然此 RFC 主要用于exports
字段,它允许声明 npm 包的入口点,但exports
和imports
字段处理完全不同的任务,即使它们具有相似的名称和语法。
原生支持路径别名理论上有以下优势:
我尝试在我的项目中配置路径别名,并在实践中测试了这些语句。
例如,让我们考虑一个具有以下目录结构的项目:
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
字段,您可能会遇到一些困难。
如果你打算在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 需要支持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 抛出一个错误,因为我们已经将模块解析配置为使用nodenext
模式。
幸运的是,最近的TypeScript 5.0 版本中添加了一种新的模块解析模式,无需在导入中指定完整路径。要启用此模式,必须在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
字段充分利用路径别名,而不会对如何编写导入路径有任何额外限制。
使用tsc
编译器构建源代码时,可能需要额外的配置。 TypeScript 的一个限制是在使用imports
字段时无法将代码构建为 CommonJS 模块格式。
因此,代码必须编译成ESM格式,并且必须在package.json
中添加type
字段,才能在Node.js中运行编译后的代码。
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": "./src/*" } }
如果您的代码被编译到单独的目录中,例如build/
,则 Node.js 可能找不到该模块,因为路径别名将指向原始位置,例如src/
。为了解决这个问题,可以在package.json
文件中使用条件导入路径。
这允许从build/
目录而不是src/
目录导入已经构建的代码。
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
要使用特定的导入条件,Node.js 应该使用--conditions
标志启动。
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 从 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 中,路径别名在dev
和build
模式下无需额外配置即可工作。
因此,我构建了一个完全空配置的测试项目。
虽然 Vite 内部使用了 Rollup,但是imports
字段并不是开箱即用的。要启用它,您需要安装@rollup/plugin-node-resolve
插件版本 11.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 的版本,必须至少为 v4.2.0。
流行库中的imports
字段目前得到了很好的支持。但是,代码编辑器呢?我在一个使用路径别名的项目中测试了代码导航,特别是“Go to Definition”功能。事实证明,代码编辑器对此功能的支持存在一些问题。
对于 VS Code,TypeScript 的版本至关重要。 TypeScript 语言服务器负责分析和浏览 JavaScript 和 TypeScript 代码。
根据您的设置,VS Code 将使用内置版本的 TypeScript 或安装在您的项目中的版本。
我结合 TypeScript v5.0.4 测试了 VS Code v1.77.3 中的imports
字段支持。
VS Code 存在以下路径别名问题:
在模块解析设置设置为nodenext
或bundler
之前,TypeScript 不使用imports
字段。因此,要在 VS Code 中使用它,需要在项目中指定模块解析。
IntelliSense 目前不支持使用imports
字段建议导入路径。这个问题有一个未解决的问题。
要绕过这两个问题,您可以在tsconfig.json
文件中复制路径别名配置。如果您不使用 TypeScript,您可以在jsconfig.json
中执行相同的操作。
// tsconfig.json OR jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
从 2021.3 版本开始(我在 2022.3.4 测试过),WebStorm支持imports
字段。此功能独立于 TypeScript 版本工作,因为 WebStorm 使用自己的代码分析器。但是,WebStorm 在支持路径别名方面有一组单独的问题:
编辑器严格遵守 Node.js 对使用路径别名的限制。如果未明确指定文件扩展名,代码导航将不起作用。这同样适用于使用index.js
文件导入目录。
WebStorm 有一个错误,阻止在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
字段的实验和经验,我确定了不同类型项目的最佳路径别名配置。
此配置适用于源代码在 Node.js 中运行而无需额外构建步骤的项目。要使用它,请按照下列步骤操作:
在package.json
文件中配置imports
字段。在这种情况下,一个非常基本的配置就足够了。
为了让代码导航在代码编辑器中工作,有必要在jsconfig.json
文件中配置路径别名。
// jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
此配置应用于源代码使用 TypeScript 编写并使用tsc
编译器构建的项目。在此配置中配置以下内容很重要:
package.json
文件中的imports
字段。在这种情况下,需要添加条件路径别名以确保 Node.js 正确解析编译后的代码。
在package.json
文件中启用 ESM 包格式是必要的,因为 TypeScript 在使用imports
字段时只能编译 ESM 格式的代码。
在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。如果不存在,则可以在jsconfig.json
文件中设置所有设置。
此配置的主要功能是它允许您绕过 Node.js 关于在导入中指定文件扩展名的限制。
配置以下内容很重要:
在package.json
文件中配置imports
字段。在这种情况下,您需要为每个别名添加一个路径数组。这将允许打包器在不需要指定文件扩展名的情况下找到导入的模块。
要修复代码编辑器中的代码导航,您需要在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 月),但它也有局限性。
此方法具有以下优点:
package.json
文件)配置路径别名。
但是,随着开发工具的发展,暂时的缺点将被消除:
imports
字段方面也存在问题。为避免这些问题,您可以使用jsconfig.json
文件。但是,这会导致两个文件中的路径别名配置重复。
imports
字段一起使用。例如,Rollup 需要安装额外的插件。
imports
字段会在导入路径上添加新的约束。这些约束与 ES 模块的约束相同,但它们会使开始使用imports
字段变得更加困难。
那么,值得使用imports
字段来配置路径别名吗?我相信对于新项目来说,是的,这种方法值得使用,而不是第三方库。
imports
字段很有可能在未来几年成为许多开发人员配置路径别名的标准方法,因为与传统配置方法相比,它具有显着的优势。
但是,如果您已经有一个配置了路径别名的项目,切换到imports
字段不会带来明显的好处。
我希望您从本文中学到了一些新东西。感谢您的阅读!
也发布在这里