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 🤗.