Найти тему

Hummingbird: создаем интернет-приложения при помощи Flutter

Оглавление

Сегодня на Flutter Live мы объявили о нашем эксперименте с запуском Flutter в Интернете. В статье мы расскажем о том, как реализуем этот проект и каких результатов достигли. В конце вы сможете найти ответы на вопросы о Interop и эмбеддинге.

Давайте начнем с краткого обзора архитектуры Flutter. Flutter - это многоуровневая система, в которой действует принцип: чем выше уровень, тем он проще и функциональнее, благодаря использованию маленького кода. Более низкие уровни, напротив, дают больше контроля за счет некоторой сложности. Если верхний уровень не может реализовать запрос разработчика, то можно опуститься на уровень ниже. Разработчик обладает доступом ко всем уровням над Flutter Engine.

                                         Flutter для мобильной архитектуры
Flutter для мобильной архитектуры

Flutter Engine представляет собой библиотеку самого низкого уровня во Flutter, dart:ui. Он не имеет ни малейшего представления о виджетах, физике, анимации или верстке (кроме верстки текста). Все, что он может, это компоновать изображения на экране и превращать их в пиксели. Было бы трудно создавать приложения непосредственно поверх dart:ui. Именно поэтому мы создали более высокие уровни.

Все, что находится выше dart:ui, мы называем "фреймворком". Все, что ниже - это "движок". Фреймворк полностью написан с использованием языка программирования Dart. Большая часть движка написана на C++, части для Android написаны на Java, а части, предназначенные для iOS, написаны на Objective-C. Некоторые базовые классы и функции в dart:ui написаны на Dart и в основном служат для взаимодействия Dart и C++.

Flutter также предлагает систему плагинов. Плагины - это коды, написанные на языке, который имеет прямой доступ к OEM-библиотекам и библиотекам сторонних разработчиков, накопленным мобильными экосистемами с течением времени. Чтобы создать плагин для Android, необходимо воспользоваться Java или Kotlin. Плагин для iOS пишется с использованием Objective-C или Swift.

Привет, интернет!

Интернет платформа развивалась на протяжении десятилетий и сейчас включает в себя множество технологий и спецификаций. Существует несколько общих терминов, используемых для описания больших коллекций связанных между собой функциями HTML, CSS, SVG, JavaScript и WebGL. Для того чтобы запустить Flutter в Интернете, нам необходимо:

  • Скомпилировать код Dart: Flutter написан на Dart, и нам нужно запустить Dart в сети.
  • Выбрать небольшую группу функций Flutter для запуска в интернете. Запускать весь код Flutter будет нецелесообразно в силу того, что отдельные его части ориентированы только на конкретную платформу, например, на Android или iOS.
  • Определиться с набором функций: со временем на веб-платформе накопилось достаточное количество функций, которые просто дублируют друг друга и обладают одинаковыми возможностями. Например, графику вы можете рисовать с помощью HTML+CSS, SVG, Canvas и WebGL.

Dart компилирует JavaScript столько, сколько существует этот язык. Многие важные приложения компилируются из Dart в JavaScript и вполне успешно функционируют сегодня. Стратегия компиляции Flutter опирается на эту же инфраструктуру.

Когда мы начали исследование, мы столкнулись с несколькими вариантами рендеринга пользовательского интерфейса. Мы быстро поняли, что конкретные слои Flutter, которые мы хотим поддерживать, определяют, какие веб-технологии нужно использовать для реализации. Мы создали три прототипа:

1. Только виджеты: Этот прототип реализовал фреймворк виджетов Flutter и предложил набор базовых виджетов макета в качестве основы для создания пользовательских виджетов. При компоновке и расположении он полагался на такие интернет-функции, как flexbox, grid layout, браузерная прокрутка с помощью overflow:scroll и т.д.

2. Виджеты + пользовательский дизайн: Этот прототип включал систему компоновки Flutter (предоставляемую RenderObject), но отображал объекты рендеринга непосредственно на HTML-элементы.

3. Сетевой движок Flutter (Flutter Web Engine): Этот прототип сохранил все слои выше dart:ui и обеспечил реализацию dart:ui, работающую в браузере.

Одна из наиболее ценных особенностей Flutter заключается в том, что его можно перенести на разные платформы. Хотя вы можете (и иногда это поощряется) писать специальный код для конкретной платформы, код, который не должен отличаться на разных платформах, может быть общим. Это позволяет создавать приложения для нескольких платформ с единой кодовой базой.

После попытки загрузить несколько приложений в интернет мы поняли, что прототипы №1 и №2 не смогут обеспечить тот уровень мобильности, который так нравится разработчикам Flutter. Поэтому мы решили выбрать прототип №3, дизайн Flutter Web Engine, поскольку он позволит использовать код на уровне фреймворка между платформами максимальное количество раз.

                                  Flutter для веб-архитектуры (Hummingbird)
Flutter для веб-архитектуры (Hummingbird)

Теперь, когда мы знаем, что хотим реализовать весь API dart:ui, нам нужно выбрать набор веб-технологий, на которых мы будем это создавать. Flutter рендерит пользовательский интерфейс по одному кадру за раз. В каждом кадре Flutter создает виджеты, выполняет верстку и рисует их на экране.

Создание виджетов

Механизм создания виджетов не зависит от среды, в которой работает приложение. Процесс просто инстанцирует объекты в памяти, отслеживает их состояние и при изменении состояния рассчитывает минимальные обновления, необходимые для нижних уровней системы, верстки и рисования. Перенос этой части в интернет был достаточно легким. После того как команда Dart внедрила поддержку super-mixin в dart2js, компилятор практически без проблем создал все виджеты и структуру виджетов в JavaScript.

Верстка

Система верстки немного сложнее. Наибольшую сложность представляла компоновка текста. Все остальное - Center, Row, Column, Stack, Scrollable, Padding, Wrap и так далее - уже заложено фреймворком и поэтому компилируется в сеть без изменений.

Во Flutter вы размещаете абзац текста, создавая объект "Paragraph" и используя метод layout(). К сожалению, в интернете отсутствует прямой API для компоновки текста. Для измерения свойств компоновки текста мы использовали прием, который заключается в том, чтобы заставить браузер выложить текст, а затем считать соответствующие параметры из элементов DOM.

При компоновке абзаца текста Flutter измеряет высоту, ширину, максимальную и минимальную внутреннюю ширину, а также алфавитные и идеографические базовые линии абзаца. Эти свойства показаны ниже:

                                          Атрибуты оформления абзаца
Атрибуты оформления абзаца

Более подробную информацию вы можете найти в инструкции к параграфам Flutter.

Для измерения этих свойств сначала мы помещаем абзац в элемент HTML DOM, а затем считываем размеры элемента. Это заставляет браузер вывести его на экран. Например, чтобы определить ширину и высоту элемента, мы используем команду offsetWidth и ее родственную команду offsetHeight. Для измерения базовой линии мы размещаем абзац в элементе, настроенном на выравнивание с помощью flex row. Рядом с абзацем мы разместим еще один элемент под названием "probe". Поскольку probe выровнен по базовой линии текста, то вызов getBoundingClientRect на нем дает нам базовую линию. Мы используем аналогичные приемы для измерения минимальной и максимальной внутренней ширины.

Рисование

И последнее, но не менее важное: нам нужно нарисовать виджеты. Эта область была наиболее динамичной во время нашего исследования, и она по-прежнему является таковой для нас. К концу фрейма все наши виджеты должны превратиться в пиксели на экране. Это означает, что в браузере они должны быть сведены к некоторой комбинации HTML/CSS, Canvas, SVG и WebGL.

Мы пока не рассматривали WebGL в основном потому, что он низкоуровневый и требует реализации того, что браузеры уже умеют делать, например, верстку текста и растеризацию 2D графики, но также потому, что мы еще не выяснили, как доступность, выделение текста и композиция с другими компонентами, не относящимися к Flutter, могут работать с WebGL.

Один из наших ранних прототипов создавал HTML-элемент для каждого объекта RenderObject. Мы получили многообещающие результаты, однако оказалось, что это слишком серьезное изменение API. Нам пришлось бы поддерживать огромную дельту кода с Flutter, поэтому мы отложили эту идею.

В настоящее время мы одновременно изучаем два подхода:

  • HTML+CSS+Canvas
  • API CSS Paint

HTML+CSS+Canvas

При таком подходе мы разделяем изображения, создаваемые фреймворком, на те, которые можно выразить с помощью HTML+CSS, и те, которые можно выразить с помощью Canvas 2D. Затем мы выводим HTML DOM, который объединяет HTML, CSS и 2D холсты.

