响应式服务端实现
本章讨论的核心,也是全书的核心,就是不赞同把响应式Web设计作为一个前端独有的技术。因为如果我们这样认为了,它就会限制我们的思考广度,我们能做什么?我们可以用什么工具?但是作为Web开发者,我们必须精益求精,必须有能力把控Web全栈中任何一个细节。本章将讨论我们如何基于后端来思考如何更好地实现响应式。
4.1 Web栈
在我们开始本章内容之前,我必须先给Web栈一个定义:Web栈是一系列栈的集合。而在讨论Web栈之前,让我们先从网络栈开始。
网络栈
网络栈是由不同网络系统相互通信的一系列协议的统称,它由下面一些层构成。
数据链路层
数据链路层对应的是硬件连接到网络的标准方式。对我们而言,通常是以以太网的形式,具体是支持IEEE 802.3标准的物理互联设备(http://bit.ly/ethernet-standards);或者也可以通过WiFi,具体是支持802.11标准的无线互联设备(http://bit.ly/1p8UW6P。
网络层
网络层对应的是网络中不同网络节点之间相互识别和通信的标准,具体是IP协议或者互联网协议。它是通过IP地址在网络中识别网络节点,并且在主机之间通过数据包的形式传递数据。IETF RFC 749主要记录了标准的互联网协议,你可以在http://bit.ly/11j3ouQ上阅读到详细信息。
传输层
传输层通常相当于TCP,全称是Transmission Control Protocol,也就是传输控制协议。在IETF RFC 793 (http://www.ietf.org/rfc/rfc793.txt)中定义。
TCP是用来在主机间建立链接的协议。IP协议负责将数据以数据包的方式进行传输,而TCP将数据包分成多个段,并且为每个段创建含有目标IP地址的header,重新组装并且对接受到的这些网络传输数据进行校验。
应用层
应用层是这一系列协议的最上层,它对应的是像HTTP这样的协议,HTTP也称为超文本传输协议。HTTP的标准是IETF RFC 2616,你也可以在http://tools.ietf.org/html/rfc2616上看到详细信息。HTTP是一种Web语言,它主要是由request(请求)/response(响应)两个动作组成。
把这些栈组织起来就得到了数据在Internet中发送和接收的整个流程步骤了,如图4-1所示。
0401.tif{1183}图4-1 用户通过TCP/IP栈发送一个请求,然后这个请求同样通过TCP/IP栈和远端服务器上的Web应用进行通信
应用层
了解所有的底层知识是非常重要的,但是对Web开发者来说,最主要的也就是我们每天都会面对的,并且可以通过编码方式控制的一层,就是应用层,也就是HTTP协议。第2章向我们展示了一个HTTP传输在TCP的连接中都要经历哪些步骤。一般来说由两个部分组成:从客户端发送一个请求,然后从服务端返回一个响应。很容易理解,对吧?不过深入了解HTTP内容对我们的工作大有裨益,现在我们来看看一个请求和一个响应的细节性问题,更深入地探讨它们的内部机制。
HTTP请求
一个HTTP请求由两个部分组成:请求正文和一系列请求header。其中,请求正文包括请求的HTTP方法或者动作,以及请求远程资源的URI。更简单来说,它表明了当前要执行的动作(获取文件,发送文件或者获取一个文件的信息)以及这个动作在哪执行(文件或者资源的地址路径),下面就是一些在HTTP1.1中支持的方法。
OPTIONS
服务器支持的HTTP请求方法的功能选项。
GET
当你请求远程资源时,如果你在HTTP header中指定了If-Modified-Since、If-Unmodified-Since、If-Match、If-None-Match或者If-Range里的一项,那么它就变成一个有条件的GET了。因为这个时候,服务器只返回符合这些请求条件的资源。一般来说,如果你想控制获取资源的方式,是获取最新的资源还是使用当前缓存的时候,就可以使用这种有条件的
GET。
HEAD
只请求远程资源的HTTP header,它主要是用来检查最后更改的日期或者确认一个URI是否可用。
POST
请求服务器对一个资源进行更新或者修改。
PUT
请求服务器创建一个新的资源。
DELETE
请求服务器删除一个资源。
请求header允许客户端为请求指定或者增加参数,这种行为和向一个函数传递参数非常类似。下面是一些非常有趣的请求header。
Host
在URI中指定的域名名称。
If-Modified-Since
这个参数在Request header中传给服务器,指示服务器只返回在指定的时间里有过更新的资源。如果资源已经更新过了,那么服务器就返回这个资源和200状态码。如果没有,那么服务器将返回304状态码。
User-Agent
这个参数标识客户端的具体特征信息。在本章中的内容里,我们在很多地方都使用了这个参数。
你可以通过一些网络探查工具——例如Charles或者Fiddler——检查HTTP请求的内容。下面的例子我们展示了一个HTTP请求的详细信息。
GET /style/base.css HTTP/1.1
Host: www.tom-barker.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7;
rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/css,*/*;q=0.1
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://www.tom-barker.com/blog/?p=x
Connection: keep-alive
HTTP响应
当服务器接收并处理了一个请求以后,它将会返回客户端一个响应信息。和HTTP请求一样,HTTP响应也由两个部分组成:状态行和响应header的属性。
状态行中有协议版本号(HTTP 1.1),状态码和描述请求状态的文本短语,状态码由三位数字组成,并且把响应分成了五个层次类别。状态码的第一位数字表面它的类别。你可以在http://bit.ly/rfc-http上查到W3C的HTTP详细规范,具体的类别如下所示。
1xx:信息
请求已收到,正在处理中。
2xx:成功
请求已经成功接收、解析并执行了。
3xx:重定向
需要进一步的跳转和更多的操作来完成当前请求。
4xx:客户端错误
请求包含了语法错误,不能被执行。
5xx:服务端错误
服务端在处理一个有效的请求时失败。
响应header里的属性值和请求header里面的属性值很像,服务端通过指定这一系列的键值对表明当前响应的具体信息。下面就是我们常用到的一些响应header。
Age
标识请求的资源从创建或者更新时到请求发生时的大概时间。
ETag
列出了服务器为这个资源分配的唯一实体标记符,这个参数在一些需要额外匹配验证的时候非常有用。
Vary
这个参数标明当前请求的哪个请求头用来表示该请求是否可被缓存。在本章的后半节中,我们将模拟基于不同的User Agent,服务器发送不同的响应。Vary这个参数非常重要,因为它让我们可以将User Agent头信息作为一个请求是否可以被缓存的决定因素之一。
下面是HTTP Response的示例。
HTTP/1.1 200 OK
Date: Sat, 29 Mar 2014 19:53:24 GMT
Server: Apache
Last-Modified: Sat, 05 May 2012 22:11:12 GMT
Content-Length: 2599
Keep-Alive: timeout=10, max=100
Connection: Keep-Alive
Content-Type: text/css
Charles
现在有非常多的工具可以观察到你的网络传输情况,这些开发者工具是以浏览器插件方式存在的(详见第2章)。还有更多可以深入分析网络传输的工具:它们中的一个佼佼者就是Charles,它在Web开发者中非常受欢迎(见图4-2)。
0402.tif{1011}图4-2 Charles主页
Charles是一个HTTP监测工具,利用Charles,你可以观察和编辑网络传输内容;Charles同样也是个HTTP代理工具,你可以使用它控制带宽流量,降低连接延迟,拦截请求,DNS欺骗,甚至映射到本地文件,使得访问本地文件可以像访问远程服务端文件一样。从http://www.charlesproxy.com/上可以下载最新的Charles。
图4-3展示了Charles的界面。这个截图按顺序展示了在一给定访问中的所有HTTP事务,我们可以看到,在展示的结果中,包含了HTTP状态、HTTP方法、主机名、传输负载和持续时间等。
0403.tif{1070}图4-3 Charles记录的HTTP传输信息
4.2 Web应用栈
到目前为止,我们一直在讨论Web应用运行所需要的基础设施和网络协议。现在我们要理解Web应用运行所需要的软件栈。绝大部分情况下,Web应用是基于客户端-服务器模式的,也可以将其看成是一个分布式计算方式。如果用一句话来总结,就是客户端向服务器端请求数据,服务器处理请求和响应。通常情况下,为了可扩展性,这些服务器分布在整个网络中。
为了符合这种模式,在下面的具体示例中,首先让我们假设浏览器就是客户端,Web服务器就是server。当我在说Web服务器的时候,指的是像Apache(https://httpd.apache.org/)和微软的IIS(http://www.iis.net/)这样的应用软件,或者是运行这些软件的硬件。
言归正传,这些Web服务器监听某些特定的端口——表示应用端口的数字,也就是HTTP请求的端口。通常情况下,HTTP请求的是80端口,HTTPS请求的是443端口。当Web服务器接收到这个请求的时候,它就会把这个请求发送给对应的资源。
类似于Ruby或者PHP,或者像HTML页面这样的静态内容,这些资源可以在服务端以代码的方式进行评估和解析。无论哪种方式,这个被发送的请求都会获得服务端的响应。
如果服务端响应的正文是HTML文件, HTML将在客户端设备上进行解析和渲染。如果内容中包含了任何JavaScript文件,它们也会被客户端设备解释运行。
0404.tif{1076}图4-4 客户端-服务器传输示例
4.3 服务端响应
现在你已经对协议和软件栈在整个Web栈中的定位和作用有了一个大概的了解,所以在整栈中,你应该做的第一件事,就是尽可能早地判断出客户端的相关特性。现在,响应式设计是客户端获取服务器端发送HTTP响应后,在客户端根据获取到的相应的客户端特性,然后根据这些特性对响应的内容进行接收、解析和渲染等一系列操作。
浏览器请求页面的架构如图4-5所示。Web服务器在80端口接收到了请求,然后传递给Web应用,Web应用随后处理这个请求并发送响应。客户端接收到这个响应以后,解析页面内容,渲染页面,在客户端设备上运行获取客户端设备特性的代码,最后根据这些特性做相应的页面显示。
0405.tif{582}图4-5 在客户端确定特性
即使用文字写把所有这些步骤列出明细,也会让人感觉过度和不需要如此复杂。
我们可以从HTTP请求的描述信息中收集客户端的一些特征信息,还记得之前说过的User Agent吗?是的,这些描述客户端的特征信息正是通过User Agent传递给Web服务器和Web应用的。我们可以在服务端,而不是客户端确定客户端的特征和能力,这将有效地简化我们发送给客户端的内容——发送针对设备专有的代码而不是全部的代码(图4-6为修改过的架构)。
看到这里,你肯定非常想知道我们是如何利用User Agent确定客户端能力的。所以首先我们来看一看User Agent。
0406.tif{967}图4-6 在服务端确定客户端的能力并且响应对应的客户端内容
检查User Agent
你可以在RFC 2616的14.43小节查看User Agent的字段规范,具体的HTTP规范可以在http://bit.ly/1tDGO Z0上找到详细信息。
User Agent是由不同的令牌组成的一个字符串,它描述了浏览器、浏览器版本、操作系统及系统版本等一系列系统信息,见表4-1。
表4-1 User Agent的一些示例字符串
浏览器 | User Agent字符串 |
---|---|
Chrome34Mac版 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 |
Safari OS7+iPhone版 | Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KH TML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 |
Safari OS6+iPad版 | Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML , like Gecko)Version/6.0 Mobile/10A5355d Safari/8536.25 |
Chrome Android手机版,并且运行在Ice Cream Sandwich版本上 | Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr;LG -L160L Build/IML 74K) AppleWebkit/534.30(KHTML , like Gecko) Version/4.0 Mobile Safari/534.30 |
你可以很容易获取这些字段并通过正则表达式解析得到相关的信息。下面这个例子,通过一个函数来确定客户端设备,从而建立起响应的客户端功能。这个例子非常简单,我们将使用JavaScript创建一个函数,像下面代码那样检查移动设备。
function detectMobileDevice(ua){
var re = new RegExp(/iPhone|iPod|iPad|Android/);
if(re.exec(ua)){
return true;
}else{
return false;
}
}
这里需要注意的是,我们将User Agent作为参数传递给detectMobileDevice函数,并且使用正则在User Agent中搜索匹配iPhone、iPad或者Android的字符串;一旦匹配,我们将返回true。
这是一个相当简单而基础的示例,它只关注了当前的客户端设备的平台或者操作系统。我们还可以使用更强大的功能检查客户端的特性,例如触摸支持和设备的最大宽度。
Google和Apple分别在http://bit.ly/1u0cHqv和http://bit.ly/ZXVAhT上发布了它们的User Agent标准。
在探讨User Agent可靠性的时候,有个词需要特别注意:当你在阅读规范的时候,你会注意到客户端SHOULD(应该)在发送的请求中包含User Agent信息。在规范中,这是一个非常明确的声明。事实上,SHOULD在IETF的说明中是作为关键字列出的。针对关键字的含义,也有一个规范说明,你可以在http://tools.ietf.org/html/rfc2119上看到这段声明。对于SHOULD这个关键字,有如下规定:
……现实中确实存在一些特殊的情况,在这些特殊的情况下需要忽略一个特殊的选项,但是必须可以理解全部的含义,并且在选择一种不同的方式之前,一定要权衡利弊。
毫不掩饰地说,这句话也就是说,客户端没有义务使用User Agent字段或者使用正确的User Agent正确标识它们自己。如果用户愿意,它甚至可以伪造User Agent进行欺骗。机器人或者爬虫经常导致一些意想不到的结果。但是这些都是非正常的情况,当我们针对普通用户进行开发的时候,相信这些User Agent的信息并不会有什么不妥之处。使用User Agent最大的痛点就是你要和所有新发行的设备保持同步,并且可以将User Agent和已知的特性和效果集联系起来。这也是为什么我们需要一个设备检测服务的原因。
设备检测服务
如果我们的客户端设备只是一些已知的设备,那么前面一个例子完全可以说明问题了。但是如果我们想检查客户端的特征和尺寸大小,该怎么做呢?我们可以根据User Agent自定义一张客户端能力对应设计表,这样我们就可以在这张定义的设计表里搜寻;或者使用一个更高级的服务,这个服务为我们提供了表和查询的能力。
设备检测服务,通过传递请求,可以为我们提供获取客户端能力的功能。这种解决方案的架构如图4-7所示,当客户端通过网络发送请求时,我们的服务接收到这个请求,在服务器层我们开启一个后门调用设备检测服务。
0407.tif{1208}图4-7 在服务器端使用设备检测服务
使用最早也是最广泛的一个设备检测服务就是Wurfl。
Wurfl
Wurfl全称是无线通用资源文件(Wireless Universal Resource FiLe)。在2011年之前,它是一个列出了设备对应相应能力的免费开放的XML文件,其结构如下所示。
<device id="generic_android_ver3_0" user_agent="DO_NOT_MATCH_ANDROID_3_0" fall_back="generic_android_ver2_4">
<group id="product_info">
<capability name="is_tablet" value="true"/>
<capability name="device_os_version" value="3.0"/>
<capability name="can_assign_phone_number" value="false"/>
<capability name="release_date" value="2011_february"/>
</group>
<group id="streaming">
<capability name="streaming_preferred_protocol" value="http"/>
</group>
<group id="display">
<capability name="columns" value="100"/>
<capability name="physical_screen_height" value="217"/>
<capability name="dual_orientation" value="true"/>
<capability name="physical_screen_width" value="136"/>
<capability name="rows" value="100"/>
<capability name="max_image_width" value="980"/>
<capability name="resolution_width" value="1280"/>
<capability name="resolution_height" value="768"/>
<capability name="max_image_height" value="472"/>
</group>
<group id="sms">
<capability name="sms_enabled" value="false"/>
</group>
<group id="xhtml_ui">
<capability name="xhtml_send_mms_string" value="none"/>
<capability name="xhtml_send_sms_string" value="none"/>
</group>
</device>
从2011年开始,Wurfl的开发者们创建了自己的公司Scientiamobile,开始围绕Wurfl提供服务,并且停止了支持个人消费方面的开放文档。他们围绕着Wurfl提供了一系列的产品,包括Wurfl Cloud——提供API访问设备数据库; Wurfl Onsite——本地安装的设备数据库;Wurfl Infuze——在服务端通过环境变量保证Wurfl数据库的可用性。
理论上,最好的解决方案就是Wurfl Infuze,因为当查询设备数据的时候没有产生文件I/O和传输延迟的额外消耗。然而门槛最低的解决方案是WurflCloud,因为它不需要内部主机,不需要安装基础设施,甚至有免费的选项。基于这些原因,我们在本章剩下的内容中讨论如何在我们的应用中集成Wurfl Cloud。
在开始前,我们应该先到Scientiamobile的主页http://www.scientiamobile.com/上查看相关的信息,图4-8是Scientiamobile网站主页的截图。
0408.tif{1238}图4-8 Scientiamobile主页
我们可以点击页面底部的Wurfl Cloud,然后会被导航到价格页面。不用担心,在Sign Up链接下面有个免费选项,点击以后,我们会被导航到图4-9所示的页面。在这个页面我们可以创建我们自己的账号。详细信息请移步http://bit.ly/1x34Psg。
0409.tif{1156}图4-9 注册一个新的账号
在建立账号以后,你需要获取一个API key,你可以在Account Settings页面上获取key,如图4-10所示。
0410.tif{1245}图4-10 在Account Settings页面上配置账号
在Account Settings(账户设置)页面,同样可以选择你需要测试哪一种设备能力(免费版本只提供5个选项)。你可以从可用能力列表中选择需要的,然后拖拽它们到自己的能力列表中,这些列表也决定了如何在你的代码中引用,如图4-11所示。
0411.tif{1245}图4-11 从Wurfl Cloud中选择你需要的能力
最后,你需要下载Wurlf Cloud的客户端,然后根据你使用的语言开始写代码,在写本书的时候,Wurfl客户端代码支持下面的语言和技术。
-
Java
-
PHP
-
Microsoft.Net
-
Python
-
Ruby
-
Node.js
-
Perl
图4-12展示了Wurfl Cloud客户端的下载界面。
0412.tif{1238}图4-12 根据你的项目情况选择Wurfl Cloud客户端
Wurfl Cloud客户端以一个ZIP文件的形式下载下来,它包含了一些可以在项目中使用的可以和Wurfl Cloud交互的类。
示例代码
下面让我们创建一个使用Wurfl Cloud的应用。在开始编码之前,先假设有如下场景。
你的项目使用的是Node.js,所以应该选择下载Node.js客户端。在下载了Wurfl Cloud客户端ZIP文件以后,解压缩到你的工程可以访问的路径上。像大多数的Node.js应用一样,你已经准备好server.js用来接收请求了,同样,已经准备好router.js路由对应的请求了。index.js,把server.js和应用逻辑(命名为responsiveApp.js)整合起来。下面就是index.js的内容。
var server = require('./server/server.js');
var router = require('./server/router.js');
var responsiveApp = require("./responsiveApp.js");
var handle = {}
handle["/"] = responsiveApp.start;
handle["/start"] = responsiveApp.start;
handle["/favicon.ico"] = responsiveApp.favicon;
server.start(router.route, handle);
Index.js加载了server.js和router.js,同时也加载了responsiveApp.js(即使你还没有创建它)。创建了一个调用处理器的对象,它会被传递给服务器,服务器会指明如何根据不同的路由URL调用不同的处理函数。在这个例子中,我们只是简单地将所有的请求(除了请求favicon的)映射到responsiveApp.js中的start函数。最后,调用server.start启动。
server.start的作用只是创建一个事件处理器,当接收到HTTP请求的时候就触发事件。随后事件处理器就将请求传递给router.js。在这里,router.js会做一层逻辑判断,先检查请求,然后根据handler对象调用相应的处理函数。
如何深入地讲解Node.js,就超出了本书的范围。如果你想深入了解Node.js,你可以阅读Shelley Powers编写的Learning Node一书。接下来,让我们在responsive-App.js中编码应用的逻辑。首先,加载HTTP模块。然后加载两个我们之前下载的主要文件(如WurflCloudClient.js和Config.js)。
var http = require('http');
var wurfl_cloud_client = require("./NodeWurflCloudClient/WurflCloudClient");
var config = require("./NodeWurflCloudClient/Config");
接下来,我们创建start函数,在这个函数体里我们只调用了一个函数——getCapabilities,同时,如果我们有favicon,那么我们就创建一个favicon函数用作返回我们的favicon。
function start(response, request) {
getCapabilities(response, request);
}
function favicon(response) {
response.writeHead(200, {
'Content-Type': 'image/x-icon'
} );
//write favicon here
response.end();
}
现在我们来看看这个函数的具体逻辑。我们创建了一个getCapabilities函数,将start函数中的两个参数请求和响应也传递给这个函数。
function getCapabilities(response, request) {
}
接下来我们需要创建两个变量:一个对象——result_capabilities;另一个是数组——request_capabilities。request_capabilities数组存储了我们想要检查的功能。那这些功能从哪里来的呢?这就是我们之前在Wurfl账号里配置的功能。
function getCapabilities(response, request) {
var result_capabilities = {};
var request_capabilities = ['is_smartphone','is_tablet','is_touchscreen', 'is_wireless_device']
创建一个名为api_key的变量,可以从Wurfl的配置界面获取,这个值也是我们使用API的凭证。再创建一个名为configuration的变量,这个变量存储着调用config.WurflCloudConfig返回的值。
var api_key = "XXXXX ";
var configuration = new config.WurflCloudConfig(api_key);
下面我们将初始化一个WurflCloudClient的实例,WurflCloudClient的构造函数有三个构造参数:request、response和之前已经初始化的configuration实例,我们把这个实例命名为WurflCloudClientObject。
这个对象是我们访问Wurfl并获取能力的关键。接下来我们需要调用这个对象的detectDevice方法,给这个对象传递三个参数:请求、request_capabilities和一个在查询结果返回时需要触发的回调函数。
WurflCloudClientObject.detectDevice(request, request_capabilities,
function(err, result_capabilities){
这个匿名回调函数的业务逻辑是根据查询结果为我们定制适当的体验,并且渲染相应的HTML、CSS和JavaScript。在这个简化了的例子中,我们只是简单地调用函数然后输出正确的数据(drawSmartphoneHomepage等)。
基于这种方式,我们无需将所有和设备或者体验相关的代码都放到媒体查询和客户端中,我们仅仅在服务器端返回和设备或者体验相关的代码。
if(err!=null){
console.log("<br>Error: " + err + “ <br/>”);
}
else{
if(result_capabilities['is_smartphone']){
drawSmartphoneHomepage(response);
}else if(result_capabilities['is_tablet']){
drawTabletHomepage(response);
}else{
drawDesktopHomepage(response);
}
}
作为参考,整个示例代码如下面所示。
var http = require('http');
var wurfl_cloud_client = require("./NodeWurflCloudClient/WurflCloudClient");
var config = require("./NodeWurflCloudClient/Config");
function start(response, request) {
getCapabilities(response, request);
}
function favicon(response) {
response.writeHead(200, {
'Content-Type': 'image/x-icon'
} );
//write favicon here
response.end();
}
function getCapabilities(response, request) {
var result_capabilities = {};
var request_capabilities = ['is_smartphone','is_tablet',
'is_touchscreen', 'is_wireless_device']
var api_key = "XXXXX ";
var configuration = new config.WurflCloudConfig(api_key);
var WurflCloudClientObject = new wurfl_cloud_client.Wurfl-CloudClient(configuration, request, response);
WurflCloudClientObject.detectDevice(request, request_capabilities,
function(err, result_capabilities){
console.log(result_capabilities);
if(err!=null){
console.log("<br>Error: " + err + “ <br/>”);
}
else{
if(result_capabilities['is_smartphone']){
drawSmartphoneHomepage(response);
}else if(result_capabilities['is_tablet']){
drawTabletHomepage(response);
}else{
drawDesktopHomepage(response);
}
}
});
}
exports.start = start;
exports.favicon = favicon;
exports.getCapabilities = getCapabilities;
4.4 缓存的影响
在开发大型网站的时候,我们常常会依赖于缓存减轻我们的服务器压力。当我们把响应式移到服务端但又缓存了响应时,问题便出现了。不管从客户端传来什么样的User Agent信息,我们将只看到当前缓存的版本而无法看到最新的响应。
为了解决这个问题,我们可以在发送响应的时候使用Vary这个HTTP响应header参数。当有请求进来的时候,服务端会基于User Agent的值做一些判断,然后决定哪些响应需要缓存起来。
{提示!}
在本书创作期间,大多数的CDN在使用Vary Response header的时候都不会缓存。如果你的CDN也是这样的,你需要针对这种情况有相应的替代方案。可能需要使用Edge Side Includes将User Agent的检查逻辑移到CDN的边缘层。
4.5 Edge Side Include
使用类似于Akamai这样的CDN来缓存内容是一种非常好的方式,它可以有效减少服务器的传输压力,这也就意味着可以有效减少维护的硬件,从而降低成本。不仅如此,CDN可以让内容更快地传递给用户。图4-13展示了这种模式下的整体架构图。
0413.tif{956}图4-13 通过边缘网络传输缓存内容
就在一个月以前,我们的CDN运营商不允许我们缓存User Agent相关的内容(再一次说明一下,是使用Vary HTTP header),所有的客户端得到的都是同一份缓存,并不是设备相关的内容。所以解决这种问题的方法就是使用Edge Side Include(ESI)语言。ESI由一些大公司制定的,包括Akamai和Oracle,并且已经提交给了W3C。你可以在http://bit.ly/1rY5WUO获取ESI的详细规范。
ESI是一种标识语言,嵌入到了HTML文档中。EDGE服务器都有一个ESI处理逻辑,用来读取ESI标签,解析逻辑,把输出渲染到HTML中。和PHP类似,ESI非常类似一种服务端脚本语言,它可以在服务端进行解析,然后输出到HTML中。而且和PHP更相似的是,ESI标签不会在客户端展示出来,它只会展示渲染以后的内容。
下面的代码是ESI的示例脚本,根据请求的User Agent数据,加载合适的内容。
<html>
<head></head>
<body>
<esi:choose>
<esi:when test="$(HTTP_USER_AGENT{'os'})=='iPhone'">
<esi:comment text="Include iPhone specific resources here" />
…
</esi:when>
<esi:when test="$(HTTP_USER_AGENT{'os'})=='iPad'">
<esi:comment text="Include iPad specific resources here"/>
…
</esi:when>
<esi:when test="$(HTTP_USER_AGENT{'os'})=='Android'">
<esi:comment text="Include Android specific resources here" />
…
</esi:when>
<esi:otherwise>
<esi:comment text="Include desktop specific resources here" />
…
</esi:otherwise>
</esi:choose>
</body>
</html>
4.6 小结
本章从多个角度全面地探讨了我们的应用程序。从应用底层探讨了协议和软件栈,并且探讨了在整个请求流程中,我们应用有哪些步骤需要处理。站在一个更高的视角上,我们不得不问自己一个问题:如何尽快从最早的请求中获取客户端设备功能?最重要的是如何尽快对响应进行处理?
为了回答这个问题,我们重点对请求的HTTP请求中的User Agent进行检测,甚至使用了第三方的设备检测服务,比如Wurfl。
不过这个解决方案的一个潜在风险就是如何处理缓存内容。一个解决方式就是使用响应header中的Vary参数。缓存服务器通过User Agent决定哪一种响应需要缓存起来。另外一种方式就是通过使用ESI,把我们的设备或者能力检测逻辑从我们的服务器移到CDN edge服务器上。
不管哪一种解决方案,只要我们将响应式的处理在服务端解决,避免客户端加载所有的响应式相关代码,避免提供两份相同内容或者无效内容的反模式,就可以减少向客户端传送的负载,取而代之的是一种更精简、更合理的响应方式,所以这种实现方式更好。而做这些复杂技术和架构的原因就是,这种实现方式对带宽、电池寿命、CPU限制等这些终端用户的设备存在的种种限制,都是一种有效的优化手段。
本文摘自:响应式Web设计性能优化
内容简介:
《响应式Web设计性能优化》展示了如何在你的项目计划中将响应性和性能很好地结合起来,在服务器端,使用Node.js提供设备专有功能, 并且将自动测试整合到持续集成环境中。而且你将学习到很多非常有用的工具和响应式框架。随着时间的推移,你将从Tom Barker宝贵的经验中领悟到响应式设计的真谛。对于前端Web开发工程师来说,本书非常值得阅读。
网友评论