Один репозиторий что бы править всеми

cb1e414e77db7a02ef7911bafb1b3364.png

Собираем кроссплатформенное (server-client, static-client, gh-pages, android, ios, mac, linux, windows, chrome extension, kuber, docker) React приложение. В этой статье я почти не затрону Deep бекенд, только чуть-чуть в конце. Но рассмотрю Open Source шаблон/заготовку для сборки кроссплатформенных React приложений который мы используем в Deep.Foundation.

Да, очевидно для максимально производительного UI/UX нужен максимально нативный Swift/Java/…, но если цель — быстро вывести продукт и иметь универсальный доступ и подход ко всему, то такой дает из коробки одно кольцо что бы править всеми для быстрого старта.

Не учитывать подготовку системы, достаточно в своем форке сразу размещать свой React код заменив содержание этого компонента:

export default function Page() {
  const deep = useDeep();
  const { t } = useTranslation();
  const router = useRouter();

  // @ts-ignore
  if (typeof(window) === 'object') window.deep = deep;
  console.log('deep', deep);

  return (
{t('sdk')} {t('sdk-description')}
); }

Зачем SDK нам в Deep.Foundation (сложно)

Кроме того что с его помощью собирается как таковой Deep.Case, SDK нужен для того что бы хранимые в Deep, в связях компоненты можно было экспортировать прямо из интерфейса в следующих версиях Deep.Case в любое кроссплатформенное приложение и сразу публиковать в сторы одной кнопкой. С этой целью в SDK изначально установлен deep-foundation/deepcase (npm, git) из которого можно импортировать React компонент который загружает наиболее подходящий компонент для отображения указанной связи.

В Deep.Case этот компонент отображает верстку при клике по связи как например компонент формулы в этом ролике.
В Deep.Case этот компонент отображает верстку при клике по связи как например компонент формулы в этом ролике.

Компонент очень гибкий, поддерживает пропс context={[...ids]} предназначенный что бы подобрать более подходящий компонент, например компоненты в базе могут быть помечены связями контекста как «элементы меню» или «полноэкранное» или «рабочее пространство» или символизировать размер, или конкретное применение. В контексте ассоциативности это могут быть любые связи, так как все единообразно. Таким образом билд sdk где в качестве index компонента это отображающий конкретную связь, конкретным способом, и предварительно наполненная Minilinks связями необходимыми для работы этого компонента и всех вложенных. Это будет рассмотрено в будущих статьях. Сейчас мы работаем над новой модульной версией Deep.Case, с разнообразными ClientHandler-ами сеток и размещения других ClientHandler-ов как например react-grid-layout или react-flow, а так-же в планах разработка ui вокруг ChakraUI grid, flex, simple-grid и пр для визуального редактирования респонсивных сеток внутри Deep.Case.

Таблица будет обновляться по мере изменения обстоятельств. В статью будут дополняться примеры и инструкции. Единообразная инструкция по кастомизации иконок приложений и расширений, splash скринов будет в статье про публикацию приложения.

Готовим себя

Для применения этого sdk требуется только базовое знание JavaScript (наши бесплатные видео уроки), React, html/css, git и NextJS. Для тонкой настройки может быть полезно Capacitor, Electron, Cardova.

Готовим среду разработки

Нам потребуется некоторый IDE, допустим VSCode. На Windows рекомендую пользоваться WSL. Так-же необходимо установить git для клонирования репозиториев и nvm для простоты управления версиями nodejs/npm. Мы используем 16 версию node поэтому установим ее по умолчанию:

nvm i 16; nvm alias default 16; nvm use default;

Для генерации Android приложений нам потребуется установленная Android Studio с следующими компонентами в SDK Tool: Android SDK Command-line Tools, Android Emulato, Android SDK Platfrom-Tool, Google Play services и некоторой версии SDK Platforms, сейчас мы будем использовать Android 14. Требуется установить Homebrew (если вы на маке) и затем Gradle.

brew install gradle

Скриншоты из Android Studio

3d43eb98009938d5d874db0ac5ad4e35.pngb697bc9d0dcdd2a1c6357c821eb30f11.png

Для генерации iOS приложений нам потребуется установленный XCode, в нашем случае версии 10. Требуется установить Homebrew и Сocoapods.

brew install cocoapods

Начинаем

В реальном кейсе, в идеале сделать форк sdk, и склонировать его, а затем в будущем обновлять его из источника следующим кодом:

git remote add sdk https://github.com/deep-foundation/sdk.git
git fetch sdk
git merge sdk/main --allow-unrelated-histories --strategy ours 

Однако мы будем работать непосредственно собирать sdk, поэтому клонируем его.

git clone https://github.com/deep-foundation/sdk.git;
cd sdk;
npm ci; (cd electron; npm ci)

В среднем после установки всех зависимостей для разработки директория sdk весит примерно до#$& 5.5ГБ, но такова цена разработки на NodeJS.

Режим разработки

Запускаем версию для разработки. В таком режиме удобно разрабатывать приложение в браузере, применяя chrome inspector и react chrome extension.

npm run dev; # запус режима разработки на PORT=3000 по умолчанию
PORT=3001 npm run dev; # запуск на альтернативном порте

Скриншот localhost:3000

1bcde70b758f8b183ad2f4a4e412ac04.png

Серверно-клиентский билд

npm run build; # генерация sdk/app, PORT использовать нельзя
npm run start; # запуск сгенерированного sdk/app, PORT=3000 по умолчанию
PORT=3001 npm run start; # запуск на альтернативном порте

Примерный вес директории sdk/app 76МБ

Скриншот localhost:3000

23e68bd366442e5aa1e194910528f154.png

Статический клиентский билд

В SDK заранее скофигурирован next-i18next

npm run export; # генерация sdk/out директории
# .html файлы в директории можно открыть
# директорию можно залить на любой статический хостинг

Примерный вес директории sdk/out 1.6МБ

Скриншот директории и открытого приложения

ce6be6be7cebc34c0dfa2f2829fab3cf.png6a7f0ef6ae50f231389e5cff44ceffca.png

Android приложение

npm run build-android; # генерирует sdk/app, sdk/out, обновляет sdk/android
npm run open-android; # запускает AndroidStudio с нужной конфигурацией из sdk/android
# генерирует apk по адресу sdk/android/app/build/outputs/apk/debug/app-debug.apk
# подробнее о генерации release билда будет в статье про публикации в сторы

Примерный вес apk файла 4.1МБ

Бывает удобно использовать capacitor config ключ server для дебага изменений в реальном времени указав в конфиге путь к запущеному приложению в режиме разработки (npm run dev).

Инструкция по запуску эмулятора и скриншот запущенного приложения.

1 Дожидаемся завершения процесса сборки в правом нижнем углу.

2 Возможно при первом запуске или после обновления зависимости к sdk бывает нужно нажать Sync Project with Gradle files.

3 Добавляем желаемое устройство для эмулятора и следуем инструкции внутри.

4 По завершению следует нажать зеленую кнопку ▶️ Run сверху. Это приведет к запуску эмулятора Android, установке на него приложения и его запуску.

a9643c54aa1916de7d24d2bc868b931c.png

iOS приложение

# Перед работой нужно установить cocoapods библиотеки используемые в ios
(cd ios/App/App; pod install);

npm run build-ios;  # генерирует sdk/app, sdk/out, обновляет sdk/ios
npm run run-ios # запускает XCode с нужной конфигурацией из sdk/ios
# где лежит билд приложения не так важно, так как любая заливка в TestFlight
# делается прямиком из XCode, это будет рассмотрено в следующей статье

После выполнения run-ios терминал предложит выбрать эмулятор ios на выбор. Выбор делается стрелочками и enter. Выберу свой se, без отпечатков пальца никуда... согласны ;)?

После выполнения run-ios терминал предложит выбрать эмулятор ios на выбор. Выбор делается стрелочками и enter. Выберу свой se, без отпечатков пальца никуда… согласны ;)?

Примерный вес apk файла 4.1МБ

Бывает удобно использовать capacitor config ключ server для дебага изменений в реальном времени указав в конфиге путь к запущеному приложению в режиме разработки (npm run dev).

Скриншот запущенного эмулятора и приложения.

f52470b9c03e06dfa7a1558618000229.png

Mac приложение

Это можно сделать только на операционной системе MacOS. Потребуется Apple Developer аккаунт. Нужно сгенерировать app-specific пароль для ADC аккаунта Apple ID и запомнить его. Затем сгенерировать teamId. Обновить переменные APPLEIDPASS, APPLEID, CSC_NAME, APPLETEAMID в вашем package.json.scripts.build-mac, а так-же выполнить security add-generic-password -l "sdk" -a "YOUR-APPLEID-EMAIL" -s "keychain" -T "" -w "APP-PASSWORD-FROM-APPLE" подменив соответствующие значения, где APP-PASSWORD-FROM-APPLE полученный ранее app-specific пароль.

npm run build-mac # генерирует sdk/app, sdk/out, обновляет sdk/electron
# генериует dmg, zip и папку mac с бинарником по адресу sdk/electron/dist

Примерный вес dmg файла 350МБ

Скриншот директории sdk/electron/dist и запущенного приложения

55462f6570a1f5230e5246cabb4e8340.png62b0d2eff9d3143ec6872296dc5a033a.png

Linux приложение

Приложение для linux можно собрать только из под на linux.

npm run build-unix # генерирует sdk/app, sdk/out, обновляет sdk/electron
# генериует Appimage и папку linux-unpacked с исполнимым файлом

Примерный вес Appimage файла 350МБ

Скриншот директории sdk/electron/dist и запущенного приложения

Использовал Elementary OS в качестве виртуальной машины.

Использовал Elementary OS в качестве виртуальной машины.

Windows приложение

npm run build-windows # генерирует sdk/app, sdk/out, обновляет sdk/electron
# генериует exe и папку linux-unpacked с исполнимым файлом

Примерный вес инсталлятора 350МБ

Примерный вес после установки 1ГБ

Скриншот директории sdk/electron/dist и запущенного приложения

Chrome расширение

npm run build-chrome-extension
# Result path: `sdk/extension.crx` and `sdk/extension.pem`

Примерный вес crx файла 1МБ

Скриншот добавленного и открытого в Chrome расширения

ba68d0c5b052d64d869c90977431cd6f.png

Переменные окружение

PORT=3000 # по умолчанию
# NextJS пробрасывает NEXT_PUBLIC_ переменные до клиента

NEXT_PUBLIC_GRAPHQL_URL= # по умолчанию не указан, выбирается в ui
NEXT_PUBLIC_DEEP_TOKEN= # по умолчанию не указан, выбирается в ui

NEXT_PUBLIC_I18N_DISABLE=0 # по умолчанию
# next-i18next не поддерживает next export, то есть бессерверный nextjs
# sdk оборачивает асинхронным i18n провайдером, если NEXT_PUBLIC_I18N_DISABLE=1
# так-же если NEXT_PUBLIC_I18N_DISABLE=1 то оригинальный next-i18next отключен
# это автоматически включается при npm run export

Бекенд

Наверняка у Вас есть свое решение для backend, и вы можете как и в любом NextJS установить необходимые именно вам способы обращаться к вашим api, или использовать уже установленные @apollo/client, axios. Мы используем в качестве бекенда Deep. Я не буду углубляться в процесс запуска дипа, это можно найти в нашем сообществе. Опишу лишь пару примеров как мы оперируем ассоциациями. С более подробным примером дипуша вернется позже в отдельной статье.

Пример React кода работы с Deep бекендом и клиентской ассоциативной памятью minilinks на хуках (сложно)

