Поддержка ECMAScript 6 в JAICP
Бета
До осени 2022 года в JAICP поддерживался только один диалект JavaScript (далее JS), а именно его реализация согласно спецификации ECMAScript 5 (ES5) на движке Nashorn. Она была предложена в 2009 году и успела значительно устареть, поэтому возможности написания JS-кода в JAICP не в полной мере отражают современные профессиональные стандарты.
-
Разработчики на JAICP не могут либо могут лишь ограниченно использовать востребованные возможности современных версий JS. В свою очередь, новые пользователи с опытом разработки не могут в полной мере применить свои навыки.
-
Среда исполнения JS в JAICP изолирована от экосистемы пакетов, сформированной сообществом JS-разработчиков, и не позволяет подключать к проектам внешние зависимости, кроме встроенных в платформу (Underscore.js и Moment.js).
Сейчас в JAICP доступна новая среда исполнения JS, которая решает обе эти проблемы и открывает возможность разрабатывать проекты на современном диалекте JS (далее ES6) с поддержкой внешних зависимостей.
Тег scriptEs6
В JAICP DSL доступен тег реакции scriptEs6
.
Тег работает аналогично script
с той разницей, что при попадании в стейт код внутри тега запускается в новой среде исполнения.
Это позволяет использовать в нем возможности ES6, недоступные внутри обычного тега script
:
state: HeadsOrTails
q!: * {орел * решка} *
scriptEs6:
const bit = $reactions.random(2); // Объявление переменной через const
$reactions.answer(`Результат: ${bit ? "орел" : "решка"}.`); // Формирование строки по шаблону
Для запуска кода используется платформа Node.js версии 20.
Это означает, что внутри него доступны возможности спецификаций вплоть до ECMAScript 2023,
в том числе разбиение кода на модули, новые структуры данных (множества Set
, отображения Map
), классы, асинхронные функции и многое другое.
Есть и другие теги JAICP DSL, внутри которых пишется JS-код: if
, elseif
, init
.
Пока что эти теги не имеют аналогов, в которых можно использовать ES6.
Подключение зависимостей
Как и раньше, сложную бизнес-логику рекомендуется не описывать напрямую внутри тега scriptEs6
, а выносить ее в отдельные JS-файлы,
которые затем подключаются к сценарию при помощи тега require
.
Однако есть важные отличия между тем, как используются именованные объекты (переменные, функции, классы) из файлов,
написанных на ES5 и ES6.
Зависимости на ES5
Если код зависимости написан на ES5 и запускается в старой среде исполнения,
то JS-файл нужно обязательно импортировать в сценарий при помощи тега require
без параметров.
При этом если на верхнем уровне вложенности этого файла объявлены переменные или функции,
то они автоматически помещаются в глобальную область видимости.
К ним можно обратиться из любого тега script
, scriptEs6
, а также из любого другого подключенного JS-файла.
- main.sc
- dialog.js
- defaults.js
require: dialog.js
require: defaults.js
theme: /
state: Hello
q!: * привет *
script:
sayHello();
function sayHello() {
// Предположим, что имя пользователя содержится в $client.name
var name = $jsapi.context().client.name;
// Переменная DEFAULT_CLIENT_NAME задана в defaults.js, и к ней можно обратиться
$reactions.answer("Привет, " + (name || DEFAULT_CLIENT_NAME) + "!");
}
var DEFAULT_CLIENT_NAME = "таинственный незнакомец";
Зависимости на ES6
В новой среде исполнения каждый JS-файл представляет собой обособленный модуль.
По умолчанию все именованные объекты доступны только внутри модуля.
Чтобы сделать их видимыми извне, их нужно экспортировать при помощи ключевого слова export
.
В ES6 существует два способа экспортировать что-либо из модуля: именованные экспорты и экспорты по умолчанию.
-
Чтобы обращаться к объектам из зависимостей на ES6 из тегов
scriptEs6
, в файле зависимости обязательно должен быть объявлен экспорт по умолчанию через директивуexport default
. Далее файл подключается в сценарий черезrequire
с дополнительными параметрами. -
Чтобы обращаться к таким объектам из других зависимостей на ES6, их можно экспортировать любым из двух способов, а импортировать через ключевое слово
import
. При этом если в самом сценарии эти экспорты не нужны, то и подключать файл через тегrequire
не нужно.
- main.sc
- dialog.js
- defaults.js
require: dialog.js
type = scriptEs6
name = dialog
# Импортировать defaults.js не нужно — он не используется в scriptEs6
theme: /
state: Hello
q!: * привет *
scriptEs6:
dialog.sayHello();
import { DEFAULT_CLIENT_NAME } from "./defaults.js"; // Расширение указывать обязательно
// Вместо function здесь используется стрелочная функция
const sayHello = () => {
// Получение $client.name через деструктуризацию
const { client: { name } } = $jsapi.context();
// Вместо || используется ?? — оператор нулевого слияния
$reactions.answer(`Привет, ${name ?? DEFAULT_CLIENT_NAME}!`);
};
export default { sayHello }; // Экспорт по умолчанию обязателен
export const DEFAULT_CLIENT_NAME = "таинственный незнакомец"; // Именованный экспорт
// Экспорт по умолчанию здесь не нужен
Параметры тега require
Чтобы подключить к сценарию JS-файл на ES6, используйте тег require
с параметрами:
type
— всегда должен иметь значениеscriptEs6
.name
— любая строка, которую можно использовать как имя JS-переменной.
Параметр name
задает имя, которое будет присвоено значению, экспортированному из модуля.
Под этим именем к нему можно обратиться из сценария.
Глобальные переменные
Для обратной совместимости со старой средой исполнения оставлена возможность обращаться к значениям, объявляемым внутри модуля, как к глобальным переменным — без необходимости каждый раз указывать пространство имен.
Если вы хотите использовать глобальные переменные, в нужном модуле объявите их как свойства системного объекта global
, после чего подключите модуль к сценарию.
При этом объявление экспорта по умолчанию внутри модуля и указание параметра name
у тега require
по-прежнему обязательны.
В примере ниже при помощи Proxy
объявляется объект $
— алиас объекта $context
,
который позволяет в более лаконичной записи обращаться к его свойствам:
- main.sc
- globals.js
require: globals.js
type = scriptEs6
name = globals
theme: /
state: Back || noContext = true
q: назад
scriptEs6:
$reactions.transition($.contextPath); // То же, что $context.contextPath
global.$ = new Proxy(
{},
{
get(target, name) {
return $context[name];
},
}
);
export default {};
Конфигурация среды исполнения в chatbot.yaml
Новая среда исполнения не требует какой-либо конфигурации:
если в сценарии есть тег scriptEs6
или подключена зависимость с таким типом, их код будет автоматически исполнен как ES6.
Однако в тестовых целях вы можете частично настроить поведение новой среды исполнения.
Для этого используйте секцию scriptRuntime
в конфигурационном файле chatbot.yaml
.
scriptRuntime:
requirementsFile: package.json
npmRcFile: .npmrc
forceEs6: true
-
requirementsFile
— путь до JSON-манифеста с объявлением внешних зависимостей, которые будут использоваться в сценарии, относительно директорииsrc
. По умолчаниюpackage.json
. -
npmRcFile
— путь до конфигурационного файла npm, относительно директорииsrc
. По умолчанию.npmrc
. -
forceEs6
— если указаноtrue
, то код всех тегов реакций (не толькоscript
, но и, например, тегаa
) будет запускаться в новой среде исполнения. По умолчаниюfalse
.предупреждениеЗапуск всех тегов в новой среде исполнения в настоящее время не оптимизирован. Рекомендуется не включать параметрforceEs6
, если важно сохранить скорость ответа бота.
Подключение npm-пакетов
Ключевое преимущество новой среды исполнения над старой заключается в том, что для расширения возможностей бота вы можете использовать не только встроенное JS API, но и внешние зависимости — npm-пакеты, написанные сторонними разработчиками.
Как подключить и использовать пакет
-
Создайте в директории
src
файлpackage.json
. Если вы переопределилиrequirementsFile
, создайте файл по тому пути, который вы задали сами. -
Задайте в этом файле свойство
dependencies
. Его значением должен быть объект, где ключи — названия требуемых пакетов, а значения — их версии.{
"dependencies": {
"luxon": "^3.0.3"
}
}подсказкаЕсли вы разрабатываете проект локально, рекомендуется устанавливать зависимости через npm или любой другой пакетный менеджер, например yarn. Пакетные менеджеры могут сами определять оптимальные версии зависимостей и заполнять файлpackage.json
автоматически. В файле могут быть заданы и другие свойства, но JAICP будет учитывать толькоdependencies
. -
Используйте пакет так же, как внутренние зависимости: импортируйте нужные объекты через
import
и обращайтесь к ним в нужных местах своего кода.- main.sc
- time.js
require: time.js
type = scriptEs6
name = time
theme: /
state: WhatTimeIsIt
q!: * который [сейчас] час *
scriptEs6:
$reactions.answer(time.now());import { DateTime } from "luxon"; // При импорте из пакетов не указывайте расширение
export default {
now: () => DateTime.now().setLocale("ru").toLocaleString(DateTime.TIME_24_SIMPLE)
};
Ограничения на доступные пакеты
Если в сценарии используются внешние зависимости,
JAICP устанавливает их перед публикацией бота в канал, фактически выполняя команду npm install
.
Во время дальнейшей работы бота JAICP уже не может выполнять никакие команды (например, те, что объявлены в секции scripts
в package.json
), поэтому:
- В JAICP не будут корректно работать пакеты, которые требуют запуска команд наподобие
npm start
. В частности, это Express и любые другие пакеты, которые самостоятельно запускают локальные серверы. - Код для запуска в новой среде исполнения не получится писать на TypeScript,
поскольку он требует отдельного этапа компиляции исходного кода через
tsc
.
Также рекомендуем с осторожностью подходить к пакетам, которые уже после запуска генерируют артефакты, необходимые им для работы. Пример такого пакета — Prisma: она генерирует клиент для подключения к СУБД по заданной схеме данных. Боты на ECMAScript 6 имеют возможность работать с файловой системой, однако она не предназначена для постоянного хранения файлов и в любой момент может быть очищена.
Конфигурация npm
Продвинутая возможность
Для дополнительного контроля над установкой npm-пакетов вы можете использовать конфигурационный файл .npmrc
.
Разместите его в директории src
или по пути, который вы задали самостоятельно как параметр npmRcFile
в chatbot.yaml
.
Например, вы можете использовать не только пакеты из публичного реестра https://registry.npmjs.org/
, но и из приватного.
Для этого в файле .npmrc
укажите адрес реестра и токен для авторизации:
registry=https://nexus.just-ai.com/repository/npm-group/
_authToken=NpmToken.<myToken>
Использование встроенного JS API
JAICP предоставляет встроенный JS API — набор глобальных переменных, функций и сервисов, которые доступны из любой точки сценария. С помощью него можно реализовать часто используемую функциональность, актуальную для самых разных проектов:
- Переменные
$session
и$client
предоставляют нативное хранилище данных о сессии и клиенте без необходимости подключать к боту свою базу данных. - Сервис
$http
дает возможность интегрировать бота практически с любой внешней системой, к которой можно обратиться по HTTP API. - Сервисы
$analytics
,$imputer
,$pushgate
и ряд других позволяют вызывать из сценария различные специализированные подсистемы самой JAICP.
Существует встроенный сервис $storage
, который доступен только на ES6.
На данный момент внутри тегов scriptEs6
и зависимостей на ES6 вы можете использовать ограниченное подмножество JS API —
не все возможности перенесены в новую среду исполнения.
Список недоступных возможностей приведен ниже и постепенно будет уменьшаться.
$http
отлично подойдет HTTP-клиент Axios.Недоступные возможности
Функции:
Сервисы:
$http
$nlp
— методыcreateClassifier
,fixKeyboardLayout
,match
,matchExamples
,matchPatterns
,setClass
.
Асинхронные методы
В старой среде исполнения все методы встроенных сервисов JS API работают синхронно — блокируют поток обработки запроса и возвращают результат напрямую. В новой среде исполнения для некоторых сервисов это поведение изменено без сохранения обратной совместимости:
$aimychat
$analytics
— методjoinExperiment
.$caila
— все методы, кромеsetClientNerId
иclearClientNerId
.$conversationApi
$faq
$gpt
$imputer
$integration
$jsapi
— методыchatHistory
,chatHistoryJson
,getVoiceMessageAsrLang
,setVoiceMessageAsrLang
.$mail
— методыsend
иsendMessage
.$nlp
— методыconform
,inflect
,parseMorph
.$pushgate
Теперь данные методы являются асинхронными и возвращают Promise
(промис).
Чтобы работать с возвращаемыми значениями, вы можете использовать стандартный интерфейс промисов: методы then
и catch
—
либо дожидаться их завершения при помощи ключевого слова await
.
require: patterns.sc
module = sys.zb-common
theme: /
state: PartOfSpeech
q!: * часть речи * ~слово $AnyWord *
scriptEs6:
const markup = await $caila.markup($parseTree._AnyWord);
$temp.pos = markup?.words?.[0]?.annotations?.pos;
if: $temp.pos
a: У слова {{$parseTree._AnyWord}} тег части речи {{$temp.pos}}.
else:
a: Не знаю...
scriptEs6
оборачивается в асинхронную функцию, поэтому ключевое слово await
разрешено.Встроенные переменные в асинхронных функциях
В среде ECMAScript 6 вы можете написать свои асинхронные функции в тегах scriptEs6
и JS-файлах.
Сейчас в таких функциях поддерживаются только следующие встроенные переменные:
Синтаксис
-
Запись значения в переменную:
$session.example = "Example";
-
Получение значения из переменной:
var example = await $session.example;
Пример
Операции с одной переменной гарантированно выполняются последовательно.
async function sendCode() {
$session.code = 1234;
$conversationApi.sendTextToClient(await $session.code);
}
В этом примере метод $conversationApi.sendTextToClient()
не отправит ответ мгновенно.
Он будет отправлен только после того, как закончилась операция по записи значения в $session.code
.
Запуск XML-тестов
JAICP предоставляет XML-тесты для автоматической проверки функциональности бота при публикации. Как и в случае с JS API, в новую среду исполнения перенесена не вся их функциональность.
Если в каком-либо тест-кейсе вы хотите проверить код, который выполняется внутри тега scriptEs6
,
для таких проверок не будут работать следующие теги:
Примеры проектов
Мы подготовили примеры проектов, которые работают в новой среде исполнения и демонстрируют ее возможности. Вы можете просмотреть их исходный код на GitHub, а оттуда же сразу создать проект в JAICP.
Полезные ссылки
-
MDN Web Docs — портал по веб-разработке от Mozilla. Его раздел про JavaScript наиболее приближен по статусу к официальной документации языка.
-
Exploring JS — книги по JS от уже упомянутого выше доктора Акселя Раушмаера, в том числе о новейших спецификациях ECMAScript.
-
Introduction To Node.js — официальное введение в Node.js. Из него вы больше узнаете про менеджмент пакетов, встроенные модули и другие особенности разработки под эту платформу.