Объединение ресурсов, отличных от JavaScript

Узнайте, как импортировать и объединять различные типы ресурсов из JavaScript.

Предположим, вы работаете над веб-приложением. В этом случае, скорее всего, вам придется иметь дело не только с модулями JavaScript, но и со всевозможными другими ресурсами — веб-воркерами (которые тоже являются JavaScript, но не являются частью обычного графа модуля), изображениями, таблицами стилей, шрифтами и т. д. Модули WebAssembly и другие.

Можно включать ссылки на некоторые из этих ресурсов непосредственно в HTML, но часто они логически связаны с повторно используемыми компонентами. Например, таблица стилей для пользовательского раскрывающегося списка, привязанная к его части JavaScript, изображения значков, привязанные к компоненту панели инструментов, или модуль WebAssembly, привязанный к связующему элементу JavaScript. В таких случаях удобнее ссылаться на ресурсы непосредственно из их модулей JavaScript и динамически загружать их при загрузке (или если) соответствующего компонента.

График, визуализирующий различные типы ресурсов, импортированных в JS.

Однако в большинстве крупных проектов есть системы сборки, которые выполняют дополнительную оптимизацию и реорганизацию контента, например объединение и минимизацию. Они не могут выполнить код и предсказать, каким будет результат выполнения, а также не могут просмотреть все возможные строковые литералы в JavaScript и угадать, является ли это URL-адресом ресурса или нет. Так как же заставить их «видеть» динамические ресурсы, загруженные компонентами JavaScript, и включить их в сборку?

Пользовательский импорт в сборщиках

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

// regular JavaScript import
import { loadImg } from './utils.js';

// special "URL imports" for assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';

loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);

Когда плагин упаковщика находит импорт либо с расширением, которое он распознает, либо с такой явной пользовательской схемой ( asset-url: и js-url: в примере выше), он добавляет указанный ресурс в граф сборки, копирует его в окончательный вариант. назначения, выполняет оптимизацию, применимую к типу ресурса, и возвращает конечный URL-адрес, который будет использоваться во время выполнения.

Преимущества этого подхода: повторное использование синтаксиса импорта JavaScript гарантирует, что все URL-адреса являются статическими и относятся к текущему файлу, что упрощает поиск таких зависимостей для системы сборки.

Однако у него есть один существенный недостаток: такой код не может работать непосредственно в браузере, поскольку браузер не знает, как обрабатывать эти пользовательские схемы импорта или расширения. Это может быть хорошо, если вы контролируете весь код и в любом случае полагаетесь на сборщик при разработке, но все чаще используются модули JavaScript непосредственно в браузере, по крайней мере, во время разработки, чтобы уменьшить трудности. Кому-то, работающему над небольшой демонстрацией, может вообще не понадобиться сборщик, даже в рабочей среде.

Универсальный шаблон для браузеров и сборщиков

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

new URL('./relative-path', import.meta.url)

Этот шаблон можно обнаружить статически с помощью инструментов, почти как если бы это был специальный синтаксис, однако это допустимое выражение JavaScript, которое также работает непосредственно в браузере.

При использовании этого шаблона приведенный выше пример можно переписать так:

// regular JavaScript import
import { loadImg } from './utils.js';

loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
  fetch(new URL('./module.wasm', import.meta.url)),
  { /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));

Как это работает? Давайте разберемся. new URL(...) принимает относительный URL-адрес в качестве первого аргумента и сопоставляет его с абсолютным URL-адресом, указанным в качестве второго аргумента. В нашем случае вторым аргументом является import.meta.url , который предоставляет URL-адрес текущего модуля JavaScript, поэтому первым аргументом может быть любой путь относительно него.

Он имеет те же компромиссы, что и динамический импорт . Хотя можно использовать import(...) с произвольными выражениями, такими как import(someUrl) , сборщики уделяют особое внимание шаблону со статическим URL-адресом import('./some-static-url.js') как способ предварительной обработки. зависимость, известная во время компиляции, но разделенная на отдельный фрагмент , загружаемый динамически.

Точно так же вы можете использовать new URL(...) с произвольными выражениями, такими как new URL(relativeUrl, customAbsoluteBase) , но new URL('...', import.meta.url) является четким сигналом для сборщиков о необходимости предварительной обработки. и включите зависимость вместе с основным JavaScript.

Неоднозначные относительные URL-адреса

Вам может быть интересно, почему сборщики не могут обнаружить другие распространенные шаблоны — например, fetch('./module.wasm') без new URL ?

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

  • index.html :
    html <script src="src/main.js" type="module"></script>
  • src/
    • main.js
    • module.wasm

Если вы хотите загрузить module.wasm из main.js , может возникнуть соблазн использовать относительный путь, например fetch('./module.wasm') .

Однако fetch не знает URL-адрес файла JavaScript, в котором она выполняется, вместо этого она разрешает URL-адреса относительно документа. В результате fetch('./module.wasm') попытается загрузить http://example.com/module.wasm вместо запланированного http://example.com/src/module.wasm и потерпит неудачу. (или, что еще хуже, молча загрузите другой ресурс, чем вы предполагали).

Обернув относительный URL-адрес в new URL('...', import.meta.url) вы можете избежать этой проблемы и гарантировать, что любой предоставленный URL-адрес будет разрешен относительно URL-адреса текущего модуля JavaScript ( import.meta.url ). прежде чем он будет передан любым загрузчикам.

Замените fetch('./module.wasm') на fetch(new URL('./module.wasm', import.meta.url)) и он успешно загрузит ожидаемый модуль WebAssembly, а также предоставит сборщикам возможность найдите эти относительные пути и во время сборки.

Инструментальная поддержка

Упаковщики

Следующие упаковщики уже поддерживают new URL :

Веб-сборка

При работе с WebAssembly вы обычно не загружаете модуль Wasm вручную, а вместо этого импортируете связку JavaScript, созданную цепочкой инструментов. Следующие цепочки инструментов могут генерировать для вас описанный new URL(...) .

C/C++ через Emscripten

При использовании Emscripten вы можете попросить его генерировать связку JavaScript в виде модуля ES6 вместо обычного скрипта с помощью одного из следующих вариантов:

$ emcc input.cpp -o output.mjs
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6

При использовании этой опции в выходных данных будет использоваться new URL(..., import.meta.url) , чтобы сборщики могли автоматически найти связанный файл Wasm.

Вы также можете использовать эту опцию с потоками WebAssembly , добавив флаг -pthread :

$ emcc input.cpp -o output.mjs -pthread
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread

В этом случае сгенерированный Web Worker будет включен таким же образом и также будет доступен для обнаружения как сборщиками пакетов, так и браузерами.

Ржавчина через wasm-pack/wasm-bindgen

Wasm-pack — основной набор инструментов Rust для WebAssembly — также имеет несколько режимов вывода.

По умолчанию он создаст модуль JavaScript, основанный на предложении по интеграции WebAssembly ESM . На момент написания это предложение все еще является экспериментальным, и результат будет работать только в комплекте с Webpack.

Вместо этого вы можете попросить wasm-pack создать совместимый с браузером модуль ES6 через --target web :

$ wasm-pack build --target web

В выводе будет использоваться описанный new URL(..., import.meta.url) , а сборщики файлов также будут автоматически обнаруживать файл Wasm.

Если вы хотите использовать потоки WebAssembly с Rust, ситуация немного сложнее. Чтобы узнать больше, ознакомьтесь с соответствующим разделом руководства .

Короткая версия заключается в том, что вы не можете использовать API произвольных потоков, но если вы используете Rayon , вы можете объединить его с адаптером wasm-bindgen-rayon , чтобы он мог создавать Workers в Интернете. Связующий элемент JavaScript, используемый wasm-bindgen-rayon, также включает в себя new URL(...) под капотом, поэтому рабочие процессы также будут доступны для обнаружения и включены сборщиками.

Будущие возможности

import.meta.resolve

Выделенный вызов import.meta.resolve(...) является потенциальным будущим улучшением. Это позволило бы разрешать спецификаторы относительно текущего модуля более простым способом, без дополнительных параметров:

new URL('...', import.meta.url)
await import.meta.resolve('...')

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

import.meta.resolve уже реализован в Node.js в качестве эксперимента , но все еще остаются нерешенные вопросы о том, как он должен работать в Интернете.

Импортировать утверждения

Утверждения импорта — это новая функция, которая позволяет импортировать типы, отличные от модулей ECMAScript. На данный момент они ограничены JSON:

фу.json:

{ "answer": 42 }

main.mjs:

import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42

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

Ознакомьтесь с описанием функций v8.dev, чтобы узнать больше об этой функции.

Заключение

Как видите, существуют различные способы включения в Интернет ресурсов, отличных от JavaScript, но они имеют различные недостатки и не работают в различных цепочках инструментов. Будущие предложения, возможно, позволят нам импортировать такие ресурсы со специальным синтаксисом, но мы еще не дошли до этого.

До тех пор new URL(..., import.meta.url) является наиболее многообещающим решением, которое сегодня уже работает в браузерах, различных сборщиках и наборах инструментов WebAssembly.