HTTP 头和 PHP header() 函数

http://unifreak.github.io/translation/Http_headers_and_PHP_header()_function

 

引言

许多初级到中级的的 PHP 程序员把 header() 函数看成某种神秘巫术. 他们能够照着代码示例把功能实现, 可是仍是不知道到底它是若是运做的. 我最开始就是这样的.php

实际上它很是简单. 在这篇文章中, 我会解释 HTTP 头(header) 是如何运做的, 它们与 PHP 的关系, 以及它们的 meta 标签 equivalents(对应物)html

但愿你读完以后, 能更顺手的使用 header() 函数, 甚至想出一些更多利用它的地方. 咱们也会讲到其余一些关于 HTTP 和 PHP 的重要话题. 可是在咱们开始讲任何程序相关的东西以前, 咱们须要先快速(而且不完整的)过一遍 HTTP (HyperTex Transfer Protocol) 运做原理linux

HTTP 概览

Headers: 对话中的词语

HTTP 是 web 服务器和客户端浏览器之间的数据传输(好比 web 页面中的 HTML, 图片, 文件)协议(‘规则’集合), 而且一般使用 80 端口. 这就是网站 URL 前面 ‘http://’ 的来源git

不少人最开始制做 web 页面的时候, 他们先在本地电脑上写 HTML, 在本地浏览器查看是否符合预期, 而后上传到服务器, 就能够在网上浏览这些页面了. 看起来好像在不管在本地查看与在服务器上查看的页面都同样, 传输的数据只有这些 HTML 以及它包含的图片. 可是实际上还有另一些许多你没看到的信息 - 头信息.程序员

头信息能够分为两大类: 你浏览器向服务器请求文件时发出的请求头信息, 服务器提供文件给浏览器时发出的响应头信息. 把这些头信息看成浏览器和服务器对话时的词语. 我喜欢把服务器想象为图书管理员, 把浏览器想象成正在请求图书资源的学者. 浏览器走向位于服务台 (80 端口) 的服务器, 说道, “Hi, 我是 Mozilla, 我正在找这个编目号是 ‘www.expertsrt.com’ 的资源. 你能够帮我找到它吗?” 服务器听到后回应 “是的, 我找到了, 让我把它给你. 这里面是 HTML 文本, 它写的是 ‘<html>...’” 浏览器开始从头至尾的读它, 而且遇到了一个图片标签, 因此向服务器要位于 src 属性指定处的图片. 服务器进行查找, 找到这个文件而后说道 “这是个 PNG 图片, 它的数据是…” 你懂的.github

另外一个对话可能像这样:web

浏览器: Hi, 我是 Mozilla, 能给我在 ‘www.expertsrt.com/moved.html’ 这里的文件吗?.sql

服务器: 那个文件已经不在那儿了, 他如今在 ‘www.expertsrt.com/newloc.html’.apache

浏览器: Hi, 我是 Mozilla, 能给我在 ‘www.expertsrt.com/newloc.html’ 这里的文件吗?编程

服务器: 我找到这个文件了. 查看它 10 秒钟而后再向我问一次. 它是一个 HTML 文本文件, 它有这些内容…

…10 秒钟…

浏览器: Hi, 我是 Mozilla, 能给我在 ‘www.expertsrt.com/newloc.html’ 这里的文件吗?

服务器: 我找到这个文件了. 查看它 10 秒钟而后再向我问一次. 它是一个 HTML 文本文件, 它有这些内容…

…10 秒钟…

浏览器: Hi, 我是 Mozilla, 能给我在 ‘www.expertsrt.com/newloc.html’ 这里的文件吗?

服务器: 我找到这个文件了. 查看它 10 秒钟而后再向我问一次. 它一个 HTML 文本文件, 它有这些内容…

…诸如这般, 直到浏览器被用户从新定向…

正如你所看到的, 使用头信息能够控制许多事情. 使用 header() 函数, 你可让服务器发送你所需的头信息, 这样你能够作除了发送 HTML 以外许多很酷的事情.

看看整个对话过程

在继续以前, 让咱们先不使用浏览器来查看一个 web 页面, 这样咱们能够看到整个对话, 更好的了解 HTTP 头的工做. 先打开命令行 (在 windows 中, 点击开始菜单->运行, 输入 cmd, 而后点击 “OK”…若是你正使用 linux, 你或许已经知道怎么打开了). 在命令行中输入:

