Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Раздача статических файлов

Статические файлы — те, что отдаются браузеру как есть, без обработки: HTML-страницы, стили, скрипты, картинки. Фреймворки раздают их одной строкой настройки, а в node:http сервер должен сам найти файл на диске, прочитать его и указать правильный Content-Type.

Папка public

Создайте в корне проекта папку public и в ней два файла:

blog/
├── public/
│   ├── index.html
│   └── style.css
├── src/
│   └── server.js
└── package.json
<!-- public/index.html -->
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Блог Анны</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <h1>Блог Анны</h1>
  </body>
</html>
/* public/style.css */
h1 {
  color: darkslateblue;
}

Тип по расширению

Браузер понимает, что получил, только по заголовку Content-Type. Соответствие расширений и типов задают объектом:

// src/static.js
export const MIME_TYPES = {
  '.html': 'text/html; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.js': 'text/javascript; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.svg': 'image/svg+xml',
};

Для неизвестных расширений используют тип application/octet-stream — «просто байты».

Функция раздачи

Добавьте в src/static.js функцию, которая ищет файл в public и отправляет его:

// src/static.js
import path from 'node:path';
import { readFile } from 'node:fs/promises';
 
const PUBLIC_DIR = path.join(import.meta.dirname, '..', 'public');
 
export async function serveStatic(pathname, res) {
  const name = pathname === '/' ? 'index.html' : pathname;
  const file = path.join(PUBLIC_DIR, name);
 
  if (!file.startsWith(PUBLIC_DIR + path.sep)) return false;
 
  try {
    const content = await readFile(file);
    const type = MIME_TYPES[path.extname(file)] ?? 'application/octet-stream';
    res.writeHead(200, { 'Content-Type': type });
    res.end(content);
    return true;
  } catch {
    return false;
  }
}

Функция возвращает true, если файл найден и отправлен, и false, если его нет. Кодировку в readFile() не указывают: картинки — это байты, а не текст.

Подключение к серверу

В src/server.js сначала пробуют отдать файл, а потом — обычные маршруты:

// src/server.js
import http from 'node:http';
import { serveStatic } from './static.js';
 
const server = http.createServer(async (req, res) => {
  const { pathname } = new URL(req.url, 'http://localhost');
 
  if (req.method === 'GET' && (await serveStatic(pathname, res))) return;
 
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Страница не найдена');
});
 
server.listen(3000);

http://localhost:3000 покажет фиолетовый заголовок «Блог Анны»: браузер загрузил страницу и отдельным запросом — /style.css.

Частая ошибка

Если склеить путь из адреса без проверки, посетитель сможет запросить /../.env и прочитать ваши пароли — это называется обходом каталога (path traversal). Поэтому в serveStatic() есть строка:

// src/static.js
if (!file.startsWith(PUBLIC_DIR + path.sep)) return false;

После path.join() все .. уже раскрыты, и если итоговый путь вышел за пределы public, файл не отдаётся. Никогда не убирайте эту проверку.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.