Мы предпочитаем HTML+CSS, потому что он опирается на список отображаемых браузером изображений. Это означает, что мы можем оставить оптимизацию растеризации изображений на усмотрение движка рендеринга браузера. То есть, мы можем применять произвольные преобразования, в частности, вращение и масштабирование, не беспокоясь о пикселизации. Мы называем эту версию холста DomCanvas.

Если мы не можем изобразить картинку с помощью HTML+CSS, мы возвращаемся к canvas. Canvas 2D позволяет нам рисовать используя почти все команды рисования Flutter. Если вы сравните Canvas во Flutter с CanvasRenderingContext2D в сети, то обнаружите много сходств. Рисование на холсте достаточно эффективно, поскольку не требует создания изменяемого дерева узлов, которое необходимо поддерживать в течение определенного времени, как это делают HTML DOM или SVG.

Одна из проблем 2D холста заключается в том, что браузеры представляют его как растровое изображение - буфер памяти, хранящий пиксели ширины и высоты. В результате масштабирование холста приводит к пикселизации. Если масштабирование приводит к изменению размера изображения, то нам необходимо изменить размер холста. Мы обнаружили, что выделение холстов довольно дорого, как и изменение их размера. Кроме того, при компоновке нескольких холстов на одной странице браузеру приходится выполнять композицию растров, что также проявляется в наших профилях. Композиция растров работает иначе, чем списки отображений. Вы можете нарисовать несколько списков отображений на одном буфере памяти. Мы называем реализацию двумерного холста Canvas реализацией BitmapCanvas. Мы также изучаем способы сделать растровые холсты более эффективными.

Для отражения непрозрачности, трансформации, смещения, clip rect и других слоев Flutter мы используем обычные HTML-элементы. Например, слой непрозрачности становится элементом <flt-opacity> с CSS-атрибутом opacity, слой трансформации - элементом <flt-transform> с CSS-атрибутом transform, а clip rect - <flt-clip-rect> с overflow: hidden.

Когда все сказано и сделано, рамка отображается на странице в виде дерева HTML-элементов с DomCanvas и BitmapCanvas в качестве узлов листа. Например:

                                         Пример HTML DOM структуры фрейма
Пример HTML DOM структуры фрейма

Эквивалентное дерево слоев Flutter (называемое flow layer) в Flutter Engine будет выглядеть следующим образом:

                                       Пример структуры слоев Flutter Engine
Пример структуры слоев Flutter Engine

Структурно они очень похожи. Самое большое различие заключается в том, что в интернете нам приходится выбирать различные способы визуализации изображений в зависимости от их содержимого.

HTML+CSS+Canvas работает во всех современных браузерах. Однако мы уже заглядываем в будущее:

API CSS Paint

CSS Paint - это новый веб-интерфейс API, который является частью более масштабного проекта Houdini. Houdini - это совместная работа многих производителей браузеров, направленная на предоставление разработчикам определенных частей движка CSS. В частности, API CSS Paint позволяет разработчикам рисовать пользовательскую графику в элементах HTML, когда эти элементы требуют этого. Например, вы можете поручить рисование фона элемента пользовательскому CSS-художнику. Он очень похож на canvas, но имеет следующие важные отличия:

  • Рисование выполняется не основным JavaScript-изолятором, а так называемым paint worklet. Он немного похож на веб-работника тем, что имеет свое собственное пространство памяти. Так называемый paint worlet запускается во время фазы рисования браузера, после фиксации изменений DOM.
  • CSS paint опирается на список отображений, а не на растровое изображение. Это дает нам лучшее из двух миров - эффективность рисования, подобную 2D холсту, и отсутствие пикселизации.
  • В настоящее время CSS paint не поддерживает рисование текста.

На момент написания статьи Chrome и Opera были единственными браузерами, поддерживающими CSS Paint в производстве. Однако другие браузеры находятся на разных стадиях создания своих разработок.

В качестве эксперимента мы поддерживаем CSS Paint API во Flutter for Web, и он уже показывает хорошие результаты, особенно в плане производительности. Наша реализация просто сериализует команды рисования в пользовательское CSS свойство. Paint worklet считывает эти команды и выполняет их. Мы отображаем текст с помощью обычных HTML-элементов <p> и <span>.

