《编程机制探析》第二十六章 页面生成技术

《编程机制探析》第二十六章 页面生成技术

Web应用程序之所以如此流行,有两个主要原因。第一个原因是界面的一致性,即浏览器内显示的HTML;第二个原因是能够支持巨大的用户访问量。
Web应用程序之所以能够支持巨大的用户访问量,主要是因为HTTP协议的无状态特性。随着技术的发展和应用的成熟,Web应用程序对用户状态的要求越来越高。HTTP协议的无状态特性就成为了一个难以绕过的阻碍。这真是,成也萧何,败也萧何。
为了克服HTTP协议的无状态特性,HTTP协议本身引入了Session的概念,但是,Session只是一个基础设施,远远不够,Web应用还得自己做大量的工作来保持用户状态。
Web应用保持用户状态的地方有两个——服务端和浏览器客户端。
如果是保存在服务端,Web应用需要为每一个用户(浏览器进程)开辟一块专用空间,或者在服务器Session空间中,或者是在app server进程的共享内存中,或者存放在另一个进程的共享内存中(如数据库或者网络中心缓存)。在这些空间中,服务器需要存储用户当前状态和当前步骤。
用户每一次请求过来,服务器就按照上次存储的状态和步骤继续运行。请求完毕之后,服务器再把当前状态和步骤存储起来,等待用户的下一步请求。
这种工作模式很像是Continuation(连续),一种时停时续的工作方式,整个工作过程看起来就像是“运行-暂停-继续运行-暂停-继续运行……”的样式。
我们可以用线程同步等待的流程来理解Continuation。当一个线程遇到同步锁需要等待的时候,线程调度程序就会把这个线程挂到该同步锁的等待队列中,那个线程同时保持着当前的运行状态,以便获取同步锁之后继续运行。
事实上,服务端状态保持的实现方案之一就是Continuation。这种方案的好处在于,编程模型自然,工作流程清晰,程序员不需要单独为用户的每一步请求写一个服务应答程序,而是可以把用户的所有操作步骤(多次请求访问)都写在同一个过程中,仿佛这些步骤就是一个完整流程的几个普通过程调用而已。
Continuation方案的坏处也很明显,那就是保留了太多的状态,增加了服务器的负担,降低了服务器的并发性能和吞吐量。要知道,状态是并发的天然敌人。
除了Continuation之外,还有一些服务端状态技术,其名字一般都和“flow”(流程)沾边,如Page Flow,Web Flow,等等。
同Continuation一样,这些“Flow”的实现也颇为繁琐,既需要为每一个页面定义一个步骤ID,也需要在服务器端存储每一个用户的当前状态和步骤。
无论是Continuation,还是Flow,它们的共有问题就是服务端状态太重,影响服务器的并发性能和吞吐量。那么,状态不存放在服务端,又能存放在哪里呢?一些开发人员把目光转向了浏览器客户端。
为什么需要把用户的操作步骤分成好几个页面?放在同一个页面中完成不成吗?当然成,用Javascript就可以。随着技术的发展,Javascript的功能越来越强大。浏览器中的Javascript可以直接向服务器发出HTTP请求,获取动态数据,并更新当前的HTML页面。在这种方案中,当前页面一直没有换,网址也没有换,所有的状态也都没有丢,都存放在浏览器的当前页面中。服务器不需要再保留大量的状态,轻装上阵,无状态就无负担,精神好,牙口就好,吃嘛嘛香,胃口倍儿棒,吞吐量大,并发性好。
当然,浏览器端状态也不是没有问题的。那就是刷新。如果用户不小心按了刷新按钮,浏览器就会重新发出请求,获得崭新的页面,当前页面中的状态就会烟消云散了。不过,这不是什么大问题,而且也不难解决,比如,Javascript可以定时把用户输入的重要信息暂存到服务器端。
从目前的情况看,浏览器端状态已经逐步压倒了服务器端状态,成为了当前的主流方案,同时也给Web应用开发模式带来了巨大的改变,省去了服务端的大量工作。服务器端不需要再保持大量的用户状态,同时,也不再需要生成大量的动态页面。这是怎么说呢?因为,在客户端状态方案中,一般只有一个页面。而且,这个页面的内容更新都是由Javascript完成的,不需要服务端操太多的心。就这样,服务端瘦了下来,客户端胖了起来,富了起来。
在这种情况下,再来讨论服务端页面生成技术,似乎没有太大必要了。但我还是想把自己在页面生成技术方面的一些心得和大家分享一下。因为,页面生成技术本质上就是字符串拼装技术。而字符串拼装技术是应用很广的技术,不仅用于HTML页面生成中,还可以用于各种文本生成中,比如,代码生成,SQL生成,页面缓存生成,等等。这些生成技术的原理是相通的,一通百通。
下面我们就来看本章的主题——页面生成技术。
首先,让我们回到MVC架构之前的混沌年代。那时候,一段典型的Web应答代码是这样的:
process( request, response) {
// request 是 HTTP Request 对象,response 是 HTTP Response 对象

html = makeHTML(request) // 根据request生成HTML

response.write(html) // 将生成的HTML写入到response中
}
上述的代码只是一种理想模式,真正的代码要散乱得多。response对象是HTTP Server提供的对象,其内部对应着一个HTTP协议层的网络数据缓冲池。HTTP Server内部的网络协议处理程序需负责将HTTP数据缓冲池的数据分块打包,传给下层的TCP/IP层。从理论上来说,HTTP数据缓冲池中的数据,越早准备好就越好,这样就给了网络协议处理程序更多的时间来分块打包。因此,一般的Web应答代码看起来是这样子的。
process( request, response) {
// request 是 HTTP Request 对象,response 是 HTTP Response 对象

parameters = request.getParameters()
// 从request中获取HTTP Request中的URL地址或者消息体中的参数

data = fetchData(parameters) // 根据参数获取动态数据

response.write( “一些固定的静态HTML片段 ”)

html1 = makeHTML1( data) // 根据动态数据生成一段动态的HTML片段
response.write(html1) // 将生成的动态HTML片段写入到 response中

// 上述过程不断重复。根据动态数据,生成各种动态HTML片段。
// 按照正确的顺序,将静态HTML片段和动态HTML片段依次写入到response中。
// 代码中到处都是response.write( “html 片段”) 这样的语句
}
可以想见,上述的代码中,必然充斥着大量的HTML片段字符串。这样的代码无疑是丑陋的。如果HTML很长的话——事实上,界面内容越来越丰富,成百上千行的HTML很常见——那么,生成HTML的代码将丑陋得令人发指。
这种HTML大量分布在代码中的情形,叫做污染——即HTML污染了代码,也叫做侵入——即HTML侵入了代码。
在一个HTML页面中,静态部分总是占大多数的,动态部分总是占小部分的。为了小部分的动态内容,把大部分的静态内容嵌入到代码中,这种做法显然是本末倒置、得不偿失的。那么,如何来解决这个问题呢?
这时候,一种直观的解决方案出现了。既然把大部分HTML嵌入到小部分代码中是很难看的,那么,换个思路,把小部分代码嵌入到HTML中不就得了?
事实上,目前的主流动态页面生成技术——如PHP、ASP、JSP、Python页面模板、Ruby页面模板等——正是采用这样的方法,在庞大的HTML文本中嵌入动态代码。
页面生成技术,就是在这个地方,误入了歧途。这种“HTML中混入代码”技术虽然是最流行的页面生成技术,但绝非是最好的技术。由于混在HTML中的代码难以管理,难以重构。这也是一种污染和侵入,代码污染了HTML,代码侵入了HTML。
“HTML中混入代码”技术带来了很多负面效应,令页面程序员深陷泥沼不可自拔。但人们并没有反思这种技术是否存在根子上的问题,而是沿着这条道路上走得越来越远,想出各种方法对这种技术进行修修补补,企图弥补其缺陷,最终的结果是,不仅没有解决原来的问题,反而引入更多的问题,从而催生了一个新的软件市场——页面技术修补技术。这并不是为了解决用户的问题,而是软件开发领域里自己制造问题,自己解决问题。这是典型的自产自销,不可避免地增加了用户的最终成本,同时也养活了一大批软件从业人员。
在软件开发领域,这种看似怪诞的事情,实则极为常见,尤其在那些被超级大公司控制的领域中。那些大公司有意地推行一些极为笨重、笨重的开发框架,从而增加开发成本和时间,来养活更多的软件从业人员。对于软件开发人员来说,这些大公司功不可没,正是因为大公司的这些做法,才保证了人才市场对软件开发人员的需求。这种现象不仅在软件领域存在,在各个领域中都存在。任何领域中,具体工作都是在基层完成的,越到上层,工作内容就越抽象。到了超级顶层,基本就剩下“吹水”的工作了,也就是说,要靠人格魅力取胜,而不是靠办事能力。到了那个层次,做人,远远比做事重要。个人觉得,那才是个人价值的真正体现。
MVC架构的出现,一定程度上减少了“HTML中混入代码”技术的负面效应。因为获取数据和页面流程的代码最大限度地移出,页面模板中只剩下尽量少的必要的页面逻辑代码。
但是,就是这些残留在页面中的代码,也给Web开发带来了很大的麻烦和困扰。在复杂的HTML模板中,一切可以应用在纯代码中的重构技术都失效了。一条if else或者for 语句有可能跨越几十行、甚至上百行HTML文本。而且,HTML文本中的代码只是一个过程中的代码片段,很难结构化。在一些特殊的情况下,显示逻辑代码可能需要用到递归——比如,展示树形结构数据的时候——这时候,HTML中的显示逻辑代码就力不从心了。
令我想不通的是,除了这些难以克服的本质问题,页面技术还在不断引入新的问题。比如,很多的主流页面模板都采用<% %>这样的百分比尖括号来包装代码。这种样式会直接破坏HTML的结构,使得浏览器无法正确HTML模板。
那么,这个问题是如何解决的呢?
有些人采用<!-- --> 这样的XML注释尖括号来包装代码,这就有效地减少了对HTML结构的破坏。但遗憾的是,采用这种方式的页面模板并不是主流技术,至少不是那些大公司支持的主流技术。
那么,掌握了技术主旋律的大公司是如何做的呢?他们的思路可谓是另辟蹊径,别出心裁。他们同样也认为<% %>这样的代码包装尖括号很难看,但是,他们不认为这是代码的错,他们认为这是格式的错。他们认为,HTML是XML格式,<% %>这样的代码包装尖括号不是XML格式,所以,才把HTML的格式破坏了,页面模板才显得很难看。不得不说,他们的想法也确实有一定的道理。
那么,他们是如何解决这个所谓的“格式问题”的呢?他们提出了“页面组件”的概念,这个概念借鉴了桌面程序开发中的“窗口组件、控件”的概念,应用到HTML页面中。首先,为了表达代码逻辑,他们定义了一套“逻辑代码”组件,即把if else for 等诸多逻辑代码变成XML格式的表达。其次,为了处理HTML元素中的动态显示部分,他们把几乎所有的HTML元素都给重新定义了一遍,定义成了另外一套“界面控件”组件,这套“界面控件”几乎就是HTML控件元素的翻版。
有了这样的“利器”,整个HTML模板就可以重写了。就这样,“页面组件”代替了原有的一部分动态HTML内容,静态HTML还是保持不变,整个HTML模板全都变成了XML格式。好吧,我承认,XML格式化这个目的确实达到了,虽然我看不出XML格式化的目的到底何在。那么,“页面组件”是如何实现的呢?“页面组件”是一套“全新”的XML格式,它最终还是要输出成为HTML格式。它是如何输出的呢?答案是,用代码来输出。
为了支持“页面组件”的HTML输出,每个“页面组件”都对应着一个后台组件程序。这些后台组件程序的作用就是根据“页面组件”的定义,输出对应的HTML。不可避免的,这些后台组件程序中,必然充斥着HTML元素字符串。我们可以看到,在开头我们讲的“HTML污染代码”的问题,又回来了。这完全是走了回头路。
那么,“页面组件”避免了“代码污染HTML”的问题吗?初看起来是这样的,都是XML格式,看起来挺整齐的。但实质上是没有。HTML中仍然存在着逻辑代码,只不过这些逻辑代码变成了XML格式。
“页面组件”完成之后,开发人员才“如梦初醒”地意识到另一个问题——可视化问题。“页面组件”并不是合法的HTML元素,也不能在浏览器中正确显示。在可视化方面,“页面组件”比起“<% %>”样式,没有任何的进步,反而变本加厉地破坏了HTML的显示结构。别急,这时候,“页面组件”的XML格式的优势就显现了出来。开发人员又开发出一套“页面组件”渲染系统,其工作原理很简单,就是解析XML格式的页面模板,遇到静态HTML就直接输出,遇到页面组件,就调用后台组件程序,输出HTML。这样得到的结果就是纯粹的HTML,就可以利用HTML渲染器来正确显示了。于是乎,“页面组件”概念的提出,又催生了两个领域的产业,一个是页面组件开发领域,一个是页面组件渲染显示领域。
在我看来,页面组件几乎没有任何优点,放眼望去,几乎全是缺点。页面组件破坏了HTML的可视化,其罪一;页面组件用XML格式表达代码,其罪二;页面组件用代码污染HTML,其罪三;页面组件用HTML污染代码,其罪四;页面组件的渲染效率很低,降低了服务响应速度,其罪五…..
页面组件的唯一优点可能就是自产自销,又创造了一大批工作岗位,又养活了一大批人吧。但养活的这批人中不包括我,因为,页面组件严重违反了我的技术审美观。
在我看来,页面技术要想一劳永逸地免除麻烦,只有一个方法,那就是从根子上着手,彻底清除页面模板中的任何代码,使得页面模板成为不含有任何可执行代码逻辑的纯粹资源文本。当然,为了显示动态数据,页面模板中还是需要保留必要的层次结构信息,以便和动态数据模型(通常也是树形结构)相对应。我这种方案称为“层次结构化文档”。
这个名称听起来是否有些熟悉?没错,HTML本身就是一种XML格式,而XML天生就是树形的层次结构化文档。那么,“层次结构化文档”的一种最简单实现,就是直接在HTML的XML DOM结构上做文章,即取出对应的XML结点,进行替换(条件分支)或者重复添加(循环)。这种方案的好处是简单易行,不需要添加任何基础设施,只需要利用现成的XML解析器就可以了。但这种方案的缺点也不容忽视,HTML通常比较复杂,层次结构比较深,元素又多又琐碎,其XML DOM结构相当笨重庞大,无论是操作,还是显示,空间和时间上的开销都比较大,会影响到性能。
一般来说,动态数据模型的层次最多也就四五层,再多也多不到哪里去。而HTML层次结构动辄十几层、几十层,其静态元素个数也远远超过(几十倍上百倍的超过)动态数据模型的数据量。直接使用XML DOM结构是不太合适的。
一个折中方案是“自定义层次结构”,即,用另外的标记(Mark)来划分文档结构。比如,我们可以用如下的being … end 样式来划分文档结构。
<html>
….
<!-- begin a -->
….
<!-- begin b -->
….
….${var}….
….
<!-- end b -->
….
<!-- end a -->

