
Собираем кроссплатформенное (server-client, static-client, gh-pages, Android, iOS, macOS, Linux, Windows, Chrome extension, Docker, Kubernetes, ...) React приложение. В этой статье я почти не затрону Deep backend, только чуть-чуть в конце. Но рассмотрю 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 (<Center p={'1em'}>
<VStack p={3} spacing={3} width={'100vw'} maxWidth={500}>
<Box pt={3}>
<Heading as={'h1'} size='xl'>
{t('sdk')}
<HStack spacing={3} float='right'>
<Button isDisabled={router.locale === 'ru'} onClick={() => router.push(router.asPath, router.asPath, { locale: 'ru' })}>ru</Button>
<Button isDisabled={router.locale === 'en'} onClick={() => router.push(router.asPath, router.asPath, { locale: 'en' })}>en</Button>
</HStack>
</Heading>
<Heading as={'h4'} size='md'>{t('sdk-description')}</Heading>
</Box>
<Connection/>
</VStack>
</Center>);
}Зачем SDK нам в Deep.Foundation (сложно)
Кроме того что с его помощью собирается как таковой Deep.Case, SDK нужен для того чтобы хранимые в Deep, в связях компоненты можно было экспортировать прямо из интерфейса в следующих версиях Deep.Case в любое кроссплатформенное приложение и сразу публиковать в магазины приложений одной кнопкой. С этой целью в SDK изначально установлен deep-foundation/deepcase-app (npm, git) из которого можно импортировать React компонент <ClientHandler linkId={123}/> который загружает наиболее подходящий компонент для отображения указанной связи.

Компонент очень гибкий, поддерживает пропс context={[...ids]} предназначенный, для того чтобы подобрать более подходящий компонент, например компоненты в базе могут быть помечены связями контекста как "элементы меню" или "полноэкранное" или "рабочее пространство" или символизировать размер, или конкретное применение. В контексте ассоциативности это могут быть любые связи, так как все единообразно. Таким образом сборка SDK где в качестве index компонента это <ClientHandler/> отображающий конкретную связь, конкретным способом, и предварительно наполненная Minilinks связями необходимыми для работы этого компонента и всех вложенных. Это будет рассмотрено в будущих статьях. Сейчас мы работаем над новой модульной версией Deep.Case, с разнообразными ClientHandler-ами сеток и размещения других ClientHandler-ов как например react-grid-layout или react-flow, а также в планах разработка UI вокруг ChakraUI grid, flex, simple-grid и пр для визуального редактирования responsive grids внутри Deep.Case.
status | server | client | github actions | |
build | ✅ | ✅ | ||
export | ✅ | ✅ | ||
docker | ✅ | 🛠️ | ||
kuber | 🛠️ | 🛠️ | ||
gh-pages | ✅ | ✅ | ||
build-ios | ✅ | 🛠️ | ||
build-android | ✅ | ✅ | ||
build-windows (on windows) | ✅ | /electron/src/server.ts | ✅ | |
build-unix (on linux) | ✅ | /electron/src/server.ts | ✅ | |
build-mac (on mac) | ✅ | /electron/src/server.ts | ✅ | |
build-chrome-extension | ✅ | 🛠️ | ||
build-firefox-extension | 🛠️ | ❌ | ||
vscode-extension | 🛠️ | ? | ❌ | |
nodeos | 🛠️ | ❌ |
Таблица и вся статья будет обновляться по мере появления новых способов сборки, или изменения из статуса. Инструкция по кастомизации иконок приложений и расширений, splash скринов будет в статье про публикацию приложения.
Готовим себя
Для применения этого sdk требуется только базовое знание JavaScript (наши бесплатные видео уроки), React, html/css, git и NextJS. Для тонкой настройки билдов может быть полезно понимание Capacitor, Electron, Cordova.
В SDK изначально установлен ChakraUI в качестве ui фреймворка на базе css-in-js, в принципе можно генерировать интерфейс в OpenChakra.
Готовим среду разработки
Нам потребуется некоторый IDE, допустим VSCode. На Windows рекомендую пользоваться WSL. Также необходимо установить git для клонирования репозиториев и nvm для простоты управления версиями nodejs/npm. Мы используем 18 версию node, поэтому установим ее как версию по умолчанию:
nvm i 18
nvm alias default 18
nvm use default
npm i -g npm@latestДля генерации Android приложений нам потребуется установленная Android Studio со следующими компонентами в SDK Tool: Android SDK Command-line Tools, Android Emulato, Android SDK Platfrom-Tool, Google Play services и некоторой версии SDK Platforms, сейчас мы будем использовать Android 14. Если вы на маке, требуется установить Homebrew, это пакетный менеджер удобный для установки библиотек. Затем автоматический сборщик Android билдов Gradle следуя инструкции на сайте или если вы на маке этой командой:
brew install gradleСкриншоты из Android Studio


