Подробный гайд о том, как можно использовать github pages для своих fullstack pet проектов с бэкендом на статических файлах)
Перед стартом несколько вводных:
Каждый шаг будет сопровождён ссылкой на соответвующий коммит из ветки main в репозитории gh-pages-demo.
Команды для терминала будут расписаны с использованием unix команд mkdir, cd, touch. Подробности легко гуглятся. Для ленивых можно глянуть linux cheat sheet
Работа с гитом тоже будет описана из терминала. Но нет никаких ограничений на использование любого GUI.
План
План реализации включает в себя несколько шагов:
Инициализируем приложение
Собираем "backend"
Собираем frontend
Добавляем production режим
Автоматизируем деплой на github pages
Бонус
Поехали!
Инициализируем приложение
Для начала нам надо засетапить наше приложение.
Создаём профиль 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 операции:
Загрузить свежую сборку приложения в ветку gh-pages нашего репозитория
Настроить 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 в наш репозиторий и нажать пару кнопок. А именно:
открываем страницу репозитория (https://github.com/userName/repoName)
заходим в настройки (https://github.com/userName/repoName/settings)
ищем в левом меню кнопку pages и заходим (https://github.com/userName/repoName/settings/pages)
ищем секцию 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:
Ссылки в статье:
Код gh-pages-demo.
инициализация feat: initial commit
добавили бэк feat: add json-server
добавили фронт feat: render api data
добавили прод режим feat: add production mode
настроили автодеплой feat: add deploy scripts
добавили настройки для клиентского роутинга feat: workaround for client side routing
Npm пакеты: json-server, gh-pages, shx, node-fs
Прочее
регистрация github.com/signup
про github pages
про CRA react-create-app
про union type
про useEffect
про useState
про keyof typeof
про кастомные роуты add-custom-routes
про раздачу статики static-file-server