Pull to refresh
VK
Building the Internet

Руководство для начинающих по прогрессивным веб-приложениям и фронтенду

Reading time 20 min
Views 58K
Original author: Angelos Chalaris

image


Разрабатывать веб-фронтенд, придерживаясь JavaScript-экосистемы, всех этих новомодных штучек и пафосных фреймворков, может быть пугающим занятием, если не сказать больше. Я давно уже хотел окунуться в это, и наконец собрался с духом. К концу этой статьи, надеюсь, вы узнаете что-нибудь новое, или хотя бы чуть больше отточите свои навыки веб-разработки. Какая ирония, что длина статьи и обширное количество информации тоже могут отпугивать. Но я очень надеюсь, что вы найдёте время осилить хотя бы интересующие вас главы. В конце каждого раздела есть абзац TL;DR, так что вы можете быстро ориентироваться в содержании.


RESTful API


Первое, о чём я хочу рассказать, это концепция RESTful API. Термин REST, или RESTful API, всплывает во многих беседах между веб-разработчиками, и на то есть веская причина. REST (REpresentational State Transfer, передача состояния представления) API и веб-сервисы предоставляют простой способ взаимодействия с архитектурой бэкенда без необходимости разбираться в этой самой архитектуре.


Проще говоря, как фронтенд-разработчик вы будете взаимодействовать с одной или несколькими конечными точками (то есть URI), являющимися частью API, расположенного на сервере, в кластере или каком-то ином бэкенде, который кто-то создал для вас. Если API хорошо проработан, то в получите ясную и понятную документацию по запросам, созданию и редактированию ресурсов на бэкенде (при условии, что вы авторизованы), без необходимости писать код или лезть в базу, лежащую на упомянутом бэкенде.


RESTful API бывают самыми разными. Наиболее популярные из них возвращают JSON-объекты, которыми можно легко манипулировать посредством JavaScript на стороне клиента, что позволяет фронтенд-разработчикам эффективно работать с одними лишь частями View и Controller паттерна MVC (Model-View-Controller).


TL;DR: RESTful API очень популярны и предоставляют фронтенд-разработчикам возможность взаимодействовать с ресурсами в вебе, сосредоточившись на разработке фронтенда и не беспокоясь об архитектуре.


AJAX


AJAX (Asyncronous JavaScript And XML) существует уже немало лет, и каждый разработчик так или иначе использовал его в своей работе (большинство из нас — посредством jQuery). Здесь я не будут углубляться в устройство AJAX, поскольку в сети есть сотни источников получше, но хочу отнять у вас минутку времени, чтобы просто восхититься теми возможностями, которые эта технология даёт фронтенд-разработчикам.


С помощью AJAX мы можем запрашивать ресурсы из одного одного или нескольких мест (или локально, если страница расположена на том же сервере, что и запрашиваемый URI) и в любое время, без замедления работы веб-приложений или необходимости начать отрисовку всех данных. Фактически, мы можем не грузить любой контент на страницу, а затем просто запрашивать его, как только будет загружена пустая HTML-страница. В сочетании с RESTful API получается невероятно гибкое, высокопортируемое и удобное в сопровождении решение для веб-приложений.


TL;DR: AJAX — очень мощный инструмент, который в сочетании с RESTful API позволяет создавать по-настоящему динамичные веб-приложения, которые быстро загружаются и отображают контент из ресурсов в вебе.


Получение контента из веба


Поначалу создание вашей первой веб-страницы может показаться довольно трудной задачей, так что будем идти постепенно. Начнём с получения контента из RESTful API с помощью AJAX-вызовов.


