Подробный гайд о том, как можно использовать github pages для своих fullstack pet проектов с бэкендом на статических файлах)

Перед стартом несколько вводных:

  • Каждый шаг будет сопровождён ссылкой на соответвующий коммит из ветки main в репозитории gh-pages-demo.

  • Команды для терминала будут расписаны с использованием unix команд mkdir, cd, touch. Подробности легко гуглятся. Для ленивых можно глянуть linux cheat sheet

  • Работа с гитом тоже будет описана из терминала. Но нет никаких ограничений на использование любого GUI.

План

План реализации включает в себя несколько шагов:

  1. Инициализируем приложение

  2. Собираем "backend"

  3. Собираем frontend

  4. Добавляем production режим

  5. Автоматизируем деплой на github pages

  6. Бонус

Поехали!

Иници��лизируем приложение

Для начала нам надо засетапить наше приложение.

Создаём профиль github

Берём готовый профиль или заводим новый аккаунт github.com/signup.
Затем заводим репозиторий.
Я заведу репозиторий с названием gh-pages-demo

Клонируем репозиторий:

- cd Documents/projects
- git clone git@github.com:robzarel/gh-pages-demo.git

Здесь представлено клонирование по ssh, но ничего не мешает клонировать через http или zip архивом.

Сетапим приложение

Cетапим typescript приложение с помощью react-create-app

- cd gh-pages-demo
- npx create-react-app . --template typescript

PS:
npx это runner для npm пакетов. Он просто запускает их, не устанавливает. Немного подробнее про npx и npm vs npx

Сохраняем

- git add .
- git commit -m "feat: initial commit"
- git push

Код:

Репозиторий: feat: initial commit

Собираем "backend"

"backend" мы будем делать с помощью json-server.
"backend" указан в ковычках, так как это всего лишь иммитация настоящего бэка, основанная на файлах)

Устанавливаем json-server

  npm install --save-dev json-server

Создаём директорию

Здесь будут хранится все наши "серверные" файлы и данные

- cd src
- mkdir server
- cd server

Настройка префикса api

Настраиваем json-server таким образом, чтобы наше апи было доступно с префиксом api

Создаём файл routes.json

- touch routes.json

Наполняем его содержимым:

{ "/api/*": "/$1" }

Подробнее про добавление кастомного роутинга в json-server: add-custom-routes

Создаём хранилище

Настраиваем экспорт наших данных для того, чтобы json-server мог их использовать.

- mkdir db
- cd db
- touch data.json
- touch index.js

index.js

Точка входа, за которой будет следить json-server:

const data = require('./data.json');

module.exports = () => ({
  data: data,
});

data.json

Сами данные, которые будут импортироваться в index.js:

{ "greeting": "Hello world" }

PS:
обратите внимание, что в module.exports мы присваиваем не простой объект, а функцию, к��торая возвращает объект. Это маленькая хитрость поможет нам сэкономить времени в процессе разработки в будущем. Подробнее про это расскажу в секции про разработку api модуля для фронта.

Пишем npm scripts

В секции scripts нашего package.json файла создаём скрипт serve для запуска нашего json-server:

    "serve": "json-server --watch ./src/server/db/index.js --routes ./src/server/routes.json --port 3001",

Здесь мы просим наш json-server о том, чтобы он

  • брал данные из нашего index.js,

  • использовал кастомные роуты из routes.json

  • использовал порт 3001

После запуска

  npm run serve

по адресу http://localhost:3001/api/data доступно содержимое нашего json файлика. Красота.

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

Переходим к фронтовой части.

Код

Репозиторий: feat: add json-server

Собираем frontend

Напишем простенький фронт, который ходит в наш свеженький бэкенд и выводит на страницу Hello World

API модуль

Для работы с нашим свежим api создадим отдельный модуль, в котором опишем структуру возвращаемых ответов. Этот модуль будет инкапсулировать в себе всю работу с бэком.

Так же для удобства напишем функцию фетчер, которая будет непосредственно ходить за json данными на наш бэк, парсить их и возвращать json в место вызова.

import getEndpoints from '../server/db';

const endpoints = getEndpoints();

type ENDPOINTS = keyof typeof endpoints;
type RESPONSE_DATA = {
  greeting: string;
};

const getJson = async <T>(endpoint: ENDPOINTS): Promise<T> => {
  const path = `http://localhost:3001/api/${endpoint}`;
  const response = await fetch(path);

  return await response.json();
};

