ESM. Выходим за рамки

8ee8e6cc8bfbcd869ab9189e85f67965.png

Итак, работая над… ну не знаю… каким-нибудь замечательным генератором статики, вы, возможно, захотите импортировать в свой код зависимости напрямую из текстовых файлов, таких как: HTML, MD, CSS, SVG или JSON. Конечно, можно использовать бандлер с соответствующим лоадером. Но, допустим, ваш кодекс самурая велит вам — никаких лишних npm install xxx и промежуточных билдов! Только хардкор! Что делать? Выход есть.

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

И еще пара формальностей перед стартом:

  • Убедитесь, что у вас установлена свежая нода, а некро-нода — надежно закопана.

  • Если хотите писать изоморфный код, не используйте расширения *.mjs, браузеры такого не любят (не забудьте в настройках package.json врубить "type": "module").

Упражнение первое. Нода. Кастомный лоадер (хук).

Не забыли ноду обновить? А то может не сработать.

Делай раз (loader.js):

import { URL } from 'url';
import { readFile } from 'fs/promises';

function checkUrl(url) {
  return !!['.html', '.htm', '.md', '.css', '.svg', '.json'].find((res) => {
    return url.endsWith(res);
  });
}

export async function load(url, context, defaultLoad) {
  if (checkUrl(url)) {
    const content = (await readFile(new URL(url))).toString();
    return {
      format: 'module',
      source: `export default ${url.endsWith('json') ? content : JSON.stringify(content)};`,
      shortCircuit: true,
    };
  }
  return defaultLoad(url, context, defaultLoad);
}

Делай два (my-app.js):

import html from './index.html';
import doc from './doc.md';
import css from './styles.css';
import svg from './image.svg';
import data from './data.json';

console.log(html, doc, css, svg, data);

Делай три:  

node --loader ./loader.js ./my-app.js

Ссылка на доку:  https://nodejs.org/api/esm.html#loaders

В документации технология помечена как экспериментальная, однако, в свежей версии, параметр --experimental-loader заменили на --loader, что, косвенно, но говорит нам о том, что технология на подходе к стабильному состоянию. В любом случае, неплохо знать о такой возможности.

Упражнение второе. Браузер. Перехват запросов.

Переносимся в браузерный рантайм. Задача та-же: хотим грузить модули, которые вообще никакие не модули. Никаких специальных хуков для ESM браузеры не поддерживают. Что делать? Идем на перехват с помощью Service Worker.

Технологии иные, но принцип сохранился. Создаем файл сервис-воркера (sw.js):

function checkUrl(url) {
  return !!['.html', '.htm', '.md', '.css', '.svg', '.json'].find((res) => {
    return url.endsWith(res);
  });
}

async function handleRequest(req) {
  let content = await (await self.fetch(req.url)).text();
  let resp = new Response(`export default ${req.url.endsWith('json') ? content : JSON.stringify(content)};`, {
    headers: { 'Content-Type': 'text/javascript' }
  });
  return resp;
}

self.addEventListener('fetch', (e) => {
  if (e.request.destination === 'script' && checkUrl(e.request.url)) {
    e.respondWith(handleRequest(e.request));
  }
});

Регистрируем воркер на странице:

navigator.serviceWorker.register('./sw.js');

Вуаля — используем кастомные импорты:

import html from './index.html';
import doc from './doc.md';
import css from './styles.css';
import svg from './image.svg';
import data from './data.json';

console.log(html, doc, css, svg, data);

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

Упражнение третье. Нода + браузер. Параметры запроса.

Теперь будет больше, кхм, наркомании. Я просто покажу тропинку, а вы сами решите, ходить по ней или нет. Итак, возможно именно Вы об этом знали, но для тех, кто не знал, я сообщу: в ESM поддерживаются такие штуки как top level await. Это значит, что перед тем, как модуль отдаст свой экспорт, вы можете совершить в нем что-то асинхронное. Например, сделать запрос и получить на него ответ. Что нам это дает? Правильно:

Делаем модуль-загрузчик (load.js):

let result = null;
const path = import.meta.url.split('#')[1];
if (path) {
  let content;
  if (typeof window === 'object') {
    content = await (await fetch(path)).text();
  } else {
    const fs = (await import('fs')).default;
    content = fs.readFileSync(path).toString();
  }
  result = path.includes('.json') ? JSON.parse(content) : content;
}
export default result;

Грузим свои ассеты через прокси-модуль:

import html from './load.js?#./index.html';
import doc from './load.js?#./doc.md';
import css from './load.js?#./styles.css';
import svg from './load.js?#./image.svg';
import data from './load.js?#./data.json';

console.log(html, doc, css, svg, data);

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

Важная деталь: экспорты каждого модуля кэшируются. И этот кэш привязан к адресу модуля. Поэтому, если ваши импортируемые файлы меняются динамически — пути к модулям тоже должны меняться. А это значит, что импорты получится использовать только динамические. Помня про поддержку top level await, мы не будем считать это большой проблемой.

Итого.

Вы можете спросить меня: -, а есть ли в этом вообще какая-то практическая ценность? Ведь можно просто использовать те-же fs.readFileSync или fetch… Я, пожалуй, уклонюсь от прямого ответа и напомню, что если звезды зажигают — значит это кому-нибудь нужно.

Надеюсь было весело. Еще больше надеюсь, что вы узнали для себя что-то полезное. Используйте больше ESM и меньше CJS. Всем — добра и мира.

© Habrahabr.ru