天天看点

★Dart-3-构建和测试Dart app

目录

          • 1.用dart:html构建UI
          • 2.使用浏览器事件构建交互
          • 3.用类包装结构和功能
          • 4.单元测试
          • 总结

是时候弄点真正的Dart代码了。使用变量、函数和类的核心语言结构,您将构建一个名为 PackList的简单的基于浏览器的装箱单应用程序(packing-list app),使用户可以跟踪假日中要做的事情。Dart的设计目标之一是让人对它熟悉;在我们在后面的章节中介绍更令人惊讶和有趣的特性之前,本章将帮助您熟悉有关变量、函数和类的Dart功能。

您将使用内置dart:html库中的Element类来构建简单的用户界面,而不是使用原始HTML构建简单的用户界面。在撰写本文时,Dart SDK中没有GUI或小部件库,尽管各种开源第三方库正在开发中。Dart开发团队的目标是使Dart成为一个“包含电池(batteries included)”的解决方案,并且最终将在SDK中包含一个UI库。不过现在,您可以通过在Dart代码中操作HTML元素来构建用户界面;当UI库出现时,了解如何做到这一点也会对您有所帮助,因为您将对小部件背后的底层机制更有信心。

尽管PackList示例应用程序简单明了,但在现实世界中,您也可以通过这种方式创建单页web应用程序。简单的UI将包含一个输入文本框,用于从用户处获取一些输入,一个按钮供用户单击,以及一个<div>以显示用户希望在假日携带的物品列表。您的应用程序将通过添加事件侦听器函数对用户事件作出反应,该函数允许用户向列表中添加项目并将其标记为已打包。最后,您将创建一个类来保存该项(以及该项是否已打包)。该设计为代码提供了结构化和可重用性。

PackList示例与web应用程序的服务器端部分无关。该应用程序可以直接从本地文件在浏览器中运行(尽管您也可以将文件托管在web服务器上)。PackList只在客户端运行;我将在第14章中讨论在客户端和服务器之间来回发送数据。该应用程序最终应如下图所示。

★Dart-3-构建和测试Dart app

到本章结束时,您将拥有一个工作的浏览器托管的单页应用程序和一组可用于验证代码的简单单元测试。

1.用dart:html构建UI

像PackList应用程序这样的单页应用程序通过在web浏览器中执行代码来创建和操作HTML元素来构建其UI。这种方法的优点是将UI显示逻辑保留在浏览器中(例如根据用户数据的状态做出布局决策),这最终可以释放服务器资源以服务更多用户。在单页应用程序设计中,服务器将Dart应用程序代码作为静态文件发送到浏览器,然后在应用程序开始运行后将数据发送到浏览器。

您将从承载Dart脚本的入口点HTML文件和在浏览器中创建HTML元素并将其附加到HTML文档的Dart代码文件构建PackList应用程序。

创建dart:html元素

在Dart中构建UI时,请使用dart:html库,它是Dart SDK中的Dart库之一。dart:html通过抽象出浏览器DOM的一些特性(类似于JavaScript流行的jQuery库),为您提供了一种与浏览器交互的标准化方法。使用dart:html,您可以创建html元素(例如按钮、<div>等)并将其附加到浏览器。HTML元素使用父元素类(以及子类,如DivElement和ButtonElement)表示,您可以通过编程访问HTML元素上可用的所有属性和方法。元素可以包含子元素、ID和样式以及附加事件监听器。

事实上,您正在使用Element(以及DivElement和ButtonElement)接口的实现,但这对您来说是透明的,您的代码编写者有一个“看起来像”Element的实例。我将在第6章中更多地讨论接口及其与类的关系。

在Dart中创建HTML元素有两种方法:

➀创建一个空的HTML标记,需要通过编程方式填充它,例如<div></div>

➁生成一个预构建的HTML标记,如

<h2 id="title">Pack<em>List</em></h2>

。(<em> 标签告诉浏览器把其中的文本表示为强调的内容)

要创建新Element,请使用new关键字调用dart:html Element构造函数。此步骤将创建一个新元素对象,用于存储在变量中,然后附加到body中。

dart:html允许您创建元素的第一种方法是创建空HTML标记:

var myElement=new-Element.tag('div');

// 'div’是HTML的tag name

第二种方法构造一个元素,其中包含在代码段中指定的子元素和属性:

var myElement=new Element.html('<h2 id=“title”>Pack<em>List</em></h2>');

// '<h2…</h2>'是HTML tag

这两种方法都有各自的优点,在构建应用程序的UI时,我们将在本节中介绍它们。

PackList应用程序将有一个<h2>标题、一个文本框、一个按钮和一个<div>元素,以包含每个度假项目。您将在Dart代码中创建这些元素,并将它们添加到document body中;生成的HTML类似于下图。

★Dart-3-构建和测试Dart app

从HTML片段创建新元素:

对于使用构造函数

new Element.html("...some html...");

创建

Element

s来说,当您想要提供一个HTML字符串来创建一个元素,并且满足以下条件之一时,此选项非常有用:

■ 预先知道元素(及其子元素)的外观。

■ 不需要在Dart中将每个子元素作为变量单独引用(你以后仍然可以通过查询来访问它们)。

您可以将任何HTML字符串传递到Element.html构造函数中,只要是生成一个顶级HTML元素就行:

“<p>Some html</p><p>Another line</p>”

以上HTML字符串无效,因为包含两个顶级<p>元素。

“<div><p>Some html</p><p>Another line</p></div>”

有效,因为两个<p>元素都在<div>这个顶级元素里面。

在Dart中声明字符串:

在Dart中声明字符串时,有许多选择。可以使用三重引号声明多行字符串,例如:

var myString = """<div>
	<p>a multiline string</p>
</div>""";
           

这将存储一个字符串,其格式如下:

<div>
	<p>a multiline string</p>
</div>
           

如果像如下声明字符串:

var myString = """<div>
					<p>a multiline string</p>
				  </div>""";
           

输出以下字符串(可能不是预期的):

<div>
					<p>a multiline string</p>
				  </div>
           

幸运的是,对于HTML代码段,我们不关心最后的字符串是否是多行的,但我们关心可读的代码。Dart允许您自动连接相邻的字符串文字(如果两个字符串相邻,即使跨换行符,它们也会连接在一起)。因此,以下两个字符串声明存储相同(但不是多行)的值:

var myString = "<div>" "<p>a string</p>" "</div>";
var myString = "<div>"
				  "<p>a string</p>"
				"</div>";
           

第二个myString可以生成漂亮、可读的代码,也不会影响HTML输出。

Element.html构造函数的理想用法如下:

var paragraphContent = "Some about box text";
Element infoBoxDiv = new Element.html("""
<div id='infoBox'>
  <h3>About PackList</h3>
  <p>$paragraphContent</p>// 注:将paragraphContent变量嵌入到带有$前缀的HTML代码段中
</div>""");
           

这是一个理想的元素集,因为您不需要引用

aboutBox <div>

的子元素,并且文本是相对静态的。paragraphContent变量嵌入在多行字符串中。

将变量嵌入到字符串声明中:

Dart还提供了一种将变量嵌入字符串的简单方法:

$variableName

${expression}

。此功能允许您按如下方式声明字符串:

var myValue = 1234;
var myString = "<p>$myValue</p>";
var myOtherString = "<p>${myValue + 1}</p>";
           

示例将"<p>1234</p>“存储在myString,将”<p>1235</p>"存储在myOtherString中。

使用名为Element.html的构造函数创建元素时,仍然可以访问元素变量上的所有属性和方法。如下:

