JS-модули позволяют расширить возможности интерфейса системы.
Архитектура JS-модуля
JS-модули является расширенной версией стандартных модулей Маркетплейса. Отличием является то, что они состоят из двух частей: backend-часть и frontend-часть.
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 вы можете найти реализацию переводом надписей и выставление локали в соответствии с локалью аккаунты системы.
Ограничения 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-запросов использовать запрещено, они не будут работать.
Нужно инициализировать переменную от 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)
Также вы можете найти работу с http-вызовами в примере cases/fiscalReceipts
библиотеки примеров @retailcrm/core-ui-extensions-examples.
Точки встраивания (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-приложения
Вам требуется выполнить сборку приложения средствами 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-часть вашего модуля.