type API = {
  get: {
    data: () => Promise<RESPONSE_DATA>;
  };
};
const api: API = {
  get: {
    data: () => getJson<RESPONSE_DATA>('data'),
  },
};

export type { RESPONSE_DATA, ENDPOINTS };
export default api;

Как мы обсуждали выше, присваивание в module.exports функции позволит нам избежать потенциальных ошибок обращения к несуществующим эндпойнтам. Достигается это путём типизации ожидаемых параметров функции фетчера данных (благодаря комбинации операторов keyof typeof)
Таким образом мы получили в ENDPOINTS union type, который описывает все возможные ручки нашего "бэкенда". И typescript не позволит нам сделать обращение к несуществующей ручке (какую бы структуру возвращаемого объекта в module.exports мы бы не з��давали).

Подробнее про keyof typeof.

PS:
При росте количества ручек можно смело разносить объявления типов и методов конкретных ручек по разным файлам, а index.ts останется просто точкой входа в модуль.

Подтягиваем данные

Подтягивать данные будем традиционно с использованием хука useEffect
Сохранять данные в локальном стейте с помощью useState

import React, { useEffect, useState } from 'react';

import api from './api';
import type { RESPONSE_DATA } from './api';

import './App.css';

function App() {
  const [data, setData] = useState<RESPONSE_DATA>();

  useEffect(() => {
    const fetchData = async () => {
      const response = await api.get.data();
      setData(response);
    };

    fetchData();
  }, []);

  return <div className='App'>{data ? <p>{data.greeting}</p> : 'no data'}</div>;
}

export default App;

Теперь при запуске в 2х разных терминалах команд

npm run serve

и

npm start

мы получим по адресу http://localhost:3000 наше приложение, которое при запуске выполняет однократный запрос за данными на http://localhost:3001/api/data и отображает результат этого запроса.

Код

Репозиторий: feat: render api data

Добавляем production режим

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

Для этого нам надо ответить на 2 вопроса:

  • гд�� нам хостить наше приложение

  • как автоматизировать выкладку приложения на этот хост

Hosting "сервера"

Github штука потрясающая и предоставляет нам готовое апи, для доступа к файлам.

Все наши файлы в открытом репозитории будут доступны на домене https://raw.githubusercontent.com
А путь к ним будет составлятся по следующему шаблону:
https://raw.githubusercontent.com/userName/projectName/branchName/relative-directory-path/fileName, где:

  • userName - имя пользователя в gitHub

  • branchName - название ветки в git

  • relative-directory-path - путь внутри репозитория до файла

  • fileName - имя файла (например data.json)

Таким образом, мы можем создать отдельную ветку в нашем репозитории, в которой будут находится наши продакшен файлы, которые и будут выполнять функцию backend эндпойнтов. Мы будем ходить к ним за данными.

Мы будем использовать ветку под названием gh-pages (об этом в следующем пункте).
И для того, чтобы как-то структурно разграничивать место хранения данных в сборке, мы будем хранить эти файлы в каталоге static/db.
Следовательно в моём случае файл data.json с данными должен располагаться по адресу:

https://raw.githubusercontent.com/robzarel/gh-pages-demo/gh-pages/static/db/data.json

Корректировка Fronetnd

API модуль

Теперь пришло время настроить наше приложение на работу в двух режимах - development и production

Для этого, нам нужно взять переменную окружения NODE_ENV (система сборки CRA автоматически нам предоставляет эту переменную и мы можем её использовать через process.env.NODE_ENV) и с её помощью скорректировать API модуль. Корректировка должна включать себя разветвление логики - в development режиме мы будем ходить за данными на наш json-server, а в production режиме - на статический сервер raw.githubusercontent.com.
Для этого просто немного подкорректируем уже написанную функцию getJson:

const getJson = async <T>(endpoint: ENDPOINTS): Promise<T> => {
  const path =
    process.env.NODE_ENV === 'development'
      ? `http://localhost:3001/api/${endpoint}`
      : `https://raw.githubusercontent.com/robzarel/gh-pages-demo/gh-pages/static/db/${endpoint}.json`;

  const response = await fetch(path);

  return await response.json();
};

Влючение статики в билд

Для того, чтобы наши файлы попали в продовую сборку, нам нужно их туда положить руками. Для этого будем использовать пакет node-fs и небольшой самописный скрипт, который просто рекурсивно копирует нужные нам файлы и складывает их в каталог build по нужному "адресу".

Устанавливаем node-fs

  npm install --save-dev node-fs

Пишем скрипт для копирования

Скрипт назовём save-json-api.js и расположим в каталоге src/server/scripts

  cd src/server
  mkdir scripts
  cd scripts
  touch save-json-api.js