Для генерации iOS приложений нам потребуется установленный XCode, в нашем случае версии 10. Так-же рекомендуется установить Homebrew и с его помощью пакетный менеджер для Objective-C и Swift - Сocoapods.
brew install cocoapodsНачинаем
В реальном кейсе, в идеале сделать форк sdk, и склонировать его, а затем в будущем обновлять его из источника следующим кодом:
git remote add sdk https://github.com/deep-foundation/sdk.git
git fetch sdk main
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.
Запуск приложения в режиме разработки сPORT=3000 по умолчанию:
npm run devЗапуск приложения в режиме разработки на альтернативном порте (3001):
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/app директории
# .html файлы в директории можно открыть
# директорию можно залить
# на любой статический хостинг (например GitHub Pages)Примерный вес директории sdk/app 1.6МБ
Скриншот директории и открытого приложения


Android приложение
npm run build-android; # генерирует sdk/app, обновляет 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/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/electron
# генериует dmg, zip и папку mac с бинарником по адресу sdk/electron/distПримерный вес dmg файла
350МБ[+23.04.2024] 100МБ
Скриншот директории sdk/electron/dist и запущенного приложения


Linux приложение
Приложение для linux можно собрать только из под linux.
npm run build-unix # генерирует sdk/app, обновляет sdk/electron
# генериует Appimage и папку linux-unpacked с исполнимым файломПримерный вес Appimage файла
350МБ[+23.04.2024] 100МБ
Скриншот директории sdk/electron/dist и запущенного приложения

Windows приложение
npm run build-windows # генерирует sdk/app, обновляет sdk/electron
# генериует exe и папку linux-unpacked с исполнимым файломПримерный вес инсталлятора
350МБ73МБПримерный вес папки после установки
1ГБ[+22.04.2024] 250МБ (размер exe файла 130МБ)[+23.04.2024] Примерный вес portable.exe 62МБ
Скриншот директории sdk/electron/dist и запущенного приложения




[+23.04.2024] portable.exe

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 exportBackend
Наверняка у Вас есть свое решение для backend, и вы можете, как и в любом NextJS, установить необходимые именно вам способы обращаться к вашим API, или использовать уже установленные @apollo/client, axios. Мы используем в качестве backend 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 => <div>
<input type="checkbox" onClick={async () => {
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}
<button onClick={async () => await deep.delete({
// удаляем всех по дереву contain у кого выше есть l.id включительно
up: { tree_id: containTree, parent_id: l.id }
})}>x</button>
</div>)}
{checked.map(l => <div>
<input type="checkbox" onClick={async () => {
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}
<button onClick={async () => await deep.delete({
// удаляем всех по дереву contain у кого выше есть l.id включительно
up: { tree_id: containTree, parent_id: l.id }
})}>x</button>
</div>)}
</>;
// а так можно допустим создать новый таск
const [value, setValue] = useState('');
return <>
<input type="text" value={value} onChange={e => setValue(e.currentTarget.value)}/>
<button onClick={async () => await deep.insert({
type_id: SyncTextFile,
string: { data: { value } },
in: { data: { type_id: Contain, from_id: deep.linkId } },
})}>+</button>
</>;
// PS сорри если где накосячил с примером ;)
// разбор реального кейса дипуша принесет в следующих статьяхПриглашаем всех к нам в Discord к совместному использованию, разработке, оптимизации и расширению способов применения репозитория из коробки ;)

