Один репозиторий что бы править всеми
Собираем кроссплатформенное (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 компонент
который загружает наиболее подходящий компонент для отображения указанной связи.
Компонент очень гибкий, поддерживает пропс 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
Для генерации 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
Серверно-клиентский билд
npm run build; # генерация sdk/app, PORT использовать нельзя
npm run start; # запуск сгенерированного sdk/app, PORT=3000 по умолчанию
PORT=3001 npm run start; # запуск на альтернативном порте
Примерный вес директории sdk/app 76МБ
Скриншот localhost:3000
Статический клиентский билд
В SDK заранее скофигурирован next-i18next
npm run export; # генерация sdk/out директории
# .html файлы в директории можно открыть
# директорию можно залить на любой статический хостинг
Примерный вес директории sdk/out 1.6МБ
Скриншот директории и открытого приложения
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, установке на него приложения и его запуску.
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, без отпечатков пальца никуда… согласны ;)?
Примерный вес apk файла 4.1МБ
Бывает удобно использовать capacitor config ключ server для дебага изменений в реальном времени указав в конфиге путь к запущеному приложению в режиме разработки (
npm run dev
).
Скриншот запущенного эмулятора и приложения.
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 и запущенного приложения
Linux приложение
Приложение для linux можно собрать только из под на linux.
npm run build-unix # генерирует sdk/app, sdk/out, обновляет sdk/electron
# генериует Appimage и папку linux-unpacked с исполнимым файлом
Примерный вес Appimage файла 350МБ
Скриншот директории sdk/electron/dist и запущенного приложения
Использовал 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 расширения
Переменные окружение
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 сорри если где накосячил с примером ;)
// разбор реального кейса дипуша принесет в следующих статьях
Наша штабная маскот улитка передает всем муциновый привет, и ждет всех желающих в гости на наших хакатонах.