Рецепт: скелет frame-приложения

Минимально жизнеспособное iframe-приложение Bitrix24: рукопожатие init, установка заголовка, сохранение состояния приложения, открытие слайдера, чистое закрытие.
Мы всё ещё дорабатываем эту страницу. Некоторых данных здесь может не хватать — скоро дополним.

Цель

Один Vue 3-компонент, демонстрирующий четыре вещи, нужные каждому iframe-приложению Bitrix24: инициализировать рукопожатие postMessage, задать заголовок родительского окна, сохранить часть состояния между перезагрузками через опции приложения и открыть другую страницу портала в слайдере.

Стек и предпосылки

  • Vue 3 + Vite (или Nuxt 3 с @bitrix24/b24jssdk-nuxt)
  • @bitrix24/b24jssdk@^1.0.0
  • Страница должна отдаваться с URL, зарегистрированного как обработчик placement в карточке вашего приложения Bitrix24. Вне iframe initializeB24Frame() завершается reject.
pnpm add @bitrix24/b24jssdk

Полный пример

src/components/AppShell.vue:

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import {
  initializeB24Frame,
  type B24Frame,
  LoggerFactory
} from '@bitrix24/b24jssdk'

const logger = LoggerFactory.createForBrowser('AppShell', import.meta.env.DEV)
const ready = ref(false)
const userTheme = ref<'light' | 'dark'>('light')
const portalDomain = ref('')
let $b24: B24Frame | null = null

onMounted(async () => {
  try {
    $b24 = await initializeB24Frame()
  }
  catch (error) {
    logger.error('initializeB24Frame failed', error)
    return
  }

  $b24.parent.setTitle('My Bitrix24 App')
  // Авто-ресайз iframe под содержимое; вызывайте снова, когда высота содержимого
  // меняется (например, после рендера ответа сети).
  await $b24.parent.resizeWindowAuto()

  // Чтение сохранённых настроек. Возвращает `null`, если ничего ещё не задано.
  const stored = $b24.options.appGet('theme') as 'light' | 'dark' | null
  if (stored) userTheme.value = stored

  const appInfo = await $b24.actions.v2.call.make({ method: 'app.info' })
  portalDomain.value = appInfo.getData().result.DOMAIN ?? ''

  ready.value = true
})

onBeforeUnmount(() => {
  $b24 = null
})

async function setTheme(value: 'light' | 'dark') {
  userTheme.value = value
  // Опция уровня приложения: хранится на портале под приложением, не под пользователем.
  await $b24?.options.appSet('theme', value)
}

async function openCrmDeals() {
  if (!$b24) return
  const url = new URL('/crm/deal/list/', `https://${portalDomain.value}`)
  await $b24.slider.openPath(url, 1200)
}

async function close() {
  await $b24?.parent.closeApplication()
}
</script>

<template>
  <section v-if="ready" class="app-shell" :data-theme="userTheme">
    <header>
      <h1>Hello from {{ portalDomain }}</h1>
    </header>

    <main>
      <button @click="setTheme(userTheme === 'light' ? 'dark' : 'light')">
        Switch theme (current: {{ userTheme }})
      </button>
      <button @click="openCrmDeals">
        Open deals in slider
      </button>
    </main>

    <footer>
      <button @click="close">Close app</button>
    </footer>
  </section>
  <p v-else>Loading…</p>
</template>

src/main.ts:

// @check-ignore: vue .vue module import — .vue files are not in tsconfig

import { createApp } from 'vue'
import AppShell from './components/AppShell.vue'

createApp(AppShell).mount('#app')

Запуск

# 1. Локальный dev-сервер (Vite)
pnpm dev
# → http://localhost:5173/ — запуск этого URL напрямую завершится reject:
#   initializeB24Frame() нужно родительское окно Bitrix24.

# 2. Пробросьте dev-сервер (ngrok / cloudflared / ваш туннель)
ngrok http 5173
# → возьмите https-URL.

# 3. В Bitrix24: Приложения → Ресурсы разработчика → Локальное приложение →
#    укажите URL обработчика placement равным URL туннеля и сохраните.

# 4. Откройте placement внутри Bitrix24.

После загрузки iframe вы увидите: заголовок родительского окна станет «My Bitrix24 App», iframe подстроится под содержимое, а в теле появится Hello from <your-portal>.bitrix24.com и две кнопки. Переключение темы сохраняется между перезагрузками (значение живёт на портале под приложением, а не в localStorage). Клик по «Open deals in slider» открывает /crm/deal/list/ того же портала в слайдере в стиле Bitrix24.

Как это работает

  • initializeB24Frame() — единственный правильный способ создать B24Frame. Он устраняет дублирование параллельных вызовов, разбирает параметры рукопожатия портала из window.name и ждёт, пока родительское окно подтвердит payload авторизации.
  • parent.setTitle() и parent.resizeWindowAuto() пересекают границу iframe через postMessage. Оба идемпотентны — вызывайте их всякий раз при изменении вёрстки.
  • options.appGet() синхронный, потому что значения загружаются один раз во время рукопожатия. options.appSet() асинхронный, потому что делает round-trip к серверу.
  • slider.openPath(url, width) принимает экземпляр URL (не строку) и возвращает Promise<StatusClose>, который разрешается, когда пользователь закрывает слайдер. CSP портала разрешает открывать URL только на домене того же портала.

Ограничения

  • Компонент должен монтироваться внутри iframe Bitrix24; запуск отдельно (например, в Storybook) сразу падает, потому что родительского окна нет. Защищайтесь проверкой window.parent === window, если нужен мягкий фолбэк.
  • $b24.options.appSet() записывает по одной опции за раз. Несколько записей в одном рендере — нормально (SDK их сериализует), но каждый вызов — это сетевой round-trip.
  • parent.closeApplication() закрывает слайдер приложения только когда placement поддерживает закрытие. Встроенные placement (виджеты сайдбара) игнорируют запрос.
  • Опции уровня приложения видны каждому пользователю приложения на портале. Для пользовательских настроек используйте userGet / userSet.

См. также