Собираем кроссплатформенное (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, это будет рассмотрено в следующей статье
Примерный вес 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 export
Backend
Наверняка у Вас есть свое решение для 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 к совместному использованию, разработке, оптимизации и расширению способов применения репозитория из коробки ;)