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

阅读数:2503 2012 年 2 月 28 日 00:00

虽然开发移动应用程序是一件棘手的事情,但是只要在开始阶段拥有正确的方向和技术基础,一切都会变得不同。在许许多多的技术替代方案面前,移动应用开发人员会不断地意识到,专攻于某个特定的平台将不再是可行之道。传统的原生平台(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<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.
module Pages =

/// 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 的组合:

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 <| 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
        member this.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 如何获取个人信息(姓名和邮件),并进行基本的客户端验证:

type Person = {
    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 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 -> 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
open IntelliFactory.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 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

] :> _

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

评论

发布