首页 > 技术文章 > 第十六章:JavaScript

sammy621 2022-04-10 23:47 原文

第十六章:JavaScript

JavaScript

JavaScript是web客户端开发的通用语言。通过node js的开发也开始在服务端获得关注。因此,它非常适合作为命令式语言添加到声明式QML语言中。QML本身作为声明式语言适用于描述用户界面结构,但不适于表达操作性代码。有时需要一种方式来表述互动操作,JavaScript因此派上了用场。

注意
Qt 社区中有一个关于在现代 Qt 应用程序中正确混合 QML/JS/Qt C++ 的问题。接受度最高的混合方式是,将应用中的JS部分最少化,将业务逻辑放在Qt C++ 里实现,UI逻辑放在QML/JS实现。

这本书突破了界限,对于一个产品开发来说,这并不总是正确的组合,也并不总适合所有人。跟随团队技术栈和个人喜好是很重要的。如有疑问,请遵循建议。
以下是关于在QML中如何使用JS的简短例子:

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)
  }
}

在QML中JavaScript可以以JS函数或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 数量最小化(各自限定在有限域内)并在后端使用 Qt C++ 完成繁重的工作时,他们会非常成功。然后后端应该经过严格的单元测试,以便前端开发人员可以信任代码并专注于所有这些小的用户界面需求。

注意
一般来说:后端开发人员是功能驱动的,前端开发人员是用户故事(界面流程)驱动的。

浏览器/HTML 对比 Qt Quick/QML

浏览器是渲染HTML并执行与HTML关联的Javascript的运行时。如今,Web 应用程序包含的 JavaScript 比 HTML 多得多。浏览器中的 Javascript 是一个标准的 ECMAScript 环境,带有一些额外的浏览器 API。浏览器中的典型JS环境是有一个名为window的全局对象,它用于跟浏览器窗体(title, location URL, DOM tree 等) 交互。浏览器提供通过id,class(样式名)等来访问DOM节点的方法(这被jQuery用于实现CSS选择器),近来也通过CSS选择器(querySelectorquerySelectorAll)来访问DOM节点。此外,还可以在一定时间后调用一个函数(setTimeout),以及重复调用(setInterval)。除了这些(以及浏览器其它API),其环境与QML/JS很相似。
另一处不同是JS如何出现在HTML和QML上。在HTML里,仅能够在初始化页面加载或事件处理(如页面加载、鼠标点击) 时运行JS。比如,一般在页面加载时初始化JS,这可以类比于QML中的Component.onCompleted。默认情况下,在浏览器里是不可以为属性绑定使用JS的(AngularJS通过增强DOM树来拥有此能力,但这已经不是标准的HTML了)。
JS在QML里是超一等公民,并深度集成到QML的渲染树中。这让代码更具可读性。除了这些不同,HTML/JS应用的开发者在使用QML/JS时应该有宾至如归的感觉。

JS 语言

本章不会带给你通常意义上的JavaScript介绍。另有其它书籍来做这些事情,请访问这个伟大的站点: Mozilla Developer Network
表面来看,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(如,false0""undefinednull)。比如,函数默认返回undefined。要验证false,得使用===严格相等操作符。==等值操作符会先做类型转换再来判断是否相等。如果可能的话,使用更快和更好的===严格相等操作符,它将判定一致性(详见比较操作符)。
在底层,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等面向对象语言的人来说,JS的运行机制有很大不同。JS不是纯粹的面向对象的语言,它是被称为基于原型的语言。每个对象都有原型对象。每个对象都是基于它的原型对象创建的。可以在Douglas Crockford编写的Javascript的优点一书中详细了解相关知识。
要测试JS的代码片段可以使用在线的JS 控制台 或构建一小段的QML代码:

import QtQuick 2.5

Item {
  function runJS() {
    console.log("Your JS code goes here");
  }
  Component.onCompleted: {
    runJS();
  }
}

JS 对象

