mobl:针对移动Web开发的DSL
简介
现在,针对移动设备像智能手机和平板电脑的应用开发很流行。Apple公司的AppStore(针对iPhone、iPod和iPad)拥有超过350,000种应用,而Android的marketplace也快速追赶上来,现在已经拥有超过200,000种应用。然而,Android和iOS并非是仅有的两种移动平台。BlackBerry也是有力的竞争者,此外还有Nokia。最近Microsoft发布了Windows Phone 7,HP也发布了新的WebOS设备。这样,对于用户来说有了多种选择,但对我们这些开发者来说却是个噩梦。我们应该针对哪种平台来开发应用程序呢?
在移动平台之间共享代码极度困难。每种平台都选择了自己的开发框架,还有自己的语言和API。对于iOS开发,你需要使用Objective-C和CocoaTouch API;对于Android开发,你要使用Java和Android API;对于Windows Phone 7,你需要使用.NET和Silverlight API。
然而,我们还是拥有一种解决方案:Web开发,特别是:Webkit。我们会看到,所有主要的移动平台供应商(除了Microsoft之外)都在Webkit之上构建了他们的移动浏览器,而Webkit是当前最新的、速度最快的开源浏览器引擎。Webkit支持多种移动应用所需要的HTML5 特性,包括侦测触摸手势(轻击、强击和缩放)、定位API(确定用户的位置),并且支持本地数据库(浏览器中的SQLite数据库,用于在本地缓存数据)。
当前,在Android、iOS、WebOS以及BlackBerry OS的六款浏览器中,都对这些特性提供了本地支持。对于不包含基于Webkit的浏览器的设备,我们还可以使用PhoneGap。PhoneGap让我们可以使用web技术(包括HTML5)开发本地应用程序,并把应用程序包装成为本地应用程序,那样就可以分发给用户了(例如,通过平台的应用程序市场)。如果平台还没有内建的WebKit浏览器,那么PhoneGap就会为其提供。PhoneGap应用程序可以在六种不同的移动平台上运行。
JavaScript框架厂商注意到了这是个机会,于是就构建了多种能够在移动Web上运行的框架。jQuery Mobile和Sencha Touch都是比较典型的例子。这些框架很容易给人留下深刻的印象,因为对于当前的开发者来说,使用它们来为移动网络开发应用程序是一种不错的方式。然而,它们还都是基于JavaScript、HTML和CSS的,它们的目的都不是要开发应用程序,而是要开发包含超链接文档的网络应用。各种框架试图对这些语言进行调整,从而适合他们的新角色,但是这会引起你的思考,专门为开发移动应用程序 而设计的语言应该是什么样子的呢?
如果我们想要设计这样的一种语言,需要解决什么样的问题呢?
- 首先要解决的就是工具的支持。从事企业级开发的开发者(比方说Java和.NET的开发者)习惯使用像Eclipse之类的IDE特性,像在键入的时候就能够突出显示错误、代码自动完成、引用解析、代码大纲以及重构等等。JavaScript和HTML在本质上就是动态的,这让它们很强大,但是也让工具厂商很难为其创建出Eclipse和InteliJ那种级别的IDE。对于当前所有语言来说,良好的IDE支持都是前提条件。
- 第二个要解决的问题是简洁。例如,用户界面框架经常会包含大量类似的代码,它们的作用就是把数据从数据库中复制到用户界面,或者把界面上的数据复制回数据库。我们的新语言应该减少开发者所需要编写的样板化代码。
- 第三个问题是JavaScript的异步编程模型。在浏览器中,JavaScript是单线程的,开发者需要使用回调机制来执行数据库查询之类耗费资源的操作,比方说,我们不会编写像下面这样的同步代码:
var results = tx.executeQuery("SELECT * FROM User");
for(var i = 0; i < results.length; i++) {
...
}tx.executeQuery("SELECT * FROM User", function(results) {
for(var i = 0; i < results.length; i++) {
...
});
我们的研究小组(软件工程研究小组,位于荷兰代尔夫特理工大学)专注于编程的转换与实现。当前,我们主要专注于领域特定语言。对于当前移动领域的开发,我们觉得有一个很好的机会,可以为移动Web开发出一种领域特定语言。我们自问:如果从头开始的话,一门专注于开发移动Web应用程序的语言,应该是什么样子的呢? 结论就是mobl。
mobl:从10,000英尺高处俯瞰
mobl是一种文本式的、静态类型、编译的语言,主要是通过它的Eclipse插件应用。这个插件提供了语法高亮显示、内嵌的错误突出显示、引用解析以及代码自动完成。mobl编译器(集成在IDE中)会在每次保存的时候把mobl模块编译成HTML、JavaScript和CSS的组合。mobl应用程序不依赖于任何特定的服务端技术,而只会处理应用程序的客户端部分。我们可以使用AJAX的方式调用(JSON)Web服务。
mobl语言有大量特性,目的都是为了提高移动开发者的生产率:
- 声明式的用户界面:使用mobl定义的用户界面是以一种声明式的方式指定的,并且会对应用程序状态的改变做出响应,这与其它方法不同,在那些方法中状态和视图是严格分离的。
- 透明的数据持久性:mobl拥有使用实体定义来定义数据模型的语言结构。对实体对象做出的变更会自动实体化到数据库中。对数据的查询也是在实体级别完成的,而并不需要字符串嵌入式的SQL查询,这和很多其它框架都是类似的。
- 原则上是静态的,在需要的时候可以是动态的:mobl语言是静态类型的语言,支持像错误突出显示、引用解析和代码自动完成等IDE特性。尽管如此,正如类型推论所说,在很多情况下我们不需要显式指定类型。有些情况下,动态类型更方便,我们可以使用
Dynamic
类型,从而可以对Dynamic
变量的属性和方法进行任意地访问。 - 同步编写脚本:我们可以在脚本语言中编写应用程序的逻辑,那看起来和带有类型的JavaScript非常相似。代码是以同步的风格编写的,并且会由编译器使用持续传递的样式转换(continuation-passing style transform)将其自动转换为异步的JavaScript代码。
简单的To-do列表
为了真正了解mobl是什么样子的,我会在这个部分演示如何实现一个简单的to-do列表管理器。
首先我们要在Eclipse中创建一个新的mobl项目,这样会得到mobl项目的基本框架,其中有唯一的应用程序文件,我们把它命名为todo.mobl
:
application todo
import mobl::ui::generic
screen root() {
header("todo")
}
这个mobl模块的第一行定义这是一个application模块。mobl有三种不同类型的模块:
- application模块:通常每个项目有一个,这是应用程序的主要入口点。
- regular模块:这是一个定义的库,通常会由一个或者多个其他(应用程序)模块导入。
- configuration模块(config.mobl):定义应用程序的配置选项。
application和regular模块包含任意数量的定义:用户界面、数据模型、样式、web服务接口和函数等等。
首先,让我们来使用entity
定义来定义一个数据模型。实体的实例会持久化到移动设备本身的本地数据库中。我们的to-do应用程序只需要唯一一个实体,名称为Task
。对于每个task对象,我们都希望能够记录它的名称,以及任务是否已经完成。
entity Task {
name : String (searchable)
done : Bool
}
对于每个属性我们都会指定名称和类型,并有选择地加上一个或者多个注解。mobl支持两种类型的注解:inverse
注解(定义反向的关系)和searchable
(在全文搜索中包含字段)。尽管这个应用程序不需要,但我们还是可以指定多个实体以及它们之间的关系,包括一对一、一对多和多对多的关系。
接下来,我们对root
屏幕进行改写,从而显示我们能够勾选和取消勾选的任务列表。在mobl中,用户界面是用screen
和control
定义的。通常screen
会通过组合大量control
来定义实体屏幕布局。这样,control
会定义更小的用户界面元素,像按钮、标签和表格等等。此外,屏幕或者控件也能够定义本地状态(使用变量),并使用控件结构来对集合进行迭代,从而根据条件显示用户界面的各个部分。
下面是我们的root
屏幕的最初定义(当程序运行时所显示的第一个屏幕)。它使用了大量mobl::ui::generic
库中的控件,包括header
(渲染出屏幕的标题)、group
(对一个或者多个item
进行分组)和checkBox
。
screen root() {
header("Tasks")
group {
list(t in Task.all()) {
item { checkBox(t.done, label=t.name) }
}
}
}
list
控件的结构与for-each循环类似:它会遍历一个集合,对于集合中的每个项目,它都会进行渲染。checkBox
与两个Task
对象属性绑定:done
和name
。数据绑定会在应用程序状态(例如本地变量或者实体属性)和用户界面之间创造出同步的关系。例如,当用户选择复选框或者取消对它的选择时,t.done
的值就会据此更新。类似地,当应用程序的某些其他部分更新任务t
的name
属性时,复选框也会自动更新它的标签。这被叫做反应性编程(reactive programming),这也是根本的mobl特性之一:用户界面会对应用程序状态的改变做出反应。
最初时,数据库是空的,从而在我们的列表中不会显示任何任务。我们怎样才能添加任务呢? 为了这个目的,我们定义了addTask
屏幕:
screen addTask() {
var newTask = Task()
header("Add") {
button("Done", onclick={
add(newTask);
screen return;
})
}
group {
item { textField(newTask.name, placeholder="Task name") }
}
}
使用var
结构我们可以创建嵌入在特定屏幕中的状态。在这种情况下,我们定义了变量newTask
,并使用新建的Task
实体实例对其进行初始化。我们把textField
控件与newTasks
的name
属性绑定。当用户输入完任务的名称时,他就会点击显示在应用程序标题处的Done按钮。按钮拥有名为onclick
的参数,它的值是Callback
,当事件发生的时候,就会执行一段命令式的应用程序逻辑。在这个特定的情况下,发生了两件事情:
- 向数据库添加了
newTask
对象。这会在后台对Task.all()
集合做出改变,使得新建的任务被自动添加到root
屏幕的list
中。 - 然后用户会返回当初的屏幕(
screen return
和函数中的return
类似)。
然而,尽管我们定义了root
和addTask
屏幕,但没有办法从一个屏幕跳转到另一个。我们需要做的就是向root
屏幕添加一个Add按钮,它会带我们跳转到addTask
屏幕。因此,我们需要在root
中把对header
控件的调用调整为下面这样:
header("Tasks") {
button("Add", onclick={ addTask(); })
}
正如你所看到的,对屏幕的调用和对一般函数的调用类似,事实上,和函数一样,屏幕也能够返回值。
现在我们的应用程序的功能已经基本完备。我们还要添加最后一个特性:搜索。在我们的数据模型中,我们对Task
的name
属性使用了(searchable)
注解。利用,我们就可以使用搜索来过滤任务列表(从而更快地找到我们想要查找的任务)。
我们需要把root
屏幕调整为下面这样:
screen root(){
var phrase = ""
header("Tasks") {
button("Add", onclick={ addTask(); })
}
searchBox(phrase)
group {
list(t in Task.searchPrefix(phrase)) {
item { checkBox(t.done, label=t.name) }
}
}
}
我们向屏幕中添加了新的本地变量phrase
,可以在其中存放查询的短语。我们使用searchBox
,并将其与phrase
绑定。然后,我们并没有遍历list
中的Task.all()
,而是遍历了搜索集合Task.searchPrefix(phrase)
。在运行的时候,当我们在搜索查询中输入内容时,搜索结果的列表就会更新。此时用户界面会再一次根据应用程序的状态(这种情况下是phrase
变量)自动调整。
现在我们已经完成了包含搜索功能的基本to-do列表应用程序的构建工作,接下来可以部署了。当我们保存mobl模块的时候,同时也会把它们编译成JavaScript、HTML和CSS,这些文件位于Eclipse项目的www/
目录下。我们可以把这些生成的文件部署到任意一个能够为静态文件提供服务的web服务器上(例如:Apache、IIS或者Tomcat),mobl完全不需要后端程序。
To-do列表之外
当然,to-do列表只是个玩具一样的例子,它只使用了一些简单的控件,像group
、item
、button
和textField
。mobl的标准库还提供了一些高级的控件,像标签组、主从视图、上下文菜单以及可扩展的列表等等。
除了定义用户界面、数据模型以及应用程序逻辑的语言结构之外,mobl还拥有以下结构:
- 定义样式,在此它使用一种与CSS非常类似的语言。
- 对Web服务的访问。从服务器拉入数据并缓存在本地。在将来,mobl还会支持透明的数据同步。
想要了解这些特性,你可以查看mobl站点上的教程。
使用DSL还是不使用DSL
我们可以把mobl描述为一种领域特定语言(DSL),也就是一种针对特定应用程序领域的语言。在传统上,DSL的领域很有限。例如,HTML是一种定义结构化Web页面的DSL。SQL是用来解释数据库查询的DSL。移动应用程序的领域比上述要大得多。事实上,它非常大,以至于需要大量你通常只能在一般目的的语言或者GPL像Java、Python和Ruby中才能够找到的特性。这些典型的GPL特性包括面向对象编程、if指令和for循环等等。既然mobl拥有GPL特性,那么它还是一种DSL吗?
我们觉得是,因为它拥有语言结构,这些结构都特别地适合数据驱动的移动Web应用程序领域。例如,如果我们使用一种带有样式支持的一般目的语言,编写的是用于处理科学计算的程序,那么就不太合理了。而把screen
结构应用于服务端计算也不是很合理。像实体、屏幕、控件、样式以及Web服务等语言特性并不针对一般目的它们都是针对特定领域的。
mobl不仅仅是针对移动开发的DSL。类似的还包括Applause和由此衍生的Applitude。然而,这些DSL的灵活性都有限。它们都拥有一系列的内建控件、内建函数,一旦这些都无法满足你的需求,你就需要重新使用Objective-C或者Java来编码了。mobl的目标就是,既要灵活,又要具备较强的表达能力。
为了达到这个目的,我们让这门语言尽可能小,并且通过使用mobl本身编写库来增加功能。例如,我们用来构建to-do列表应用程序的控件都没有内建在语言之中。相反,它们都是从mobl库导入的,而这个库本身又是使用mobl定义的。在最低层级上,对控件的实现使用了低级的HTML标签和CSS样式。一般用户只会使用高级别的概念控件,像标题或者按钮等等,而专家级的开发者能够通过调整库中的底层HTML代码来精确地设计按钮的显示样式。
mobl除了能够在库中定义控件之外,它还拥有暴露了大量HTML5 API的库,包括集合定位、使用Canvas进行2D绘图以及WebSockets等等。这些库只是封装了已存在的JavaScript API,而mobl API使用mobl的本地接口,这使得它能够调用本地JavaScript代码。这样,mobl就可以通过库机制进行扩展,这让用户可以扩展平台,而不需要扩展语言和编译器本身。
在特定的情况下,mobl会为特定的库添加句法的特性。例如,对于查询,mobl暴露了Collection
类型,它拥有对实体对象的集合进行过滤、排序和分页的方法。我们可以像下面这样来调用这些方法:
var doneTasks = Task.all().filter("done", "=", true)
.order("date", false).limit(10);
很明显,这些语法有些麻烦。因此,mobl为查询添加了句法特性,让我们可以把上面的查询写成这样:
var doneTasks = Task.all() where done == true
order by date desc limit 10;
这不仅更加简洁,而且现在IDE可以检查事实上Task
是否拥有done
和date
属性,并且提供了恰当的代码自动完成功能。
结论
mobl没有在已存在的语言基础之上构建框架,而是从头开始,构建了一种外部DSL。这种方法有优点也有缺点。缺点在于用户需要学习新语言、新库以及新的工具。优点在于,选择这种语言来进行设计,可以显著减少开发者所要编写的代码。在mobl中,我们保持它的语法与语义与JavaScript类似,从而让开发者觉得这种语言很熟悉。此外,mobl能够集成到现有的Eclipse IDE和外围工具中,提供在输入时检测错误、引用解析、代码自动完成和保存时编译等功能。
mobl还是一种很年轻的语言。第一次公开发布是在2011年1月。它的编译器、工具和文档还在逐步完善中。尽管如此,我们觉得它已经显示出在移动领域使用DSL的潜力。