Перейти к содержимому

VK Mini Apps

VK Mini Apps ↗ — кроссплатформенные мини-приложения в экосистеме ВКонтакте ↗. VKUI изначально задумывался как набор компонентов для таких приложений. Со временем библиотека вышла за эти рамки, но по-прежнему отлично подходит для мини-приложений. Ниже — рекомендации по интеграции.

Сначала устанавливаем библиотеки:

npm i --save @vkontakte/vk-bridge @vkontakte/vk-bridge-react

Далее подписываемся на изменения из VK Bridge и передаём их в ConfigProvider из VKUI. Используем готовые хуки из @vkontakte/vk-bridge-react ↗.

App.tsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import vkBridge, { parseURLSearchParamsForGetLaunchParams } from '@vkontakte/vk-bridge';
import { useAppearance, useInsets, useAdaptivity } from '@vkontakte/vk-bridge-react';
import { Platform, ConfigProvider, AdaptivityProvider, AppRoot } from '@vkontakte/vkui';
import { transformVKBridgeAdaptivity } from './helpers/transformVKBridgeAdaptivity';
 
// Инициализируем VK  Mini App
vkBridge.send('VKWebAppInit');
 
const App = () => {
  const vkBridgeColorScheme = useAppearance() || undefined; // Вместо undefined можно задать значение по умолчанию
  const vkBridgeInsets = useInsets() || undefined; // Вместо undefined можно задать значение по умолчанию
  const vkBridgeAdaptivityProps = transformVKBridgeAdaptivity(useAdaptivity()); // Конвертируем значения из VK Bridge в параметры AdaptivityProvider
  const { vk_platform } = parseURLSearchParamsForGetLaunchParams(window.location.search); // [опционально] Платформа может передаваться через URL (см. https://dev.vk.com/mini-apps/development/launch-params#vk_platform)
 
  return (
    <ConfigProvider
      colorScheme={vkBridgeColorScheme}
      platform={vk_platform === 'desktop_web' ? 'vkcom' : undefined}
      isWebView={vkBridge.isWebView()}
      hasCustomPanelHeaderAfter={true} // Резервируем правую часть PanelHeader под кнопки управления VK Mini Apps. Через параметр customPanelHeaderAfterMinWidth можно регулировать ширину этой области (по умолчанию, используется 90)
    >
      <AdaptivityProvider {...vkBridgeAdaptivityProps}>
        {/* Для VK Mini Apps рекомендуем использовать mode="full" (выставлен по умолчанию, для примера указан явно) */}
        <AppRoot mode="full" safeAreaInsets={vkBridgeInsets}>
          {/* Ваше приложение */}
        </AppRoot>
      </AdaptivityProvider>
    </ConfigProvider>
  );
};
 
const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!), если используется TypeScript
root.render(<App />);
./helpers/transformVKBridgeAdaptivity.ts
import {
  type AdaptivityProps,
  getViewWidthByViewportWidth,
  getViewHeightByViewportHeight,
  ViewWidth,
} from '@vkontakte/vkui';
import type { UseAdaptivity } from '@vkontakte/vk-bridge-react';
 
/**
 * Требуется конвертировать данные из VK Bridge в те, что принимает AdaptivityProvider из VKUI.
 */
export const transformVKBridgeAdaptivity = ({
  type,
  viewportWidth,
  viewportHeight,
}: UseAdaptivity): AdaptivityProps => {
  switch (type) {
    case 'adaptive':
      return {
        viewWidth: getViewWidthByViewportWidth(viewportWidth),
        viewHeight: getViewHeightByViewportHeight(viewportHeight),
      };
    case 'force_mobile':
    case 'force_mobile_compact':
      return {
        viewWidth: ViewWidth.MOBILE,
        density: type === 'force_mobile_compact' ? 'compact' : 'regular',
      };
    default:
      return {};
  }
};

На стартовой странице мини-приложения нужно включить свайпбек нативного клиента, чтобы пользователь мог выйти из мини-приложения. Для этого вызывайте определённые методы VK Bridge в зависимости от типа мини-приложения:

  • В стандартном мини-приложении ВКонтакте при переходах отправляйте VKWebAppSetSwipeSettings ↗ с history: true на первой панели и history: false на остальных.
  • Во внутреннем мини-приложении ВКонтакте отправляйте VKWebAppEnableSwipeBack ↗ при переходе на первый экран и VKWebAppDisableSwipeBack ↗ — на любой другой.
import vkBridge from '@vkontakte/vk-bridge';
 
const SomeViews = () => {
  const [history, setHistory] = useState(['main']);
  const activePanel = history[history.length - 1];
  const isFirst = history.length === 1;
 
  const go = React.useCallback((panel) => setHistory((prevHistory) => [...prevHistory, panel]), []);
  const goBack = React.useCallback(() => setHistory((prevHistory) => prevHistory.slice(0, -1)), []);
  const handleProfileClick = () => go('profile');
  const handleMainClick = () => go('main');
 
  React.useEffect(() => {
    // Для стандартных мини-приложений делайте так:
    vkBridge.send('VKWebAppSetSwipeSettings', { history: isFirst });
    // Для внутренних мини-приложений делайте так:
    vkBridge.send(isFirst ? 'VKWebAppEnableSwipeBack' : 'VKWebAppDisableSwipeBack');
  }, [isFirst]);
 
  return (
    <View history={history} activePanel={activePanel} onSwipeBack={goBack}>
      <Panel id="main">
        <div onClick={handleProfileClick}>Main</div>
      </Panel>
      <Panel id="profile">
        <div onClick={handleMainClick}>Profile</div>
      </Panel>
    </View>
  );
};

Мини-приложению доступна почти вся площадь экрана, поэтому для корректной навигации используйте PanelHeader на каждом экране. Он должен содержать название приложения и значок Назад (см. PanelHeaderBack) там, где он нужен.

Для лучшего пользовательского опыта в VK Bridge есть события вызова вибрации на устройствах, которые её поддерживают.

@vkontakte/vk-bridge-react ↗ предоставляет функцию runTapticImpactOccurred, которая отправляет событие VKWebAppTapticImpactOccurred ↗ и заранее проверяет, что вибрация доступна на устройстве.

Виброотклик удобно использовать, например, в onRefresh у PullToRefresh.

import * as React from 'react';
import { runTapticImpactOccurred } from '@vkontakte/vk-bridge-react';
 
const Users = () => {
  const [users, setUsers] = React.useState([
    { id: 1, name: 'Placeholder', avatarUrl: 'https://placehold.co/100' },
  ]);
  const [fetching, setFetching] = React.useState(false);
 
  const onRefresh = React.useCallback(() => {
    setFetching(true);
    // Вызываем виброотклик
    runTapticImpactOccurred('light');
  }, []);
 
  return (
    <View activePanel="users">
      <Panel id="users">
        <PanelHeader>Пользователи</PanelHeader>
 
        <PullToRefresh onRefresh={onRefresh} isFetching={fetching}>
          <Group>
            <List>
              {users.map(({ id, name, avatarUrl }, i) => {
                return (
                  <Cell key={i} before={<Avatar src={avatarUrl} />}>
                    {name}
                  </Cell>
                );
              })}
            </List>
          </Group>
        </PullToRefresh>
      </Panel>
    </View>
  );
};