Перед предисловием
В комментариях к первой части мне справедливо заметили об уточнении терминологии. Поэтому свой проект теперь буду называть auto reloader (далее AR). Название первой части статьи сохраню старым для истории.
Предисловие
Через 2 недели после написания этого простейшего релоадера, я сумел полностью настроить webpack и webpack-dev-server, что по идее должно было привести к полному отказу от использования моего «велосипеда».
Во время настройки новой сборки я стремился поддержать возможность старой, гарантированно работающей сборки на случай «а мало ли чего».
Старая сборка характерна отсутствием в проекте import/require, которые не поддерживаются браузерами, а также тем, что на этапе разработки все .js файлы подключены в index.html внутри body. Это и рабочие файлы проекта и все библиотеки.
При этом, как я говорил ранее, все библиотеки лежат в аккуратной папочке lib.
Файл package.json был практически девственно чист в части dependencies и devDependencies(впрочем как и scripts). Только express, пакет для proxy на сервак и мои добавленные socket.io и node-watch.
Таким образом задача по сохранению старой сборки была довольно несложной, а вот задача по настройке новой – наоборот – усложнялась поисками нужных пакетов и их версий.
В итоге цели добиться удалось. Я заполнил package.json нужными пакетами, создал entry.js и entry.html как входные файлы webpack. В .js положил все-все импорты
Чтобы было наверняка, в плагине ProvidePlugin я описал одну библиотеку, которую webpack будет подставлять в нужные места «по требованию». Но сейчас понимаю, что можно и без этого обойтись. Попробую удалить, посмотрим, что выйдет.
Таким образом поддержал отсутствие импортов в основном проекте и сохранил исходный index.htm.
Это в теории должно было позволить мне собирать старым сборщиком и – что очень важно лично для меня – поддержать разработку с помощью моего auto reloader.
На практике я нашел одно место, где появился export. Одна наша самописная мини-библиотека была выполнена в форме объекта.
Для нормального функционирования сборки webpack ее необходимо экспортировать, для нормальной старой сборки достаточно просто скрипта в index.html. Ну ничего, беру и переписываю ее сервисом ангуляра. Copy + past + небольшие изменения и – вуаля – работает!
Пробую новую сборку – работает, webpack-dev-server соответственно тоже.
Пробую свой auto reloader – работает!
Кайф!
С предисловием покончено, идем дальше.
Завязка.
Поработав пару дней на webpack-dev-server, никак не могу отогнать от себя мысли, какой же он долгий.
А он при этом очень быстрый, перезагрузка буквально через 3-4 секунды после сохранения.
Но я уже привык, что мой AR в силу отсутствия сборки перезагружает прямо сразу после ctrl+s.
В итоге сначала держу оба инструмента запущенными, а потом и вовсе работаю только через AR.
Для себя выделил выделил 4 причины, почему:
1. AR быстрее.
2. С ним понятнее, что поломалось, т.к. виден весь стек ошибки в исходных файлах. Путешествие по бандлу w-d-s не требуется.
3. W-d-s не перезагружает, когда я поменял что-то в html- файле, который вставлен через include в другой html файл. Мой – в силу того, что вотчит всю папку проекта и перезагружает по любому изменению, кроме исключений – перезагружает. Тут оптимизация w-d-s стреляет себе в ногу.
4. Ну и субъективное. Мне нужно часто смотреть на бэк с разных серваков и соответственно запускать это в браузере на разных портах. Для обслуживания AR достаточно 1 скрипта
“example”: “node ./server.js”
Который запускается
npm run example 1.100 9001
или
npm run example 1.101 9002
Где 1.101 сервак, а 9001 порт для отображения на моем localhost.
Удобно в общем, не надо помнить разные имена скриптов, а просто пишешь параметры при старте скрипта и все ок.
Переменные попадают в process.argv и я их оттуда успешно себе вынимаю внутри server.js
Что касается w-d-s, то пока что такое удобство мне реализовать не удалось, пришлось сделать несколько скриптов на основные сочетания. Хорошо хоть одну конфигу для разработки используют.
В общем удобнее мне с написанным велосипедом работать.
Ну а раз удобнее, решил я проект развивать.
Какие есть варианты?
1.При изменении css файлов не перезагружать страницу, но накатывать изменения.
2.Аналогично, но уже .html
3.Попробовать разные другие варианты, кроме location.reload() для js.
4.Аналогично 1-2, но уже .js
5.Уйти от index.html+entry.html в сторону одного единственного файла для обеих сборок. т.е. прийти к ситуации, когда то, что собирается webpack, будет работать и на моем AR.
6.Прикрутить поддержку scss
CSS- прикольно, то, что надо.
HTML – тоже прикольно, тоже то, что надо.
Location.reload(). Не знаю, чем он плох, но было бы интересно рассмотреть различные доступные варианты.
JS – прикольно, было бы круто это сделать, но нужно ли реально, учитывая, сколько потрачу сил?
5 и 6 – это уже попахивает сборкой, а значит быстроты скорее всего уже не будет.
Вывод: п. 4 — 6 не планируются, пункты 1 – 2 буду делать, пункт 3 погляжу что вообще есть. На первый взгляд задача сложна, но я умею разбивать на подзадачи! Разбиваю и решаю идти поэтапно, при этом думаю только о текущем этапе(ага, как же! уже о html думаю вовсю)
Основная часть.
CSS.
Задача состоит из двух подзадач:
1.Найти измененный файл.
2.Перезагрузить css, не перезагружая страницу.
Начинаю со второй. Используя предыдущий опыт, первым делом иду гуглить, а как же вообще можно релоадить css без релоада страницы.
Натыкаюсь на ряд статей, среди которых наиболее интересным мне кажется вот эта
Ребята просто обходят все link с css в head и меняют атрибут href на новый.
Взял их идею за основу и реализовал свой вариант.
У меня по прежнему 2 файла.
server.js — это простой сервер + проверка на исключения + логика отслеживания изменений + отправка изменений сокетом.
watch.js — на клиенте. Принимает от сервера сообщения о изменениях + location.reload(). Сейчас в watch.js добавил логику проверки имени на css и замены css, если необходимо. Можно бы вынести в отдельный модуль, но пока кода мало смысла не вижу. Первая итерация получилась вот такая:
server.js
const express = require('express'),
http = require('http'),
watch = require('node-watch'),
proxy = require('http-proxy-middleware'),
app = express(),
server = http.createServer(app),
io = require('socket.io').listen(server),
exeptions = ['git', 'js_babeled', 'node_modules', 'build', 'hotreload'], // исключения,которые вотчить не надо, файлы и папки
backPortObj = { /* перечень машин,куда смотреть за back*/ },
address = process.argv[2] || /* адрес машины с back*/,
localHostPort = process.argv[3] || 9080,
backMachinePort = backPortObj[address] || /* порт на back машине*/,
isHotReload = process.argv[4] || "y", // "n" || "y"
target = `http://192.168.${address}:${backMachinePort}`,
str = `Connected to machine: ${target}, hot reload: ${isHotReload === 'y' ? 'enabled' : 'disabled'}.`,
link = `http://localhost:${localHostPort}/`;
server.listen(localHostPort);
app
.use('/bg-portal', proxy({
target,
changeOrigin: true,
ws: true
}))
.use(express.static('.'));
if (isHotReload === 'y') {
watch('./', { recursive: true }, (evt, name) => {
let include = false;
exeptions.forEach(item => {
if (`${name}`.includes(item)) include = true;
})
if (!include) {
console.log(name);
io.emit('change', { evt, name, exeptions });
};
});
};
console.log(str);
console.log(link);
watch.js
const socket = io.connect();
const makeCorrectName = name => name.replace('\\','\/');
const findCss = (replaced) => {
const head = document.getElementsByTagName('head')[0];
const cssLink = [...head.getElementsByTagName('link')]
.filter(link => {
const href = link.getAttribute('href');
if(href === replaced) return link;
})
return cssLink[0];
};
const replaceHref = (cssLink, replaced) => {
cssLink.setAttribute('href', replaced);
return true;
};
const tryReloadCss = (name) => {
const replaced = makeCorrectName(name);
const cssLink = findCss(replaced);
return cssLink ? replaceHref(cssLink, replaced) : false;
};
socket.on('change', ({ evt, name, exeptions }) => {
const isCss = tryReloadCss(name);
if (!isCss) location.reload();
});
Интересно, что пакет node-watch присылает мне имя измененного файла в виде path\to\file.css, тогда как в href путь пишется path/to/file.css. т.к. я проверяю файл по полному имени пришлось менять слэш на обратный для осуществления проверки.
И это работает!
Однако осталось 3 проблемы.
1.Вариант точно работает для chrome и точно не работает для edge. Здесь надо покопать, т.к. все-таки мультибаузерность в верстке(а ведь именно для верстки это усовершенствование) очень нужна.Но, вероятно, это связано со 2 проблемой.
2.Браузер умный: кэширует уже подгруженные файлы и при неизменении параметров – не изменяет ничего. То есть в теории, если сохранять файл с тем же именем, браузер посчитает, что ничего не изменилось и не перезагрузит содержимое. Для борьбы с этим ребята каждый раз меняют имя. У меня на chrome работает и без этого, однако, это слишком важный нюанс.
3.нужно однозначное совпадение имени. т.е. если задавать в link абсолютный путь(начинается с ./), то программа не находит совпадение.
./path/to/file != path/to/file в понимании логики моего кода. И это тоже необходимо исправить.
Таким образом мне нужно каждый раз обновлять имя файла, чтобы не было кэширования.
А точнее, каждый раз изменять атрибут href у link, в которой изменился css файл.
Подробнее читаю про это здесь
Ребята по ссылке выше борются с кэшированием очень элегантно, беру их вариант:
cssLink.setAttribute('href', `${hrefToReplace}?${new Date().getTime()}`);
Далее мне нужно сравнивать имя файла. У меня появился 1 вопросительный знак в строке, значит могу обойтись без регулярных выражений(не изучал их пока) в пользу вот такого самописного метода:
const makeCorrectName = (name) => name
.replace('\\', '/')
.split('?')[0];
Работает!
Дальше мне нужно однозначно определять путь до файла.
Я не очень хорошо владею магией абсолютного, относительного и вообще путей. Некоторое недопонимание вопроса идет как раз из-за этого.
Путь в href может начинаться с ‘.’, ‘/’ или сразу с имени.
В свободное время думал над этим вопросом.
Точка входа — index.html(а в моем случае entry.html) — всегда(как правило) на верхнем уровне. А css файлы, подключаемые скриптами, всегда(как правило) где-то в глубине. Таким образом — повторюсь — путь всегда будет одинаковым(названия папок и файла), различаться будет только первый символ.
Таким образом после отделения части с вопросительным знаком, по такой же схеме снова разбиваю строку, но уже по ‘/’, далее убираю предполагаемую первую точку и соединяю элементы массива в одну строку, по которой и буду сравнивать для точного поиска.
Выглядит это вот так:
const findFullPathString = (path) => path
.split('/')
.filter((item) => item !== '.')
.filter((item) => item)
.join('');
Запускаю код, ура, работает!
А что с Edge?
А с Edge проблема скрывалась не там, где ее искали.
Оказалось, что мой код в части css не работал в Edge, а я по своей невнимательности просто этого не заметил.
Проблема скрывалась в методе обработки коллекции DOM элементов.
Как известно, коллекция DOM элементов — это не массив, соответственно методы массива с ней не работают(точнее говоря, некоторые работают, некоторые нет).
Я привык делать так:
const cssLink = [...head.getElementsByTagName('link')]
Но старый добрый Edge не понимает этого и именно это было причиной.
Смело меняю и теперь это делается так:
const cssLink = Array.from(head.getElementsByTagName('link'))// special for IE
Запускаю, проверяю, работает!
Картинка получилась мелкая, небольшое пояснение.
Слева Chrome, по центру Firefox, справа Edge. Специально ввожу в input значение, чтобы показать, что перезагрузки страницы не происходит, а css меняется практически мгновенно.
Задержка в видео связана с задержкой между изменением и сохранением файла.
В плане css работать с chromeDevTools может быть быстрее за счет того, что у них можно, например, margin стрелочкой вверх/вниз изменять, но у меня css обновляет тоже так же быстро и все из одного редактора.
Стоит отметить, что на момент публикации статьи пользуюсь своим велосипедом на постоянной основе без каких-либо доработок уже порядка 2 недель и желания поменять на w-d-s нет. Как нет и желания для css в простых ситуациях пользоваться devTools!
Итого, server.js остался прежний, а watch.js приобретает следующий вид:
watch.js
const socket = io.connect();
const findFullPathString = (path) => path
.split('/')
.filter((item) => item !== '.')
.filter((item) => item)
.join('');
const makeCorrectName = (name) => name
.replace('\\', '/')
.split('?')[0];
const findCss = (hrefToReplace) => {
const head = document.getElementsByTagName('head')[0];
const replacedString = findFullPathString(hrefToReplace);
const cssLink = Array.from(head.getElementsByTagName('link'))// special for IE
.filter((link) => {
const href = link.getAttribute('href').split('?')[0];
const hrefString = findFullPathString(href);
if (hrefString === replacedString) return link;
});
return cssLink[0];
};
const replaceHref = (cssLink, hrefToReplace) => {
cssLink.setAttribute('href', `${hrefToReplace}?${new Date().getTime()}`);
return true;
};
const tryReloadCss = (name) => {
const hrefToReplace = makeCorrectName(name);
const cssLink = findCss(hrefToReplace);
return cssLink ? replaceHref(cssLink, hrefToReplace) : false;
};
socket.on('change', ({ name }) => {
const isCss = tryReloadCss(name);
if (!isCss) location.reload();
});
Красота!
Послесловие.
Следующим шагом хочу попробовать релоадить HTML, но пока мое видение выглядит очень сложным. Нюанс в том, что у меня angularjs и это должно работать вместе.
Буду очень рад конструктивной критике и вашим комментариям, как можно улучшить мой маленький проект, а так же советам и статьям по вопросу с HTML.