Подробный гайд о том, как можно использовать 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
-
Автор: Lazarev Boris
Thanks for the informative article. Unogeeks is the top Oracle Fusion SCM Training Institute, which provides the best Oracle Fusion SCM Training