Руководство для начинающих по прогрессивным веб-приложениям
Опубликовано: 2022-03-10Прогрессивные веб-приложения могут стать следующим важным событием для мобильного Интернета. Первоначально предложенные Google в 2015 году, они уже привлекли большое внимание из-за относительной простоты разработки и почти мгновенного выигрыша в пользовательском опыте приложения.
Дальнейшее чтение на SmashingMag:
- Строительные блоки прогрессивных веб-приложений
- Основы диалогового дизайна: советы по созданию чат-бота
- Создание первоклассного приложения, использующего ваш веб-сайт
- Создание полноценного веб-приложения в Foundation For Apps
Прогрессивное веб-приложение использует новейшие технологии для объединения лучших веб-приложений и мобильных приложений . Думайте об этом как о веб-сайте, созданном с использованием веб-технологий, но который действует и ощущается как приложение. Недавние достижения в браузере и в доступности сервис-воркеров, а также в Cache и Push API позволили веб-разработчикам разрешить пользователям устанавливать веб-приложения на домашний экран, получать push-уведомления и даже работать в автономном режиме.
Прогрессивные веб-приложения используют гораздо большую веб-экосистему, плагины и сообщество, а также относительную простоту развертывания и обслуживания веб-сайта по сравнению с собственным приложением в соответствующих магазинах приложений. Те из вас, кто занимается разработкой как для мобильных устройств, так и для Интернета, оценят, что веб-сайт можно создать за меньшее время, что API не нужно поддерживать с обратной совместимостью (все пользователи будут запускать одну и ту же версию вашего веб-сайта). код, в отличие от фрагментации версий нативных приложений) и что приложение, как правило, будет легче развертывать и поддерживать .
Почему прогрессивные веб-приложения?
Исследование показало, что в среднем приложение теряет 20% своих пользователей на каждом этапе между первым контактом пользователя с приложением и началом его использования. Пользователь должен сначала найти приложение в магазине приложений, загрузить его, установить, а затем, наконец, открыть. Когда пользователь найдет ваше прогрессивное веб-приложение, он сможет сразу же начать его использовать, избавившись от ненужных этапов загрузки и установки. А когда пользователь вернется в приложение, ему будет предложено установить приложение и перейти на полноэкранный режим.
Тем не менее, нативное приложение определенно не так уж и плохо. Мобильные приложения с push-уведомлениями обеспечивают в три раза больше удержания, чем их аналоги без push-уведомлений, а пользователь в три раза чаще повторно открывает мобильное приложение, чем веб-сайт. Кроме того, хорошо спроектированное мобильное приложение потребляет меньше данных и работает намного быстрее, поскольку некоторые ресурсы находятся на устройстве.
Прогрессивное веб-приложение использует преимущества характеристик мобильного приложения, что приводит к улучшению удержания пользователей и производительности без сложностей, связанных с обслуживанием мобильного приложения.
Случаи применения
Когда следует создавать прогрессивное веб-приложение? Нативное обычно рекомендуется для приложений, к которым, как вы ожидаете, пользователи будут часто возвращаться, и прогрессивное веб-приложение ничем не отличается. Flipkart использует прогрессивное веб-приложение для своей популярной платформы электронной коммерции Flipkart Lite, а SBB использует прогрессивное веб-приложение для онлайн-регистрации, позволяя пользователям получать доступ к своим билетам без подключения к Интернету.
При оценке того, должно ли ваше следующее приложение быть прогрессивным веб-приложением, веб-сайтом или собственным мобильным приложением, сначала определите своих пользователей и наиболее важные действия пользователей. Будучи «прогрессивным», прогрессивное веб-приложение работает во всех браузерах, и каждый раз, когда в браузере пользователя обновляются новые и улучшенные функции и API-интерфейсы, его возможности улучшаются.
Таким образом, нет никаких компромиссов в пользовательском опыте с прогрессивным веб-приложением по сравнению с традиционным веб-сайтом; однако вам, возможно, придется решить, какие функции поддерживать в автономном режиме, и вам придется облегчить навигацию (помните, что в автономном режиме у пользователя нет доступа к кнопке «Назад»). Если ваш веб-сайт уже имеет интерфейс, похожий на приложение, применение концепций прогрессивных веб-приложений только улучшит его .
Если определенные функции необходимы для критических действий пользователя, но пока недоступны из-за отсутствия кросс-браузерной поддержки, лучшим вариантом может быть родное мобильное приложение, гарантирующее одинаковый опыт для всех пользователей.
Характеристики прогрессивного веб-приложения
Прежде чем мы перейдем к коду, важно понять, что прогрессивные веб-приложения имеют следующие характеристики:
- Прогрессивный . По определению, прогрессивное веб-приложение должно работать на любом устройстве и постепенно улучшаться, используя все функции, доступные на устройстве пользователя и в браузере.
- Обнаруживаемый . Поскольку прогрессивное веб-приложение — это веб-сайт, оно должно быть доступно поисковым системам. Это большое преимущество перед нативными приложениями, которые по-прежнему отстают от веб-сайтов по поисковой способности.
- Можно связать . В качестве еще одной характеристики, унаследованной от веб-сайтов, хорошо спроектированный веб-сайт должен использовать URI для указания текущего состояния приложения. Это позволит веб-приложению сохранять или перезагружать свое состояние, когда пользователь добавляет в закладки или делится URL-адресом приложения.
- Отзывчивый . Пользовательский интерфейс прогрессивного веб-приложения должен соответствовать форм-фактору устройства и размеру экрана.
- Похожий на приложение . Прогрессивное веб-приложение должно выглядеть как родное приложение и быть построено на модели оболочки приложения с минимальным обновлением страницы.
- Независимость от подключения . Он должен работать в областях с низким уровнем подключения или в автономном режиме (наша любимая характеристика).
- Возможность повторного вовлечения . Пользователи мобильных приложений с большей вероятностью будут повторно использовать свои приложения, а прогрессивные веб-приложения предназначены для достижения тех же целей с помощью таких функций, как push-уведомления.
- Устанавливаемый . Прогрессивное веб-приложение можно установить на главный экран устройства, что делает его легко доступным.
- свежий . Когда публикуется новый контент и пользователь подключен к Интернету, этот контент должен быть доступен в приложении.
- Безопасный . Поскольку прогрессивное веб-приложение имеет более интимный пользовательский интерфейс и поскольку все сетевые запросы могут быть перехвачены сервисными работниками, крайне важно, чтобы приложение размещалось по протоколу HTTPS, чтобы предотвратить атаки «человек посередине».
Давайте Код!
Наше первое прогрессивное веб-приложение Sky High будет моделировать расписание прибытия в аэропорт. Когда пользователь впервые обращается к нашему веб-приложению, мы хотим показать ему список предстоящих рейсов, полученный из API. Если у пользователя нет подключения к Интернету и он перезагружает веб-приложение, мы хотим показать ему расписание рейсов таким, каким оно было, когда он в последний раз загрузил его с подключением.
Основы
Первая характеристика прогрессивного веб-приложения заключается в том, что оно должно работать на всех устройствах и улучшаться на устройствах и браузерах, которые это позволяют. Поэтому мы создали наш веб-сайт с использованием традиционного HTML5 и JavaScript, который имитирует получение данных из фиктивного API. Во всем приложении мы используем небольшие фрагменты Knockout для обработки наших привязок Model-View-ViewModel (MVVM) — облегченную структуру JavaScript, которая позволяет нам привязывать наши модели JavaScript к нашим представлениям HTML. Мы решили использовать Knockout, потому что он относительно прост для понимания и не загромождает код; однако вы можете заменить его на любой другой фреймворк, например React или AngularJS.
Наш веб-сайт следует рекомендациям Google по материальному дизайну, набору принципов, определяющих дизайн и взаимодействие. Материальный дизайн не только служит единым стандартом для приложений и устройств, но и придает дизайну смысл. Мы использовали материальный дизайн для представления прибывающих в Sky High, чтобы придать нашему прогрессивному веб-приложению внешний вид и функции, характерные для приложения.
Наконец, мы протестировали наше приложение, чтобы убедиться, что оно работает без зависаний и что прокрутка работает плавно. Было показано, что рендеринг без рывков улучшает взаимодействие с пользователем. Стремитесь к рендерингу 60 кадров в секунду.
Для этой демонстрации мы получим статический файл JSON вместо реального API. Это просто для того, чтобы все было просто. В реальном мире вы бы запросили API или использовали WebSockets.
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sky-High Airport Arrivals</title> <link async rel="stylesheet" href="./css/style.css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,600,300italic,600italic" rel="stylesheet" type="text/css"> </head> <body> <header> <div class="content"> <h3>Arrivals</h3> </div> </header> <div class="container"> <div class="content"> <ul class="arrivals-list" data-bind="foreach: arrivals"> <li class="item"> <span class="title" data-bind="html: title"></span> <span class="status" data-bind="html: status"></span> <span class="time" data-bind="html: time"></span> </li> </ul> </div> </div> <script src="./js/build/vendor.min.js"></script> <script src="./js/build/script.min.js"></script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sky-High Airport Arrivals</title> <link async rel="stylesheet" href="./css/style.css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,600,300italic,600italic" rel="stylesheet" type="text/css"> </head> <body> <header> <div class="content"> <h3>Arrivals</h3> </div> </header> <div class="container"> <div class="content"> <ul class="arrivals-list" data-bind="foreach: arrivals"> <li class="item"> <span class="title" data-bind="html: title"></span> <span class="status" data-bind="html: status"></span> <span class="time" data-bind="html: time"></span> </li> </ul> </div> </div> <script src="./js/build/vendor.min.js"></script> <script src="./js/build/script.min.js"></script> </body> </html>
Файл index.html
является относительно стандартным. Мы создали HTML-список и привязали к нему arrivals
нашего свойства View Model с помощью Knockout через атрибут data-bind=“foreach: arrivals”
. arrivals
View Model объявлено в файле page.js
ниже и представлено в модуле Page
. На нашей HTML-странице для каждого элемента в массиве arrivals
мы привязали свойства title
, status
и time
к HTML-представлению.
страница.js
(var Page = (function() { // declare the view model used within the page function ViewModel() { var self = this; self.arrivals = ko.observableArray([]); } // expose the view model through the Page module return { vm: new ViewModel(), hideOfflineWarning: function() { // enable the live data document.querySelector(".arrivals-list").classList.remove('loading') // remove the offline message document.getElementById("offline").remove(); // load the live data }, showOfflineWarning: function() { // disable the live data document.querySelector(".arrivals-list").classList.add('loading') // load html template informing the user they are offline var request = new XMLHttpRequest(); request.open('GET', './offline.html', true); request.onload = function() { if (request.status === 200) { // success // create offline element with HTML loaded from offline.html template var offlineMessageElement = document.createElement("div"); offlineMessageElement.setAttribute("id", "offline"); offlineMessageElement.innerHTML = request.responseText; document.getElementById("main").appendChild(offlineMessageElement); } else { // error retrieving file console.warn('Error retrieving offline.html'); } }; request.onerror = function() { // network errors console.error('Connection error'); }; request.send(); } } })();
Этот файл page.js
предоставляет модуль Page
, который содержит нашу hideOfflineWarning
vm
showOfflineWarning
. View Model ViewModel
— это простой литерал JavaScript, который будет использоваться во всем приложении. Свойство, arrivals
в ViewModel, — это observableArray
Knockout, который автоматически привязывает наш HTML к массиву JavaScript, позволяя нам помещать элементы в наш массив в JavaScript и автоматически обновлять HTML-код страницы.
Функции hideOfflineWarning
и showOfflineWarning
позволяют остальной части нашего приложения вызывать эти функции для обновления пользовательского интерфейса страницы, который показывает, подключены ли мы к сети. showOfflineWarning
добавляет класс loading
к нашему элементу HTML arrivals-list
, чтобы скрыть список, а затем извлекает файл HTML offline.html через offline.html
. Предполагая, что файл был успешно получен ( response.status === 200
), мы добавляем его в наш HTML. Конечно, если мы не используем сервис-воркеры и пользователь не подключен к Интернету, то получить offline.html
будет невозможно, и поэтому пользователь увидит автономную страницу браузера.
Бизнес-логика, из которой мы извлекаем данные из нашего API и привязываем их к нашим моделям представления и представлениям, находится в arrivals.js
и является стандартной функциональностью MVVM с использованием Knockout. В файле arrivals.js
мы просто инициализируем службы и модели представления, которые будем использовать во всем приложении, и предоставляем функцию — Arrivals.loadData()
— которая извлекает данные и привязывает их к модели представления.
Манифест веб-приложения
Давайте сделаем наше веб-приложение более похожим на приложение. Файл манифеста веб-приложения — это простой файл JSON, соответствующий спецификации W3C. С его помощью можно запускать веб-приложение в полноэкранном режиме как отдельное приложение, назначать значок, который будет отображаться при установке приложения на устройство, а также назначать тему и цвет фона для приложения. Кроме того, Chrome на Android заранее предложит пользователю установить веб-приложение с помощью баннера установки веб-приложения. Чтобы отобразить запрос на установку, ваше веб-приложение должно:
- иметь действительный файл манифеста веб-приложения,
- обслуживаться по протоколу HTTPS,
- зарегистрировать действительного сервисного работника,
- были посещены дважды, с интервалом не менее пяти минут между посещениями.
manifest.json
{ "short_name": "Arrivals", "name": "Arrivals at Sky High", "description": "Progressive web application demonstration", "icons": [ { "src": "launcher-icon.png", "sizes": "48x48", "type": "image/png" }, { "src": "launcher-icon-96.png", "sizes": "96x96", "type": "image/png" }, { "src": "launcher-icon-144.png", "sizes": "144x144", "type": "image/png" }, { "src": "launcher-icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "launcher-icon-256.png", "sizes": "256x256", "type": "image/png" } ], "start_url": "./?utm_source=web_app_manifest", "display": "standalone", "orientation": "portrait", "theme_color": "#29BDBB", "background_color": "#29BDBB" }
Давайте разберем этот файл манифеста:
-
short_name
— это удобочитаемое имя приложения. В Chrome для Android это также имя, сопровождающее значок на главном экране. -
name
также является удобочитаемым именем приложения и определяет, как приложение будет отображаться в списке. -
description
предоставляет общее описание веб-приложения. -
icons
определяет массив изображений различных размеров, которые будут служить набором значков приложения. В Chrome для Android значок будет использоваться на заставке, на главном экране и в переключателе задач. -
start_url
— это начальный URL-адрес приложения. -
display
определяет режим отображения по умолчанию для веб-приложения:fullscreen
,standalone
,minimal-ui
илиbrowser
. - Orientation определяет
orientation
веб-приложения по умолчанию:portrait
илиlandscape
. -
theme_color
— это цвет темы по умолчанию для приложения. В Android это также используется для окрашивания строки состояния. -
background_color
определяет цвет фона веб-приложения. В Chrome он также определяет цвет фона экрана-заставки. -
related_applications
не реализован в нашем примере, но используется для указания собственных альтернатив приложений в различных магазинах приложений.
Добавьте ссылку manifest.json
в тег head
файла index.html
:
<link rel="manifest" href="./manifest.json">
Как только пользователь добавит веб-приложение на свой главный экран, он сможет повторно взаимодействовать с вашим приложением сразу же со своего устройства, без необходимости напрямую открывать браузер. Вы можете видеть, что это гораздо больше, чем веб-закладка.
Добавить на главный экран в Chrome для Android из Smashing Magazine на Vimeo.
Сервисные работники
Одним из наиболее интересных аспектов прогрессивных веб-приложений является то, что они могут работать в автономном режиме. С помощью сервис-воркеров можно показать данные, которые были получены в предыдущих сессиях приложения (используя IndexedDB), или, наоборот, показать оболочку приложения и сообщить пользователю, что он не подключен к Интернету (подход, который мы использовали). взято из этой демонстрации). После повторного подключения пользователя мы можем получить последние данные с сервера.
Все это возможно с помощью сервис-воркеров, которые представляют собой сценарии, управляемые событиями (написанные на JavaScript), которые имеют доступ к событиям в масштабе домена, включая сетевые выборки. С их помощью мы можем кэшировать все статические ресурсы, что может значительно сократить сетевые запросы и значительно повысить производительность.
Оболочка приложения
Оболочка приложения — это минимум HTML, CSS и JavaScript, необходимых для работы пользовательского интерфейса. Нативное мобильное приложение включает оболочку приложения как часть своего дистрибутива, в то время как веб-сайты обычно запрашивают это по сети. Прогрессивные веб-приложения заполняют этот пробел, помещая ресурсы и активы оболочки приложения в кэш браузера. В нашем приложении Sky High мы видим, что наша оболочка приложения состоит из верхней панели заголовка, шрифтов и любого CSS, необходимого для элегантного отображения.
Чтобы начать работу с сервис-воркерами, нам сначала нужно создать файл JavaScript нашего сервис-воркера, sw.js
, помещенный в корневой каталог.
sw.js
// Use a cacheName for cache versioning var cacheName = 'v1:static'; // During the installation phase, you'll usually want to cache static assets. self.addEventListener('install', function(e) { // Once the service worker is installed, go ahead and fetch the resources to make this work offline. e.waitUntil( caches.open(cacheName).then(function(cache) { return cache.addAll([ './', './css/style.css', './js/build/script.min.js', './js/build/vendor.min.js', './css/fonts/roboto.woff', './offline.html' ]).then(function() { self.skipWaiting(); }); }) ); }); // when the browser fetches a URL… self.addEventListener('fetch', function(event) { // … either respond with the cached object or go ahead and fetch the actual URL event.respondWith( caches.match(event.request).then(function(response) { if (response) { // retrieve from cache return response; } // fetch as normal return fetch(event.request); }) ); });
Давайте более внимательно посмотрим на нашего сервис-воркера. Во-первых, мы устанавливаем переменную cacheName
. Это используется для определения того, были ли внесены какие-либо изменения в наши кэшированные активы. В этом примере мы будем использовать статическое имя, что означает, что наши ресурсы не изменятся и не потребуют обновления.
self.addEventListener('install', function(e) { // declare which assets to cache }
Событие install
срабатывает на этапе установки сервис-воркера и срабатывает только один раз, если сервис-воркер уже установлен. Таким образом, обновление страницы не приведет к повторному запуску этапа установки. На этапе установки мы можем объявить, какие активы будут кэшироваться. В нашем примере выше мы кэшируем один файл CSS, два файла JavaScript, наш файл шрифтов, наш автономный HTML-шаблон и, конечно же, корень приложения. self.skipWaiting()
заставляет ожидающего работника службы стать активным.
До сих пор мы объявляли наш сервис-воркер, но прежде чем мы увидим, как он вступит в силу, нам нужно сослаться на него в нашем JavaScript. В нашем приложении прописываем его в main.js
// Register the service worker if available. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then(function(reg) { console.log('Successfully registered service worker', reg); }).catch(function(err) { console.warn('Error whilst registering service worker', err); }); } window.addEventListener('online', function(e) { // Resync data with server. console.log("You are online"); Page.hideOfflineWarning(); Arrivals.loadData(); }, false); window.addEventListener('offline', function(e) { // Queue up events for server. console.log("You are offline"); Page.showOfflineWarning(); }, false); // Check if the user is connected. if (navigator.onLine) { Arrivals.loadData(); } else { // Show offline message Page.showOfflineWarning(); } // Set Knockout view model bindings. ko.applyBindings(Page.vm);
Мы также включили два прослушивателя событий, чтобы проверить, изменилось ли состояние сеанса с online
на offline
или наоборот. Затем обработчики событий вызывают различные функции для извлечения данных с помощью метода Arrivals.loadData()
и для включения или отключения автономного сообщения с помощью Page.showOfflineWarning
и Page.hideOfflineWarning
соответственно. Наше приложение также проверяет, находится ли пользователь в данный момент в сети, используя navigator.onLine, и либо извлекает данные, либо показывает предупреждение об офлайне соответственно. И в последней строке main.js
мы применяем привязки Knockout к нашей модели Page.vm
Если мы загрузим наше приложение в первый раз (с помощью Chrome Developer Tools), мы не увидим ничего нового. Однако после перезагрузки мы увидим, что ряд сетевых ресурсов был получен от сервисного работника. Это оболочка нашего приложения.
Автономный тест
Пользователь, запускающий приложение без подключения к Интернету (при условии, что он уже был на странице), просто приведет к отображению оболочки приложения и офлайн-предупреждения — улучшение по сравнению с рыскающим t-rex в Chrome. Как только пользователь установил сетевое соединение, мы отключаем предупреждение и получаем последние данные.
The Guardian применяет особенно интересный подход, когда офлайн-пользователи заходят на его веб-сайт, предоставляя кроссворд:
Всплывающие уведомления
Push-уведомления позволяют пользователям получать своевременные обновления из приложений, которым они доверяют, помогая им повторно взаимодействовать с приложениями. Push-уведомления в Интернете позволяют вам взаимодействовать со своей аудиторией, даже когда браузер закрыт.
Push API поддерживается в браузерах Chrome, Opera и Samsung и находится в стадии разработки в Firefox и Microsoft Edge. К сожалению, нет никаких указаний на то, что эта функция будет реализована в Safari.
Представление
Одна из самых простых побед с помощью сервис-воркеров заключается в том, что мы можем повысить производительность практически без усилий. Сравнение нашего веб-сайта с самим собой до того, как были реализованы сервис-воркеры, до того, как мы получили более 200 КБ при загрузке страницы; теперь он уменьшен до 13 КБ. В обычной сети 3G загрузка страницы заняла бы 3,5 секунды; теперь это занимает 500 миллисекунд.
Эти улучшения производительности являются радикальными, потому что само приложение очень маленькое и имеет ограниченную функциональность. Тем не менее, за счет правильного использования кэширования можно значительно улучшить производительность и воспринимаемую производительность, особенно для пользователей в местах с низкой скоростью подключения.
Маяк
Команда Google Chrome создала инструмент для тестирования прогрессивных веб-приложений. Lighthouse работает на Node.js или в виде плагина для Chrome, его также можно найти на GitHub.
Чтобы запустить тест Lighthouse, ваш веб-сайт должен быть доступен в сети, а это означает, что вы не можете тестировать его на localhost
.
Для начала скачайте пакет npm:
npm install -g GoogleChrome/lighthouse
После установки запустите Chrome (начиная с версии 52):
npm explore -g lighthouse -- npm run chrome lighthouse https://incredibleweb.github.io/pwa-tutorial/
Результат запуска Lighthouse будет виден в командной строке и будет оценивать ваш веб-сайт в соответствии с реализованными вами функциями и свойствами прогрессивного веб-приложения, например, используете ли вы файл manifest.json
или доступна ли ваша страница в автономном режиме. .
Заключение
Эта статья — просто закуска к прогрессивным веб-приложениям. Мы могли бы сделать гораздо больше, чтобы создать то приложение, которое ищут пользователи, будь то поддержка push-уведомлений с помощью Push API, обеспечение возможности повторного взаимодействия с приложением или использование IndexedDB и фоновой синхронизации для улучшения работы в автономном режиме.
Кроссбраузерная поддержка
Это все еще первые дни для прогрессивных веб-приложений, и кросс-браузерная поддержка все еще ограничена, особенно в Safari и Edge. Однако Microsoft открыто поддерживает прогрессивные веб-приложения и к концу года должна реализовать больше функций.
- Сервисные работники и Cache API . Поддерживается в браузерах Chrome, Firefox, Opera и Samsung. В разработке для Microsoft Edge, ожидается, что он будет доступен к концу 2016 года. Рассматривается для Safari.
- Добавить на главный экран . Поддерживается в Chrome, Firefox, Opera, браузере Android и браузере Samsung. Похоже, Microsoft указывает, что прогрессивные веб-приложения будут доступны в виде списков магазинов. Планов на сафари пока нет.
- Нажмите API . В основном поддерживается в браузерах Chrome, Firefox, Opera и Samsung. В разработке в Microsoft Edge. Планов на сафари пока нет.
Если больше разработчиков воспользуются преимуществами функций, предлагаемых прогрессивными веб-приложениями, которые относительно легко внедрить и принесут немедленное вознаграждение, то пользователи предпочтут использовать эти веб-приложения в поддерживаемых браузерах, надеясь убедить других поставщиков браузеров адаптироваться.
Исходный код
Весь исходный код для этого руководства доступен в репозитории Github, а демонстрация доступна на страницах GitHub.