Первым делом нужно найти высококачественный API, желательно такой, который возвращает JSON. Вот для примера несколько ресурсов, которые выводят placeholder-контент и пригодны для создания примера веб-приложения:


  • JSON Placeholder — placeholder-текст, выдаваемый в JSON-формате, подходящий для многих вариантов использования. Очень прост для начинающих, идеально подходит для заполнения макета (mockup) веб-приложения, вроде того, что мы с вами сделаем.
  • Unsplash It — placeholder-контент не будет полным без изображений, и именно отсюда их надо брать. Документация очень понятная и простая в применении, так что можно сразу начинать пользоваться.
  • Random User Generator  — ещё один высококачественный ресурс, предоставляющий сгенерированные пользовательские профили, которые можно сконфигурировать под свои нужды. Здесь есть куча настроек, но для целей статьи нужно совсем немного.

Мы воспользуемся этими тремя ресурсами, но вы можете прошерстить список в конце статьи в поисках более интересных API, пригодных для генерирования placeholder-контента для вашей страницы.


Первое, что нужно сделать, прежде чем начать писать код, это посетить конечную точку одного из API и посмотреть, что вы будете получать. Давайте отправим GET-запрос в Random User Generator, просто для проверки. Вы получите что-то подобное:


image


Это называется JSON-файл (JavaScript Object Notation), который должен выглядеть для вас знакомым, если вы когда-либо использовали объекты в JavaScript. Давайте запросим тот же ресурс программным способом:


// Отправляем 'GET'-запрос на заданный URL и после его завершения исполняем callback-функцию
function httpGetAsync(url, callback) {
  var xmlHttp = new XMLHttpRequest();
  xmlHttp.onreadystatechange = function() {
    if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
      callback(xmlHttp.responseText);
  };
  xmlHttp.open('GET', url, true);
  xmlHttp.send(null);
}

Именно это и делает приведённый код, который занимает всего 10 строк (9 без комментария). Скармливаем соответствующую URL конечную точку API и функцию callback, и мы увидим какой-то контент. Допустим, вы создали пустую HTML-страницу и залинковали вышеприведённый JS-код. Теперь просто выполним её в браузерном инструментарии для разработчиков:


httpGetAsync(
  'https://randomuser.me/api/',
  function(e){console.log(e)}
);

Вы получите результат, аналогичный предыдущему, только теперь он будет обёрнут в двойные кавычки. Естественно, ведь это строковое значение. И что ещё естественнее, теперь мы можем конвертировать его в JS-объект посредством JSON.parse(), а затем использовать так, как предполагалось.


Мы только что получили от API какой-то контент и вывели его в консоли. Прежде чем пойти дальше, давайте немного изучим этот API, чтобы впоследствии эффективнее его использовать.


В частности, я бы сосредоточился на параметрах seed и results, которые можно встроить в наш URL, чтобы каждый раз получать тот же набор пользователей. Для их добавления нужно просто добавить после URL знак ?, а затем {parameter_name}={value}, разделяя параметры с помощью &. Полагаю, все об этом знают, но всё же нужно было об этом упомянуть. В этой статье я также воспользуюсь параметром nationalities, чтобы быть уверенным, что в HTML не будет не-латинских символов, которые усложнят мне жизнь. Отныне и впредь, моей целевой конечной точкой для получения пользователей будет эта, она поможет мне получить тех же 25 пользователей, и всех из США.


TL;DR: Чтобы начать разрабатывать веб-приложение, нужно найти высококачественный JSON API. С помощью 10 строк JavaScript-кода можно легко запрашивать ресурсы у RESTful API, которые выдают случайных пользователей, и потом конвертировать их в JavaScript-объекты.


Представление и головоломка с CSS-фреймворком


Решив задачу получения какого-то контента от API, мы теперь можем отрисовать его, чтобы показать пользователю. Но работа с CSS-стилями и селекторами может быть той ещё головной болью, так что многие прибегают к помощи CSS-фреймворка. Весьма популярен Bootstrap, он хорошо задокументирован, имеет большое и приятное сообщество, и богат своими возможностями. Но я воспользуюсь mini.css, который разработал сам и знаю лучше, чем Bootstrap. Подробнее об этом фреймворке можете почитать в других моих статьях.


Инструкции по использованию классов и селекторов фреймворка применимы только к mini.css, но с очень небольшими правками они будут верны и для Bootstrap (считайте представление веб-приложения своей домашней работой, потому что здесь я не буду вдаваться в подробности относительно CSS).


Прежде чем мы сможем что-то вывести на экран, нам нужно создать оболочку приложения. Ничего особенного, только панель навигации и маленький заголовок, ну ещё может быть футер. Я воспользуюсь элементом <header>, который уже присутствует в mini.css, но вы можете решить эту задачу так, как вам удобнее. Также я добавлю в панель навигации .drawer, который может быть заменён какими-нибудь кнопками, и, наконец, <footer>, чтобы приложение выглядело чисто и аккуратно:


image


Я буду понемногу показывать свой код (после добавления карточки пользователя), но сначала скажу, почему использование CSS-фреймворка является хорошей идей. Попросту говоря, когда вы строите для веба, то существует слишком много тестовых вариантов, и некоторые комбинации устройства/ОС/браузера получаются более своеобразными, чем другие. CSS-фреймворк работает с этим особенностями по своему усмотрению, хотя в то же время предоставляет вам базовый каркас, который поможет создать адаптивное приложение (responsive application). Адаптивность крайне важна в разработке для мобильных устройств, так что почитайте об этом побольше.


Допустим, нас всё устраивает в оболочке приложения, давайте перейдём к отрисовке данных, полученных от Random User Generator. Я не собираюсь усложнять эту процедуру и выведу для каждого пользователя имя, изображение, ник, почту, местоположение и день рождения. Но вы можете придумать свой набор позиций. Не забудьте обратиться к ключу .results вашего объекта после парсинга JSON-данных, потому что в него всё обёрнуто. Чтобы всё выглядело красиво, я воспользуюсь замечательным пакетом иконок Feather, а также классом .card из mini.css и кое-какими CSS-трюками.


Пример приложения с карточками случайно сгенерированных пользователей.


Теперь наше приложение динамически заполняется контентом, который мы выводим на экран с помощью JavaScript и HTML. Мы только что создали наше первое представление (View), являющееся причудливым способом сказать, что мы создали визуальное отображение данных, запрошенных у API.


TL;DR: Отзывчивость и стили крайне важны для любого веб-приложения, так что будет хорошей идеей использовать CSS-фреймворк для создания простой HTML-оболочки приложения, а затем отрисовки с помощью JavaScript данных, полученных от API.


JavaScript-библиотеки


Наш макет веб-приложения работает хорошо, но JavaScript-код, особенно метод renderUsers(), получился довольно запутанным и выглядит очень трудным в сопровождении. К счастью, есть хороший способ сделать всё то же самое, но с помощью инструмента, которым сегодня пользуются все клёвые пацаны — React. Если кто-то из вас о нём не слышал: это очень мощная JavaScript-библиотека, сильно облегчающая отрисовку и обновление страницы, при этом повышая скорость её работы с помощью виртуального DOM и методики, известной как diffing. В сети есть куча толковых публикаций на этот счёт.


Преобразовать наш код в React не сложно. Фактически, нужно лишь вынести цикл из процесса отрисовки, чтобы можно было извлекать функцию, которая отрисовывает по одному пользователю за раз, а затем итерировать по нашему массиву пользователей, отрисовывая их как раньше. Здесь несколько специфических для React моментом, вроде того факта, что имя нашей новой функции должно начинаться с прописной буквы, мы должны передавать её единственный аргумент props, а элементы из списка должны быть привязаны к ключам. Звучит громоздко, но на самом деле это совсем не трудно соблюдать.


Также React позволяет использовать JSX, благодаря чему мы можем писать HTML внутри JavaScript без необходимости оборачивать его в кавычки. Так что наш код может стать ещё немного чище. Однако помните, что придётся сделать некоторые преобразования, например из class в className. Но со временем вы привыкните, а отладочные сообщения в консоли действительно очень помогают в решении таких проблем.


Я также взял на себя смелость преобразовать три инлайненых SVG в их собственные функции, так что теперь весь процесс отрисовки выглядит так:


// Функциональный компонент для иконки почты.
function SvgMail(props){
  return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#424242" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>;
}
// Функциональный компонент для иконки календаря.
function SvgCalendar(props){
  return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#424242" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>;
}
// Функциональный компонент для иконки прикрепления к карте.
function SvgMapPin(props){
  return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#424242" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>;
}
// Функциональный компонент для иконки пользовательской карточки.
function UserCard(props){
  var user = props.user;
  return <div className="col-md-offset-1 col-lg-offset-2">
    <div className="card fluid" id={"user_"+user.login.username}>
      <div className="section row">
        <div className="col-sm-2 col-md-1">
          <img src={user.picture.medium} alt={user.login.username} className="user-image" />
        </div>
        <div className="col-sm">
          <h4 className="user-name">{user.name.title} {user.name.first} {user.name.last}
            <small>{user.login.username}</small>
          </h4>
        </div>
      </div>
      <div className="section">
        <p className="user-email"> <SvgMail />&nbsp;&nbsp;{user.email}</p>
      </div>
      <div className="section">
        <p className="user-birth"> <SvgCalendar />&nbsp;&nbsp;{user.dob.split(" ")[0].split("-").reverse().join("/")}</p>
      </div>
      <div className="section">
        <p className="user-location"> <SvgMapPin />&nbsp;&nbsp;{user.location.city}, {user.location.state}</p>
      </div>
    </div>
  </div>;
}
// Отрисовка списка пользователей в виде карточек.
function renderUsers(){
  var userCards = users.map(
    function(user, key){
      return <UserCard user={user} key={key}/>;
    }
  );
  ReactDOM.render( <div>{userCards}</div> ,contentArea);
}

Мы просто извлекли из предыдущего кода функциональный компонент, сделав его многократно используемой сущностью. Стоит взглянуть, что происходит под капотом, когда Babel преобразует HTML, предоставленный по вызовам React.createElement():


