WebContainers: загальний огляд, приклади застосування, актуальні проблеми
Всім привіт, мене звати Олександр Зіневич і я відповідаю за розвиток Node.js та Ruby компетенцій у компанії Avenga. Я захоплююсь програмуванням, технологіями та усім, що з цим пов’язано.
Рік тому я вперше почув про WebContainers і те, що браузери та сучасний веб розвинувся настільки, щоб виконувати Node.js безпосередньо у браузері. Тоді це працювало лише у Google Chrome, але вже цього року технологія стала фактично кросбраузерною і підтримується більшістю сучасних браузерів.
Саме тому я дослідив цю технологію детальніше і хочу поділитись своїми думками у даному матеріалі.
Для чого нам потрібно запускати Node.js у браузері
Це питання миттєво виникло в моїй голові, коли я вперше прочитав про WebContainers (далі — вебконтейнери). І якщо задуматись, то відповідь дуже проста. Це може бути корисним для усіх застосунків, які запускають окремі віртуальні машини для виконання користувацького коду і обробки його результатів. Наприклад:
- Документація — вебзастосунки для роботи з документацією вже давно стали окремими інтерактивними системами, які дозволяють не лише читати документи, але і виконувати окремі шматки коду та взаємодіяти з відповідними результатами.
- Навчальні програми — вебзастосунки для навчання також часто дозволяють писати програмний код у навчальних цілях та виконувати його.
- Розробка — вебзастосунки — пісочниці типу codesanbox чи схожі, що дозволяють писати джаваскрипт code і виконувати його.
Типово, загальна архітектура таких застосунків виглядає так:

Data Layer — рівень сховища даних, де ми зберігаємо всі види даних, як користувацькі, так і необхідні для коректної роботи застосунку в цілому.
Client Layer — це те, що в нашому браузері. Фронтенд завантажується з сервера та надає нам зручний інтерфейс для роботи з даними та сервером.
Server Layer — з одного боку хостить і інколи рендерить наш фронтенд, а з іншого — містить власне всю бізнес-логіку, необхідну для коректної роботи бекенд-частини нашого застосунку.
У випадку застосунків, описаних вище, на цьому рівні власне і міститься логіка розгортання окремих віртуальних машин та взаємодії з ними. Така архітектура хоч і цілком робоча, але робить бекенд та інфраструктуру значно складнішою та підвищує вартість.
Якщо ж спробувати використати сучасні можливості веббраузерів і перенести відповідальність за виконання користувацького коду на самих клієнтів, то можемо отримати ряд переваг:

Витрати на інфраструктуру точно зменшаться, оскільки клієнтський код буде виконуватись на клієнтській системі, швидкодія виросте, оскільки не потрібно буде витрачати час на комунікацію із сервером. Вебконтейнери — це технологія, що дає нам можливість це реалізувати.
WebContainers. Загальний огляд
WebContainers — це технологія, яку створила компанія StackBlitz. Це середовище, яке дозволяє виконувати Node.js код, а також «команди операційної системи» безпосередньо у браузері. Для кращого розуміння можете вважати, що вебконтейнери — це свого роду віртуальна POSIX сумісна операційна система у вкладці вашого браузеру.
Ця операційна система побудована на таких існуючих технологіях сучасних браузерів:
- Service Workers — щось на зразок проксі-серверу, який вміє перехоплювати мережеві запити з браузера на сервер, керувати політиками кешування та являє собою основу для PWA та офлайн режиму роботи вебзастосунків;
- Web Workers — дозволяє досягнути багатопоточності і виконувати важкі довгі задачі в окремому потоці, не блокуючи при цьому основний. Тим самим, забезпечуючи безперервний відгук користувацького інтерфейсу.
- WebAssembly — бінарний формат, що використовується для написання вебзастосунків і дозволяє досягнути фактично нативного перфомансу.
- SharedArrayBuffers — це окремий об’єкт, який дозволяє не лише зберігати бінарні дані, а й ділитись ними з різними потоками. Цей об’єкт доступний лише для застосунків, які працюють у Cross-origin isolated режимі.
Cross-origin isolated режим
Цей режим вмикається, коли ваш сервер разом з фронтендом і всіма необхідними файлами повертає такі заголовки:

Ввімкнувши цей режим, ви відразу отримаєте доступ до SharedArrayBuffer-у, але ваш застосунок матиме ряд обмежень.
Так, наприклад, заголовок ‘Cross-Origin-Embedder-Policy’: require-corp змушує браузер обмежувати завантаження зовнішніх ресурсів та їхню роботу.

