Руководство для начинающих по прогрессивным веб-приложениям и фронтенду
- среда, 2 августа 2017 г. в 03:12:48
Разрабатывать веб-фронтенд, придерживаясь JavaScript-экосистемы, всех этих новомодных штучек и пафосных фреймворков, может быть пугающим занятием, если не сказать больше. Я давно уже хотел окунуться в это, и наконец собрался с духом. К концу этой статьи, надеюсь, вы узнаете что-нибудь новое, или хотя бы чуть больше отточите свои навыки веб-разработки. Какая ирония, что длина статьи и обширное количество информации тоже могут отпугивать. Но я очень надеюсь, что вы найдёте время осилить хотя бы интересующие вас главы. В конце каждого раздела есть абзац TL;DR, так что вы можете быстро ориентироваться в содержании.
Первое, о чём я хочу рассказать, это концепция 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 (Asyncronous JavaScript And XML) существует уже немало лет, и каждый разработчик так или иначе использовал его в своей работе (большинство из нас — посредством jQuery). Здесь я не будут углубляться в устройство AJAX, поскольку в сети есть сотни источников получше, но хочу отнять у вас минутку времени, чтобы просто восхититься теми возможностями, которые эта технология даёт фронтенд-разработчикам.
С помощью AJAX мы можем запрашивать ресурсы из одного одного или нескольких мест (или локально, если страница расположена на том же сервере, что и запрашиваемый URI) и в любое время, без замедления работы веб-приложений или необходимости начать отрисовку всех данных. Фактически, мы можем не грузить любой контент на страницу, а затем просто запрашивать его, как только будет загружена пустая HTML-страница. В сочетании с RESTful API получается невероятно гибкое, высокопортируемое и удобное в сопровождении решение для веб-приложений.
TL;DR: AJAX — очень мощный инструмент, который в сочетании с RESTful API позволяет создавать по-настоящему динамичные веб-приложения, которые быстро загружаются и отображают контент из ресурсов в вебе.
Поначалу создание вашей первой веб-страницы может показаться довольно трудной задачей, так что будем идти постепенно. Начнём с получения контента из RESTful API с помощью AJAX-вызовов.
Первым делом нужно найти высококачественный API, желательно такой, который возвращает JSON. Вот для примера несколько ресурсов, которые выводят placeholder-контент и пригодны для создания примера веб-приложения:
Мы воспользуемся этими тремя ресурсами, но вы можете прошерстить список в конце статьи в поисках более интересных API, пригодных для генерирования placeholder-контента для вашей страницы.
Первое, что нужно сделать, прежде чем начать писать код, это посетить конечную точку одного из API и посмотреть, что вы будете получать. Давайте отправим GET-запрос в Random User Generator, просто для проверки. Вы получите что-то подобное:
Это называется 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-объекты.
Решив задачу получения какого-то контента от API, мы теперь можем отрисовать его, чтобы показать пользователю. Но работа с CSS-стилями и селекторами может быть той ещё головной болью, так что многие прибегают к помощи CSS-фреймворка. Весьма популярен Bootstrap, он хорошо задокументирован, имеет большое и приятное сообщество, и богат своими возможностями. Но я воспользуюсь mini.css, который разработал сам и знаю лучше, чем Bootstrap. Подробнее об этом фреймворке можете почитать в других моих статьях.
Инструкции по использованию классов и селекторов фреймворка применимы только к mini.css, но с очень небольшими правками они будут верны и для Bootstrap (считайте представление веб-приложения своей домашней работой, потому что здесь я не буду вдаваться в подробности относительно CSS).
Прежде чем мы сможем что-то вывести на экран, нам нужно создать оболочку приложения. Ничего особенного, только панель навигации и маленький заголовок, ну ещё может быть футер. Я воспользуюсь элементом <header>
, который уже присутствует в mini.css, но вы можете решить эту задачу так, как вам удобнее. Также я добавлю в панель навигации .drawer
, который может быть заменён какими-нибудь кнопками, и, наконец, <footer>
, чтобы приложение выглядело чисто и аккуратно:
Я буду понемногу показывать свой код (после добавления карточки пользователя), но сначала скажу, почему использование 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-код, особенно метод 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 /> {user.email}</p>
</div>
<div className="section">
<p className="user-birth"> <SvgCalendar /> {user.dob.split(" ")[0].split("-").reverse().join("/")}</p>
</div>
<div className="section">
<p className="user-location"> <SvgMapPin /> {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-отрисовки в функциональные компоненты — задача простая, и результат получается гораздо удобнее в сопровождении. При этом разработчик получает больше контроля над своим кодом, и повышается степень повторного использования.
В большинстве веб-приложений больше одного представления, так что этим мы и займёмся. Воспользуемся другими двумя API, упомянутыми в начале, чтобы создать второе представление, которое динамически заполняется и содержит текст и изображения, при перезагрузке страницы каждый раз новые. Если вы читали внимательно, то сделаете это без подсказок, но я очерчу направление действий:
https://jsonplaceholder.typicode.com/comments?postId={id}
, что позволит получить 5 сегментов текста за раз в JSON-формате, инкрементируя {id}
.https://unsplash.it/800/800?image={id}
, при этом будем каждый раз получать какую-то картинку. Для этого воспользуемся кодом, который будет генерировать случайный {id}
для каждого запроса. В моём Codepen-примере я также добавил массив неправильных значений {id}
, которые я извлёк из API, так что у нас не будет ни одного ответа без изображения.После аккуратного кодирования всего упомянутого и последующего совершенствования, у вас получится нечто, схожее с 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-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 и обновите, то начнёт работать оффлайн-версия страницы, использующая закэшированные ресурсы.
Мы только что превратили наше веб-приложение в прогрессивное, но разработка ещё не закончена. Последним шагом будет сборка для production.
TL;DR: Сервис-воркеры позволяют веб-приложениям настраивать кэши и использовать их для загрузки ресурсов без использования сети, превращая веб-приложения в прогрессивные.
Всё, что мы сделали, прекрасно работает на 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:
Как видите, приложение получилось хорошо оптимизированным и качественным. Теперь я могу с гордостью выложить его на Github. Вот ссылка на законченное приложение; а вот ссылка на исходный код. Я взял на себя смелость добавить индикаторы Online/Offline, но вы можете их выкинуть. Теперь я могу установить приложение на смартфон и показать друзьям!
TL;DR: Прежде чем релизить веб-приложение, стоит проверить его качество с помощью Lighthouse и оптимизировать, насколько возможно.
Вот и всё мы создали с нуля прогрессивное веб-приложение. Надеюсь, вы чему-то научились. Спасибо за уделённое время!