使用WebSharper和F#开发移动应用

虽然开发移动应用程序是一件棘手的事情,但是只要在开始阶段拥有正确的方向和技术基础,一切都会变得不同。在许许多多的技术替代方案面前,移动应用 开发人员会不断地意识到,专攻于某个特定的平台将不再是可行之道。传统的原生平台(iOS,Android,Windows Phone 7,Windows Mobile等等)没必要搞得那么复杂,并且没必要固定到某个软件栈(software stack),因为后者不仅学习曲线陡峭,而且需要解决许多问题才能摸清它们各自平台的内在联系。如果没有足够强的驱动力,在那些原生平台上进行开发的 话,那么随后紧密控制的应用程序开发和销售渠道会让事情变得更糟糕。

不过还好,至少有两种方法可以摆脱在原生平台上开发的困境。一种方法是采用更加熟悉的编程语言和开发环境,并将结果转换为原生代码(这一般发生在iOS上的开发,如类似MonoTouch的解决方案)。在一定程度上,这种方法依赖于学习类似的复杂API,以及在API之上进行特殊处理以完成正确映射,从而获得原生设备的能力。

另一种方法是选择基于Web的移动应用程序。虽然它们的开发环境有些蹩脚,但是消除了对特定平台相关技术的需求,并将应用程序放到基于通用的Web 标准(如HTML5,CSS3和JavaScript)基础之上,这大大简化了跨平台的扩展能力。然而我们从Web中学到的一件事情是:不能也不应当期望 它所能做的事情以及提供的服务必须能被任何设备使用。你可以预期未来移动平台版本和操作系统会进一步模糊传统的“原生”和“Web”应用程序之间的界限。

目前,开发基于Web的跨平台移动应用程序已经有了像PhoneGapRhomobile,和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<Action>

    | Logout
    | Echo of string

根据sitelet的服务目的(如REST 服务),可以在其中加入任意的内容,如返回任意的包含XML或HTML内容的文件。如果需要对URL空间进行细粒度的控制、需要它们能够自动的从行动类型 中推测出、或是使用其中一种策略通过把更小的sitelet结合在一起以满足两种需求,那么可以通过手工构造路由和控制器,

sitelet还带有一个类型安全的模板语言,该语言基于XML标记并使用特殊的占位符。当你将后缀为.template.xml的文件加入到WebSharper Visual Studio解决方案中时,它们会被自动地转换为F#代码并包含在构建列表中。

下面显示了同样是来自于样例sitelet应用程序模板中的Skin.template.xml中的模板标注:

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
    <titlegt;Your site title</title>
    <link href="~/themes/reset.css" rel="stylesheet" type="text/css" />

    <link href="~/themes/site.css" rel="stylesheet" type="text/css" />

</head>
<body>
    <div>
        <div id="loginInfo">${LoginInfo}</div>

<div id=header>
<div id=banner>${Banner}</div>
<div id=menu>${Menu}</div>
<div class=closer></div>
</div>

<div id=main-container>
<div id=main>${Main}</div>
<div id=sidebar>${Sidebar}</div>
<div class=closer></div>
</div>

<div id=footer>${Footer}</div>
</div>
</body>
</html>

上述模板会在默认的命名空间下创建一个叫做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<Action> =
    let menu (ctx: Context<Action>)=
        [
            A [Action.Home |> ctx.Link |> HRef] -< [Text "Home"]
            A [Action.Contact |> ctx.Link |> HRef] -< [Text "Contact"]
            A [Action.Echo "Hello" |> ctx.Link |> HRef] -< [Text "Say Hello"]
            A [Action.Protected |> ctx.Link |> RandomizeUrl |> HRef] -< [Text "Protected"]
            A ["~/LegacyPage.aspx" |> ctx.ResolveUrl |> HRef] -< [Text "ASPX Page"]
         ]
         |> List.map (fun link ->

             Label [Class "menu-item"] -< [link]
         )
     Templates.Skin.Skin (Some title)
         {
             LoginInfo = Widgets.LoginInfo
             Banner = fun ctx -> [H2 [Text title]]
             Menu = menu
             Main = main
             Sidebar = fun ctx -> [Text "Put your side bar here"]
             Footer = fun ctx -> [Text "Your website. Copyright (c) 2011 YourCompany.com"]
         }

这里的main是一个生成XML/HTML元素列表的函数,它与内部处理菜单的meanu函数类似。另外,还要注意一下context对象是怎样利 用管道运算符将各种不同的Action映射到安全的URL上的(注:管道|>操作符用来像函数发送参数,例如 x |> f 等同于f(x))。

你还可以定义各种小型的抽象类型,使你的应用程序代码变得更加简洁。下面是一个链接操作符(=>),用作创建超链接:

/// A helper function to create a hyperlink
let ( => ) title href =
    A [HRef href] -< [Text title]

现在你可以在sitelet中定义主页了,如下:

/// The pages of this website.
modulePages =

/// The home page.
let HomePage : Content<Action> =
Template “Home” <| fun ctx ->
[
H1 [Text “Welcome to our site!”]
“Let us know how we can contact you” => ctx.Link Action.Contact
]

一旦定义好所有的页面,就可以创建一个sitelet来显示网站了。下面显示了三个更小的sitelet的组合:

letEntireSite =

// 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 <| fun action ->
match action with
| Action.Contact -> Pages.ContactPage
| Action.Echo param -> Pages.EchoPage param
| Action.Login action -> Pages.LoginPage action
| Action.Logout ->
// Logout user and redirect to home
UserSession.Logout ()
Content.Redirect Action.Home
| Action.Home -> Content.Redirect Action.Home
| Action.Protected -> Content.ServerError

// A sitelet for the protected content that requires users to log in first.
let authenticated =
let filter : Sitelet.Filter<Action> =
{
VerifyUser = fun _ -> true
LoginRedirect = Some >> Action.Login
}

Sitelet.Protect filter
<| 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<SampleSite.Action> with
        member this.Sitelet = EntireSite
        memberthis.Actions = []

[<assembly: WebsiteAttribute(typeof<SampleWebsite>)>]
do ()

Formlet —— 组合一流的类型安全表单

Formlet是 最近一套来自学术界的形式体系,它是WebSharper不可分割的一部分。而WebSharper则是最初实现Formlet的几个框架之一。 Formlet代表了一流的、类型安全的、可组合的数据表单,它与你可能一直在用的ASP.NET或其他Web框架中的非严格类型的方法有着很大的不同。 WebSharper实现中包含了从属formlet,其中formlet的一部分从属于另一部分,例如从属于多选项的下拉框或是输入框中的输入值;flowlets是一种定制的布局,它用来在一个formlet计算表达式或F#一元结构中以一种类似向导的顺序方式一步步渲染每一个formlet。

下面是一个简单的formlet,它返回一个字符串值,其中各种不同的增强被增量式应用于其上:

let nameF =
    Controls.Input ""
    |> Validator.IsNotEmpty "Empty name not allowed"
    |> Enhance.WithValidationIcon
    |> Enhance.WithTextLabel "Name"

Formlet可以被映射到任意类型的返回值上,例如一个百分比输入控件可能会返回0到100之间的浮点数值,或者一个组合框可能会生成可区分联合 (discriminated union)中的某种类型(可能有也可能没有标记值)。你可以用许多方式将多个较小的formelet组合成更大的formlet。最简单的方法是使用 Formlet.Yield函数将任意类型的值封装成该类型的formlet,并结合<*>操作符组合两个(或通过连续调用组合多 个)formlet:

Formlet.Yield (fun v1 v2 ... vn -> <compose all v’s>)
<*> formlet1
<*> formlet2
...

<*> formletn

下面的例子展示了Formlet如何获取个人信息(姓名和邮件),并进行基本的客户端验证:

typePerson = {
    Name: string
    Email: string
}

[<JavaScript>]
let PersonFormlet () : Formlet<Person> =
let nameF =
Controls.Input “”
|> Validator.IsNotEmpty “Empty name not allowed”
|> Enhance.WithValidationIcon
|> Enhance.WithTextLabel “Name”
let emailF =
Controls.Input “”
|> Validator.IsEmail “Please enter valid email address”
|> Enhance.WithValidationIcon
|> Enhance.WithTextLabel “Email”
Formlet.Yield (fun name email -> { Name = name; Email = email })
<*> nameF
<*> emailF
|> Enhance.WithSubmitAndResetButtons
|> Enhance.WithLegend “Add a New Person”
|> Enhance.WithFormContainer

图2显示了嵌入在sitelet页面后的结果。注意页面样式是由从属的CSS资源提供的,它会在引用formlet代码时自动加载到页面中(事实 上,准确地说是发生在调用Enhance.WithFormContainer时)。WebSharper中高级的依赖性跟踪功能会在页面处于服务状态时 为其自动收集所依赖的资源。这个功能非常便利,它为使用各种不同的WebSharper扩展和使用第三方的JavaScript库节省了大量的时间和精 力,并且它从根本上消除了手工跟踪页面所需资源的需要。

图2:包含验证以及各种增强的简单formlet

上面formlet例子中的[<JavaScript>]标注指示WebSharper将代码段翻译为JavaScript。每个控件 中增强的验证器均为WebSharper formlet库的一部分,并且它们提供客户端验证,因此Validator.IsEmail将确保在formlet在到达一种可接受状态前只键入了合法 的邮件地址。你还可以调用自定义的函数或者通过进一步加强手头的formlet来提供额外的验证。如果某个函数被标记为[<Rpc>]并从客 户端代码中调用,那么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 JSYUI,或是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 -> user, pass, remember)
            <*> (Controls.TextField "" Theme.C "Username: "

                |> Validator.IsNotEmpty "Username cannot be empty!")
            <*> (Controls.Password "" Theme.C "Password: "
                |> Validator.IsRegexMatch "^[1-4]{4,}[0-9]$" "The password is wrong!")
            <*> Controls.Checkbox true Theme.C "Keep me logged in "

                |> Enhance.WithSubmitButton "Log in" Theme.C
        let rememberText =
            if remember then "" else "not "

        do! Formlet.OfElement (fun _ ->
            Div [
                H3 [Text ("Welcome " + username + "!")]
                P [Text ("We will " + rememberText + "keep you logged in.")]
            ])
    }
    |> Formlet.Flowlet

你可以使用必要的jQuery移动功能将登录序列组合进HTML标记中(可以使用多几行的代码将其很好地抽象出来),然后添加到sitelet页面上:

Div [HTML5.Attr.Data "role" "page"] -< [
    Div [HTML5.Attr.Data "role" "header"] -< [
        H1 [Text "WebSharper Formlets for jQuery Mobile"]>
    ]

Div [HTML5.Attr.Data “role” “content”] -< [
loginSequenceF
]

Div [HTML5.Attr.Data “role” “footer”] -< [
P [Attr.Style “text-align: center;”] -< [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
openIntelliFactory.WebSharper.Mobile

type CurrentLocationControl() =
inherit Web.Control()

[<JavaScript>]
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(<<your-bingmaps-key>>, loc.Lat, loc.Long, [“Address”],
fun result ->
let locInfo = result.ResourceSets.[0].Resources.[0]
label.Text <- “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 |> ignore

let map =
Div []
|>! OnAfterRender (fun this ->
// Renders a Bing Maps control
let map = Bing.Map(this.Body, MapOptions)
map.SetMapType(Bing.MapTypeId.Road)
setMap map)

// Returns the HTML markup for this control
Div [
label
Br []
map
] :> _

图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)关注我们,并与我们的编辑和其他读者朋友交流。

This entry was posted in Achitecture, ASP.NET, UI. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s