Локальный сервер для разработки статического сайта с нуля
Вступление
Локальный сервер разработки — полезный инструмент для web разработки и позволяет:
- Мгновенно видеть изменения в браузере 
- Имитировать работу правил реального хостинга 
- Автоматизировать рутинные операции 
Зачем делать с нуля?
Всё это заложено в базовых инструментах, но интересно ведь сделать самим, чтобы чему-то научиться и разобраться: как те или иные вещи работают.
Предыдущий материал:
Если вы пропустили предыдущую вводную статью, то вот она - про то, как создаю генератор статического сайта с нуля на NodeJS!
Ключевые функции системы и идея процесса:
- Локальный HTTP сервер раздаёт сгенерированные странички и статику по правилам хостинга
- File Watcher отслеживает изменения в файлах с кодом генератора и исходными материалами
- При изменениях запускается процесс пересборки сайта
- Открытые страницы сайта в браузере получают секретное сообщение через EventStream и автоматически перезагружаются
Как следить за изменениями в файлах?
Начал со стандартного fs.watch функционала в NodeJS. После проблем с дублированием событий от файловой системы я решил переключиться на chokidar, в качестве базового решения для кросс-платформенного окружения.
Интересно? Возможно вас интересует система, которая строит индекс и следит за реальными изменениями содержания файлов? Чтобы генератор запускался только тогда, когда содержание файла реально изменилось? Если да, обязательно напишите мне об этом!
Перегенерация сайта
Просто запускаем процесс перегенерации сайта в отдельном процессе. Необходимо сгенерировать особенный билд для разработки (добавление скрипта на каждую страницу, для перезагрузки страницы из дев-сервера).
Тут так же будет полезна функция debounce, мы не хотим получить 30 запусков перегенерации на 30 измененных файлах. Просто добавляем таймер чтобы дождаться, что изменений больше нет, и запускаем процесс генерации.
export const debounce = (fn, ms) => {
  let timeoutId = null;
  return (...args) => {
    if (timeoutId != null) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
    return new Promise((resolve) => {
      timeoutId = setTimeout(() => {
        timeoutId = null;
        const result = fn(...args);
        resolve(result);
      }, ms);
    });
  };
};
Если предыдущий процесс генерации ещё не завершился - просто прерываем его (abort) и запускаем новый. Для примера, как может выглядеть использование такой утилиточной функции:
const proc = runProcess("node", ["--import", "tsx", "scripts/dev-rebuild.ts"]);
// после этого мы можем:
// 1. дождаться завершения процесса
await proc.promise;
// 2. или прервать и завершить процесс, при этом proc.promise так же должен завершиться (с ошибкой)
await proc.abort();
Локальный HTTP сервер
Тут мы просто создаём и запускаем стандартный самописный HTTP сервер (например, на http://localhost:3000/), который раздаёт файлы из локальной директории по соответствующим адресам.
Раздача файлов по адресам (роутинг)
Логика локального сервера должна, по возможности, повторяет основные настройки правил маршрутизации адресов вашего хостинга. Пример: правило обязательного добавления / в конец пути, или адрес соответствует определенной или корневой странице:
- /→- /index.html
- /about→- /about.html
- /about/→- /about/index.html
Режим разработки и перезагрузка страниц
Для режима разработки я добавляю Event Stream подключение, чтобы страница могла получать сообщение о том, что доступна новая версия сайта (после внесения изменений в код). Для подключение к EventStream со стороны локальной версии сайта, на каждую сгенерированную HTML страницу добавляем скрипт. Просто оборачиваем в <script> в конце <body> блока:
// инициируем канал сообщений с дев-сервером по адресу http://localhost:3000/dev-server
new EventSource("/dev-server").addEventListener("message", (ev) => {
  // если получаем от сервера сообщение `reload`
  if (ev?.data === "reload") {
    // перезагружаем страницу
    location.reload();
  }
});
Даже лучше, мы на лету по запросу добавлять этот сниппет самим локальным сервером при обработке всех валидных .html документов. Таким образом, генератор сайта вообще ничего не знает о существовании перезагрузок страницы, и все статические страницы (например, игровые кастомные страницы), так же получают возможность перезагрузки после перегенерации сайта.
На локальном сервере добавляем слушателя на event-stream подключение. Необходимо хранить все подключения в памяти и оповещать все открытые страницы сообщением о перезагрузке.
// здесь храним все локальные подключения
const clients = new Set();
http.createServer(async (req, res) => {
    // обрабатываем запрос по нужному адресу
    if (req.url === '/dev-server') {
        // указываем что данный запрос инициирует event-stream
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive'
        });
        // отправляем для теста пустое сообщение об инициализации, двойной перенос строки указывает на конец сообщения
        res.write('data: init\n\n');
        // добавляем новое соединение (еще одна страница открылась)
        clients.add(res);
        // завершаем обработчик и удаляем соединение
        req.on('close', () => {
            clients.delete(res);
            res.end();
        });
        return;
    }
    // ...
}
export const reloadPages = () => {
    for (const client of clients) {
        client.write('data: reload\n\n');
    }
};
Заключение
Моя система теперь:
- Автоматически пересобирает страницы сайта 
- Синхронизирует изменения между открытыми вкладками веб-браузера 
- Работает намного быстрее благодаря - debounce
 Ваше мнение важно!
- Напишите, понравилась ли вам статья? 
- Какую тему разобрать следующей? 
- Подписывайтесь в соц. сетях, чтобы не пропустить новые материалы! 
- Ещё больше обсуждений в моём телеграм канале!