QJSEngine 评估结果不包含函数
Result of QJSEngine evaluation doesn't contain a function
我正在将 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 控制台并写入相同的行来看到相同的行为:
您不能调用 undefined
的函数 foo()
,因为它不存在。您可以做的是通过全局对象调用它:
这与您的 C++ 代码看到的相同。因此,要访问和调用foo()
函数,需要通过QJSEngine
的globalObject()函数访问:
#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
.
的行大致相同
这种方法的好处是您无需修改脚本即可运行。
缺点是,如果您计划重复使用相同的 QJSEngine
来调用多个脚本,那么以这种方式编写 JavaScript 代码可能会导致问题,尤其是当其中的函数具有相同的名称时。具体来说,您评估的对象将永远保留在全局命名空间中。
QScriptEngine
以 QScriptContext
的形式解决了这个问题:push()
评估代码之前的新上下文,然后 pop()
。然而,no such API exists in QJSEngine
.
解决此问题的一种方法是为每个脚本创建一个新的 QJSEngine
。没试过,不知道会贵多少。
documentation looked like it might hint at another way around it,但我不太明白每个脚本如何处理多个函数。
在与同事交谈后,我了解到一种使用 an object as an interface 解决问题的方法:
#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"
您可以在我刚刚链接到的文章中详细了解这种方法。这是它的摘要:
- 声明一个对象,只要您定义需要 "exported" 到 C++ 的内容,就可以向该对象添加属性。
- "module function" 将其接口对象作为参数 (
exports
),允许函数外部的代码创建它并将其存储在
变量 ((this.object = {})
).
但是,正如文章所述,这种方法仍然使用全局范围:
The previous pattern is commonly used by JavaScript modules intended for the browser. The module will claim a single global variable and wrap its code in a function in order to have its own private namespace. But this pattern still causes problems if multiple modules happen to claim the same name or if you want to load two versions of a module alongside each other.
如果您想更进一步,请阅读本文直至结束。不过,只要您使用唯一的对象名称,就没问题。
这是一个 "real life" 脚本如何更改以适应此解决方案的示例:
之前
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 模块:
For larger pieces of functionality, you may want to encapsulate your code and data into modules. A module is a file that contains script code, variables, etc., and uses export statements to describe its interface towards the rest of the application. With the help of import statements, a module can refer to functionality from other modules. This allows building a scripted application from smaller connected building blocks in a safe way. In contrast, the approach of using evaluate() carries the risk that internal variables or functions from one evaluate() call accidentally pollute the global object and affect subsequent evaluations.
所有需要做的就是将文件重命名为 .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);
我正在将 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 控制台并写入相同的行来看到相同的行为:
您不能调用 undefined
的函数 foo()
,因为它不存在。您可以做的是通过全局对象调用它:
这与您的 C++ 代码看到的相同。因此,要访问和调用foo()
函数,需要通过QJSEngine
的globalObject()函数访问:
#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
.
这种方法的好处是您无需修改脚本即可运行。
缺点是,如果您计划重复使用相同的 QJSEngine
来调用多个脚本,那么以这种方式编写 JavaScript 代码可能会导致问题,尤其是当其中的函数具有相同的名称时。具体来说,您评估的对象将永远保留在全局命名空间中。
QScriptEngine
以 QScriptContext
的形式解决了这个问题:push()
评估代码之前的新上下文,然后 pop()
。然而,no such API exists in QJSEngine
.
解决此问题的一种方法是为每个脚本创建一个新的 QJSEngine
。没试过,不知道会贵多少。
documentation looked like it might hint at another way around it,但我不太明白每个脚本如何处理多个函数。
在与同事交谈后,我了解到一种使用 an object as an interface 解决问题的方法:
#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"
您可以在我刚刚链接到的文章中详细了解这种方法。这是它的摘要:
- 声明一个对象,只要您定义需要 "exported" 到 C++ 的内容,就可以向该对象添加属性。
- "module function" 将其接口对象作为参数 (
exports
),允许函数外部的代码创建它并将其存储在 变量 ((this.object = {})
).
但是,正如文章所述,这种方法仍然使用全局范围:
The previous pattern is commonly used by JavaScript modules intended for the browser. The module will claim a single global variable and wrap its code in a function in order to have its own private namespace. But this pattern still causes problems if multiple modules happen to claim the same name or if you want to load two versions of a module alongside each other.
如果您想更进一步,请阅读本文直至结束。不过,只要您使用唯一的对象名称,就没问题。
这是一个 "real life" 脚本如何更改以适应此解决方案的示例:
之前
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 模块:
For larger pieces of functionality, you may want to encapsulate your code and data into modules. A module is a file that contains script code, variables, etc., and uses export statements to describe its interface towards the rest of the application. With the help of import statements, a module can refer to functionality from other modules. This allows building a scripted application from smaller connected building blocks in a safe way. In contrast, the approach of using evaluate() carries the risk that internal variables or functions from one evaluate() call accidentally pollute the global object and affect subsequent evaluations.
所有需要做的就是将文件重命名为 .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);