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

от автора

Итак, работая над… ну не знаю… каким-нибудь замечательным генератором статики, вы, возможно, захотите импортировать в свой код зависимости напрямую из текстовых файлов, таких как: 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. Всем — добра и мира.


ссылка на оригинал статьи https://habr.com/ru/post/685850/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *