paint-brush
如何以原生方式在前端项目中配置路径别名经过@nodge
10,905 讀數
10,905 讀數

如何以原生方式在前端项目中配置路径别名

经过 Maksim Zemskov20m2023/05/04
Read on Terminal Reader

太長; 讀書

imports 字段很有可能在未来几年成为许多开发人员配置路径别名的标准方式。与传统配置方法相比,它具有显着优势,并且已得到通用开发工具的支持(截至 2023 年 4 月)。但是,它也有一些限制,可以通过遵循推荐的配置实践来减轻这些限制。
featured image - 如何以原生方式在前端项目中配置路径别名
Maksim Zemskov HackerNoon profile picture
0-item
1-item

关于路径别名

项目通常会演变为复杂的嵌套目录结构。因此,导入路径可能变得更长且更混乱,这会对代码的外观产生负面影响,并使理解导入代码的来源变得更加困难。


使用路径别名可以通过允许定义与预定义目录相关的导入来解决问题。这种方法不仅解决了理解导入路径的问题,而且还简化了重构期间代码移动的过程。


 // 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-hqtsconfig-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 包的入口点,但exportsimports字段处理完全不同的任务,即使它们具有相似的名称和语法。


原生支持路径别名理论上有以下优势:


  • 无需安装任何第三方库。


  • 无需预先构建或即时处理导入即可运行代码。


  • 任何使用标准导入解析机制的基于 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 模块,并导致必须满足两个新要求:


  1. 必须指定文件的完整路径,包括文件扩展名。


  2. 不允许指定目录路径并期望导入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 抛出一个错误,因为我们已经将模块解析配置为使用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字段充分利用路径别名,而不会对如何编写导入路径有任何额外限制。

使用 TypeScript 构建代码

使用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 中,路径别名在devbuild模式下无需额外配置即可工作。


因此,我构建了一个完全空配置的测试项目。

卷起

虽然 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代码

对于 VS Code,TypeScript 的版本至关重要。 TypeScript 语言服务器负责分析和浏览 JavaScript 和 TypeScript 代码。


根据您的设置,VS Code 将使用内置版本的 TypeScript 或安装在您的项目中的版本。


我结合 TypeScript v5.0.4 测试了 VS Code v1.77.3 中的imports字段支持。


VS Code 存在以下路径别名问题:


  1. 在模块解析设置设置为nodenextbundler之前,TypeScript 不使用imports字段。因此,要在 VS Code 中使用它,需要在项目中指定模块解析。


  2. 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 在支持路径别名方面有一组单独的问题:


  1. 编辑器严格遵守 Node.js 对使用路径别名的限制。如果未明确指定文件扩展名,代码导航将不起作用。这同样适用于使用index.js文件导入目录。


  2. WebStorm 有一个错误,阻止在imports字段中使用路径数组。在这种情况下,代码导航完全停止工作。


 { "name": "my-awesome-project", // OK "imports": { "#*": "./src/*" }, // This breaks code navigation "imports": { "#*": ["./src/*", "./src/*.ts", "./src/*.tsx"] } }


幸运的是,我们可以使用解决 VS Code 中所有问题的相同技巧。具体来说,我们可以在tsconfig.jsonjsconfig.json文件中复制一个路径别名配置。这允许不受任何限制地使用路径别名。

推荐配置

根据我在各种项目中使用imports字段的实验和经验,我确定了不同类型项目的最佳路径别名配置。

没有 TypeScript 或 Bundler

此配置适用于源代码在 Node.js 中运行而无需额外构建步骤的项目。要使用它,请按照下列步骤操作:


  1. package.json文件中配置imports字段。在这种情况下,一个非常基本的配置就足够了。


  2. 为了让代码导航在代码编辑器中工作,有必要在jsconfig.json文件中配置路径别名。


 // jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }


使用 TypeScript 构建代码

此配置应用于源代码使用 TypeScript 编写并使用tsc编译器构建的项目。在此配置中配置以下内容很重要:


  1. package.json文件中的imports字段。在这种情况下,需要添加条件路径别名以确保 Node.js 正确解析编译后的代码。


  2. package.json文件中启用 ESM 包格式是必要的,因为 TypeScript 在使用imports字段时只能编译 ESM 格式的代码。


  3. tsconfig.json文件中,设置 ESM 模块格式和moduleResolution 。这将允许 TypeScript 在导入时建议忘记的文件扩展名。如果不指定文件扩展名,编译后的代码将不会在 Node.js 中运行。


  4. 要修复代码编辑器中的代码导航,必须在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 关于在导入中指定文件扩展名的限制。


配置以下内容很重要:


  1. package.json文件中配置imports字段。在这种情况下,您需要为每个别名添加一个路径数组。这将允许打包器在不需要指定文件扩展名的情况下找到导入的模块。


  2. 要修复代码编辑器中的代码导航,您需要在tsconfig.jsonjsconfig.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字段不会带来明显的好处。


我希望您从本文中学到了一些新东西。感谢您的阅读!

有用的链接

  1. 用于实现导出和导入的 RFC
  2. 一组测试,以更好地了解 imports 领域的能力
  3. Node.js 中导入字段的文档
  4. Node.js 对 ES 模块中导入路径的限制

也发布在这里