Unicorn();3.0.0 beta

// Assets server with dependencies resolution

独角兽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配置。

生效条件

独角兽模块位于某一条流水线中。当传入独角兽模块的请求和响应数据同时满足以下条件时,模块才会生效:

因此,以下情况下,独角兽模块不会对请求做处理:

异常处理

独角兽在处理请求的过程中,如果发生了一些可预见的异常(比如请求的文件不存在),独角兽会直接返回一个合适的响应。但如果发生了独角兽无法处理的异常(比如磁盘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                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

独角兽在处理这种请求时,会把-打头的文件及其依赖从整个依赖关系中排除掉,因此排除掉xa的依赖关系如下:

        y
       /
      a

所以独角兽会返回以下响应:

<= HTTP/1.1 200 OK
   ...

   $(y) + $(a)

另外,假设xy两个文件都要做跨页面缓存时,以下请求方式是一种做法:

=> 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)

可以看到,ba的环状依赖被打破掉。

依赖缺失

无论是普通的依赖申明,还是动态依赖申明,一旦依赖的某个文件不存在时,独角兽会返回404响应。

© 2011-2014 Alibaba.com, Inc.