Vite в браузере

TL;DR

Мы сделали browser-vite — исправленную версию Vite, работающую в браузере с помощью Workers.

Как это работает — в двух словах

  • Service Worker: заменяет HTTP-сервер Vite. Например, перехватывает HTTP-вызовы встроенного iframe.
  • Web Worker: Запускается браузером Vite для обработки вне основного потока.
  • Вызовы к файловой системе заменяются файловой системой в памяти.
  • Импорт файлов со специальными расширениями (.ts, .tsx, .scss…) преобразуется.

Проблемы

Отсутствие реальной файловой системы

Vite много работает с файлами. Файлами проекта, а также конфигурационными файлами, наблюдателями и глобусами. Их трудно реализовать в браузере с шиммированной in-memory FS. Мы удалили watchers, globs и вызовы конфигурационных файлов, чтобы ограничить сложность и поверхностный API.

Файлы проекта остаются в ФС in-memory, к которым браузер-vite и плагины vite могут нормально обращаться.

Никаких «node_modules»

Vite полагается на наличие node_modules для разрешения зависимостей. И он связывает их в оптимизации Dependencing Pre-Bundling при запуске.

Мы не хотели запускать папку node_modules в памяти браузера, потому что, по нашему мнению, это слишком много данных для загрузки и хранения в памяти браузера. Поэтому мы тщательно вычеркнули из Vite резольверы узлов и Dependencing Pre-Bundling.

Пользователи browser-vite должны создать плагин Vite для разрешения импорта голых модулей.

Наши продукты: Backlight.dev, Components.studio и WebComponents.dev, работают с оптимизатором бандлеров на стороне сервера уже 2 года. Мы создали плагин Vite для browser-vite для автоматического разрешения зависимостей узлов. На момент написания этой заметки этот серверный оптимизатор не имеет открытого исходного кода.

Regex «lookbehind»

Некоторые регексы в Vite используют lookbehind. Это отлично работает локально при выполнении Node.js, но не поддерживается в Safari.

Поэтому мы переписали регексы для большей совместимости с браузерами.

Горячая перезагрузка модуля (HMR)

Vite использует WebSockets для передачи изменений кода от сервера (нода) к клиенту (браузеру).

В browser-vite сервер — это ServiceWorker + Vite worker, а клиент — iframe. Поэтому мы изменили коммуникацию с WebSockets на пост-сообщение в iframe.

Для этого код Vite на стороне клиента в iframe был заменен на специальную браузерную версию, обрабатывающую сообщения вне WebSockets.

Как использовать

На момент написания статьи это не просто «подключи и работай». Чтобы использовать browser-vite, нужно многое понять, прочитав внутреннюю обработку Vite.

Примечание: Этот пост может устареть со временем, поэтому обязательно проверьте
в README browser-vite для получения актуальной информации об использовании browser-vite.

Установка

Установите пакет browser-vite npm.

$ npm install --save browser-vite
Вход в полноэкранный режим Выйти из полноэкранного режима

или

$ npm install --save vite@npm:browser-vite
Войти в полноэкранный режим Выйти из полноэкранного режима

Импортировать «vite» в «browser-vite».

iframe — окно для browser-vite

Вам нужен iframe, который будет показывать страницы, обслуживаемые внутри browser-vite.

Service Worker — внутрибраузерный веб-сервер

Service Worker будет перехватывать определенные запросы URL, поступающие из iframe.

Вот пример с использованием Workbox.

workbox.routing.registerRoute(
  /^https?://HOST/BASE_URL/(/.*)$/,
  async ({
    request,
    params,
    url,
  }: import('workbox-routing/types/RouteHandler').RouteHandlerCallbackContext): Promise<Response> => {
    const req = request?.url || url.toString();
    const [pathname] = params as string[];
    // send the request to vite worker
    const response = await postToViteWorker(pathname)
    return response;
  }
);
Вход в полноэкранный режим Выход из полноэкранного режима

В большинстве случаев сообщение отправляется на «Vite Worker» с помощью postMessage или broadcast-channel.

Vite Worker — обработка запроса

Vite Worker — это Web Worker, который будет обрабатывать запросы, полученные от Service Worker.

Пример создания Vite Server:

import {
  transformWithEsbuild,
  ModuleGraph,
  transformRequest,
  createPluginContainer,
  createDevHtmlTransformFn,
  resolveConfig,
  generateCodeFrame,
  ssrTransform,
  ssrLoadModule,
  ViteDevServer,
  PluginOption
} from 'browser-vite';

