65.9K
CodeProject 正在变化。 阅读更多。
Home

从Silverlight到HTML5

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (58投票s)

2011 年 7 月 1 日

CPOL

31分钟阅读

viewsIcon

221387

downloadIcon

1835

本文介绍了我在将为 Windows Phone 7 编写的 Silverlight 控件迁移到使用 JavaScript 和 HTML5 重写的跨平台版本方面的经验。

Outline

概述

本文介绍了我在将为 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 通常会省略可见的滚动条,而是用滑动的手势代替,从而节省宝贵的屏幕空间。但是,如果您面临着长长的信息列表,需要多次滑动才能到达列表底部附近的位置,这将非常麻烦!

这就是快速列表控件的作用。列表中的数据按类别排列,每个类别顶部都有一个“跳转按钮”。单击跳转按钮会显示类别视图,您可以在其中单击以“跳转”到所需位置。对于按日期或字母排序的数据,这效果很好。

以下截图显示了我开发的控件的实际运行效果

JumpListBasic.png

您可以在我之前的 CodeProject 文章中阅读有关此控件开发的全部内容。

JavaScript 快速回顾

我的 JavaScript 经验并不多,所以在开始这项开发之前,我认为我应该好好学习一下。我的许多同事都有一本 Douglas Crockford 的《JavaScript: The Good Parts》,所以我不想学习坏的东西,我认为这是一个不错的起点。

TheGoodParts.jpg

《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 开发工具。

chromeDevTools.png

上面大部分 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;
}

ulli 元素也经过样式化,以删除标准情况下应用于这些元素的项目符号和内边距。

ul.itemList
{
    padding: 0;
}

ul.itemlist li
{
    list-style-type: none;
}

要实现这一点,itemList div 需要与其父级具有相同的高度。不幸的是,这仅用 CSS 并不容易实现。Silverlight 快速列表控件使用网格布局,以便项目列表和类别列表共享相同的内​​容区域。然而,HTML 中没有直接的等效项。缺乏像样的网格支持是创建经典网页布局(如三列和页脚布局)有如此多 hack 方法的原因(这就是为什么我仍然乐于使用表格!)。一些未来的 CSS 功能,如 Grid Layout ModuleTemplate Layout Module,看起来非常有前途,但它们似乎都还没有达到在浏览器中实现初步实现的阶段。

目前,解决此问题的最佳方法是使用 JavaScript。

// set the height of the itemList to that of the container
$itemList.height($jumpList.height());
$itemList.width($jumpList.width());

这可以实现所需的效果。

JumpList1.jpg

虽然缺乏像样的网格布局令人沮丧,但值得注意的是,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 函数是可见的。

Intellisense.jpg

然而,这种方法也有一些限制;例如,它无法确定您的 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}"
});

上面的模板渲染姓氏为粗体,如下所示:

JumpList2.jpg

添加跳转按钮

为了将项目列表转换为快速列表,我们需要一种方法将每个项目分配给一个类别,并在每个类别项上方渲染该类别。这就是按钮,单击时会显示类别视图,因此称为“跳转按钮”。

在微件中添加了另一个选项属性“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;
}

快速列表已开始成形。

JumpList3.jpg

请注意,样式创建了相当大的按钮,并且每个项目周围都有宽边距。这是因为它们打算在移动设备上使用,其中字体大小和点击区域需要比桌面设备更大。

可以通过 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);
    });
  }

  ...
}

然而,这并没有按照您预期的那样工作!点击任何跳转按钮都会报告:

Alert.jpg

起初令人费解,但原因其实很简单。我们之前已经看到,由于缺乏块级作用域,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 版的快速列表有一个炫酷的揭示动画,其中每个类别按钮都会旋转并缩放到视图中。

JumpListAnimation.png

让我们看看使用 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 实现非常相似。

HTML5JumpListAnimation.png

此效果的实现比我为 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 日 - 修改了快速列表以使用列表语义(从 divul/li),并修复了一些拼写错误。
  • 2011 年 7 月 1 日 - 文章首次发布。
© . All rights reserved.