Я мигрирую QScriptEngine
код для QJSEngine
и столкнулся с проблемой, когда я не могу вызывать функции после оценки скриптов:
#include <QCoreApplication>
#include <QtQml>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QJSEngine engine;
QJSValue evaluationResult = engine.evaluate("function foo() { return \"foo\"; }");
if (evaluationResult.isError()) {
qWarning() << evaluationResult.toString();
return 1;
}
if (!evaluationResult.hasProperty("foo")) {
qWarning() << "Script has no \"foo\" function";
return 1;
}
if (!evaluationResult.property("foo").isCallable()) {
qWarning() << "\"foo\" property of script is not callable";
return 1;
}
QJSValue callResult = evaluationResult.property("foo").call();
if (callResult.isError()) {
qWarning() << "Error calling \"foo\" function:" << callResult.toString();
return 1;
}
qDebug() << "Result of call:" << callResult.toString();
return 0;
}
Результат этого сценария:
Script has no "activate" function
Эта же функция может быть вызвана, когда я использовал QScriptEngine
:
scriptEngine->currentContext()->activationObject().property("foo").call(scriptEngine->globalObject());
Почему функция не существует как свойство результата оценки, и как я ее называю?
Этот код приведет к foo()
оценивается как объявление функции в глобальной области видимости. Поскольку вы не называете это, в результате QJSValue
является undefined
, Вы можете увидеть то же поведение, открыв консоль JavaScript в вашем браузере и написав ту же строку:
Вы не можете вызвать функцию foo()
из undefined
потому что его не существует. Что вы можете сделать, это вызвать его через глобальный объект:
Это то же самое, что видит ваш код C ++. Поэтому для доступа и вызова foo()
функция, вам нужно получить к ней доступ через globalObject () функция QJSEngine
:
#include <QCoreApplication>
#include <QtQml>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QJSEngine engine;
QJSValue evaluationResult = engine.evaluate("function foo() { return \"foo\"; }");
if (evaluationResult.isError()) {
qWarning() << evaluationResult.toString();
return 1;
}
if (!engine.globalObject().hasProperty("foo")) {
qWarning() << "Script has no \"foo\" function";
return 1;
}
if (!engine.globalObject().property("foo").isCallable()) {
qWarning() << "\"foo\" property of script is not callable";
return 1;
}
QJSValue callResult = engine.globalObject().property("foo").call();
if (callResult.isError()) {
qWarning() << "Error calling \"foo\" function:" << callResult.toString();
return 1;
}
qDebug() << "Result of call:" << callResult.toString();
return 0;
}
Выход этого кода:
Result of call: "foo"
Это примерно так же, как строка, которую вы опубликовали, которая использует QScriptEngine
,
Преимущество этого подхода в том, что вам не нужно трогать свои скрипты, чтобы заставить его работать.
Недостатком является то, что написание кода JavaScript таким способом может вызвать проблемы, если вы планируете использовать то же самое QJSEngine
вызывать несколько сценариев, особенно если в них есть одинаковые имена. В частности, объекты, которые вы оценили, навсегда останутся в глобальном пространстве имен.
QScriptEngine
было решение этой проблемы в виде QScriptContext
: push()
свежий контекст, прежде чем оценивать свой код, и pop()
после этого. Тем не мение, такого API нет в QJSEngine
.
Одним из способов решения этой проблемы является создание нового QJSEngine
для каждого сценария. Я не пробовал, и я не уверен, насколько дорого это будет.
документация выглядела так, как будто она может намекнуть на это иначе, но я не совсем понял, как это будет работать с несколькими функциями на сценарий.
Поговорив с коллегой, я узнал о подходе, который решает проблему, используя объект как интерфейс:
#include <QCoreApplication>
#include <QtQml>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QJSEngine engine;
QString code = QLatin1String("( function(exports) {""exports.foo = function() { return \"foo\"; };""exports.bar = function() { return \"bar\"; };""})(this.object = {})");
QJSValue evaluationResult = engine.evaluate(code);
if (evaluationResult.isError()) {
qWarning() << evaluationResult.toString();
return 1;
}
QJSValue object = engine.globalObject().property("object");
if (!object.hasProperty("foo")) {
qWarning() << "Script has no \"foo\" function";
return 1;
}
if (!object.property("foo").isCallable()) {
qWarning() << "\"foo\" property of script is not callable";
return 1;
}
QJSValue callResult = object.property("foo").call();
if (callResult.isError()) {
qWarning() << "Error calling \"foo\" function:" << callResult.toString();
return 1;
}
qDebug() << "Result of call:" << callResult.toString();
return 0;
}
Выход этого кода:
Result of call: "foo"
Вы можете прочитать об этом подходе подробно в статье, на которую я только что ссылался. Вот краткое изложение этого:
exports
), позволяя коду вне функции создавать его и сохранять в(this.object = {})
).Однако, как говорится в статье, этот подход все еще использует глобальную область:
Предыдущий шаблон обычно используется модулями JavaScript, предназначенными для браузера. Модуль будет запрашивать одну глобальную переменную и упаковывать свой код в функцию, чтобы иметь собственное личное пространство имен. Но этот шаблон по-прежнему вызывает проблемы, если несколько модулей требуют одно и то же имя или если вы хотите загрузить две версии модуля рядом друг с другом.
Если вы хотите продвинуться дальше, прочитайте статью до конца. Пока вы используете уникальные имена объектов, все будет хорошо.
Вот пример того, как сценарий «реальной жизни» может измениться, чтобы приспособиться к этому решению:
function activate(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
}
function equipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
function unequipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
function destroy(thisEntity, gameController) {
}
( function(exports) {
exports.activate = function(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
}
exports.equipped = function(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
exports.unequipped = function(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
exports.destroy = function(thisEntity, gameController) {
}
})(this.Pistol = {});
Car
скрипт может иметь функции с одинаковыми именами (activate
, destroy
и т. д.), не затрагивая Pistol
,
По состоянию на Qt 5.12 QJSEngine
имеет поддержку для правильных модулей JavaScript:
Для большей функциональности вы можете инкапсулировать ваш код и данные в модули. Модуль — это файл, который содержит код сценария, переменные и т. Д. И использует операторы экспорта для описания своего интерфейса с остальной частью приложения. С помощью операторов импорта модуль может ссылаться на функциональность других модулей. Это позволяет безопасно создавать сценариев приложения из небольших связанных блоков. В противоположность этому, подход с использованием методаvaluate () несет в себе риск того, что внутренние переменные или функции из одного вызова метода define () могут случайно загрязнить глобальный объект и повлиять на последующие оценки.
Все, что нужно сделать, это переименовать файл, чтобы иметь .mjs
расширение, а затем преобразовать код следующим образом:
export function activate(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
}
export function equipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
export function unequipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
export function destroy(thisEntity, gameController) {
}
C ++ для вызова одной из этих функций выглядит примерно так:
QJSvalue module = engine.importModule("pistol.mjs");
QJSValue function = module.property("activate");
QJSValue result = function.call(args);