Наш текущий механизм сериализации не особенно эффективен - это дерево с вложенными списками, преобразованное в JSON, но в рамках проекта Houdini мы планируем добавить поддержку типизированных массивов. Когда она станет доступной, мы будем кодировать команды рисования в виде типизированных массивов, а не строк JSON. Типизированные массивы являются переносимыми, что означает, что они могут быть переданы из основного изолята в paint worklet по ссылке. Копирование памяти не требуется.

Взаимодействие и внедрение

Вызов библиотек Dart из Flutter

Веб-приложения Flutter имеют полный доступ ко всем существующим библиотекам Dart, которые сегодня работают в Сети.

Вызов библиотек JavaScript из Flutter

Веб-приложения Flutter полностью поддерживают JS-интероп пакеты Dart: package:js и dart:js.

Использование CSS в веб-приложениях Flutter

В настоящее время Flutter предполагает полный контроль за корректностью и производительностью веб-страницы. Например, мы используем только небольшое подмножество CSS, которое следует определенным принципам производительности, таким как https://csstriggers.com/. Размещение произвольного CSS на странице может привести к непредсказуемому поведению Flutter.

Еще одна причина избегать CSS в приложении Flutter for Web заключается в том, что по своей конструкции Flutter должен знать все свойства макета при рендеринге фрейма. CSS действует как "черный ящик". Например, если вы хотите отобразить прокручиваемый список виджетов, вам придется создать HTML для каждого из них и применить необходимые CSS-свойства (например, flex-direction row и overflow: scroll). Затем браузер верстает все и выводит на экран. Код приложения не участвует в процессе верстки.

Наконец, в духе сохранения переносимости кода Flutter на разные платформы, мы избегаем CSS, чтобы иметь возможность запускать один и тот же код на Android и iOS.

Эмбеддинг Flutter в существующие веб-приложения

Мы еще не обеспечили должной поддержки для этого, но намерены изучить этот вопрос в будущем. Пара подходов, которые мы рассматриваем, это <iframe> и shadow DOM.

Встраивание компонентов, не относящихся к Flutter

Мы еще не добавили поддержку для встраивания компонентов, не относящихся к Flutter - пользовательских элементов, компонентов React, компонентов Angular - в веб-приложение Flutter, но мы намерены изучить этот вопрос. Один из возможных вариантов - использовать представление платформы для вставки постороннего контента в веб-приложение Flutter. Один из важных аспектов, который необходимо рассмотреть, - это то, какое влияние может оказать посторонний контент на производительность и корректность работы приложений. Поскольку компоненты, не относящиеся к Flutter, скорее всего, будут содержать произвольный CSS, как упоминалось выше, это может быть проблематично. Необходимы дополнительные исследования.

Портативность

Наша цель - сделать как можно больше фреймворка доступным для сети. Однако это не означает, что произвольное приложение Flutter будет работать в интернете без изменений кода. Веб-приложение Flutter - это все равно веб-приложение; оно находится в "песочнице" браузера и может делать только то, что позволяет браузер. Например, если ваше приложение Flutter использует нативный плагин, который не имеет поддержки в сети, такой как ARCore, вы не сможете запустить приложение в Интернете. Аналогично, нет прямого доступа к файловой системе или низкоуровневой сети.

Текущее состояние

Мы создали достаточно веб-движка для рендеринга большей части Flutter Gallery. Мы не переносили виджеты из Купертино, но все виджеты Material, Material Theming, а также демонстрационные приложения Shrine и Contact Profile работают в Интернете.

Запуск Flutter в компьютерном браузере Chrome

Где находится исходный код?

Мы планируем открыть исходный код этого проекта в ближайшее время и очень хотим поделиться им с сообществом разработчиков. Этот проект начался как исследование внутри внутреннего дерева исходных текстов Google. Мы намерены перенести разработку на GitHub, как только наш код стабилизируется и у нас появится возможность отсоединить его от нашей внутренней инфраструктуры. Тем временем, не удивляйтесь, если вы увидите связанные с интернетом запросы под организацией github.com/flutter!

Заключение

Надеюсь, этот пост дал вам представление о проблемах, которые мы решаем, чтобы Flutter хорошо работал в Сети. Мы приветствуем ваши мысли и идеи.

Переведено на русский язык с сайта: https://medium.com/flutter/hummingbird-building-flutter-for-the-web-e687c2a023a8