Собираем кроссплатформенное (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гиг.... Даже сказать нечего