Перейти к содержимому

Как обновить страницу без перезагрузки javascript

  • автор:

LiveReload — обновление javascript без полной перезагрузки страницы (на примере mithril)

Не так давно я начал пользоваться такой полезной штукой как livereload (для тех, кто не знает, что это — есть статья на хабре). Livereload отслеживает изменения в коде веб-приложения и перегружает страницу в браузере при необходимости. При этом со стилями Livereload поступает умнее и заменяет их на лету без перезагрузки, что выглядит волшебно.

К сожалению с javascript такой номер не прокатывает — Livereload не знает как гуманно заменить только изменившиеся скрипты и перегружает всю страницу. Это особенно печалит если использовать инструмент вроде mithril, в котором представление (читай — html) задается так же в javascript. Если я меняю модель или контроллер, то тут все понятно, но если я меняю класс у дива в представлении (скажем, подбирая правильное сочетание bootstrap классов), то перезагрузка страницы кажется излишней — ну вот же, я поменял одну функцию, просто перерисуй view с ее помощью!

В целом не страшно, конечно (работали же как-то без этого раньше), но почему бы не сделать работу еще немного удобнее?

Для тех, кто торопится
  • репозиторий с демонстрацией — склонировать, npm install, gulp serve

Анимация того, что получилось в итоге

Постановка

У нас есть чистые функции, отображающие модель на представление (читай — html). Нам нужно сделать так, что если меняется такая функция, то мы должны в браузере подгрузить новую версию, подменить исходную и, в случае с Mithril, вызвать m.redraw().

Самый простой способ подмены — запихать все такие функции в глобальный объект, а в местах их прежнего обитания расставить обращения к этому объекту. Что-то типа такого:

//file1.js window.MyObj.MyFn = function (c) < return m("h1", c.text) >//file2.js var Page = < controller: function () < this.text = "Hello"; >, view: function (c) < return window.MyObj.MyFn(c); >> 

Теперь мы можем перезагружать один только file1.js и дергать m.redraw(), после чего представление будет перерисовано. При этом текущее состояние системы (в нашем случае хранящееся в контроллере Page) сохранится.

Однако вручную делать такое разделение не хочется — во-первых нарушается целостность компонента (довольно удобно при разработке менять только один файл, настраивая и поведение и внешний вид компонента), во-вторых есть уже написанный код, в-третьих пришлось бы больше объяснять новым разработчикам. Значит нужно парсить исходный код и извлекать нужные нам функции.

  1. как найти нужные нам функции в существующем коде и извлечь их в отдельный скрипт
  2. как научить LiveReload обновлять этот скрипт без перезагрузки страницы
Извлечение view-функций
  • в теле функции могут использоваться идентификаторы (переменные, функции, и пр.) доступные через замыкание, а не только те что передаются в аргументах функции
  • некоторые функции не надо выносить даже если они носят имя view
  • некоторые функции надо выносить даже если они не носят имя view
var model = new Model(); //какой-то метод модели, по совпадению называется view model.view = function () < this.viewed = true; >//метод представления, хотя и не называется view function formatDate(date) < return m("span.date", [ m("span.year", date.getFullYear()), formatMonth(date), (date.getDate()+1) ]) >var Page1 = < //каноничный mithril-овский метод view //formatDate и model доступны через замыкание, и еще m - глобально //date, в свою очередь, объявляется внутри самой функции view: function() < var date = new Date(); return m("h1", [model.title(), " today is ", formatDate(date)]); >> 

Для решения проблемы 1 парсинга кода регекспами будет явно недостаточно, нужен синтаксический разбор. К счастью, есть библиотека esprima, которая разбирает переданный ей js-код и выдает синтаксическое дерево (можете поиграться здесь) как обычный json. Обход такого дерева не должен составить труда, надо только разобраться со всеми возможными типами узлов дерева, чтобы не пропустить какие-то случаи. Например запись

var a = 5; 

совсем не то же самое что

var a; a = 5; 

(соответственно результаты парсинга вот и вот)

Теперь несложно и функцию найти, и все ее зависимости. Тела функций вынимаем и складируем отдельно, в исходном коде помещаем вызов глобальной функции, и собираем код обратно с помощью библиотеки escodegen.

Для решения задач 2 и 3 пригодилось бы что-то вроде аттрибутов C# или аннотаций Java — какой-то способ пометить нужные функции. Т.к. в javascript такого способа не предусмотрено, пришлось придумать — пусть аттрибутом будет строка, являющаяся первым выражением в теле функции. И если значение аттрибуты «__stateless», то функцию надо извлекать, а если «__ignore» — то не надо.

