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

Поддержка 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. Из него вы больше узнаете про менеджмент пакетов, встроенные модули и другие особенности разработки под эту платформу.