infoBoxDiv.children.add(new Element.html("<p>a second paragraph</p>");
var id = infoBoxDiv.id;
           

创建HTML元素的第二种方法是使用Element.tag()构造函数,它为您提供了一个空元素,您可以在Dart代码中填充和操作该元素。

通过tag name创建元素:

var itemInput = new Element.tag("input");// 创建映射到HTML的InputElement:<input></input>
itemInput.id = "txt-item";
itemInput.placeholder = "Enter an item";
           

这将创建如下所示的HTML:

无论使用Element.tag()还是Element.html(),都会返回一个看起来像dart:html元素的对象。不过,有时访问特定元素类型(如InputElement或ButtonElement)上可用的额外属性很有用。对于Dart的可选类型,运行的代码不关心您是否指定了元素的实际类型,但是声明元素的特定类型可能会很有用,因此如果您试图以对特定对象没有意义的方式使用元素,类型检查器可以提供警告,从而提供帮助。例如,您可以使用以下任一项创建新文本框:

var itemInput = new Element.tag("input");
Element itemInput = new Element.tag("input");
InputElement itemInput = new Element.tag("input");
           

第三行指定itemInput是一个InputElement(而不是ButtonElement或DivElement)让工具(和代码的其他读取器)确认您打算处理InputElement。您还可以使用Dart编辑器中InputElement的特定属性和方法方便地完成代码。

您希望在HTML中找到的所有元素都在dart:html库中定义了一个等效的类型,包括最新的HTML5元素,如CanvasElement。

Dart还提供了dart:dom库。它提供了对浏览器DOM的直接访问,相当于JavaScript DOM操作,但代价是无法使用一致的Dart元素接口。

添加元素到HTML document:

dart:html在库的顶层定义了一个文档属性,它本身包含一个body元素属性。在代码中引用此document.body属性,使用如下形式:

document.body.children.add(...some element...)

顶级文档还定义了document.head属性,该属性可用于将元关键字或文档标题元素等元素动态附加到HTML页面标题中。
★Dart-3-构建和测试Dart app
import "dart:html";
main() {
	var title = new Element.html("<h2>PackList</h2>");
	document.body.children.add(title);
	
	InputElement itemInput = new Element.tag("input");
	document.body.children.add(itemInput);
	
	ButtonElement addButton = new Element.tag("button");
	document.body.children.add(addButton);
	
	DivElement itemContainer = new Element.tag("div");
	document.body.children.add(itemContainer);
}
           

当应用程序开始运行时,您会得到包含以下代码段的HTML:

<body>
	<h2>PackList</h2>
	<input></input>
	<button></button>
	<div></div>
</body>
           

除了向body中添加元素外,还需要填充新元素的某些属性,例如:

inputButton.placeholder = "Enter an item";
addButton.text = "Add";
addButton.id = "add-btn";
           

**注:**无论在将元素添加到浏览器主体之前还是之后执行此操作,浏览器都将根据需要进行更新,以反映HTML元素的当前状态。

最后,要完成UI,需要添加一个<div>元素来包含假日项目列表。通过使用一些element.style属性(如下清单所示),您可以将样式信息直接应用到<div>(在现实世界中,使用CSS进行布局格式化)。

import "dart:html";
main() {
	var title = new Element.html("<h2>PackList</h2>");
	document.body.children.add(title);
	
	InputElement itemInput = new Element.tag("input");
	itemInput.id = "txt-item";
	itemInput.placeholder = "Enter an item";
	document.body.children.add(itemInput);
	
	ButtonElement addButton = new Element.tag("button");
	addButton.id = "btn-add";
	addButton.text = "Add";
	document.body.children.add(addButton);
	
	DivElement itemContainer = new Element.tag("div");
	itemContainer.id = "items";
	itemContainer.style.width = "300px";// set element style:width
	itemContainer.style.border = "1px solid black";// set element style:border
	itemContainer.innerHTML = "&nbsp;";
	document.body.children.add(itemContainer);
}
           

当您运行应用程序时,它会生成如下图所示的UI。

★Dart-3-构建和测试Dart app

通过使用Element.html构造函数,还可以更简洁地创建itemContainer DivElement,如下所示:

记住:

■ 您可以使用Element.html(…snippet…)或Element.tag(…tag name…)创建元素类型。

■ dart:html库定义了现代浏览器能够理解的所有元素。

■ Dart编辑器可以帮助您提供属性的自动完成信息(api.dartlang.org上的API文档可以帮助提供更多详细信息)。

■ 元素在浏览器中成为HTML标记。属性是这些标记上的属性。

■ 所有元素(包括body)都有一个包含其子元素列表的children属性。

2.使用浏览器事件构建交互

要让UI对用户事件(如按钮单击)作出反应,请使用事件监听器。dart:html事件监听器是一个接受单个事件参数的函数,该参数是实现

Event

接口的类型,如以下代码段中的类型:

myEventListenerFunction(Event event) {
	window.alert("Look - an event has been triggered");
}
           

event参数提供有关事件的额外信息。和Element及ButtonElement类型一样,Event是一种通用类型。根据创建事件的元素,可以处理特定类型的事件。例如,如果是MouseEvent,则event参数包含一个标志,指示单击了左按钮还是右按钮。PackList应用程序不需要知道这一点,尽管它只需要知道单击了“Add”按钮,所以通用Event对象就可以了。

main() {
	// ...snip ui element creation code...
	addButton.onClick.listen((event) {
		var packItem = itemInput.value;
		var listElement = new Element.html("<div class='item'>${packItem}<div>");
		itemContainer.children.add(listElement);
		itemInput.value= "";// clear the inputBox
	});
}
           
★Dart-3-构建和测试Dart app

语法糖:

// 定义一
int add(int a, int b) {
	return a + b;
}
// 升级:定义二
int add(int a, int b) => a + b;
// 再升级:定义三
add(a, b) => a + b;// 无参数类型 且 无返回类型,这都是允许的
           

在PackList应用程序中,addButton.onClick.listen()将函数作为其参数,您可以传入事件处理程序,而无需先给它命名,如下所示:

addButton.onClick.listen(
	(event) {// 匿名函数
		// function body
	}
);
// 也可以先将函数存储在一个变量中,如下:
var myEventListener = (event) => ...single statement...;
addButton.onClick.listen(myEventListener);
           

记住:

■ 函数具有多行语法和速记语法。

■ 函数返回类型信息和参数类型信息是可选的。

■ 匿名函数可以作为参数传递并存储在变量中。

响应浏览器事件:

例如,按钮可能产生如下事件:

addButton.onClick
addButton.onDrag
addButton.onMouseMove
           

事实上,这些事件中的每一个都是另一个列表(特别是

EventListenerList

),其中包含一个事件监听器列表。

添加事件监听:addButton.onClick.listen(myEventListenerFunction);

移除事件监听:addButton.onClick.remove(myEventListenerFunction);

重构事件监听器以便重用:

itemInput.onKeyPress.listen((event) {
	if (event.keyCode == 13) {// keyCode 13 is the Enter key
		var packItem = itemInput.value;
		var listElement = Element.html("<div class='item'>${packItem}</div>");
		itemContainer.children.add(listElement);
		itemInput.value = "";
	}
});
           

应将该代码块提取到main()函数之外的单独函数中,以便在应用程序中重用:

addItem() {
	var packItem = itemInput.value;
	var listElement = Element.html("<div class='item'>${packItem}</div>");
	itemContainer.children.add(listElement);
	itemInput.value = "";
}
           

这样一来,实现点击“Add”按钮或键盘按下Enter键,都完成添加一个条目的功能:

addButton.onClick.listen((event)=>addItem());
itemInput.onKeyPress.listen((event) {
	if (event.keyCode == 13)
		addItem();
});
           

很明显,由于addItem()函数是定义在main()外的,addItem()函数本身就会因元素未定义而报错。但这种封装出一个可复用的函数的策略是很好的。这就通过CSS选择器来解决这个问题。

查询HTML元素:

querySelector()函数的官方说明:

dart:html

Element? querySelector(String selectors)

Type: Element? Function(String)

查找与指定选择器组匹配的此文档的第一个子元素。除非您的网页包含多个文档,否则顶级querySelector方法的行为与此方法相同,因此您应该使用它来保存键入的几个字符。

参数selectors是一个满足CSS选择器语法的字符串。

var element1 = document.querySelector('.className');

var element2 = document.querySelector('#id');

★Dart-3-构建和测试Dart app
addItem() {
	var itemInputList = querySelectorAll("input");
	InputElement itemInput = itemInputList[0];
	DivElement itemContainer = querySelector("#items");
	// ...snip...rest the function body
}
           

到目前为止,完整的应用程序列表如下,它现在通过让用户单击鼠标或按Enter键添加条目来对事件作出反应。

import 'dart:html';
main() {
	var title = new Element.html("<h2>PackList</h2>");
	document.body.children.add(title);
	
	InputElement itemInput = new Element.tag("input");
	itemInput.id = "txt-item";
	itemInput.placeholder = "Enter an item";
	itemInput.onKeyPress.listen((event) {
		if (event.keyCode == 13)
			addItem();
	});
	document.body.children.add(itemInput);
	
	ButtonElement addButton = new Element.tag("button");
	addButton.id = "btn-add";
	addButton.text = "Add";
	addButton.onClick.listen((event) => addItem());
	document.body.children.add(addButton);
	
	DivElement itemContainer = new Element.tag("div");
	itemContainer.id = "items";
	itemContainer.style.width = "300px";
	itemContainer.style.border = "1px solid black";
	itemContainer.innerHTML = "&nbsp;";
	document.body.children.add(itemContainer);
}
addItem() {
	var itemInputList = querySelectorAll("input");
	InputElement itemInput = itemInputList[0];
	
	DivElement itemContainer = querySelector("#items");
	var itemText = itemInput.value;
	var listElement = new Element.html("<div class='item'>${itemText}<div>");
	itemContainer.children.add(listElement);
	itemInput.value = "";
}
           

此例中,还可以通过itemContainer.querySelectorAll(".item")来得到所有新添加的条目元素。

记住:

■ dart:html中的事件监听器是一个接受单个事件参数的函数。

■ 您可以添加多个事件监听器来监听正在引发的单个事件。

■ dart:html允许您使用querySelector()函数使用CSS选择器查询单个元素。

■ 使用querySelectorAll()函数查询多个子元素。

3.用类包装结构和功能

PackList应用程序的最后一步是用户能够勾选打包的项目。用户应该能够通过单击项目在正在打包和未打包的项目之间切换。

★Dart-3-构建和测试Dart app
<body>
	<style type="text/css">
		.item { cursor:pointer; }// 表明是可点击的
		.packed { text-decoration:line-through; }// 当item应用此style时是删除线效果
	</style>
	... etc ...
           

Dart除了作为一种标准的、单继承的、多接口的、基于类的语言之外,还添加了一些不错的特性。

你可能已经注意到,这些特性之一是类不是强制性的(与C#和Java不同)。函数可以不被包装在类中而存在;main()和addItem()函数存在于顶级作用域中,而不是类的一部分。与C#和Java不同的是,您可以将任意多个类放入一个Dart文件中——没有任何限制,但您可以将代码拆分为单独的文件,以保持源代码的组织,这一主题将在第5章中介绍。

除此之外,Dart中的类具有构造函数、方法和属性,这些构造函数、方法和属性可以是公共的,也可以是私有的,并且它们具有用于getter和setter的特殊语法。我们将首先处理构造函数,它将允许您创建类。

构造PackItem类:

class PackItem {
	var itemText;// 用户输入的内容
	var uiElement;// 将要添加的UI元素
	PackItem(this.itemText) {}
}
           
注:更好的设计应该有两个类:一个负责UI布局,另一个负责保存和操作数据。然后,您可以将一个应用程序松散地耦合到另一个应用程序。
PackItem(this.itemText) {}
以上this关键字不可省,因为它等效于:
PackItem(itemText) {
	this.itemText = itemText;
}
扩展:
PackItem(this.itemText, color, quantity) {
	this.color = color;
	this.quantity = quantity;
}
           

现在重构addItem()函数如下:

addItem() {
	var itemInputList = querySelectorAll("input");
	InputElement itemInput = itemInputList[0];
	DivElement itemContainer = querySelector("#items");
	var packItem = new PackItem(itemInput.value);
	itemContainer.children.add(packItem.uiElement);// packItem.uiElement为null,严重的问题!
	itemInput.value = "";
}
           

使用getter和setter:

getter(或setter)是以get(或set)关键字为前缀的方法。getter不能接受任何参数,setter必须接受单个参数。

class PackItem {
	var _uiElement;// uiElement属性重命名为_uiElement,使它为private的,对外通过getter/setter公开
	DivElement get uiElement => _uiElement;
	set uiElement(DivElement value) => _uiElement = value;
}
           
请记住,可选键入意味着您不需要指定类型信息。但是,对于类的用户来说,提供有关调用方可以使用的公共属性和方法的类型信息是很有用的(这里就是这么做的)。类型信息是用于人员和工具的文档。
var packItem = new PackItem("Sunscreen");
packItem.uiElement = new Element.tag("div");// =赋值,调用了setter
itemContainer.children.add(packItem.uiElement);// dot获取,调用了getter
           

我们是不是可以直接如下定义PackItem,还可以减轻繁琐代码呢?

class PackItem {
	var uiElement;
}
           

答:一个好的类设计器将以一种快速执行的方式设计getter和setter。允许类的用户认为他们正在读取属性是一种很好的做法,即使在getter或setter中正在进行惰性初始化或其他处理。

➀getter——只读属性

class PackItem {
	var itemText;
	var _uiElement;
	DivElement get uiElement {// 只有getter没有setter,故而“只读”
		if (_uiElement == null) {
			_uiElement = new Element.tag("div");
			_uiElement.classes.add("item");
			_uiElement.text = this.itemText;
		}
		return _uiElement;
	}
}
           

➁setter——更新属性

★Dart-3-构建和测试Dart app

首先,添加private _isPacked属性以及setter和getter,如下所示。

class PackItem {
	//...snip... other code
	var _isPacked = false;
	bool get isPacked => _isPacked;
	set isPacked(value) {
		_isPacked = value;
		if (_isPacked == true) {
			uiElement.classes.add("packed");
		} else {
			uiElement.classes.remove("packed");
		}
	}
}
           

最后一个任务是向uiElement添加一个click监听器,以执行打包和未打包之间的切换。

DivElement get uiElement {
	if (_uiElement == null) {
		_uiElement = new Element.tag("div");
		_uiElement.classes.add("item");
		_uiElement.text = this.itemText;
		_uiElement.onClick.listen((event) => isPacked = !isPacked);
	}
	return _uiElement;
}
           

随着最后的繁荣,您现在有了一个可以工作的客户端应用程序,它可以转换为JavaScript,并作为一组静态文件部署在任何web服务器上,以在所有现代浏览器中运行。它使用一个包含构造函数、getter和setter的Dart类。

记住:

■ Dart类类似于C#和Java类。

■ 构造函数参数可以自动初始化属性值。

■ getter和setter可以与属性互换。

■ 在getter、setter、properties和其他您希望人们使用的方法上使用类型信息是一种很好的做法。

完整代码及效果:

main.html

<!DOCTYPE html>
<html>
<head>
    <style type="text/css">
        .item { cursor:pointer; }
        .packed { text-decoration:line-through; }
    </style>
    <script type="application/dart" src="main.dart"></script>
    <script defer src="main.dart.js"></script>
</head>
<body>
</body>
</html>
           

main.dart

import 'dart:html';
main() {
  var title = Element.html("<h2>PackList</h2>");
  document.body!.children.add(title);
  var itemInput = Element.tag("input") as InputElement;
  itemInput.id = "txt-item";
  itemInput.placeholder = "Enter an item";
  itemInput.onKeyPress.listen((KeyboardEvent event) {
    if (event.keyCode == 13) {// Enter key
      addItem();
    }
  });
  document.body!.children.add(itemInput);

  ButtonElement addButton = Element.tag("button") as ButtonElement;
  addButton.id = "btn-add";
  addButton.text = "Add";
  addButton.onClick.listen((event) => addItem());
  document.body!.children.add(addButton);

  DivElement itemContainer = Element.tag("div") as DivElement;
  itemContainer.id = "items";
  itemContainer.style.width = "300px";
  itemContainer.style.border = "1px solid black";
  itemContainer.innerHtml = "&nbsp;";
  document.body!.children.add(itemContainer);
}

addItem() {
  var itemInputList = querySelectorAll("input");
  InputElement itemInput = itemInputList[0] as InputElement;
  DivElement itemContainer = querySelector("#items") as DivElement;
  var packItem = PackItem(itemInput.value);
  itemContainer.children.add(packItem.uiElement);
  itemInput.value = "";
}

class PackItem {
  var itemText;
  var _uiElement;
  var _isPacked = false;

  PackItem(this.itemText) {}

  bool get isPacked => _isPacked;

  set isPacked(value) {
    _isPacked = value;
    if (_isPacked == true) {
      uiElement.classes.add("packed");
    } else {
      uiElement.classes.remove("packed");
    }
  }

  DivElement get uiElement {
    if (_uiElement == null) {
      _uiElement = Element.tag("div");
      _uiElement.classes.add("item");
      _uiElement.text = itemText;
      _uiElement.onClick.listen((event) => isPacked = !isPacked);
    }
    return _uiElement;
  }
}
           
★Dart-3-构建和测试Dart app
4.单元测试

测试您编写的代码是软件开发中的标准最佳实践。Dart工具在提供的类型信息的帮助下提供代码的静态分析,但能够在浏览器和控制台的服务器端代码中运行可重复的单元测试有助于验证代码的质量。在Dart中,单元测试套件是您编写的另一个应用程序,与实际应用程序并排。然后,您可以在浏览器中运行单元测试应用程序,它在浏览器中执行每个测试并将结果输出到浏览器。

让我们看一些代码。您将向源文件夹中添加一个新的应用程序,与PackList.dart和PackList.html文件并列。单元测试PackListTest.html文件简单明了,只包含对PackListTest.dart单元测试应用程序的脚本引用,如下所示。

PackListTest.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>PackListTest</title>
	</head>
	<body>
		<script type="application/dart" src="PackListTest.dart"></script>
	</body>
</html>
           

现在您已经有了测试应用程序的HTML文件,可以开始向PackListTest.dart添加代码了。此文件导入将要测试的PackList.dart应用程序,还导入Dart的单元测试框架和单元测试框架提供的单元测试配置脚本。单元测试配置将单元测试库链接到一组输出函数,这些函数将结果输出到浏览器窗口或服务器控制台。

Client vs Server限制:

虽然Dart可以在浏览器(客户端)和服务器上运行,但许多特定库只能在客户端运行,有些库仅在服务器上有效。

dart:html库与浏览器DOM交互,仅当dart在web浏览器中运行时才可用。dart:html库不会在服务器端dart可执行文件上运行,因为没有可用的浏览器DOM。

测试任何导入dart:html的代码都不能作为服务器端测试,因为服务器端虚拟机不包含用于html交互的浏览器DOM。

下面清单显示了启动和运行客户端测试套件应用程序所需的最少代码。导入真正的PackList.dart应用程序时,需要提供导入前缀,否则两个顶级main()函数(一个在PackList应用程序中,另一个在PackListTest应用程序中)之间会发生冲突。导入前缀允许您区分两个不同的main()函数。(我们将在第5章更深入地访问此主题。)使用命令行或Dart编辑器菜单选项中的pub install命令导入单元测试框架。

import "package:unittest/unittest.dart";// 导入unit-test框架
import "package:unittest/html_config.dart";// 导入client-side HTML配置
// import "package:unittest/vm_config.dart";// 如果在server-side VM上测试,则导入VM配置
import "PackList.dart" as packListApp;// 导入你将要测试的库
main() {
	useHtmlConfiguration();// 建立client-side HTML配置
	// useVmConfiguration();// 如果在server-side VM上测试,则建立VM配置
	// todo: Add tests here
}
           

要启用包管理,您需要指定在编辑器创建的

pubspec.yaml

文件中使用单元测试依赖项。第5章更详细地讨论了pub包管理器和pubspec语法。对于此测试,您只需要向应用程序的pubspec.yaml文件中添加一个依赖项部分,如以下代码段所示:

dependencies:
	unittest: any
           

然后从编辑器菜单中运行Pub get,将单元测试依赖项拉入项目中。

但,在我本机报错:

The current Dart SDK version is 2.14.2.

Because simple_webapp depends on unittest any which requires SDK version <2.0.0, version solving failed.

改为如下的:(正常,但无法运行单元测试代码,或许应该降级版本)

dependencies:
	test:
           

创建单元测试:

现在,您已经拥有了开始编写单元测试所需的所有样板文件。测试是您创建的一个函数,它被传递到test()函数中,该函数来自unittest库,它是单元测试框架的一部分。在下一章中,我们将详细介绍函数的传递;目前,创建测试的语法如下图所示。

★Dart-3-构建和测试Dart app

在单元测试中,可以调用任何有效的Dart代码。通常,单元测试应该检查期望值,例如,值是否为null,或者对象的属性是否被正确分配。为此,可以使用

expect()

函数(也由unittest库提供),该函数接受两个参数:在测试中生成的实际值和允许提供所需值的匹配器。下面清单显示了此设置的一个简单版本,其中包含两个测试。第一种方法要求新创建的PackItem实例不为null,第二种方法验证构造函数中指定的itemText值是否由类上的属性正确返回。

// snip boilerplate imports
main() {
	useHtmlConfiguration();
	test("PackItem constructor", () {
		var item = new PackItem("Towel");
		Expect.isNotNull(item);
	});
	
	test("PackItem itemText property", () {
		var item = new PackItem("Towel");
		expect(item.itemText, equals("Towel"));
});
}
           

每个测试的输出显示在浏览器窗口中,以及失败测试的堆栈跟踪。

★Dart-3-构建和测试Dart app

自动化测试运行

基于浏览器的单元测试可以借助外部客户端测试框架实现自动化,例如Selenium(www.seleniumhq.org),您可以从连续构建服务器启动该框架。Selenium可以在应用程序中导航单元测试页面,在浏览器中运行单元测试。您可以根据呈现的浏览器内容将Selenium配置为通过或失败。Dart还提供了Selenium web驱动程序绑定,允许在Dart中直接编写Selenium脚本。有关更多详细信息,请参阅API.dartlang.org上的Dart webdriver API。

提供的单元测试常用匹配器如下表:

★Dart-3-构建和测试Dart app

创建一个自定义匹配器:

创建自定义匹配器需要扩展提供的

BaseMatcher

类。

我们将在第6章和第7章中详细介绍扩展类,但要扩展基本匹配器,可以创建一个提供两个函数的类:

matches()

函数和

descripe()

函数。函数的作用是:根据期望值是否匹配,返回布尔值。下面的清单显示了一个自定义matcher类,该类验证两个不同的packitem是否包含等效的itemText值。

// snip boilerplate
class CustomMatcher extends BaseMatcher {
	PackItem _expected;
	CustomMatcher(this._expected);
	
	bool matches(PackItem actual) {// 实现matchs()函数
		if (_expected.itemText == actual.itemText) {
			return true;
		} else {
			return false;
		}
	}
	Description describe(Description description) {// 实现describe函数
		description.add("itemText");
	}
}
main() {
	useHtmlConfiguration();
	test("PackItem custom", () {
		var packItem1 = new PackItem("Towel");
		var packItem2 = new PackItem("Towel");
		expect(packItem2, new CustomMatcher(packItem1));
	}
}
           

本节让您快速了解Dart中的单元测试。通过在代码旁边创建单元测试并在代码更改时运行它们,单元测试将成为工具箱中的另一个工具,用于创建按预期运行的正确代码。

记住

■ 您可以在浏览器或服务器上运行单元测试。

■ 基于浏览器的单元测试可以导入使用dart:html的库。

■ 服务器端单元测试无法导入使用dart:html的库。

■ 单元测试使用expect()函数和匹配器来验证测试的预期。

■ 您可以通过扩展BaseMatcher类来构建自定义匹配器。

总结

现在,您已经了解了我将在本书中使用的类和速记函数语法等结构。在本章中,我们已经讨论了很多方面,涉及了一些主题的表面,我们将更深入地讨论这些主题。让我们总结一下讨论的内容:

■ dart:html库提供了一种与浏览器交互的简洁方式,使用Dart类、属性和列表来创建和操作浏览器DOM元素。

■ dart:html定义了许多事件监听器函数可以监听的事件。事件监听器是接受单个事件参数的函数,您可以添加多个事件监听器来监听单个事件。

■ Dart函数有一个单行速记,它也会自动返回表达式的结果。

■ Dart中的类与Java和C#中的类相似,并且具有属性和方法。它们还有特殊的get和set关键字来定义getter和setter。

■ 类上的getter和setter可以通过调用代码与属性互换使用,而代码不知道是使用属性还是getter或setter。

■ 我们研究了Dart中的单元测试,它可以在浏览器和服务器上运行。您使用单元测试来围绕代码创建期望,并使用内置或自定义匹配器将实际输出与期望输出进行匹配。

在现实世界中,您需要添加跨浏览器会话(可能在浏览器本地存储中)存储数据的功能,并将数据发送回服务器。本书的第3部分和第4部分介绍了这些主题,它们更深入地讨论了建立应用程序。

现在,您已经在Dart中了解了web应用程序的基本知识,是时候稍微改变一下策略,更详细地研究Dart语言的概念了。这些概念同样适用于浏览器或服务器上的Dart。在下一章中,我们将研究Dart中的函数,它们是任何应用程序的构建块;例如,您会发现函数也可以是变量。