Допустим мы заранее в Deep.Case создали ассоциативный пакет @ivansglazunov/checked и в нем связи User |- Checked -> Any для обозначения факта завершенности. Будем использовать уже существующий в пакете @deep-foundation/coreтип SyncTextFileв качестве хранилища значения. Причастность нашей псевдо задачи к пользователю будем обозначать фактом вложенности экземпляра SyncTextFileв пользователя посредствам экземпляров уже существующего в пакете @deep-foundation/coreтипа Contain .

Приведенный ниже код, это лишь пример, @ivansglazunov/checkedпакета не существует.

const deep = useDeep();
// deep.linkId указывает на связь авторизованного в этом клиенте пользователя
// эти два запроса вернут одинаковое количество связей
// однако этот способ сделает больше join и нагрузки на сервер
// и вернет иерархию связей
const { data: nested, loading } = deep.useDeepSubscription({
  type_id: { _id: ['@deep-foundation/core', 'SyncTextFile'] },
  in: {
    from_id: deep.linkId, type_id: { _id: ['@deep-foundation/core', 'Contain'] },
  },
  return: { checkeds: {
    relation: 'in',
    type_id: { _id: ['@ivansglazunov/checked', 'Checked'] }
  } }
});
// nested // { ...link, checkeds: link[] }[]
// а этот сделает поиск по ассоцитавной индексации деревьев
// так как для работы системы прав мы вкладываем Checked экземпляр Contain связью
// он доступен в едином дереве собственности
// в этом случае мы заранее получим используемые идентификаторы
// что бы снизить нагрузку на запросы
// это можно сделать по разному, это не самый оптимальный способ
// но для наглядного примера сойдет...
const { data: Checked } = deep.useDeepId('@ivansglazunov/checked', 'Checked');
const { data: SyncTextFile } = deep.useDeepId('@deep-foundation/core', 'SyncTextFile');
const { data: Contain } = deep.useDeepId('@deep-foundation/core', 'Contain');
const { data: containTree } = deep.useDeepId('@deep-foundation/core', 'containTree');
const { data: all, loading } = deep.useDeepSubscription({
  // верни все те связи у кого выше по дереву containTree есть указанная связь
  up: {
    tree_id: containTree,
    parent_id: deep.linkId,
  },
  // нас интересуют только SyncTextFile и Checked связи
  type_id: { _in: [Checked, SyncTextFile] },
});
// all // link[] // всё найденное плоским списком
// Все найденные данные доступны в нашем Глубинном аналоге
// клиентского MeteorJS minimongo - minilinks.
// например можно найти именно все не чекнутые SyncTextFile в оперативной памяти
// на клиенте и вывести на экран
const unchecked = deep.useMinilinksSubscription({
  type_id: SyncTextFile,
  _not: { in: { type_id: Checked } },
});
const checked = deep.useMinilinksSubscription({
  type_id: SyncTextFile,
  in: { type_id: Checked },
});
return <>
  {unchecked.map(l => 
{ await deep.insert({ type_id: Checked, from_id: deep.linkId, to_id: l.id, // и обязательно вкладываем его в дерево владения // что бы можно было иерархически искать или давать права in: { data: { type_id: Contain, from_id: l.id } }, }); }}/> {l?.value?.value}
)} {checked.map(l =>
{ await deep.delete({ type_id: Checked, from_id: deep.linkId, to_id: l.id }); // любопытно то что благодаря minilinks можно записать это так await deep.delete(l.inByType[Checked][0].id]); // или так await deep.delete(deep.minilinks.query({ type_id: Checked, to_id: l.id })[0].id); }}/> {l?.value?.value}
)} ; // а так можно допустим создать новый таск const [value, setValue] = useState(''); return <> setValue(e.currentTarget.value)}/> ; // PS сорри если где накосячил с примером ;) // разбор реального кейса дипуша принесет в следующих статьях

Наша штабная маскот улитка передает всем муциновый привет, и ждет всех желающих в гости на наших хакатонах.

Наша штабная маскот улитка передает всем муциновый привет, и ждет всех желающих в гости на наших хакатонах.

© Habrahabr.ru