SOHA - 面向服务的 HTML 应用程序(概念和原则)






4.81/5 (12投票s)
提出了一种 Web 应用程序编程模型,该模型提倡 Web 标准,通过消除经典的 Web 服务器页面来实现高可伸缩性,并且无需浏览器插件即可实现丰富的交互性。
引言
大约六个月前,我的经理问我:“我们要开始一个新的面向消费者的 Web 应用程序,应该使用什么技术?”
我一直是创建业务线 Web 应用程序的富 Internet 应用程序(RIA)的忠实拥护者,但由于 史蒂夫·乔布斯的一些观点 以及我们的 Web 应用程序需要被日益流行的消费类手持 Web 设备访问,我不得不搁置使用 Flash/Flex/Silverlight 构建整个站点的选项。注意力转向那些经典的 Web 服务器页面技术,如 ASP.NET、ASP.NET MVC、JSP、PHP、Ruby on Rail、ColdFusion……。由于我们已经拥有 RESTful 服务,可以在中间层利用这些服务来实现业务逻辑和数据访问,并且这些 Web 服务器框架中的每一个都倾向于使用其自己的脚本语言将表示层与数据访问层耦合起来,经过评估,结论是这些服务器页面技术对于构建松散耦合、支持丰富交互、面向服务、广泛可访问的 Web 应用程序并没有太大帮助。
此外,现代浏览器拥有更令人兴奋的可编程功能,并且自 IE8 以来,Internet Explorer 变得更加符合 Web 标准,JavaScript 库(jQuery 和 jQuery UI,Prototype 和 Prototype UI,MooTools,ExtJs,Yahoo! UI Library (YUI),Dojo Toolkit,DHTMLX,等等流行库)更加紧凑、灵活且功能强大,可以动态地构建和操作 DOM,CSS 和 CSS3 在构建视觉上引人注目的布局和视觉效果方面能力更强,HTML 5 即将到来,越来越多的面向服务的架构被应用于应用程序,多核客户端机器使得客户端计算能力大大增强,等等。所有这些因素都有助于我确认,可以在没有任何服务器页面框架的情况下构建 Web 应用程序。
这让我想,何不构建一个“RIA”,既没有服务器页面,又没有对浏览器插件的依赖?通过让 Web 服务器仅提供静态
HTML/JavaScript/CSS/图像文件,难道不能实现高可伸缩性吗?它还可以实现移动设备的基本访问,更好地利用现有的、经过测试的 RESTful 服务作为后端。这个想法开始成为一个非常有趣的话题,促使我进行更深入的研究。
我绝对确信可以通过 Ajax 在运行时操作 DOM,动态地在客户端创建页面内容应该不是问题。然而,使用静态
文件 HTML/CSS/JavaScript,并且在没有服务器页面帮助的情况下,如何处理用户会话?如何安全地导航?如何处理用户登出后的书签 URL?如何在页面之间传输用户数据?如何检测最终用户浏览器中的 Cookie/JavaScript 设置?此外,更高级的话题,如何跨多个静态
HTML 文件共享公共 HTML 内容(如页眉和页脚)?(客户端主页?)如何通过在用户登录后应用不同的体验来处理用户偏好?
在回答了所有这些问题并拥有一个工作原型后,我给我的经理回复了我的建议:我们可以使用 Web 标准技术——HTML/CSS/Ajax 加 Apache——来构建 Web 应用程序,从而构建一个没有 Web 服务器页面在服务器端生成任何 HTML 的 RIA,并且它不需要 Flash Player/Silverlight/JavaFX 等浏览器插件,我们的现有 RESTful 服务可以作为后端几乎零更改地重用,并且客户端和服务器保持松散耦合。
这就是 SOHA - 面向服务的 HTML 应用程序的起源,让我们进一步探讨。
什么是 SOHA?
SOHA,即面向服务的 HTML 应用程序,是一种 Web 应用程序编程模型,它将 HTML 用作模型模板,CSS 用作视图,JavaScript 用作控制器,并利用 Ajax 与 Web 服务进行交互。它提倡 Web 标准,通过消除服务器端对经典 Web 服务器页面(JSP、ASP、PHP、Ruby On Rail、ColdFusion 等)的需求来实现高可伸缩性,并且无需浏览器插件(Flash/Flex、Silverlight、JavaFX 等)即可实现丰富的交互性。
面向服务的 HTML 应用程序是 Ajax 的扩展,它有三大原则:
- 应用程序服务器托管一个与表示无关的服务和数据访问层,该层通过 HTTP 提供数据,专注于数据访问和业务逻辑。其 API 通常设计为 RESTful 风格,以 JSON 或 XML 格式响应客户端请求,并且可以同时服务不同的客户端应用程序(Web、桌面、移动);
- Web 服务器仅处理 HTTP,没有服务器页面框架进一步处理 HTTP 请求来生成 HTML,Web 前端由一组静态文件组成,包括 HTML、CSS、JavaScript 和其他数字资产,Web 服务器是一个纯粹的 HTTP 服务器,根据请求提供这些静态文件,换句话说,HTML 是被提供的,而不是在服务器端生成的;
- 客户端采用 Web 标准技术(HTML、CSS、JavaScript)来创建内容布局、视觉效果和丰富的交互式用户界面,只需要客户端浏览器启用 JavaScript,不需要任何浏览器插件,包括 Flash Player、Silverlight 运行时或 JavaFx。
SOHA 将 Ajax 的范围从构建页面区域和异步与服务交互扩展到更高的应用程序范围,包括:管理应用程序导航,通过在客户端重用共享部分动态构建页面,通过有条件地应用 CSS 和 JavaScript 来改变整个应用程序的外观和行为,管理安全页面的用户授权,跟踪已认证用户的页面间数据传输,等等。
借助 SOHA,Web 服务和数据访问层可以使用 WCF 和 IIS 配合 .NET 4.0 来创建 RESTful 风格的服务,服务请求和响应通常设计为 JSON 格式,主要是因为 JavaScript 将是其主要消费者。服务层的主要考虑因素是需要以标准的方式公开服务 API,只要服务 API 是 RESTful 风格的,并且其负载为 JSON 格式,就可以被不同平台上的不同应用程序使用,我们就认为这是一个 SOHA 友好的 Web 服务。至于实现,服务可以由 WCF 实现,也可以由 ASP.NET MVC 配合 RESTful 端点 来实现,或者由其他能够有效构建 RESTful 服务的技术来实现。
在工作中,我使用 Ruby on Rail 创建了服务层(服务项目中没有 HTML),后来由其他团队成员将其转换为 GlassFish 上的 Java EE 服务。使用什么并不重要,重要的是服务如何被公开。通常,应用程序架构依赖于标准而不是特定供应商的框架是一个好主意;这将为架构层面的灵活性和可预测性带来高度信心。设计 RESTful 风格的 Web 服务 API 并以 JSON 格式作为其负载是 SOHA 中一个重要的考虑因素,以实现服务层上的可重用性和表示无关性。
在服务层后面,通常有数据库用于持久化。同样,SOHA 对使用哪种数据库引擎不感兴趣,它只强调数据访问需要通过 REST 服务暴露。实际上,当为技术栈选择服务框架时,可以根据数据库与服务框架的配合效率以及团队中选定数据库的专业知识来确定数据库引擎。在我工作的项目中,最初是 SQLite 配合 Ruby on Rail,然后随着数据容量的增长迁移到 MySQL,最终在团队中有了专门的服务和数据库人员时切换到 Oracle。在数据库更新、迁移和切换期间,这些更改在很大程度上被封装在服务层中,服务 API 保持相对稳定,因此客户端 Ajax 代码不需要任何更改。
服务和数据访问层通常托管在应用程序服务器上,数据库服务器物理上是分离的,位于防火墙后面,只能由服务代码访问。由于大多数服务框架都有自己的托管环境(如 IIS+.NET、TomCat 或 GlassFish、JBoss 等),因此它们通常与 Web 服务器分离。为了强制执行同源策略,Web 服务器需要配置为应用程序服务器的代理。
在我们的 Web 应用程序中,我们使用 Apache 作为 Web 服务器,它可以很容易地根据 URL 模式匹配配置为 GlassFish 应用程序服务器的代理,这将确保具有相同主机、协议和端口的服务 URL 通过同源测试。
在 SOHA 中,Web 服务器位于服务层的前面。它不生成 HTML,也不运行任何应用程序逻辑。它是一个纯粹的 HTTP 服务器,只响应 HTML、CSS、JavaScript 和其他数字资产(如图像或声音文件)的请求静态文件。经典的服务器页面技术在 SOHA 中没有作用,让开发人员无需担心前端的可伸缩性,Web 服务器默认实现最大可伸缩性,只有服务层需要担心如何扩展。
在 SOHA 客户端,它鼓励使用标准的 Web 技术来构建用户界面。为了更好地利用 HTML 作为将必要的应用程序位下载到浏览器的主要载体,我们特意为我们站点的每个 HTML 文件选择了XHTML 1.0 transitional 作为 DocType。这个选择基本上将 HTML 变成了一个 XML 应用程序;我们进一步按照 XHTML 模板的原则设计了每个 XHTML 文档。
XHTML 模板文件不像 Web 服务器页面那样包含具体或用户特定的内容,它只有一个 XHTML 标记的“骨架”或“占位符”,用于显示运行时用户数据。与 Flex/Silverlight 的 RIA 相比,我们在 SOHA 中失去了数据绑定的功能,但通过有意使用 XHTML 作为模板,我们可以围绕这些模板构建自己的渲染逻辑以提高生产力。这个原则和设计也要求从一开始就“干净的 HTML”,没有内联样式和内联脚本,只有代表页面模板的结构清晰的 XHTML。
由于 SOHA XHTML 只代表结构化数据和模板,所有视觉方面,包括布局(大小、浮动、边距、内边距……)、视觉效果(背景颜色和图像、字体、边框、分隔线……),以及一定程度的交互性(鼠标悬停显示/隐藏、伪类)都由 CSS 处理。在 MVC 架构中,布局和视觉效果被认为是视图,在 SOHA 中,我们可以说视图由 CSS 控制。
当然,JavaScript 可以动态地创建和应用/更新 CSS,但我们特意限制了 JavaScript 用于创建 CSS 规则。所有 CSS 规则都创建在外部链接的 CSS 文件中;JavaScript 可以有条件地更新 DOM 元素上的 CSS,但绝不能创建规则。此设计也遵循关注点分离的规则,CSS 作为视图,JavaScript 作为控制器。
在 SOHA 中,JavaScript 作为控制器,它不仅控制/更新视图,还控制/更新数据模型,还控制整个应用程序的视觉主题和共享 HTML 区域的可重用性。这是通过根据用户数据或 UI 逻辑动态加载 HTML/CSS/JavaScript 来实现的,jQuery 在此方面提供了出色的支持。
Ajax、JavaScript 操作 DOM 的强大功能,加上新的 HTML 5 功能,这些技术可以在网页中创建丰富的交互性,同时尽可能广泛地保持可访问性和可访问性,任何支持 Web 的设备都可以毫无限制地访问该网站。这是 SOHA 提倡客户端 Web 标准的最重要原因,无需依赖浏览器插件即可渲染网页。特别是对于业务线 Web 应用程序,UI 包含大量文本和数据内容,SOHA 可以实现富 Internet 应用程序,而无需浏览器插件。就像 Web 服务器上没有服务器页面一样,客户端不需要浏览器插件是 SOHA 的另一个显著特点。
有一点值得深入讨论的是,SOHA 并不适合所有 Web 应用程序。例如,如果您的网站的主要内容是流式视频或音频,那么网站架构需要以内容为导向并依赖插件,SOHA 在这种情况下无济于事。与 HTML 5 尚未成熟 的原因相同,如果 Web 应用程序的主要功能是让用户创建图形密集型内容,例如编辑图像,那么 SOHA 也不适合。在大多数 LOB Web 应用程序中,网页内容主要关于信息(而非多媒体内容)、文本和大量数据,SOHA 在关注点分离、减少依赖、丰富的交互性以及广泛的可访问性(可供各种平台和设备访问)方面非常有意义,我们的面向消费者的 Web 应用程序就属于此类 Web 应用程序。
好了,关于高层讨论就到这里,让我们来看看在实际应用程序中应用 SOHA 时遇到的一些挑战和解决方案。
基础和检测
为了实现面向服务的 HTML 应用程序,客户端有一些常见的任务尤为重要,因为没有服务器页面的帮助,这些任务包括:
- 确保用户浏览器启用了 JavaScript,否则应用程序无法运行。因为 JavaScript 是控制器,我们需要提示用户在其浏览器中启用 JavaScript。
- 检测浏览器中的 Cookie 设置,如果未启用,请提示用户启用。因为 SOHA 的导航机制依赖于它(稍后将详细介绍)。
- 检测浏览器是否不支持本地存储(如 Safari 3 和 Opera 10.1),因为 SOHA 的页面间数据传输(DOM 在导航到新页面时会重建)依赖于它。
- 动态加载 HTML 片段的能力,以便从客户端共享 HTML 区域(如页眉和页脚)。
- 能够动态加载 CSS 和 JavaScript 文件,以动态更改外观和行为。
以上所有项目都通过一个 jQuery 插件解决,详细信息在我的上一篇文章《使用 jQuery 进行 Web 应用通用任务》中,它有更多关于实现细节的讨论,所有源代码和演示 HTML 页面都已提供,您可以更仔细地了解它是如何与 jQuery 一起工作的。从 SOHA 的角度来看,有一些值得重申的注意事项。
客户端主页和用户控件
“客户端主页”可以通过客户端动态加载 HTML 来实现。这项技术将 Ajax 的范围扩展到跨页面使用,它实现了站点中一致的网页结构,如页眉、页脚和导航菜单。传统上,服务器页面以主页、用户控件等形式提供可重用性。通过将 Ajax 扩展到这一点,无需服务器页面即可实现相同的结构。我的上一篇文章中的演示 HTML 页面有一个可用的示例和用于动态加载页眉和页脚的所有源代码,站点的导航项是页眉的一部分,也可以由多个页面共享。
loadDivHTML: function(divID, htmlURL, onSuccess)
{
$('#' + divID).load(htmlURL, onSuccess);
}
下面是演示页面的脚本,它使用 loadDivHTML
来动态加载页眉和页脚。
$(function ()
{
var paramHF = qs['headerfooter'];
if (paramHF == 'both')
{
$.cbexp.loadDivHTML('pageHeader', 'html/_header.html', function() {
$.cbexp.loadDivHTML('pageFooter', 'html/_footer.html');
});
}
});
使用 CSS 进行页面布局
主页的另一个方面是关于页面布局,例如页眉在顶部,页脚在底部,具有一定的宽度和高度,页面内容始终居中于浏览器窗口。在页面内容中,它可以是两栏布局。一栏可能在左侧有侧边栏,右侧有详细内容,另一种反向布局可以是右侧有侧边栏,详细内容位于左侧。这种常见的布局可以通过外部 CSS 文件定义,无需更改 HTML,通过动态加载不同的布局 CSS 文件,可以实时更改页面布局。
同样,在演示 HTML 页面中可以找到一个带有源代码的工作示例。它实现了动态布局更改,而无需更改 HTML,只应用不同的 CSS 文件。
以下 layout.base.css 由原型站点中的所有页面共享。
#pageContainer
{
margin: 0px auto 0px auto;
width: 960px;
}
#pageHeader
{
margin:0px auto 0px auto;
width:960px;
height:100px;
text-align:left;
}
#contentContainer
{
margin: 0px;
padding-top: 20px;
padding-bottom: 20px;
min-height: 500px;
}
#pageFooter
{
margin: 0px auto;
padding-bottom: 20px;
width: 960px;
position: relative;
}
除了 layout.base.css,以下是 CSS 的两栏布局。
@import url('cbexp.layout.base.css');
#main_grid-nav {
margin-left: 10px;
margin-right: 10px;
width:220px;
float: left;
position: relative;
}
#main_grid-content {
margin-left: 10px;
margin-right: 10px;
width:700px;
float:right;
position: relative;
}
动态加载以下反向两栏布局 CSS 文件时,它可以在不更改页面 HTML 的情况下更改页面布局。
@import url('cbexp.layout.base.css');
#main_grid-nav {
margin-left: 30px;
margin-right: 30px;
width: 200px;
float: right;
position: relative;
}
#main_grid-content {
margin-left: 30px;
margin-right: 0px;
width: 670px;
float: left;
position: relative;
}
SOHA 中的站点主题
如上所述,CSS 是 SOHA 中的视图,CSS 不仅关乎布局,还关乎视觉效果。在同一个演示页面上,它显示页面的主题(颜色、字体、图像、布局等)可以动态更改,而无需更改 HTML(这是 HTML 不应包含内联样式的另一个原因)。它通过创建不同的 CSS 文件(visual.base.css、layout.base.css)将布局与视觉效果分离来实现,更多信息请参阅文章。
当站点中的多个页面以一致的方式控制布局和视觉主题,并且动态 CSS/HTML 数据与用户帐户相关联时,“用户偏好”逻辑就可以基于服务数据在客户端实现。
以下 demo.blue.css 是蓝色主题的纯视觉样式表规则。
body
{
background: #3275ac url(../img/bk_texture_blue.png) repeat-x;
}
#pageHeader
{
background: transparent url(../img/bk_header_blue.png) no-repeat 0 top;
}
#navigation ul li a:hover {
background-color: #3275ac;
}
#contentContainer
{
background-color: #f4f4f4;
}
一个纯视觉样式表(无布局)的绿色主题可能看起来像这样。
body
{
background: #6f8881 url(../img/bk_texture_green.png) repeat-x;
}
#pageHeader
{
background: transparent url(../img/bk_header_green.png) no-repeat 0 top;
}
#navigation ul li a:hover {
background-color: #6f8881;
}
#contentContainer
{
background-color: #f4f4f4;
}
以下代码演示了如何按需加载布局样式表和主题(视觉)样式表。
//plugin code to support dynamically loading CSS
loadPageCSS: function (cssUrl)
{
$('<link />').appendTo('head').attr({
rel: "stylesheet",
type: "text/css",
href: cssUrl
});
}
//page script code to change page layout and theme on demand:
$(function ()
{
var qs = $.cbexp.getQueryString();
var paramCT = qs['theme'];
if (paramCT == "green")
loadGreenTheme();
else if (paramCT == "blue")
loadBlueTheme();
});
function loadGreenTheme()
{
$.cbexp.loadPageCSS('css/cbexp.demo.green.css');
$.cbexp.loadPageCSS('css/layout/cbexp.layout.2column.css');
}
function loadBlueTheme()
{
$.cbexp.loadPageCSS('css/cbexp.demo.blue.css');
$.cbexp.loadPageCSS('css/layout/cbexp.layout.2column_r.css');
}
动态加载 JavaScript
另一个实际用例是动态加载 HTML/CSS/JS 以提供上下文敏感内容,例如为不同用户提供不同的促销信息。从技术上讲,每个不同的促销活动仅仅是 HTML/CSS/JS 文件的不同组合,通过利用上述技术,我们可以在页面上条件性地提供它们。结合适当的跟踪机制,这将成为进一步业务智能分析的轻量级而强大的数据源。
《使用 jQuery 进行 Web 应用通用任务》的源代码中有一个名为 loadPageScript
的方法,它利用 jQuery 的支持按需加载脚本文件。动态加载脚本的一个用例是更改常用“接口”(不同脚本文件实现的方法,条件性加载)的行为;另一个是用例是语言特定的“字典”文件,位于语言名称文件夹下,每个脚本文件都有相同的字典对象名称和相同的条目键,但定义不同。主页的控制器 JavaScript 代码可以根据用户所在的区域设置加载正确的字典文件。
所有这些功能在 Web 应用程序中都不是新的,新的是它们是在客户端实现的,没有任何 Web 服务器页面的帮助。从源代码可以看出,使用 jQuery 构建时,实际代码非常简洁易学。
在我们的原型 Web 应用程序中,有一个 config.js 文件定义了 APP_LOCALE
,例如:var APP_LOCALE = "en-us"
,“en-us”是 JavaScripts 目录下的语言文件夹名称。在该语言文件夹中,包含特定于语言的格式化程序(如货币和日期格式化程序)和字典(如错误代码到错误消息字典)。这些特定于语言的脚本可以根据 APP_LOCALE
设置动态加载。
//plugin code to support loading script file dynamically
loadPageScript: function (jsUrl, onSuccess)
{
$.getScript(jsUrl, onSuccess);
}
//page script file that loads language specific formatter and
//dictionary script file based on APP_LOCALE setting:
$(function ()
{
$.cbexp.loadPageScript("javascripts/" + APP_LOCALE +
"/cbexp.formatter.js", function ()
{
$.cbexp.loadPageScript("javascripts/" + APP_LOCALE +
"/cbexp.error.dictionary.js");
});
});
敬请期待
这是面向服务的 HTML 应用程序的讨论的第一部分。请注意,客户端处理多个页面之间的 HTML 内容共享(客户端主页和用户控件)、CSS 中布局与视觉效果的分离、用户偏好和主题、动态构建页面内容以及更改外观和行为等,都是在没有服务器页面且客户端无需插件的情况下完成的。在没有服务器帮助的情况下,管理会话、用户身份验证和授权、处理书签 URL、在 DOM 重建时共享数据以及有效与 RESTful 服务交互等方面将面临一些挑战。在 SOHA 的第二部分,我们将讨论解决我们 SOHA Web 应用程序中这些挑战的技术细节。敬请期待。
[更新于 2010/10/18:进一步讨论已发布:SOHA - 面向服务的 HTML 应用程序(会话与安全)]