《编程机制探析》第二十五章 Web开发架构

阅读更多
《编程机制探析》第二十五章 Web开发架构

前面章节讲述了HTTP协议的方方面面,从本章开始,我们进入到Web编程开发的世界。
Web应用程序这种说法,主要是针对桌面程序来说的。桌面程序的图形界面元素十分丰富,交互性、操作性也十分良好。Web应用程序的界面,传统来说,只有一种,就是在浏览器中显示的HTML。
一开始的时候,HTML并不是为了应用程序而设计的图形界面,而是以内容文本表现为主要目的的文本结构。HTML具备了最基本的图形界面元素,但是,从功能、效果、交互性、可操作性来说,都比桌面程序简陋了许多。Web应用拥有与生俱来的优势,Web应用是基于HTTP协议的,而HTTP协议是一次性应答协议,不保持长连接,这就使得Web应用可以支持大用户量和访问量。由于这个优势, Web应用程序蓬勃发展,四面开花,HTML开始承担起应用程序图形界面的重任。
在Web应用程序的发展历程中,HTML的局限也越来越明显。为了提高HTML的表现能力和互操作能力,HTML与时俱进,不断引入新的特性,其中一个重要举措是植入各种插件,提供更加丰富的界面功能。同时,HTML规范本身也在修订中,现在HTML5方兴未艾,提供了很多以往由插件提供的媒体功能。现代的Web应用程序界面,比起从前,不知强了多少倍。当然,比起桌面程序来,还是有不少的差距。但对于大多数的应用来说,却是足够了。
Web应用程序的应答模式很简单,就是一问一答:浏览器发出HTTP Request,网站服务器收到HTTP Request,生成HTTP Response,返回到浏览器。
从前面章节已经给出过例子,HTTP Response除了头部几行之外,剩下的部分全都是HTML。那么,Web应用程序的主要任务其实就是生成HTML页面。相应的,页面生成技术,也是Web应用开发中的一个重要主题。关于这个主题,后面有专门的章节讲解。本章主要关注Web开发的总体架构。
Web应用程序的HTML界面有两个主要特点。
首先,Web应用是“应用程序”,HTML中的Form(表单)和Button(按钮)元素,使得Web应用程序具有了最基本的用户响应功能。
其次,Web应用程序也是内容提供服务程序。HTML本身是一种树形结构化的文档定义。Web应用程序的HTML界面,很大程度上也是一种网页文档。
从这两方面来看,Web应用程序很像是桌面程序中的一个类别——多文档编辑程序。当然,Web应用程序的文档编辑功能很弱,只能通过提交动态数据来改变网站上的HTML页面。
Web应用程序和多文档编辑程序更相似的地方还在于程序组织结构上,两者的结构都是文档数据 + 文档展示界面 + 流程控制,即我们熟知的Model-View-Control(简称MVC)结构。其中,Model对应文档数据,View对应文档视图,Control对应文档视图控制。
多文档编辑程序的MVC结构一目了然,我们以办公软件中的多文档编辑程序(比如Word)为例。Model就是文档文件,View就是文档视图,Control就是那些和文档视图切换相关的功能菜单。
Model和View之间是多对多的关系。一个文档文件可以用多种视图来展示,一种视图可以用来展示各个文档文件。这体现了MVC架构在代码重用方面的优势。
相对于多文档编辑程序的简单清晰的MVC结构来说,Web应用程序的MVC结构就复杂了许多。事实上,Web应用程序只是借鉴了多文档编辑程序的“MVC架构”这个名词,在具体的应用上,Web应用程序对MVC架构进行了重大的扩充和修改,赋予了MVC架构丰富的新内涵,最后夺取了MVC架构定义的话语权。现在,各种资料中提到的MVC,主要就是指Web MVC。
前面的章节中讲过,网站服务器分为CGI和app server两种模式。这两种模式的区别在于,app server模式下提供的内存共享模型更加丰富一些,响应用户请求的线程之间可以共享app server进程空间内的同一块内存,在Session实现、数据共享方面多了一些选择。除此之外,两者的开发架构并没有显著区别。无论是CGI模式,还是app server模式,都可以应用MVC开发架构。
需要提醒的时候,Web MVC架构并不是一个没有争议的规范定义。由于Web MVC架构比多文档MVC复杂了许多,Web MVC存在着各种形态,其对应的Model-View-Control也有不同的含义。本书采取的Web MVC定义只是目前最流行的一种,也是我觉得结构划分最清晰的一种。
在Web MVC中,各种具体对应物有了新的变化。Model不再是指文档文件,而是指动态数据,进而指提供动态数据的对应程序。而View也不再是指视图,而是指嵌入了页面展示逻辑的HTML模板,进而指提供页面展示逻辑的对应程序。Control不再是文档视图切换,而是包含了服务程序分派、页面流程控制等复杂功能。
Model和View,后面有专门章节讲解。本章重点讲解Control部分,因为Control是整个Web应用程序的入口点。
Control有两个主要任务。第一个任务是“URL到服务程序的映射”,这个任务也叫做Dispatch(分派,即根据URL将请求分派到对应的服务程序),实现这个功能的模块通常叫做Dispatcher(分派器)。第二个任务是“页面流程控制”,这个任务的内容比较丰富,包括数据流向、模板选择、页面转向等等诸多内容。
我们先来看第一个任务——URL到服务程序的映射。首先,我们需要了解URL的含义。URL是Universal Resource Locator的缩写,这个词汇的本义,我们不去管它,我们只关心URL在具体应用中的含义。
我们知道,用户通过URL来访问网站上的对应服务。URL的最初含义是网址,即网站上的资源文件地址。这是静态网页时代的概念。在静态页面网站上,URL对应网站上的某个具体资源文件路径。在动态网站上,URL对应着某个提供动态内容服务的程序。这正是Control(或者说Control中的Dispatcher模块)要做的第一个任务——把URL映射到对应的服务程序上去。
“URL映射到服务程序”的实现方案分为两种——静态配置和动态解析。
静态配置,很容易理解。就是把映射规则一条条写到代码里面,比如:
case URL of
   “login” -> loginService(request, response)
   “regisiter” -> regisiterService(request, response)
   ….

