Що таке веб-стандарти та як працює веб-браузер?
Розповім я вам одну історію. Якось я розробляв черговий компонент з вибором дати для нашої системи проектування. Компонент складається з поля для текстового введення та спливаючого календаря, що відображається при натисканні мишею по цьому полю. Потім календар, що випливає, можна закрити, клацнувши поза ним або якщо вибрано дату.
У більшості реалізацій такої логіки клацнути поза календарем застосовувалися конкретні слухачі подій, прикріплені до DOM. Але я хотів зробити наш компонент з вибором дати доступним так, щоб можна було відкривати календар із вкладками і таким же чином його закривати. Крім того, слухачі подій можуть вступати в конфлікт один з одним, якщо ви розмістите на сторінці кілька таких компонентів для вибору дати.
А якщо просто покластися на нативні події фокусування і розмиття, а не зв’язуватися з клацаннями поза полем? У такому разі, природно, підтримуються вкладки, події торкання та клацання, і все це вже реалізовано у браузері. Єдина проблема, яку доведеться вирішити в даному випадку, така: що робити, якщо ви клацаєте спливаючим полем, але без вибору дати. Тоді фокус переміщається на календар, в результаті з полем для введення відбувається розмиття, і зрештою спливаюче поле ховається.
Тут я задумався, а чи є спосіб зробити клацання, але не зрушувати фокус. Нашвидкуруч погуглив, я знайшов, як це зробити: придушити подію mouseDown, за замовчуванням спрацьовує для спливаючого поля. Все так: одного рядка вистачило, щоб усі клацання працювали, але поле для введення тексту залишалося у фокусі.
Здавалося: і все, рішення знайдено. Давайте рухатись далі. Але щось мене зупиняло. Чому саме mouseDown, а не mouseUp зупиняє фокус, але пропускає клацання? Це частина якогось чинного стандарту? Чи працюватиме це в кросбраузерному режимі? Бібліотека React Testing, за допомогою якої у нас робилися інтеграційні тести, також не підтримувала цієї можливості, тому мені довелося б змінювати функцію симуляції.
Що таке веб-стандарт?
Добре, оскільки відповіді зі Stack Overflow мені було недостатньо, то де ще шукати інформацію про поведінку веб-браузерів, як не в стандартних стандартах?
Ймовірно, ви чули про W3C , він же – Консорціум Всесвітньої Павутини. Це міжнародне співтовариство, яке розробляє відкриті стандарти для Інтернету. W3C прагне гарантувати, що всі керуються одними і тими самими нормами, і нам не доведеться підтримувати десятки різних оточень. Якщо зайдете на їхній сайт, то знайдете список усіх стандартів , над якими вони працюють.
Давайте заглянемо в один документ, де можуть бути відповіді на наші запитання – UI Events Standard(Стандарт подій інтерфейсу користувача). У цьому документі вказано потік подій DOM, визначено список подій та порядок їх виконання. Якщо вам здавалося, що веб-стандарти – це нудні, каламутні простирадла тексту, через які доводиться продиратися – одразу переходьте до розділу DOM Event Architecture (Архітектура подій DOM). Тут пояснено сплив подій, розказано про захоплення подій, а сам текст забезпечений веселими картинками. Проте це дуже конкретний документ, саме такий, яким і має бути стандарт. Вас здивує його якість, він насправді дуже якісно написаний, рясніє прикладами та рекомендаціями.
Також в ньому визначено і нашу подію mouseDown, зокрема, як вона діє за умовчанням:
“У багатьох реалізаціях подія мусила вживатися, щоб ініціювати ряд контекстно-залежних дій, що виконуються за умовчанням. Такі дії можна запобігти, якщо ця подія скасована. До таких дій, що виконуються за замовчуванням, можуть входити: початок перетягування, де об’єкт, що перетягується, – це зображення або посилання; початок виділення тексту, т. д. Крім того, в деяких реалізаціях передбачається можливість панорамування за допомогою миші, що активується, коли середня кнопка миші втоплена в момент диспетчеризації події, що минув.
Добре, для нашої події передбачені деякі дії за умовчанням, але в самій події фокусу немає нічого специфічного, оскільки фокус дійсно залежить від браузера. Давайте познайомимося з ними.
Знайомство з браузерними двигунами
Сучасний браузер – це дуже складний зразок софту з базою коду приблизно десятки мільйонів рядків . Тому браузер зазвичай ділиться на кілька частин.
Щоб визначити, де саме визначаються події фокусу, потрібно зробити огляд, який би дозволив зрозуміти, за що відповідає кожна частина. Почнемо з Chromium і документації щодо його проектування Getting Around The Chrome Source Code . Як бачите, тут безліч модулів і логіка, за яку відповідають модулі, у всіх модулів різна.
Давайте коротко розберемо їх усе, щоб зрозуміти, як взаємодіють усі ці компоненти.
- chrome: це базова програма з логікою запуску, інтерфейсом користувача і всіма вікнами. Він містить проекти для chrome.exe та chrome.dll. Тут ви також знайдете ресурси, наприклад, іконки чи курсори.
- content: це серверна частина програми, що обробляє комунікацію з дочірніми процесами.
- net: це мережна бібліотека, яка допомагає виконувати запити на веб-сайти.
- base: це місце для загального коду, що поділяється між усіма субпроектами. Сюди можуть бути включені такі речі, як операції над рядками, узагальнені утиліти, тощо.
- blink: це двигун рендерингу, що відповідає за весь конвеєр відображення, у тому числі, за дерева DOM, стилі, події, інтеграцію з V8.
- v8: остання більшість браузерного движка на JavaScript. Завдання цього компонента – компілювати JavaScript у нативний машинний код.
Як бачите, браузер складається з кількох незалежних частин, які спілкуються один з одним через API. З погляду розробника зазвичай найцікавіші Blink та V8. Дії за замовчуванням, що визначаються браузером, не входять до складу V8, але у Blink всі вони мають бути визначені та реалізовані. Але перш ніж переходити до бази коду Blink, давайте розберемося, як веб-браузери працюють з точки зору користувача.
Конвеєр візуалізації
Уявіть, що ви вводите в браузер адресу домену, а потім браузер вибирає та завантажує набір ресурсів: HTML, CSS, файли JS, зображення, ярлики. Але що має статися далі?
На першому кроці HTML-файли проходитимуть синтаксичний розбір і перетворюватимуться на дерево DOM . DOM – це не тільки внутрішнє уявлення сторінки, але й API, що відкривається JavaScript для виконання запитів і модифікації рендерингу за допомогою системи так званих «прив’язок».
Наступний крок після побудови дерева DOM – це обробка стилів CSS. Для цієї мети в браузерах є інструмент аналізу CSS, що збирає модель стильових правил. Побудувавши модель стильових правил, можна об’єднати їх із набором стилів, що задаються за замовчуванням (що надаються браузером) і обчислити остаточне значення кожної стильової властивості для кожного елемента DOM. Цей процес називається роздільною здатністю (або перерахунком) стилів .
У наступній частині описує макет , потрібно визначити візуальну геометрію для всіх елементів. На даному етапі кожен елемент отримує координати (x та y), ширину та висоту. Двигун компонування обчислює всі зони виходу за межі та веде їх облік – яка частина елемента буде видимою, а яка ні.
Коли у нас будуть всі координати для всіх елементів, настане час для відображення . Для цієї операції ми скористаємося координатами з попереднього кроку та кольорами із стильових правил, після чого скомбінуємо на їх основі список інструкцій з малювання. Важливо малювати елементи у правильному порядку, щоб, накладаючись одне одного, вони зберігали правильну «поверховість». Цей порядок можна змінити за допомогою стильового правила z-index.
Давайте виконаємо наші інструкції для відображення за списком і перетворимо їх на растр колірних значень. Цей етап називається розтеризацією . Саме зараз ми візьмемо наші зображення та декодуємо їх у карту бітів.
Пізніше розтеризована карта бітів зберігатиметься у пам’яті GPU. На даному етапі в роботу включаються бібліотеки, що абстрагують апаратне забезпечення та виклики до OpenGL і DirectX під Windows. Коли GPU отримує команди для відображення растрового малюнку, на дисплеї відображаються відповідні пікселі.
Ось у нас є всі найважливіші частини рендерингового конвеєра. Але що станеться, якщо прокрутити сторінку або застосувати на ній деяку анімацію? Насправді рендеринг не статичний. Для представлення змін використовуються анімаційні кадри. Кожен кадр повністю відображає стан контенту у конкретний час. Найскладніше при організації цього процесу – досягти потрібної продуктивності. Щоб анімація йшла гладко, необхідно генерувати щонайменше 60 кадрів на секунду. Провернути весь конвеєр 60 разів на секунду буде практично неможливо, особливо на повільних пристроях.
А що, якщо ми не будемо щоразу рендерити все заново, а надамо спосіб інвалідувати елемент на конкретному етапі. Скажімо, якщо ви динамічно змінюєте колір кнопки, то браузер помітить її вузол як інвалідний, і в наступному кадрі анімації її буде відображено заново. Якщо нічого не зміниться, ми зможемо повторно використовувати старий кадр.
Цей спосіб зручний для оптимізації невеликих динамічних змін вмісту. Давайте подумаємо, що робити, якщо потрібно змінювати великі області. Наприклад, після повного прокручування сторінки всі пікселі на новому екрані будуть іншими. Тому сторінка декомпонується на рівні, кожен із яких раструється незалежно. Рівень може бути дуже малий і представляти лише один вузол a DOM. Потім всі ці рівні комбінуватимуться в іншому потоці, який називається потік композитора . За такої оптимізації не доводиться повторно раструвати все відразу, достатньо виконувати ці операції для невеликих шарів, а потім правильно компонувати їх один з одним.
Отже, ми коротко розглянули, що робить Blink і як виглядає конвеєр рендерингу. Давайте заглибимося в код.
Навігація по базі коду Blink
Здається, ми наближаємося до фінішної прямої. Давайте відкриємо репозиторій Blink і розглянемо його.
Швидко стає зрозуміло, що, хоча ми змогли значно конкретизувати наше вихідне питання, масив коду поки що все одно занадто великий, щоб вручну знайти в ньому конкретний рядок, що відповідає за запобігання фокусу.
Давайте спробуємо пошукати в Google на ім’я події:
mousedown site:https://chromium.googlesource.com/chromium/blink/+/master/Source
Приводить нас до файлу EventHandler , де можна знайти деталі реалізації для багатьох вхідних подій. У тому числі, той рядок, який нас найбільше цікавить.
bool swallowEvent = !dispatchMouseEvent(EventTypeNames::mousedown, mev.innerNode(), m_clickCount, mouseEvent);
Значення , що повертається dispatchMouseEvent
означає «продовжувати обробляти так, як задано за замовчуванням», тому, у разі використання preventDefault
, swallowEvent
дорівнює true
.
Трохи нижче розташований виклик події фокусування, який спрацьовує лише у випадку, якщо swallowEvent == false
.
swallowEvent = swallowEvent || handleMouseFocus(MouseEventWithHitTestResults(mouseEvent, hitTestResult), sourceCapabilities);
Можете дослідити не тільки подію фокусування, але і всі дії, які задаються за умовчанням для події mouse down, у тому числі виділення, перетягування і роботу з повзунком. Також тут реалізуються подія відпускання кнопки миші та подія подвійного клацання.
Gecko і WebKit
Діставшись сюди, ми вже встигли чимало покопатися у вихідному коді браузерів і дуже непогано уявляємо собі їхню структуру. Так чому б не перевірити Firefox чи взагалі Safari. Двигун браузера Firefox називається Gecko, а двигун Safari – WebKit.
У Gecko також передбачена оглядова сторінка для розробників, тому легко усвідомити основні концепції цього движка. Спираючись на досвід роботи з Chrome, ви знайдете тут акуратний файл EventStateManager на 6000 рядків коду, в якому для подій задаються дії та стандартні поведінки.
WebKit– Це браузерний двигун від Apple, що використовується в Safari та інших продуктах Apple. Blink від Chrome є форком WebKit, тому у них багато спільного, і мені не важко знайти реалізації подій в їх версії файлу EventHandler .
Тепер, переконавшись, що можна безпечно придушити подію, що мусила встановити, я можу повернутися до початку цієї посади і спокійно доопрацювати інструмент для вибору дати.
Заключення
Разом ми пройшли шлях після простої проблеми до введення в веб-стандарти і розбору деталей, що стосуються реалізації браузерів.
Нехай вас не лякає прихована складність наявних модулів, що навіть стосуються браузера або компілятора. На Вас чекає захоплююча подорож. Ймовірно, ви легко знайдете, що тут можна було б покращити, і, що набагато важливіше, на власному досвіді набудете унікальних знань про те, як саме влаштовано. Особисто я перелопатив гору документації, працюючи над цим дослідженням, і рекомендую всім діяти так само. У всіх браузерах надається чудова документація, тому я справді не впевнений, а чи потрібно для роботи ще щось, крім неї.
Переклад статті “What are Web Standards and how does Web Browser work?“