telnet expertsrt.com 80

而后回车. 这会连接到 expersrt.com 的 80 端口. 而后, 复制并粘贴下面的文字:

GET / HTTP/1.1
Host: expertsrt.com

若是你输入或粘贴这些文字的时候, 命令行除了光标的闪烁没看到任何动静的话, 不要担忧 – 它们确实被发送到服务器了. 第一行说明你使用 GET 请求方法去获取资源 / (这里是目标主机上基目录里的文件), 而且你在使用 HTTP 1.1 版本. 第二行告诉服务器你想要链接到哪台主机. 当你输入 ‘expertsrt.com’ 后, 回车两次 (只需两次). 你应当马上获得相似下面的响应:

HTTP/1.1 301 Moved Permanently
Date: Wed, 08 Feb 2006 07:44:07 GMT
Server: Apache/2.0.54 (Debian GNU/Linux) mod_auth_pgsql/2.0.2b1 mod_ssl/2.0.54 OpenSSL/0.9.7e
Location: http://www.expertsrt.com/
Content-Length: 233
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>301 Moved Permanently</title> </head><body> <h1>Moved Permanently</h1> <p>The document has moved <a href="http://www.expertsrt.com/">here</a>.</p> </body></html> 

哎呀! 看起来好像咱们请求的文件已经不在那儿了; 它已经被移到新的地方了: http://www.expertsrt.com. 若是你使用浏览器, 你只会看到 HTML - 在第一个空白行以前的都是头信息. 实际上, 现代浏览器比这更智能 - 当他们看到第三行的新的 URL 时, 会自动转向那里, 这样你就不用手动再输入这个新的 URL 了. 让咱们去这个新的 URL. 这时可能你已经断开链接了. 若是这样, 只需按向上键, telnet 命令会出现, 而后回车以从新链接. 若是你没有断开链接, 那直接输入下面的文字就行:

GET / HTTP/1.1
Host: www.expertsrt.com

而后回车两次. 你会看到另外一个相似的响应, 告诉你那个页面实际上在 http://www.expertsrt.com/index.php. 服务器真挑剔是否是? ;-) 重复上面的操做, 不过此次输入

GET /index.php HTTP/1.1
Host: www.expertsrt.com

注意咱们想要的文件名在第一行. 这一次咱们屏幕被文字刷满了: 这就是来自 ERT 主页的 HTML. 这里的头信息看起来是这样的

HTTP/1.1 200 OK
Date: Wed, 08 Feb 2006 08:20:07 GMT
Server: Apache/2.0.54 (Debian GNU/Linux) mod_auth_pgsql/2.0.2b1 mod_ssl/2.0.54 OpenSSL/0.9.7e
X-Powered-By: PHP/4.4.0
Transfer-Encoding: chunked
Content-Type: text/html

很简单是否是? 咱们来继续探讨这跟你编程有什么关系. 若是你不明白咱们讲到的全部事情也没有关系. 重要的是对浏览器和服务器如何交互的有个大体印象, 以及意识到并无什么魔法在里面. 最终就是这些

  1. 浏览器和服务器经过使用头信息来进行交互
  2. 头信息在主要内容以前发送, 而且用两个 CRLF/换行符 来和主要内容分割开
  3. 在头信息部分, 每一行就是一个头. 首先是头的名字, 而后是一个冒号一个空格, 而后是这个头的名/值

    头名: 头值

  4. 头信息能够包括许多类型的信息和指示, 以便浏览器和服务器用来告知对方接下来该作什么

提示: 若是你是那种刨根问底的人, 你能够看看 RFC 2616, 那是 HTTP/1.1 的完整规范. 尤为是 14 章, 包含每个头的完整定义

PHP header(): 基础

注意在咱们最终获得的主页中的 X-Powered-By: PHP/4.4.0 和 Content-Type: text/html 这两个头信息. PHP 一开始就被设计成输出 HTML ( PHP 中的 H 即表明 ‘Hypertext’), 而且在 PHP 脚本第一次生成输出(好比, 使用 echo)时, 会自动为你包含这些头信息. 这很是方便, 但也形成许多 PHP 新手对头信息的困惑 - 在像 Perl 这样不是一开始就被设计成用于 web 开发的语言中, 不包含你本身的头而直接发送输出会产生 ‘500 Internal Server Error’ 错误, 因此 Perl 的 web 开发者不得不当即学习关于头信息的知识

header() 函数发送 HTTP 响应头信息, 并且只作这件事

使用这个函数, 你可让你的脚本发送你选择的头信息给浏览器, 创造一个很是有用的动态结果. 可是, 你须要知道关于 header() 函数的第一件事就是你必须在 PHP 发送任何输出(这会使 PHP 自动发送默认的头信息)以前使用它

我怀疑有哪一个 PHP 程序员没有见到过以下的错误消息

Warning: Cannot modify header information - headers already sent by.....

如咱们所说的, 响应头信息用一个空白行和主要内容分割. 这意味你仅能够发送头信息一次, 若是你的脚本有任何输出 (即便一个在 <?php 标签以前的空白行或空格), PHP 就会自动发送头信息. 例如, 看一下下面这个脚本, 看起来逻辑上很正常:

Welcome to my website!<br /> <?php if($test){ echo "You're in!"; } else{ header('Location: http://www.mysite.com/someotherpage.php'); } ?> 

这个脚本判断 $test 是否为 true, 若是不是则使用 Location 头重定向访问者. 看到问题所在了吗? ‘Welcome…’ 文字始终会发送出去, 因此默认的头信息会自动被发送. 在调用 header() 时已经太晚了: 用户只看到一条错误消息 (若是你把错误报告关掉了, 则只会看到 ‘Welcome…’ 文字), 而不是被重定向

基本上有两种解决方法. 第一个就是重写代码

