从Silverlight到HTML5






4.96/5 (58投票s)
本文介绍了我在将为 Windows Phone 7 编写的 Silverlight 控件迁移到使用 JavaScript 和 HTML5 重写的跨平台版本方面的经验。
Outline
- 概述
- 引言(及 Windows 8)
- Silverlight 快速列表控件
- JavaScript 快速回顾
- 入门
- 渲染简单的列表
- 创建 jQuery 小部件
- 整理代码 - JSLint
- 编辑器和 IDE
- jQuery 模板
- 添加跳转按钮
- 为类别按钮添加动画(Hello CSS3!)
- 滚动列表
- 移动设备上的滚动
- 结论
概述
本文介绍了我在将为 Windows Phone 7 编写的 Silverlight 控件迁移到使用 JavaScript 和 HTML5 重写的跨平台版本方面的经验。我写这篇文章的目的不是要逐一比较 HTML5 / CSS3 / JavaScript 和 Silverlight 的功能;如果你仔细看,你会发现大多数功能在这两者之间是可以映射的。相反,我想捕捉这两种截然不同的技术在开发方法和总体“感觉”上的差异。
您可以在此处 iPod Touch 上查看完成的 HTML5 控件的实际运行效果
您也可以在 我的博客上的浏览器中 查看它的运行情况。
引言(及 Windows 8)
我于几个月前为 Windows Phone 7 编写了我的原始 快速列表控件。创建跨平台的 JavaScript 等效版本我早就想做了。毫无疑问,HTML5 及其作为跨平台应用程序开发平台的潜力正在 gaining 普及和势头。Silverlight、HTML5 和 Flash/Flex 这些竞争技术使得选择哪种技术变得困难,这一点我已在 一篇近期白皮书中深入讨论过。有趣的是,虽然有许多技术可以用于跨平台 Web 应用程序开发,但对于移动设备而言,只有一种:HTML5。幸运的是,移动浏览器在 HTML5 采用方面领先于桌面浏览器,这就是为什么我决定将我编写的一些 Windows Phone 7 代码移植到 HTML5,以便在 iPhone、Android 和 BlackBerry 上运行,这会是一个有趣的练习。
近期关于 Windows 8 发布的新闻报道引起了 开发社区的极大担忧和困惑。微软一直宣传 HTML / JavaScript 将是 Windows 8 及其 Metro 主题用户界面的重要组成部分。这让许多人开始思考 Silverlight 的未来,许多人认为 Silverlight 应该是 Metro 界面的首选技术。我不会深入探讨我对此事的看法;我将引导读者阅读 Mike Brown 的一篇博文,我认为这篇博文对当前情况提供了务实且周全的总结。
抛开近期事件不谈,很明显 HTML5 的发展势头日益增长。我认为任何开发者尝试以“JavaScript 的方式”进行开发都是明智之举。
Silverlight 快速列表控件
Windows Phone 7 的快速列表控件解决了移动应用程序开发者面临的一个有趣问题。在桌面端,精确的鼠标移动可用于抓取和拖动滚动条,从而导航大型信息列表。移动 UI 通常会省略可见的滚动条,而是用滑动的手势代替,从而节省宝贵的屏幕空间。但是,如果您面临着长长的信息列表,需要多次滑动才能到达列表底部附近的位置,这将非常麻烦!
这就是快速列表控件的作用。列表中的数据按类别排列,每个类别顶部都有一个“跳转按钮”。单击跳转按钮会显示类别视图,您可以在其中单击以“跳转”到所需位置。对于按日期或字母排序的数据,这效果很好。
以下截图显示了我开发的控件的实际运行效果
您可以在我之前的 CodeProject 文章中阅读有关此控件开发的全部内容。
JavaScript 快速回顾
我的 JavaScript 经验并不多,所以在开始这项开发之前,我认为我应该好好学习一下。我的许多同事都有一本 Douglas Crockford 的《JavaScript: The Good Parts》,所以我不想学习坏的东西,我认为这是一个不错的起点。
《The Good Parts》是一本有趣的书。作者对 JavaScript 语言持一种坦率诚实的看法。将这本书的大小与《Definitive Guide》放在一起比较总是很有趣的。这可能不准确地反映了 JavaScript 中有多少是“好的”,但它确实让我们思考——这到底是什么样的语言,需要一本书来小心地引导你避开“坏的部分”?你肯定找不到类似《Good Parts》的书籍来介绍 Java、C# 或任何其他主流编程语言。
缺乏熟悉的面向对象构造、缺乏块级作用域、一个含义与 C# 和 Java 中的 'this' 关键字 截然不同的“this”关键字,以及 无数其他的 JavaScript 陷阱,都给这门语言带来了坏名声,尤其是在通过 C# 或 Java 接触 JavaScript 的开发者中间。
JavaScript 语言的容错性意味着你可以用相当随意的方式写出运行的代码。然而,随着 HTML5 的流行以及我们使用 JavaScript 语言构建日益复杂的应用程序,对这门语言的深入理解变得越来越重要。为此,《Good Parts》是一本有用的指南。
开始
有了《The Good Parts》,我开始思考要使用哪种 JavaScript 继承模式来创建快速列表控件;伪经典?原型?我应该使用 模块模式吗?然后我的头开始疼,我开始三思而后行!直到我意识到,与 Silverlight 和 WPF、WinForms、Swing (Java) 以及几乎所有其他 UI 框架不同,你不需要继承现有的类来创建控件。
这让我思考 Silverlight 与 HTML / JavaScript 之间的一个基本区别。在 Silverlight 中,UI(用户界面)在 XAML 中定义,这是一个 XML 文件,然后经过解析创建一个 UI 的内存表示(可视化树)。树中的所有元素都必须继承自一个公共基类 DependencyObject
。本质上,XAML 不过是创建对象图的一种便捷语法。另一方面,HTML 不是创建 JavaScript 对象的语法,远非如此!HTML 描述了页面的结构,浏览器会解析它来创建一个文档对象模型(DOM)。浏览器提供了一个 API,可以使用 JavaScript 语言来操作这个 DOM。两者“松散耦合”且可以(并且经常)各自独立存在。而 XAML 与 Silverlight 框架“紧密耦合”,其唯一目的是创建该框架中包含的类的实例。
HTML 和 JavaScript 的独立性使我摆脱了与继承模式相关的棘手决定,你可以简单地创建你的标记并操作它。我还发现,当尝试执行更复杂的交互时,这种独立性也有优势,但稍后会详细介绍...
JavaScript 开发者很少直接操作 DOM 提供的 API,而是倾向于使用抽象 API。抽象 API 对 JavaScript 开发有几个好处,首先,不同浏览器提供的 DOM API 不同;其次,抽象 API 通常提供比它们所抽象的 DOM API 更强大的功能。
有许多抽象 API 可供选择,所以我决定不一一评述,而是选择最流行的一个:jQuery。
渲染简单的列表
是时候停止为正确的事情烦恼,开始写代码了!
我的测试页面有以下简单的标记
<html>
<head>
<script type="text/javascript" src="jquery-1.6.1.js"></script>
<script type="text/javascript" src="jumpList.js"></script>
<link rel="stylesheet" type="text/css" href="jumpList.css" />
</head>
<body>
<div class="jumpList" style="width:200px; height:300px"/>
</body>
</html>
其中带有 jumpList
类的 div
是我希望创建控件的 DOM 元素。
顺带一提,我犯了个错误,将 script 标签设为自闭合元素,即 <script />
,这 不幸不起作用。我浪费了不少时间在这个恼人的新手错误上!
我的 JumpList JavaScript 代码的第一个版本创建了一个 people
对象数组(使用我在互联网上找到的 创建随机名称 的代码),并按姓氏排序。然后代码遍历这个数组,在带有 itemList
类的父 ul
中为每个人添加一个新的 li
元素。
// create the test data
var people = [];
for (var i=0;i<20;i++) {
people.push({
surname : getName(4, 10, '', ''),
forename : getName(4, 6, '', '')
});
}
// sort the data
var sortFunc = function(a, b) {
return a.surname.localeCompare(b.surname);
}
people.sort(sortFunc);
// populate the jump list
$(document).ready(function () {
// create the category and item lists
var $jumpList = $(".jumpList");
var $itemList = $("<ul class='itemList'>");
for (var i = 0; i < people.length; i++) {
// add an item to the list
var $jumpListItem = $("<li class='jumpListItem'>").text(
people[i].surname + ", " + people[i].forename);
$itemList.append($jumpListItem);
}
// add the item list
$jumpList.append($itemList);
});
要查看此代码创建的结构的最佳方法是使用基于浏览器的开发工具,例如 Firefox 的 Firebug 或内置的 Chrome 开发工具。
上面大部分 JavaScript 代码对于 C# 开发者来说可能相对容易理解,但美元符号函数 $() 可能值得一提。jQuery 定义了一个名为 '$' 的全局函数,用于通过提供 CSS 选择器或 HTML 片段来创建 jQuery 对象。jQuery 以其流式接口而闻名,它允许您链式调用函数。
$("div.test").add("p.quote").addClass("blue").slideDown("slow");
这种流式风格对于使用(扩展方法)LINQ 语法的 C# 开发者来说很熟悉。然而,它们的工作方式却截然不同。流式 C# LINQ API 依赖于定义在 IEnumerable
接口上的扩展方法,其中每个方法都返回一个 IEnumerable
。在 jQuery 中,jQuery 对象(由美元函数返回的对象)包装了一组 DOM 节点,每个函数都会操作这些节点并返回一个 jQuery 对象。
使用全局函数 '$' 使 jQuery 非常简洁。但是,其他库可以,也确实会定义相同的全局函数。例如,Microsoft 的 ASP.NET AJAX 库提供了一个美元函数作为按 ID 获取 DOM 元素的简写。幸运的是,jQuery 有一个 noConflict 模式,可以省略全局美元函数。
在上面的代码中,我在所有 jQuery 对象变量前都加上了美元符号。这是一个流行的约定,但对执行本身没有影响。
到目前为止,上面的代码创建了一个嵌套的 ul
,其中包含我们的元素列表。Windows Phone 7 控件我正在复制的功能允许您滚动一个长数据列表,因此嵌套的 ul
需要裁剪到其父级的高度,以便您可以滚动其内容。CSS overflow
属性启用了此功能。
.itemList
{
overflow-y: auto;
}
ul
和 li
元素也经过样式化,以删除标准情况下应用于这些元素的项目符号和内边距。
ul.itemList
{
padding: 0;
}
ul.itemlist li
{
list-style-type: none;
}
要实现这一点,itemList
div 需要与其父级具有相同的高度。不幸的是,这仅用 CSS 并不容易实现。Silverlight 快速列表控件使用网格布局,以便项目列表和类别列表共享相同的内容区域。然而,HTML 中没有直接的等效项。缺乏像样的网格支持是创建经典网页布局(如三列和页脚布局)有如此多 hack 方法的原因(这就是为什么我仍然乐于使用表格!)。一些未来的 CSS 功能,如 Grid Layout Module 和 Template Layout Module,看起来非常有前途,但它们似乎都还没有达到在浏览器中实现初步实现的阶段。
目前,解决此问题的最佳方法是使用 JavaScript。
// set the height of the itemList to that of the container
$itemList.height($jumpList.height());
$itemList.width($jumpList.width());
这可以实现所需的效果。
虽然缺乏像样的网格布局令人沮丧,但值得注意的是,HTML 的主要重点是流式布局,页面的高度根据其内容而变化。内容的垂直滚动对于 HTML 来说很自然。而 Silverlight 应用程序通常受限于固定的屏幕尺寸,就像桌面应用程序通常那样。在这种情况下,开发者通常会安排 UI 控件以填充可用空间。HTML5 可能没有像样的网格布局,但反过来 Silverlight 也没有可以混合文本、图像和其他内容的像样的流式布局。
到目前为止,事情进展不太顺利!我花了太多时间纠结于继承模式,卡在我的 script 标签上,并因需要 JavaScript 代码来实现我想要的布局而感到沮丧。我有一种感觉,许多 C# 开发者会在此阶段止步并直接放弃 JavaScript。幸运的是,情况会好转!
创建 jQueryUI 小部件
到目前为止,我的代码的重用性不高,控件所在的 div 元素是硬编码的。jQuery 有自己的 UI 框架,称为 jQueryUI,它包含一组小部件(即控件),如按钮和日期选择器。我不确定 jQueryUI 有多受欢迎,当前框架只包含八个小部件。但我还是决定尝试一下。jQueryUI 的文档在描述如何定义自己的小部件方面有点单薄,但我发现一篇非常受欢迎的博文 非常出色地描述了该过程。
将上面的代码重写为 jQueryUI 小部件是一个直接的过程,遵循了所引用博文中描述的简单模式。创建一个具有 options
属性的对象,该属性允许在创建小部件时传入变量。_init
函数用于构建小部件所需的 UI,它使用与上面相同的代码。唯一的区别是,不再硬编码测试 HTML 文档中 div 的 CSS 选择器,而是小部件框架将 this.element
属性设置为要构建小部件的 DOM 元素。
var JumpList = {
// initial values are stored in the widget's prototype
options: {
items: []
},
// jQuery-UI initialization method
_init: function () {
var $jumpList = $(this.element),
$itemList = $("<ul class='itemList'>");
for (var i = 0; i < this.options.items.length; i++) {
// add an item to the list
var person = this.options.items[i];
var $jumpListItem = $("<li class='jumpListItem'>").text(
person.surname + ", " + person.forename);
$itemList.append($jumpListItem);
}
// add the item list
$jumpList.append($itemList);
// set the height of the itemList to that of the container
$itemList.height($jumpList.height());
$itemList.width($jumpList.width());
}
};
最后,在使用此小部件之前,我们必须按如下方式注册它:
$.widget("ui.jumpList", JumpList);
在测试 HTML 文档中使用它非常简单,如下所示:
$(".jumpList").jumpList({
items: people
});
最终结果是,我们现在有了一个可重用的微件,上面的语法使我们有可能通过匹配多个 DOM 元素的 CSS 选择器来创建多个微件实例。
这里还有一些相当有趣的事情正在发生,通过将上面定义的 JumpList
对象注册到 jQueryUI 微件框架,这导致在 jQuery 对象中添加了一个新的函数“jumpList
”。这究竟是如何工作的?
虽然您可以在 C# 中通过使用扩展方法来“模拟”向现有类型添加方法,但在 JavaScript 中,您可以通过向对象的原型添加函数来向现有对象添加函数。这允许您向现有对象添加新功能(即 Mixins),而不会受到传统继承层次结构的约束。同样,这也是经典面向对象继承对 JavaScript 来说不太相关的原因。
整理代码 – JSLint
到目前为止,我还没有讨论工具和 IDE。事实上,在开发的前几个小时里,我使用的是 Notepad++,一个面向开发者的记事本,具有语法高亮、强大的搜索功能、插件等等。在处理 JavaScript IDE 之前,我想先看看代码质量。
JavaScript 语言的容错性以及它众多的陷阱(或“坏的部分”)意味着很容易编写结构糟糕、难以维护的代码。Douglas Crockford,《The Good Parts》的作者,编写并积极维护 JSLint,这是一个用于提高 JavaScript 代码质量的静态分析工具。Notepad++ 有一个 JSLint 插件,安装并运行它之后,它发现了我的代码中的许多问题,例如缺少分号和缩进不一致等。您会发现 JSLint 经常能发现简单的拼写错误和语法错误以及样式问题,因此每次刷新浏览器窗口之前都运行 JSLint 可以为您节省时间。
JSLint 发现我的代码中一个特别有趣的问题与以下代码片段有关:
var $jumpList = $(this.element),
$itemList = $("<ul class='itemList'>");
for (var i = 0; i < this.options.items.length; i++) {
// add an item to the list
var person = this.options.items[i];
var $jumpListItem = $("<li class='jumpListItem'>").text(
person.surname + ", " + person.forename);
$itemList.append($jumpListItem);
}
对于上面的代码,JSLint 建议我“将 var
声明移到函数的顶部”。这突出了 JavaScript 语言中 C# 开发者难以理解的另一个特性。首先,如果您省略 var
关键字,您将不会创建局部变量,而是创建一个实际上是全局变量的东西。其次,JavaScript 没有块级作用域,因此上面代码中定义的 $jumpListItem
变量将作用于包含它的函数。JSLint 鼓励您将所有变量声明分组到每个函数的开头,以避免混淆。
var $jumpList = $(this.element),
$itemList = $("<ul class='itemList'>"),
$jumpListItem,
person,
i;
for (i = 0; i < this.options.items.length; i++) {
// add an item to the list
person = this.options.items[i];
$jumpListItem = $("<li class='jumpListItem'>").text(
person.surname + ", " + person.forename);
$itemList.append($jumpListItem);
}
编辑器和 IDE
Silverlight 开发者之间有个笑话,说切换到 HTML5 就是抛弃 Visual Studio 而改用记事本。虽然 JavaScript 的 IDE 支持不如 C# 和 Java,但肯定比使用记事本要好得多!
Eclipse IDE 具有良好的 JavaScript 支持,提供代码结构视图和重构工具。自动完成功能还支持 JSDoc,为您提供编辑器内的函数摘要。Eclipse 还内置了许多代码质量检查,这些检查可以镜像 JSLint 识别出的许多问题。
Visual Studio 也大大改进了其 JavaScript 支持;虽然它在表示 JavaScript 文件结构方面不如 Eclipse,但 IntelliSense(自动完成)支持已大大改进。IDE 会为您伪执行 JavaScript 代码,从而能够非常准确地“猜测”运行时可用的函数。例如,微件框架添加到 jQuery 原型的 jumpList
函数是可见的。
然而,这种方法也有一些限制;例如,它无法确定您的 JavaScript 文件在运行时是如何组合的,因此在编辑 jumpList.js 文件时,您无法获得 jQuery API 的 IntelliSense!有一些方法可以为 IDE 提供所需的信息,但这些信息并未得到很好的记录。有关详细信息,请参阅我朋友 Luke Page 的博客。
Visual Studio 也缺乏像样的 JavaScript 代码质量检查,不过 Luke 也开发了一个 非常棒的 JSLint 插件,它已在开发者中变得非常流行。
总之,关于工具的话题就到此为止,我们还是回到 JumpList 控件...
jQuery 模板
在当前正在进行的工作中,姓氏和名字属性被硬编码到微件的代码中,这极大地限制了其通用性。为了解决这个问题,我们需要一种方法来允许开发者指定一个模板,该模板详细说明了如何渲染每个项目,即类似 Silverlight 的 DataTemplate
的概念。JavaScript 和 HTML5 都没有类似的 C#,所以我们必须另寻他法。这是一个常见的问题,正如预期的那样,有许多框架提供了潜在的解决方案。我选择不混用不同的 JavaScript 框架,而是尝试了 jQuery 模板插件(巧合的是,它是由 Microsoft 编写的!)。
模板插件的 API 非常简单,只向 jQuery 对象添加了几个函数。要使用此功能,我在微件的选项中添加了一个 itemTemplate
属性。在初始化微件时,使用 $.template
函数编译此模板,并使用 $.tmpl
函数通过此模板渲染每个项目。
// 'compile' the template
$.template("itemTemplate", this.options.itemTemplate);
// add each item to the list
for (i = 0; i < this.options.items.length; i++) {
item = this.options.items[i];
// create the div that contains the item
$jumpListItem = $("<li class='jumpListItem'>");
// render the item using the named template
$.tmpl("itemTemplate", item).appendTo($jumpListItem);
// add the the jumplist
$itemList.append($jumpListItem);
}
现在,微件已完全解耦于它渲染的对象数组,模板在实例化时作为以下方式传递给微件:
$(".jumpList").jumpList({
items: people,
itemTemplate : "<b>${surname}</b>, ${forename}"
});
上面的模板渲染姓氏为粗体,如下所示:
添加跳转按钮
为了将项目列表转换为快速列表,我们需要一种方法将每个项目分配给一个类别,并在每个类别项上方渲染该类别。这就是按钮,单击时会显示类别视图,因此称为“跳转按钮”。
在微件中添加了另一个选项属性“categoryFunction
”,它将每个项目映射到一个类别。这里我们取姓氏的第一个字母。
$(".jumpList").jumpList({
items: people,
itemTemplate: "${surname}, ${forename}",
categoryFunction: function (person) {
return person.surname.substring(0,1).toUpperCase();
}
});
构建项目列表的循环现在会添加跳转按钮。
this._$itemList = $("<div class='itemList'/>");
// create the item list with jump buttons
for (i = 0; i < this.options.items.length; i++) {
item = this.options.items[i];
category = this.options.categoryFunction(item);
if (category !== previousCategory) {
previousCategory = category;
// create a jump button and add to the list
$jumpButton = $("<a class='jumpButton'/>").text(category);
$jumpButton.attr("id", category.toString());
$categoryItem = $("<li class='category'/>");
$categoryItem.append($jumpButton);
this._$itemList.append($categoryItem);
// store a reference to the button for this category
jumpButtons[category] = $jumpButton;
}
// create an item
$itemMarkup = $("<div class='jumpListItem'/>");
$.tmpl("itemTemplate", item).appendTo($itemMarkup);
// associate the underlying object with this node
$itemMarkup.data("dataContext", item);
// add the item to the list
$categoryItem.append($itemMarkup);
}
请注意上面使用了 jQuery 的 data()
函数,这是一个很棒的小功能,它允许您将任意数据与 DOM 元素关联。在这里,我创建了提供给快速列表的项目与其表示它们的 DOM 元素之间的关系。我将其称为 DataContext
,以致敬它模仿的 Silverlight 功能!
提供一些合适的 CSS。
a.jumpButton
{
background: #55FF55;
margin: 7px;
padding: 5px;
width: 30px;
height: 30px;
display: block;
}
.jumpListItem
{
margin: 10px;
}
快速列表已开始成形。
请注意,样式创建了相当大的按钮,并且每个项目周围都有宽边距。这是因为它们打算在移动设备上使用,其中字体大小和点击区域需要比桌面设备更大。
可以通过 jQuery 的 click()
函数添加点击事件处理程序,我处理跳转按钮点击的第一种尝试如下:
// create the item list with jump buttons
for (i = 0; i < this.options.items.length; i++) {
...
if (category !== previousCategory) {
...
// create a jump button and add to the list
$jumpButton = $("<a class='jumpButton'/>").text(category);
$jumpButton.click(function () {
alert("category clicked: " + category);
});
}
...
}
然而,这并没有按照您预期的那样工作!点击任何跳转按钮都会报告:
起初令人费解,但原因其实很简单。我们之前已经看到,由于缺乏块级作用域,JSLint 鼓励您将变量声明移到函数的开头。因此,无论按下哪个按钮,我们都会看到 category 的最终值。
这个问题可以通过一个非常有趣的语言特性来解决,这个特性在 C# / Java 中找不到。对点击处理程序的以下修改可以实现所需行为:
$jumpButton.click(function (cat) {
return function () {
alert("category clicked: " + cat);
};
} (category));
这里发生了什么?这是 JavaScript 中两个有趣概念的混合。第一个是立即执行函数,一个在创建时立即执行的函数。在这种情况下,我们正在创建一个函数并立即调用它,传入 category。第二个是闭包,其中对函数作用域内变量的引用被“捕获”。
使用闭包可以解决问题,但实际上有一种更简单的方法,它使用单个函数而不是为每个类别创建一个新函数,因此消耗的内存更少(考虑到闭包会导致对函数作用域内变量的引用,它们可能会消耗大量内存)。
鼠标事件像 Silverlight 中的事件冒泡到可视化树一样,冒泡到 DOM。因此,一种更简单的解决方案是将单个点击事件处理程序添加到列表,而不是在每个元素上都添加一个。
我想将这个事件处理程序移到初始化代码之外,以避免膨胀函数;然而,这带来了另一个挑战。函数中 this
变量的值取决于调用该函数的方式。对于从 DOM 元素引发的事件,例如点击事件,this
是对事件源元素的引用。将事件处理程序移到 JumpList 对象的属性意味着我们不再通过 this
引用 JumpList 对象。
为了解决这个问题,包装了微件组件的各种 jQuery 对象被从初始化函数中提取出来,并成为 JumpList 对象的属性。父 div(包含项目和跳转按钮)的点击事件处理程序使用 bind()
函数注册,该函数允许您添加将在事件处理函数中传递的数据。这里传递了一个指向快速列表的引用。然后,事件处理程序可以通过 event.data
属性获取对快速列表的引用,通过 event.target
属性获取源元素(即跳转按钮或项目)。
var JumpList = {
// jQuery-UI initialization method
_init: function () {
...
// create the item list with jump buttons
for (i = 0; i < this.options.items.length; i++) {
...
}
// add a click handler to the itemList
this._$itemList.bind("click", { jumpList: this },
this._itemListClickHandler);
...
},
// Handles click on the itemlist, this is either a jump list item or
// jump button click
_itemListClickHandler: function (event) {
var jumpList = event.data.jumpList,
$sourceElement = $(event.srcElement);
// handler jump list item clicks - resulting in selection changes
if ($sourceElement.hasClass("jumpListItem")) {
if (!$sourceElement.hasClass("selected")) {
// remove any previous selection
jumpList._$itemList.find(".selected").removeClass("selected");
// select the clicked element
$sourceElement.addClass("selected");
// fire the event
jumpList._trigger('selectionChanged', 0,
$sourceElement.data("dataContext"));
}
}
// handle jump button clicks
if ($sourceElement.hasClass("jumpButton") === true) {
// fade out the itemlist and show the categories
jumpList._$itemList.addClass('faded');
jumpList._$categoryList.addClass('visible');
});
}
},
_$itemList: undefined,
_$categoryList: undefined,
...
};
上面的事件处理程序会在单击跳转按钮时显示类别视图(稍后详细介绍),并实现一个非常简单的选择机制,向被点击的项目添加一个 selected
类,并触发一个 selectionChanged
事件。在这里,您可以看到使用 jQuery data()
函数提取 DOM 元素所表示的“绑定”项,模仿了 Silverlight DataContext
的概念。
我们可以在创建微件时为其添加一个事件处理程序:
$(".jumpList").jumpList({
...
selectionChanged: function (event, selectedItem) {
console.log(selectedItem);
}
});
令人印象深刻的是,选择功能仅用 5 行代码即可实现,并且事件会自动创建。
为类别按钮添加动画(Hello CSS3!)
创建类别按钮的代码与创建快速列表按钮和项目的代码非常相似,所以我在此不再赘述。如果您有兴趣,可以深入研究代码。
在快速列表初始化时创建的 DOM 元素会产生以下结构:
<div class="jumpList">
<ul class="itemList">
<li class="category">
<a class="jumpButton" id="A">A</a>
<li class="jumpListItem">Afufylug, Efotda</li>
</li>
<li class="category">
<a class="jumpButton" id="B">B</a>
<li class="jumpListItem">Bastajcyr, Laej</li>
<li class="jumpListItem">Bexgamila, Ryjl</li>
</li>
...
</ul>
<div class="categoryList" >
<a class="categoryButton">A</a>
<a class="categoryButton">B</a>
<a class="categoryButton">C</a>
<a class="categoryButton">D</a>
...
</div>
</div>
categoryList
div
在 CSS 中被样式化,使其覆盖 itemList
并最初被隐藏。
Windows Phone 7 版的快速列表有一个炫酷的揭示动画,其中每个类别按钮都会旋转并缩放到视图中。
让我们看看使用 CSS / HTML / JavaScript 重现这一点有多容易……
CSS3 规范增加了对转换的支持,允许您缩放、旋转和倾斜元素。这提供了与 Silverlight 的 RenderTransform
非常相似的功能。我们可以为 categoryButton
状态定义样式,如下所示:
a.categoryButton
{
opacity: 0;
-webkit-transform: scale(0,0) rotate(-180deg);
}
a.categoryButton.show
{
opacity: 1;
-webkit-transform: scale(1.0, 1.0) rotate(0deg);
}
类别按钮的初始状态是透明的,缩放为零,旋转 -180 度。当添加 show
类时,按钮变得可见并缩放到/旋转到其原始位置。
应用 show
类会立即更改这些属性。为了平滑地动画化旋转、缩放和透明度变化,我们可以使用新的 CSS3 过渡功能。
a.categoryButton
{
-webkit-transition-property: -webkit-transform , opacity;
-webkit-transition-duration: 0.3s;
}
最后,为了按顺序触发每个元素的动画,需要一些代码……
以下函数会遍历 jQuery 元素列表中的项,在每个元素上以短暂的延迟触发一些操作(即函数)。还有一个布尔值会传递给函数,以指示何时到达最后一个元素。
// A function that invokes the given function for each of the elements in
// the passed jQuery node-set, with a small delay between each invocation
_fireAnimations: function ($elements, func) {
var $lastElement = $elements.last();
$elements.each(function (index) {
var $element = $(this);
setTimeout(function () {
func($element, $lastElement.is($element));
}, index * 20);
});
},
我们可以使用此函数来动画化隐藏类别按钮,如下所示,将“show
”类从每个元素中移除,这将导致 CSS3 过渡回原始状态。当到达最后一个元素时,父 div 被隐藏,移除类别列表。
// hide the category buttons
jumpList._fireAnimations(jumpList._$categoryList.children(),
function ($element, isLast) {
$element.removeClass('show');
if (isLast) {
jumpList._$categoryList.removeClass('visible');
}
});
最终结果与我最初的 Windows Phone 7 实现非常相似。
此效果的实现比我为 Windows Phone 7 控件编写的等效代码要简单得多,也更简洁。请参阅早期文章中 “类别按钮图块动画”部分。
在此,值得提一下关于供应商特定 CSS 属性的内容。目前,由于过渡和转换的 CSS 规范尚未最终确定,支持它们的浏览器会在每个属性名称前使用供应商特定的前缀。不幸的是,这意味着如果您想使用这些较新的 CSS 功能,您将面临大量的重复。例如,我想禁用快速列表中的文本选择。这需要以下 CSS 才能确保跨平台支持:
.jumpList
{
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
这些前缀可能非常令人沮丧,并且会带来维护问题。然而,它们是必要的恶。过去的经验表明,允许供应商过早实现 CSS 功能可能会导致巨大的问题,正如 Eric Meyer 在他的文章 “Prefix or Posthack” 中所描述的那样。
由于我的主要目标是为移动设备创建跨平台快速列表,所以我很高兴地将自己限制在使用 -webkit
前缀,因为 iPhone、Android 和 BlackBerry 都使用 webkit 浏览器。
滚动列表
生成类别按钮的 for
循环使用 jQuery data()
函数在每个类别按钮和它对应的跳转按钮之间创建关系。为包含每个类别按钮的 categoryList div
添加了点击处理程序。同样,这遵循了本文前面介绍的模式。如果您对细节感兴趣,请查看相关的源代码。
使快速列表完全功能的最后一步是,在单击类别按钮时将列表滚动到正确的位置。这可以使用 jQuery 的 scrollTop
函数来实现。我们只需将跳转按钮的位置添加到当前滚动位置,如下所示:
jumpList._$itemListContainer.scrollTop(
$jumpButton.position().top + jumpList._$itemListContainer.scrollTop());
同样,与 Silverlight 快速列表相比,实现滚动功能已被证明非常简单。
移动设备上的滚动
在拥有了一个功能齐全的快速列表控件后,我决定是时候在移动设备上进行测试了。在我的 Windows Phone 7 上加载一个包含快速列表微件的页面,我惊讶地发现它的表现相当不错。目前使用的 WP7 版 IE 不支持 HTML5 / CSS3,但 CSS 会优雅地降级,所以控件仍然功能齐全。令人恼火的是,我的 HTML 快速列表滚动得相当流畅……咳咳(如果您不是 WP7 开发者,并且想知道为什么这令人恼火,请查看 这篇博文,或者直接搜索“WP7 ListBox 滚动性能”)。
然后我借了一部朋友的 Android 手机,它配备了 webkit 浏览器。这时,我立刻遇到了一个问题。无论我如何滑动屏幕,快速列表都拒绝滚动。经过一番苦思冥想,原来这是移动 webkit 浏览器的限制,无法滚动具有“overflow”样式的块级元素的内容。您可以想象,这是一个非常大的问题,在各种论坛上 得到了很多关注。
那么,我所有的工作都是徒劳的吗?幸运的是,由于这是 HTML5 移动应用程序开发人员面临的常见问题,互联网上有各种解决方案。我找到的第一个是 iScroll(之所以取这个名字,是因为 iPhone 也有一个 webkit 浏览器,因此也存在相同的 bug),它解决了问题。使用 iScroll 对 JavaScript 和快速列表标记进行了一些重写,但它确实解决了问题。
最终结果非常接近我用 Silverlight 编写的快速列表,但增加了在 iPhone、iTouch、Android、BlackBerry 上运行的优势,并且有点讽刺的是,随着即将发布的 Mango 版本(增加了 HTML5 支持),Windows Phone 7 也能运行。
该控件非常有 Metro 风格,因此在 Windows Phone 以外的移动设备上可能显得格格不入;然而,它一直是尝试跨平台 HTML 移动开发的一个有用工具。
结论
我很高兴地完成了 HTML5 快速列表控件的编写,但这篇文章不仅仅是为了好玩(嗯,我想,除非 CodeProject 开始付费给作者,否则它主要还是为了好玩!)。很明显,备受瞩目的 HTML5 正在崛起,随之而来的是 CSS3,当然还有 JavaScript。我想看看 HTML5 的开发体验与 Silverlight 的相比如何,老实说……我感到惊讶!
让我感到惊讶的一件事是我使用 JavaScript 的生产力水平。我对这门语言并不陌生,但我肯定不像使用 Silverlight 那样精通 JavaScript(以及相关的技术)。但我能够在大约与 Silverlight 相同的时间内创建一个大致等效的快速列表控件。
这可以归因于多种因素,首先也是最明显的一点是 JavaScript 和 CSS 的简洁性。并排比较实现“旋转图块揭示”等效果,显示 CSS3 比 Silverlight Storyboards 简洁得多。此外,CSS3 过渡不会强迫您担心类型;您可以像处理颜色和位置一样,以完全相同的方式过渡它们。
然而,我使用 JavaScript 的生产力相对较高的另一个不那么明显的原因。使用 Silverlight(或 WPF、WinForms 等)时,当您通过扩展或修改现有控件来创建新控件时,您在很大程度上要依赖现有框架控件提供的 API。如果您发现需要执行原始控件作者认为不重要而未提供 API 的操作,您将面临一项艰巨的技术挑战。例如,查找 Silverlight ListBox 控件中当前可见的项目需要对 Silverlight 框架的工作原理有相当深入的了解。JavaScript 和 HTML 之间的关系与 C# 和 XAML 之间的关系感觉相当不同。因为 JavaScript、HTML 以及 CSS 本身作为独立的技术存在,它们比 C# 和 XAML 的耦合度要松散得多。因此,没有什么东西是“隐藏”的,结果更容易扩展现有行为。
(顺便说一句,我知道 Silverlight 的可视化树提供了一个类似 DOM 的接口,您可以随意导航和修改它。我编写了 LINQ-to-VisualTree 正是为此目的;然而,它仍然不如 JavaScript / HTML 那样松散耦合。
虽然我对 JavaScript / HTML 的生产力印象深刻,但值得注意的是,这是从编写新控件的角度来看的。我认为如果我把自己放在控件消费者的位置,我的视角可能会有所不同。例如,我发现为我的 JavaScript 快速列表添加事件只需要很少的代码。这得益于 jQueryUI 微件框架自己表达事件的机制。因此,该事件在快速列表微件上并不立即可见。相比之下,为 Silverlight 快速列表添加事件需要更多的努力;然而,它使用标准的 C# 事件,该事件在公共 API 上立即可见,并且是表示选择变化的 well-understood 机制。这是两者之间更大差异的一个例子...
Silverlight(以及 WPF、WinForms 等)框架提供了一种标准的构建控件、处理数据和创建模板的方式;最终结果是所有 Silverlight 应用程序都遵循类似的模式。由于 JavaScript 不是一个 UI 框架(尽管它可以用于此目的),它缺乏这种标准化。我预计集成第三方控件和库在使用 JavaScript 时会涉及更多工作,因为每个控件都会采用不同的模式。此外,有许多 JavaScript 框架可供选择,如果需要混合搭配使用,可能会导致 API 不一致。
我个人认为,习惯了“Microsoft Stack”的开发者经常会在 JavaScript 上遇到困难。JavaScript 的工具支持,无论您选择哪个 IDE,都无法与 Silverlight 的相比。对于经验丰富的 JavaScript 开发者来说,我认为这不会构成太大障碍,就像经验丰富的 Silverlight 开发者通常比新手开发者更少依赖 IDE 一样。然而,对于刚刚开始 JavaScript 开发的人来说,这是一个更大的问题。尤其是考虑到该语言本身的各种陷阱和难点!如果我做泛化,我会预计一个平均技能水平的 Silverlight 开发团队将比一个平均技能水平的 JavaScript 开发团队生产力更高,代码质量更好。
我的建议是,任何从 C#、WPF 和 Silverlight 过渡到 JavaScript 的人,都应避免试图将熟悉的 C# 概念映射到 JavaScript。如果您试图在这两者之间进行映射,您很可能会以一种不那么有利的眼光看待 JavaScript,陷入如何处理面向对象编程和变量作用域等问题。我建议您重新审视 JavaScript,阅读 Douglas Crockford 的《The Good Parts》,并学习“坏的部分”以及如何避免它们。一定要对您的代码使用 JSLint 这样的工具;编写 sloppy JavaScript 太容易了。我还建议避免那些试图将其塑造成面向对象语言的复杂 JavaScript 框架,其中一个值得注意的例子是 Microsoft AJAX 框架;由于 JavaScript 的特性,它们是略显脆弱的包装器。
由于 JavaScript 语言缺乏结构,我可以看到使用多个独立开发团队构建大型、可扩展、模块化应用程序可能会面临挑战。如果您要进行此类开发,我敦促您查看 Google 的 Closure Compiler,它使用 JavaDoc 风格的注解,编译器可以使用这些注解来检查类型并管理依赖关系。
总而言之,我对 JavaScript、HTML 和 CSS3 的体验感到惊喜;是的,它们仍然存在跨浏览器问题和许多陷阱,而且可能永远都会存在。然而,一旦您绕过了这些问题,它们就是一个简洁而强大的技术组合,在可达性(即跨浏览器、跨平台、跨操作系统)方面,它们拥有王牌!
我将来肯定会更多地使用 JavaScript,但我也不会放弃 Silverlight;它仍然是一个强大而优雅的框架。我正在为我的未来做好准备,确保我已准备好迎接任何可能发生的事情……
历史
- 2011 年 7 月 5 日 - 修复了几个损坏的链接。
- 2011 年 7 月 4 日 - 修改了快速列表以使用列表语义(从
div
到ul
/li
),并修复了一些拼写错误。 - 2011 年 7 月 1 日 - 文章首次发布。