Нехай наш основний застосунок знаходиться за адресою a.example, а сервер, з якого нам потрібно завантажити додаткові ресурси (скрипт, картинку та відео) за адресою b.example.
Якщо наш основний вебзастосунок завантажений із заголовком COEP: require-corp, то браузер дозволить завантаження картинки, для якої явно налаштована CORS-політика, що дозволяє завантаження для вебзастосунку з джерелом a.example або *(для всіх).
Також браузер дозволить завантаження скрипту, який має явно описану політику завантаження з інших джерел. Проте завантаження відео браузер заблокує, оскільки немає явного дозволу на завантаження цього відео для вебзастосунку з джерелом a.example.

Схожим чином політика Cross-Origin-Opener-Policy обмежує роботу з діалоговими вікнами. Нехай наш вебзастосунок знаходиться за адресою a.exampl, якщо діалогове вікно, що відкриває вебзастосунок, має таке ж саме походження, то будь-яка комунікація та взаємодія буде без обмежень.
Проте якщо походження буде іншим і адреса буде до прикладу b.example, то браузер заблокує як доступ від вашого вебзастосунку до такого вікна, так і навпаки, від дочірнього діалогового вікна до батьківського застосунку.
Власне через такі обмеження ми не можемо використовувати Cross-origin isolation режим для роботи застосунків всіх типів, бо це створить проблеми для процесів авторизації та онлайн-оплати, для використання зовнішніх ресурсів тощо.
Проте, з іншого боку, через свою підвищену безпеку у цьому режимі доступні такі складні функції, як SharedArrayBuffer.
Вертаючись до WebContainers — як я зазначав вище, по суті — це віртуальна операційна система безпосередньо у браузері. Типова операційна система має надавати можливість працювати з файловою системою, створювати потоки і запускати в них відповідні процеси, а також працювати з мережею.

У WebContainers це все реалізовано за допомогою сучасних функцій браузера. Файлова система побудована на основі SharedArrayBuffer, багатопоточність побудована на WebWrokers, а робота з мережею на ServiceWorkers. Разом це все керується за допомогою WebAssembly
Детальніше про WebContainers
Перше, з чого потрібно почати роботу з WebContainers — це завантаження. WebContainers надають метод boot для цього, який необхідно викликати всього один раз, коли ваш застосунок завантажився.
Після виклику цього методу він поверне нам посилання на об’єкт, що матиме методи, необхідні для подальшої взаємодії з вебконтейнером.
import { WebContainer } from '@webcontainer/api';
window.addEventListener('load', async () => {
webcontainerInstance = await WebContainer.boot();
});
Вебконтейнер через властивість fs надає можливість для роботи з файлами та взаємодіяти з віртуальною файловою системою. Доступними є наступні методи:
- mkdir — створення директорії.
- readdir — вичитування вмісту директорії.
- readFile — вичитування файлу.
- rm — видалення.
- writeFile — запис у файл.
Цих методів цілком вистачає для роботи з поодинокими файлами, проте коли ми працюємо з реальними проєктами, там зазвичай багато файлів. Сам вебконтейнер після завантаження по суті порожній, містить лише файли та код, необхідний для коректної роботи самого вебконтейнера.
Щоб запустити якийсь застосунок всередині вебконтейнера необхідно завантажити в нього усі відповідні файли. Після завантаження об’єкт-екземпляр вебконтейнера містить метод mount, що дозволяє нам завантажувати багато файлів за один раз.
Цей метод очікує, що всі файли будуть описані у вигляді JSON об’єкту, зображеного нижче:
const files = {
'package.json': {
file: {
contents: `
{
"name": "vite-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.0.4"
}
}`,
},
},
};
Після того, як ви виконаєте код:
await webcontainerInstance.mount(files);
Де files — це об’єкт у форматі, зазначеному вище, усі відповідні файли завантажаться у вебконтейнер і будуть готові до використання та взаємодії.
Крім цього вебконтейнер дає нам можливість запускати команди операційної системи за допомогою методу spawn.
webcontainerInstance.spawn('mkdir', ['newProject']);
webcontainerInstance.spawn('ls', ['src', '-l']);
Першим параметром у цьому методі є власне команда, яку потрібно виконати, а наступним параметром є масив параметрів, які потрібно передати у цю команду. Звісно, не абсолютно всі команди, що ви знаєте, коректно тут працюватимуть, — це середовище, що лише за певними параметрами схоже на операційну систему, а не є нею.
Вебконтейнер — це середовище для виконання Node.js застосунків. Саме тому коли ми тільки завантажуємо вебконтейнер, всередині вже є налаштована уся Node.js екосистема, необхідна для роботи.
Зокрема на момент написання статті у вебконтейнері відразу після завантаження є доступною Node.js 16 версії, npm чи yarn, для управління пакетами. Слід зауважити, що і npm, і yarn всередині не є тими самими менеджерами пакетів, які ми використовуємо у нашій повсякденній розробці.

