C++是编译性语言,修改了代码之后必须进行编译才能生效。当项目比较大的时候,项目的编译时间可能很长,如果仅仅因为一个小修改就需要将整个项目编译一遍,开发效率会很低。而JavaScript是脚本语言,修改之后不需要编译,能立即生效,这就能弥补C++在大型项目中的不足,从而提升开发效率。
在大型项目中我们一般将项目拆分成对效率要求比较高且比较稳定的引擎模块,以及业务变动比较大需要经常修改的业务模块。对于引擎模块由于比较稳定我们通过C++实现,而对于一些独立的业务模块由于经常需要变动和修改我们通过JavaScript实现,这样一来哪怕是业务出现变动,我们也不需要将整个引擎重新编译一遍了。同时,引擎模块的开发者和业务逻辑模块的开发者可以并行开发,提升开发效率。
同时,由于业务模块是通过脚本实现的,我们可以在主模块保持不变的情况下,通过修改业务脚本对已经存在的业务模块进行扩展和升级。这样就极大的提升了应用的灵活性。
之所以可以在QT中使用JavaScript是因为QML模块是基于JavaScript的,它提供了JavaScript 解析和执行的引擎。为了在QT中使用JavaScript引擎,我们现在pro文件中引入qml模块,配置如下:
QT += qml
C++编译器默认是识别不了JavaScript代码的,我们必须通过QT提供的QJSEngine类来执行JavaScript代码。对应的调用流程如下所示:
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//QJSEngine提供了ECMA-262标准内嵌的对象
//包括:Math、Date、String
//这些模块不需要扩展可以直接使用
QJSEngine engine;
//添加console模块
engine.installExtensions(QJSEngine::ConsoleExtension);
//通过QJSValue实现JS和C++之间的数据交换,类似于QVariant
//不仅可以包含基本类型(bool、int、long、string)
//还可以包含JS中Object和function的引用
QJSValue ret = engine.evaluate("console.log('hello js in qt');");
if(ret.isError())
{
qDebug() << "JavaSript Error:" <<ret.toString();
}
else
{
qDebug() << "JavaScript Succeed";
}
//获取JS执行的结果
QJSValue result = engine.evaluate("(6 * 8) / 2");
if(result.isError())
{
qDebug() << "JavaSript Error:" <<result.toString();
}
else
{
qDebug() << "Result is:" << result.toInt();
}
return a.exec();
}
QJSEngine通过evaluate()执行JavaScript代码,代码可以是代码文件里面写死的字符串,也可以是通过外部文件读取的内容。执行之后返回的结果通过QJSValue传递回来。QJSValue类似于QVariant可以实现各种类型数据之间的相互转换。通过QJSValue我们就可以实现JavaScript和C++之间的数据交换了。和QVariant不同QJSValue不仅可以包含基本数据,还可以包含JavaScript Object,我们可以通过QJSValue的setProperty()和property()来操作对象的属性,通过call()方法执行对象的函数方法。这样通过JavaScript返回的对象,就可以在C++中进行使用了。
在JavaScript中调用C++类的最简单的方法就是利用QT的元对象系统将C++对象封装成JavaScript能识别的QJSValue。JavaScript和C++之间的数据交换都是通过QJSValue实现的。这里我们将一个QPushButton封装成QJSValue然后设置成QJSEngine的全局对象,从而实现JavaScript对QPushButton的操作。对应的调用流程如下所示:
#pragma execution_character_set("utf-8")
#include
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QJSEngine engine;
//添加console模块
engine.installExtensions(QJSEngine::ConsoleExtension);
//将QPushButton添加成JS中全局对象
QPushButton *button = new QPushButton();
//newObject()方法用来封装已经存在的QObject实例
QJSValue scriptButton = engine.newQObject(button);
//将QPushButton设置成名为QtButton的全局对象
engine.globalObject().setProperty("QtButton", scriptButton);
//在JS脚本中设置按钮的属性并关联对应的信号
//QPushButton中通过Q_PROPERTY宏声明的属性可以在JavaScript中使用
//QPushButton的槽函数也可以在JavaScript中调用
QJSValue result = engine.evaluate("QtButton.text= '测试按钮';\
QtButton.checkable=true;\
QtButton.setChecked(true);\
QtButton.toggled.connect(function(){console.log('QtButton toggled!');});\
QtButton.show();");
//获取JS执行的结果
if(result.isError())
{
qDebug() << "JavaSript Error:" <<result.toString();
}
else
{
qDebug() << "JavaScript Succeed";
}
return a.exec();
}
QT的槽函数一般都是没有返回值的,QT也不会对槽函数数的返回值进行处理。但如果我们在JavaScript中想调用包含返回值的C++方法怎么办,我们可以通过Q_INVOKABLE宏声明带有返回值的方法,这样的方法在JavaScript中是能感知到的。对应的方法定义如下:
public:
Q_INVOKABLE int DoSomething();
有时候,为了调用方便开发者会给C++对象定义很丰富的接口,但是我们并不想把所有的接口都暴露给JavaScript,这时候就需要对JavaScript的访问进行限制。
对JavaScript访问权限限制的最安全的方法是对原始C++对象进行封装,只将必要的接口和属性暴露给JavaScript。此外我们还可以在封装类中对某些自定义数据结构进行转换,方便JavaScript中的调用。为了将C++业务逻辑和JavaScript的业务逻辑拆分开来,建议在封装类中将JavaScript调用的信号和原始的信号区分开来,防止两边的业务逻相互干扰。
JavaScript只能访问C++对象中作用域为public和protected的槽函数和通过Q_INVOKABLE声明的函数,无法访问private作用域下的函数,我们可以通过修改函数的作用域来限制JavaScript的访问。
对于Q_PROPERTY声明的属性字段,我们可以通过SCRIPTABLE声明来限制JavaScript的访问,对应的声明流程如下:
Q_PROPERTY(QString myString READ myString SCRIPTABLE false)
在JavaScript中创建C++对象的途径主要有两种,一种是直接创建,另一种是通过已经存在的对象间接创建。下面介绍一下这两种创建方式的实现方法:
1.通过注册新类型创建
首先将自定义类型的构造函数声明成Q_INVOKABLE
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include
#include
class MyObject : public QObject
{
Q_OBJECT
public:
//将构造函数声明成Q_INVOKABLE
Q_INVOKABLE explicit MyObject(QObject *parent = nullptr):QObject(parent)
{
}
public slots:
//public 槽函数在js中调用
void slot_test_myobject()
{
qDebug() << "call slot test";
}
};
#endif // MYOBJECT_H
然后通过newQMetaObject方法将自定义的类型注册到JavaScript引擎中并进行调用。实现如下:
#pragma execution_character_set("utf-8")
#include
#include
#include
#include "myobject.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QJSEngine engine;
//添加console模块
engine.installExtensions(QJSEngine::ConsoleExtension);
//通过newQMetaObject函数将自定义的类型引入JS引擎
//将自定义的类型绑定到某个全局对象属性
engine.globalObject().setProperty("MyObject",engine.newQMetaObject(&MyObject::staticMetaObject));
//调用自定义类型
QJSValue result = engine.evaluate("var newObject = new MyObject(); newObject.slot_test_myobject();");
//获取JS执行的结果
if(result.isError())
{
qDebug() << "JavaSript Error:" <<result.toString();
}
else
{
qDebug() << "JavaScript Succeed";
}
return a.exec();
}
2.通过已经存在的对象创建
在对象中添加一个Q_INVOKABLE方法,然后在对应的方法中创建新对象。
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include
#include
class MyObject : public QObject
{
Q_OBJECT
···
···
public:
//创建新对象
Q_INVOKABLE QObject* createNewObject()
{
return new MyObject();
}
Q_INVOKABLE void OutputInfo(QString inputContent)
{
qDebug() << inputContent;
}
};
#endif // MYOBJECT_H
通过引擎中已经存在的对象创建新对象,实现如下:
#pragma execution_character_set("utf-8")
#include "widget.h"
#include
#include
#include
#include "myobject.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QJSEngine engine;
//添加console模块
engine.installExtensions(QJSEngine::ConsoleExtension);
//通过newQMetaObject函数将自定义的类型引入JS引擎
//将自定义的类型绑定到某个全局对象属性
engine.globalObject().setProperty("MyObject",engine.newQMetaObject(&MyObject::staticMetaObject));
//通过已经存在的对象创建新对象
QJSValue result = engine.evaluate("var newObject = new MyObject(); \
newObject.slot_test_myobject();\
var createdObject = newObject.createNewObject();\
createdObject.OutputInfo('create a new Object');\
");
//获取JS执行的结果
if(result.isError())
{
qDebug() << "JavaSript Error:" <<result.toString();
}
else
{
qDebug() << "JavaScript Succeed";
}
return a.exec();
}
有时候我们需要将某个函数暴露给JavaScript引擎来实现某个特定逻辑。但是QT只支持将基于QObject的类函数暴露给QJSEngine。我们可以对原始函数进行类封装,将函数封装成类的成员函数之后进行引擎注册,对应的实现如下:
将函数封装到某个基于QObject的类中
class MyObject : public QObject
{
Q_OBJECT
...
public:
Q_INVOKABLE void OutputInfo(QString inputContent)
{
qDebug() << inputContent;
}
};
将函数定义成JavaScript的一个属性然后进行调用
#pragma execution_character_set("utf-8")
#include "widget.h"
#include
#include
#include
#include "myobject.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QJSEngine engine;
//添加console模块
engine.installExtensions(QJSEngine::ConsoleExtension);
//类的JS封装
QJSValue myObjectJS = engine.newQObject(new MyObject());
//将类的成员函数注册成JS的全局属性
engine.globalObject().setProperty("OutputInfo",myObjectJS.property("OutputInfo"));
//执行对应的函数
QJSValue result = engine.evaluate("OutputInfo('defined function from C++');");
//获取JS执行的结果
if(result.isError())
{
qDebug() << "JavaSript Error:" <<result.toString();
}
else
{
qDebug() << "JavaScript Succeed";
}
return a.exec();
}
QT可以自动的将对应的数据类型转换成JavaScript能识别的数据类型,对应的转换规则如下:
基本类型(int、short、bool、double、char)不需要转换在JavaScript中可以直接访问。
QT框架的基本类型(QString、QUrl、QColor、QFont、QDate、QPoint、QSize、QRect等)会被转换成包含属性值列表的对象(Object)。
QDateTime和QTime会被转成成JavaScript的Date对象
QT中通过Q_ENUM宏定义的枚举变量,在JavaScript中可以直接使用
QT中通过Q_FLAG宏定义的标志位,也可以在JavaScript中直接使用
QObject*会自动转换成JavaScript的封装对象
QVariant中包含的能转换的数据类型会被识别到
QVariantList等价于JavaScript中的对象列表
QVariantMap等价于JavaScript中包含多种属性的对象
数据容器对象(QList, QList, QList,QList, QStringList, QList, QVector, QVector,QVector)不需要数据类型转换,在JavaScript中可以直接访问。
如果上面的数据类型转换无法满足业务需求,我们还可以将QJSValue作为中介,实现更加复杂的自定义数据类型的转换。
QT默认是无法实现用户自定义的数据类型在JavaScript和C++之间自动转换的。我们可以通过Q_GADGET宏声明自定义C++数据类型,通过Q_PROPERTY宏来声明属性字段,从而实现自定义类型在JavaScript和C++之间的自动转换。