Как зеленый джуниор свой hot-reloader писал

в 18:04, , рубрики: javascript

Предыстория

Пару строк обо мне для общего понимания уровня автора и решаемой проблемы.

Меня зовут Евгений и я веб-разработчик зеленый junior frontend developer.
Еще какой-то год назад я работал в совершенно другой сфере и только в теории задумывался о смене профессии, но примерно в декабре 2018 нашел свое и начал действовать.
Примерно через полгода тотального обучения я устраиваюсь работать frontend-программистом. За плечами обучение фундаментальным вещам(мне хочется так думать), js, взаимодействие с DOM, react+redux. HTML и CSS самый минимум+ общее понимание о bootstrap и сборке, работа с git, командной строкой.
Помимо теории сделано пара учебных проектов, в том числе чат на react+redux, а так же пара попыток реализации каких-то своих задумок.
В общем, такой себе стандартный современный джентельменский набор для начинающего front'a.
Первые полторы недели настраиваю виртуальную машину, там куча всего и все мне незнакомо и непонятно.
По ходу дела знакомлюсь с новыми инструментами и технологиями: с базами данных(и ставлю себе очередную закладку в список «выучить»), putty, wincsp и пр.
Успешно прохожу эту полосу препятствий и перехожу к фронту.

Предисловие

Уже написав свой релоадер и эту статью, я нашел аналоги в том числе на Хабре. Однако все-таки решил опубликовать свой велосипед.

Начало

У нас довольно большой проект, доставшийся в наследство, написанный на angularJS, со всеми его прелестями. Мне после React'а он показался динозавром, но ничего, покупаю курсы по angularjs, быстро въезжаю и начинаю приносить пользу.

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

Но и минусов хватает.

Первая проблема: проект собирается каким-то древним минимизатором и использовать современный синтаксис js нельзя. Никаких () => {}, const res = [...data, subRes], async/await…

Вторая проблема: нет ни webpack, ни даже хотя бы gulp, а соответственно нет и привычного мне webpack-dev-server c его прекрасным hot reload.

Написал. Сохранил. F5. Неудобно. Боль? Не прям боль, но очень неудобно.

Третья проблема: сборка проекта .bat файлом, в котором часть проекта просто копируется, часть библиотек собираются без минимизации, часть минимизируются в один файл, остальные файлы проекта-в другой. Список библиотек в третьем файле. Список файлов для сборки в четвертом. И так далее.

Четвертая проблема: все библиотеки аккуратно лежат в папочке libs и подключаются скриптом в index.html. Все-все, кроме express и proxy для него(они не участвуют в сборке, а только для разработки).
И далеко не везде есть версии или указание на конкретную библиотеку.

На обучении я жил в прекрасном мире функционального программирования, полном es6+, webpack-dev-server, tdd, eslint, автоматической сборкой и так далее.

А тут во взрослом мире все совсем по-другому…

Завязка

Но работать мне нравится, препятствия рассматриваю как возможности саморазвития, компания хорошая, обстановка отличная, глаза горят!

В рабочее время выполняю рабочие задачи, в свободное пытаюсь что-то улучшить.

Середина июня, начинаю с попытки прикрутить webpack, но первый подход ожидает полный провал. Неделю мучаюсь, сильно от этого устаю, временно откладываю.

Решаю начать с малого — подключаю новый синтаксис через babel. Дописываю в наш волшебный build.bat первоначальную обработку babel'ем, но что-то ломает идиллию и наш старый минификатор спотыкается. Ищу проблему.

Спотыкается на одной из библиотек из аккуратной папочки libs. Смотрю файлы библиотек: они уже минифицированы и в старом синтаксисе.

Говорю babel — «ты сюда не ходи… код башка попадет, совсем плохо будет». Проверяю: все работает! Ура! Теперь мне доступны все те приятные новые стильные модные молодежные штуки! Первая победа. Приятно. Думаю по такому случаю переименовать скрипт в e.bat(e-Evgen), но не решаюсь.

Новый синтаксис так знаком и приятен, но мысли о кривой сборке не покидают меня.

Конец июня-начало июля. Делаю второй подход, более основательный, но снова упираюсь в ошибки между webpack и angularjs. Снова неделя изысканий.

Как-то раз провожу несколько дней и частично ночей за поиском решения, натыкаюсь на крайне интересные выступления с конференции HolyJS, где ребята уже довольно глубоко копают в webpack. Продвигаюсь в его понимании, но материал пока не понимаю до конца.

Интерес укрепляется.

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

Середина июля, мне в руки попадает похожий на наш проект с настроенной сборкой. Иду с ним в третий подход и практически настраиваю у нас webpack, но в конце ловлю новые ошибки, на решение которых времени уже не хватает, работа накатывает с новой интенсивностью + морально меня это опустошает, вновь откладываю это дело.

Основная часть

Середина августа. В итоге приятель рассказывает про изучение node.js и его желание написать собственный hot-reloader. Мысли о нашей сборке вспыхивают у меня с новой силой.

Задача: reload текущую страницу при обновлении файлов в проекте.
Особенности: все библиотеки подключаются в index.html, нельзя require, не говоря уже об import. Сборка перед reload пока не нужна, только reload. В сервере для разработки, который проксирует запросы на наш бэк, пакеты использовать я могу, а так же могу require!

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

Мыслительный процесс идет и в голове формируется видение решения.
Простейший сервер(как у нас), в нем я обойду всю папку и подпапки и сформирую дерево с датами изменений каждого файла.

Далее через каждые n миллисекунд буду обходить еще и еще и сравнивать значения времени изменений. Изменилось — reload. Приятель подсказывает — «не изобретай велосипед, есть watch в node.js». Отлично, буду использовать его. В server.js настрою watch за папкой проекта и по изменению чего-то внутри буду вызывать location.reload().

Первая итерация:

server.js

var express = require('express');
var app = express();
var server = require('http').Server(app); 

const port = 9080;
server.listen(port);
app.use(express.static(__dirname + '/src'));
location.reload().

Первая проблема — location- это не переменная node.js(в этот момент я обретаю понимание, почему мои попытки обращения к process.env на фронте тоже были безуспешны))).

Вторая проблема — как дать front'у понять, что нужно делать reload?

Выход — websocket! Идея заманчива + я с ними работал «на пол-шишки», когда писал чат, общее представление имею. Заодно делаю счетчик перезагрузок за сессию, добавляю переменную и обработку отдающему ее запросу.

Пробую:

server.js

var express = require('express'); // Подключаем express
var app = express();
var server = require('http').Server(app); // Подключаем http через app
var io = require('socket.io')(server); // Подключаем socket.io и указываем на сервер
var fs = require('fs');

const port = 9080;
server.listen(port);
app.use(express.static(__dirname + '/src'));

let count = 0;
app.get('/data', (req, res) => {
  res.data = count;
  res.send(`${count}`);
})

const dir = './src';
fs.watch(dir, () => {
  io.emit('change', {data: count});
  count += 1;
})

На фронте делаю простейший App на angularjs

app.js

angular.module('App', [])
.controller('myAppCtrl',['$scope', '$timeout','$http',
($scope, $timeout, $http) => {
    $scope.title = 'Страничка для тестирования простейшего хот релоада без пересборки';
    $scope.count = 0;
    $scope.todo = [
        'прикрутить рекурсивность папок,поискать стандартные методы',
        'проверить на отслеживание node.js watch файлы других типов',
        'периодически проходить отслеживаемую папку и смотреть,не появились ли в ней или вложенные файлы и папки',
        'прикрутить линтер, codeclimate и travis к этому проекту'
    ]
    $scope.marks = [
        'watcher не смотрит рекурсивно на каталоги внутри'
    ]

    // var socket = io();
    //     socket.on('change', (data) => {
    //         console.log(data.data);
    //         $scope.count = data.data;
    //         console.log('scope.count: ', $scope.count);
    //         $scope.$digest();//
    //         location.reload();//agfr
    //     })

    $http({method: 'GET',url: 'data'})
    .then(response => {
        $scope.count = response.data;//
    });
}])

И отдельный модуль, который ее reload. Отдельный, чтобы в проект лишнего не попадало.

watch.js

var socket = io();
        socket.on('change', () => {
            location.reload(); 
        })

Работает! Файлы кроме js тоже отслеживает(мало ли!): проверял .json, .css.
Проверяю вложенные папки — не работает.

Думаю, ладно, сейчас запилю рекурсивно. На всякий случай гуглю и — вуаля- есть готовое
решение.

Добавляю этот пакет.

server.js

var express = require('express'); // Подключаем express
var app = express();
var server = require('http').Server(app); // Подключаем http через app
var io = require('socket.io')(server); // Подключаем socket.io и указываем на сервер
var fs = require('fs');
var watch = require('node-watch');

const port = 9080;
server.listen(port);
app.use(express.static(__dirname + '/src'));

let count = 0;
let changed = [];
app.get('/data', (req, res) => {
  res.data = count;
  res.send({count, changed});
})
const translate = {
  remove: "удален",
  update: "изменен"
}
watch('./', { recursive: true }, function(evt, name) {
  io.emit('change', {data: count}); 
  count += 1;
  changed = [{name, evt}, ...changed];
});

Ура, работает!

Вспоминаю про eslint, codeclimate и travis.
Устанавливаю первый, добавляю остальное.
Подчищаю код, все var на const и так далее.

Linter ругается, что angular is not defined, но у меня особенности подключения библиотек в проекте диктуют такое поведение, отключаю. Заодно немного прикручиваю переменные из командной строки, запускаю, все работает!

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

В итоге получилось вот так:

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);

app.js

var socket = io.connect();

socket.on('change', ({ evt, name, exeptions }) => {
		location.reload();
});

запускающий скрипт в package.json просто вызывает server.js из-под node и запускается это вот так:

npm start 1.100 8080

Написал. Сохранил.F5

В заключении хочу поблагодарить Ваню, моего друга, местами вдохновителя и главного пинателя, а так же Сашу — человека, которого я считаю своим наставником!

Вместо послесловия

А через 2 недели, в последний день своего испытательного срока, я таки прикрутил webpack и webpack-dev-server на наш проект, отправив тем самым свой hot reloader пылиться на полку истории.

Однако эти 2 недели мы использовали его каждый день и он исправно делал свое дело!

Автор: euhoo

Источник


* - обязательные к заполнению поля