var model = new Model(); //какой-то метод модели, по совпадению называется view model.view = function () < "__ignore"; this.viewed = true; >//метод представления, хотя и не называется view function formatDate(date)

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

Плагин к LiveReload

Теперь, когда мы умеем доставать нужные нам функции и сохранять их отдельным js-файлом, надо научить LiveReload обновлять его без перезагрузки всей страницы.

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

function Plugin() < this.reload = function(path) < console.log("reloaded", path); return true; >> window.LiveReload.addPlugin(Plugin) 

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

Для перезагрузки мы будем удалять существующий элемент script и добавлять в DOM новый, причем к URL скрипта каждый раз будет добавляться текущее время — чтобы предотвратить кэширование.

 doReloadScript: function (scriptNode) < var oldSrcBase = scriptNode.src.split("?")[0], parent = scriptNode.parentNode, newNode = this.window.document.createElement("script"); parent.removeChild(scriptNode); newNode.src = [oldSrcBase, new Date().getTime()].join('?'); parent.appendChild(newNode); >, 
Встраивание в процесс сборки

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

Я не буду описывать создание плагина для gulp, все делалось строго по туториалам и примерам других плагинов (типа gulp-coffee и gulp-concat), ничего необычного. В итоге gulpfile.js выглядит примерно так:

. other requires var changed = require('gulp-changed'); var extract = require('gulp-livereload-mithril'); var server; //используется двумя gulp-задачами, поэтому вынесено // собирает проект и оповещает livereload-сервер (если запущен) gulp.task('compile', function () < // Начинаем обработку coffee-файлов. // Допустим у нас их два - main.coffee и dashboard.coffee gulp.src("src/**/*.coffee") // ->main.coffee, dashboard.coffee // компилируем в js .pipe(coffee()) // -> main.js, dashboard.js // наш плагин - извлекаем view-функции .pipe(extract()) // -> main'.js, dashboard'.js, st8less.js // проверяем какие файлы действительно поменялись по сравнению со старыми версиями. // допустим поменялся только st8less.js .pipe(changed("public", < hasChanged: changed.compareSha1Digest >)) // -> st8less.js // копируем его в public .pipe(gulp.dest("public")) // -> st8less.js // оповещаем LiveReload сервер об изменениях .pipe(server ? server.notify() : gutil.noop()); >); // запускает наш сервер, LiveReload-сервер и следит за изменениями // чуть что - вызывает задачу compile gulp.task('serve', ['compile'], function () < server = gls.new('./server.js'); server.start(); gulp.watch(SRC, ['compile'] /* no need to notify here*/); >); 

Обратите внимание на использование gulp-changed. Если мы меняем только main.coffee, то на выходе получаем обновленный main.js и st8less.js, причем если мы меняли view-функцию, то main.js по факту будет точно таким же. Но время изменения у main.js все равно поменяется, и в результате LiveReload перезагрузит всю страницу. Чтобы этого не случилось, необходимо сравнить фактическое содержимое, что и делает плагин gulp-changed.

Плагины для gulp и LiveReload лежат в отдельном репозитории — gulp-livereload-mithril. Он, в свою очередь, ссылается на библиотеку st8less описанную выше.

Неявная подгрузка нового скрипта

Наш плагин создает новый js-файл (st8less.js), и надо сослаться на него из html-страницы. Можно было попросить пользователя сделать это самостоятельно, но я подумал: все равно я меняю пользовательские js-файлы, почему бы не добавить в один из них простой document.write?

Так и было сделано, но этого оказалось недостаточно. Если, скажем, мы добавим document.write в начало main.js, а main.js где-то в середине уже использует вынесенные в функции, то мы получим ошибку, т.к. свежедобавленный элемент script еще не начал подгружать наш скрипт.

Надо как-то подгрузить указанный скрипт здесь и сейчас, и другого способа, кроме как послать синхронный ajax-запрос, я не нашел. Добавляемая в начало одного из скриптов конструкция выглядит так:

