Стандарт ES2015 (ES6) принёс в JS систему модулей — ESM. Те самые
import ... from ..., которые стали неотъемлемой частью разработки на JS. У ESM в браузере есть ограничения:- Путь к модулю должен быть относительным (начинаться с
/, ./, ../) или абсолютным;- Путь к модулю должен заканчиваться именем файла с расширением
.js;- Импортировать можно только JS-модули.
ESM не стоит на месте. Недавно в стандарт добавили JSON и CSS модули. В будущем ожидается добавление HTML модулей. Также появилась возможность указывать псевдонимы для путей к модулям с помощью Import maps.
Сегодня многие проекты используют системы сборки для упаковки кода. В кодовых базах таких проектов будет нечто похожее на ESM. Но это не настоящий ESM, а специальный синтаксис инструкций для сборщиков:
// 'react' — невалидный путь без псевдонима
import React from 'react';
// импорт .css возможен только через with
import styles from './styles.css';
// импорт изображений не поддерживается
import imgUrl from './img.png';
// недопустимый символ ~ в пути
import { someUtil } from '~/src/utils.js';
// glob patterns не поддерживаются
import * as pages from './pages/*/*.js';
// невалидный путь к модулю
import workletUrl from 'worklet:./worklet.js';
// параметр url игнорируется
import assetAsURL from './asset.js?url';
Это удобно, но не работает в браузере. Поэтому сборщики выполняют преобразования. Как и во что превращаются импорты под капотом — зависит от сборщика и настроек. Тем не менее, нестандартные импорты можно заставить работать в браузере.
Импорт в браузере — это HTTP-запрос к ресурсу. Запрос можно перехватить с помощью Service Worker. Браузер ожидает валидный JS-модуль, значит Service Worker должен его вернуть в ответ на перехваченный запрос импорта.
Для текстовых форматов (HTML, CSS) проще всего создать модуль, который экспортирует содержимое запрашиваемых файлов в виде строки. Дополнительно HTML можно преобразовать в объект
DocumentFragment, а CSS в объект CSSStyleSheet.// sw.js
async function createHTMLModule(html) {
return `
const template = document.createElement('template');
template.innerHTML = \`${html}\`;
export default template.content;
`;
}
async function createCSSModule(css) {
return `
const styles = new CSSStyleSheet();
styles.replaceSync(\`${css}\`);
export default styles;
`;
}
self.addEventListener('fetch', (event) => {
const { destination, url } = event.request;
if (destination === 'script') {
const response = await self.fetch(url);
const text = await response.text();
let module = text;
if (url.endsWith('.html')) {
module = createHTMLModule(text);
} else if (url.endsWith('.css')) {
module = createCSSModule(text);
}
event.respondWith(
new Response(
module,
{
headers: {
'Content-Type': 'text/javascript'
}
}
)
);
}
});
// main.js
import template from './template.html';
import styles from './styles.css';
console.log(template);
// DocumentFragment
console.log(styles);
// CSSStyleSheet
Само собой, Service Worker должен быть подключен к странице и зарегистрирован до того, как будут обрабатываться нестандартные импорты. Таким образом можно импортировать, в принципе, любые ресурсы, предоставляя их браузеру в виде JS-модулей.
#js