<?php if($test){ echo 'Welcome to my website<br />You're in!'; } else{ header('Location: http://www.mysite.com/someotherpage.php'); } ?> 

第二个就是使用输出缓冲, 这个解决方法更为优雅易用. 在咱们上面的例子中, 重写代码并不困难, 可是试想一下若是有不少 HTML 须要移动位置 - 这样作就会很麻烦, 也会让咱们的代码更难追踪. 虽然咱们第一个示例致使了错误, 可是逻辑上是没错的. 输出缓冲可让你一直保留(‘缓冲’)输出(即便是 PHP 代码以外的 HTML)直到你明确指示了把输出发送给浏览器. 这样你就能够随意编写你的代码, 知道你指定了你须要指定的头信息, 而后明确指示发送这些输出. 两个相关的函数是 ob_start()ob_flush(), ob_start() 用于打开输出缓冲, ob_flush() 会发送缓冲了的输出:

<?php ob_start(); //开始输出缓冲 ?> Welcome to my website! <?php if(true){ echo "You're in!"; } else{ header('Location: http://www.mysite.com/someotherpage.php'); } ob_flush(); //输出缓冲中的数据 ?> 

我鼓励你读一下全部关于输出缓冲的函数, 很是有用. 你应当尽早的把输出缓冲发送出去, 尤为当你有许多东西想要发送的时候. 不然你的页面会看起来加载的很慢, 由于全部的内容只有被组装完毕后才发送, 而不是当可用的时候当即就被发送出去.

提示: 第二个参数 若是你调用 header() 不止一次发送同一个头, 这个头的值将会是_最后_调用的 header() 中设置的值. 如,

<?php header('Some-Header: Value-1'); header('Some-Header: Value-2'); ?> 

会产生 Some-Header: Value-2 这个结果. 你能够经过设置第二个参数来发送同一个头两次. 这个参数默认是 true. 若是你设置其为 false, 那么第二个头值不会替换第一个, 而是两个都被发送. 因此下面的代码

<?php header('Some-Header: Value-1'); header('Some-Header: Value-2', false); //不要替换第一个 ?> 

将会产生 Some-Header: Value-1, Value-2 这个结果. 你不多会用到这个, 可是知道它也不错.

知道了 HTTP header 和 PHP 如何配合以后, 让咱们来看一些更为具体的例子.

PHP header(): 一些例子

提示: 下面这个代码片段都是截取自完整的工做代码. 当你在本身的程序中包含他们的时候, 记得定义全部你本身的变量, 赋给他们默认值, 以及遵循其余最佳实践.

使用 Location 头重定向

咱们已经在上面看到过几回了: 它会重定向浏览器.

<?php header('Location: http/www.mysite.com/new_location.html'); ?> 

虽然你给它一个相对 URL 没准也能工做, 可是根据 HTTP 规范, 你真的应该使用一个绝对 URL.

一个容易犯的错误就是在使用了 Location header 以后不当即使用 exit 以结束执行 (你可能不是老是想要结束执行, 可是大部分时间是的). 之因此这是一个错误是由于 PHP 代码会继续执行, 即便用户已经被重定向到新的 URL. 在最好的状况下, 这会没必要要的使用系统资源. 在最坏的状况下, 你可能会执行一些让本身后悔的操做. 看一下下面的代码:

<?php //重定向访问级别低于 4 的用户 if (check_access_level($username) < 4){ header('Location: http://www.mysite.com/someotherpage.php'); } //向高于访问级别 4 的用户发送秘密邮件 mail_secret_code($username); echo 'The secret email is on its way!'; ?> 

未受权用户的确被重定向了, 可是由于代码会继续执行, 他们一样会收到邮件. 为了不这种状况, 针对已受权用户的代码能够写到 else{} 声明中, 可是直接在 header() 后面使用 exit 来结束代码执行会更为干净容易一些.

<?php //重定向访问级别低于 4 的用户 if (check_access_level($username) < 4){ header('Location: http://www.mysite.com/someotherpage.php'); exit; //中止代码执行 } //向高于访问级别 4 的用户发送秘密邮件 mail_secret_code($username); echo 'The secret email is on its way!'; ?> 

使用 Refresh 头重定向

RefreshLocation 同样能够重定向用户, 可是你能够延迟重定向. 例如, 下面的代码会在显示当前页面 10 秒钟后重定向用户到新的页面:

<?php header('Refresh: 10; url=http://www.mysite.com/otherpage.php'); echo 'You will be redirected in 10 seconds'; ?> 

另外一个常见的用途就是经过重复的’重定向’一个页面到它自身来强制更新页面 (参见上面的第二个 ‘对话’). 例如, 这里是一个简单的例子, 页面会从 10 开始向下数, 每一个数字之间有 3 秒间隔:

<?php if(!isset($_GET['n'])){ $_GET['n'] = 10; } if($_GET['n'] > 0){ header('Refresh: 3; url=' . $_SERVER['PHP_SELF'].'?n=' . ($_GET['n']-1) ); echo $_GET['n']; } else{ echo 'BLAST OFF!'; } ?> 

提示: 若是刷新时间设置成 0, 则 Refresh 头实际上和 Location 头彻底同样

使用 Content-Type 头来提供不一样类型的文件以及生成动态内容

服务器用 Content-Type 头告诉浏览器本身将要发送什么类型的数据. 使用这个头信息, 你可让 PHP 脚本输出任何类型的文件, 从纯文本文件到图片文件到 zip 文件等等. 下面的表格列举了最经常使用的一个 MIME 类型:

经常使用 MIME 类型:

类型 描述
text/html HTML (PHP 默认)
text/plain 纯文本
image/gif GIF 图片
image/jpeg JPEG 图片
image/png PNG 图片
video/mpeg MPEG 视频
audio/wav WAV 音频
audio/mpeg MP3 音频
video/mov mov 视频
video/quicktime Quicktime 视频
video/x-ms-wmv Windows WMV 视频
audio/x-ms-wma Windows WMA 音频
audio/x-realaudio RealPlayer 音频/视频 (.rm)
audio/x-pn-realaudio RealPlayer 音频/视频 (.ram)
video/x-msvideo ms 视频
video/avi AVI 视频
application/pdf PDF 文档
application/msword MS Word .doc 文件
application/zip Zip 文件
application/octet-stream 其余. 数据. 用于强制下载或使用应用打开.*
x-foo/x-bar 其余. 数据. 用于强制下载或使用应用打开.*

你能够用此来作一些有趣的事情. 好比, 你可能想要向用户发用一个预先格式化过的文本文件, 而不是 HTML:

<?php header('Content-Type: text/plain'); echo $plain_text_content; ?> 

另或者你想要提示用户下载文件, 而不是在浏览器中查看它. 使用 Content-Disposition 头, 这很容易, 你甚至能够推荐一个文件名给用户:

<?php header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; ' .'filename="plain_text_file.txt"'); echo $plain_text_content; ?> 

另或者你须要提供文件文件, 可是又但愿隐藏文件的真实路径和名字, 而且只让已登陆的用户下载:

<?php if($b_is_logged_in){ header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; ' .'filename="'.$different_filename.'"'); readfile('/path/to/files/' . $filename); } else{ echo 'You are not authorized to view this file'; } ?> 

又或者你已经使用 PHP 的图片函数动态生成了一个图片, 想要展现给用户. 你能够建立一个 build_image.php 文件, 像这样

<?php //生成图片, 而后 header('Content-Type: image/jpeg'); imagejpeg($image_resouce); ?> 

提示: 小心 magic_quotes! PHP 会自动使用反斜杠转移特殊字符, 这一开始看起来是个好主意, 可是大多数好的程序员认为 (a) 这会鼓励不去验证输入的粗心代码, 而且 (b) 在良好的代码中会产生本不应有 (若是 magic_quote 关闭) 的麻烦. 其中一个麻烦就是二进制数据被破坏. 在上面这个例子中, 若是 magic_quotes_runtime被启用, 则 readfile() 输出的数据可能被添加反斜杠, 致使发送给用户的文件被破坏. 完美状况下, 你应该在 php.ini 文件中关闭 magic_quotes_runtime 选项, 可是若是你没有权限访问这个配置文件, 你可使用 set_magic_quotes_runtime() 函数 (给它传个数字 0) 关闭它.

使人高兴的是, 最近的一次 PHP 开发者会议显示, 在将来版本(6+) 的 PHP 中 magic quotes 会被弃用. 可是在全部人升级到这个版本的 PHP 以前, 记住这个致使的问题会节省你不少麻烦和疑问.

你能够在 URL 中传递生成图片所需的参数, 而后使用 $_GET 获取它们. 而后在另外一个页面, 你可使用 img 标签来包含这个图片:

<img src="build_image.php<?php echo "?$user_id&amp;$caption"; ?>"> 

可用的地方几乎讲不完. 你 PHP 变成越多, 越会发现 Content-Type 头真的是你的好朋友

提示: 浏览器处理各式 MIME类型 的_预期_方式以及_实际_方式可能并不一致 (尤为是 Internet Explorer), 因此你最好是在你须要支持的浏览器中测试一下. PHP 参考中的用户评论有许多关于此的技巧.

防止页面缓存

PHP 页面一般会生成很是动态的内容, 为了防止用户由于页面缓存而错过了更新过的页面, 告诉浏览器不要缓存特定的页面一般很是有用. 下面的代码在可能会访问你网站的浏览器中工做的很好:

<?php header('Cache-Control: no-cache, no-store, must-revalidate'); //HTTP/1.1 header('Expires: Sun, 01 Jul 2005 00:00:00 GMT'); header('Pragma: no-cache'); //HTTP/1.0 ?> 

Expires 头能够是任何已通过去的日期. 对于 MIME 类型, 浏览器 (尤为是较老的) 可能不会老是正确的理解你的缓存指示 (虽然大部分现代浏览器会).

其余应用

还有另一个可使用头信息的地方, 好比设置 HTTP 响应码, 或者执行 HTTP 认证 (若是你做为 Apache 模块来使用 PHP 的话). 如今, 你了解了 header() 如何工做及怎么使用它, 你能够用它作你以前想都没想到的许多事情了.

PHP 中的请求头信息

咱们讲了怎么使用响应头信息了. 咱们还能够从浏览器发给服务器的请求头信息中获取不少信息. 有两种方法来获取. 第一, 许多 [$_SERVER数组][server]中的值都是由传来的请求头信息决定的. 第二, 若是 PHP 是做为 Apache 模块使用的, apache_request_headers() 会返回一个包含全部请求头信息的数组 (甚至那些不在 $_SERVER 中的).

安全第一: 不要信任请求头信息

由于请求头信息发自浏览器, 浏览器又能够在客户端被控制, 因此你永远不要信任来自请求头, 又和你站点安全紧密相关的头信息. 一个很好的例子就是 $_SERVER['HTTP_REFERER']变量, 这个变量应该包含一个用户转自的源 URL. 一个新手的常见错误就是认为他们可使用这个来确保用户只会经过特定路径来访问页面, 所以他们便无需关心服务器端的数据验证. 例如, 看看下面的代码, 它试着去确保数据是从一个特定的页面发送过来的, 而不是从另外一个站点

<?php if($_SERVER['HTTP_REFERER'] != 'http://www.mysite.com/myform.html'){ header('Refresh: 5; url=http://www.mysite.com/myform.html'); echo 'You must use the form on my site...redirecting now.'; } else{ insert_data($_POST['var1'], $_POST['var2']); } ?> 

这或许会阻止那些不是很精通的黑客经过他的浏览器提交一个自定义的表单来提交数据, 可是任何一个稍微高深一些的黑客均可以经过使用 telnet 来提交数据, 包括请求头信息

Referer: http://www.mysite.com/myform.html

而后轻易的躲过这层保护机制. 这里所要讲的重点是: 使用 HTTP 请求头信息来统计一些数据以便提供更好的用户体验 - 大部分的请求头信息都是发自真实的浏览器并且能够被信任…可是不要在有关安全的问题上依赖任何请求 header

使用 HTTP 请求头信息

你能够用它作几件事. 使用 $_SERVER['HTTP_USER_AGENT']你能够探测用户生成他使用的什么浏览器. 你能够检查 $_SERVER['HTTP_ACCEPT_LANGUAGE'] (可能要配合 $_SERVER['HTTP_ACCEPT_CHARSET'] 和一些 IP 地理位置 ) 来决定向用户展现什么语言. 虽然 $_SERVER['HTTP_REFERER'] 对于安全目的并不能被依赖, 可是能够用来统计你网站的流量, 或者根据用户的访问路径来定制显示内容. 若是由于某些缘由你想要操做原始的请求字符串, 你可使用 $_SERVER['QUERY_STRING']. 查看 $_SERVER['REQUEST_METHOD'] 能够知道你的页面是经过 GET 仍是 POST 方法访问的. 还有不少能够帮助你作许多有创意的事情的信息等着你去发现.

HTML meta 标签中的 HTTP 头信息 equivalents(对应物)

颇有可能在阅读本文以前, 你已经用过下面的 HTML meta 标签重定向用户了:

<meta http-equiv="refresh" content="0;http://www.mysite.com/somepage.html" /> 

看起来很熟悉? ‘http-equiv’ meta 标签即 HTTP 响应头的’对应物’, 引入它们是为了让没有服务器端编程能力的人在写 HTML 页面的时候也能使用强大的头信息功能. 使用这些 meta 标签很简单: 它们能够被放在文档 <head> 中的任何地方, http-equiv 属性包含头名, content 属性包含头值.

我发现这些 meta 标签最开始也会和 HTTP 头同样让人困惑, 可是如今它们在你看来应该很简单了. 虽然我更喜欢使用 PHP 的 header() 函数, 可是 meta 标签的 HTTP equivalents 对于像指定字符集这样的事情会更顺手一些. 好比, 我常常在 HTML 页面中使用 (有时候 PHP 页面中也会用到):

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 

提示: 使用 meta 标签来指定头信息并不被一致的支持, 因此一般来说使用头信息自己会更加安全和快速. 另外很明显, 还有一些头的名值对并不能用 meta equivalents 来指定: 在真正的头信息被发送, 浏览器已经把文档读取为 HTML 以后, 你是不能再去设置 Content-Typeimage/png 的 ;-)

结语

如今咱们讲完了, 你应该对 HTTP 的工做原理以及如何使用响应请求头信息以及如何把它们应用到本身的代码中有了很好的认识. 这些知识也会让你在 web 应用的效率和安全方面有更审慎的思考. 我但愿在你继续编程的时候, 会发现你使用 HTTP 头信息更加顺手了, 也能经过使用它们让你的工做更加轻松, 你的页面更好了.

还有额外一点, 记住头信息就像是词语: 它们交流信息并请求某些操做被执行, 可是自己并不强制任何事情. 99.9% 的状况下, 浏览器和服务器和谐合做, 事情发展很顺利. 但记住在现实中, 是否是你会遇到一些混蛋 (黑客), 或者一些只想按照本身意愿作事的东西 (Internet Explorer). web 开发从不少角度讲是一个客服性质的工做, 因此你应该尽全力避免这些东西, 知足客户的 ‘特殊须要’ :-)