(function loadScriptSynchronously() '); var req = new XMLHttpRequest(); req.open('GET', path, false); req.send(); var src = req.responseText eval(src) >.call()); 

При желании можно этот ужас отключить, передав плагину . Тогда придется добавлять тэг script вручную.

Итог

Задача оказалась вполне разрешимой, причем приведенное решение можно применить не только для mithril, но и для других подобных случаев — на ум приходят react и angular.js 1.x (зачастую html-верстка для директив помещается прямо в js-код директивы).

    esprima не поддерживает полностью ES 6, т.е. если в вашем коде встречаются, скажем, генераторы, плагин использоваться не удастся.
    Решение: предварительно превращать ES6-код в ES5

 m "pre", JSON.stringify(someValueIWantToWatch) 
Где посмотреть:

Спасибо за внимание, надеюсь было полезно.

Update URL without reloading the page using JavaScript

In this post, we will learn how to update the URL without reloading the page using JavaScript. We will see how to use the History API to manipulate the browser history and the URL, and how to handle popstate events to update our page content.

Updating the URL of a web page without reloading it is a common task that we may encounter in web development. For example, we may want to change the URL to reflect the current state of the page, such as filtering, sorting, or pagination. However, changing the URL of a web page usually triggers a page reload, which can be undesirable for various reasons. For instance, reloading the page can cause flickering, performance issues, or loss of user input. Therefore, we need a way to update the URL without reloading the page.

Fortunately, modern browsers support an HTML5 feature called the History API, which allows us to manipulate the browser history and the URL without reloading the page. In this post, we will learn how to use this feature in JavaScript to update the URL without reloading the page.

1. Overview of the History API

The History API is a set of methods and properties that enable us to interact with the browser history and the URL. The browser history is a stack of URLs that the user has visited in a browser tab. The user can navigate through the history using the back and forward buttons, or using keyboard shortcuts. The History API allows us to:

  1. Get the current state object of the history entry using history.state .
  2. Push a new state object and URL to the history stack using history.pushState(state, title, url) .
  3. Replace the current state object and URL in the history stack using history.replaceState(state, title, url) .
  4. Pop a state object and URL from the history stack using history.back() , history.forward() , or history.go(delta) .

The state object is an arbitrary JavaScript object that can store some data related to the URL. It can be retrieved using history.state or from the popstate event object. The title argument is currently ignored by most browsers, but it may be used in the future. The url argument is a relative or absolute URL that will be displayed in the address bar.

2. Updating URL without reloading the page

There are two ways in HTML5 which can update the URL of the current webpage – using either History.pushState() and History.replaceState() . Let’s discuss these in detail:

1. Using History.pushState() function

To create a new entry in the browser history and update the URL, we can use History.pushState() method. This will allow the user to go back to the previous URL using the back button. It takes the state, title, and URL as a parameter. We can call it using the DOM Window history object, i.e., window.history . Here’s an example:

Как обновить страницу в JavaScript: простая инструкция

Для обновления страницы в JavaScript можно использовать метод location.reload().

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

javascript location.reload(); 

Этот код вызовет перезагрузку текущей страницы.

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

html  

Это создаст кнопку, которая вызовет перезагрузку страницы, когда пользователь на нее нажмет.

Также можно задать время задержки для перезагрузки страницы:

javascript setTimeout(function()< location.reload(); >, 5000); 

Это вызовет перезагрузку страницы через 5 секунд после загрузки страницы.

Важно помнить, что обновление страницы может прервать любые действия, выполняющиеся на странице, такие как процессы отправки данных на сервер или выполнения функции. Поэтому следует быть осторожным при использовании метода location.reload().

Обновить страницу с помощью JS / HTML / PHP

JS -метод location.reload() перезагружает текущую вкладку браузера и действует также как кнопка «Обновить страницу».

location.reload();

Пример перезагрузки страницы кликом на ссылку или кнопку:

Цикличное обновление страницы с задержкой

В коде используется тот же location.reload() выполняемый с задержкой setTimeout() в тридцать секунд.

setTimeout(function()< location.reload(); >, 30000);

Перезагрузка страницы с задержкой

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

 Обновить страницу через 2 секунды  
Пример:

Перезагрузка страницы с подтверждением

Чтобы пользователь мог подтвердить действие, можно применить метод вызова диалогового сообщения confirm.

if (confirm('Вы действительно хотите обновить страницу?'))

Или по клику на ссылку:

 Обновить страницу  
Пример:

Обновление родительской страницы из IFrame

Для обращения к ресурсам родительской страницы из IFrame используется объект parent , подробнее в статье «Как обновить iframe».

parent.location.reload();

Перезагрузка страницы с помощью HTML

Добавление мета-тега в страницы заставит её перезагрузится. Значение атрибута content больше нуля задает задержку в секундах.

   Document . 

Перезагрузка страницы из PHP

Обновить страницу прямо с сервера можно c помощью функции header() , отправив заголовок « Refresh: 5 », где значение «5» указывает интервал в пять секунд.

header("Refresh: 5");

Важно, чтобы перед вызовом функции не было отправки контента в браузер, например echo .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *