Github pages для pet проектов

в 16:01, , рубрики: Git, github, github actions, github pages, ReactJS

Подробный гайд о том, как можно использовать 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:
Ссылки в статье:

Автор: Lazarev Boris

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js