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

Выполнение динамических скриптов

warning

Данная статья носит ознакомительный характер.

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

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

Предлагаю рассмотреть следующий случай: на форме авторизации в приложении есть поле, в которое пользователь должен ввести свой номер телефона. Далее будем называть это поле просто phone. Частым требованием при реализации подобных виджетов является валидация входных данных при вводе. В нашем случае, нам требуется проверить, чтобы поле phone соответствовало правильному паттерну.

Как это можно реализовать средствами Duit:

  1. На стороне сервера связать с виджетом TextField, который отвечает за ввод телефона, действие, которое будет отправлять текущее значение виджета на бекенд.
  2. Валидируем полученное значение, отправляем в качестве результата валидации новое состояние виджета.
  3. Обновляем состояние.

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

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

к сведению

Пример с полноценной реализацией данного подхода можно найти в репозитории duit_hetu_extension

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

Рассмотрим подробно все необходимые для реализации шаги.

Выбор языка и среды выполнения​

Dart - компилируемый язык программирования. Для нас это означает то, что мы не сможем выполнить в нашей программе Dart-код, который был получен нами в рантайме, например, в виде строки или файла.

В связи с этим, одним из главных требований при выборе языка для реализации скриптов является возможность "на лету" интерпретировать полученный код и затем выполнять его. Для этих целей подходят виртуальные машины других языков программирования (в т.ч. и Dart).

Пример доступных сред выполнения:

  1. dart_eval - компилятор и интерпретатор байт-кода для языка Dart, написанный на Dart, обеспечивающий динамическое выполнение и отправку кода для Flutter и Dart AOT.
  2. lua_dardo - виртуальная машина Lua, написанная на Dart и реализующая версию Lua5.3.
  3. hetu_script - легковесный скриптовый язык, написанный исключительно на Dart для встраивания в приложения Flutter.

Далее все примеры будут рассматриваться в разрезе интеграции hetu_script, как наиболее производительного решения.

Реализация ScriptingCapabilityDelegate​

В новой версии Duit управление скриптами осуществляется через систему возможностей (capabilities). За обработку и выполнение динамических скриптов отвечает делегат, реализующий миксин ScriptingCapabilityDelegate.

final class HetuInitOptions {
Map<String, Function> externalFunctions;
ExternalHetuConfig config;

HetuInitOptions({
required this.config,
this.externalFunctions = const {},
});
}

В этом конкретном примере мы создали класс HetuInitOptions, который будет использоваться для конфигурации среды выполнения.


Следующим шагом будет реализация непосредственно делегата.

final class HetuScriptingManager with ScriptingCapabilityDelegate {
late final UIDriver _driver;
late final Hetu _hetuInstance;
final HetuInitOptions options;

HetuScriptingManager({
required this.options,
});


void linkDriver(UIDriver driver) {
_driver = driver;
}


Future<void> initScriptingCapability() async {
_hetuInstance = Hetu(
config: options.config.toHetuConfig(),
)..init(
externalFunctions: {
...options.externalFunctions,
"request": (
String url,
Map<String, dynamic> meta,
Map<String, dynamic> body,
) async {
// Используем транспорт драйвера для выполнения запросов
return await _driver.request(
url,
meta,
body,
);
}
},
);
}


Future<Map<String, dynamic>?> execScript(
String functionName, {
String? url,
Map<String, dynamic>? meta,
Map<String, dynamic>? body,
}) async {
return await _hetuInstance.invoke(
functionName,
positionalArgs: [
url,
meta,
body,
],
);
}


Future<void> evalScript(String source) async {
await _hetuInstance.eval(source);
}


void releaseResources() {
// Очистка ресурсов, если необходимо
}
}

Миксин ScriptingCapabilityDelegate требует переопределить ряд методов. Рассмотрим их назначение подробнее.

linkDriver​

Метод linkDriver вызывается автоматически при инициализации драйвера и передает ссылку на экземпляр UIDriver. Это позволяет делегату взаимодействовать с другими частями системы, например, использовать транспорт для сетевых запросов.

initScriptingCapability​

Метод initScriptingCapability отвечает за инициализацию среды выполнения. Здесь следует настраивать виртуальную машину, регистрировать внешние функции и т.д.

evalScript​

Метод evalScript выполняет компиляцию и интерпретацию скрипта на этапе парсинга JSON-структуры Duit представления. Если виджет имеет связанное действие и это действие является ScriptAction, то его исходный код будет обработан этим методом.

execScript​

Метод execScript вызывается во время выполнения действия. Обязательный параметр functionName - название функции для вызова. Параметры url, meta, body во время вызова действия будут переданы в качестве аргументов в скрипт автоматически. Где url - адрес запроса или название события, meta - метаданные, body - тело запроса или полезные данные для выполнения действия.


Последним шагом будет передача созданного менеджера в конструктор драйвера. Рекомендуется использовать DuitDriverCompat для работы с capability-based API.

//main.dart

late final DuitDriverCompat driver;


void initState() {
driver = DuitDriverCompat(
transportManager: HttpTransportManager(
url: "/layout",
baseUrl: "http://localhost:3000",
),
scriptingManager: HetuScriptingManager(
options: HetuInitOptions(
config: ExternalHetuConfig(),
),
),
);
super.initState();
}

Создание ScriptAction​

Рассмотрим, как создать действие-скрипт. DSL-библиотеки предоставляют специальные функции для удобного создания подобных действий.

action := NewScriptAction(
"scipt1",
[]*ActionDependency{
{
Id: "dependency1",
Target: "value",
},
},
&ScriptData{
SourceCode: `fun testFn() {...}`, //script text
FunctionName: "function name", //function name to call
},
)