Локальный сервер для разработки статического сайта с нуля
Вступление
Локальный сервер разработки — полезный инструмент для 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
Ваше мнение важно!
Напишите, понравилась ли вам статья?
Какую тему разобрать следующей?
Подписывайтесь в соц. сетях, чтобы не пропустить новые материалы!
Ещё больше обсуждений в моём телеграм канале!