虽然开发移动应用程序是一件棘手的事情,但是只要在开始阶段拥有正确的方向和技术基础,一切都会变得不同。在许许多多的技术替代方案面前,移动应用开发人员会不断地意识到,专攻于某个特定的平台将不再是可行之道。传统的原生平台(iOS,Android,Windows Phone 7,Windows Mobile等等)没必要搞得那么复杂,并且没必要固定到某个软件栈(software stack),因为后者不仅学习曲线陡峭,而且需要解决许多问题才能摸清它们各自平台的内在联系。如果没有足够强的驱动力,在那些原生平台上进行开发的话,那么随后紧密控制的应用程序开发和销售渠道会让事情变得更糟糕。
\不过还好,至少有两种方法可以摆脱在原生平台上开发的困境。一种方法是采用更加熟悉的编程语言和开发环境,并将结果转换为原生代码(这一般发生在iOS上的开发,如类似MonoTouch的解决方案)。在一定程度上,这种方法依赖于学习类似的复杂API,以及在API之上进行特殊处理以完成正确映射,从而获得原生设备的能力。
\另一种方法是选择基于Web的移动应用程序。虽然它们的开发环境有些蹩脚,但是消除了对特定平台相关技术的需求,并将应用程序放到基于通用的Web标准(如HTML5,CSS3和JavaScript)基础之上,这大大简化了跨平台的扩展能力。然而我们从Web中学到的一件事情是:不能也不应当期望它所能做的事情以及提供的服务必须能被任何设备使用。你可以预期未来移动平台版本和操作系统会进一步模糊传统的“原生”和“Web”应用程序之间的界限。
\目前,开发基于Web的跨平台移动应用程序已经有了像PhoneGap,Rhomobile,和AppMobi的解决方案,它们依赖于使用JavaScript API暴露原生设备功能,并通过在原生的Shell应用程序中运行上述API编写的代码来渲染Web应用程序。这听起来像是一个不错的提议,但是前提是需要使用JavaScript开发。另外一种选择是基于领域专用语言(Domain Specific Language, DSL)。此外,InfoQ上有一篇文章讨论了移动Web应用程序开发现状。
\WebSharper
\WebSharper旨在解决上面的一些问题。首先,它可以使用F#开发整个Web和移动应用程序,整个开发过程不仅可以享受F#简洁的语法和强大的函数式结构,还可以减少许多过去需要经常性编写的代码。其次,它为常见的Web相关的琐碎工作提供了一系列丰富的抽象及eDSL语法,例如组合HTML、定义Web表单、管理所需资源、安全地处理URL以及其他许多工作。WebSharper之所以特别适合于大型企业级应用程序开发,是因为这些抽象都是强类型的:例如,构造Web表单时产生错误数据类型,或尝试为错误的输入控件添加表格验证,都会造成编译期错误,这再次大大的缩短了开发时间。
\利用sitelet构造站点
\WebSharper 2.0 引入了sitelet,它是类型安全的头等网站元素。sitelet定义在“action”联合类型之上,它包含了所表示站点中全部网页/内容的集合,还包含一个路由和一个控制器用作在动作(action)和真实的内容之间来回地映射URL请求。
\(点击图片进行放大)
\ \图1:WebSharper Visual Studio模板中的样例网页
\下面是从安装后的WebSharper样例sitelet应用程序模板中抽取的一个简单的Action类型,它定义了图1中元素较少的样例网页。
\/// Actions that correspond to the different pages in the site.\type Action =\ | Home\ | Contact\ | Protected\ | Login of option\u0026lt;Action\u0026gt;\\ | Logout\ | Echo of string\
根据sitelet的服务目的(如REST 服务),可以在其中加入任意的内容,如返回任意的包含XML或HTML内容的文件。如果需要对URL空间进行细粒度的控制、需要它们能够自动的从行动类型中推测出、或是使用其中一种策略通过把更小的sitelet结合在一起以满足两种需求,那么可以通过手工构造路由和控制器,
\sitelet还带有一个类型安全的模板语言,该语言基于XML标记并使用特殊的占位符。当你将后缀为.template.xml的文件加入到WebSharper Visual Studio解决方案中时,它们会被自动地转换为F#代码并包含在构建列表中。
\下面显示了同样是来自于样例sitelet应用程序模板中的Skin.template.xml中的模板标注:
\\u0026lt;html xmlns=\"http://www.w3.org/1999/xhtml\"\u0026gt;\\\u0026lt;head\u0026gt;\ \u0026lt;titlegt;Your site title\u0026lt;/title\u0026gt;\ \u0026lt;link href=\"~/themes/reset.css\" rel=\"stylesheet\" type=\"text/css\" /\u0026gt;\\ \u0026lt;link href=\"~/themes/site.css\" rel=\"stylesheet\" type=\"text/css\" /\u0026gt;\\\u0026lt;/head\u0026gt;\\u0026lt;body\u0026gt;\ \u0026lt;div\u0026gt;\ \u0026lt;div id=\"loginInfo\"\u0026gt;${LoginInfo}\u0026lt;/div\u0026gt;
\u0026lt;div id=\"header\"\u0026gt;
\u0026lt;div id=\"banner\"\u0026gt;${Banner}\u0026lt;/div\u0026gt;\u0026lt;div id=\"menu\"\u0026gt;${Menu}\u0026lt;/div\u0026gt;\u0026lt;div class=\"closer\"\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;
\u0026lt;div id=\"main-container\"\u0026gt;
\u0026lt;div id=\"main\"\u0026gt;${Main}\u0026lt;/div\u0026gt;\u0026lt;div id=\"sidebar\"\u0026gt;${Sidebar}\u0026lt;/div\u0026gt;\u0026lt;div class=\"closer\"\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;
\u0026lt;div id=\"footer\"\u0026gt;${Footer}\u0026lt;/div\u0026gt;
\u0026lt;/div\u0026gt;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;
\上述模板会在默认的命名空间下创建一个叫做Templates.Skin的模块,用以组合标记片段到占位符中。考虑下面的函数,它接受标题(title)和网页主要内容(main)作为参数,并使用生成的模板函数构造出网页:
\/// A template function that renders a page with a menu bar, based on the Skin template.\let Template title main : Content\u0026lt;Action\u0026gt; =\ let menu (ctx: Context\u0026lt;Action\u0026gt;)=\ [\ A [Action.Home |\u0026gt; ctx.Link |\u0026gt; HRef] -\u0026lt; [Text \"Home\"]\ A [Action.Contact |\u0026gt; ctx.Link |\u0026gt; HRef] -\u0026lt; [Text \"Contact\"]\ A [Action.Echo \"Hello\" |\u0026gt; ctx.Link |\u0026gt; HRef] -\u0026lt; [Text \"Say Hello\"]\ A [Action.Protected |\u0026gt; ctx.Link |\u0026gt; RandomizeUrl |\u0026gt; HRef] -\u0026lt; [Text \"Protected\"]\ A [\"~/LegacyPage.aspx\" |\u0026gt; ctx.ResolveUrl |\u0026gt; HRef] -\u0026lt; [Text \"ASPX Page\"]\ ]\ |\u0026gt; List.map (fun link -\u0026gt;\\ Label [Class \"menu-item\"] -\u0026lt; [link]\ )\ Templates.Skin.Skin (Some title)\ {\ LoginInfo = Widgets.LoginInfo\ Banner = fun ctx -\u0026gt; [H2 [Text title]]\ Menu = menu\ Main = main\ Sidebar = fun ctx -\u0026gt; [Text \"Put your side bar here\"]\ Footer = fun ctx -\u0026gt; [Text \"Your website. Copyright (c) 2011 YourCompany.com\"]\ }\
这里的main是一个生成XML/HTML元素列表的函数,它与内部处理菜单的meanu函数类似。另外,还要注意一下context对象是怎样利用管道运算符将各种不同的Action映射到安全的URL上的(注:管道|\u0026gt;操作符用来像函数发送参数,例如 x |\u0026gt; f 等同于f(x))。
\你还可以定义各种小型的抽象类型,使你的应用程序代码变得更加简洁。下面是一个链接操作符(=\u0026gt;),用作创建超链接:
\/// A helper function to create a hyperlink\let ( =\u0026gt; ) title href =\ A [HRef href] -\u0026lt; [Text title]\
现在你可以在sitelet中定义主页了,如下:
\/// The pages of this website.\module Pages =
/// The home page.
let HomePage : Content\u0026lt;Action\u0026gt; =
Template \"Home\" \u0026lt;| fun ctx -\u0026gt;
[
H1 [Text \"Welcome to our site!\"]
\"Let us know how we can contact you\" =\u0026gt; ctx.Link Action.Contact
]
...
一旦定义好所有的页面,就可以创建一个sitelet来显示网站了。下面显示了三个更小的sitelet的组合:
\let EntireSite =\
// A simple sitelet for the home page, available at the root of the application.
let home =
Sitelet.Content \"/\" Action.Home Pages.HomePage
// An automatically inferred sitelet created for the basic parts of the application.
let basic =
Sitelet.Infer \u0026lt;| fun action -\u0026gt;
match action with
| Action.Contact -\u0026gt; Pages.ContactPage
| Action.Echo param -\u0026gt; Pages.EchoPage param
| Action.Login action -\u0026gt; Pages.LoginPage action
| Action.Logout -\u0026gt;
// Logout user and redirect to home UserSession.Logout ()
Content.Redirect Action.Home
| Action.Home -\u0026gt; Content.Redirect Action.Home
| Action.Protected -\u0026gt; Content.ServerError
// A sitelet for the protected content that requires users to log in first.
let authenticated =
let filter : Sitelet.Filter\u0026lt;Action\u0026gt; =
{
VerifyUser = fun _ -\u0026gt; true
LoginRedirect = Some \u0026gt;\u0026gt; Action.Login
}
Sitelet.Protect filter
\u0026lt;| Sitelet.Content \"/protected\" Action.Protected Pages.ProtectedPage
// Compose the above sitelets into a larger one.
Sitelet.Sum
[
home
authenticated
basic
]
借助上面的sitelet,你所需要做的就是标注它为sitelet,然后,你瞧,你的站点可以在基于ASP.NET的Web容器里工作了(WebSharper Visual Studio模板提供了必要的Web.config改动):
\/// Expose the main sitelet so it can be served.\/// This needs an IWebsite type and an assembly level annotation.\type SampleWebsite() =\ interface IWebsite\u0026lt;SampleSite.Action\u0026gt; with\ member this.Sitelet = EntireSite\ member this.Actions = []\\
[\u0026lt;assembly: WebsiteAttribute(typeof\u0026lt;SampleWebsite\u0026gt;)\u0026gt;]
do ()
Formlet —— 组合一流的类型安全表单
\Formlet是最近一套来自学术界的形式体系,它是WebSharper不可分割的一部分。而WebSharper则是最初实现Formlet的几个框架之一。Formlet代表了一流的、类型安全的、可组合的数据表单,它与你可能一直在用的ASP.NET或其他Web框架中的非严格类型的方法有着很大的不同。WebSharper实现中包含了从属formlet,其中formlet的一部分从属于另一部分,例如从属于多选项的下拉框或是输入框中的输入值;flowlets是一种定制的布局,它用来在一个formlet计算表达式或F#一元结构中以一种类似向导的顺序方式一步步渲染每一个formlet。
\下面是一个简单的formlet,它返回一个字符串值,其中各种不同的增强被增量式应用于其上:
\let nameF =\ Controls.Input \"\"\ |\u0026gt; Validator.IsNotEmpty \"Empty name not allowed\"\ |\u0026gt; Enhance.WithValidationIcon\ |\u0026gt; Enhance.WithTextLabel \"Name\"\
Formlet可以被映射到任意类型的返回值上,例如一个百分比输入控件可能会返回0到100之间的浮点数值,或者一个组合框可能会生成可区分联合(discriminated union)中的某种类型(可能有也可能没有标记值)。你可以用许多方式将多个较小的formelet组合成更大的formlet。最简单的方法是使用Formlet.Yield函数将任意类型的值封装成该类型的formlet,并结合\u0026lt;*\u0026gt;操作符组合两个(或通过连续调用组合多个)formlet:
\\Formlet.Yield (fun v1 v2 ... vn -\u0026gt; \u0026lt;compose all v’s\u0026gt;)\\u0026lt;*\u0026gt; formlet1\\u0026lt;*\u0026gt; formlet2\...\\\u0026lt;*\u0026gt; formletn\
下面的例子展示了Formlet如何获取个人信息(姓名和邮件),并进行基本的客户端验证:
\type Person = {\ Name: string\ Email: string\}\
[\u0026lt;JavaScript\u0026gt;]
let PersonFormlet () : Formlet\u0026lt;Person\u0026gt; =
let nameF =
Controls.Input \"\"
|\u0026gt; Validator.IsNotEmpty \"Empty name not allowed\"
|\u0026gt; Enhance.WithValidationIcon
|\u0026gt; Enhance.WithTextLabel \"Name\"
let emailF =
Controls.Input \"\"
|\u0026gt; Validator.IsEmail \"Please enter valid email address\"
|\u0026gt; Enhance.WithValidationIcon
|\u0026gt; Enhance.WithTextLabel \"Email\"
Formlet.Yield (fun name email -\u0026gt; { Name = name; Email = email })
\u0026lt;*\u0026gt; nameF
\u0026lt;*\u0026gt; emailF
|\u0026gt; Enhance.WithSubmitAndResetButtons
|\u0026gt; Enhance.WithLegend \"Add a New Person\"
|\u0026gt; Enhance.WithFormContainer
图2显示了嵌入在sitelet页面后的结果。注意页面样式是由从属的CSS资源提供的,它会在引用formlet代码时自动加载到页面中(事实上,准确地说是发生在调用Enhance.WithFormContainer时)。WebSharper中高级的依赖性跟踪功能会在页面处于服务状态时为其自动收集所依赖的资源。这个功能非常便利,它为使用各种不同的WebSharper扩展和使用第三方的JavaScript库节省了大量的时间和精力,并且它从根本上消除了手工跟踪页面所需资源的需要。
\图2:包含验证以及各种增强的简单formlet
\上面formlet例子中的[\u0026lt;JavaScript\u0026gt;]标注指示WebSharper将代码段翻译为JavaScript。每个控件中增强的验证器均为WebSharper formlet库的一部分,并且它们提供客户端验证,因此Validator.IsEmail将确保在formlet在到达一种可接受状态前只键入了合法的邮件地址。你还可以调用自定义的函数或者通过进一步加强手头的formlet来提供额外的验证。如果某个函数被标记为[\u0026lt;Rpc\u0026gt;]并从客户端代码中调用,那么WebSharper将会生成代码执行RPC(远程过程调用)并自动处理客户端和服务端的值传递。你可以无缝使用任意复杂的F#对象,如嵌套列表(nested list)、映射(map)、集合(set)或序列(sequence),而不用担心它们在内部被如何映射。这统一了客户端和服务端代码,并且大大地减少了开发时间。事实上,客户端和服务端代码在开发过程中通常位于同一个F#文件中,只是它们被组织进同一命名空间下的不同模块中。
\许多WebSharper模式可以用来开发客户端-服务器应用程序,我们通常建议使用sitelet和formlet一起工作,并提供各种编码指导来最大限度地提高开发人员的工作效率,但是你也可以借助WebSharper在大量的ASP.NET代码基础上开发混合型的应用程序,或者基于WebSharper的功能改善现有的ASP.NET应用程序。
\从抽象中构建来满足所需
\有时,你可能需要跳出标准WebSharper formlet库的范围来为应用程序实现表单(或者整个UI)。例如,你可能想要使用不同的输入控件来渲染formlet,因为简单的CSS重写可能不能够满足你所想要的外观和感觉。其它时候,你想重用现有的JavaScript控件库,如Ext JS,YUI,或是jQuery UI来得到更精细的外观和感觉。WebSharper为上述的第三方库提供了大量的扩展包,其中一些扩展包还提供了formlet抽象。
\下面的简短例子在jQuery移动扩展中使用了Formlet,通过在Formlet.Do中使用flowlet布局以及组合熟悉的Formlet.Yield一起完成了两个步骤的登录序列:
\let loginSequenceF =\ Formlet.Do {\ let! username, password, remember =\ Formlet.Yield (fun user pass remember -\u0026gt; user, pass, remember)\ \u0026lt;*\u0026gt; (Controls.TextField \"\" Theme.C \"Username: \"\\ |\u0026gt; Validator.IsNotEmpty \"Username cannot be empty!\")\ \u0026lt;*\u0026gt; (Controls.Password \"\" Theme.C \"Password: \"\ |\u0026gt; Validator.IsRegexMatch \"^[1-4]{4,}[0-9]$\" \"The password is wrong!\")\ \u0026lt;*\u0026gt; Controls.Checkbox true Theme.C \"Keep me logged in \"\\ |\u0026gt; Enhance.WithSubmitButton \"Log in\" Theme.C\ let rememberText =\ if remember then \"\" else \"not \"\\ do! Formlet.OfElement (fun _ -\u0026gt;\ Div [\ H3 [Text (\"Welcome \" + username + \"!\")]\ P [Text (\"We will \" + rememberText + \"keep you logged in.\")]\ ])\ }\ |\u0026gt; Formlet.Flowlet\
你可以使用必要的jQuery移动功能将登录序列组合进HTML标记中(可以使用多几行的代码将其很好地抽象出来),然后添加到sitelet页面上:
\\Div [HTML5.Attr.Data \"role\" \"page\"] -\u0026lt; [\ Div [HTML5.Attr.Data \"role\" \"header\"] -\u0026lt; [\ H1 [Text \"WebSharper Formlets for jQuery Mobile\"]\u0026gt;\ ]\
Div [HTML5.Attr.Data \"role\" \"content\"] -\u0026lt; [
loginSequenceF
]
Div [HTML5.Attr.Data \"role\" \"footer\"] -\u0026lt; [
P [Attr.Style \"text-align: center;\"] -\u0026lt; [Text \"IntelliFactory\"]
]
]
一旦你在WebSharper移动项目中调整移动配置文件,产生了Android包(也可以选择Windows Phone 7),那么将其安装至手机,你会看到如图3所示的界面:
\图3:运行在Android上的jQuery移动formlet
\使用WebSharper移动API和第三方地图控件
\Formlet和sitelet大大简化了Web开发和移动开发,并且提供了健壮、类型安全且可组合的抽象来为应用程序的部分模块进行建模。WebSharpe中的另一个基础抽象是pagelets,它由多个formlet搭建而成。pagelet代表了一流的、可组合的客户端标注及行为。WebSharper的pagelet不仅与ASP.NET控件兼容,还可以直接嵌入到ASP.NET标记中。
\下面的例子是实现了地图控件的pagelet,运行结果如图4所示:
\open IntelliFactory.WebSharper\open IntelliFactory.WebSharper.Bing\\open IntelliFactory.WebSharper.Html\open IntelliFactory.WebSharper.JQuery\open IntelliFactory.WebSharper.Mobile\
type CurrentLocationControl() =
inherit Web.Control()
[\u0026lt;JavaScript\u0026gt;]
override this.Body =
let screenWidth = JQuery.Of(\"body\").Width()
let MapOptions =
Bing.MapViewOptions(
Credentials = bingMapsKey,
Width = screenWidth - 10,
Height = screenWidth - 10,
Zoom = 16)
let label = H2 []
let setMap (map : Bing.Map) =
let updateLocation() =
// Gets the current location
let loc = Mobile.GetLocation()
// Sets the label to be the address of the current location
Rest.RequestLocationByPoint(\u0026lt;\u0026lt;your-bingmaps-key\u0026gt;\u0026gt;, loc.Lat, loc.Long, [\"Address\"],
fun result -\u0026gt;
let locInfo = result.ResourceSets.[0].Resources.[0]
label.Text \u0026lt;- \"You are currently at \" + JavaScript.Get \"name\" locInfo)
// Adds a pushpin at the current location
let loc = Bing.Location(loc.Lat, loc.Long)
let pin = Bing.Pushpin loc
map.Entities.Clear()
map.Entities.Push pin
map.SetView(Bing.ViewOptions(Center = loc))
// Keep updating your location regularly
JavaScript.SetInterval updateLocation 1000 |\u0026gt; ignore
let map =
Div []
|\u0026gt;! OnAfterRender (fun this -\u0026gt;
// Renders a Bing Maps controllet map = Bing.Map(this.Body, MapOptions)
map.SetMapType(Bing.MapTypeId.Road)
setMap map)
// Returns the HTML markup for this control
Div [
label
Br []
map
] :\u0026gt; _
图4:通过Bing地图控件和地址栏显示当前位置信息
\该控件使用WebSharper移动API获取当前GPS位置。IntelliFactory.WebSharper.Mobile命名空间下还有许多实用工具用于底层移动设备间的交互,包括获取加速计数据,访问摄像头的能力以及显示原生的警告信息。未来版本的WebSharper移动API也将会包含平台相关的扩展,如蓝牙通信能力等等。
\总结
\如果你还没有用过X-to-JavaScript工具来帮助编写Web和移动应用程序的话,你也许想知道为什么它们的数量会有如此之多,以及是什么原因让人们想要去使用它们。WebSharper是一种针对F#的健壮的Web开放框架,它正在被一些企业级应用程序积极使用。WebSharper解决了许多Web和移动开发中经常遇到的问题,并且提供了众多的功能,如安全URL,自动资源跟踪,客户端标注及功能中提供的类型安全且可组合的抽象,带有客户端验证的声明式Web表单以及网站价值。
\WebSharper 2.3.28+更新和后续的2.4发布版本包含了用于移动Web开发的Visual Studio模板,使用模板你可以快速尝试和实验本文中的两个例子。你也可以在这里和这里下载到源代码,包括最后生成的Android包。
\关于作者
\Adam Granicz 是F#的资深业内人士和核心社区成员,他与人合著过三本F#书籍,包括与F#的语言设计者Don Syme合著的《Expert F# 2.0》。他的公司IntelliFactory专注于高级F#项目咨询,并为使用F# 进行Web、移动以及云端应用程序开发塑造未来,公司还开发了F#的首个Web开发框架——WebSharper。你可以通过granicz.adam {at} intellifactory.com与他联系,还可以关注他的Twitter,或者在函数式编程天堂FPish里找到他。
查看英文原文:F# mobile development with WebSharper
\感谢侯伯薇对本文的审校。
\给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。