function SvgMapPin(props) {
  return React.createElement(
    'svg',
    { xmlns: 'http://www.w3.org/2000/svg', width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', stroke: '#424242', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' },
    React.createElement('path', { d: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z' }),
    React.createElement('circle', { cx: '12', cy: '10', r: '3' })
  );
}

C другой стороны, если вы хотите начать с более лёгкой версии React, без JSX и некоторых более причудливых и сложных вещей, то я очень рекомендую обратиться к Preact, потому что это прекрасная альтернатива, которая может уменьшить время загрузки на одну-две секунды.


TL;DR: Преобразование с помощью React неструктурированных вызовов HTML-отрисовки в функциональные компоненты — задача простая, и результат получается гораздо удобнее в сопровождении. При этом разработчик получает больше контроля над своим кодом, и повышается степень повторного использования.


Добавляем второе представление (view)


В большинстве веб-приложений больше одного представления, так что этим мы и займёмся. Воспользуемся другими двумя API, упомянутыми в начале, чтобы создать второе представление, которое динамически заполняется и содержит текст и изображения, при перезагрузке страницы каждый раз новые. Если вы читали внимательно, то сделаете это без подсказок, но я очерчу направление действий:


  • Мы будем отправлять GET-запрос на адрес https://jsonplaceholder.typicode.com/comments?postId={id}, что позволит получить 5 сегментов текста за раз в JSON-формате, инкрементируя {id}.
  • Также мы будем отправлять GET-запросы на адрес https://unsplash.it/800/800?image={id}, при этом будем каждый раз получать какую-то картинку. Для этого воспользуемся кодом, который будет генерировать случайный {id} для каждого запроса. В моём Codepen-примере я также добавил массив неправильных значений {id}, которые я извлёк из API, так что у нас не будет ни одного ответа без изображения.
  • После выполнения обоих запросов будем создавать карточку для каждого поста, случайным образом приписывая пользователя из списка в качестве автора, и затем отрисуем с помощью React.
  • Также добавим кнопку переключения между двумя представлениями, чтобы иметь возможность перейти к списку пользователей. Я добавлю её в выпадающее меню, но вы можете делать на свой вкус.

После аккуратного кодирования всего упомянутого и последующего совершенствования, у вас получится нечто, схожее с Singe Page Application (SPA): https://codepen.io/chalarangelo/pen/zzXzBv


TL;DR: Чтобы ваш макет выглядел как настоящее веб-приложение, достаточно добавить второе представление и некоторые интерактивные элементы.


Загружаем ещё больше контента


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


// Получает степень прокрутки страницы.
function getScrollPercent() {
  return (
    (document.documentElement.scrollTop || document.body.scrollTop) 
    / ( (document.documentElement.scrollHeight || document.body.scrollHeight) 
    - document.documentElement.clientHeight) 
    * 100);
}

Метод подсчитывает все кейсы и возвращает значение в диапазоне [0,100] (включительно). Если забиндить на window события scroll и resize, то можно быть уверенным, что когда пользователь достигнет конца страницы, то будет подгружен ещё контент. Вот как выглядит наше веб-приложение после добавления бесконечной прокрутки в представление с постами.


TL;DR: Бесконечная прокрутка — главное свойство любого современного веб-приложения. Она легка в реализации и позволяет динамически подгружать контент по мере необходимости.


Примечание: Если вы всё читаете и следуете всем рекомендациям, то сделайте 10-минутный перерыв, потому что следующие несколько глав довольно сложны и требуют больше усилий.


Манифест веб-приложения


Прежде чем мы сможем сказать, что действительно сделали веб-приложение (не говоря уже о прогрессивном веб-приложении), нам нужно поработать с файлом manifest.json, который состоит из кучи свойств нашего приложения, включая name, short_name, icons. Манифест предоставляет клиентскому браузеру информацию о нашем веб-приложении. Но прежде чем мы им займёмся, нам нужно с помощью npm и настройки React поднять приложение и запустить на localhost. Полагаю, что с обеими вещами вы уже знакомы и не столкнётесь с трудностями, поэтому переходим к манифесту.


По моему опыту, чтобы получилось работающее приложение, вам понадобится заполнить так много полей, что я предлагаю воспользоваться Web App Manifest Generator и заполнить, что хотите. Если у вас есть время, чтобы сделать аккуратную иконку, то в этом вам поможет Favicon & App Icon Generator.


После того как всё сделаете, ваш манифест будет выглядеть так:


{
  "name": "Mockup Progressive Web App",
  "short_name": "Mock PWA",
  "description": "A mock progressive web app built with React and mini.css.",
  "lang": "en-US",
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#1a237e",
  "icons": [
    {
      "src": "\/android-icon-48x48.png",
      "sizes": "48x48",
      "type": "image\/png",
      "density": "1.0"
    },
    {
      "src": "\/android-icon-72x72.png",
      "sizes": "72x72",
      "type": "image\/png",
      "density": "1.5"
    }
  ]
}

Создав манифест, мы «отполировали» наше веб-приложение, сделав его поведение таким, как нужно. Но это только начало пути по превращению нашего веб-приложения в прогрессивное.


TL;DR: Файл manifest.json используется для определения конкретных свойств веб-приложения, прокладывая нам путь к созданию прогрессивного веб-приложения.


Сервис-воркеры (Service Workers)


Последним шагом является создание сервис-воркеров. Это одно из ключевых требований к прогрессивному веб-приложению, чтобы оно в какой-то степени работать автономно. Хотя сервис-воркеры используются уже давно, документация для них выглядит всё ещё слишком технической и сложной. Но в последнее время эта тенденция начала меняться. Если хотите больше почитать о прогрессивных веб-приложениях и сервис-воркерах, обратитесь к источникам в конце статьи.


Мы будем создавать самый простой вариант сервис-воркера, позволяющий кэшировать ответы на запросы, передавая их по мере доступности. Сначала создадим файл service-worker.js, который и будет нашим сервис-воркером.


Теперь давайте разберёмся с тремя основными событиями, с которыми нам придётся работать:


  • install генерируется при первой загрузке и используется для выполнения начальной установки сервис-воркера, вроде настройки оффлайн-кэшей.
  • activate генерируется после регистрации сервис-воркера и его успешной установки.
  • fetch генерируется при каждом AJAX-запросе, отправленном по сети, и может использоваться для обслуживания закэшированных ресурсов (особенно полезно при отсутствии сети).

Первое, что нам нужно сделать при установке, это воспользоваться CacheStorage Web API для создания кэша для веб-приложения и хранения любого статичного контента (иконок, HTML, CSS и JS-файлов). Это очень просто сделать:


// caches — это глобальная переменная только для чтения, являющаяся экземпляром CacheStorage
caches.open(cacheName).then(function(cache) {
  // Что-нибудь делаем с кэшем
});
// Источник: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/open

Можно быстро создать простой обработчик события install, который кэширует наши index.html, JavaScript-файл и manifest.json. Но сначала нужно задать имя кэша. Это позволит нам отделить версии тех же файлов или данных от оболочки приложения. Здесь мы не будем вдаваться в подробности относительно кэшей, но в конце статьи есть полезные ссылки. Помимо имени кэша, нужно определить, какие файлы будут кэшироваться с помощью массива. Вот как выглядит обработчик install:


var cacheName = 'mockApp-v1.0.0';
var filesToCache = [
  './',
  './index.html',
  './manifest.json',
  './static/js/bundle.js'
];

self.addEventListener('install', function(e) {
  e.waitUntil(caches.open(cacheName)
    .then(function(cache) {
      return cache.addAll(filesToCache)
        .then(function() {
          self.skipWaiting();
        });
      }));
});

Меньше чем в 20 строках мы сделали так, что наше веб-приложение использует кэш для своих ресурсов. Позвольте пояснить оду вещь. В ходе разработки наш JS-код компилируется в файл ./static/js/bundle.js. Это одна из причуд нашего development/production-окружения, и мы разберёмся с ней на следующем этапе.


Обработчик activate тоже довольно прост. Его главная задача: обновлять кэшированные файлы, когда что-то меняется в оболочке приложения. Если нам нужно обновить любые файлы, то нам придётся изменить cacheName (желательно в стиле SemVer). Наконец, обработчик fetch проверит, хранится ли в кэше запрошенный ресурс. Если он там есть, то будет передан из кэша. В противном случае ресурс будет запрошен как обычно, а ответ будет сохранён в кэше, чтобы его можно было использовать в будущем. Соберём всё вместе:


self.addEventListener('activate', function(e) {
  e.waitUntil(caches.keys()
    .then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName)
          return caches.delete(key);
      }));
  }));
  return self.clients.claim();
});

