QJSEngine评估的结果不包含函数

Mit*_*tch 1 javascript c++ qt qtscript qjsengine

我正在将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;
}
Run Code Online (Sandbox Code Playgroud)

该脚本的输出是:

 Script has no "activate" function
Run Code Online (Sandbox Code Playgroud)

我使用时可以调用相同的函数QScriptEngine:

 scriptEngine->currentContext()->activationObject().property("foo").call(scriptEngine->globalObject());
Run Code Online (Sandbox Code Playgroud)

为什么函数不作为评估结果的属性存在,我该如何调用它?

Mit*_*tch 5

该代码将导致在foo()全局范围内被评估为函数声明.既然你没有打电话,结果QJSValueundefined.您可以通过在浏览器中打开JavaScript控制台并编写相同的行来查看相同的行为:

JavaScript的评价,结果

你不能调用函数foo()undefined,因为它不存在.你能做什么,是通过全局对象调用它:

JavaScript的通话

这与您的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;
}
Run Code Online (Sandbox Code Playgroud)

此代码的输出是:

Result of call: "foo"
Run Code Online (Sandbox Code Playgroud)

这与您发布的使用的行大致相同QScriptEngine.

这种方法的好处是您无需触摸脚本即可使其工作.

缺点是,如果您计划重复使用相同的QJSEngine方法调用多个脚本,以这种方式编写JavaScript代码会导致问题,尤其是当其中的函数具有相同的名称时.具体而言,您评估的对象将永远保留在全局命名空间中.

QScriptEngine在评估代码之前,以及之后的形式提供了解决此问题的方法QScriptContext:push()新的上下文pop().但是,不存在此类APIQJSEngine.

解决此问题的一种方法是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;
}
Run Code Online (Sandbox Code Playgroud)

此代码的输出是:

Result of call: "foo"
Run Code Online (Sandbox Code Playgroud)

您可以在我刚刚链接到的文章中详细了解此方法.以下是它的摘要:

  • 声明一个对象,只要定义需要"导出"到C++的内容,就可以添加属性.
  • "模块函数"将其接口对象作为参数(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) {
}
Run Code Online (Sandbox Code Playgroud)

( 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 = {});
Run Code Online (Sandbox Code Playgroud)

一个Car脚本可以有相同的名称(功能activate,destroy等等),而不影响那些Pistol.


从Qt 5.12开始,QJSEngine支持适当的JavaScript模块:

对于更大的功能,您可能希望将代码和数据封装到模块中.模块是包含脚本代码,变量等的文件,并使用导出语句来描述其与应用程序其余部分的接口.在import语句的帮助下,模块可以引用其他模块的功能.这允许以安全的方式从较小的连接构建块构建脚本化应用程序.相反,使用evaluate()的方法带来的风险是,一个evaluate()调用的内部变量或函数会意外地污染全局对象并影响后续评估.

所有需要做的是重命名文件以具有.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) {
}
Run Code Online (Sandbox Code Playgroud)

调用这些函数之一的C++看起来像这样:

QJSvalue module = engine.importModule("pistol.mjs");
QJSValue function = module.property("activate");
QJSValue result = function.call(args);
Run Code Online (Sandbox Code Playgroud)