网站的开发并不是一蹴而就的。有可能先开发出基本功能,上线测试,再逐步提供更完善的功能。在这个过程中,URL映射规则可能会改变或者扩充。
如果上述映射代码是动态类型解释语言的话,那么,没问题,不需要停机重新编译,直接修改代码中的URL映射规则就可以了。
如果上述映射代码是静态类型编译语言的话,那么事情就麻烦了。代码修改之后,网站服务器需要停止,代码需要重新编译和部署,之后,网站服务器再重启恢复服务。为了避免这种麻烦,程序员通常把URL映射部分移出到代码之外的配置文件中(通常是XML格式),这就解决了代码重新编译和部署的问题。
静态配置的优点在于一目了然,有多少URL映射,有多少服务程序,写得清清楚楚。但静态配置有个根深蒂固的弱点,那就是无法自动扩充。每增加一个新的服务程序,就需要添加一条新的映射配置。动态解析方案,则解决了这个问题。
动态解析方案中,程序员不需要写URL配置文件,但是,需要定义一套简单明了的映射解析规则,然后,按照这个规则,解析URL,直接把URL映射到对应的服务程序上去。比如,上述的映射代码可以写成:
call ( URL + “Service”,  [request, response] )
意思就是,把对应的URL部分取出来,加上“Service”这个字符串,就构成了一个函数名,然后,把[request, response]作为参数,调用这个函数。这就实现了把URL映射到服务程序的功能。
上述的URL解析规则只是一个示例。不过,真实的解析规则也比这个规则复杂不了多少。因为,动态解析的要点就是解析规则简洁明了,否则就失去了实用的价值。
静态配置方案曾经是主流,但如今,越来越多的Web开发框架开始采用动态解析方案。关于URL映射,有一个流行的说法,叫做“Convention Over Configuration”(惯例优于配置)。这里的Convention(惯例),就是指简单明了的URL解析规则。“Convention Over Configuration”(惯例优于配置)的意思就是,动态解析方案优于静态配置方案。
讲到URL映射,就不得不提到“URL美化”这个技术。
如果不做任何修饰美化的话,一个网站的原始URL看起来是这个样子的:
http://somesite.com/inbox.php?a=1
这样的URL比较难看,一堆的?、&、=等杂七杂八的符号,看起来很不清爽。这个URL可以美化成:
http://somesite.com/inbox/a/1.htm
这个美化工作实现并不难,只需要在网站服务器前端加一个URL字符串处理程序,就可以把类似于inbox/a/1.htm这样的URL字符串转化成inbox.php?a=1,这就得到了可以被Web应用程序正确处理的URL。
这里要注意一点,URL处理程序并不是对难看的原始URL进行美化,而是对美化后的URL进行还原。因为,我们在网页中编写的URL都是经过美化的URL,用户访问的也是美化后的URL。
URL还原处理的功能,可以放在外置的URL处理程序中实现,也可以在Control中实现。如果在Control中实现的话,就可以直接在“URL动态解析”程序中实现,直接把美化后的URL解析为服务程序需要的参数,从而省略“把美化的URL还原”这个步骤。
URL美化之后的样式多种多样。有些人喜欢把URL美化成类似于静态网页资源的样式。比如前面的例子:
http://somesite.com/inbox/a/1.htm
有些人喜欢把URL美化成REST风格的样式(RESTful),比如:
http://somesite.com/inbox/a/1
这种REST风格的URL在目前颇为流行。那么,什么叫做REST呢?这就说来话长了。不过,鉴于REST这个词汇如此流行,即使说来话长,也得说一说。
REST并不是“休息”的意思,而是“Representational State Transfer”的缩写。这个词组的原意比较怪异,直译过来也是怪怪的。这里不去追究其本意,只介绍和我们相关的内容。
REST这个概念,是在Web Service刚刚兴起的年代提出的。那时候,Web Service完全是XML RPC的天下,主流协议是SOAP(Simple Object Access Protocol,这个名词也没什好解释的,知道有这个东西就行了),那是一个用XML来定义远程函数调用接口的规范,包括消息头、消息包装、调用服务名、参数名、参数类型、参数值等等诸多信息。
正是在这个大环境下,一个博士生在毕业论文提出了REST概念,矛头直指SOAP。我现在还记得当年那篇文章的大意。
文中抨击了SOAP的弱点,说SOAP相当于把服务名等信息隐藏在邮包的内部,每一个接受到SOAP的经手人(服务器)都得把邮包拆开(即深入到SOAP格式的消息体内部),才能得知需要的服务名。
文中提出了另外一个思路——REST风格。在REST方案中,服务名和参数不再包装在某种协议(如SOAP)内部,而是直接体现在URL上。
比如,getUser(id = 1001)这样的远程调用不再写成这样的格式:

  1001