self.addEventListener('fetch', function(e) {
  e.respondWith(caches.match(e.request)
    .then(function(response) {
      return response || fetch(e.request)
        .then(function (resp){
          return caches.open(cacheName)
            .then(function(cache){
              cache.put(e.request, resp.clone());
              return resp;
          })
        }).catch(function(event){
          console.log('Error fetching data!');
        })
      })
  );
});

Мы построили сервис-воркер, но его нужно зарегистрировать изнутри веб-приложения, чтобы воспользоваться его преимуществами. Достаточно добавить несколько строк кода в наш основной JS-файл, и мы готовы превратить наше веб-приложение в прогрессивное:


if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('./service-worker.js')
    .then(function() { console.log('Registered service worker!'); });
}

После всех настроек, откройте в Chrome Dev Tools окно Application и посмотрите, всё ли работает как нужно. Сервис-воркер должен правильно зарегистрироваться, активироваться и запуститься. Если пролистаете вниз, поставите галочку Offline и обновите, то начнёт работать оффлайн-версия страницы, использующая закэшированные ресурсы.


image


Мы только что превратили наше веб-приложение в прогрессивное, но разработка ещё не закончена. Последним шагом будет сборка для production.


TL;DR: Сервис-воркеры позволяют веб-приложениям настраивать кэши и использовать их для загрузки ресурсов без использования сети, превращая веб-приложения в прогрессивные.


