Apache服务器在内容协商方面提供了一个不太起眼但非常实用的功能,就是Type Map文件,中文常称为类型映射文件。它的作用是告诉服务器:同一个资源存在多个不同版本,比如同一个页面有中文版、英文版和日文版,服务器可以根据浏览器发送的Accept-Language、Accept-Charset等请求头,自动挑选最合适的版本返回给客户端。相比用PHP或者Java在后端写一堆判断逻辑,Type Map只需要一个文本文件就能完成这件事,配置简单、维护成本低。本文将从基本概念、文件格式、服务器配置以及与MultiViews的对比几个方面,详细讲解Type Map的用法。

Type Map文件的基本格式与语法规则
Type Map文件本质上是一个纯文本文件,遵循RFC 822的邮件头格式,有点像HTTP响应头的写法。一个典型的Type Map文件包含多个记录块,每个块描述一个资源变体,块与块之间用空行分隔。每个块由若干个指令组成,常见的指令包括URI、Content-type、Content-language、Content-encoding和qs。
下面是一个完整的例子,假设网站首页有三个语言版本:
URI: index.zh.html URI: index.en.html Content-type: text/html Content-language: en qs: 0.9 URI: index.zh.html Content-type: text/html; charset=UTF-8 Content-language: zh qs: 1.0 URI: index.ja.html Content-type: text/html; charset=UTF-8 Content-language: ja qs: 0.5
第一块只写了URI,表示默认变体,当浏览器没有提供任何可匹配的语言偏好时,服务器会返回这个版本。其余每个块中,URI指明该变体对应的实际文件路径,路径是相对于Type Map文件所在目录的。Content-language声明该变体的语言,可以写多个语言,用逗号分隔,例如Content-language: zh, en表示该文件同时适用于中文和英文用户。qs是质量系数,取值范围0.0到1.0,用来表达服务端对不同变体的偏好程度,当多个变体都能匹配浏览器请求时,qs值高的优先级更高。
需要注意几个细节:指令名不区分大小写,但为了规范建议统一首字母大写;Content-type中可以带charset参数;如果变体文件经过了压缩,还要加上Content-encoding指令,例如Content-encoding: gzip。Type Map文件本身不能包含HTML内容,它只是映射描述文件,真正的页面内容存放在各个变体文件里。
如何在Apache中启用Type Map处理
Type Map文件默认是不会被服务器识别的,必须通过配置告诉Apache用什么处理器来解析它。最常见的方式是在httpd.conf、虚拟主机配置或者.htaccess文件中加入下面这行:
AddHandler type-map .var
这行配置的含义是:所有以.var为扩展名的文件都交给type-map处理器处理。.var是Apache文档中惯用的扩展名,当然你也可以换成其他名字,比如.typemap,只要扩展名与AddHandler指令对应即可。配置完成后,假设网站根目录下存在index.var、index.zh.html、index.en.html三个文件,当用户访问http://www.ipipp.com/index.var时,服务器会读取index.var的内容,根据请求头进行协商并返回对应的语言版本。
如果希望用户直接访问目录就触发协商,可以利用DirectoryIndex指令,把var文件设为目录索引:
DirectoryIndex index.var index.html
这样用户访问http://www.ipipp.com/时,服务器会优先查找index.var并执行内容协商。另外要确认站点对应的目录中AllowOverride允许使用FileInfo,否则.htaccess里的AddHandler不会生效。修改配置后记得重启Apache或者等待htaccess重新加载。一个容易踩的坑是:如果把Type Map文件放在了没有执行权限的目录,或者变体文件的URI路径写错,服务器会返回406 Not Acceptable或者500错误,排查时应先检查var文件中的URI是否指向真实存在的文件。
还有一个实用的配置是CacheNegotiatedDocs指令。早期版本中协商得到的响应默认不会被代理缓存,加上CacheNegotiatedDocs on后,协商响应也可以被缓存,这对内容基本稳定的多语言站点能明显提升性能。
Type Map与MultiViews两种协商方式对比
Apache实现内容协商有两条路:一是本文讲的Type Map文件,二是mod_negotiation模块提供的MultiViews选项。两者最终目的相同,但工作方式差别很大,选择哪一种取决于站点规模和维护习惯。
MultiViews的启用方式是在Options指令中加上它:
<Directory /var/www/site>
Options +Indexes +MultiViews
</Directory>启用后,当服务器收到一个并未真实存在的文件请求,例如index.html,它会在目录中查找所有名为index的文件,根据扩展名推断内容类型和语言,然后自动协商。这种方式的好处是零维护,新增一个语言版本只要按命名规范放一个文件进去即可,不需要写映射文件。但缺点也很明显:扩展名的推断依赖AddLanguage、AddCharset等指令的配置,规则比较隐晦,出了问题不容易排查;而且目录下文件较多时,协商过程会有额外的文件系统开销。
Type Map则完全相反,所有变体关系都显式写在映射文件里,逻辑一目了然,qs质量系数也能精确控制每个版本的优先级,特别适合变体较多、需要精细控制的场景。它的缺点是每新增或删除一个版本都要手动编辑var文件。简单站点用MultiViews省事,正式项目或多语言版本复杂的站点更推荐Type Map。此外,两者的错误处理也不同,MultiViews找不到合适变体时可能返回404,而Type Map协商失败返回406,可以根据这个差异辅助判断问题出在哪一层。
浏览器语言匹配原理与常见问题排查
内容协商能否生效,最终取决于浏览器发送的Accept-Language头。例如Chrome中把语言偏好设置为中文优先,请求头大致是Accept-Language: zh-CN,zh;q=0.9,en;q=0.8。Apache会拿这个头与Type Map中每个变体的Content-language和qs做加权计算,综合得分最高的变体胜出。如果浏览器偏好的是没有对应变体的语言,服务器会退回默认变体或者qs最高的变体。
实际使用中有几个常见问题值得注意。第一,语言代码要写规范,zh与zh-CN在协商中会被区别对待,如果变体声明为zh而浏览器发送zh-CN,Apache可以匹配,但反过来声明zh-CN就无法匹配仅发送zh的浏览器,所以声明时建议用更宽泛的zh,或者同时提供两个变体。第二,qs不要随意全写成1.0,那样等于放弃服务端的偏好控制,应该给希望优先返回的版本更高的值。第三,协商结果会通过Vary响应头告知缓存设施,如果配合CDN使用,务必确认CDN正确处理了Vary: Accept-Language,否则可能出现不同语言用户拿到同一份缓存页面的情况。
调试时可以先绕开协商,直接访问各个变体文件确认它们本身能正常打开,再访问var文件观察返回的语言是否与浏览器设置一致。必要时用curl模拟不同的请求头:
curl -H "Accept-Language: en" -i http://127.0.0.1/index.var curl -H "Accept-Language: zh-CN" -i http://127.0.0.1/index.var
对比两次响应的Content-Language头,就能快速验证协商逻辑是否正确。掌握这套机制后,配合URL重写规则把var文件伪装成干净的路径,一个轻量、无后端依赖的多语言站点就搭建完成了。