Перейти к основному содержимому

Поддержка 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) с поддержкой внешних зависимостей.

подсказка
Формально ES6 — сокращенное название стандарта ECMAScript 2015, однако JAICP поддерживает и возможности более поздних стандартов. Для простоты в этой статье они объединены под названием 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), классы, асинхронные функции и многое другое.

подсказка
Подробнее о возможностях ES6 по сравнению с ES5 вы можете узнать, например, в книге доктора Акселя Раушмаера Exploring ES6.

Есть и другие теги JAICP DSL, внутри которых пишется JS-код: if, elseif, init. Пока что эти теги не имеют аналогов, в которых можно использовать ES6.

Подключение зависимостей​

Как и раньше, сложную бизнес-логику рекомендуется не описывать напрямую внутри тега scriptEs6, а выносить ее в отдельные JS-файлы, которые затем подключаются к сценарию при помощи тега require. Однако есть важные отличия между тем, как используются именованные объекты (переменные, функции, классы) из файлов, написанных на ES5 и ES6.

Зависимости на ES5​

Если код зависимости написан на ES5 и запускается в старой среде исполнения, то JS-файл нужно обязательно импортировать в сценарий при помощи тега require без параметров.

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

require: dialog.js
require: defaults.js

theme: /

state: Hello
q!: * привет *
script:
sayHello();

Зависимости на ES6​

В новой среде исполнения каждый JS-файл представляет собой обособленный модуль. По умолчанию все именованные объекты доступны только внутри модуля. Чтобы сделать их видимыми извне, их нужно экспортировать при помощи ключевого слова export.

В ES6 существует два способа экспортировать что-либо из модуля: именованные экспорты и экспорты по умолчанию.

  • Чтобы обращаться к объектам из зависимостей на ES6 из тегов scriptEs6, в файле зависимости обязательно должен быть объявлен экспорт по умолчанию через директиву export default. Далее файл подключается в сценарий через require с дополнительными параметрами.

  • Чтобы обращаться к таким объектам из других зависимостей на ES6, их можно экспортировать любым из двух способов, а импортировать через ключевое слово import. При этом если в самом сценарии эти экспорты не нужны, то и подключать файл через тег require не нужно.

require: dialog.js
type = scriptEs6
name = dialog

# Импортировать defaults.js не нужно — он не используется в scriptEs6

theme: /

state: Hello
q!: * привет *
scriptEs6:
dialog.sayHello();

Параметры тега require​

Чтобы подключить к сценарию JS-файл на ES6, используйте тег require с параметрами:

  • type — всегда должен иметь значение scriptEs6.
  • name — любая строка, которую можно использовать как имя JS-переменной.

Параметр name задает имя, которое будет присвоено значению, экспортированному из модуля. Под этим именем к нему можно обратиться из сценария.

подсказка
Явная квалификация каждого модуля соответствующим именем позволяет не засорять глобальное пространство имен.

Глобальные переменные​

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

Если вы хотите использовать глобальные переменные, в нужном модуле объявите их как свойства системного объекта global, после чего подключите модуль к сценарию. При этом объявление экспорта по умолчанию внутри модуля и указание параметра name у тега require по-прежнему обязательны.

предупреждение
Использование глобальных переменных не рекомендуется: такой подход засоряет глобальное пространство имен и приводит к неявным зависимостям между модулями.

В примере ниже при помощи Proxy объявляется объект $ — алиас объекта $context, который позволяет в более лаконичной записи обращаться к его свойствам:

require: globals.js
type = scriptEs6
name = globals

theme: /

state: Back || noContext = true
q: назад
scriptEs6:
$reactions.transition($.contextPath); // То же, что $context.contextPath

Конфигурация среды исполнения в 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-пакеты, написанные сторонними разработчиками.

Как подключить и использовать пакет​

  1. Создайте в директории src файл package.json. Если вы переопределили requirementsFile, создайте файл по тому пути, который вы задали сами.

  2. Задайте в этом файле свойство dependencies. Его значением должен быть объект, где ключи — названия требуемых пакетов, а значения — их версии.

    {
    "dependencies": {
    "luxon": "^3.0.3"
    }
    }
    подсказка
    Если вы разрабатываете проект локально, рекомендуется устанавливать зависимости через npm или любой другой пакетный менеджер, например yarn. Пакетные менеджеры могут сами определять оптимальные версии зависимостей и заполнять файл package.json автоматически. В файле могут быть заданы и другие свойства, но JAICP будет учитывать только dependencies.
  3. Используйте пакет так же, как внутренние зависимости: импортируйте нужные объекты через import и обращайтесь к ним в нужных местах своего кода.

    require: time.js
    type = scriptEs6
    name = time

    theme: /

    state: WhatTimeIsIt
    q!: * который [сейчас] час *
    scriptEs6:
    $reactions.answer(time.now());

Ограничения на доступные пакеты​

Если в сценарии используются внешние зависимости, 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 — не все возможности перенесены в новую среду исполнения. Список недоступных возможностей приведен ниже и постепенно будет уменьшаться.

подсказка
Если нужная функциональность напрямую не связана с JAICP, используйте внешние зависимости, чтобы компенсировать ее недостаток. Например, вместо $http отлично подойдет HTTP-клиент Axios.

Недоступные возможности​

Функции:

Сервисы:

  • $http
  • $nlp — методы createClassifier, fixKeyboardLayout, match, matchExamples, matchPatterns, setClass.

Асинхронные методы​

В старой среде исполнения все методы встроенных сервисов JS API работают синхронно — блокируют поток обработки запроса и возвращают результат напрямую. В новой среде исполнения для некоторых сервисов это поведение изменено без сохранения обратной совместимости:

Теперь данные методы являются асинхронными и возвращают 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, для таких проверок не будут работать следующие теги:

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

Примеры проектов​

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

  • MDN Web Docs — портал по веб-разработке от Mozilla. Его раздел про JavaScript наиболее приближен по статусу к официальной документации языка.

  • Exploring JS — книги по JS от уже упомянутого выше доктора Акселя Раушмаера, в том числе о новейших спецификациях ECMAScript.

  • Introduction To Node.js — официальное введение в Node.js. Из него вы больше узнаете про менеджмент пакетов, встроенные модули и другие особенности разработки под эту платформу.