export async function createServer(
  const config = await resolveConfig(
    {
      plugins: [
        // virtual plugin to provide vite client/env special entries (see below)
        viteClientPlugin,
        // virtual plugin to resolve NPM dependencies, e.g. using unpkg, skypack or another provider (browser-vite only handles project files)
        nodeResolvePlugin,
        // add vite plugins you need here (e.g. vue, react, astro ...)
      ]
      base: BASE_URL, // as hooked in service worker
      // not really used, but needs to be defined to enable dep optimizations
      cacheDir: 'browser',
      root: VFS_ROOT,
      // any other configuration (e.g. resolve alias)
    },
    'serve'
  );
  const plugins = config.plugins;
  const pluginContainer = await createPluginContainer(config);
  const moduleGraph = new ModuleGraph((url) => pluginContainer.resolveId(url));

  const watcher: any = {
    on(what: string, cb: any) {
      return watcher;
    },
    add() {},
  };
  const server: ViteDevServer = {
    config,
    pluginContainer,
    moduleGraph,
    transformWithEsbuild,
    transformRequest(url, options) {
      return transformRequest(url, server, options);
    },
    ssrTransform,
    printUrls() {},
    _globImporters: {},
    ws: {
      send(data) {
        // send HMR data to vite client in iframe however you want (post/broadcast-channel ...)
      },
      async close() {},
      on() {},
      off() {},
    },
    watcher,
    async ssrLoadModule(url) {
      return ssrLoadModule(url, server, loadModule);
    },
    ssrFixStacktrace() {},
    async close() {},
    async restart() {},
    _optimizeDepsMetadata: null,
    _isRunningOptimizer: false,
    _ssrExternals: [],
    _restartPromise: null,
    _forceOptimizeOnRestart: false,
    _pendingRequests: new Map(),
  };

  server.transformIndexHtml = createDevHtmlTransformFn(server);

  // apply server configuration hooks from plugins
  const postHooks: ((() => void) | void)[] = [];
  for (const plugin of plugins) {
    if (plugin.configureServer) {
      postHooks.push(await plugin.configureServer(server));
    }
  }

  // run post config hooks
  // This is applied before the html middleware so that user middleware can
  // serve custom content instead of index.html.
  postHooks.forEach((fn) => fn && fn());

  await pluginContainer.buildStart({});
  await runOptimize(server);

  return server;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Псевдокод для обработки запросов через browser-vite

import {
  transformRequest,
  isCSSRequest,
  isDirectCSSRequest,
  injectQuery,
  removeImportQuery,
  unwrapId,
  handleFileAddUnlink,
  handleHMRUpdate,
} from 'vite/dist/browser';

...

async (req) => {
  let { url, accept } = req
  const html = accept?.includes('text/html');
  // strip ?import
  url = removeImportQuery(url);
  // Strip valid id prefix. This is prepended to resolved Ids that are
  // not valid browser import specifiers by the importAnalysis plugin.
  url = unwrapId(url);
  // for CSS, we need to differentiate between normal CSS requests and
  // imports
  if (isCSSRequest(url) && accept?.includes('text/css')) {
    url = injectQuery(url, 'direct');
  }
  let path: string | undefined = url;
  try {
    let code;
    path = url.slice(1);
    if (html) {
      code = await server.transformIndexHtml(`/${path}`, fs.readFileSync(path,'utf8'));
    } else {
      const ret = await transformRequest(url, server, { html });
      code = ret?.code;
    }
    // Return code reponse
  } catch (err: any) {
    // Return error response
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Как это сравнивается с Stackblitz WebContainers

«WebContainers:
Запуск Node.js нативно в вашем браузере»

WebContainers от Stackblitz также может запускать Vite в браузере. Вы можете элегантно перейти на сайт vite.new, чтобы получить рабочую среду.

Мы не эксперты в WebContainers, но, в двух словах, там, где browser-vite подстраивает FS и HTTPS-сервер на уровне Vite, WebContainers подстраивает FS и многое другое на уровне Node.js, и Vite работает на нем с несколькими дополнительными изменениями.

Он доходит до хранения node_modules в WebContainer, в браузере. Но он не запускает npm или yarn напрямую, потому что это заняло бы слишком много места (я полагаю). Они передали эти команды Turbo — своему пакетному менеджеру.

WebContainers может запускать и другие фреймворки, такие как Remix, SvelteKit или Astro.

Это волшебно ✨ Это умопомрачительно 🤯 Мы очень уважаем то, что создала команда Stackblitz.

Одним из недостатков WebContainers является то, что сегодня он может работать только в Chrome, но, возможно, скоро будет работать в Firefox. browser-vite работает в Chrome, Firefox и Safari сегодня.

В двух словах, WebContainers работает на более низком уровне абстракции для запуска Vite в браузере. browser-vite работает на более высоком уровне абстракции, очень близко к самому Vite.

Метафорически, для любителей ретро-игр, browser-vite немного похож на UltraHLE 🕹️😊.

(*) gametechwiki.com: Эмуляция высокого/низкого уровня

Что дальше?

Браузер-вайт лежит в основе наших решений. Мы постепенно внедряем его во все наши продукты:

  • Backlight.dev
  • Components.studio
  • WebComponents.dev
  • Replic.dev (новое приложение появится очень скоро!).

В дальнейшем мы будем продолжать инвестировать в browser-vite и сообщать о его развитии. В прошлом месяце мы также объявили о спонсорстве Vite через Evan You и Patak, чтобы поддержать этот замечательный проект.

Хотите узнать больше?

  • Репозиторий GitHub: browser-vite
  • Присоединяйтесь к нашему серверу Discord, у нас есть канал #browser-vite 🤗.

Оставьте комментарий

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