Копировать файлы будем в каталог static/db:

const fs = require('node-fs');
const getDb = require('../db');

const db = getDb();

fs.mkdir('./build/static/db', () => {
  for (let [key, value] of Object.entries(db)) {
    fs.writeFile(
      `./build/static/db/${key}.json`,
      JSON.stringify(value),
      (err) => {
        if (err) throw err;
      }
    );
  }
});

И не забудем создать отдельный npm script для запуска этого скрипта сразу после создания сборки приложения:

    "save-json-api": "node ./src/scripts/save-json-api.js",
    "build": "react-scripts build && npm run save-json-api",

Теперь при запуске npm run build у нас будут автоматически копироваться все файлы данных, которые мы будем экспортировать из нашего src/server/db/index.js.

PS:
Обратите внимание, что благодаря тому, что мы в module.exports используем функцию, а не объект, мы добиваемся назависимости между файлововой структурой для локальной разработки и файловой структурой, которая будет в проде.

Так как формирование продовой файловой структуры происходит динамически)) Это даёт нам гибкость и удобство работы с файлами - локально данные могут быть сгруппированы по сущностям. А в прод уезжать уже скомпонованные по потребностям конкретного "эндпойнта"

Код

Репозиторий: feat: add production mode

5 Автоматизируем деплой на github pages

Для того, чтобы выложить наше приложение на github Pages нам понадобится выполнить 2 операции:

  1. Загрузить свежую сборку приложения в ветку gh-pages нашего репозитория

  2. Настроить Github Pages

Пакет gh-pages

Для публикации нашего приложения мы будем использовать готовый пакет gh-pages.
Пакет выполняет выполнит за нас сборку и отправку билда в ветку нашего репозитория, под названием gh-pages.

Установка gh-pages

  npm install --save-dev gh-pages

Добавление npm scripts

В секции scripts нашего package.json файла прописываем скрипты predeploy и deploy для запуска пакета gh-pages.

    "predeploy": "rm -rf build && npm run build",
    "deploy": "gh-pages -d build"

Так же необходимо добавить секцию homepage в package.json, для того, что бы публичный путь до наших ресурсов (js, css файлов) в сборке был корректным и приложение запустилось))

"homepage": "https://robzarel.github.io/gh-pages-demo",

Код

Репозиторий: feat: add production mode

Настройка Github Pages

Здесь нам понадобится зайти на github в наш репозиторий и нажать пару кнопок. А именно:

  1. открываем страницу репозитория (https://github.com/userName/repoName)

  2. заходим в настройки (https://github.com/userName/repoName/settings)

  3. ищем в левом меню кнопку pages и заходим (https://github.com/userName/repoName/settings/pages)

  4. ищем секцию Build and deployment. В ней: 4.1 в разделе Source выбираем Deploy from branch 4.2 в разделе Branch выбираем нашу ветку gh-pages 4.3 жмём кнопку save

Всё, теперь при пуше в ветку gh-pages, ваш билд автоматически (по истечении некоторого времени) будет доступен по адресу https://useName.github.io/repositoryName.
В моём случае это https://robzarel.github.io/gh-pages-demo

Бонус: Клиентский роутинг

Если вы решите пилить клиентский роутинг, то заметите, что ваш react-router не отрабатывает и гитхаб говорит вам, что такой страницы не существует.

Для обхода этого ограничения (или бага, как посмотреть) вам нужно положить в билд файл 404.html с содержимым вашего index.html....))) Делаем это следующим образом:

Устанавливаем пакет для копирования файлов shx (для простоты )

  npm install --save-dev shx

И запускаем копирование в момент сразу после сборки билда

    "build": "react-scripts build && npm run save-json-api && shx cp build/index.html build/404.html",

Теперь при обращении по несуществующему урлу, гитхаб будет рендерить 404.html и ваше приложение будет запускаться как ожидается (т.е. будет работать клиентский роутинг)

Код

Репозиторий: feat: workaround for client side routing

Итого

Теперь при запуске команды

  npm run deploy

У нас будет автоматически:

  • запускаться очистка каталога build от старого содержимого

  • запускаться сборка приложения

  • итог сборки будет отправлятся в ветку gh-pages в наш github репозиторий

  • github action автоматически раскатят билд

Профит))

Всё, на этом наша настройка завершена)
Мы успешно настроили приложение как для локальной разработки, так и для продакшен режима )

Удачи в разработки ваших pet проектов))

PS:
Ссылки в статье: