所有的好的Web框架都有一套好的通道截拦的机制,Nancy在我看来是处理最好的。那什么是请求通道那?下面的图可能说的比较清楚些:
正如名称中描述的,一个典型的Web请求在到达最终响应前会穿过一定数量的模块,然后反向通过这些模块到达浏览器。
请求到底要经过多少类型的模块需要根据框架而定。有的多,有的少。越多的处理,响应的就越慢。
定位一个轻量级和模块化的Web框架,它的通道中有很少的环节,使得开发人员可以更好的控制模块如何、在什么情况下可以与框架进行交互。
当我们谈论通道截拦的时候,主要是指Nancy公开的几个主要回调点(钩子函数),使得你可以在请求链的回调点加入你自己的模块或代码段。
到目前为止,你可以见到了Nancy众多的功能 -- 身份验证就是一个好的例子。 -- 你可能会想在通道处理中会需要很多代码量来支撑这个功能。
事实是,Nancy基本上没有在身份验证方面编写什么代码,即使你调用身份验证的方法,在通道中也不会添加什么代码处理。只有在你添加了this.RequiresAuthentication();行后,请求通道才会进行身份验证截拦。
验证部分也就是注册了路由模块的Before 和 After 管道,让我们看下面的验证校验页面:
using Nancy;
using Nancy.Responses;
using Nancy.Security;
namespace nancybook.modules
{
public class AuthRoutes : NancyModule
{
public AuthRoutes() : base("/auth")
{
Before += ctx =>
{
return (this.Context.CurrentUser == null)
? new HtmlResponse(HttpStatusCode.Unauthorized)
: null;
};
Get[@"/"] = _ => View["auth/index"];
}
}
}
上面的处理其实和 RequiresAuthentication 的功能是一样的。
通过这一段代码,你可以很清晰的看到两种结果。首先可能会返回null, 这不会影响到处理进程,其实是不会有什么影响。
再者就是返回一个响应对象(例子中是 403 Unauthorized),进程不会再往下执行,而是直接返回给客户端。
当然你可能已经意识到这不仅能用于身份验证,还可以做一些资源的检查,其他条件的校验或者返回异常信息等不一样的结果。
假如你想提供某种形式的前端缓存,例如 你可以拦截 Before管道,判断请求是否已经缓存,如果已经缓存就返回缓存的版本。只有在没有缓存或缓存过期的时候访问路由模块的处理程序。
你也可以像下面提供After处理:
After += ctx =>
{ //... Code here ...
}
在启用After管道的时候,你已经可以访问到相应对象了(路由处理程序生成的),通过Nancy上下文就可以对它进行随意的修改。
比如你可以检查一些环境变量,在返回到浏览器前可以修改已经生成的200 ok 的相应为403响应。
我想,你可以更加倾向于使用After管道来缓存客户端请求,下次相同请求就可以查找预先的缓存,而不是再一次到数据库中读取。
值得注意的是附加在路由模块上的代码会在该模块的每次请求中调用,这也就意味着你的代码要耗时长一点,客户端的请求响应速度会被减缓。
应用级别的挂接
你也可以在整个应用级别上注册Before或After管道,而不只是在模块基础上。启用应用级别的意味着每个模块、每个路由都会触发注册的挂接,无论怎么定义。另外,如果你注册了一个应用级的挂接,并在一个特殊的模块上也单独做了注册挂接,两个挂接都会被调用,应用级的优先。
这也意味着在应用级别的挂接上返回一个响应对象,模块级别的挂接就不再会被调用。同理,模块级别的挂接返回了响应对象,模块的处理也就不会被调用。
你可以定义一个bootstrapper ,重载ApplicationStartup 或RequestStartup 方法来注册应用级别的挂接。这两个方法会暴露一个Pipelines 参数,它包含BeforeRequest 或AfterRequest 属性,可以像使用Before 或After挂接一样来看待。
你也将会使用到OnError属性,可以用来在应用系统中实现一个异常处理程序。举个例子,在你的数据库检索代码中,如果检索不到一条客户端请求的记录,你可以会抛出一个Database Object not found 的自定义异常。你可以使用自定义的bootstrapper
来拦截这个异常,然后返回一个404 文件未找到异常,就类似下面:
using System.Text;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.Conventions;
using Nancy.Session;
using Nancy.TinyIoc;
namespace nancybook
{
public class CustomBootstrapper : DefaultNancyBootstrapper
{
protected override void ApplicationStartup(
TinyIoCContainer container,
IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
// Add an error handler to catch our entity not found exceptions
pipelines.OnError += (context, exception) =>
{
// If we've raised an EntityNotFound exception in our data layer
if (exception is EntityNotFoundException)
return new Response()
{
StatusCode = HttpStatusCode.NotFound,
ContentType = "text/html",
Contents = (stream) =>
{
var errorMessage = Encoding.UTF8.GetBytes("Entity not
found");
stream.Write(errorMessage, , errorMessage.Length);
}
};
// If none of the above handles our exception, then pass it on as a 500
throw exception;
};
}
}
}
输出缓存
using System;
using System.Collections.Specialized;
using System.Runtime.Caching;
using Nancy;
namespace nancybook
{
public class CacheService
{
private static readonly NameValueCollection _config = new
NameValueCollection();
private readonly MemoryCache _cache = new MemoryCache("NancyCache",
_config);
private readonly CacheItemPolicy _standardPolicy = new CacheItemPolicy
{
Priority = CacheItemPriority.NotRemovable,
SlidingExpiration = TimeSpan.FromSeconds() // This can be changed };
public void AddItem(string itemKey, Response itemToAdd)
{
_cache.Add(new CacheItem(itemKey, itemToAdd), _standardPolicy);
}
public Response GetItem(string itemKey)
{
return (Response)_cache.Get(itemKey);
}
}
}
protected override void ConfigureApplicationContainer(TinyIoCContainer
container)
{
base.ConfigureApplicationContainer(container);
container.Register<CacheService>().AsSingleton();
}
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using nancybook.Models;
using Nancy;
namespace nancybook.modules
{
public class CachingRoutes : NancyModule
{ readonly CacheService _myCache;
public CachingRoutes(CacheService myCache) : base("/caching")
{
_myCache = myCache;
Get["/"] = x =>
{
var cacheData = new CacheDemo() {WhenRequested = DateTime.Now};
return View["caching/index.html", cacheData];
};
Before += ctx =>
{
string key = ctx.Request.Path;
var cacheObject = _myCache.GetItem(key);
return cacheObject;
};
After += ctx =>
{
if(ctx.Response.StatusCode != HttpStatusCode.OK)
{
return;
}
string key = ctx.Request.Path;
if(_myCache.GetItem(key) == null)
_myCache.AddItem(key, ctx.Response);
};
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nancy Demo | Caching Example</title>
<link href="~/content/bootstrap.min.css" rel="stylesheet"
type="text/css"/>
</head>
<body>
<div class="container">
<div class="page-header">
<h1 style="display: inline-block">Nancy Demo <small>Caching
Example</small></h1>
<h1 style="display: inline-block" class="pull-right"><small><a
href="~/" title="Click to return to demo home page">home <span
class="glyphicon glyphicon-home"></span></a></small></h1>
</div>
<h4>This page was requested at <strong class="textsuccess">@Model.WhenRequested</strong></h4>
<br/><br/>
<p class="lead">This example uses before and after filters attached
directly to the module servicing this request.</p>
<p>If you observe the time this page was created when refreshing it,
you'll see the page is handled by an output cache; this cache has a sliding
window of seconds.</p>
<p>
As long as you’re refreshing the page, you'll reset the timer and the
cache will continue to wait seconds after the last request before
expiring. If you request the page then
leave it for seconds before re-requesting it, you'll see you get a
new copy.
</p>
</div>
<script src="~/scripts/jquery-2.1.3.min.js"></script>
<script src="~/scripts/bootstrap.min.js"></script>
</body>
</html>
CacheDemo.cs
using System;
namespace nancybook.Models
{
public class CacheDemo
{
public DateTime WhenRequested { get; set; }
}
}
总结