而是直接写成user/1001这样的URL形式。这样,服务器一看到这个URL,立刻就知道这个请求是要求什么服务。
在这种样式下,user/1001就类似于一个查询条件(user=1001)。1001可以看作是user这个资源库中的一个ID。这个字串看起来就像是一次数据查询——“请给我ID为1001的user”。
在REST方案中,URL不再叫做URL(Universal Resource Locator),而是叫做URI(Universal Resource Identifier)
REST样式的Web Service有很多优点,如清晰易懂,对于代理服务器(Proxy)友好,等等。
W3网站上有两个XML查询相关的规范——XPath和XQuery。
XQuery结构复杂,能力强大,能够与关系数据库的SQL相媲美,不适合用于URI中。
XPath就是一行,用来定位XML元素的路径、属性和值。这简直是为REST风格量身定做的协议。
我个人倾向于REST风格,一方面因为REST简洁明了,另一方面因为我不喜欢用XML格式来描述函数调用。如果是数据资源查询导向的服务类型,那么首选REST。如果是参数复杂的远程调用的服务类型,可以尝试改造成REST样式,如果实在难以改造,也不必强求。
关于URL映射的内容,就讲到这里,我们继续讲解Control的第二个重要任务——页面流程控制。这个任务的内容比较繁杂,我们一步一步来讲解。
HTTP Request中有一个Header,叫做Method。这是一个非常重要的属性,定义了HTTP Request对网站资源的操作。最常见的Method取值有两种:Get和Post。
当我们通过网址访问网站的时候——比如,我们在浏览器中输入网址,或者点击网页中的网址链接——HTTP Request的Method就是Get。
当我们通过HTML Form(表单)中的输入框和按钮向网站服务器提交数据的时候,HTTP Request的Method通常就是Post。为什么要说“通常”呢?因为,HTML Form有一个属性叫做Method,对应着HTTP Request的Method。
如果我们把Form的Method属性定义为Get,那么,HTTP Request就和网址访问一样,Form输入框中的数据参数就会跟在URL的后面,样式如 ?a=1&b=2,HTTP Request的消息主题部分就是空的。
如果我们把Form的Method属性定义为Post(缺省值就是Post),那么,Form输入框中的数据参数就不会跟在URL的后面,而是出现在HTTP Request的消息主体中。
URL参数的容量是有限的。如果我们想提交大量数据到网站服务器的话——比如,发表文章——我们就应该使用Post。当然,Post也是有数据容量上限的,不过这个上限要比Get高很多。
大部分情况下,HTML Form的Method都是Post。为了便于后面的讨论,我们就把HTML Form提交等同于Post,把网址访问等同于Get。
服务器端对于Get和Post的处理流程大同小异,只除了一点:Post流程处理中多了一步参数值验证(Validation)。
在出现HTML Form、需要用户输入数据的户界面中(如注册、登录、查询、输入数据等),服务器端都要对用户输入的数据进行验证。进行数据验证(Validation)的程序叫做Validator(验证器)。
成熟的Web开发框架一般都会提供一些现成的常见验证器。比如,长度验证器(输入框中的数据长度必须在某个范围之内),数字验证器(输入框中的所有字符都必须是数字),等等。这些验证器还可以组合起来使用,要求输入框中的数据同时符合几个验证器的要求。这是一个典型的Compositor Pattern。
根据代码运行位置的不同,验证器可以分为两类——浏览器网页中的Javascript验证器和服务器端验证器。
一般来说,上述的常见验证器都会同时实现两套方案,一套是浏览器网页中运行的Javascript验证器,一套是服务器端的验证器。这是为了提供双重保险。
用户输入数据首先要经过浏览器网页中运行的Javascript验证器的验证,如果不通过,Javascript验证器直接给出用户提示,要求重新输入。这种做法的好处是不需要重新连接服务器,节省时间和网络流量。
有时候,用户浏览器不支持Javascript或者支持得不好,Javascript验证器没有起作用,数据没有经过验证,就发到了服务器端,这时候,服务器端的验证器就可以发挥作用了。这就是双保险。即使浏览器中验证过一遍,服务器端再验证一遍也无妨,这点开销相对于网络通信和数据查询来说,几乎是可以忽略不计的。
除了Web框架中提供的现成验证器之外,程序员还可以自定义验证器。同样,可以自定义浏览器Javascript验证器和服务端验证器。有些验证器涉及到服务器端数据,必须在服务端实现。比如,用户名存在验证器,密码错误验证器,等等。
除了数据验证这个步骤之外,Post和Get的处理流程就基本一致了,都是根据Request参数获取数据(Model)和页面模板(View),然后,生成HTML页面,写入到Response中。
如何获取数据(Model),如何获取页面模板(View),如何生成HTML页面,这是Request处理流程的三大关键步骤。所有的Web开发框架都是在这三大关键步骤上作文章。不同的Web框架,对这三大关键步骤的实现和组织也不一样。
下面,我用函数式伪代码来描述一种我最欣赏的MVC架构。为什么要用函数式呢?因为函数式语言中,函数本身就是对象,不需要另外的对象来包装,表达起来简单清晰,更加时候用来描述MVC流程。这种MVC架构以结构清晰取胜。函数式伪代码描述如下:
mvc ( request, response) =
url = request.url
service = dispatch( url )
# 以上部分是Dispatcher部分,把URL映射到具体的服务函数

  (model, view) = service(request, response)
  # 这是最关键的一步。整个框架核心就体现在这里
  # 服务函数返回model和view。