Для коректної роботи всередині вебконтейнера команда Stackblitz була змушена розробити свій власний менеджер пакетів turbo, який з одного боку повністю сумісний з npm чи yarn, але з іншого боку встановлює пакети так, щоб це правильно працювало усередині вебконтейнера.
Зокрема turbo не виконує жодних install-scripts для залежностей, в процесі завантаження виконує ряд оптимізацій та заміняє несумісні речі на поліфіли, що побудовані на WebAssembly.
І остання річ, яка цілковито поламала мені мозок, — це можливість запускати сервер в середині вебконтейнера та взаємодіяти з ним далі. Якщо ваш сервер запускається без додаткових налаштувань, то це може виглядати так:
await webcontainerInstance.spawn('npm', ['run', 'start']);
// Wait for `server-ready` event
webcontainerInstance.on('server-ready', (port, url) => {
// ...
});
У цьому коді ми запускаємо сервер і додаємо обробник на те, коли він запуститься. Сам вебконтейнер дає нам таку можливість. Коли сервер запуститься, то у обробнику ми отримаємо також адресу та порт, за якими можна доступитись до сервера, що працює усередині вебконтейнера.
Оскільки localhost — це ключове слово, зарезервоване системою, то вебконтейнер автоматично створює власну унікальну адресу, яку проксює за допомогою сервіс-воркера всередину себе, тому не лякайтесь, коли в обробнику побачите, що адреса дуже довга і містить символи, які ви точно туди не додавали.
Приклади використання
Далі я підготував кілька відео з прикладами того, як працюють вебконтейнери. У цьому відео є приклад запуску окремих процесів усередині вебконтейнера за допомогою команди spawn:
У другому відео можемо бачити, що використовуючи node.js екосистему, яка встановлена у вебконтейнері за замовчанням, створюється React.js застосунок. Коли він запускається і в обробнику є доступ до адреси, за якою доступний застосунок, — ця адреса передається відповідному iframe, тому бачимо і початкову компоненту React.js:
У третьому відео ми завантажуємо у вебконтейнер невеличкий express.js застосунок, разом з юніт-тестами для нього. Усередині вебконтейнера є можливість не тільки запускати сам застосунок, але й виконувати тести.
Актуальні проблеми
- Власні імплементації Циклу подій, файлової системи, багатопоточності. З одного боку, це необхідний крок, щоб все коректно функціонувало, проте на практиці це створює додаткові проблеми. Так, стандартні задачки на співбесідах на цикл подій і console.log на різних його фазах, у вебконтейнері матимуть інакший вивід, ніж на вашому робочому лептопі. Саме через власні імплементації базових речей, на гітхабі можна побачити велику кількість досить дивних проблем з популярними бібліотеками, які люди пробують використати всередині вебконтейнера.
- Підтримка офлайн-режиму. Попри те, що в офіційній документації вказано, що вебконтейнери можуть працювати офлайн, на ділі це не так. На гітхабі вже є інформація про те, що вебконтейнер має залежності на сервери stackblitz, які не кешуються належним чином, а тому офлайн-режим не працює так, як мав би.
- Закритість коду. Технологія вебконтейнерів не відкрита, оскільки компанія Stackblitz планує її монетизувати. Закритість коду затримує вплив спільноти на цю технологію.
- Крім власне воркерів і асетів, які необхідні для коректної роботи, при встановленні нових пакетів вони фактично встановлюються з серверів Stackblitz задля кращої оптимізації. Це те, що Turbo робить під капотом. Відповідно якщо ми інсталюємо якісь пакети всередині вебконтейнера, вони можуть бути застарілі, так само як і версія Node.js.
- Зсередини вебконтейнера можливі комунікації із зовнішніми сервісами лише з використанням Http-протоколу.
Підсумки
Вебконтейнери — це ще один крок у напряму Веб як платформа, саме міграції типових десктопних застосунків у веб. Через те, що в цій технології ми намагаємося змусити запрацювати в браузері те, чого там ніколи не повинно було бути, використовується багато обхідних рішень, а це створює певні проблеми.
Фахівцям в Stackblitz довелось докласти величезних зусиль, щоб змусити це запрацювати у браузері. Під час розробки технології була дуже тісна співпраця з браузерами і виправлення критичних багів, різних вразливостей. Навіть це вже дуже вагомий вклад у розвиток вебу.
Технологія WebContainers — це точно те, на що варто звернути увагу, оскільки це напрямок, у якому, скоріш за все, браузери розвиватимуться і далі.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
8 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів