Pull to refresh

Как зеленый джуниор свой <s>hot</s>Auto-reloader писал. Часть 2. CSS

Reading time8 min
Views1.8K

Перед предисловием


В комментариях к первой части мне справедливо заметили об уточнении терминологии. Поэтому свой проект теперь буду называть 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 положил все-все импорты всего до чего дотянулся самого необходимого, а entry.html – это просто копия index.html, в которой поубирал все лишние скрипты подключения файлов.

Чтобы было наверняка, в плагине 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

Запускаю, проверяю, работает!
image
Картинка получилась мелкая, небольшое пояснение.
Слева 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.
Tags:
Hubs:
Total votes 4: ↑4 and ↓0+4
Comments0

Articles