第二章设计 Web 门户和 Widget 架构

 由于其致力于在一个单独页面上面提供功能,Ajax web 门户都是 Ajax 技术的杰作。在页面上提供众多的功能,同时又有服务器端和客户端良好的性能,对于架构是一个非常大的挑战。一些挑战只有在整合众多特性的 web 应用程序或者聚合其他网站内用的程序上面才有的。

本章解释了 Dropthings 的架构,你也可以在你的应用程序使用这样的架构。我们将检测一系列的架构挑战,包括怎样在一个页面中运行多个 widget ,快速的加载一个 web 门户,处理安全问题(DOS 攻击),处理用户数据等等。

任何 web 门户的核心都是支持 widget,这是一种能够使得用户定制他们自己起始页的机制,并且通过它,能够使得提供者使得其服务有效,不论是在一个公司的部门还是一个第三方组织,像路透社。

在一个像我们本书中使用的 ASPNET 实现中,default.aspx 是首页,它显示了 widget 并能够添加,删除,移动,定制 widget 不容任何刷新或者回发就能完成。

这个应用程序会记住用户的动作和定制,所以,在下次访问的时候,他将会看到和他上次离开时相同的 widget 和配置。web 门户也代表性的允许匿名用户来使用很多的特性,包括添加,编辑,删除 widget,以及创建多页面,不用注册情况下进行偏好设定。

一个 Dropthings widget 基本上一个 web 控件。可以使一个自定义控件或者服务器控件,但是它就像一个在 ASPNET 页面生命周期中的标准的服务器控件。Widget 支持 PostBack,ViewState,Seesion,和缓存。惟一的不同就是 Dropthings 的Widget 必须要实现 IWidget – 一个自定义接口 – 来整合 Widget 框架,并且使用核心框架提供的服务。一个定制的 Ajax 控件扩展为 widget 提供了拖放功能。widget 框架以及其核心将会在本章后面解释。(看“使用一个 widget 框架”节)。

一个 widget 被宿主在一个框架或者容器里面。这个容器提供了标题栏,由标题,编辑链接,最小化/最大化按钮,以及关闭按钮。widget 自身在标题栏下方的正文部分。事件,像改变标题,点击编辑链接,最小化/最大化,关闭,都是通过 IWidget 接口来通知。

在一个 web 门户中,使用异步的 PostBack 以及异步获取数据很重要,这样用户尽可能少的感觉到页面刷新。Widget 被作为支持 PostBack 的常规 ASPNET 控件来开发。所以,Dropthings 使用的核心的 widget 框架,将 widget 放进 UpdatePanel 保证所有的 PostBack 都是异步的。

尽管,你可以不用注册使用像 Dropthings 一样的站点,但是注册可以永久的保存页面,这样,当你在不同的计算机上,你可以登录并且获得相同的设置。ASPNET Membership 和 Profile Provider 允许匿名用户用户注册之后,保存状态。页面和 widget 的状态被保存在各自的表中。

对象模型

ASPNET Membership Provider 提供了用户和角色。如果用户有一个或者多个页面,每一个页面能够包含一个或者多个 widget 实例。widget 和 widget 实例的不同在于 widget 像一个类,然而一个 widget 实例是这个类的实例。举个例子,Flickr 照片的 widget 从 Flickr 加载照片。当用户添加了这样一个 widget 到页面上,它变成了 Flickr widget 的实例。尽管 widget 名词这本书里面都在使用,它实际代表着一个 widget 的实例。

图 2-1 显示了完整的对象模型

图 2-1. web 门户对象模型,有用户,用户设置(UserSetting),以及相关联的页面(Pages)。一个页面能够包含 widget 实例。

一个对象模型由用户开始,包含了一些设置以及一个或者多个页面。每一个页面又包含了0个或者多个 widget 实例。

应用程序组件

Dropthings 使用外观模式来提供一个单独的业务层的入口。它支持访问处理用户、页面 、widget等等的内部子系统。这个外观叫做 DashboardFacade(见图 2-2)。

图 2-2. Default.aspx 调用业务逻辑层的 DashboardFacade 来执行所有的操作,DashboardFacade 使用工作流操作数据库,工作流通过 DatabaseHelper 和 DatabaseContext 访问数据库。

在 Web 层,Default.aspx 是入口点。它使用 DashboardFacade 来完成像操作,像添加一个新的标签或者 widget,或者保存一个 widget 的状态。DashboardFacade 根据不用的操作调用不同的工作流,工作流通过 Windows WorkFlow Foundation (WF) 创建,执行真正的操作任务,在第四章会有介绍。每一个工作流由一个或者多个活动组成。每一个活动象一个类执行一些单元任务。活动使用 DatabaseHelper 和 DatabaseContext 类来操作数据库。DatabaseHelper 是一个执行通用数据库操作的类。DashBoardDataContext 是 LINQ to SQL 生成的,映射实体和数据库表的。

数据模型

为了完成应用程序中使用的数据模型,我们使用 ASPNET Membership Provider 默认的数据库表— aspnet_Users 表包含了所有的用户账号。架构由于其他的信息而被扩展了。(见图 2-3)。

图 2-3.aspnet_Users 表包含了用户,其他的表提供了实体。

一些表的重要的详细信息:

  • aspnet_Users 是ASPNET Membership 默认的表。然而,仅仅包含了匿名用户的账号。注册了的账号在 aspnet_membership 表。他并没有出现在图2-3中,因为他和这里面的表格没有关系。
  • Page 与aspnet_users 表的UserId有外键关系。
  • Widget 表包含了详细信息或者主人列表。它定义了每一个 Widget 的标题和其被动态加载的位置。也定义了用户第一次访问时候 Widget 的默认情况。
  • WidgetInstance 表的 PageId 和WidgetId 分别外键关联到 Page 和 Widget 表的ID列。
  • UserSetting 表的 UserId 外键关联到 aspnet_users 表的 UserId 列。

表 2-1 显示了表的索引情况及解释

关于建立正确的索引的通用性的方案:

  • 一个聚合索引应用在连续的字段上面,例如,自增长的整数字段。因为 SQL Server 在数据库物理文件中,基于一个聚合索引来排列,如果选择一些不连续增长的字段,那么在做 INSERT 和 DELETE 操作的时候,将会重拍非常多的页。
  • 外键字段是非聚合索引,因为他们并不是以递增顺序增加的

解决方案文件

Dropthings 解决方案由一个 Web 工程和四个 C#工程组成,可以在 www.codeplex.com/dropthings 下载。

Default.aspx
    控制起始页上的 widget

WidgetService.asmx
    暴露一些访问起始页上的 widget 的web service 方法

Proxy.asmx
    允许 widget 从外部源,或者构成起始页其他部分的 widget 中获得内容。

WidgetContainer.ascx
    共有的 widget 框架,宿主一个 widget 于其中,并且作为一个核心框架和真正的 widget 的桥梁。

Widgets 存储在 Widgets目录下。每一个 Widget 都被设计成一个 web 控件,所有相关的资源,像图片,CSS,JavaScript 都位于Widgets 目录的子目录中(见图 2-4)。

 

图 2-4. web 工程的目录,构成站点的文件

Update Panels

UpdatePanel 允许你异步的更新局部的起始页,给任何的站点一个 Ajax 的外观和风格。然而,UpdatePanel 是页面上的明显的拖累。你有越多的 UpdatePanel,异步的回发就会变的越慢,因为处理流程中要定位那个部分的回发和呈现。当你把 UpdatePanel 放进 UpdatePanel 的时候,情况就会更加复杂了。所以,在做架构的决定之前,自习的学习页面的布局很重要。

在 Dropthings,整个的 Widget 区域,使用 UpdatePanel 是一个很好的选择,因为,当用户切换标签的时候,这里需要重新加载。同样的,页面标签部分(添加新标签,删除标签的地方)他们自己也应该考虑放到 UpdatePanel 里面,因为标签的操作需要不影响页面其他部分。


添加 Widget 的区域也是在一个 UpdatePanel 里面,这样就可以异步的进行处理了(见图 2-5)。

图 2-5. Dropthings 首页使用了三个UpdatePanel

将整个的 widget 区域都放在一个 UpdatePanel 里面将会导致 在添加和删除 widget的时候性能的降低,因为为了反映当前 widget 的变化,整个的UpdatePanel 都需要被刷新。在异步更新的时候,需要传输大量的HTML 和 JavaScript。所以,一个更好的策略是将每一列放到一个 UpdatePanel 里面。在一列上面的更新,只是需要异步更新那一列的 UpdatePanel 而不是整个 widget 区域(见图 2-6)。

Figure 2-6. 使用了三个 UpdatePanel,每列一个。当一个 widget 添加或者被删除,所属的列就会更新。

当你从一列拖放一个 widget 到另外一列,不需要更新 UpdatePanel,因为 UI 已经通过 JavaScript 更新了。你只需要通知服务器端,哪一个 widget 移动了。服务器端能够重新计算出所有 widget 的新位置,就像客户端做得一样。所以,当拖放 widget 的时候,并没有一个异步的 PostBack;仅仅是添加或者删除的时候,才需要。

拖放操作

有两种方法来实现拖放操作:自由表单和列感知。Protopage (www.protopage.com)  是使用自由表单式拖放功能的,他可以允许你拖放 widget 到页面任意的地方。widget 的位置都是绝对的位置。但是 Live.com, IGoogle 和 PageFlakes允许列感知式的组织。这允许你将 Widgets 垂直方向在一列中拖放,也允许跨列拖放。列感知式的组织,使得页面总是干净清爽,因为 widget 很好的组织在了各列之中。这个方法被大多数 web 门户使用(见图 2-7)。

图 2-7. 显示了在列之间拖放 widget ,虚线框指示了,当前 widget 将被放置的位置。

在多列之间实现拖放动作,页面将会分成三列,每一列都是一个 ASPNET Panel 控件,widget 可以被添加到任意一个 Panel里面。widget 可以添加到任意一个 panel 里面。拖放的实现是使用自定制的extender。

有两种类型的拖放动作需要支持:在同一列拖放 widget 和不同列之间拖放。如果我们将每一个列都作为一个拖放区域,使用 ASPNET AJAX 框架的 IDragTarget 接口,然后,每一个 widget 是一个 IDragSource 就可以在列之间拖放了。更加挑战的是在相同的列内切换位置,就是将它们重新排序。举个例子,如果你把一个 widget 向下拖放,下面的 widget 将会跳上来,填补空白区域。相同的,如果你把一个 widget 覆盖到另外一个上面,被覆盖的 widget 需要向下移动,为被拖放的 widget 留出空间。这些动作都作为 Extender 实现,所以,你可以容易的将 Extender 附加到一个 panel 上面,这样,它就是一个 IDragTarget 并且提供了重新排序功能。

那么,怎么在拖放动作之后,将 widget 的位置异步的传送给服务器端呢?当你完成了一个拖放移动,它反映在 UI 上面,但是服务器并不知道发生了。任何的 PostBack 形式通知服务器都会破坏用户体验,因为整个页面或者列都要刷新。服务器需要在后台异步的被通知,这样用户就注意不到 widget 的位置在被拖放传递给了服务器,并且保存了起来。第二个挑战是提供完整个拖放功能的 Extender。Extender 需要附加在每一列的 panel 上面,并且将其作为一个拖放目标,也需要链接到 widget 的拖放处理,它允许widget 它被移动到任何的拖放目标中。

在下面一节中,你将会看到怎样添加 widget 和其容器到起始页上面。

使用一个 Widget 框架

Dropthings 使用了一个 widget 框架,允许你专注于提供 widget 本身的功能,而不用担心身份,权限,用户信息,个性化设置或者存储。Widget 从 widget 框架或者 Core 中获得这些功能,见图 2-8.

图 2-8.Widget 自动被身份,权限验证,定制化,以及从宿主获得存储和公用类库,这允许你轻易的向 web 门户中以 widget 的形式添加功能。Core 协调组织了各个服务。

另外,你可以独立于宿主工程创建 widget。在你的本地多于开发的计算机上,你不需要整个 web 门户的源代码就可以创建 widget。你只需要做得是,创建一个标准的 ASPNET 2.0 网站,创建一个用户控件,让它完成一个正规的 PostBack 模型(不用担心 JavaScript),实现一个小接口,你就完成了。你不需要担心我创建的 Dropthings 的 widget框架中的 Ajax 和 JavaScript。架构允许你使用常规的 ASPNET 2.0 控件, Ajax Control Tookit 控件(http://www.asp.net/ajax/ajaxcontroltoolkit/samples),以及所有的 ASPNET AJAX 的Extender。完整的服务器端编程支持,你可以使用 .NET 2.0,3.0 或者 3.5,也可以使用 ViewState 来存储临时状态。ASPNET 缓存可以用来缓存 widget 数据。这个方法比所有的你可以找到的 web 门户的都要好很多,有些你只能 JavaScript 来创建 widget,有些遵循指定的 API,有的要遵循一个严格的没有PostBack的模式

在 Dropthings widget 框架中,core 通过ASPNET membership Provider 完成身份,权限验证。这使得允许 widget 在加载的时候可以访问用户信息。Core 也提供了一个数据存储服务,用以 widget 保存自己的状态,用户动作,像展开缩小 widget,移动删除 widget。Core 和 widget 的交互通过一个 widget 容器。widget 容器像一个中间人一样宿主了真正的 widget。widget 容器知道哪个 widget 实例正在宿主,也提供了像存储服务或者通知。一个页面宿主一个或者多个 widget 容器,但是每一个 widget 容器只能宿主一个 widget(见图 2-9)。

图 2-9.一个页面包含了多个 widget 容器,每一个widget 容器只能包含一个 widget。

一个 widget 的代码是直接的,就像一个常规的 web 控件,你可以在 Page_Load 里面完成工作。你也可以获得 ASPNET 用户控件的事件。 Widget 像 SharePoint 的 web part ,有一点好处是你可以使用用户控件替换掉自定义控件。用户控件可以使得你可以使用 Visual Studio,而它不能处理自定义控件。你也可以将 widget 放进一个.ascx 文件中,这要求没有编译到 DLL 中,或者将 DLL 部署到服务器上面 -- 复制.ascx 文件,就可以使用了。

举个例子,你想要一个显示照片的 widget 假设是来自 Flickr。你可以将 widget 写成一个用户控件,按照通常处理事件的方式来处理事件。下面的代码,当控件在页面上加载时,显示照片:

protected void Page_Load(objectsender, EventArgs e)
{
if( !base.IsPostBack )
this.ShowPictures(0);
else
this
.ShowPictures(PageIndex);
}

给 widget 一个 LinkButton 控件,切换显示的图片,在其事件处理里面写导航代码,就像你做其他服务器端的导航一样:

protected void LinkButton1_Click(object sender, EventArgs e)
{
if (this.PageIndex > 0) this.PageIndex--;
this.ShowPictures(this.PageIndex);
}
protected void LinkButton2_Click(object sender, EventArgs e)
{
this.PageIndex++;
this.ShowPictures(this.PageIndex);
}

ASPNET 页面生命周期和常规的页面流程相同。在 widget 里面你可以使用任何 ASPNET 控件,并且利用它们的事件。容器提供了 widget 的容器以及框架,并且定义了一个头和一个正文区域。真正的 widget 在运行时,被容器加载到正文区域里面。Core 为每一个 widget 都创建一个 widget 容器,然后容器动态加载正真的 widget 到其正文区域。Widget 容器不是 widget 框架的一部分,而且你只需要写一次(见图 2-10)。然而,widget 开发者不需要写容器,因为只需要写真正的 widget。

图 2-10. widget 容器是一个 ASPNET web 控件,有一个头区域和一个正文区域,widget 加载在正文区域。

widget 容器是一个用户控件,当页面加载的时候,它为每一个 widget 实例而动态的被创建。widget 本身也是一个用户控件,容器用过使用 Page.LoadControl(“…”) 来动态加载它们。

宿主在容器中的真正的 widget 被加载到一个 UpdatePanel 控件里面。这样,不管 widget 执行了多少次的 PostBack ,widget 容器不会执行 PostBack。

设计 widget 容器

设计一个好的 widget容器问题在于正确整合 UpdatePanel 的方式。首先,决定 ASPNET 控件在 UpdatePanel 的最优分布,比较困难。把整个的 widget 容器都放进 UpdatePanel 工作起来不错,每一个widget 容器里面只有一个 UpdatePanel ,这样,开销会比较小。但是问题是,带有 Extender 的界面被附加到 UpdatePanel 里面的 HTML 项目了。当UpdatePanel 刷新的时候,它将会删除 ASPNET 控件生成的已有的 HTML,并且创建新的。这样就导致了所有附加在前面 HTML 的 Extender 都销毁了,除非 Extender 也在 UpdatePanel 里面。把 Extender 放进 UpdatePanel 意味着当 UpdatePanel 刷新,一个新的 Extender 实例就会创建和初始化。在一个 PostBack 之后,将会减慢 UI 更新,当处理 Widget 的时候更加明显。

你可以将头部区域和正文区域分别放进不同的 UpdatePanel - 一个UpdatePanel 宿主头部区域另外一个宿主真正的 widget。这使得你在修改 widget 上面的内容时,更新正文区域,而不用更新头区域,这样附加在头部区域的 Extender 就不会丢失(例如:一个拖放 Extender)。但是这意味着所有的 Extender 的控件都需要放进头部区域的 UpdatePanel , 这会影响性能。所以,尽管分割为头部区域和正文区域并不能带来一些性能上面的提升,没有你需要的那么好(如图 2-11)。

图 2-11. 一个 widget 容器有两个 UpdatePanel,一个在头区域,一个在正文区域,这里加载真正的 widget。

然而,为了更好的性能,头部的UpdatePanel 不包含整个头部信息,而仅仅是标题和头部按钮,会怎样呢?那么,当头部的 UpdatePanel 更新了(举个例子,当用户点击了按钮),整个头部没有被重建,只是在 UpdatePanel 里面的标题和按钮更新了。这样的方式,拖放 Extender 附加到头部区域 UpdatePanel 之外的部分(见图 2-12)。

图 2-12.widget 容器最终的设计,将一些东西拿出了头部的 UpdatePanel,来优化 widget 的性能。

widget 容器很好实现。有一个头部区域,包含了 展开/收起/关闭 按钮,还有一个正文区域,宿主了真正的 widget。在图 2-4 显示的 Dropthings 解决方案目录中,WidgetContainer.ascx 文件包含了这些标记(见示例 2-1)。

示例 2-1.  .ascx 文件内容

<asp:Panel ID="Widget" CssClass="widget" runat="server">
<
asp:Panel id="WidgetHeader" CssClass="widget_header" runat="server">
<
asp:UpdatePanel ID="WidgetHeaderUpdatePanel" runat="server" UpdateMode="Conditional">
<
ContentTemplate></ContentTemplate>
</
asp:UpdatePanel>
</
asp:Panel>
<
asp:UpdatePanel ID="WidgetBodyUpdatePanel" runat="server" UpdateMode="Conditional" >
<
ContentTemplate>
<
asp:Panel ID="WidgetBodyPanel" runat="Server">
</
asp:Panel>
</
ContentTemplate>
</
asp:UpdatePanel>
</
asp:Panel>
<
cdd:CustomFloatingBehaviorExtender ID="WidgetFloatingBehavior" DragHandleID="WidgetHeader"
TargetControlID="Widget" runat="server" />

整个的 widget 容器在一个 panel 控件里面。第一个子控件是头部的 panel,包含了 WidgetHeaderUpdatePanel 和头部区域的内容。在 widget 的标题部分,有用来便捷的按钮,还有展开,缩小 widget 的按钮。WidgetBodyUpdatePanel 在运行时宿主 widget。真正的 widget 通过调用 Page.LoadControl(….) 来加载,然后被添加到正文的 panel (译者: body panel)中。也包括 CustomFloatingBehavior ,它附加到 widget 的头部,使得整个的 widget 都可以拖放。

添加 widget

一个 widget 有设置部分和正文部分组成。正文部分,只要 widget 是没有被最小化,就会一直显示。设置部分只在当用户点击头部的 "edit” 链接的时候才会显示。设置部分存储了 widget 的定制的选项。举个例子,使用一个 Flickr 照片 widget,设置包含,允许用户选择显示什么类型的照片,输入标签,或者属于一个用户的 id。设置区域始终是隐藏的,直到用户需要他们,但是设置的时候你可以弄随便多的选项。设置部分可以使用一个常规的 ASPNET panel,里面包含了所有的设置项目。默认的,panel 不可见,但是当一个用户点击了 “edit”  链接,widget 就会显示它。

像前面提到的, widget 做为一个常规的 web 服务器控件被创建。为了整合 widget 功能,需要实现 IWidget 接口,它定义了 widget 怎样的和容器进行交互(见示例 2-2)。

示例 2-2. IWidget 接口

public interface IWidget
{
void Init(IWidgetHost host);
void ShowSettings();
void HideSettings();
void Minimized();
void Maximized();
void Closed();
}

IWidget 接口定义了一个方法,当它初始化 widget 区域和保存其状态的时候,通知 widget。当用户点击了“edit” 链接,ShowSetting 方法通知 widget 来显示其设置区域。当用户点击了最大化或者最小化链接(加号或者减号图标),maximize 或 minimize 被调用。当用户关闭了 widget ,Cloesd 被调用,并且清除所有存储在数据库中的数据。这些都是回发事件的回调方法,完成用户设定好的动作。

Widget 通过 IWidget.Init 方法获得一个 IWidgetHost 实例。这个接口暴露了和容器交互的方法,也是容器提供的服务。IWidgetHost 允许 widget 使用框架提供的方法,包括身份验证,通知和状态存储。举个例子:

public interface IWidgetHost
{
void SaveState(string state);
string GetState();
void Maximize();
void Minimize();
void Close();
bool IsFirstLoad { get; }
}

各个 IWidgetHost 提供的方法如下:


SaveState

以XML存储数据(或者其他的),但是因为数据需要序列化为字符串格式,XML是一个较好的选择。无论存储le 状态,都可以使用 GetState 在第二次加载的时候获得。

GetState

获得你使用 SaveState 保存了的状态。

Maximize

最大化 widget 并且显示 widget 正文。相当于用户点击“+”按钮 button, 只是 widget 执行它.

Minimize

最小化 widget 并且隐藏正文区域。相当于用户点击“-”按钮 button, 只是 widget 执行它.

Close

永久的从页面上删除 widget。

IsFirstLoad

确定是否是在页面上第一次加载或者一个异步 PostBack 发生在当前 widget 或者其他 widget 上。

IsFirstLoad 属性是狡猾的。想一下当一个用户点击了按钮,按钮经过一个 PostBack,将会发生什么?基本上,整个页面以及服务器端加载的所有的 widget 都要重新初始化。因为 widget 是用户控件,ASPNET 触发了所有的 widget 的 Page_Load 方法。现在,widget 需要知道是否是一个 PostBack 还是第一次加载,因为,内容是来自不同的来源的。举个例子,Flickr 相片 widget 直接在 Flickr 加载照片,但是在 PostBack 中,可以从 ViewState 或者其他的缓存中获得照片。IWidgetHost 的 IsFirstLoad 属性就是通知 widget 是第一次加载还是一个 PostBack 加载。

你也许想知道,为什么不使用ASPNET 自带的 Page.IsPostback ?它能够确定的告知是一个 PostBack 还是第一次访问页面。多标签的 Ajax web 门户重新定义了第一次加载,因为,并不是所有的标签都在第一次访问的时候加载;并且标签上面的 widget 仅在其所属的标签加载的时候才被激活。想象一个用户切换标签,加载一系列不同的 widget,但是所有的标签都在相同的 ASPNET 页面上。所以,当你在标签上点击,一个常规的 ASPNET 异步回发到 ASPNET 页面。现在,你正在加载新标签上面的 widget,而不是旧的标签上面。如果新标签上面的 widget 调用了 Page.IsPostBack ,他们将会得到 一个 true,因为点击标签,是一个常规的ASPNET PostBack。但是对于正在加载的 widget 来说,这是在新标签上面的第一次加载,对于他们不是一个 PostBack。它们在尝试从 ViewState 里面获取数据的时候会失败,因为没有它们的 ViewState 存在。这意味着,用户不能用常规的 ASPNET PostBack 概念定义 widget,这就是为什么 IWidgetHost 使用自己定义的 widget 的PostBack 而不同于常规的 ASPNET PostBack。

最大程度优化第一次访问的用户体验

web 门户最挑战的部分就是第一次访问的体验。在第一次访问中,一个新的用户获得一个实现设置好了的 widget ,方便以后使用(见图 2-13)。

图 2-13. 第一次访问 web 门户,需要设置用户账号,创建页面,以及能够以后进一步定制的,预先安排的 widget。

在第一次访问中,在用户使用之前,页面完成如下工作:

  • 使用ASPNET Membership 创建一个新的账户
  • 使用ASPNET Profile Provider 创建一个新的 Profile
  • 为用户创建一个新的页面
  • 创建页面上默认的 widget
  • 使用默认数据设置 widget,例如,根据IP,显示用户所在城市的天气
  • 输出 widget 以及相关的客户端脚本
  • 传度整个客户端框架以支持 web 门户功能。

这里的挑战是立刻执行服务器端的任务,这样,在服务器将页面内容之前不能有明显的延迟。一旦响应传递,浏览器需要下载 Ajax框架,widget 脚本,图片,CSS 等,这也会需要较长时间。为了给用户一个明显的快的速度,服务器需要立刻就将内容发送出来,并且是逐步的下载,其余的内容在用户看页面的内容的时候加载。

基本上,在第一次访问的时候,应用程序需要提供几乎 web 门户的每一个方面,因为你不知道用户拿他做什么。使用 Dropthings ,在第一次访问的时候,用户可以使用应用程序所有的功能。举个例子,用户可以拖放 widget ,添加新的标签页,安排页面上的内容,然后注册成为用户,继续使用设置好了的页面。用户在一个单独的页面上,完成这些任务,没有 PostBack,不能转到其他页面上。如果页面上允许 PostBack ,或者单独的页面被分割成多个页面,然后我们仅仅传送最基本的内容和客户端特性,像拖放。当用户触发一个 PostBack 或者转到其他页面完成其他的功能。因为 PostBack 或者到转到其他页面是不允许的,如果整个客户端框架在用户已经使用页面上的功能的时候还没有准备好,那么,将会出现 JavaScript 错误,导致动作失效。

同时,你需要在第一次访问时,保证提供所有的这些特性而不减慢第一次加载页面。否则,第一次访问的体验将会变慢,用户将会对站点失去兴趣。这是很大的挑战,你需要使得用户第一次访问尽可能的快,这样她能狗立即使用站点,而不用厌烦的看着浏览器的进度条。

下面有一些做法可以避免一个速度缓慢的第一次访问:

  • 平行的加载HTML和脚本,这样,用户看一些页面上显示了的内容,然后在后端下载脚本和图片。这在感觉上面增加了站点的速度。
  • 分布下载脚本,首先,下载核心的 Ajax 运行时,然后输出 UI。这样,用户看到了页面的变化,就不会感觉到不耐烦。
  • 在 widget 输出了之后,再下载附加特性的脚本。举个例子,Extender 可以在内容输出之后再下载。
  • 延迟那些不立即需要的脚本,在后面下载他们。通常,用户不会立刻使用拖放功能,这使得你可以延迟对话框,tip的脚本加载。
  • 整合多个小的脚本文件到一个大的脚本文件。你可以为每一个特定功能创建一个 JavaScript 文件。举个例子,每一个 ASPNET Extender 有一个或者多个 JavaScript 文件。尝试保持文件较小 ,并且在 web 应用程序中保持多个小文件。对于每一个文件,到达服务器并且将文件传回浏览器需要200到400毫秒。所以,每一个脚本文件都要耗费200到400毫秒,如果有五个脚本文件,那么应用程序话费一秒,在每一个网络传输。现在,将所有的下载文件时间加起来,五个大脚本文件,很容易的占用10秒。这样,你需要认真的思考(和测试)这样优化脚本文件的大小,以及尽可能的减少网络传输。理想中的,你应该试着仅仅只传输一个大的 JavaScript 文件,整合了所有的第一次访问需要的 JavaScript 小文件。

Rendering a Second-Visit Experience 展示第二次访问的体验

第二次访问是一块蛋糕。用户账户通过浏览器的 Cookie 可以用了,通过 ASPNET Membership Provider 获得。所有的 Ajax 脚本已经在浏览器的缓存中。所以,你仅仅需要加载已经存在的页面和页面上的 widget ,并且将其输出到浏览器(见图 2-14)。

图 2-14. 在第二次访问,用户账户,页面,widget 都已经创建,这样,应户加载起来会非常快。

在第二次访问的时候,web 门户做的:

  • 通过ASPNET Membership Provider 从加密的浏览器缓存中获得用户
  • 加载用户页面,并且为每一个页面创建标签
  • 找到当前的页面
  • 加载当前页面上面的所有 widget
  • 允许 widget 加载以前的状态
  • 输出 widget 和脚本
  • 传递客户端脚本框架(应该是已经缓存在浏览器了)

因为客户端脚本框架,widget 脚本,以及 Extender 脚本都已经缓存在浏览器中,第二次访问的持续时间基本上花费在服务器以及浏览器初始化脚本的时间。然而,在这期间,如果浏览器的缓存过期了,缓存的 JavaScript 就会丢失。如果这发生了,浏览器将会重新下载脚本,用户在下次访问的时候将会发现会有一些延迟。

改进 ASPNET AJAX 性能

这里有一些改进 ASPNET AJAX 性能的方法。

服务器端渲染 vs 客户端渲染

Ajax web 门户能够使用两种方法呈现:从服务器端或者从客户端。服务器端呈现意味着页面上的 HTML,和所有的 widget 在服务器端代码创建,由服务器端代码呈现,静态的传给浏览器。所以,浏览器仅仅是呈现 HTML加载 JavaScript(或者 Ajax 框架,Extender,widget 脚本等等),并且初始化它。iGoogle 是一个很好的服务器端呈现的例子。

相对的,客户端呈现意味着 widget 的内容不会随着页面的而被呈现。一旦脚本下载并被初始化,页面调用一个 Web Service 来获得 widget 的内容,并且一个一个的动态的创建页面上的 widget。

服务器端呈现的优势有:

  • 使用服务器端技术,充分发回其潜力。举个例子,你可以使用所有的 ASPNET 特性。
  • 一下子就能呈现传递整个页面到客户端,如果页面上没有很多的 widget ,感觉上速度比客户端呈现更快。
  • 显示内容,然后下载 Ajax 运行时,核心脚本,widget 脚本等等,然后随后初始化他们。因为用户在整个页面下载完之前看到 widget ,她将感到页面加载是迅速的。

服务器端加载的劣势有:

  • 页面输出缓存每一次访问都被传递,因为不知道使用用户已经改变了页面在其他的电脑商,或者是够页面的内容已经通过其他地方法改变了。
  • 所有的 widget 都需要使用服务器端技术,像ASPNET。你不能开发只有 JavaScript 的 widget。
  • 如果一个 widget 不遵循 ASPNET 形式的 PostBack 模型,它将需要大量的客户端框架的支持。举个例子,你将必须提供一些功能,像展开,缩起,关闭,调整大小,在客户端框架中,还要使用 web Service 来保证服务器端和客户端的同步。

客户端呈现的优势:

  • 不需要服务器端框架。
  • Widget 可以使用客户端提供所有的功能
  • 完全的客户端 web 门户不需要 PostBack,甚至是异步PostBack,这可以改善体验
  • 响应可以通过 web Service 调用缓存。这样,下次用户回来,缓存的响应从浏览器的缓存中加载,呈现的非常快。就像 default.aspx 每一次访问,你可以轻易的决定从缓存中加载内容或者是获得一个更新的页面。

客户端呈现的缺点:

  • widget 大部分需要使用 JavaScript 开发,这样比开发一个常规的 ASPNET  控件要复杂很多。
  • 大量的客户端脚本,使得浏览器变慢。
  • 浏览器调试支持,和服务器端调试相比要弱很多。

Runtime Size Analysis 运行时大小分析

ASPNET 有一个很大的运行时,有核心框架,UpdatePanel 的脚本,拖放功能的脚本。另外,我们需要 Ajax Control Toolkit。这些总共是惊人的 564KB,12个脚本实例。下载的大小取决与Extender 和 Ajax 特性的使用,通常的使用一个 Extender 的输出如图 2-15。

图 2-15. 模拟一个 256 KB 的带宽用户,第一次访问

为了捕捉下载信息,通过限制数据传输速度模拟一个慢速的 Internet。我使用了一个叫做 Charles  (www.xk72.com/charles) 的工具. 在过程中,256KB 的线路上花费了20秒下载。明显的,这个速度不能接受。下面解释了图 2-15 里面每一个文件的作用。所有的都以 /Dashboard/WebResource.axd 或者 /Dashboard/ScriptResource.axd 开头的JavaScript,下面的列表根据大小显示了功能的详细信息。

21.64 KB
     处理 PostBack 的脚本
83.38 KB
     Microsoft Ajax 核心运行时
30.16 KB
     UpdatePanel, 局部更新,异步 postback 脚本
136.38 KB
     演示版本的 Ajax,支持拖放脚本
36.02 KB
     真正的拖放功能脚本,在预览库里面
45.25 KB
     Ajax Control Toolkit
4.08 KB
     Timer 脚本
140.86 KB
     ACT animation 框架
18.05 KB
     ACT 动作基础实现,Ajax Control Toolkit 将会需要
16.48 KB
     ACT animation 动作
7.32 KB
    我自己定制的拖放动作
9.73 KB
    我自己定制的漂浮动作

整个运行时的负载太大了,你不能让用户在真正使用之前等待20秒来下载 Ajax 脚本,所以减少下载量:

  • 去除预览版本的 Ajax,使用 ACT 实现拖放功能
  • 使用 IIS6 来压缩传递的脚本
  • 整合多个文件到一个文件

ACT 用友自己的 DragDropManager ,拖放功能需要。你可以使用 Sys.Preview.UI.DragDropManager , 但是 DragDropManager 单独就会为整个预览库添加将近 180KB 的脚本。通过使用 ACT 的 DragDropManager 你可以去掉预览运行时并且较小7秒钟的延迟时间。除了预览脚本,需要下载的脚本如图 2-16.

图 2-16. 不加载 CTP 版本的 ASPNET AJAX,将会节省大概 180 KB.

当 IIS6 压缩开启,情况显著的改善了,如图 2-17。

图 2-17. IIS 压缩明显的减少了每个文件的下载量

整个下载量从448 KB下降到了 163 KB,下降了 64%!

脚本下载分为两部。首先,下载核心运行时,然后下载 ACT 和其他的脚本。运行时下载完显示内容。所以,显示内容的时间将会明显减小,因为在内容显示在屏幕上之前,只需要下载 50 KB,未压缩则需要 130KB。

ScriptManager 有一个 LoadScriptsBeforeUI 属性,你可以设置为 false,延迟加载脚本,在内容显示后显示脚本。这把脚本引用放到<body> 标签里面。这样,你可以先看到内容,然后是附加的脚本,Extender 和 ACT 脚本:

<asp:ScriptManagerID="ScriptManager1"runat="server"EnablePartialRendering="true"
LoadScriptsBeforeUI="false"ScriptMode="Release">

你可以显式的设置 ScriptMode=Release 获得高度优化的Ajax 运行时脚本, You can explicitly set ScriptMode=Release to 获得 emit highly optimized Ajax runtimescripts during local debugging to gauge their size on a production server.

 

Reducing Extenders and UpdatePanels to Improve Browser Response

因为 Extender 需要下载和初始化,Extender 越少,你的页面下载和初始化的速度越快。每个 Extender 使用类似下面的表达式来初始化:

        Sys.Application.add_init(function() {
$create(CustomDragDrop.CustomDragDropBehavior, { "DragItemClass":
"widget", "DragItemHandleClass": "widget_header", "DropCallbackFunction":
"WidgetDropped", "DropCueID": "DropCue1", "id": "CustomDragDropExtender1"
}, null, null,
$get("LeftPanel"));
});

 

这里 CustomDragDropBehavior 被初始化了。如果 Extender 在初始化中有很多操作,浏览器就会卡,尤其是 Internet Explorer 6.有时候,IE 6在初始化的时候会假死。为了避免这种情况,你要避免将太多的 Extender 绑定在 widget 上,以及一些富客户端的体验. 你可以按需加载 Extender,等等,把拖放功能初始化推后。通常,用户使用页面之前先会浏览页面,这样,你就可以轻易的推后拖放的初始化。使用一个延迟 Timer。另外的一种想法是程序动态创建 Extender,而不是将他们放在 ASPX 或者 ASCX 代码上,这样,就能推迟 Extender 的加载时间。所以替换掉这个:

 <cdd:CustomFloatingBehaviorExtender ID="WidgetFloatingBehavior"
DragHandleID="WidgetHeader" TargetControlID="Widget" runat="server" />

你可以强制其在整个页面加载完成后,浏览器可以空闲出来做一些重量的 JavaScript 操作:

var floatingBehavior = $create(CustomDragDrop.CustomFloatingBehavior,
{ "DragHandleID": handleId, "id": behaviorId, "name": behaviorId }, {}, {}, child);

比较 Debug 模式和 Release 模式

做性能测试时,确保你关闭了 debug 模式。如果它开启了,Ajax 将会生成调试版本的脚本,他们是巨大的,而且充满了调试描述。他们特别慢,你几乎不能在 IE 6上拖放 widget,甚至是一个双核的机器。一旦你切换到 release 模式,将会使用一个优化的版本的脚本,非常快。

添加身份和权限验证

由于要使用到 web Service 和异步 PostBack,一个 web 门户将会面对四个挑战:

  • 验证调用者的身份,保证所有异步 PostBack 和 web Service 的用户都是验证用户。
  • 调用的权限验证,保证调用者有权限来做异步 PostBack 和 WebService 调用。
  • 防止溢出。保证调用者不能持续不断的调用 Service ,防止一个溢出系统。
  • 防止使用机器人程序访问 default.aspx

web 门户相同对待于匿名用户和注册用户。然而,你可以对匿名用户限制一些功能,只有注册用户才能使用,主要的原因是注册用户的信息可以永久的保存,而匿名用户的信息的 Cookie 可能会丢失。几乎所有的WebService都可以被任何调用,除了一些用户信息相关的服务,像修改用户的 email 地址。

对于 web 门户的架构,挺起来安全方面会比较简单,实际上更加复杂。为了安全的目的,你要保证每一个 WebService 调用都发生在调用这的页面,或者 widget 上。举个例子,当从页面上删除一个 widget,你需要验证 widget 属于的页面,刚刚做了刚才的调用。如果不是,这明显是一个黑客破坏行为。相同的,当你删除一个页面,你需要保证它属于这个用户而不是其他人的。这是一个很大的性能上面的影响,因为这样的检查总是需要查询数据库。如果你有一个必须注册的站点,或者内网站点,你可以不用检查这些,因为你信任注册用户多余匿名用户。但是,从你支持匿名用户开始,你就不能留下任何没有检验的 WebService。举个例子,当检测一个 widget,你需要或者的这 widget 的页面,并且确保其属于调用者。一旦你验证了,这个调用合法,你可以更改 widget 的位置了。如果你不做这些,任何人运行一个程序,调用 Service ,输入任意的 widget ID 并且更改其他用户的页面设置。

溢出尝试是另外的一个问题。任何人都可以持续的调用 “创建新 widget” Service,通过巨大的 widget 来淹没数据库,这样使得数据库变大变慢,所以,你需要实现对一个 web Service 调用的一个配额,举个例子,每分钟最大100次调用,每个页面 100 个 widget,每一个 IP 每天注册10 个用户,等等。

第四个问题是关于重复的点击页面。想象一下,禁用 Cookie 情况下访问门户。每一次访问都是第一次访问,这会强迫程序注册一个新用户,创建页面,把 widget 放入页面,这将会导致搞数据库的高度频繁,增大数据库表。这样,你需要保证没人持续访问 default.aspx 来试图淹没你的系统。这样,你需要做:

  • 隔绝机器人和爬虫。
  • 当机器人访问站点的时候,呈现一个不同的结果。
  • 防止创建一个新的匿名用户。
  • 对于相同的IP,限制无Cookie 访问的数量。

最后的一点比较重要,举个例子,一个像 192.168.1.1 的IP 一小时内只能访问 default.aspx 50次。但是你不能将这个值设置的太低,因为很多人是通过代理服务器来访问,这些人就是相同的 IP。所以,挑战是选择一个合理的值,这样不会使得你的服务器挂掉,又能不限制用户使用代理服务器。防止DoS 攻击的方法将会在下小节仔细说明。

防止拒绝服务攻击

Web服务是最吸引黑客的目标,因为甚至一个刚上学前班的黑客都能都过重复的调用 消耗量大的web 服务是一个服务器挂掉。Ajax 门户是运行这样的dos攻击的最佳目标,因为,如果你在不保存Cookie的情况下,重复的访问首页,每一个的访问都会创建一个新的用户,一个新的页面设置,新的widgets,或者诸如此类的. 第一次访问,是开销最大的一次。尽管如此,他是最容发现的一个使得站点挂掉。你能自己试一下,就是仅仅这样简单的代码:

       for (int i = 0; i < 100000; i++)
{
WebClient client = new WebClient();
client.DownloadString("http://www.dropthings.com/default.aspx");
}

让你非常吃惊的,你将会注意到,在一系列的调用之后,你不能获得一个有效地响应。这并不是你成功的是这个站点挂掉了,而是你的请求被拒绝了。你高兴与你不能获得任何的服务了,这样,你活着了拒绝服务(对于你自己)。我们非常高兴的拒绝了你的服务。Deny You of Service (DYOS).

这里我是用的方法是使用一种开销并不大的方式来记住多少个请求来自同一个IP。当这个数字超过了入口,拒绝后面的服务一段时间。这个方式要记住调用这个IP在ASPNET的Cache,并且,将一定每一个IP的请求数量存入其中。当这个数字超过预定的界限,拒绝后面的服务一段时间,像10分钟。在 10分钟后,重新允许这个IP的请求。

我有一个类叫做 ActionValidator ,它包含了第一次访问,重复访问,匿名PostBack。添加新的模块,添加新的页面等等的数量。他会检查,是否这个动作的数量超过了没有。

public static class ActionValidator
{
private const int DURATION = 10; // 10 min period
public enum ActionTypeEnum
{
FirstVisit = 100, // The most expensive one, choose the value wisely
ReVisit = 1000, // Welcome to revisit as many times as the user likes
Postback = 5000, // Not much of a problem
AddNewWidget = 100,
AddNewPage = 100,
}


一个静态方法叫做IsValid 来进行检测。如果没有超过请求的限制,它将会返回true,否则就返回false。一旦,你获得一个false,那么,你就可以调用 Request.End()(译者:应该是Response.End()吧。。)使得ASPNET不用进行下一步的处理。或者,你也将其转到一个页面,并且显示“祝贺,你的拒绝服务攻击成功了!”

   public static bool IsValid(ActionTypeEnum actionType)
{
HttpContext context = HttpContext.Current;
if (context.Request.Browser.Crawler) return false;
string key = actionType.ToString() + context.Request.UserHostAddress;
var hit = (HitInfo)(context.Cache[key] ?? new HitInfo());
if (hit.Hits > (int)actionType) return false;
else hit.Hits++;
if (hit.Hits == 1)
context.Cache.Add(key, hit, null, DateTime.Now.AddMinutes(DURATION),
System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.
CacheItemPriority.Normal, null);
return true;
}

缓存的key由动作类型已经客户端ip共同构成。首先,它检查当前动作,当前ip,是否有任何的记录,如果没有记录,开始计数并且在这个时间段内的记录到缓存中。缓存对象的绝对日期保证了在这个时间段后,数据将会被清除,并别重新开始记录。如果当在缓存中已经有相应的数据的时候,检测最后的点击次数,判断是否超过限制。如果没有超过限制,增加这个计数器,不用再一次的将更改过的数据存储在缓存中通过:Cache[url]=hit;因为hit对象是一个引用,修改它,就意味着也在缓存中修改了,事实上,如果你将其重新的放进缓存中,缓存过期时间将会重新计算,这样,你的计算逻辑就失败了。

这个方法的使用非常简单,在default.aspx:

    protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// Check if revisit is valid or not
if (!base.IsPostBack)
{
// Block cookie less visit attempts
if (Profile.IsFirstVisit)
{
if (!ActionValidator.IsValid(ActionValidator.ActionTypeEnum.FirstVisit))
Response.End();
}
else
{
if (!ActionValidator.IsValid(ActionValidator.ActionTypeEnum.ReVisit))
Response.End();
}
}

else
{
// Limit number of postbacks
if (!ActionValidator.IsValid(ActionValidator.ActionTypeEnum.Postback)) Response.
End();
}
}

当然,你可以使用一些思科的防火墙,来防止DOS攻击。你将会从你的服务器提供商那里获得保证,他们的整个网络都是DOS或者DDOS(分布式 DOS)攻击免疫的。他们保证的是网络级的攻击,像TCP SYN 攻击,或者malformed packet floods 等等。没有办法调查的包,找到不带Cookie的加载站点过多次,或者添加太多的widgets的特定的ip。这叫做应用程序级的攻击,没有硬件的保护。这就必须在你的代码中实现。

很少的站点使用了应用程序级的方法来预防DOS攻击。这样,通过写一个简单的循环,连续不断的点击开销大的页面或者是web service,很轻松的使服务器挂掉。我希望这个简单而有效的类能够帮助你来防止DOS攻击。

总结

这章里,你了解到了创建 Dropthings 一样的门户的架构的基础,这样的架构封装和大部分客户端的功能,使得开发者创建 widget 变的容易。特别的,你将会看到怎样使用一个 widget 框架来推进其开发和部署。

创建一个丰富的客户端体验的 web 门户是一个非常大的挑战;最大的条找就是第一次访问,巨大的脚本需要下载。一个门户也是易受伤害的,也要保证安全问题。现在你知道了架构的挑战,下一章,我们来使用 ASPNET Ajax 创建 web 层。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值