Dart:一种全新的 Web 编程体验

作者:James Slocum

长期以来,JavaScript 一直垄断着客户端 Web 编程。它拥有非常庞大的用户群,并且已经用它编写了无数的库。当然,它是一门完美的语言,没有任何缺陷!不幸的是,事实并非如此。JavaScript 并非没有问题,并且存在大量的库和“转译器”,试图解决 JavaScript 更多古怪的行为。JQuery、Coffeescript、Objective-J 和 RubyJS 就是人们一直在尝试改进 JavaScript 的例子。然而,一个新的竞争者正以强大的姿态加入战局。

谷歌 Dart 横空出世,这是一种新的 JavaScript 替代语言。Dart 是对 JavaScript 应该是什么样子的彻底重新构想。它需要自己的运行时环境(谷歌根据三条款 BSD 许可证免费提供),并且有自己的语法和库。Dart 是一种面向对象的语言,具有浓厚的 Java 风格,但它保留了许多受人喜爱的 JavaScript 范式,例如一等函数。

那么,我为什么要选择使用 Dart 呢?问得好!我选择 Dart 是因为它与 JavaScript 完全决裂,并且它具有我熟悉和喜爱的面向对象编程风格。由于我有 Java 背景,Dart 的学习曲线似乎没有 JavaScript 那么陡峭。此外,由于 Dart 非常新,它让我有机会成为该语言的早期采用者并观察其发展。

安装 Dart

在您可以使用 Dart 编程之前,您需要从 http://dartlang.org 获取副本。我选择仅安装 SDK;但是,可以选择获取包含 SDK 的完整 Dart 集成开发环境。Eclipse 用户会对 IDE 感到宾至如归,因为它基于 Eclipse 组件。

要安装 SDK,我只需解压缩文件并将整个目录复制到 $HOME/bin。然后我修改了我的路径变量以查找我创建的文件夹


PATH=$PATH:$HOME/bin/dart-sdk/bin 

现在我可以从任何地方运行 dart、dart2js 和 dartdoc。

语言特性

核心 Dart 语言非常简单明了。可用的基本数据类型有 var(存储任何对象)、num(存储任何数字类型)、int、double、String、bool、List(数组)和 Map(关联数组)。所有这些数据类型都在 dart:core 库中声明。dart:core 始终可用,无需导入。函数也可以被视为一种数据类型,因为 Dart 将它们视为一等对象。您可以将函数赋值给变量,并将它们作为参数传递给其他函数,或者编写内联匿名函数。

对于流程控制,您有“常用”的 if、else if、else、for、while 和 do-while 循环、break、continue、switch 和 assert。异常通过 try-catch 块处理。

Dart 对面向对象编程有很多支持。类使用 class 关键字定义。每个对象都是某个类的实例,所有类都继承自 Object 类型。Dart 只允许单继承。extends 关键字用于从 Object 以外的父类继承。抽象类可用于定义具有某些默认实现的接口。它们不能直接实例化,但可以使用工厂构造函数来创建直接实例化的外观。抽象类使用 class 声明前面的 abstract 修饰符定义。

标准库

Dart 附带了一个令人印象深刻的标准库。我在我的示例中使用了一些库和类,因此提前了解它们的功能将很有帮助。我无法涵盖所有内容,但我将涵盖我个人认为最有用的内容。

正如我之前所说,dart:core 定义了所有可用的核心数据类型。但这还不是全部!dart:core 还包含正则表达式类,这对于任何标准库来说都是非常宝贵的补充。Dart 使用与 JavaScript 相同的正则表达式语法。

dart:io 提供了让您的程序与世界互动的类。File 和 Directory 类可用于与本地文件系统交互。File 类还允许您在特定文件上打开输入和输出流。如果您想编写跨平台代码并允许用户指定其操作系统本地文件的路径,Path 类提供了一个非常好的 Path.fromNative(String path) 构造函数,它会将 Windows 和 UNIX 路径转换为它们的 Dart 对等路径。此库中还包含 Socket 和 ServerSocket 类,可用于传统的网络通信。HttpServer 类允许您快速编程 Web 服务器。如果您想向您的应用程序添加自定义 rest API,这将非常棒。dart:io 只能在服务器端应用程序中导入和使用,所以不要尝试在您的浏览器应用程序中使用它!

dart:html 包含与客户端浏览器文档对象模型交互所需的所有类。此库是编写任何在浏览器中运行的客户端代码所必需的。该库定义了两个静态方法:Element query(String selector) 和 List<Element> queryAll(String selector)。这些方法允许您使用级联样式表选择器从浏览器的 DOM 中抓取 HTML5 元素。(稍后我会展示一个示例。)

dart:math、dart:json 和 dart:crypto 提供了难以割舍的助手。dart:math 提供了程序员期望的所有静态数学方法。dart:json 提供了 JSON 助手类。它只有三个静态方法:parse(String json),它返回一个包含解析文档的 Map;String stringify(Object object) 和 void printOn(Object object, StringBuffer output) 可用于将对象序列化为 JSON。任何对象都可以通过实现 toJson() 方法使其可序列化。dart:crypto 具有用于执行 md5、sha1 和 sha256 哈希的助手。还有一个 CryptoUtils 类,其中包含用于将字节转换为十六进制和字节转换为 base64 的方法。

服务器端编程

让我们通过查看一些服务器端编程来开始了解 Dart


import 'dart:io';

void main(){
  String fileName = './test.txt';
  File file = new File(fileName);

  var out = file.openOutputStream();
  out.writeString("Hello, Dart!\n");
  out.close();
}

看起来是不是很熟悉?在这一点上,它与 Java 程序没有太大区别。您首先导入 dart:io 库。这使您可以访问 File 和 OutputStream 类。接下来,您声明一个 main 方法。就像 Java 和 C 一样,main 充当所有程序的入口点。File 对象用于保存对文件系统上单个文件的引用。为了写入此文件,您打开文件的输出流。如果文件不存在,此方法将创建文件;如果文件存在,则清除文件内容。它返回一个 OutputStream 对象,然后可以使用该对象将数据发送到文件。您写入一个字符串并关闭 OutputStream。

要运行此程序,请将其保存到名为 first.dart 的文件中。然后使用 SDK 提供的 Dart 运行时环境


$ dart first.dart

当您的程序完成时,您应该在同一目录中看到一个名为 test.txt 的文件。打开它,您将看到您的文本。

Dart 有趣的地方在于所有 I/O 都是基于事件的。这与 Node.js 的风格非常相似。每次您调用执行 I/O 的方法时,它都会被添加到事件队列中,并且该方法立即返回。几乎每个 I/O 方法都接受一个回调函数或返回一个 Future 对象。这种设计选择的原因是为了可扩展性。由于 Dart 在单线程中运行您的代码,因此非阻塞异步 I/O 调用是允许软件扩展到数百甚至数千用户的唯一方法。

清单 1. wunder.dart

import 'dart:io';
import 'dart:uri';
import 'dart:json';

void main(){
  List jsonData = [];
  String apiKey = "";
  String zipcode = "";
  
  //Read the user supplied data form the options 
  //object
  try {
    apiKey = new Options().arguments[0];
    zipcode = new Options().arguments[1];
  } on RangeError {
    print("Please supply an API key and zipcode!");
    print("dart wunder.dart <apiKey> <zipCode>");
    exit(1);
  }

  //Build the URI we are going to request data from
  Uri uri = new Uri("http://api.wunderground.com/"
      "api/${apiKey}/conditions/q/${zipcode}.json");

  HttpClient client = new HttpClient();
  HttpClientConnection connection = 
                           client.getUrl(uri);
  connection.onResponse = 
                  (HttpClientResponse response) {
  
    //Our client has a response, open an input 
    //stream to read it
    InputStream stream = response.inputStream;
    stream.onData = () {
    
      //The input stream has data to read, 
      //read it and add it to our list
      jsonData.addAll(stream.read());
    };

    stream.onClosed = () {

      //response and print the location and temp.
      try {
        Map jsonDocument = 
         JSON.parse(new String.fromCharCodes(jsonData));
        if (jsonDocument["response"].containsKey("error")){
          throw jsonDocument["response"]["error"]["description"];
        }
        String temp = 
        ↪jsonDocument["current_observation"]["temperature_string"];
        String location = jsonDocument["current_observation"]
          ["display_location"]["full"];
        
        print('The temperature for $location is $temp');
      } catch(e) {
        print("Error: $e");
        exit(2);
      }
    };
  
    //Register the error handler for the InputStream
    stream.onError = () {
      print("Stream Failure!");
      exit(3);
    };
  };
  
  //Register the error handler for the HttpClientConnection
  connection.onError = (e){
    print("Http error (check api key)");
    print("$e");
    exit(4);
  };
}

