欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-17 13:38:24
...
《编程机制探析》第二十六章 页面生成技术

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(轻量级模式)的设计模式上实现的。