Сборка для production


Всё, что мы сделали, прекрасно работает на localhost, но осмотр production-файлов после быстрого выполнения npm run build обнаруживает кучу ошибок, которые нужно исправить. Прежде всего, многие файлы неправильно залинкованы на/из HTML-страницы. Для решения этой проблемы нужно добавить одну строку в package.json:


"homepage" : "."

После пересборки и открытия HTML-страницы в браузере мы видим, что большая часть заработала правильно. Добавив строку со свойством "homepage", мы сказали системе собрать скрипт так, чтобы в ссылках все пути были изменены на ., то есть теперь они являются относительными путями.


Следующая проблема: наш сервис-воркер указывает на неправильные JS-файлы, потому что static/js/bundle.js больше не существует. Если внимательнее посмотреть на собранные файлы, то станет понятно, что наш JS был преобразован в файл main.{hash}.js, где часть {hash} отличается для каждой сборки. Нам нужно иметь возможность загружать из сервис-воркера файл main.js. Следовательно, нужно переименовать его, но этого сломает ссылку внутри index.html, чего мы не хотим. Для решения обеих проблем воспользуемся сборочными инструментами из реестра npm — renamer и replace. Также нам нужно минифицировать наш service-worker.js, поскольку по умолчанию он не слишком компактен, потому что является частью папки public, так что воспользуемся ещё и утилитой uglify-js.


npm install --save-dev renamer
npm install --save-dev replace
npm install --save-dev uglify-js

Три простые команды, и мы получаем нужные нам инструменты. Пользоваться ими сравнительно легко после ознакомления с документацией, так что я продолжу и покажу, как выглядит свойство "scripts" в моём package.json после создания всех необходимых скриптов.


"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build && npm run build-rename-replace && npm run build-sw",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "build-sw" : "npm run build-replace-sw-refs && npm run build-minify-sw",
    "build-rename-replace": "npm run build-rename && npm run build-replace",
    "build-rename": "npm run build-rename-js && npm run build-rename-css",
    "build-rename-js": "renamer --regex --find main\\.\\w+\\.js --replace main.js build/static/js/*.js",
    "build-rename-css": "renamer --regex --find main\\.\\w+\\.css --replace main.css build/static/css/*.css",
    "build-replace": "npm run build-replace-js && npm run build-replace-css",
    "build-replace-js": "replace main\\.\\w+\\.js main.js build/index.html",
    "build-replace-css": "replace main\\.\\w+\\.css main.css build/index.html",
    "build-replace-sw-refs": "replace './static/js/bundle.js' './static/js/main.js','./static/css/main.css' build/service-worker.js",
    "build-minify-sw" : "uglifyjs build/service-worker.js --output build/service-worker.js"
  }

Давайте разберёмся, что тут происходит:


  • build-rename запускает два скрипта, каждый из которых меняет имена JavaScript- и CSS-файлов, которые были созданы с желаемыми наименованиями (main.js и main.css соответственно).
  • build-replace запускает два скрипта, каждый из которых меняет меняет ссылки на JavaScript- и CSS-файлы на переименованные версии.
  • build-rename-replace собирает предыдущие две команды в одну.
  • build-sw обновляет в сервис-воркере ссылку на static/js/bundle.js, которая теперь указывает на новый файл main.js, а также добавляет ссылку на main.css. Затем минифицирует сервис-воркер.
  • Наконец, build собирает вместе всё вышеперечисленное в наш процесс сборки по умолчанию, так что все файлы попадают в папку build и готовы к выкладыванию на сайт.

