Добрый день, уважаемые читатели. Эта статья посвящена проблеме, с которой сталкиваются разработчики более-менее серьезных веб-сайтов, а именно – проблеме автоматического обновления скриптов в браузере пользователя после деплоя. Суть проблемы заключается в том, что одностраничное приложение, загруженное в браузере пользователя, не знает о том, что скрипты были только что изменены, и, следовательно, необходимо обновиться. С точки зрения пользователя это выглядит как непонятные ошибки, возникающие на ровном месте, или как отказ приложения выполнять свои функции. Все это – следствие того, что версия скриптов на сервере изменилась, и приложение просто не может их найти и загрузить. Конечно, все решается нажатием на F5, но в данной статье я покажу, как можно автоматизировать это действие, избавить пользователя от головной боли и сделать это красиво. Данная статья представляет из себя туториал и предназначена как для опытных разработчиков, так и для новичков. Решение, представленное здесь, разработано для VueJS версии 2.6.x, но сами алгоритмы легко могут быть портированы на любой js-фреймворк на основе webpack'а. Статья имеет веб-интерфейс, где вы сможете пощупать решение своими руками, а также исходники на github. Ссылки для ленивых:

→ Веб-интерфейс
→ Проект на github Также, в качестве эксперимента, в статью будет встроено два бонуса. В бонусах я решил добавить полезную на мой взгляд информацию, которая сама по себе не достойна отдельной статьи, но о которой хотелось бы рассказать. Идея решения заключается в введении версионности веб-приложения на уровне сборки. Каждый раз при сборке мы сохраняем информацию о времени сборки, а во время работы приложения проверяем, изменилось ли это время. В случае изменения, обновляем страницу, попутно предупреждая пользователя об изменениях. Решение представлено в виде плагина для Vue, который находится в коде проекта по пути /src/plugins/AutoReload. Отдельного пакета для npm не делал. Демонстрационное веб-приложение представляет коробочную заготовку vue-cli, в которую добавлены стандартные фичи: store, роутер, axios для веб-запросов и dayjs для работы с датами. Интерфейс построен на Element. Первая задача – сохранение информации о версии в сборке. Данную задачу можно выполнять разными способами: Итак, рассмотрим последний вариант в контексте VueJS и Webpack. Как известно, сборщик webpack выполняет серверные js-скрипты, которые собственно и выполняют сборку. В них мы можем встроить свой код, который будет формировать специальный файл, скажем version.json, который будет содержать полезную для нас информацию. В контексте нашей задачи достаточно записать туда дату сборки, но мы пойдем чуть дальше и добавим информацию о типе сборки (development/production/etc.) и версию проекта из файла package.json. Получим примерно такой файл: Чтобы сформировать данный файл, нужно встроить код в скрипт vue.config.js, вот так: Далее код самого генератора version.json: Теперь каждый раз при сборке приложения через команды serve, build или другие будет формироваться новая версия файла version.json, которая может быть прочитана приложением через обычный get-запрос, что нам и требуется. Примечание №1. Файл version.json помещается в папку public, содержимое которой в VueJS копируется в выходной каталог «как есть». В других фреймворках может потребоваться другое целевое расположение. Примечание №2. Файл version.json нужно исключить из git'а, чтобы его изменение не вызывало конфликтов при командной разработке, ведь изменяться он будет постоянно даже в процессе отладки. Исходя из идеи решения плагин будет выполнять следующие функции: Любой уважающий себя плагин должен иметь возможности для настройки. Наш плагин не является исключением, хотя его настройки ограничены рамками задачи, для которой он разрабатывался: Код файла конфигурации можно найти в исходниках или под спойлером. Данный плагин имеет одну особенность, – он использует роутер и уведомления Element'а, поэтому его нужно включать в методе create главного компонента Vue: Главной функцией является check с опциональным параметром href. Именно эта функция вызывается по таймеру или при переходе по маршруту, в этом случае передается адрес целевой страницы. Функция получает текущую версию сборки из файла version.json и сравнивает дату сборки с ранее сохраненным значением. Если значение отличается, пользователю показывается уведомление, затем сохраняется новое значение версии сборки и происходит перезагрузка страницы. Если обновление происходит по таймеру, эмулируется нажатие F5 (window.location.reload(true)). Если же обновление происходит при переходе по маршруту, пользователь направляется на целевую страницу маршрута. Это важно, т.к. исполнение кода никогда не дойдет до next() в хуке роутера после обновления страницы. В принципе на этом статью можно было бы закончить, но мы пойдем чуть дальше и воспользуемся побочными эффектами нашего решения. Я не случайно добавил в файл version.json дополнительную информацию о версии проекта и типе сборки. В реальном проекте эта информация выводилась на специальной «скрытой» странице, в которую можно было попасть, указав ее путь в URL. В демонстрационном проекте страничка с информацией о сборке находится прямо в меню, и там можно увидеть содержимое файла version.json в удобном для человека виде. Это может быть полезно, когда проект имеет кучу версий и веток, которые выкладываются на отдельные стенды. Посмотрев на эту страницу можно увидеть, когда проект был обновлен, и какой тип сборки там использовался. Также туда можно добавить информацию о бэкенде, но в этом проекте его нет. Прошу обратить внимание, что информация об интервале обновления формируется в соответствии с правилами русского языка: «60 секунд», а не «60 секунды». Программисты практически всегда пренебрегают такими мелочами, хотя решение лежит на поверхности и не требует глубоких знаний. Именно этому вопросу посвящен второй бонус статьи, под спойлером. Исходное решение было написано на C# много лет назад, а затем портировано на JS почти в неизменном виде. Я приведу код обоих решений и примеры использования. Исходник на C#: https://pastebin.com/T1PsMy4N Исходник на JS: https://pastebin.com/a4z25b1H Примеры использования: У любого решения есть плюсы и минусы, а также свои особенности и, конечно, простор для дальнейших доработок. Здесь проведу краткий анализ своего решения в плане его ограничений и доработок. Если приложение работает через балансер, и сборка происходит на каждом из веб-серверов отдельно, то, очевидно, версия сборок будет отличаться минутами или секундами. Поэтому, если пользователь по какой-то причине будет перекинут на другой веб-сервер, у него произойдет обновление скриптов из-за различий во времени сборки. Является ли это проблемой? Скорее нет, чем да, потому что балансер, как правило, настроен таким образом, чтобы пользователь всегда направлялся на один из вебов, а не прыгал между ними. Если все-таки такая проблема актуальна для вас, я вижу два варианта решения: сборка на одном сервере или введение проверки на разность дат сборок, чтобы она превышала некий лимит. В плагине решена проблема подгрузки асинхронных маршрутов, но остается нерешенной проблема, когда пользователь производит действие, вызывающее подгрузку асинхронного компонента. С точки зрения браузера это загрузка обычного js-файла с сервера. Но после деплоя и до срабатывания автоматического обновления браузер будет пытаться загрузить несуществующий файл. У данной проблемы нет нормального решения, хотя есть вариант с перехватом/оборачиванием кода подтягивания асинхронных компонентов, чтобы в этот момент также вызывался код проверки изменения версии сборки. В любом случае, максимум, что возможно будет сделать – перезагрузить страницу целиком. Возможна ситуация, когда пользователь заполняет форму, и в процессе заполнения происходит обновление страницы. Данные будут потеряны, равно как и всё содержимое store. С другой стороны, есть гарантия, что пользователь не попытается загрузить старую версию формы. Как вариант, можно не инициировать обновление страницы, а давать пользователю выбор: продолжить работу или обновить страницу. Честно говоря, не припомню сайты, которые публикуют какие-либо changelog'и, но было бы интересно внедрить такой функционал в модуль автообновления. Хотя лично меня всегда бесит, когда тот же телеграм пишет мне в личку список новых возможностей. Каждый раз я удаляю тот чат, но он все равно оживает при следующем обновлении. А для веба это, наверное, и не нужно вовсе. Если вы хотите увидеть плагин в действии, воспользуйтесь демонстрационным стендом. Нажмите на кнопку, которая сбрасывает сохраненную в плагине версию, что приведет к срабатыванию автоматического обновления после истечения минутного таймера или при переходе в другой раздел меню. Надеюсь, что данная статья поможет вам решить проблему с автоматическим обновлением скриптом или хотя бы даст направление исследования. Также прошу оценить, насколько вам зашли «бонусы», и стоит ли их использовать в дальнейшем? Понравилась статья? Посмотрите другие: Разработчик