Собираем кроссплатформенное (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}/> который загружает наиболее подходящий компонент для отображения указанной связи.

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

Компонент очень гибкий, поддерживает пропс 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

✅

ssr and api

react and i18n

✅

export

✅

react and async i18n

✅

docker

✅

ssr and api

react and i18n

?️

kuber

?️

ssr and api

react and i18n

?️

gh-pages

✅

react and async i18n

✅

build-ios

✅

react and async i18n

?️

build-android

✅

react and async i18n

✅

build-windows (on windows)

✅

/electron/src/server.ts

react and async i18n

✅

build-unix (on linux)

✅

/electron/src/server.ts

react and async i18n

✅

build-mac (on mac)

✅

/electron/src/server.ts

react and async i18n

✅

build-chrome-extension

✅

react and async i18n

?️

build-firefox-extension

?️

react and async i18n

❌

vscode-extension

?️

?

❌

nodeos

?️

react and i18n

❌

Таблица будет обновляться по мере изменения обстоятельств. В статью будут дополняться примеры и инструкции. Единообразная инструкция по кастомизации иконок приложений и расширений, 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, это будет рассмотрено в следующей статье
После выполнения run-ios терминал предложит выбрать эмулятор ios на выбор. Выбор делается стрелочками и enter. Выберу свой se, без отпечатков пальца никуда... согласны ;)?
После выполнения 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МБ 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 и запущенного приложения
Использовал Elementary OS в качестве виртуальной машины.
Использовал Elementary OS в качестве виртуальной машины.

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 к совместному использованию, разработке, оптимизации и расширению способов применения репозитория из коробки ;)

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

Комментарии (5)


  1. alysnix
    20.04.2024 17:28
    +2

    союз "чтобы" пишется слитно.


    1. Konard
      20.04.2024 17:28
      +2

      Исправлено.


  1. danilkinkin
    20.04.2024 17:28
    +6

    350мб и 1гиг.... Даже сказать нечего