# 这里的model是所有需要显示的数据。通常是树形结构。
# 这里的view只是一个字符串,代表页面模板的名字
 
pageTemplate = findTemple( view )
  # 根据view获取真正的页面模板

  write(model, pageTemplate, response)
  # 把model和pageTemplate结合起来,写入到response里面

上述的伪代码就是一个MVC框架的简单示意代码。框架用户只需要实现对应的service函数,比如,loginService(request, response),registerService(request, response)。框架负责找到并调用这些service函数,完成整个请求。
在上述代码中,有一条语句:pageTemplate = findTemple( view )
这条语句根据View(一个简短的字符串名称)找到对应的HTML页面模板。同URL映射到服务程序一样,这也是一个映射工作,即,把View字符串名称映射到对应的HTML页面模板文件对象。
同“URL映射服务程序”类似,“View映射到页面模板对象”的实现方案也分为两种——静态配置和动态解析。
静态配置很简单,就是类似于
case View of
   “success” -> “successed.html”
   “fail” -> “login.html”
   ….
这样的代码。我们可以把这段代码用XML形式表示,就成了配置文件。这个配置文件我们可以单独放置,也可以和URL映射配置文件合成同一个文件,大多数MVC框架正是这么做的。
动态解析,同样也很简单。我们可以指定简单的规则,比如给View字符串名称加上前缀和后缀,就可以构成目标文件名,等等。
一开始进行Web开发的时候,我都是用的现成的Web框架。在使用的过程中,总是感到有些地方不满意,于是,就开始研究其他的Web框架,看看是否有更好的方案。在找到更好的Web框架的同时,我又会发现新的不足之处。到了最后,我就萌发了自己设计Web框架的想法。我相信,很多人和我有一样的经历。
本章中讲述的内容,是我在设计和开发Web框架的过程中总结出来的。希望能对大家有所帮助。在结束本章之前,我阐述一下Web框架设计的两个原则:灵活和完熟。
同时具备了灵活和完熟特点的Web框架,是可大可小的框架,能够给用户带来最大的自由和方便。
灵活的意思就是,设计框架的时候,尽量灵活,框架中每一个部分都是可插拔、可拆卸、可替换的。
比如,“URL映射服务程序”部分,完全暴露出来。用户可以使用动态解析方案,也可以使用静态配置方案,还可以编写自己的方案,替换掉框架中的现有方案。“View映射到页面模板对象”部分,也照此处理。
再比如,数据验证器(Validator)部分也完全暴露出来,不仅允许用户自定义验证器,还允许用户自定义验证器的工作流程。由于验证器的逻辑和页面流程经常混杂在一起,这个地方比较复杂,需要精心设计。
又比如,动态页面生成部分,也完全暴露出来,允许用户采用各种各样的方案,最大限度提高生产率或者运行效率。
其他部分也都可以照此办理。这种设计思路的好处是灵活度高、可定制程度高、重用度高。
由于每一个部分都可以单独使用,也可以和其他部分组合使用,用户可以按照自己的需求进行定制,每个模块都可以达到最大的利用率。
但是,这种设计思路的缺陷也是很明显的。这样设计出来的框架必然是千疮百孔,处处可插拔,意味着处处是需要填补的孔洞。
如何弥补这个缺陷呢?那就是第二个原则——完熟。
框架开发者必须提供一套或者几套完熟的成例方案,对应常见的Web应用。这个工作可能很低级,就是一个排列组合工作。也可能很繁琐,因为选项太多,组合起来的各种可能性就更多。程序库管理更是一个噩梦。你需要提供各式各样的程序打包,组合形式有多少,程序包的种类就有多少。总之,这个工作比起开发框架模块来,显得不那么有吸引力。但是,这个工作是必须做的。

你可能感兴趣的:(web,mvc,框架,rest)