Довольно часто в проектах встречается использование относительных import/require. Если это маленький проект, и подключается модуль из текущей папки, то это приемлемо, но при разрастании проекта и глубины вложенности папочной структуры без слез смотреть на это нельзя:
import { User } from '../../user/model';
import { Article } from '../../article/model';
import { Cache } from '../../../../cache';
import { MongoDB } from '../../../../mongodb';
Основные минусы относительных путей:
Они плохо читаются и загрязняют код. Человеку требуются когнитивные усилия, чтобы интерпретировать эти
../../
в реальный путь. Гораздо проще читаются пути от корня к файлам.Редакторы кода не всегда корректно исправляют относительные пути при перемещении файла. А если редактор не смог добавить autoimport сущности из модуля при написании кода, то одна боль писать руками относительный import в более или менее развесистом проекте. Копирование import/require из другого файла тоже плохо работает, ибо если новый файл лежит на другом уровне вложенности, то придется во всех скопированных import/require добавлять/удалять
../../
.При любом перемещении файла надо поменять не только пути в import/require в других файлах, которые его подключают, но и в самом перемещаемом файле меняются import/require (надо будет добавить/убрать лишние перемещения по каталогам). Это создает дополнительный «шум» в системе контроля версии, хотя по сути ничего не поменялось. С абсолютным путем этой проблемы бы не было.
В некоторых языках, как в Python, возможность писать импорты от корня проекта есть из коробки, но в JavaScript этого нет. К счастью, это довольно не сложно добавить.
Есть несколько способов решения проблемы:
Cимлинк
Создать симлинк в корне вашей системы, который ведет к вашему проекту, и использовать его для написания require/import.
// sudo ln -s /Users/mitya/project prj
// Вместо
const User = require('../../model/User');
// Можно будет использовать вот такую запись
const User = require('/prj/src/model/User');
Минус в том, что с симлинками могут быть «сюрпризы». Далеко не во всех операционках это легко. В современной MacOS есть определенные нюансы (надо использовать synthetic.conf). Кроме того, из-за странной работы с симлинками от корня на MacOS, компилятор typescript при компиляции проекта по симлинку от корня генерит что-то странное. Как в Windows, не знаю, но наверняка есть проблемы или ограничения. Судя по всему проблем не будет только на Linux. К минусам так же можно отнести то, что чисто гипотетически название симлинка может конфликтовать (например, ваш домашний и рабочий проект использует симлинк prj
) и невозможность разложить копии проекта в разные папки и запустить без правки путей к модулям (тут виртуализация и контейнеризация поможет).
NODE_PATH
Другой вариант это добавить пути к вашим папкам в переменную окружения NODE_PATH. По умолчанию, она содержит пути для того, чтобы node js могла подключить модули из node_modules, но мы можем использовать ее в своих целях:
// Вместо
const User = require('../../model/User');
// Можно использовать вот такую запись
const User = require('src/model/User');
// при старте указываем пути, в которых node должна искать модули
// NODE_PATH=./ node src/app/main.js
// Для typescript необходимо добавить baseUrl, который должен совпадать с NODE_PATH
{
"compilerOptions": {
"baseUrl": "."
}
}
Минус это то, что не все редакторы хорошо работают c NODE_PATH в плане перехода к файлу из объявления import/require. Но довольно давно появился способ получше.
Path aliases
Path aliases - это функционал для использования алиасов для путей внутри require/import.
// Вместо
const User = require('../../model/User');
// Можно использовать вот такую запись
const User = require('@/model/User');
Для того чтобы такая запись работала, нам потребуется модуль module-alias
:
npm i module-alias -S
Для того чтобы module-alias
мог сопоставить алиасы в реальный путь в файловой системе необходимо добавить следующую запись в package.json
:
"_moduleAliases": {
"@": "src"
}
Затем в самом вверху файла, который является точкой сборки приложения, добавить import/require самого модуля:
// src/app/main.js
require('module-alias/register'); // import 'module-alias/register';
Другой вариант подключения это загружать модуль через командую строку:
node -r ./node_modules/module-alias/register src/app/main.js
Для того чтобы использовать path aliases в typescript необходимо добавить в tsconfig.json
директиву path
, в которой указать алиасы до исходников:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
А папку для алиасов поменять в package.json
на папку с итоговой сборкой, в нашем случае dist
:
"_moduleAliases": {
"@": "dist"
},
Теперь наши импорты с aлиасами должны работать:
node -r ./node_modules/module-alias/register dist/app/main.js
Если вы пишете бэкенд и для запуска проекта используете ts-node
, то вам потребуется модуль ts-config-path
для работы path aliases:
npm i tsconfig-paths -S;
npx ts-node -r tsconfig-paths/register src/app/main.ts;
Я же для запуска бэкенда на typescript использую tsx, он из коробки понимает path aliases:
npm i tsx -D;
npx tsx src/app/main.ts;
Алиасов можно создавать сколько душе угодно:
{
"compilerOptions": {
"paths": {
"app/*": ["./src/app/*"],
"config/*": ["./src/config/*"],
"shared/*": ["./src/shared/*"],
"cache/*": ["./src/cache/*"],
"tests/*": ["./src/tests/*"]
},
}
Подробнее про path alias можно почитать здесь и здесь. Используя path
вместе с baseUrl
можно организовать довольно интересные схемы подключения модулей из разных папок.
Теперь мы можем писать красивые и понятные import/require:
import { User } from '@component/user/model';
import { Article } from '@component/article/model';
import { Cache } from '@cache/cache';
import { MongoDB } from '@db/mongodb';