Unicorn();3.0.0 beta
独角兽HTTP服务器(以下简称独角兽)是一个高级静态资源服务器系统,除了支持基本的静态文件服务外,还支持文件合并、服务端依赖管理等高阶功能。
入门
我们先把独角兽服务器环境搭建起来,以便接下来可以手把手地介绍如何使用独角兽。
独角兽是为天马HTTP中间件(以下简称天马)开发的一个功能模块,而后者又运行在NodeJS之上,因此环境搭建的过程非常类似编写一个NodeJS应用程序。
准备目录
工作目录
首先新建一个目录作为工作目录,然后在该目录下运行以下命令(目前独角兽还没有发布到NPM上,以下命令只是示意一下,暂时不要当真):
npm install pegasus npm install unicorn
最后,我们在工作目录下再新建一个app.js
文件。完成这些操作后,应该能得到以下目录结构:
- workdir/
- node_modules/
+ pegasus/
+ unicorn/
app.js
服务目录
然后再新建一个目录(例如/home/admin/deploy/
)作为服务目录,用于存放对外服务的静态资源文件。有了一个空荡荡的目录后,我们可以再放入一点文件(例如index.html
)。完成这些操作后,应该能得到以下目录结构:
- /home/admin/deploy/
index.html
编写代码
接下来,我们需要编辑app.js
,使用天马编写一个HTTP服务器,并配置和使用独角兽模块。
var http = require('http'),
pegasus = require('pegasus'),
unicorn = require('unicorn');
var router = pegasus()
.mount('/', unicorn('file:///home/admin/deploy/'));
http.createServer(router).listen(8080);
启动服务
虽然生产环境下的服务运行方式要更加复杂和周全,但我们在开发环境下暂时只需要在工作目录下运行node app.js
命令把服务跑起来。正常的话,服务目录里的文件应该能够被访问了:
=> GET http://127.0.0.1:8080/index.html
<= HTTP/1.1 200 OK
Content-Type: text/html
...
$(index.html)
我们约定使用
$(pathname)
来表示请求的文件的内容,后边的示例中也是如此。
规格描述
适用场景
独角兽适合为小文件(兆级别)提供静态资源服务,不适合为大文件(百兆级别)提供下载服务。
配置选项
unicorn(config:Object|string)
unicorn
是一个模块工厂函数,在创建模块实例的时候可以传入一个config
配置。
config.source:string
配置文件源所在位置。独角兽在读取被请求的静态文件时,使用
source + pathname
作为请求地址,并使用天马的服务端请求读取文件内容,支持file:
、http(s):
与loop:
三种请求协议。当
config
中只包含source
字段时,可以直接使用该字段的值作为配置项:unicorn('file:///some/dir/')
config.expires:number
文件缓存有效期,以秒为单位,默认为一年。
生效条件
独角兽模块位于某一条流水线中。当传入独角兽模块的请求和响应数据同时满足以下条件时,模块才会生效:
request.method()
等于GET
或HEAD
。response.status()
等于404
。
因此,以下情况下,独角兽模块不会对请求做处理:
不是一个
GET
或HEAD
请求。.mount('/', [ function (context) { context.request.method('POST'); }, unicorn('file:///home/admin/deploy/') ])
上一个模块已经处理了请求。
.mount('/', [ function (context) { context.response .status(200).data('Hello World!'); }, unicorn('file:///home/admin/deploy/') ])
异常处理
独角兽在处理请求的过程中,如果发生了一些可预见的异常(比如请求的文件不存在),独角兽会直接返回一个合适的响应。但如果发生了独角兽无法处理的异常(比如磁盘IO错误),独角兽会把异常抛给天马。通过天马的路由器回调函数可以捕获异常,并可以记录一些日志。
pegasus(function (err) {
console.error(err.stack);
})
.mount('/', unicorn('file:///home/admin/deploy/'));
静态文件服务
独角兽首先是一个基本的静态资源服务器,能够处理以下形式的任何静态文件请求:
GET /some/file
例如,在处理GET /index.html
时,如果能够读取到请求的文件,则返回如下的200响应:
=> GET /index.html
<= HTTP/1.1 200 OK
Content-Type: text/html
Expires: Tue, 26 May 2015 07:46:28 GMT
Cache-Control: max-age=31536000
Last-Modified: Sun, 04 May 2014 08:32:49 GMT
$(index.html)
可以看到,响应头里包含了文件类型、最后修改日期、缓存有效期等基本信息,响应数据则是文件自身的内容。
异常处理
当请求的文件无法被读取时,独角兽返回404响应。
=> GET /unexist.js
<= HTTP/1.1 404 Not Found
文件合并
独角兽允许浏览器通过以下形式的URL,一次请求多个同类型的文件。
GET /base/??a.js,b.js
这个URL可以拆分为以下两个文件路径:
"base/a.js" "base/b.js"
独角兽从文件源读取文件内容,并按照文件路径出现的顺序合并文件内容后返回响应。
=> GET /base/??a.js,b.js
<= HTTP/1.1 200 OK
Content-Type: application/javascript
Expires: Mon, 25 May 2015 13:40:23 GMT
Cache-Control: max-age=31536000
Last-Modified: Mon, 25 May 2014 13:40:23 GMT
...
$(base/a.js) + $(base/b.js)
文件合并后,响应头中的最后修改日期使用多个文件当中最新时间。
异常处理
文件路径重复
当URL中同一个文件路径出现了一次以上时,独角兽会忽略重复的路径,而不会把同一个文件合并两次。
=> GET /base/??a.js,b.js,a.js
<= HTTP/1.1 200 OK
...
$(base/a.js) + $(base/b.js)
文件不存在
当URL中的任意文件无法被读取时,独角兽返回404响应。
=> GET /base/??a.js,b.js,unexist.js
<= HTTP/1.1 404 Not Found
文件类型不一致
当URL中出现的所有文件的文件类型(MIME)不一致时,独角兽返回500响应。
=> GET /base/??a.js,b.css
<= HTTP/1.1 500 Internal Server Error
元数据
独角兽除了能够处理通常的静态资源文件外,还能够处理带有元数据(meta,描述数据的数据)的文件。通常的文件只包含文件自身的数据,示意如下:
+--------+
| DATA |
+--------+
而独角兽允许文件在头部包含一段额外的数据,用于保存与文件相关的元数据:
+--------+
| META |
+--------+
| DATA |
+--------+
元数据类似于一种注释,描述了关于文件的一些额外信息,例如文件类型,或是文件的最后修改日期。
数据格式
独角兽支持的元数据使用JSON格式,按照以下二进制方式保存在文件头部:
+-------+-----+-----+------+------+
| MAGIC | VER | LEN | JSON | TAIL |
+-------+-----+-----+------+------+
MAGIC
8字节,内容固定为
2F 2A 21 6D 65 74 61 20
,按utf8
编码转换为字符串后内容为/*!meta
,用于判断一个文件是否带有元数据。VER
4字节,保留字段,用于在将来标识数据格式的版本。
LEN
4字节,按
utf8
编码转换为字符串后内容为JSON二进制数据的十六进制字节长度。由于四位十六进制数字最大值为FFFF
,因此JSON二进制数据的最大长度为65535字节。JSON
JSON字符串按
utf8
编码转换为二进制数据后的结果。TAIL
2字节,内容固定为
2A 2F
,按utf8
编码转换为字符串后内容为*/
。
以下是一个带有元数据的文件的示例:
MAGIC VER LEN
----------------------- ----------- -----------
2F 2A 21 6D 65 74 61 20 20 20 20 20 20 20 20 32 | /*!meta 2
7B 7D 2A 2F 0A 76 61 72 20 61 3B | {}*/.var a;
----- -----
JSON TAIL
如果按utf8
将以上文件转换为字符串后,内容如下:
/*!meta 2{}*/
var a;
可以看到,以上文件虽然带有元数据,但由于JSON是空的,所以并不包含任何实质内容。
数据字段
独角兽使用元数据中的以下字段实现一些功能:
mime
元数据中可以通过mime
字段来指定文件的MIME类型,这样独角兽就不再根据文件扩展名来判断文件的MIME类型。
{ "mime": "application/javascript" }
mtime
元数据中可以通过mtime
字段,以GMT
格式来指定文件的最后修改日期,这样独角兽就不再读取文件原本的最后修改日期。
{ "mtime": "Mon, 23 May 2014 08:46:54 GMT" }
requires
元数据中可以通过requires
字段来申明文件依赖的其它文件的绝对路径,这样独角兽就会针对该文件启用服务端依赖管理。
{ "requires": [ "b.js" ] }
数据生成
元数据虽然转换为字符串后是可读的,但很明显,元数据不是开发者手写出来的。如果希望利用上独角兽的元数据相关功能,就需要借助构建工具来对发布上线的静态文件做编译。
例如,开发者可以在a.css
当中按照以下方式申明依赖:
@import 'b.css';
p {color:red;}
在a.css
发布上线前,开发者可以使用构建工具将a.css
中的依赖申明语句转换为独角兽支持的元数据,转换后的文件内容如下:
/*!meta 16{"requires":["b.css"]}*/
p {color:red;}
异常处理
如果请求的文件带有元数据,但是数据损,无法被正常解析,这种情况下独角兽会抛出异常。
服务端依赖管理
文件可以通过元数据中的requires
字段申明依赖,而独角兽会对这类文件启用服务端依赖管理。
例如有以下文件间依赖关系,下边的文件依赖上边的文件。
d e
\ /
b c
\ /
a
如果我们请求a
时,独角兽会从a
出发,实时解析整个依赖树,并按照深度优先和后续遍历的方式决定文件的输出顺序,并最终返回a
自身及其依赖的所有文件的内容。因此有以下结果:
=> GET /a
<= HTTP/1.1 200 OK
...
$(d) + $(e) + $(b) + $(c) + $(a)
如果没有在文件中申明依赖,我们就需要在浏览器端构造出/??d.js,e.js,b.js,c.js,a.js
这样的请求来实现相同结果,但服务端依赖解析让我们只需要请求入口文件,而不用关系它的依赖。
多重入口
服务端依赖管理可以和文件合并结合起来使用。例如有以下依赖关系:
b c e f
\ / \ /
a d
当通过/??a,d
形式的URL请求多个文件时,有以下结果:
=> GET /??a,d
<= HTTP/1.1 200 OK
...
$(b) + $(c) + $(a) + $(e) + $(f) + $(d)
动态依赖
有时候一个文件的依赖关系无法事先确定。例如在以下目录中,一个JS组件有可能依赖到任何一个多语言文件,但这种依赖关系需要由使用组件的页面来决定。
- deploy/
- i18n/
en-us.js
zh-cn.js
dialog.js
动态依赖特性可用于解决这类问题。在dialog.js
中,可以申明以下依赖关系;
dialog.js { "requires": [ "i18n/{i18n}.js" ] }
可以看到,文件路径中有{x}
这样的占位符,这是一个动态依赖申明。
如果我们使用通常的方式请求dialog.js
,独角兽会忽略掉无法确定的动态依赖申明。
=> GET /dialog.js
<= HTTP/1.1 200 OK
...
$(dialog.js)
但是,我们可以在URL中带上对应的参数,帮助独角兽确定动态依赖申明。
=> GET /dialog.js?i18n=zh-cn
<= HTTP/1.1 200 OK
...
$(i18n/zh-cn.js) + $(dialog.js)
依赖排除
假设有以下依赖关系,其中的x
是一个通用类库。
x y
\ /
a
如果我们直接请求a
,能得到以下响应:
=> GET /a
<= HTTP/1.1 200 OK
...
$(x) + $(y) + $(a)
但是如果我们希望单独请求x
,让多个页面能共享x
的缓存时,使用a
的页面应该会发起以下两个请求:
=> GET /x
=> GET /a
如果a
当中申明了对x
的依赖,页面就会加载两份x
的代码,不但影响性能,还可能出问题。如果我们不在a
当中申明对x
的依赖,这又是一种不太灵活的特殊处理。因此,我们可以按照以下方式发起第二次请求:
=> GET /??-x,a
独角兽在处理这种请求时,会把-
打头的文件及其依赖从整个依赖关系中排除掉,因此排除掉x
的a
的依赖关系如下:
y
/
a
所以独角兽会返回以下响应:
<= HTTP/1.1 200 OK
...
$(y) + $(a)
另外,假设x
和y
两个文件都要做跨页面缓存时,以下请求方式是一种做法:
=> GET /??x,y
=> GET /??-x,-y,a
但这种方式不太灵活,每当需要跨页缓存的文件发生变动时,所有页面的URL都需要修改。于是我们可以采用以下方式:
x y x y
\ / \ /
common a
我们可以创建一个common
文件,专用于加载需要跨页缓存的文件,如此依赖,当需要从一次请求中排除被跨页缓存的文件时,就可以使用以下请求方式:
=> GET /common
=> GET /??-common,a
异常处理
重复依赖
独角兽在对一次请求做服务端依赖管理时,会排除掉依赖树中的重复节点。因此,对于以下依赖关系:
b c c e
\ / \ /
a d
有以下结果:
=> GET /??a,d
<= HTTP/1.1 200 OK
...
$(b) + $(c) + $(a) + $(e) + $(d)
可以看到,c
不会被合并两次。
环状依赖
独角兽会自动打破环状依赖。对于以下依赖关系:
a
|
b c
\ /
a
有以下结果:
=> GET /a
<= HTTP/1.1 200 OK
...
$(b) + $(c) + $(a)
可以看到,b
对a
的环状依赖被打破掉。
依赖缺失
无论是普通的依赖申明,还是动态依赖申明,一旦依赖的某个文件不存在时,独角兽会返回404响应。