在使用JS时,一些对象和方法会经常被用到。以下是对它们的一部分的集合介绍:

  • Math.floor(v), Math.ceil(v), Math.round(v) - 对浮点数向上取整、向下取整、四舍五入

  • Math.random() - 在0和1之间取随机数

  • Object.keys(o) - 从一个对象中取键(包括对象)

  • JSON.parse(s), JSON.stringify(o) - 在JS对象和JSON字符串间转换

  • Number.toFixed(p) - 对浮点数取固定精度

  • Date - 操作日期
    你也可以在这里找到它们:JavaScript 手册
    这里有一些QML与JS一起使用的简短的例子。关于在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()
    }
}

创建一个JS控制台

我们将通过一个小例子来创建一个JS控制台。我们需要一个输入框,用户可以在其中输入他的 JS 表达式,预期应该有一个输出结果列表。由于这应该更像一个桌面应用程序,我们使用 Qt Quick Controls 模块。

注意
这个项目中的 JS 控制台对测试非常有益。增强的 Quake-Terminal 效果,也能够给客户留下深刻印象。要正确地使用它,需要控制 JS 控制台计算的范围,例如当前可见的屏幕,主要数据模型,单例核心对象,或以上所有因素。



我们用Qt Creator来创建Qt Quick UI project类型的工程,以使用Qt Quick 控件。给工程起个名为JSConsole。向导完成后,我们已经拥有了一个应用的基本结构,这个应用有一个窗体、一个退出菜单。
为了能输入,我们使用TextField和一个Button,以对输入内容进行计算。表达式的计算结果使用一个有列表模型ListModel的列表视图ListView来展示,两个标签来展示表达式和对其的计算结果。
我们的应用程序会分为两个文件:

  • JSConsole.qml:应用的主要视图
  • jsconsole.js:用于计算用户表达式的脚本库

JSConsole.qml

Application window 程序窗体

// JSConsole.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import "jsconsole.js" as Util

ApplicationWindow {
    id: root
    
    title: qsTr("JSConsole")
    width: 640
    height: 480
    visible: true

    menuBar: MenuBar {
        Menu {
            title: qsTr("File")
            MenuItem {
                text: qsTr("Exit")
                onTriggered: Qt.quit()
            }
        }
    }

Form

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 {
                    id: delegate
                    required property var model
                    width: ListView.view.width
                    Label {
                        Layout.fillWidth: true
                        color: 'green'
                        text: "> " + delegate.model.expression
                    }
                    Label {
                        Layout.fillWidth: true
                        color: delegate.model.error === "" ? 'blue' : 'red'
                        text: delegate.model.error === "" ? "" + delegate.model.result : delegate.model.error
                    }
                    Rectangle {
                        height: 1
                        Layout.fillWidth: true
                        color: '#333'
                        opacity: 0.2
                    }
                }
            }
        }
    }
}

调用脚本库

计算函数jsCall并非由其本身完成表达式的计算的,为清晰易读起见,这部分逻辑被移动到了JS模块(jsconsole.js)。

import "jsconsole.js" as Util
function jsCall(exp) {
    const data = Util.call(exp)
    // insert the result at the beginning of the list
    outputModel.insert(0, data)
}

注意
为了安全起见,我们不使用 JS 中的 eval 函数,因为这将允许用户修改本地作用域。我们使用 Function 构造函数在运行时创建一个 JS 函数,并将我们的作用域作为 this 变量传入。由于每次创建函数时它不充当闭包并存储自己的范围,因此我们需要使用 this.a = 10将值存储在函数的作用域内。此作用域由脚本设置为作用域变量。

jsconsole.js

// jsconsole.js
.pragma library

const scope = {
    // our custom scope injected into our function evaluation
}

function call(msg) {
    const exp = msg.toString()
    console.log(exp)
    const data = {
        expression : msg,
        result: "",
        error: ""
    }
    try {
        const fun = new Function('return (' + exp + ')')
        data.result = JSON.stringify(fun.call(scope), null, 2)
        console.log('scope: ' + JSON.stringify(scope, null, 2), 'result: ' + data.result)
    } catch(e) {
        console.log(e.toString())
        data.error = e.toString()
    }
    return data
}

调用函数返回的数据是一个带有返回值、表达式和错误属性的 JS 对象:data: { expression: "", result: "", error: "" }。我们可以直接在 ListModel 中使用这个 JS 对象,然后从委托中访问它,例如delegate.model.expression 为我们提供了输入的表达式。

推荐阅读