Собираем кроссплатформенное (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.
Готовим среду разработки
Нам потребуется некоторый 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 (если вы на маке) и затем 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.
Запуск приложения в режиме разработки сPORT=3000
по умолчанию:
npm run dev
Запуск приложения в режиме разработки на альтернативном порте (3001):
export 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 файлы в директории можно открыть
# директорию можно залить
# на любой статический хостинг (например GitHub Pages)
Примерный вес директории 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, это будет рассмотрено в следующей статье
Примерный вес 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МБ100МБ
Скриншот директории sdk/electron/dist и запущенного приложения
Linux приложение
Приложение для linux можно собрать только из под linux.
npm run build-unix # генерирует sdk/app, sdk/out, обновляет sdk/electron
# генериует Appimage и папку linux-unpacked с исполнимым файлом
Примерный вес Appimage файла
350МБ100МБ
Скриншот директории sdk/electron/dist и запущенного приложения
Windows приложение
npm run build-windows # генерирует sdk/app, sdk/out, обновляет sdk/electron
# генериует exe и папку linux-unpacked с исполнимым файлом
Примерный вес инсталлятора
350МБ73МБПримерный вес папки после установки
1ГБ250МБ (размер exe файла 130МБ)
Скриншот директории 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
Наверняка у Вас есть свое решение для 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 к совместному использованию, разработке, оптимизации и расширению способов применения репозитория из коробки ;)
alysnix
союз "чтобы" пишется слитно.
Konard
Исправлено.