Без импортных карт не обойтись, если при разработке вы не хотите терять время на комплектацию.

Проблемы с URL-адресами в качестве спецификаторов модулей

Оператор import импортирует живые привязки из модуля, указанного спецификатором модуля:

import defaultExport from 'moduleSpecifier';

Когда браузеры начали поддерживать модули ES, в качестве спецификаторов модуля принимались только допустимые URL-адреса. Недавно реализованные карты импорта расширили выбор спецификаторов. Чтобы понять, как карты импорта облегчают разработку JavaScript, давайте сначала рассмотрим ограничения и преимущества URL-адресов в роли спецификаторов модулей. Чтобы продемонстрировать свойства URL-адресов, я буду использовать Visual Studio Code, популярный среди веб-разработчиков редактор кода.

Пример кода для этого раздела сообщения сохраняется в специальной подпапке moduleSpecifiers. Код предполагается выполнять в браузере.

Если index.html обслуживается расширением VSCode Live server, браузер загружает его с http://127.0.0.1:5500/moduleSpecifiers/index.html. Обратите внимание, URL-адрес стартовой страницы включает имя папки, добавленное к URL-адресу хоста.

Представьте себе обычную ситуацию — модуль, сохраненный глубоко в дереве папок, должен импортировать модуль, общий для многих модулей, например, эмиттер событий или константы. В моем примере кода view.js зависит от eventEmitter.js. Предположим, что расположение eventEmitter.js не изменится, потому что import во многих зависимых модулях придется скорректировать. Рассмотрим преимущества и недостатки возможных операторов import в view.js.

Абсолютные URL

Обычно они используются для загрузки модуля с другого веб-узла, обычно принадлежащего какой-либо сети доставки контента. view.js может импортировать eventEmitter.js, используя подробные URL-адреса:

import emitter from 'http://127.0.0.1:5500/moduleSpecifiers/js/shared/events/eventEmitter.js';

Это утверждение не нужно корректировать, если во время разработки view.js перемещается в любую другую папку.

Но поскольку URL-адрес содержит ненужную информацию, его необходимо скорректировать при смене сервера разработки или переименовании исходной папки moduleSpecifiers.

Абсолютные URL пути

Абсолютные URL пути начинаются с /. Они относятся к абсолютному URL-адресу веб-сервера, на котором размещена страница. Абсолютные URL-адреса пути трудно использовать в веб-приложениях, которые начинаются с URL-адреса, включающего контекстный путь конкретного приложения в дополнение к имени домена. Веб-приложения, развернутые на веб-серверах Java, таких как Tomcat, обычно имеют контекстный путь.

Абсолютный URL-адрес кратчайшего возможного пути в операторе view.js import также излишне длинный:

import emitter from '/moduleSpecifiers/js/shared/events/eventEmitter.js';

URL-адрес не нужно корректировать, если view.js перемещен в другую папку или если код обслуживается с другого веб-сервера.

Если исходная папка moduleSpecifiers переименована, необходимо скорректировать URL-адреса.

Относительные URL пути

URL-адреса, начинающиеся с ./ или ../, относятся к URL-адресу модуля импорта. Это лучший вариант для спецификаторов модулей, но у них есть ограничения.

Оператор import в view.js может показаться неудобным из-за множества неразборчивых двойных точек:

import emitter from '../../../../shared/events/eventEmitter.js';

Теперь URL-адрес не нужно корректировать, если я могу изменить сервер или переименовать исходную папку.

Но мне может понадобиться настроить количество сегментов с двумя точками, если переместить view.js в другую папку.

Более удобные спецификаторы модулей

Начиная с Chrome 89, сложный относительный URL-адрес в приведенном выше операторе import можно заменить более кратким спецификатором модуля.

import emitter from 'shared/events/eventEmitter.js';

или даже более короткий спецификатор голого модуля

import emitter from 'eventEmitter';

или произвольный путь

import emitter from 'a/b/c/d.js';

Современные спецификаторы shared/events/eventEmitter.js и eventEmitter не только короче, но и позволяют view.js перемещаться без необходимости настраивать import файла eventEmitter.js, пока eventEmitter.js не перемещается.

Только для одного модуля view.js такое переназначение URL бесполезно. Но довольно удобно, когда приложение состоит из сотен модулей, которые зависят от каких-то общих модулей, таких как библиотеки, утилиты или константы. С картой импорта модули можно перемещать без настройки imports импорта общих модулей.

Чтобы включить три описанных выше спецификатора, я добавил их сопоставления в карту импорта в index.html.

Включение импорта карты

Карту импорта можно включить, вставив тег <script type="importmap"> перед первым <script type=”module”>. HTML-страница может содержать только одну карту импорта.

Все значения карты импорта должны быть действительными URL-адресами. Относительные URL-адреса пути разрешаются относительно URL-адреса HTML-файла, содержащего карту импорта.

В картах импорта есть два типа ключей:

  • спецификаторы модуля, каждый из которых указывает на модуль
  • спецификаторы папки - они заканчиваются на / и указывают на папку.

Карта, которую я использовал в своем примере кода, содержит оба типа ключей.

<script type="importmap">
        {
           "imports": {
               "shared/events/": "./js/shared/events/",
               "eventEmitter": "./js/shared/events/eventEmitter.js",
               "a/b/c/d.js": "./js/shared/events/eventEmitter.js"
           }
        }
</script>

Браузер обращается к карте при загрузке модулей.

В заявлении

import emitter from 'shared/events/eventEmitter.js';

спецификатор модуля shared/events/eventEmitter.js содержит ключ карты shared/events/. Браузер заменяет ключ его значением ./js/shared/events/, создавая URL-адрес относительно index.html./js/shared/events/eventEmitter.js.

Когда спецификатор модуля соответствует ключу карты без косой черты в конце, спецификатор просто заменяется значением ключа. С приведенной выше картой импорта спецификаторы в

import emitter from 'eventEmitter';
import emitterInexistentPath from 'a/b/c/d.js';

также преобразуются в ./js/shared/events/eventEmitter.js.

Модули связывания переназначены в карте импорта

Хотя объединение во время разработки можно рассматривать как пустую трату времени, объединение стабильного кода перед выпуском в рабочую среду может быть хорошей идеей, особенно если доступ к приложению будет осуществляться не только из внутренней сети.

Давайте посмотрим, как связать модули, переназначенные в карте импорта. Когда я впервые собирал приложение, полагающееся на карту импорта, я не мог найти подходящего подключаемого модуля карты импорта Rollup. Поэтому я создал свой собственный. Его короткий код следует описанному выше механизму переназначения спецификаторов модулей:

function ImportmapPlugin({ imports }) {
    const moduleMap = {};
    const packageMap = {};
    Object.entries(imports).forEach(([key, path]) => {
        if (key.endsWith('/')) moduleMap[key] = path;
        else packageMap[key] = path;
    });
    return {
        resolveId(source, importer) {
            const path = moduleMap[source];
            if (path) return  toAbsolutePath(path);
            const key = Object.keys(imports).find(key =>        source.startsWith(key));
            if (key)
                return  toAbsolutePath(source.replace(key, imports[key]));
            return null;
        }
    };
}

Плагин должен быть снабжен копией объекта карты импорта, используемого в файле HTML.

Чтобы убедиться, что плагин создает идеальный пакет, скачайте пример кода, перейдите в папку moduleSpecifiers и выполните команды:

npm install
rollup -c

Более подробное описание карт импорта см. в официальной документации. Мой пример кода можно скачать с Github и выполнить в браузере.