Теперь при запуске npm run build должна генерироваться готовая к production версия нашего прогрессивного веб-приложения, которую можно хостить где угодно, просто копируя файлы из папки build.


TL;DR: Создание веб-приложения для production является интересной проблемой, требующей использования сборочных инструментов и разрешения скриптов. В нашем случае достаточно было обновить имена и ссылки.


Последние шаги


Это был трудный путь, но теперь наше прогрессивное веб-приложение готово к production. Прежде чем поделиться им с миром, выложив куда-нибудь на Github, хорошо бы оценить его качество с помощью инструмента Lighthouse, который выставляет приложению баллы и выявляет проблемы.


После прогона моей development-сборки на localhost я обнаружил несколько моментов, требующих исправления, вроде того, что у картинок нет атрибутов alt, внешние ссылки не имеют атрибута rel="noopener", а само веб-приложение не имеет географической привязки (landmark region), что было легко исправлено с помощью HTML-тега <main>. Исправив всё, что мог, я получил такой результат в Lighthouse:


image


Как видите, приложение получилось хорошо оптимизированным и качественным. Теперь я могу с гордостью выложить его на Github. Вот ссылка на законченное приложение; а вот ссылка на исходный код. Я взял на себя смелость добавить индикаторы Online/Offline, но вы можете их выкинуть. Теперь я могу установить приложение на смартфон и показать друзьям!


image


image


image


TL;DR: Прежде чем релизить веб-приложение, стоит проверить его качество с помощью Lighthouse и оптимизировать, насколько возможно.


Вот и всё мы создали с нуля прогрессивное веб-приложение. Надеюсь, вы чему-то научились. Спасибо за уделённое время!


Ссылки


  • MDN’s Getting started with AJAX — отличный ресурс для тех, кто хочет больше узнать об AJAX и как его использовать в веб-приложениях.
  • Если выбираете JSON API, то вас могут заинтересовать эти:
    UI Names — ещё один ресурс для генерирования случайных пользователей с большим количеством настроек.
    Robohash — генерирует аватары роботов на основании заданной строки. Удобно при создании пользовательских профилей или placeholder-изображений.
    Adorable Avatars  — генерирует смешные аватары на основании заданной строки. Настроек чуть меньше, чем в Robohash, но задачу выполняет.
  • Из огромной экосистемы CSS-фреймворков можно выделить такие популярные продукты, как Bootstrap, Semantic UI, Bulma и Foundation. Предлагаю и свою альтернативу, mini.css, если вам нужно что-то более лёгкое.
  • Для UI-иконок обычно берут FontAwesome, но я рекомендую Feather, более лёгкие и минималистичные.
  • Если хотите больше узнать о diffing виртуального DOM, то очень рекомендую почитать эту статью.
  • Среди всех современных JavaScript-библиотек, React является самой популярной. Но хороши также Preact и VueJS, выбирайте, что больше нравится.
  • Манифесты веб-приложений хорошо задокументированы на сайтах MDN и Google Developers. Также можете воспользоваться Web App Manifest Generator, если хотите легко и быстро сгенерировать manifest.json.
  • Говоря о сервис-воркерах и прогрессивных веб-приложениях, я очень рекомендую создавать своё первое приложение так, как описано здесь и здесь. Также обратите внимание на жизненный цикл сервис-воркера. А если хотите заняться этой темой ещё плотнее, почитайте Indicating Offline (я его использовал для создания Online/Offline-индикаторов) и Offline Recipes for Service Workers.
  • Относительно CacheStorage Web API я рекомендую почитать эту статью, а также а также держать под рукой документацию MDN.
  • Если хотите оценить своё веб-приложение и исправить найденные проблемы, воспользуйтесь Lighthouse и Mobile Speed Test.
  • Наконец, изучите исходный код и демку на Github.
Tags:
Hubs:
+27
Comments 130
Comments Comments 130

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен