Web форма построенная на LWC + salesforce apex

Всем привет! Я работаю в компании FLEETCOR [1] в департаменте разработки Salesforce.

Сама платформа Salesforce имеет узкоспециализированные инструменты и технологии для разработки в самой платформе (Salesforce — это CRM). Хотя последняя frontend-технология LWC уже заявлена как open source.

Хочу рассказать об одном из последних наших проектов, довольно объемном и интересном со стороны используемых решений и технологий.

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

С концептом более или менее понятно. Теперь о технологическом стеке.

На стороне пользователя это интерфейс, построенный на Lightning Web Components с использованием Lightning Design System для оформления. Там, где не хватало лайтнинга, мы использовали классику — HTML5 + CSS3 + JavaScript.

На стороне бэкенда это встроенный язык платформы, он называется Apex и построен на всем известной Java. Также веб-сервисы для обработки данных на нашей стороне, написанные на Java, база данных на Oracle для хранения данных. Связь платформы Salesforce (в облаке) с нашими собственными системами через интеграционную платформу IBM Gateway и внутренний транспорт на нашей стороне Apache Kafka.

Теперь немного об интерфейсе. Он, как уже написано выше, построен на Lightning Web Components. Из названия понятно, что использована концепция компонентного фронта. Есть базовый компонент, представляющий всю форму, а внутри множество компонентов, каждый из которых представляет собой логическую часть интерфейса (блок информации, объединенной общим смыслом, или блок управляющих кнопок).

Это отдельный компонент.
Это отдельный компонент.

Так как наша компания интернациональная, имеется практика делать интерфейс мультиязычным. К счастью, платформа Salesforce позволяет делать это нативными инструментами.

У нас эта мультиязычность была реализована путем создания справочника «лейблов» в системе, и каждый такой «лэйбл» имел настройку перевода на нужные языки. Это все организовано через инструмент платформы custom label, так что в дальнейшем админы могут без проблем подправить форму, где нужно, без багов, косяков и правки кода. Все через настройки, и это круто! Я с командой всегда стараюсь все делать максимально удобно пользователей, администраторов систем и для дальнейшей поддержки :)

С технической точки зрения это выглядит так: 1) есть настройка «лейбла» (название на оригинале, английский + переводы на нужные языки); 2) дальше все «лейблы» импортируются в общем утилитном компоненте проекта; 3) все заинтересованные компоненты используют утилитный.

Таким образом, код получается структурированным и чистым. Ну и небольшой пример.

Настройка «лейбла» с переводом (аналогично нескольким записям в таблицах БД).
Настройка «лейбла» с переводом (аналогично нескольким записям в таблицах БД).

Импорт в утилитный компонент:

// textUtils.js import TARIFF_AND_SERVICES from "@salesforce/label/c.TariffAndServices"; import TARIFF from "@salesforce/label/c.Tariff"; import CARDS_AMOUNT from "@salesforce/label/c.CardsAmount"; // ... const LABELS = { // ... TARIFF_AND_SERVICES, TARIFF, CARDS_AMOUNT, // ... }; // ... class TextUtils {} export { TextUtils, LABELS, // ... };

Использование в компоненте:

// tariffAndServices.js import { LightningElement, api, wire, track } from "lwc"; import { LABELS } from "c/textUtils"; // ... export default class TariffAndServices extends LightningElement { // ... }
<!-- tariffAndServices.html --> <template> <lightning-card variant="base"> <h3 slot="title" class="header exportable-header">{labels.TARIFF_AND_SERVICES}</h3> <lightning-combobox name="selectedTariff" class="field exportable-field exportable-picklist" label={labels.TARIFF} options={tariffs} value={tariffAndServices.selectedTariff} placeholder={labels.CHOOSE_TARIFF} onchange={handelChange} > </lightning-combobox> <lightning-input name="cardsAmount" class="field exportable-field" label={labels.CARDS_AMOUNT} required max-length="4" min="0" max="1998" value={tariffAndServices.cardsAmount} onchange={handelChange} type="number" > </lightning-input> <!-- ... --> </lightning-card> </template>

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

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

{ "streetName": "ул. Покровка", "region": "г. Москва", "postCode": "101000", "city": "г. Москва", "buildingNumber": "40", "block": "2А", "address": "г. Москва, вн.тер.г. муниципальный округ Басманный, ул. Покровка, д. 40, стр. 2А" }

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

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

Раз.

// функция возвращает массив строк, основанный на массиве stringArr, который наиболее подобен строке targetString const findSimilarStringInArr = (stringArr, targetString) => { // stringArr — массив строк, где ищем // targetString — подобное чему ищем let resultAsArr = []; if (targetString != null) { let index; let maxStringSimilarityCoef = 0; // ищем максимально подобную строку из массива с помощью расчета коэффициента подобия for (let i = 0; i < stringArr.length; i++) { let stringSimilarityCoeff = getStringSimilarityCoeff( targetString, stringArr[i] ); // запоминаем максимум коэффициента подобия и расположение наиболее похожего на targetString элемента в stringArr if (stringSimilarityCoeff > maxStringSimilarityCoef) { maxStringSimilarityCoef = stringSimilarityCoeff; index = i; } } // добавляем этот наиболее похожий элемент в результирующий массив resultAsArr.push(stringArr[index]); // смотрим элементы исходного массива слева от наиболее подобного let comparisonValue = stringArr[index]; for (let i = index - 1; i >= 0; i--) { // наращиваем проверяемую строку добавлением элемента слева от максимально подобного и проверяем ее коэффициент подобия comparisonValue += stringArr[i]; let stringSimilarityCoeff = getStringSimilarityCoeff( targetString, comparisonValue ); // до тех пор, пока коэффициент не начнет уменьшаться, добавляем новые элементы в начало результирующего массива // справа налево от максимально подобного // как только коэффициент уменьшается, возвращаем проверяемую строку к изначальному значению максимально подобного элемента if (stringSimilarityCoeff < maxStringSimilarityCoef) { comparisonValue = stringArr[index]; break; } resultAsArr.unshift(stringArr[i]); maxStringSimilarityCoef = stringSimilarityCoeff; } // таким образом тут мы получаем resultAsArr, набор элементов которого наиболее похож на targetString // теперь смотрим элементы исходного массива справа от наиболее подобного for (let i = index + 1; i < stringArr.length; i++) { // наращиваем проверяемую строку добавлением элемента слева от максимально подобного и проверяем ее коэффициент подобия comparisonValue += stringArr[i]; let stringSimilarityCoeff = getStringSimilarityCoeff( targetString, comparisonValue ); // до тех пор, пока коэффициент не начнет уменьшаться, добавляем новые элементы в конец результирующего массива // слева направо от максимально подобного // как только коэффициент уменьшается, заканчиваем сравнение if (stringSimilarityCoeff < maxStringSimilarityCoef) { break; } resultAsArr.push(stringArr[i]); maxStringSimilarityCoef = stringSimilarityCoeff; } // возвращаем массив подстрок, который при склеивании будет наиболее подобен строке targetString return resultAsArr; } };

Два.

// функция рассчитывает коэффициент подобия двух строк const getStringSimilarityCoeff = (firstString, secondString) => { if (firstString != null && secondString != null) { if (firstString.length != 0 && secondString.length != 0) { // превращаем строки в набор символов firstString = firstString.replace(/[\s.,%]/g, ""); secondString = secondString.replace(/[\s.,%]/g, ""); // счетчик совпадений let sumOfSameSymbols = 0; // сравниваем строки for (let sym of firstString) { if (secondString.includes(sym)) { // убираем уже найденные совпадения secondString = secondString.replace(sym, ""); sumOfSameSymbols += 1; } } // рассчитываем коэффициент let coeff = sumOfSameSymbols / (firstString.length + secondString.length - sumOfSameSymbols); return coeff; } } };

Тут можно заметить, что при поиске слева или справа проверяемая строка наращивает новые элементы в произвольном порядке (не так, как расположены элементы в исходном массиве). Но для этого алгоритма и функции расчета коэффициента это не так важно, поэтому для упрощения и ускорения направление не учитывается.

Второй кейс уже относится конкретно к платформе Salesforce.

Так как часть пользователей у нас пользуется старым интерфейсом платформы SF Classic, нам нужно было предоставить доступ к странице, построенной на новом компонентом интерфейсе, через этот старый интерфейс.

Работает так: открываем через стандартную кнопку из классики страницу Visualforce, в ней открываем Aura Component, а уже в нем — lwc-компонент. Проще говоря, новая технология в старой обертке.

Из-за этого стандартные lwc-toast-уведомления не работают. Мы сделали кастомный Toast. Возможно, кто-то уже его ищет.

<!--customToast.html--> <template> <!-- показать или скрыть--> <template if:true={showToastModel}> <div class="slds-notify_container"> <!-- стиль обертки зависит от типа уведомления--> <div class={outerClass} role="status"> <!-- тип уведомления--> <span class="slds-assistive-text">{type}</span> <!-- сам текст уведомления--> <!-- стиль сообщения зависит от типа--> <span class={innerClass} title={message}> <!-- иконка также зависит от типа--> <lightning-icon icon-name={iconName} alternative-text="icon" styleclass="slds-icon slds-icon_small" variant="inverse" size="small" ></lightning-icon> </span> <div class="slds-notify__content"> <h2 class="slds-text-heading_small">{message}</h2> </div> <div class="slds-notify__close"> <!-- кнопка-иконка для ручного скрытия сообщения--> <lightning-button-icon icon-name="utility:close" size="small" variant="border-filled" class="slds-button slds-button_icon slds-button_icon-inverse" alternative-text="next" onclick={closeModel} ></lightning-button-icon> </div> </div> </div> </template> </template>
//customToast.js import { LightningElement, api, track } from "lwc"; export default class CustomToast extends LightningElement { // тип может быть success, error или info, меняется стиль @track type; @track message; @track showToastModel = false; // время автоматического исчезновения, публичное свойство, можно настроить @api autoCloseTime = 3000; // метод для вызова уведомления из других компонентов @api showToast(type, message) { this.type = type; this.message = message; this.showToastModel = true; setTimeout(() => { this.closeModel(); }, this.autoCloseTime); } // метод, автоматически скрывающий уведомление closeModel() { this.showToastModel = false; this.type = ""; this.message = ""; } // геттеры используются в самом кастомном Toast для определения стиля уведомления в зависимости от типа get iconName() { return "utility:" + this.type; } get innerClass() { return ( "slds-icon_container slds-icon-utility-" + this.type + " slds-icon-utility-success slds-m-right_small slds-no-flex slds-align-top" ); } get outerClass() { return "slds-notify slds-notify_toast slds-theme_" + this.type; }

И он просто внедряется в любой компонент без доработок, вот так:

<!-- это основной компонент--> <c-custom-toast></c-custom-toast>

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

// основной компонент showToast(toastType, toastMessage) { this.template .querySelector("c-custom-toast") .showToast(toastType, toastMessage); }
1
3 комментария