</html>

这种划分标记十分简单,解析器也很容易实现,至少比XML解析器和代码语法解析器简单太多了。上述的自定义标记把文档划分成两三层,最多不超过十个结点。如果用XML DOM来表达的话,十几层、几十层都可能有的,至于结点,那就更多了,成百上千都有可能。
有了层次结构化文档之后,又该如何使用它呢?有两种方案,第一种方案叫做“分块取用”,第二种方案叫做“层次匹配”。
“分块取用”和XML DOM操作类似,就是把层次结构化文档当做一棵文档树来使用,想显示哪个结点,就显示哪个结点,想显示多少次,就显示多少次。
这种做法的好处是,简单、直观、灵活、强大。程序员可以任意操作这些文本结点到达任何目的,比如,递归显示树形数据,页面布局插入其他页面的结点,等等。要知道,在“HTML中混入代码”方案中,这些功能的实现是相当麻烦和困难的。
这种做法虽然好处多多,但有一个鲜明的缺点,那就是页面逻辑代码和特定页面技术绑定得太紧。那些操作文本结点的后台代码就是页面逻辑,这些代码需要调用具体的页面技术API,这就意味着Web应用代码需要在代码中引入这个具体的页面技术开发包,从而造成依赖。这也是一种侵入和污染。一个设计良好的Web开发框架,是不应该允许这种情况出现的。而且,那些操作文本结点的代码都需要调用具体的API,由于这些代码需要取用并操作的文本结点,通常都比较繁琐。而且,这些代码是无法重用的。如果换一种页面技术,这些代码只能弃而不用。
更好的选择是第二种方案——“层次匹配”。在这种方案中,后台页面逻辑代码并不直接操作文本结点,而是根据文本层次结构,对动态数据模型进行包装,生成一个用来“匹配显示”的“页面数据模型”,供层次匹配引擎程序使用。匹配引擎程序把“页面数据模型”和“层次结构化文档”匹配起来,直接输出结果HTML。
这个匹配引擎程序的实现非常简单,比脚本解释器的实现简单太多。在页面显示中,动态内容的显示只有三种情况:不显示,显示一次,显示多次。
在“HTML中混入代码”技术中,这三种情况分别用if 、else、for 等语句表示。实际上,这是根本不必要的。
在匹配引擎程序中,这三种情况都可以只用一种结构来表示,那就是List。当List中的元素个数为0,那就是不显示;当List中的元素为1,那就显示一次;当List中的元素为多个,那就显示多次。就这样,匹配引擎只用一种数据结构,就可以表达所有的显示逻辑。
“层次匹配”的好处是显而易见的。首先,显示逻辑代码不需要调用具体的页面技术API,只需要生成“页面数据模型”,这就解除了对具体页面技术的依赖。其次,显示逻辑代码存在于后台,不在页面模板中,从理论上来说,“层次匹配”和“分块取用”是同样强大的,同样可以实现递归显示树形数据、页面布局插入其他页面的结点等高级功能,只需要在页面数据模型和匹配引擎上做些文章即可。再次,“层次匹配”的显示逻辑代码的重用度是最高的,因为,“页面数据模型”是对动态数据模型的包装,这份数据既可以用于匹配引擎,也可以用于其他页面技术,比如“分块取用”和“HTML中混入代码”,所以,这部分显示逻辑代码是完全可以重用的。
除了上述优点之外,“层次匹配”的最大优势是污染度和侵入度最低。页面模板里面一点代码都没有,免除了代码对HTML的污染和侵入;代码里一点HTML也没有,一点具体的页面技术依赖也没有,免除了HTML和页面技术对代码的污染和侵入。当然,也不能说一点侵入和污染都没有。下面我们就来讲这个问题。
“层次匹配”的页面逻辑代码需要根据动态数据模型构造出页面数据模型。在构造页面数据模型的过程中,需要在动态数据模型之上加入一些“显示开关”类的数据结构。
比如,这样的页面逻辑,如果A小于0,就不显示动态文本t001,如果A > 0就显示动态文本t001。那么,显示逻辑就需要根据A的值,生成对应的数据结构(元素个数为0的List,或者元素个数为1的List)。现在的问题是,这些和显示开关对应的页面数据结构应该放在哪里呢?
如果是Javascript这般强大的动态类型解释语言,问题相对容易解决。我们只要在原始动态数据模型之上添加新的“显示开关”属性就可以了。如果是静态类型编译语言,就比较麻烦了,只能采用HashMap之类的动态数据结构来构造整个数据模型,因为HashMap可以任意添加新的属性条目。这也是我的建议。因为页面显示本来就涉及到大量的动态性,如果能用动态类型语言就尽量用,如果不能用,那就尽量使用HashMap这样的动态数据结构。
上述的“页面数据模型”构造问题是存在于后台代码中的根深蒂固的无法消除的问题。这部分侵入和污染是不可避免的。因为,我们总得找一个地方实现这个页面逻辑。我们只能想办法减少这部分侵入和污染。不过,两害相权取其轻,与其他页面技术相比,这点小问题还是可以接受的。
“层次匹配”的另一个问题存在于页面模板中。在前面的例子中,<!-- begin a -->和${var}这样的标记对HTML也造成了一定的污染和侵入。当然,相对于HTML混入的代码来说,这点污染和侵入可以忽略不计。不过,另外一个问题——模板解析器,却是无法忽略不计的。
根据我自己的经验,匹配引擎的开发是令人愉快多的,几个递归结构加上模式匹配就搞定了。但是,涉及到字符串处理的模板解析器就不同了,繁琐细碎,涉及到大量的字符串查找和比较,只能一步步死抠和细抠,十分令人头痛和厌恶。
除此之外,模板解析器还有一个问题,那就是自定义标记的问题。在HTML这样的XML格式文档中,我们可以用<!—begin … -->这样的XML注释作为自定义标记。但是,如果换成其他的文本,比如说代码和SQL,这样的标记就不太合适了。为了让模板解析器达到最大的通用性,最好引入一套机制,允许用户自定义文档划分标记。这就进一步增加了模板解析器的复杂性。
因此,即使这种模板解析器比脚本解析器和XML解析器简单了许多,也是相当繁琐的。那么,有没有办法避免模板解析器的问题呢?答案是,有。下一章讲述一种能够免除模板解析器的方案。这种方案是在一种叫做Flyweight Pattern(轻量级模式)的设计模式上实现的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值