转 Katana 项目入门
Katana 项目入门
当 ASP.NET 首次在 2002 年发布时,时代有所不同。 那时,Internet 仍处于起步阶段,大约有 5.69 亿用户,每个用户平均每天访问 Internet 的时间为 46 分钟,大约有 3 百万个网站。 仅仅在 10 年之后,相同的测量指标揭示,大约有 22.7 亿个 Internet 用户,每个用户平均每天访问 Internet 的时间为 4 小时,大约有 5.55 亿个网站(请参阅 bit.ly/MY7GzO)。
很显然,这种增长相应地刺激了对应用程序开发者的需求变化,具体表现在生成和运行 Web 应用程序的基础框架、工具和运行时等方面。 现代 Web 应用程序必须能够快速地发展,充分利用许多不同的组件和框架。它们的占用空间必须很小,这样才能在云的大型运行时环境中有效地运行。
确保 ASP.NET 能够满足这些当前需求和未来需求正是 Katana 项目的主要目标。
Katana 简介
Katana 项目实际可以追溯到 Microsoft 外部一个名为 Open Web Interface for .NET (OWIN) 的开放源代码项目。OWIN 是一种定义 Web 服务器和应用程序组件之间的交互的规范(请参阅 owin.org)。 由于这一规范的目的是发展一个广阔且充满活力的、基于 Microsoft .NET Framework 的 Web 服务器和应用程序组件生态系统,因此它可以将服务器与应用程序之间的交互减少到一小部分类型和单个函数签名,这个函数签名被称为应用程序委托(即 AppFunc):
- using AppFunc = Func<IDictionary<string, object>, Task>;
基于 OWIN 的应用程序中的每个组件都向服务器提供应用程序委托。 然后,这些组件链接成一个管道,基于 OWIN 的服务器将会向该管道推送请求。 为了更有效地使用资源,管道中的所有组件都应该是异步的,这体现在返回 Task 对象的应用程序委托中。
包括应用程序状态、请求状态和服务器状态等在内的所有状态都保存在应用程序委托上指定的 IDictionary<string, object> 对象中。 这种数据结构称为环境字典,随着请求通过管道时会从一个组件传递到另一个组件。 虽然任何键/值数据都可以插入到环境字典中,但 OWIN 规范为某些 HTTP 核心元素定义了键,如图 1 中所示。
图 1 HTTP 请求的必需环境字典键
键名称 | 值说明 |
"owin.RequestBody" | 一个带有请求正文(如果有)的流。如果没有请求正文,Stream.Null 可以用作占位符。 |
"owin.RequestHeaders" | 请求标头的 IDictionary<string, string[]>。 |
"owin.RequestMethod" | 一个包含请求的 HTTP 请求方法的字符串(例如 GET 和 POST)。 |
"owin.RequestPath" | 一个包含请求路径的字符串。 此路径必须是应用程序委托的“根”的相对路径。 |
"owin.RequestPathBase" | 一个字符串,包含对应于应用程序委托的“根”的请求路径部分。 |
"owin.RequestProtocol" | 一个包含协议名称和版本的字符串(例如 HTTP/1.0 或 HTTP/1.1)。 |
"owin.RequestQueryString" | 一个字符串,包含 HTTP 请求 URI 的查询字符串组成部分,不带前导“?”(例如 foo=bar&baz=quux)。 该值可以是空字符串。 |
"owin.RequestScheme" | 一个字符串,包含用于请求的 URI 方案(例如 HTTP 或 HTTPS)。 |
定义一组基本的环境字典键/值对,使得许多不同的框架和组件作者可以在一个 OWIN 管道中进行互操作,而不必强制实施对特定 .NET 对象模型的协议,例如针对 ASP.NET MVC 中的 HttpContextBase 或 ASP.NET Web API 中的 HttpRequestMessage/HttpResponseMessage 的协议。
应用程序委托和环境字典这两个元素构成了 OWIN 规范。 Katana 项目是 Microsoft 创建和推出的基于 OWIN 的组件和框架集合。
Katana 组件可以通过体系结构堆栈查看,如图 2 中所示。
图 2 Katana 项目体系结构
此堆栈由以下层组成:
- 主机:运行应用程序的进程,可以是从 IIS 或独立可执行文件到您自己的自定义程序的任何内容。 主机负责启动、加载其他 OWIN 组件和正常关闭。
- 服务器:负责绑定到 TCP 端口,构造环境字典和通过 OWIN 管道处理请求。
- 中间件:这是为处理 OWIN 管道中的请求的所有组件指定的名称。 它可以是从简单压缩组件到 ASP.NET Web API 这样的完整框架,不过从服务器的角度而言,它只是一个公开应用程序委托的组件。
- 应用程序:这是您的代码。 由于 Katana 并不取代 ASP.NET,而是一种编写和托管组件的新方式,因此现有的 ASP.NET Web API 和 SignalR 应用程序将保持不变,因为这些框架可以参与 OWIN 管道。 事实上,对于这些类型的应用程序,Katana 组件只需使用一个小的配置类即可见。
在体系结构方面,Katana 可以按其中每个层都能够轻松替代的方式分解,通常不需要重新生成代码。 在处理 HTTP 请求时,各个层一起工作,方式类似于图 3 中所示的数据流。
图 3 Katana 中数据流的示例
使用 Katana 生成现代 Web 应用程序
现代 Web 应用程序通常具有四项功能:
- 服务器端标记生成
- 静态文件服务
- 用于处理 AJAX 请求的 Web API
- 实时消息
使用所有这些功能生成应用程序时,需要专用于相关功能的各种不同框架。 不过,从这些框架编写应用程序颇具挑战性,当前要求在 IIS 上托管不同的应用程序部分,可能需要使用应用程序和虚拟目录将这些部分相互隔离。
与此相对的是,通过 Katana 可以利用各种不同的 Web 技术编写现代 Web 应用程序,然后在所需的任何位置托管该应用程序,并且只在一个 HTTP 端点下公开。 这样可提供多种优势:
- 部署过程非常简单,因为只涉及一个应用程序,而不是每种功能一个应用程序。
- 可以添加验证之类的额外功能,这些功能可应用于管道中的所有下游组件。
- 不同的组件,无论是 Microsoft 还是第三方组件,都可以通过环境字典对同一个请求状态执行操作。
现在,我将研究一个示例应用程序。该应用程序具有您应该熟悉的域:错误跟踪。 该应用程序将提供一组处于各种不同状态(积压、正在处理和已完成)的错误,并且我可以将错误在这些状态之间变换。 此外,由于许多人可能正在同时管理错误,当错误的状态发生变化时,应用程序将会实时更新所有浏览器。 下面是我将用于生成应用程序的内容:Nancy (nancyfx.org) 用于服务器端标记生成和静态文件服务;ASP.NET Web API(asp.net/web-api) 用于处理 AJAX 请求;而 SignalR (signalr.net) 用于实时消息服务。
另外,我不打算花费很多时间在浏览器客户端的标记和脚本上,我将使用 Knockout.js 来将 HTML 标记与 Web API 及 SignalR 数据分隔开。
需要牢记的首要原则是,我正在将所有这些不同的框架编写到一个 OWIN 管道中,这样当新功能可用时,我只需将这些功能插入到管道,即可将它们添加到应用程序中。
开始使用
Katana 的其中一个目标就是能够让您精确地控制添加到应用程序中的功能(这样您就可以精确控制在每个请求的处理性能方面的工作)。 牢记这一点后,我将从在 Visual Studio 2013 Preview 中创建一个空的新 ASP.NET Web 应用程序项目开始,如图 4 中所示。
图 4 Visual Studio 2013 Preview 中的新 ASP.NET Web 应用程序项目
即使是空的 Web 项目模板也能提供有用的功能,因为在默认情况下,它们会将编译的程序集直接放在 /bin 文件夹而不是 /bin/debug 文件夹中(这一点在其他项目类型中很常见)。 默认的 Katana 主机会在此 /bin 文件夹中查找程序集。 您可以将基于 Katana 的应用程序创建为类库,但您需要修改项目属性以便符合此结构,或者提供自己的自定义应用程序加载程序,该加载程序能够在其他文件夹结构中搜索程序集和类型。
下一步,我将使用 Nancy Web 框架生成服务器端标记生成代码。
Nancy 的语法非常简洁,可以快速轻松地生成基于 HTTP 的站点和服务。 本练习中更重要的事项是,与 ASP.NET Web API 类似,该应用程序与 System.Web.dll 没有任何依赖关系,并且它的构建目的是为了在 OWIN 管道中运行。 诸如 ASP.NET MVC 之类的框架与 System.Web.dll 具有依赖关系(截止编写本文时),这使得它们不太适合用于非 IIS 托管方案。
大多数情况下,在您向应用程序添加新功能时,您将从添加 NuGet 程序包开始。 (您可以在docs.nuget.org 上详细了解 NuGet。)在编写本文时,此处使用的许多程序包都是预发布版本,因此请确保可在 NuGet 对话框中显示预发布程序包。
为了将 Nancy 添加到应用程序中,我只需安装 Nancy NuGet 程序包即可。 不过,因为我还希望在 OWIN 管道中运行 Nancy,我打算安装 Nancy.Owin 程序包 (nuget.org/packages/nancy.owin)。 这会将 Nancy 程序包作为依赖关系安装,并且提供额外的辅助方法在我的 OWIN 管道中配置 Nancy。
下一步,我需要创建 Nancy 模块(类似于模型视图控制器,或者称 MVC、控制器)来处理请求,还需要创建视图来向浏览器显示内容。 下面是模块 (HomeModule.cs) 的代码:
- public class HomeModule : NancyModule
- {
- public HomeModule() {
- Get["/"] = _ => {
- var model = new { title = "We've Got Issues..." };
- return View["home", model];
- };
- }
- }
正如您看到的那样,对于声明定向到应用程序根 (“/”) 的请求的模块,应该由关联 lambda 中定义的匿名委托来处理。 该函数会创建一个包含页标题的模型,并指示 Nancy 呈现“主”视图,并将模型传递到视图。 如图 5 中所示,该视图会将模型的标题属性同时插入到页标题和 h1 元素中。
图 5 Home.html
- <!DOCTYPE html>
- <html >
- <head>
- <title>@Model.title</title>
- </head>
- <body>
- <header>
- <h1>@Model.title</h1>
- </header>
- <section>
- <h2>Backlog</h2>
- <ul class="bugs" id="backlog">
- <li>a bug</li>
- </ul>
- </section>
- <section>
- <h2>Working</h2>
- <ul class="bugs" id="working">
- <li>a bug</li>
- </ul>
- </section>
- <section>
- <h2>Done</h2>
- <ul class="bugs" id="done">
- <li>a bug</li>
- </ul>
- </section>
- </body>
- </html>
有关这些列表的更多信息,请参阅 Nancy 文档。
现在,我已经实现了基本的 Nancy 功能,我需要建立 OWIN 管道并配置 Nancy 模块来参与该管道。 为此,我首先需要安装 Katana 主机和服务器组件,然后编写少量的探测代码来设置 OWIN 管道并将 Nancy 插入该管道中。
对于 Katana 主机和服务器组件,我将从使用 IIS Express 和 System.Web 入手,因为这些在本质上可以为 Visual Studio 所理解,并且将在生成应用程序时实现顺畅的 F5 体验。 为了将 System.Web 主机合并到项目中,我安装了 NuGet 程序包 Microsoft.Owin.Host.SystemWeb (bit.ly/19EZ2Rw)。
默认的 Katana 组件使用多个不同的约定来加载和运行 OWIN 应用程序,包括启动类。 当 Katana 主机加载 OWIN 应用程序时,它会根据以下规则发现并运行启动类(按照优先级顺序):
- 如果 web.config 文件包含带有 key=“owin:AppStartup”的 appSetting,则加载程序将使用设置值。 该值必须是有效的 .NET 类型名称。
- 如果程序集包含属性 [assembly:OwinStartup(typeof(MyStartup))],则加载程序将使用属性值中指定的类型。
- 如果这些条件中的任意一个为真,加载程序将通过查找名为 Startup 的类型(带有与签名 void Configure(IAppBuilder app) 匹配的方法)来扫描已加载的程序集。
在本示例中,我将允许加载程序扫描该类的程序集。 不过,如果您的项目中有许多不同的类型和程序集,那么使用 appSetting 或程序集属性来防止不必要的扫描可能会更好。
我将创建启动类,该类将初始化 OWIN 管道并将 Nancy 作为管道组件添加。 我创建一个名为 Startup 的新类,并按下面所示添加配置方法:
- public class Startup
- {
- public void Configuration(IAppBuilder app)
- {
- app.UseNancy();
- }
- }
UseNancy 是 Nancy.Owin NuGet 程序包公开的扩展方法。 虽然您可以使用 IAppBuilder 的更通用的 Use 方法来添加中间件,不过许多中间件库将提供这些有用的扩展方法,这些方法可以简化配置过程。
此时,您可以在 Visual Studio 中使用 F5 运行项目,并且可以看到一个虽然不足以令人非常兴奋,但功能完备的 Web 应用程序。 此时,OWIN 管道包含单个组件,即 Nancy,如图 6 中所示。
图 6 包含单个组件的正常运行的 Web 应用程序
将数据与 ASP.NET Web API 合并
目前,HTML 视图由主静态标记构成。 我现在将为用户提供一些可处理的实际错误。 对于许多现代 Web 应用程序,将数据提供给浏览器客户机的任务已从服务器端标记生成框架(例如 Nancy 模块)转移到单独的 Web API 服务。 接下来,浏览器会加载 HTML 页并立即执行 JavaScript,后者将从 Web API 中提取数据并在页自身中动态生成 HTML 标记。
我将从使用 ASP.NET Web API 框架构造 Web API 开始入手。 通常,第一步是安装 Web API NuGet 程序包。 为了确保轻松地将 ASP.NET Web API 插入到 OWIN 管道中,我将安装 Microsoft.AspNet.WebApi.Owin 程序包 (bit.ly/1dnocmK)。 此程序包会将 ASP.NET Web API 框架的其余部分作为依赖关系安装。 在安装框架后,我将创建一个简单的 API,如图 7 中所示。
图 7 BugsController.cs
- public class BugsController : ApiController
- {
- IBugsRepository _bugsRepository = new BugsRepository();
- public IEnumerable<Bug> Get()
- {
- return _bugsRepository.GetBugs();
- }
- [HttpPost("api/bugs/backlog")]
- public Bug MoveToBacklog([FromBody] int id)
- {
- var bug = _bugsRepository.GetBugs().First(b=>b.id==id);
- bug.state = "backlog";
- return bug;
- }
- [HttpPost("api/bugs/working")]
- public Bug MoveToWorking([FromBody] int id)
- {
- var bug = _bugsRepository.GetBugs().First(b => b.id == id);
- bug.state = "working";
- return bug;
- }
- [HttpPost("api/bugs/done")]
- public Bug MoveToDone([FromBody] int id)
- {
- var bug = _bugsRepository.GetBugs().First(b => b.id == id);
- bug.state = "done";
- return bug;
- }
- }
该 API 包含一个可从存储库返回一组错误对象的方法,还有一些可将错误在不同状态之间变换的方法。 有关 ASP.NET Web API 的详细信息,请参阅 asp.net/web-api。
现在我已定义了一个 ASP.NET Web API 控制器,我需要将它添加到现有的 OWIN 管道。 为此,我只需将下面的代码行添加到启动类的配置方法中:
- var config = new HttpConfiguration();
- config.MapHttpAttributeRoutes();
- config.Routes.MapHttpRoute("bugs", "api/{Controller}");
- app.UseWebApi(config);
就如 Nancy 一样,ASP.NET Web API OWIN 程序包提供了 UseWebApi 扩展方法,因此可以轻松地将 ASP.NET Web API 合并到现有的 OWIN 管道中。 现在,OWIN 管道包含两个组件,即 ASP.NET Web API 和 Nancy,如图 8 中所示。
图 8 包含两个组件的 OWIN 管道
当请求进入管道中时,如果它们与其中一个 ASP.NET Web API 路由规则匹配,ASP.NET Web API 就会处理该请求并生成响应。 否则,请求会继续通过管道,Nancy 会尝试处理该请求。 如果没有任何管道组件可以处理特定的请求,默认的 Katana 组件将返回 HTTP 404 响应。
尽管我已经有正常运行的 ASP.NET Web API,但它目前未由主视图访问。 因此,我将添加代码,以便从 Web API 使用数据,并且生成每个不同状态的错误列表:积压、正在处理和已完成。 对于此任务,我将利用 Knockout.js,这是一个 JavaScript 的“模型-视图-视图模型”(MVVM) 库。 有关 Knockout 的更多信息,请参阅 knockoutjs.com。
为了使用 Knockout 启用在客户端动态生成 HTML 标记的功能,我首先需要做的工作是从 ASP.NET Web API 中提取所有错误,并创建 Knockout 可绑定到 HTML 元素的 viewModel。 如图 9 所示。
图 9 设置错误 viewModel
- <script>
- $(function () {
- var viewModel;
- $.getJSON('/api/bugs', function(data) {
- var model = data;
- viewModel = {
- backlog: ko.observableArray(
- model.filter(function(element) { return element.state === 'backlog'; })),
- working: ko.observableArray(
- model.filter(function(element) { return element.state === 'working'; })),
- done: ko.observableArray(
- model.filter(function(element) { return element.state === 'done'; })),
- changeState: function (bug, newState) {
- var self = this;
- $.post('/api/bugs/' + newState, { '': bug.id }, function(data){
- self.moveBug(data);
- });
- },
- moveBug: function (bug) {
- // Remove the item from one of the existing lists
- ...
- // Add bug to correct list
- this[bug.state].push(bug);
- }
- };
- ko.applyBindings(viewModel);
- })
- })
- </script>
在创建 viewModel 后,Knockout 接下来可以通过将 viewModel 绑定到使用 Knockout 特定属性修饰的 HTML 元素,动态地生成并更新 HTML 内容。 例如,积压列表可以从 viewModel 使用图 10 中所示的属性生成。
图 10 用于生成积压列表的属性
- <section>
- <h2>Backlog</h2>
- <ul class="bugs" id="backlog" data-bind="foreach:backlog">
- <li>
- [<span data-bind="text: id"></span>] <span data-bind="text: title"></span>:
- <span data-bind="text: description"></span>
- <ul>
- <li><a href="#" data-bind
- ="click: $root.changeState.bind($root, $data,
- 'working')"
- <li><a href="#" data-bind
- ="click: $root.changeState.bind($root, $data,
- 'done')"
- </ul>
- </li>
- </ul>
- </section>
使用 SignalR 添加实时更改通知
此时,我已经有了一个功能完备的单页 Web 应用程序。 用户可以浏览到主视图,并将错误在不同的错误状态之间变换。 此外,当前功能级别的基础技术 Nancy 和 ASP.NET Web API 在相同的 OWIN 管道中一起运行。
不过,我打算更深入一步,允许不同的用户实时查看其他用户对错误所做的更新。 为此,我将利用 SignalR 库。该库提供了客户机和服务器 API 来管理浏览器与 Web 服务器之间的实时消息交换。 编写 SignalR 的目的也是为了在 OWIN 管道中运行,因此将它添加到现有的应用程序中不过是小事一桩。
我将使用一项名为 Hub 的 SignalR 功能,但在本文中不会讨论有关 SignalR 的细节。Hub 使客户端和服务器能够相互调用对方的方法。 (有关 SignalR 的深入介绍,请参阅 bit.ly/14WIx1t。)在我的应用程序中,当 ASP.NET Web API 接收到更改错误状态的请求时,它将会更新错误,然后通过 SignalR Hub 将更新后的错误广播给当前连接到应用程序的所有浏览器客户端。
我将从在服务器上创建 Hub 来开始此过程。 因为我不会利用任何其他 SignalR 功能,因此我的 Hub 将只包含以下空的类定义:
- [HubName("bugs")]
- public class BugHub : Hub
- {
- }
为了将广播从 ASP.NET Web API 发送到 Hub,我首先需要获取器运行时上下文的实例。 我可以通过添加以下 BugsController 构造函数来实现此目的:
- public BugsController()
- {
- _hub = GlobalHost.ConnectionManager.GetHubContext<BugHub>();
- }
从其中一个 MoveToXX 操作,我接下来可以将更新的错误广播到所有连接的浏览器客户端:
- _hub.Clients.All.moved(bug);
在主视图中,将几个脚本引用添加到 SignalR JavaScript 库后,我可以使用以下代码连接到 bugsHub 并开始侦听“已变换”消息:
- $.connection.hub.logging = true;
- var bugsHub = $.connection.bugs;
- bugsHub.client.moved = function (item) {
- viewModel.moveBug(item);
- };
- $.connection.hub.start().done(function() {
- console.log('hub connection open');
- });
请注意,当我通过 moved 函数从服务器接收到调用时,我按照该项的单击处理程序的相同方式调用 viewModel moveBug 方法。 差别在于,由于此方法是 SignalR 广播的结果,因此所有浏览器客户端可以同时更新其 viewModel。 通过打开两个浏览器窗口,在其中一个窗口中进行更改,然后在另一个窗口中查看状态更改,即可清楚地了解这一点。
正如我所提到的,将 SignalR 添加到 OWIN 管道中不过是小事一桩。 我只需将下面的代码添加到启动类的配置方法中:
- app.MapSignalR();
这将创建类似图 11 中所示的管道。
图 11 包含三个组件的 OWIN 管道
转向自托管
我现在有了一个正常运行的错误管理应用程序,但仍缺少一些关键功能来执行某些有意思的操作。 我已经使用 Microsoft 和第三方 Katana 中间件组件,逐步向应用程序添加了一些功能。 不过,今天许多此类功能已可以使用 ASP.NET HttpModules 和 HttpHandlers 来实现。 因此,除了提供更加简单的、代码驱动的方法来编写管道组件之外,我真正实现了哪些功能?
答案就是记住图 2 中的高级 Katana 体系结构图表。 到目前为止,我只使用了 Katana 堆栈的最上面两层。不过,所有这些层都可以轻松地替换,包括服务器和主机。
为了进行演示,我将处理我的整个管道,将它提升到 IIS 和 System.Web.dll 之外,并将它放在一个简单的轻型 HTTP 服务器上,该服务器是由名为 OwinHost.exe 的 Katana 可执行文件托管的。 自托管在许多方案中是非常有用的,从开发计算机上未安装 Web 服务器的安装情形,到在使用进程隔离并且不公开 Web 服务器访问的共享托管环境中部署应用程序的生产情形。
我将从安装以下附加 NuGet 程序包开始:
- Microsoft.Owin.Host.HttpListener (bit.ly/153aIca)
- OwinHost (bit.ly/162Uzj8)
然后,我将重新生成应用程序。 请注意,对于在新服务器和主机之上运行应用程序,重新生成操作并不是必需的。 唯一的要求是,在运行时,/bin 文件夹中必须存在这些文件,而重新生成是一种将文件复制到 /bin 的便捷方式。
在安装程序包和复制文件之后,我打开命令提示符,导航到 Web 项目的根文件夹,然后从程序包文件夹中调用 OwinHost.exe,如图 12 中所示:
> ..
\packages\OwinHost.2.0.0\tools\OwinHost.exe
图 12 从程序包文件夹中调用 OwinHost.exe
默认情况下,OwinHost.exe 将启动,加载 Microsoft.Owin.Host.HttpListener 服务器,然后开始侦听端口 5000。 接下来,我可以导航到 http://localhost:5000,确认整个应用程序正在运行。
此外,几乎所有的默认设置都可以使用命令行开关进行覆盖。 例如,如果您希望侦听其他端口,可以提供 -p 12345。 如果希望使用完全不同的服务器,请使用 -s your.custom.server.assembly。 Katana 设计的强大之处在于它的模块化。 当堆栈中的任何层发生创新后,它们可以立即集成到正在运行的应用程序。 并且,由于堆栈的所有组件之间的约定只是应用程序委托,创新的步伐可以远远超越目前提供的功能。
这仅仅是一个开端
Katana 2.0 将随 Visual Studio 2013 一起发布。 新版本有两个值得关注的方面:
- 为自托管提供核心基础结构组件
- 提供了一套丰富的验证中间件(包括 Facebook、Google、Twitter 和 Microsoft Account 这样的社交提供商)以及适用于 Windows Azure Active Directory、cookie 和联合身份验证的提供程序
在发布 Katana 2.0 后,将会立即开始未来的 Katana 组件集的开发工作。 具体细节和优先级仍有待确定,不过您可以在 katanaproject.codeplex.com 中创建问题,推动这种讨论。 最后,本文中的所有代码位于bit.ly/1alOF4m 中。