[Из песочницы] Как зеленый джуниор свой hot-reloader писал
Предыстория
Меня зовут Евгений и я веб-разработчик зеленый 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 ().
Первая итерация:
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! Идея заманчива + я с ними работал «на пол-шишки», когда писал чат, общее представление имею. Заодно делаю счетчик перезагрузок за сессию, добавляю переменную и обработку отдающему ее запросу.
Пробую:
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
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. Отдельный, чтобы в проект лишнего не попадало.
var socket = io();
socket.on('change', () => {
location.reload();
})
Работает! Файлы кроме js тоже отслеживает (мало ли!): проверял .json, .css.
Проверяю вложенные папки — не работает.
Думаю, ладно, сейчас запилю рекурсивно. На всякий случай гуглю и — вуаля- есть готовое
решение.
Добавляю этот пакет.
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, но у меня особенности подключения библиотек в проекте диктуют такое поведение, отключаю. Заодно немного прикручиваю переменные из командной строки, запускаю, все работает!
В пн пришел на работу и прикрутил все это хозяйство на рабочий проект. Пришлось немного изменить, заодно внести правки, чтобы можно было менять некоторые параметры запуска из командной строки и исключения, чтобы не вотчил все подряд.
В итоге получилось вот так:
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);
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 недели мы использовали его каждый день и он исправно делал свое дело!