JS-модули
Скопировать ссылку на статью
Скопировано

JS-модули позволяют расширить возможности интерфейса системы.

Архитектура JS-модуля

JS-модули является расширенной версией стандартных модулей Маркетплейса. Отличием является то, что они состоят из двух частей: backend-часть и frontend-часть.

Важно!

На Github представлен полноценный пример реализации модуля с backend-частью на Symfony @retailcrm/specialist-booking.

Также доступна библиотека примеров @retailcrm/core-ui-extensions-examples.

Backend-часть JS-модуля

Backend-часть JS-модуля разрабатывается также, как и у стандартных модулей. В ее подготовке вам помогут статьи про основные требования к модулям, подключение и активацию модуля и добавление модуля в Маркетплейс.

Backend-часть модуля должна как минимум содержать логику по подключению и активации модуля в Маркетплейсе.

Если frontend-часть модуля будет обращаться к backend-части, то последняя помимо базовой логики должна также предоставлять ендпоинты для обработки запросов, отправляемых из frontend-части.

Важно!

После того, как вы подготовили backend-часть модуля, заведите его в разделе «Модули» партнерского кабинета. Далее обратитесь в партнерский отдел, чтобы для вашего модуля активировали JS-функциональность.

Frontend-часть JS-модуля

Frontend-часть JS-модуля должна представлять собой приложение, написанное на Javascript и Vue 3. Приложение будет выполняться в изолированной среде iframe. Выполнение приложения производится с помощью библиотеки удаленного рендеринга @omnicajs/vue-remote. Связь между приложением и основным интерфейсом системы (далее «хостом») осуществляется через postMessage.

К JS-приложениям предъявляются определенные требования и они содержат ограничения на используемый функционал Vue и JS, о чем описано подробнее ниже.

Требования к JS-модулям

1. Разрешенные компоненты для вывода на странице

В случае, когда JS-модуль добавляет функциональность на существующие страницы системы, непосредственно на странице (например, странице заказа) JS-модулям разрешено выводить только компоненты UiToolbarButton и UiToolbarLink из предоставляемых готовых Vue-компонентов в количестве до 2 штук в рамках одного target-а.

Подробнее с компонентами можно ознакомиться в витрине компонентов.

2. Отображение дополнительного контента

JS-модуль по клику на кнопку или ссылку может отображать дополнительные данные, формируемые самостоятельно или подгружаемые из сторонних систем. Для вывода этой информации предпочтительно использовать Vue-компонент боковой панели UiModalSidebar. В случае, если контент достаточно широкий: табличные данные, карта и другая подобная информация — допустимо использовать Vue-компонент модального окна UiModalWindow.

Подробнее с компонентами можно ознакомиться в витрине компонентов.

3. Локализация JS-модуля

Все тексты и надписи, которые выводятся во frontend-части JS-модуля, должны быть представлены на 3-х языках: русском, английском и испанском, и выводиться в той локали, которая задана в аккаунте системы.

В качестве библиотеки для организации переводом рекомендуется использовать vue-i18n. В примерах JS-модулей @retailcrm/core-ui-extensions-examples вы можете найти реализацию переводом надписей и выставление локали в соответствии с локалью аккаунты системы.

4. Вывод изображений в JS-модуле

Для вывода изображений требуется использовать Vue-компонент UiImage и указывать свойство resize, чтобы отображалась уменьшенная версия изображения для экономии трафика и ускорения загрузки изображений.

Если нужно вывести аватарку, то используйте специальный Vue-компонент UiAvatar.

Точки встраивания (targets) и доступные в них данные

Приложение может встраиваться в определенные точки интерфейса (в коде они называются targets) и оперировать доступными в этих точках данными.

При подготовке JS-приложения вам нужно определиться с перечнем точек встраивания.

В разных точках доступен разный набор данных. Данные реактивные, то есть изменения применяются ко входным объектам при их изменении в интерфейсе (в частности в формах, например, форме заказа, форме клиента и т.п). Некоторые поля мутабельные, то есть их можно изменять в JS-приложении и эти изменения применятся к форме. Такие поля в справочнике отмечены как readonly: false.

Для получения объекта из контекста и полей из объекта, используйте функции из @retailcrm/embed-ui/index.d.ts, например:

import {
    useOrderCardContext,
    useSettingsContext,
    useField,
} from '@retailcrm/embed-ui'

const order = useOrderCardContext()
const address = useField(order, 'delivery.address')

const settings = useSettingsContext()
const locale = useField(settings, 'system.locale')

Перечень точек встраивания

Доступные Vue-компоненты

Ряд Vue-компонентов, на которых строится интерфейс системы, доступен для JS-модулей. Вы можете использовать их для создания интерфейса JS-модуля.

Подробная информация о возможностях конфигурации компонентов доступна в витрине компонентов.

Доступны props и слоты (дефолтные и именнованные). Для каждого компонента они описаны в витрине. Методы компонентов, компонент Transition, scoped slots, refs, директивы (кроме v-if/v-show) и модификаторы на данный момент недоступны.

Также доступны события. Актуальный список событий можно посмотреть тут.

Для использования добавьте пакет компонентов в package.json вашего модуля:

npm i --save @retailcrm/embed-ui-v1-components

или

yarn add @retailcrm/embed-ui-v1-components

После чего в файле вашего компонента импортируйте нужный компонент и используйте:

// cases/someModule/SomeExtension.vue

<template>
    <UiToolbarButton>
        {{ t('click') }}
    </UiToolbarButton>
</template>

<script lang="ts" setup>
import { UiToolbarButton } from '@retailcrm/embed-ui-v1-components/remote'

//...
</script>

Разработка JS-приложения

В приложении нужно написать Vue-компонент(-ы) вашего JS-приложения. Например:

// cases/phoneReactive/PhoneReactiveExtension.vue

<template>
    <UiToolbarButton v-if="phone" @click="phone = t('callMade')">
        {{ t('callOn') }}{{ phone }}
    </UiToolbarButton>
</template>

<script lang="ts" setup>
import {
    useOrderCardContext,
    useField,
    useSettingsContext,
} from '@retailcrm/embed-ui'

import { UiToolbarButton } from '@retailcrm/embed-ui-v1-components/remote';
import { useI18n } from 'vue-i18n'
import { watch } from 'vue'

// set locale

const settings = useSettingsContext()
const locale = useField(settings, 'system.locale')

settings.initialize()

const i18n = useI18n()
const t = i18n.t

watch(locale, locale => i18n.locale.value = locale, { immediate: true })

// init order fields

const context = useOrderCardContext()
const phone = useField(context, 'customer.phone')

context.initialize()
</script>

<i18n locale="en-GB">
{
    "callOn": "Let's call on ",
    "callMade": "Call made"
}
</i18n>

<i18n locale="es-ES">
{
    "callOn": "Vamos a llamar a ",
    "callMade": "Llamada realizada"
}
</i18n>

<i18n locale="ru-RU">
{
    "callOn": "Звоним на ",
    "callMade": "Звонок совершен"
}
</i18n>

Во Vue-компоненте важно сразу предусмотреть переводы на 3 языка: русский, английский и испанский.

И далее инициализировать приложение в точках встранивания. Например:

// cases/phoneReactive/index.ts

import { createWidgetEndpoint } from '@retailcrm/embed-ui'
import { fromInsideIframe } from '@remote-ui/rpc'
import { createI18n } from 'vue-i18n'

import PhoneReactiveExtension from './PhoneReactiveExtension.vue'

createWidgetEndpoint({
    async run (createApp, root, pinia) {
        const i18n = createI18n({ legacy: false, fallbackLocale: 'en-GB' })
        const app = createApp(PhoneReactiveExtension)

        app.use(pinia)
        app.use(i18n)
        app.mount(root)

        return () => app.unmount()
    },
}, fromInsideIframe())

Примеры расширений вы можете посмотреть в библиотеке примеров @retailcrm/core-ui-extensions-examples.

Также могут быть полезны для изучения @omnicajs/vue-remote и @retailcrm/embed-ui.

Возможности и ограничения JS-модулей

1. Ограничения на значения атрибутов элементов в шаблонах Vue-компонентов

Элементам можно присваивать атрибуты, например:

<div
    :class="someClass"
    :style="someStyle"
    :data-value="someValue"
/>

В примере выше атрибуты — это class, style и data-value. Нужно помнить, что в эти атрибуты можно передавать только простые стандартные данные: string, number, boolean, null, undefined. Также могут быть переданы массивы простых данных, объекты ключ-значение, в которых значения — простые данные, либо комбинации перечисленных типов. Любые другие данные, переданные как атрибуты, приведут к сбою работы виджета.

Например, нельзя передать экземпляр такого класса:

class MyClass {
  toString() {
    return somethingThatIsString
  }
}

и ожидать, что произойдет автоматическая конвертация. Вместо этого произойдет сбой.

Также на данный момент не поддерживается атрибут ref из Vue на элементах:

<div ref="myRef" />

2. Выполнение HTTP-запросов

В JS-приложении можно выполнять HTTP-запросы только через JS API. Другие способы выполнения HTTP-запросов использовать запрещено, они не будут работать.

Важно!

Вы можете найти работу с http-вызовами в примере cases/fiscalReceipts библиотеки примеров @retailcrm/core-ui-extensions-examples.

Нужно инициализировать переменную от useHost() функции, у которой есть метод httpCall() для http-вызовов.

import { useHost } from '@retailcrm/embed-ui'

const host = useHost()

const { body, status } = await host.httpCall('/get-dictionary')
if (status === 200) {
  const response = JSON.parse(body) // или другой способ обработки, если ожидается не-JSON ответ
} else {
  throw new Error(`HTTP request failed with status: ${status}, body: ${body}`)
}

У функции httpCall(path: string, payload?: string|object) 2 параметра:

  • Обязательный параметр path, на который в бекенд модуля будет отправлен запрос
  • Опциональный параметр payload, в котором строкой или json-объектом можно передать дополнительные параметры

Примеры вызовов:

  • host.httpCall('/get-dictionary')
  • host.httpCall('/get-dictionary', 'somePayload')
  • host.httpCall('/get-dictionary', { order_id: orderId })

При вызове функции система отправляет на {baseUrl}/{path} бекенда модуля POST-запрос c Content-Type: application/x-www-form-urlencoded'.

Например, если модуль задал baseUrl, равный https://some-module.tech и указал path, равный /get-dictionary, то запрос придет на https://some-module.tech/get-dictionary.

В запросе может прийти 2 form-encoded параметра:

  • clientId=<clientIdForAccount> — обязательный параметр со значением clientId, которое модуль установил в данном аккаунте системы
  • payload=<payload> — опциональный параметр, если был передан payload

Если payload был указан строкой, то он будет передан как есть. Если был указан json-объект, то в payload будет передано его строковое представление

Пример передаваемых form-encoded данных:

clientId=client-id-xxx
payload={"order_id":85}

В ответ нужно вернуть ответ с нужным HTTP-статусом и телом. Они будут возвращены в ответе функции httpCall() как есть. В разборе ответа стоит проверять статус и обрабатывать ожидаемый и неожидаемый статус ответа.

Если в ответе возвращается JSON, то в JS-модуле его нужно самостоятельно распарсить.

Система отводит 2 секунды на установку соединения и 5 секунд на формирование ответа. Если превышен таймаут, то вернется HTTP-статус 500 с соответствующей ошибкой завершения по таймауту.

Важно!

При инициализации JS-модуля прямо на странице системы через window['CRM'].embed.register() HTTP-вызовы, содержащиеся в его логике, будут возвращать HTTP-статус 503 c телом ответа REQUIRED_INTEGRATION_MODULE.

Более подробный пример с http-вызовами

import {
    useOrderCardContext,
    useHost,
    useField,
} from '@retailcrm/embed-ui'

import { ref } from 'vue'

const order = useOrderCardContext()
const orderId = useField(order, 'id')

order.initialize()

const host = useHost()

const fetchDictionary = async () => {
  const { body, status } = await host.httpCall('/get-dictionary', { order_id: orderId })
  if (status === 200) {
    return JSON.parse(body) // или другой способ обработки, если ожидается не-JSON ответ
  }

  throw new Error(`HTTP request failed with status: ${status}, body: ${body}`)
}

// можно при взаимодействии с интерфейсом вызвать
const makeSomething = async (data) => {
  const { body, status } = await host.httpCall('/make-something', data)

  if (status !== 200) {
    throw new Error(`HTTP request failed with status: ${status}, body: ${body}`)
  }

  // опционально, обработка ответа
}

const dictionary = ref([])

fetchDictionary().then(data => dictionary.value = data)

3. Работа с пользовательскими полями

JS-приложение может взаимодействовать с пользовательскими полями тех сущностей, в которых они поддерживаются (но на данный момент это только заказ order в таргетах order/card:*).

Важно!

Работу с пользовательскими полями можно посмотреть в примере модуля «Запись к специалисту» @retailcrm/specialist-booking.

Также вы можете найти работу с пользовательскими полями в примере cases/customFieldViewer библиотеки примеров @retailcrm/core-ui-extensions-examples.

В JS API существует специальный тип реактивного контекста, который используется для взаимодействия с пользовательскими полями, определенными в аккаунте.

Для получения схемы контекста нужно использовать функцию useContext:

import { useContext } from '@retailcrm/embed-ui-v1-contexts/remote/custom'

const custom = useContext('order')
custom.initialize()

В функции нужно передать символьный код сущности. В перечне точек встраивания перечислены символьные коды сущностей с доступными пользовательскими полями в колонке «Коды пользовательских контекстов».

Далее из контекста можно получить поля и работать с ними:

/** Импорт composable-утилиты для получения пользовательского контекста. */
import { useContext } from '@retailcrm/embed-ui-v1-contexts/remote/custom'

/** Импорт composable-утилиты для получения вычисляемого поля для пользовательского контекста. */
import { useCustomField } from '@retailcrm/embed-ui'

/**
 * Для получения контекста нужно указать его символьный код. useContext возвращает pinia хранилище
 */
const custom = useContext('order')

/**
 * Метод возвращает `Promise<CustomContextSchema>`
 *
 * Также схему после инициализации можно получить из хранилища: `custom.schema`
 *
 * В метод можно передать в качестве аргумента метод с сигнатурой `(rejection: Rejection) => void` для обработки ошибки,
 * которую может сгенерировать система, если указанный контекст не существует.
 * Определение для `Rejection` можно найти здесь:
 * https://github.com/retailcrm/embed-ui/blob/82477ce0a9124e598d3a84db3739c850509c0fcc/packages/v1-types/context.d.ts#L101-L101
 */
custom.initialize()

/**
 * Вычисляемое поле на основе пользовательского контекста.
 * В useCustomField указываем сначала контекст, затем код поля в контексте.
 * Если код не существует (или контекст еще не инициализирован), значение в поле будет равно `null`
 * Если не указывать третьим параметром опции, то утилита вернет WritableComputedRef. Но это не значит, что setter будет
 * гарантированного работать. Если в настройках поля, пришедших из CRM указан флаг `readonly` равный true, то код
 * будет вызывать ошибку. Также ошибка будет появляться при попытки записи, если контекст еще не проинициализирован.
 */
const someField = useCustomField(custom, 'code')

/**
 * Можно указать, какой тип для конкретного поля ожидается, чтобы дальше TypeScript
 * корректно подсвечивал его. Также можно указать дополнительные опции:
 * `readonly` - логический флаг, который укажет, что поле только для чтения; если этот флаг указан как `true`,
 *   вместо WritableComputedRef будет возвращен ComputedRef
 * `onReject` - обработчик отклонения попытки записи в поле на стороне системы
 *
 * Список доступных значений для параметра kind можно найти здесь:
 * https://github.com/retailcrm/embed-ui/blob/82477ce0a9124e598d3a84db3739c850509c0fcc/packages/v1-types/context.d.ts#L153-L153
 *
 * Если тип поля не совпадает с указанным в присланной схеме, то значение поля всегда будет null
 */
const viewedAt = useCustomField(custom, 'viewedAt', { kind: 'datetime' })

someField.value = 'some-string'
viewedAt.value = new Date().toISOString()

Для полей dictionary и multiselect_dictionary можно загрузить список значений, используя утилиту useDictionary, которая предоставляет клиент для запроса списка значений справочника.

import type { CustomDictionary } from '@retailcrm/embed-ui-v1-types/context'

import { useContext } from '@retailcrm/embed-ui-v1-contexts/remote/custom'
import { useCustomField } from '@retailcrm/embed-ui'
import { useDictionary } from '@retailcrm/embed-ui-v1-contexts/remote/custom'

const custom = useContext('order')

const dictionary = useDictionary()
const options = ref<CustomDictionary>([])
const loaded = ref(false)

custom.initialize().then(schema => {
  const descriptor = schema?.fields.find(f => f.code === 'accessory')
  if (descriptor && 'dictionaryCode' in descriptor) {
    dictionary.query(descriptor.dictionaryCode).then((result) => {
      options.value = result
      loaded.value = true
    })
  } else {
    throw new Error('No dictionary for field with code ' + props.code)
  }
})

const accessory = useCustomField(custom, 'accessory', { kind: 'dictionary' })

4. Генерация ссылок на страницы системы

Важно!

Функциональность генерации ссылок доступна в @retailcrm/embed-ui с версии v0.5.15.

Ссылки на страницы системы можно формировать только указанным ниже способом. Если вы указываете ссылки в явном виде, вы рискуете тем, что они могут стать неработоспособными в случае их изменения со стороны системы.

Вы можете генерировать ссылки на страницы системы, перечисленные в справочнике роутов.

Для генерации требуется добавить в проект JS-модуля пакет @omnicajs/symfony-router, который предоставляет код генерации:

yarn add @omnicajs/symfony-router

Ниже показан пример генерации ссылки на страницу системы. Если текущий пользователь с правами администратора, то подставляется ссылка на страницу пользователя в разделе Настройки либо ссылка на страницу менеджера в противном случае.

<template>
    <UiLink v-if="isAdmin" :href="router.generate('crm_manager_show', { id: userId })">
        View profile
    </UiLink>

    <UiLink v-else :href="router.generate('crm_users_edit', { id: userId })">
        Edit profile
    </UiLink>
</template>

<script lang="ts" setup>
import { useContext as useSettings } from '@retailcrm/embed-ui-v1-contexts/remote/settings'
import { useContext as useUser } from '@retailcrm/embed-ui-v1-contexts/remote/user/current'

import {
    useField,
    useRouter
} from '@retailcrm/embed-ui'

const user = useUser()
const userId = useField(user, 'id')
const isAdmin = useField(user, 'isAdmin')
user.initialize()

const settings = useSettings()
// Нужно инициализировать контекст настроек,
// т.к. useRouter утилита использует этот контекст и его поле `system.routing`
settings.initialize()

// useRouter возвращает ComputedRef
const router = useRouter()
</script>

Также вы можете посмотреть генерацию и подстановку ссылок в кейсе cases/orderNotes библиотеки примеров @retailcrm/core-ui-extensions-examples.

Встраивание в интерфейс системы

Сборка JS-приложения

Вам требуется выполнить сборку приложения средствами webpack, vite и т.п. В @retailcrm/core-ui-extensions-examples сборка производится командой yarn build. В папке сборки должен появиться набор из index.html, css-файла и js-файла.

После этого требуется создать в папке сборки файл manifest.json

Структура manifest.json

Файл manifest.json содержит метаданные для JS-приложения. В @retailcrm/core-ui-extensions-examples файл создается автоматически при выполнении команды make zip-archive. Пример содержимого файла manifest.json:

{
  "code": "core-ui-extensions",
  "version": 154,
  "targets": ["order/card:delivery.address"],
  "entrypoint": "index.html",
  "scripts": ["extension.xxx.js"],
  "stylesheet": "extension.xxx.css"
}

Где:

  • code: string — уникальный код приложения. Обязательный параметр
  • version: number — версия приложения. Значение должно быть целым числом и больше 0. Обязательный параметр
  • targets: string[] — точки встраивания приложения. Обязательный параметр
  • entrypoint: string — HTML-файл, который является точкой входа для приложения. Обязательный параметр
  • scripts: string[] — JS-файлы приложения. Обязательный параметр
  • stylesheet: ?string — CSS-файл приложения. Необязательный параметр

Встраивание JS-приложения в интерфейс в ходе разработки

В ходе разработки для целей отладки вы можете встраивать JS-приложение на тех страницах, на которых находятся точки встраивания (targets) приложения.

Для этого запустите сервер, который будет отдавать файлы вашего приложения на базе nodejs, nginx или другого веб-сервера. В @retailcrm/core-ui-extensions-examples есть пример сервера в файле server.mjs. Вы можете его запустить командой node server.mjs.

После этого на нужной странице системе достаточно вызвать window['CRM'].embed.register(). Например:

window['CRM'].embed.register({
  "uuid": "62aa8145-ed53-4862-b28f-f1bc6b36a3a3",
  "targets": [
    "order/card:delivery.address"
  ],
  "entrypoint": "http://localhost:3000/extension/62aa8145-ed53-4862-b28f-f1bc6b36a3a3",
  "stylesheet": "http://localhost:3000/extension/62aa8145-ed53-4862-b28f-f1bc6b36a3a3/stylesheet"
})

Значение uuid может быть произвольным.

Публикация frontend-части

Публикация в маркетплейсе

Как было описано в начале статьи, модуль заводится в маркетплейсе, как и любой другой модуль, и через партнерский отдел в нем требуется активировать JS-функциональность.

После этого в карточке модуля будет доступна форма «JS-файл» для загрузки архива JS-части модуля. В @retailcrm/core-ui-extensions-examples есть команда make zip-archive, где можно посмотреть, как создать архив с модулем.

Архив загружается через форму. Если модуль опубликован в Маркетплейсе, то загруженная версия начнет инициализироваться в интерфейсе аккаунтов системы, где включен модуль.

При изменении исходного кода модуля (добавлении новых функций или исправлении ошибок) вам требуется его собрать, создать архив с новым кодом и загрузить через форму. Новая версия будет практически сразу доступна в аккаунтах, где включен модуль (возможны задержки до 10 мин).

Регистрация JS-модуля в отдельно-взятом аккаунте

Если вы разрабатываете кастомизированный JS-модуль для определенного аккаунта системы, вы можете зарегистрировать его с помощью API-метода POST /api/v5/integration-modules/{code}/edit, указав следующие данные:

  • integrationModule[baseUrl] — базовый адрес сервера модуля
  • integrationModule[integrations][embedJs][entrypoint] — относительный путь к HTML-файлу, который является точкой входа
  • integrationModule[integrations][embedJs][stylesheet] — относительный путь в css-стилям
  • integrationModule[integrations][embedJs][targets] — массив точек встраивания

Если вызов метода был успешный, то на страницах целевого аккаунта, на которых находятся указанные точки встраивания, будет инициализироваться JS-часть вашего модуля.

Благодарим за отзыв.
Была ли статья полезна?
Нет
  • Рекомендации не помогли
  • Нет ответа на мой вопрос
  • Текст трудно понять
  • Не нравится описанный функционал
Да
Предыдущая статья
Работа с ценами на модуль
В Маркетплейс можно размещать как бесплатные, так и платные модули.
Следующая статья
Точки встраивания JS-модулей
Точки интерфейса, куда могут встроиться JS-модули, а также доступные в этих точках объекты