在清单 1 中,您可以看到这种基于事件的 I/O 风格与 HttpClient.getUrl(Uri url) 方法返回的 HttpClientConnection 对象一起使用。此对象在后台工作,等待来自 HTTP 服务器的响应。为了知道何时收到响应,您必须注册一个 onResponse(HttpClientResponse response) 回调方法。我创建了一个匿名方法来处理这个问题。另请注意,在程序的底部,我也注册了一个 onError() 回调。别担心;所有回调都在 HTTP 事务开始之前注册。

一旦 onResponse() 回调执行,我就会从响应中提取 InputStream 对象以开始读取数据。我注册了 onData()onClosed()onError() 回调来处理 InputStream 可能处于的不同状态。onData() 只是从流中读取字节并将它们附加到 jsonData 列表对象。只要有数据要读取,就保证会调用 onData()。一旦流到达“文件末尾”,就会执行 onClosed()。此时,我知道来自 HttpRequest 的所有数据都已传输和读取,因此我可以使用 JSON 助手类将响应解析为 Map 对象并将最终结果打印给用户。如果一切顺利,程序实际上是从这里退出的。如果 InputStream 中出现错误,则会调用 onError() 回调,程序将从那里退出。

要运行此程序,请使用 Dart 运行时环境调用它。您需要从 Weather Underground (http://www.wunderground.com/weather/api) 注册一个 API 密钥。别担心;它是完全免费的。获得 API 密钥后,您可以查看任何美国邮政编码的当前温度


$ dart wunder.dart ec7....93b 10001
The temperature for New York, NY is 57.2 F (14.0 C)
客户端 Dart

既然您已经了解了 Dart 在服务器端的用途,那么让我们来看看它真正为之设计的用途:客户端。Dart 擅长编程大型浏览器应用程序。不幸的是,空间限制阻止我展示一个真正的大型应用程序。相反,我将介绍一个不太大但非常酷的应用程序,它使用了 HTML5 Canvas 对象。让我们使用 Dart 进行手指绘画。

清单 2. fingerpaint.dart

library fingerpaint;

import 'dart:html';

class DrawSurface {
  String _color = "black";
  int _lineThickness = 1;
  CanvasElement _canvas;
  bool _drawing = false;
  var _context;
  
  DrawSurface(CanvasElement canvas) {
    _canvas = canvas;
    _context = _canvas.context2d;
    _canvas.on.mouseDown.add((Event e) => _onMouseDown(e));
    _canvas.on.mouseUp.add((Event e) => _onMouseUp(e));
    _canvas.on.mouseMove.add((Event e) => _onMouseMove(e));
    _canvas.on.mouseOut.add((Event e) => _onMouseUp(e));
  }

  set lineThickness(int lineThickness) {
    _lineThickness = lineThickness;
    _context.lineWidth = _lineThickness;
  }

  set color(String color) {
    _color = color;
    _context.fillStyle = _color;
    _context.strokeStyle = _color;
  }

  int get lineThickness => _lineThickness;

  int get color => _color;
  
  void incrementLineThickness(int amount){
    _lineThickness += amount;
    _context.lineWidth = _lineThickness;
  }

  String getPNGImageUrl() {
    return _canvas.toDataUrl('image/png');
  }

  _onMouseDown(Event e){
    _context.beginPath();
    _context.moveTo(e.offsetX, e.offsetY);
    _drawing = true;
  }

  _onMouseUp(Event e){
    _context.closePath();
    _drawing = false;
  }

  _onMouseMove(Event e){
    if (_drawing == true){
      _drawOnCanvas(e.offsetX, e.offsetY);
    }
  }

  _drawOnCanvas(int x, int y){
    _context.lineTo(x, y);
    _context.stroke();
  }

}

void main() {
  CanvasElement canvas = query("#draw-surface");
  DrawSurface ds = new DrawSurface(canvas);
  
  List<Element> buttons = queryAll("#colors input");
  for (Element e in buttons){
    e.on.click.add((Event eve) {
      ds.color = e.id;
    });
  }

  var sizeDisplay = query("#currentsize");
  sizeDisplay.text = ds.lineThickness.toString();

  query("#sizeup").on.click.add((Event e) {
    ds.incrementLineThickness(1);
    sizeDisplay.text = ds.lineThickness.toString();
  });

  query("#sizedown").on.click.add((Event e) {
    ds.incrementLineThickness(-1);
    sizeDisplay.text = ds.lineThickness.toString();
  });

  query("#save").on.click.add((Event e) {
    String url = ds.getPNGImageUrl();
    window.open(url, "save");
  });
}
清单 3. fingerpaint.html

<!DOCTYPE html>
<html>
   <head>
      <h3>Finger Paint</h3>
      <link rel="stylesheet" href="fingerpaint.css" />
   </head>
   <body>
      <h1>Finger Paint</h1>
      <div>
         <canvas id="draw-surface" width="800px" height="600px">
         </canvas>
      </div>
      <div id="colors">
         <input id="white" type="button"></input>
         <input id="red" type="button"></input>
         <input id="black" type="button"></input>
         <input id="blue" type="button"></input>
         <input id="green" type="button"></input>
         <input id="purple" type="button"></input>
         <input id="yellow" type="button"></input>
         <input id="orange" type="button"></input>
         <input id="brown" type="button"></input>
      </div>
      <div>
         <input id="sizeup" type="button" value="Increase width">
         </input>
         <input id="sizedown" type="button" value="Decrease width">
         </input>
         <span id="currentsize"></span>
      </div>
      <div>
         <input id="save" type="button" value="Save"></input>
      </div>
      <script type="application/dart" src="fingerpaint.dart">
      </script> 
      <script type="application/javascript"
         src="http://dart.googlecode.com/svn/trunk/dart/client/dart.js">
      </script>
   </body>
</html>
清单 4. fingerpaint.css

#draw-surface {
   border-style: solid;
   border-width: 2px;
}
#white {
   background-color: white;
   width: 30px;
}
#red {
   background-color: red;
   width: 30px;
}
#black {
   background-color: black;
   width: 30px;
}
#blue {
   background-color: blue;
   width: 30px;
}
#green {
   background-color: green;
   width: 30px;
}
#purple {
   background-color: purple;
   width: 30px;
}
#yellow {
   background-color: yellow;
   width: 30px;
}
#orange {
   background-color: orange;
   width: 30px;
}
#brown {
   background-color: brown;
   width: 30px;
}

在我们的简单手指绘画应用程序中,将为用户提供的每种颜色设置按钮,以及用于增加和减少笔画粗细的按钮。如果您无法保存杰作并与世界分享,那么绘画有什么用呢?因此,让我们制作一个保存按钮,将画布转换为 PNG 图像。

首先,让我们看一下这个项目的标记。在清单 3 中,您可以看到有一个 HTML5 网页,其中包含一个名为 draw-surface 的画布元素。这就是艺术作品的创作地。画布下方是控制按钮,允许用户选择颜色和笔画宽度,以及保存按钮。文档的最后一部分是最有趣的部分。有两个 script 元素。第一个是 script 标签,其 type 属性设置为“application/dart”。此脚本类型目前仅被 Chromium 的一个分支 Dartium (http://www.dartlang.org/dartium) 识别。第二个是启动 Dartium 中的 Dart VM 所需的 JavaScript 引导文件。它还有一个特殊的第二个函数,我稍后会谈到。

现在让我们看看应用程序本身。在清单 2 的顶部,我通过导入 dart:html 来启动程序。所有客户端应用程序都必须导入此库才能访问 DOM。接下来,我创建一个名为 DrawSurface 的类,它将充当画布对象的容器类。构造函数接受一个 CanvasElement 并获取其 2D 渲染上下文。它还注册所有回调以处理绘图表面上的鼠标移动。当用户在绘图表面画布上的某个位置按下鼠标时,我开始绘制路径。

当用户在按下按钮的情况下移动鼠标时,我将线段添加到绘图中。当用户释放鼠标或移出画布元素时,我关闭绘图路径。

我为 color 和 lineThickness 属性实现了 getter 和 setter。在 setter 方法中,我确保在任何更改时更新渲染上下文。我还添加了两个方法 incrementLineThickness(int amount),它允许用户按一定量调整 lineThickness,而不是仅仅设置它,以及 getPNGImageUrl() 以公开画布元素的 toDataUrl() 方法。此方法将允许保存按钮起作用。

在 main 中,我使用静态 query(String selector) 函数通过其 ID 获取画布元素。query 函数接受任何 CSS 选择器并返回一个与其匹配的 Element 对象。如果页面上有多个您想要访问的元素,您可以使用 queryAll(String selector) 函数,它将返回一个 List<Element> 对象。我使用此函数一次性收集所有颜色按钮,并注册 onClick() 事件以将其当前颜色设置为其各自的 ID 值。

最后,我注册了用于向上和向下调整大小按钮的回调,这些按钮将线条粗细更改 1。我还注册了保存按钮的回调,以从画布中抓取 PNG 数据 URL 并在新窗口中打开它。然后,用户可以通过右键单击图像并选择“图片另存为”来保存图像。

运行应用程序

如果您选择下载并安装完整的 Dart 编辑器包,您已经安装了 Dartium。如果像我一样,您选择仅安装 SDK,则需要从 http://www.dartlang.org/dartium 获取 Dartium 的副本。要安装它,我只需解压缩文件并在我的 $HOME/bin 目录中创建到我提取的 chrome 程序的符号链接


$ ln -s /path/to/unzipped/folder/chrome dartium

安装完成后,您可以使用以下命令从命令行运行此应用程序


$ dartium fingerpaint.html
注意

Dartium 是一个实验性浏览器,因此它应该仅用于在本地开发 Dart 应用程序。不要将其用作您的普通浏览器!可能存在尚未发现的安全漏洞或稳定性问题。

图 1. 手指绘画应用程序在 Dartium 上运行,Dartium 是 Chromium 的一个特殊分支。

在其他浏览器上运行

如果您没有 Dartium 也没关系。还记得 fingerpain.html 中的引导脚本行吗?除了启动 Dartium 中的 Dart VM 之外,如果不支持 Dart,它还会回退到 JavaScript 应用程序。JavaScript 应用程序必须与 Dart 应用程序同名,扩展名为 .dart.js。Dart SDK 附带了一个名为 dart2js 的实用程序,它会将 Dart 浏览器应用程序转换为 JavaScript 应用程序,以便在任何浏览器中使用。要转换此应用程序,您可以在 fingerpaint.dart 上运行 dart2js


$ dart2js -ofingerpaint.dart.js fingerpaint.dart

完成后,您将看到几个新文件,包括 fingerpaint.dart.js。

现在,该应用程序可以在任何可以处理 JavaScript 的浏览器中运行。我个人建议使用 Dartium 进行应用程序开发,然后将应用程序转换为 JavaScript 以供发布。

图 2. 转换为 JavaScript 后,手指绘画应用程序现在可以在任何浏览器上运行。这里它在 Firefox 上运行。

Dart 的现状

我很想告诉您,社区已经张开双臂欢迎 Dart,但这根本不是事实。负责人担心 Dart 会成为下一个 VBScript 并损害开放 Web。到目前为止,Microsoft、Mozilla 和 Apple 都拒绝了将 Dart 运行时嵌入到其浏览器中的想法。随着 Dart 的成熟和普及,我希望看到这种立场逆转,但目前,dart2js 是让 Dart 项目在线供所有人使用的唯一方法。

结论

Dart 是一种出色的语言,它为编写大型、客户端、面向对象的应用程序提供了一种全新的方法。我很高兴使用它,我希望您也会喜欢。这种语言的潜力是无限的,我希望看到它在未来得到广泛采用。

资源

Dart 主页:http://www.dartlang.org

Dart API 参考:http://api.dartlang.org/docs/bleeding_edge/index.html

Dartium:http://www.dartlang.org/dartium

Dart 源代码和错误跟踪:http://code.google.com/p/dart

HTML5 for Publishers:http://shop.oreilly.com/product/0636920022473.do

加载 Disqus 评论