14.JavaScript(JavaScript)
本章的作者:jryannel
** 注意: **
最新的构建时间:2016/03/21
这章的源代码能够在assetts folder找到。
JavaScript 是 Web 客户端开发的通用语言。它也开始主要由节点 js 在 Web 服务器开发上牵引。因此,它是作为声明式命令式 QML 语言的一种很好的补充。QML 本身作为声明式语言用于表达用户界面层次结构,但限于表达操作代码。有时你需要一种表达操作的方式,这里有 JavaScript 发挥作用。
** 注意: **
Qt 社区中有一个关于在现代 Qt 应用程序中有关 QML/JS/QtC ++ 的正确混合的问题。普遍同意的推荐方法是将应用程序的JS部分限制在最小限度,并在 QtC++ 内部实现业务逻辑,并在 QML/JS 内部实现 UI 逻辑。这本书趋向这种边界的划分,通常对于一个产品的开发这不一定是正确的混合方式,不是对于所有人都适用。最重要的是根据你的团队技能和个人品味而定。在接受推荐的时候保持你的怀疑。
这里有一个简短的例子,如 JS 看起来像,混合在 QML 中:
Button {
width: 200
height: 300
property bool checked: false
text: "Click to toggle"
// JS function
function doToggle() {
checked = !checked
}
onTriggered: {
// this is also JavaScript
doToggle();
console.log('checked: ' + checked)
}
}
所以 JavaScript 可以作为一个独立的 JS 函数在 QML 中的许多地方,作为一个 JS 模块,它可以在每一个属性绑定的右侧。
import "util.js" as Util // import a pure JS module
Button {
width: 200
height: width*2 // JS on the right side of property binding
// standalone function (not really useful)
function log(msg) {
console.log("Button> " + msg);
}
onTriggered: {
// this is JavaScript
log();
Qt.quit();
}
}
在使用 QML 定义用户界面时,使用 JavaScript 完成功能。那么我们需要写多少的 JavaScript 才合适呢? 这取决于我们的风格和你对 JS 开发的熟悉程度。JS 是一种松散型语言,这使得你很难发现类型缺陷。函数参数接受不同类型的变量值,会导致非常难发现严重的 Bug。发现缺陷的方法是严格的单元测试或者验收测试。因此如果你在 JS 中开发真正的逻辑(不是粘贴代码)你应该使用测试优先的方法。通常使用这种混合开发非常成功的团队(Qt/C++ 与 QML/JS),他们都会最小化前段逻辑中使用的 JS,在后端 QtC++ 中完成更加复杂的工作。后端遵循严格的单元测试,这样前段的开发者可以信任这些代码并且专注于用户界面的需求。
** 注意: **
一般来说:后端开发人员是功能驱动的,前端开发人员是用户需求驱动的。
14.1 Browser/HTML 与 QtQuick/QML
浏览器用于呈现 HTML 并在运行时执行与 HTML 相关联的 Javascript。现在的现代 Web 应用程序包含更多的 JavaScript,然后是 HTML。浏览器中的 Javascript 是一个标准的 ECMAScript 环境,具有一些浏览器添加。浏览器中的典型 JS 环境知道访问浏览器窗口的 window 对象。还有 jQuery 用来提供 CSS 选择器的基本 DOM 选择器。另外还有一个 setTimeout 函数在一段时间后调用函数。除此之外,环境是类似于 QML/JS 的标准 JavaScript 环境。
不同的是 JS 出现在 HTML 和 QML 中的形式。在HTML中,我们只能在事件处理程序中添加 JS(例如,页面加载,鼠标按下)。例如,我们的 JS 在页面加载时正常初始化,这与 QML 中的 Component.onCompleted 相当。例如,我们不能使用 JS 进行属性绑定(至少不直接,AngularJS 会增强 DOM 树以允许这些,但是这与标准的 HTML 很不同)。
所以在 QML 中的 JS 更加优秀,并且与 QML 的渲染树高度集成。使得语言更具有可读性。除了这些,开发过 HTML/JS 应用程序的人会觉得在 QML/JS 中开发起来非常容易上手。
14.2 语言
本章不介绍 JavaScript 的一般介绍。还有其他书籍,JavaScript 的一般介绍,请访问 Mozilla 开发者网站。
在表面上 JavaScript 是一种非常常见的语言,与其他语言没有什么不同:
function countDown() {
for(var i=0; i<10; i++) {
console.log('index: ' + i)
}
}
function countDown2() {
var i=10;
while( i>0 ) {
i--;
}
}
但是要注意,JS 具有函数范围而不是像 C++ 中的范围(见函数和函数范围)。
声明 if ... else, break, continue 也按预期工作。switch case 也可以比较其他类型,而不仅仅是整数值:
function getAge(name) {
// switch over a string
switch(name) {
case "father":
return 58;
case "mother":
return 56;
}
return unknown;
}
JS 知道可能是假的几个值,例如 (false、0、""、undefined、null)。 例如,函数默认返回 undefined。要测试假使用 === 身份运算符。 == 等于运算符将执行类型转换以测试相等性。如果可能,使用更快更好的 === 严格的等价运算符来测试身份(请参阅比较运算符)。
在引擎罩下,javascript 有自己的做事方式。例如数组:
function doIt() {
var a = [] // empty arrays
a.push(10) // addend number on arrays
a.push("Monkey") // append string on arrays
console.log(a.length) // prints 2
a[0] // returns 10
a[1] // returns Monkey
a[2] // returns undefined
a[99] = "String" // a valid assignment
console.log(a.length) // prints 100
a[98] // contains the value undefined
}
对于来自 C++ 或 Java 的用于 OO 语言的人来说,JS 也是如此。JS 不是纯粹的 OO 语言,它是所谓的基于原型的语言。每个对象都有一个原型对象。基于他的原型对象创建一个对象。请阅读 Javascript the Good Parts by Douglas Crockford 一书中的更多信息。
要测试一些小的 JS 代码片段,我们可以使用在线 JS 控制台或仅构建一小部分 QML 代码:
import QtQuick 2.5
Item {
function runJS() {
console.log("Your JS code goes here");
}
Component.onCompleted: {
runJS();
}
}
14.3 JS 对象
在使用 JS 时,有一些更常用的对象和方法。这是他们的小集合。
- Math.floor(v), Math.ceil(v), Math.round(v) —— 对浮点数向下取整、向上取整、四舍五入取整
- Math.random() —— 创建 0 到 1 之间的随机数
- Object.keys(o) —— 从对象获取键值(包括QObject)
- JSON.parse(s), JSON.stringify(o) —— JS 对象和 JSON 字符串之间的转换
- Number.toFixed(p) —— 调整精度
- Date —— 日期操作
我们也可以在以下位置找到它们:JavaScript 参考
这里有一些小而有限的例子,如何使用 JS 与 QML。它们告诉我们如何在 QML 中使用 JS 的方法。
** 打印 QML 元素中的所有键 **
Item {
id: root
Component.onCompleted: {
var keys = Object.keys(root);
for(var i=0; i<keys.length; i++) {
var key = keys[i];
// prints all properties, signals, functions from object
console.log(key + ' : ' + root[key]);
}
}
}
** 将对象解析为 JSON 字符串并返回 **
Item {
property var obj: {
key: 'value'
}
Component.onCompleted: {
var data = JSON.stringify(obj);
console.log(data);
var obj = JSON.parse(data);
console.log(obj.key); // > 'value'
}
}
** 当前日期 **
Item {
Timer {
id: timeUpdater
interval: 100
running: true
repeat: true
onTriggered: {
var d = new Date();
console.log(d.getSeconds());
}
}
}
** 按名称调用函数 **
Item {
id: root
function doIt() {
console.log("doIt()")
}
Component.onCompleted: {
// Call using function execution
root["doIt"]();
var fn = root["doIt"];
// Call using JS call method (could pass in a custom this object and arguments)
fn.call()
}
}
14.4 创建一个 JS 控制台
作为一个例子,我们将创建一个 JS 控制台。我们需要一个输入字段,用户可以输入他的 JS 表达式,理想情况下应该有输出结果列表。因为这应该更像是桌面应用程序,我们使用 QtQuick Controls 模块。
** 注意: **
我们下一个项目中的 JS 控制台可以真正有益于测试。增强了 Quake-Terminal 效果,也是打动客户的好主意。要明智地使用它,我们需要控制 JS 控制台评估的范围,例如。当前可见屏幕,主要数据模型,单例对象或其它的东西。
我们用 Qt Creator 使用 QtQuick 控件创建一个 Qt Quick UI 项目。我们称之为 JSConsole 项目。 向导完成后,我们已经有一个应用程序的基本结构,并显示一个应用程序窗口和一个菜单来退出应用程序。
对于输入,我们使用 TextField 和 Button 发送用于测试的输入。表达式评估的结果使用 ListView 以 ListModel 为模型显示,两个标签显示表达式和评估结果。
// part of JSConsole.qml
ApplicationWindow {
id: root
...
ColumnLayout {
anchors.fill: parent
anchors.margins: 9
RowLayout {
Layout.fillWidth: true
TextField {
id: input
Layout.fillWidth: true
focus: true
onAccepted: {
// call our evaluation function on root
root.jsCall(input.text)
}
}
Button {
text: qsTr("Send")
onClicked: {
// call our evaluation function on root
root.jsCall(input.text)
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Rectangle {
anchors.fill: parent
color: '#333'
border.color: Qt.darker(color)
opacity: 0.2
radius: 2
}
ScrollView {
id: scrollView
anchors.fill: parent
anchors.margins: 9
ListView {
id: resultView
model: ListModel {
id: outputModel
}
delegate: ColumnLayout {
width: ListView.view.width
Label {
Layout.fillWidth: true
color: 'green'
text: "> " + model.expression
}
Label {
Layout.fillWidth: true
color: 'blue'
text: "" + model.result
}
Rectangle {
height: 1
Layout.fillWidth: true
color: '#333'
opacity: 0.2
}
}
}
}
}
}
}
评估函数 jsCall 本身不进行评估,这已经被移动到 JS 模块(jsconsole.js)以更清晰的分离。
// part of JSConsole.qml
import "jsconsole.js" as Util
...
ApplicationWindow {
id: root
...
function jsCall(exp) {
var data = Util.call(exp);
// insert the result at the beginning of the list
outputModel.insert(0, data)
}
}
为了安全起见,我们不使用 JS 的 eval 函数,因为这将允许用户修改本地范围。我们使用 Function 构造函数在运行时创建一个 JS 函数,并将其作为这个变量传递给我们。由于函数在每次不作为闭包并存储自己的范围时创建,因此我们需要使用 this.a = 10 将该值存储在该函数范围内。该范围由脚本设置到作用域变量。
// jsconsole.js
.pragma library
var scope = {
// our custom scope injected into our function evaluation
}
function call(msg) {
var exp = msg.toString();
console.log(exp)
var data = {
expression : msg
}
try {
var fun = new Function('return (' + exp + ');');
data.result = JSON.stringify(fun.call(scope), null, 2)
console.log('scope: ' + JSON.stringify(scope, null, 2) + 'result: ' + result)
} catch(e) {
console.log(e.toString())
data.error = e.toString();
}
return data;
}
来自调用函数的数据返回是一个具有结果,表达式和错误属性的 JS 对象 data: { expression: {}, result: {}, error: {} } 我们可以直接在 ListModel 中使用这个 JS 对象,然后从代理访问它。model.expression 给我们输入表达式。为了简化示例,我们忽略错误结果。
本章完。