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


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


    В комментариях к первой части мне справедливо заметили об уточнении терминологии. Поэтому свой проект теперь буду называть 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.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

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

    Самое читаемое