项目 Katana 概述

作者 :Howard Dierking

ASP.NET 框架已经存在了十多年,该平台使无数网站和服务得以开发。 随着 Web 应用程序开发策略的发展,该框架已能够与 ASP.NET MVC 和 ASP.NET Web API等技术同步发展。 随着 Web 应用程序开发的下一个进化步骤进入云计算领域,project Katana 提供了一组基础组件来 ASP.NET 应用程序,使其能够灵活、可移植、轻量级,并提供更好的性能 – 换句话说,project Katana 云可优化 ASP.NET 应用程序。

为什么选择卡塔纳 - 为什么现在?

无论讨论的是开发人员框架还是最终用户产品,了解创建产品的基本动机都很重要,其中一部分包括了解产品是为谁创建的。 ASP.NET 最初是考虑到两个客户而创建的。

第一组客户是经典 ASP 开发人员。 当时,ASP 是用于通过交织标记和服务器端脚本来创建动态数据驱动的网站和应用程序的主要技术之一。 ASP 运行时为服务器端脚本提供了一组对象,这些对象抽象了基础 HTTP 协议和 Web 服务器的核心方面,并提供了对其他服务(如会话和应用程序状态管理、缓存等)的访问权限。虽然功能强大,但随着规模和复杂性的增长,经典 ASP 应用程序成为管理的挑战。 这主要是由于脚本环境中缺少结构,加上代码和标记交错导致的代码重复。 为了在解决一些挑战的同时利用经典 ASP 的优势,ASP.NET 利用.NET Framework面向对象的语言提供的代码组织,同时保留经典 ASP 开发人员已习惯的服务器端编程模型。

ASP.NET 的第二组目标客户是 Windows 商业应用程序开发人员。 与习惯于编写 HTML 标记和代码以生成更多 HTML 标记的经典 ASP 开发人员不同,WinForms 开发人员在) 习惯于设计时体验(包括画布和一组丰富的用户界面控件)之前 (VB6 开发人员。 ASP.NET 的第一个版本(也称为“Web Forms”)提供了类似的设计时体验,以及用户界面组件的服务器端事件模型和一组基础结构功能, (ViewState) ,以在客户端和服务器端编程之间创建无缝开发人员体验。 Web Forms在 WinForms 开发人员熟悉的有状态事件模型中有效地隐藏了 Web 的无状态性质。

历史模型提出的挑战

最终结果是一个成熟、功能丰富的运行时和开发人员编程模型。 然而,随着功能丰富,出现了一些明显的挑战。 首先,框架是 整体式的,在逻辑上不同的功能单元在相同的System.Web.dll程序集 (紧密耦合,例如,具有 Web 窗体框架的核心 HTTP 对象) 。 其次,ASP.NET 作为较大.NET Framework的一部分包括在内,这意味着两次发布之间的时间大约是几年。这使得 ASP.NET 很难跟上快速发展的 Web 开发中发生的所有更改的步伐。 最后,System.Web.dll本身以几种不同的方式耦合到特定的 Web 托管选项:Internet Information Services (IIS) 。

进化步骤:ASP.NET MVC 和 ASP.NET Web API

Web 开发中发生了许多变化! Web 应用程序越来越多地开发为一系列小型、重点组件,而不是大型框架。 组件的数量以及释放它们的频率正以越来越快的速度增加。 很明显,要跟上 Web 的步伐,框架需要更小、分离、更集中,而不是更大、功能更丰富,因此 ,ASP.NET 团队采取了一些进化步骤,使 ASP.NET 成为一系列可插入 Web 组件,而不是单个框架

早期变化之一是,由于 Ruby on Rails 等 Web 开发框架,众所周知的模型-视图-控制器 (MVC) 设计模式的普及。 这种构建 Web 应用程序的样式使开发人员可以更好地控制应用程序的标记,同时仍保留标记和业务逻辑的分离,这是 ASP.NET 的初始卖点之一。 为了满足对这种 Web 应用程序开发方式的需求,Microsoft 利用机会,通过开发 ASP.NET MVC 带外 (,而不是将其包含在.NET Framework) 中来更好地定位自己。 ASP.NET MVC 作为独立下载发布。 这使工程团队能够灵活地比以往更频繁地交付更新。

Web 应用程序开发中的另一个主要转变是从动态的、服务器生成的网页转变为静态初始标记,其中包含通过 AJAX 请求与后端 Web API 通信的客户端脚本生成的动态页面部分。 这种体系结构转变有助于推动 Web API 的兴起和 ASP.NET Web API框架的开发。 与 ASP.NET MVC 一样,ASP.NET Web API的发布提供了另一个机会,ASP.NET 更模块化的框架进一步演变。 工程团队利用这一机会,构建了 ASP.NET Web API,使其不依赖于 System.Web.dll中找到的任何核心框架类型。 这实现了两件事:第一,这意味着 ASP.NET Web API可以完全独立地发展 (并且它可以继续快速迭代,因为它是通过 NuGet) 交付的。 其次,由于没有外部依赖项System.Web.dll,因此没有 IIS 的依赖项,ASP.NET Web API包括能够在自定义主机 (运行的功能,例如控制台应用程序、Windows 服务等)

未来:一个灵活的框架

通过将框架组件彼此分离,然后在 NuGet 上发布它们,框架现在可以 更独立、更快地进行迭代。 此外,Web API 自承载功能的强大功能和灵活性对需要 小型轻量级主机 的开发人员非常有吸引力。 事实上,事实证明,其他框架也希望此功能具有吸引力,这给每个框架带来新的挑战,即每个框架在自己的基址上运行,并需要在启动、停止等) (单独进行管理。 新式 Web 应用程序通常支持静态文件服务、动态页面生成、Web API 和最近的实时/推送通知。 期望其中每个服务都应独立运行和管理是不现实的。

需要的是单个托管抽象,使开发人员能够从各种不同的组件和框架编写应用程序,然后在支持主机上运行该应用程序。

适用于 .NET 的开放 Web 接口 (OWIN)

Rack 在 Ruby 社区中的优势的启发,.NET 社区的几个成员开始在 Web 服务器和框架组件之间创建抽象。 OWIN 抽象的两个设计目标是它很简单,并且对其他框架类型的依赖项最少。 这两个目标有助于确保:

  • 可以更轻松地开发和使用新组件。
  • 应用程序可以更轻松地在主机和整个平台/操作系统之间移植。

生成的抽象由两个核心元素组成。 第一个是环境字典。 此数据结构负责存储处理 HTTP 请求和响应所需的所有状态,以及任何相关的服务器状态。 环境字典的定义如下:

IDictionary<string, object>

OWIN 兼容的 Web 服务器负责使用 HTTP 请求和响应的正文流和标头集合等数据填充环境字典。 然后,应用程序或框架组件负责使用其他值填充或更新字典,并写入响应正文流。

除了指定环境字典的类型外,OWIN 规范还定义了核心字典键值对的列表。 例如,下表显示了 HTTP 请求所需的字典密钥:

键名 值说明
"owin.RequestBody" 包含请求正文的流(如果有)。 如果没有请求正文,可将 Stream.Null 用作占位符。 请参阅 请求正文
"owin.RequestHeaders" IDictionary<string, string[]>请求标头的 。 请参阅 标头
"owin.RequestMethod" 包含string请求 (的 HTTP 请求方法的 ,例如 、 "GET""POST") 。
"owin.RequestPath" 包含 string 请求路径的 。 路径必须相对于应用程序委托的“根”;请参阅 路径
"owin.RequestPathBase" 一个 string ,它包含与应用程序委托的“根”对应的请求路径部分;请参阅 路径
"owin.RequestProtocol" 包含 string 协议名称和版本 (的 ,例如 "HTTP/1.0""HTTP/1.1") 。
"owin.RequestQueryString" 一个 string ,包含 HTTP 请求 URI 的查询字符串组件,不带前导“?” (,例如 "foo=bar&baz=quux" ,) 。 该值可以是空字符串。
"owin.RequestScheme" 包含string用于请求的 URI 方案 (,例如 、 "http""https") ;请参阅 URI 方案

OWIN 的第二个关键元素是应用程序委托。 这是一个函数签名,用作 OWIN 应用程序中所有组件之间的主接口。 应用程序委托的定义如下:

Func<IDictionary<string, object>, Task>;

然后,应用程序委托只是 Func 委托类型的实现,其中函数接受环境字典作为输入并返回 Task。 此设计对开发人员有几个影响:

  • 编写 OWIN 组件所需的类型依赖项数量非常少。 这大大增加了 OWIN 对开发人员的可访问性。
  • 异步设计使抽象能够高效处理计算资源,尤其是在 I/O 密集型操作中。
  • 由于应用程序委托是一个原子执行单元,并且环境字典作为委托上的参数携带,因此可以轻松将 OWIN 组件链接在一起以创建复杂的 HTTP 处理管道。

从实现的角度来看,OWIN 是) (http://owin.org/html/owin.html 规范。 它的目标不是成为下一个 Web 框架,而是 Web 框架和 Web 服务器交互方式的规范。

如果你已经调查过 OWINKatana,你可能也注意到 了 Owin NuGet 包 和Owin.dll。 此库包含单个接口 [IAppBuilder]/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) ,该接口将 OWIN 规范 第 4 节 中所述的启动序列形式化和编纂。 虽然生成 OWIN 服务器不需要 ,但 [IAppBuilder]/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) 接口提供了具体的参考点,Katana 项目组件使用它。

Katana 项目

虽然 OWIN 规范和Owin.dll都是社区拥有的,社区运行开放源代码工作,但 Katana 项目代表一组 OWIN 组件,尽管这些组件仍然开放源代码,但是由 Microsoft 生成和发布的。 这些组件包括基础结构组件(如主机和服务器)以及功能组件(如身份验证组件和绑定到 SignalRASP.NET Web API 等框架)。 该项目具有以下三个高级目标:

  • 可移植 – 组件应该能够在新组件可用时轻松替换它们。 这包括所有类型的组件,从框架到服务器和主机。 此目标的含义是,第三方框架可以在 Microsoft 服务器上无缝运行,而 Microsoft 框架可以在第三方服务器和主机上运行。
  • 模块化/灵活 - 与许多框架不同,其中包括在默认情况下启用的无数功能,Katana 项目组件应较小且重点突出,从而让应用程序开发人员在确定要在应用程序中使用哪些组件时拥有控制权。
  • 轻型/高性能/可缩放 – 通过将框架的传统概念分解为一组由应用程序开发人员显式添加的小型、重点组件,生成的 Katana 应用程序可以消耗更少的计算资源,因此,处理更多的负载,而不是其他类型的服务器和框架。 由于应用程序的要求需要来自底层基础结构的更多功能,因此可以将这些功能添加到 OWIN 管道,但应用程序开发人员应该做出明确的决定。 此外,较低级别组件的可替换性意味着,当它们变得可用时,可以无缝引入新的高性能服务器,以提高 OWIN 应用程序的性能,而不会破坏这些应用程序。

使用 Katana 组件入门

第一次引入时, Node.js 框架立即引起人们注意的一个方面是编写和运行 Web 服务器的简单性。 如果 Katana 目标是按照 Node.js来制定的,人们可能会说,Katana 带来了 许多好处, Node.js(和框架(如它) )而不强迫开发人员放弃她所知道的一切开发 ASP.NET Web 应用程序。 若要使此语句成立,Katana 项目入门在本质上应该同样简单, Node.js

创建“Hello World!”

JavaScript 和 .NET 开发之间的一个显著区别是编译器存在 (或不存在) 。 因此,简单的 Katana 服务器的起点是 Visual Studio 项目。 但是,我们可以从最少量的项目类型开始:空 ASP.NET Web 应用程序。

ASP.Net 项目 - WebApplication1 菜单的屏幕截图,其中描述了如何“开始”窗口窗格创建“Hello World”项目。显示一个窗口,其中包含可供选择的不同模板以及用于添加核心引用和单元测试的选项。

接下来,我们将 Microsoft.Owin.Host.SystemWeb NuGet 包安装到项目中。 此包提供在 ASP.NET 请求管道中运行的 OWIN 服务器。 可以在 NuGet 库 中找到它,并且可以使用 Visual Studio 包管理器对话框或包管理器控制台通过以下命令进行安装:

install-package Microsoft.Owin.Host.SystemWeb

安装包 Microsoft.Owin.Host.SystemWeb 将安装一些附加包作为依赖项。 其中一个依赖项是 Microsoft.Owin,它是一个库,它提供用于开发 OWIN 应用程序的多种帮助程序类型和方法。 我们可以使用这些类型快速编写以下“hello world”服务器。

public class Startup
{
   public void Configuration(IAppBuilder app)
   {
      app.Run(context =>
      {
         context.Response.ContentType = "text/plain";
         return context.Response.WriteAsync("Hello World!");
      });
   }
}

现在可以使用 Visual Studio 的 F5 命令运行这个非常简单的 Web 服务器,并且包含对调试的完全支持。

切换主机

默认情况下,前面的“hello world”示例在 ASP.NET 请求管道中运行,该管道在 IIS 上下文中使用 System.Web。 这本身可以增加巨大的价值,因为它使我们能够受益于 OWIN 管道的灵活性和可组合性,以及管理功能和 IIS 的整体成熟度。 但是,在某些情况下,IIS 提供的优势可能不是必需的,并且希望使用更小、更轻量级的主机。 那么,在 IIS 和 System.Web 之外运行简单的 Web 服务器需要什么?

为了说明可移植性目标,从 Web 服务器主机移动到命令行主机只需将新的服务器和主机依赖项添加到项目的输出文件夹,然后启动主机。 在此示例中,我们将在名为 OwinHost.exe 的 Katana 主机中托管 Web 服务器,并使用基于 Katana HttpListener 的服务器。 与其他 Katana 组件类似,将使用以下命令从 NuGet 获取这些组件:

install-package OwinHost

然后,在命令行中,我们可以导航到项目根文件夹,只需运行 OwinHost.exe 安装在其相应 NuGet 包) 的工具文件夹中的 (。 默认情况下, OwinHost.exe 配置为查找基于 HttpListener 的服务器,因此不需要其他配置。 在 Web 浏览器中导航,显示 http://localhost:5000/ 应用程序现在通过控制台运行。

开发人员命令 Promt 和浏览器窗口的屏幕截图,其中显示了在命令行上输入的命令的比较,以及 Web 浏览器中“Hello World”项目的外观。

Katana 体系结构

Katana 组件体系结构将应用程序划分为四个逻辑层,如下所示: 主机、服务器、中间件应用程序。 组件体系结构的考虑方式是,在许多情况下,可以轻松替换这些层的实现,而无需重新编译应用程序。

体系结构层图显示了四个图条,这些条描述了应用程序体系结构划分的逻辑层。

主机

主机负责:

  • 管理基础流程。

  • 协调工作流,以便选择服务器并构造将处理请求的 OWIN 管道。

    目前,基于 Katana 的应用程序有 3 个主要托管选项:

IIS/ASP.NET:使用标准 HttpModule 和 HttpHandler 类型,OWIN 管道可以作为 ASP.NET 请求流的一部分在 IIS 上运行。 ASP.NET 托管支持是通过将 Microsoft.AspNet.Host.SystemWeb NuGet 包安装到 Web 应用程序项目中启用的。 此外,由于 IIS 同时充当主机和服务器,因此 OWIN 服务器/主机的区别将在此 NuGet 包中混为一体,这意味着,如果使用 SystemWeb 主机,开发人员无法替代备用服务器实现。

自定义主机:Katana 组件套件使开发人员能够在自己的自定义进程中托管应用程序,无论是控制台应用程序、Windows 服务等。此功能看起来类似于 Web API 提供的自承载功能。 以下示例演示 Web API 代码的自定义主机:

static void Main()
{
    var baseAddress = new Uri("http://localhost:5000");

    var config = new HttpSelfHostConfiguration(baseAddress);
    config.Routes.MapHttpRoute("default", "{controller}");
       
    using (var svr = new HttpSelfHostServer(config))
    {
        svr.OpenAsync().Wait();
        Console.WriteLine("Press Enter to quit.");
        Console.ReadLine();
    }
}

Katana 应用程序的自承载设置类似:

static void Main(string[] args)
{
    const string baseUrl = "http://localhost:5000/";

    using (WebApplication.Start<Startup>(new StartOptions { Url = baseUrl })) 
    {
        Console.WriteLine("Press Enter to quit.");
        Console.ReadKey();
    }
}

Web API 和 Katana 自承载示例之间的一个显著区别是,Katana 自承载示例中缺少 Web API 配置代码。 为了同时实现可移植性和可组合性,Katana 将启动服务器的代码与配置请求处理管道的代码分开。 然后,配置 Web API 的代码包含在类 Startup 中,该类在 WebApplication.Start 中另外指定为类型参数。

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();
        config.Routes.MapHttpRoute("default", "{controller}");
        app.UseWebApi(config);
    }
}

本文稍后将更详细地讨论启动类。 但是,启动 Katana 自承载进程所需的代码看起来与你目前在 ASP.NET Web API自承载应用程序中可能使用的代码非常相似。

OwinHost.exe:虽然有些人希望编写自定义进程来运行 Katana Web 应用程序,但许多人更愿意只启动预建的可执行文件,该可执行文件可以启动服务器并运行其应用程序。 在此方案中,Katana 组件套件包括 OwinHost.exe。 从项目的根目录中运行时,此可执行文件将启动服务器, (默认情况下它使用 HttpListener 服务器) 并使用约定来查找和运行用户的启动类。 对于更精细的控制,可执行文件提供了许多额外的命令行参数。

开发人员命令提示符的屏幕截图,其中显示了在服务器上运行应用程序时命令提示符的代码示例。

服务器

虽然主机负责启动和维护运行应用程序的进程,但服务器的责任是打开网络套接字,侦听请求,并通过用户指定的 OWIN 组件的管道发送请求, (你可能已经注意到,此管道是在应用程序开发人员的 Startup 类) 中指定的。 目前,Katana 项目包括两个服务器实现:

  • Microsoft.Owin.Host.SystemWeb:如前所述,IIS 与 ASP.NET 管道同时充当主机和服务器。 因此,选择此托管选项时,IIS 会管理主机级问题,例如进程激活和侦听 HTTP 请求。 对于 ASP.NET Web 应用程序,它会将请求发送到 ASP.NET 管道。 Katana SystemWeb 主机注册 ASP.NET HttpModule 和 HttpHandler,以在请求流经 HTTP 管道时截获请求,并通过用户指定的 OWIN 管道发送请求。
  • Microsoft.Owin.Host.HttpListener:如其名称所示,此 Katana 服务器使用.NET Framework的 HttpListener 类打开套接字并将请求发送到开发人员指定的 OWIN 管道。 这是目前 Katana 自承载 API 和 OwinHost.exe的默认服务器选择。

中间件/框架

如前所述,当服务器接受来自客户端的请求时,它负责通过 OWIN 组件的管道传递它,这些组件由开发人员的启动代码指定。 这些管道组件称为中间件。
在非常基本的级别,OWIN 中间件组件只需实现 OWIN 应用程序委托,使其可调用。

Func<IDictionary<string, object>, Task>

但是,为了简化中间件组件的开发和组合,Katana 支持中间件组件的一些约定和帮助程序类型。 其中最常见的是 OwinMiddleware 类。 使用此类生成的自定义中间件组件如下所示:

public class LoggerMiddleware : OwinMiddleware
{
    private readonly ILog _logger;
 
    public LoggerMiddleware(OwinMiddleware next, ILog logger) : base(next)
    {
        _logger = logger;
    }
 
    public override async Task Invoke(IOwinContext context)
    {
        _logger.LogInfo("Middleware begin");
        await this.Next.Invoke(context);
        _logger.LogInfo("Middleware end");
    }
}

此类派生自 OwinMiddleware,实现一个构造函数,该构造函数接受管道中下一个中间件的实例作为其参数之一,然后将其传递给基构造函数。 用于配置中间件的其他参数也声明为下一个中间件参数后的构造函数参数。

在运行时,中间件通过重写 Invoke 的方法执行。 此方法采用 类型的 OwinContext单个参数。 此上下文对象由 Microsoft.Owin 前面所述的 NuGet 包提供,并提供对请求、响应和环境字典的强类型访问,以及一些其他帮助程序类型。

中间件类可以轻松地添加到应用程序启动代码中的 OWIN 管道,如下所示:

public class Startup
{
   public void Configuration(IAppBuilder app)
   {
      app.Use<LoggerMiddleware>(new TraceLogger());

   }
}

由于 Katana 基础结构只是构建 OWIN 中间件组件的管道,并且组件只需支持应用程序委托即可参与管道,因此中间件组件的复杂性范围从简单的记录器到整个框架(如 ASP.NET、Web API 或 SignalR)。 例如,将 ASP.NET Web API添加到以前的 OWIN 管道需要添加以下启动代码:

public class Startup
{
   public void Configuration(IAppBuilder app)
   {
      app.Use<LoggerMiddleware>(new TraceLogger());

      var config = new HttpConfiguration();
      // configure Web API 
      app.UseWebApi(config);

      // additional middleware registrations            
   }
}

Katana 基础结构将基于中间件组件添加到 Configuration 方法中 IAppBuilder 对象的顺序生成中间件组件的管道。 在我们的示例中,LoggerMiddleware 可以处理流经管道的所有请求,而不管这些请求的最终处理方式如何。 这样,中间件组件 ((例如身份验证组件) )可以处理管道的请求,该管道包含多个组件和框架 (例如 ASP.NET Web API、SignalR 和静态文件服务器) 。

应用程序

如前面的示例所示,不应将 OWIN 和 Katana 项目视为新的应用程序编程模型,而应将其视为将应用程序编程模型和框架与服务器和托管基础结构分离的抽象。 例如,在生成 Web API 应用程序时,开发人员框架将继续使用 ASP.NET Web API 框架,无论应用程序是否使用 Katana 项目中的组件在 OWIN 管道中运行。 应用程序开发人员可以看到与 OWIN 相关的代码的一个位置是应用程序启动代码,开发人员在其中组成 OWIN 管道。 在启动代码中,开发人员将注册一系列 UseXx 语句,通常针对将处理传入请求的每个中间件组件注册一个语句。 此体验的效果与在当前 System.Web 世界中注册 HTTP 模块的效果相同。 通常,更大的框架中间件(如 ASP.NET Web API 或 SignalR)将在管道末尾注册。 交叉中间件组件(例如用于身份验证或缓存的中间件组件)通常在管道的开头注册,以便它们处理管道中稍后注册的所有框架和组件的请求。 中间件组件彼此分离,从底层基础结构组件分离,使组件能够以不同的速度发展,同时确保整个系统保持稳定。

组件 - NuGet 包

与许多当前的库和框架一样,Katana 项目组件作为一组 NuGet 包提供。 对于即将推出的版本 2.0,Katana 包依赖项关系图如下所示。 (单击图像查看更大的视图。)

组件 - NuGet 包层次结构的关系图。此图描绘了库树,其中的框架为项目组件连接,并通过一组 NuGet 进行交付。

Katana 项目中几乎每个包都直接或间接依赖于 Owin 包。 你可能还记得,这是包含 IAppBuilder 接口的包,该接口提供 OWIN 规范第 4 节中所述的应用程序启动序列的具体实现。 此外,许多包依赖于 Microsoft.Owin,后者提供一组用于处理 HTTP 请求和响应的帮助程序类型。 包的其余部分可分类为托管基础结构包 (服务器或主机) 或中间件。 Katana 项目外部的包和依赖项以橙色显示。

Katana 2.0 的托管基础结构包括基于 SystemWeb 和 HttpListener 的服务器、用于使用 OwinHost.exe 运行 OWIN 应用程序的 OwinHost 包,以及用于在自定义主机 (中自托管 OWIN 应用程序的 Microsoft.Owin.Hosting 包,例如控制台应用程序、Windows 服务等)

对于 Katana 2.0,中间件组件主要侧重于提供不同的身份验证方法。 提供了一个用于诊断的附加中间件组件,该组件支持开始页和错误页。 随着 OWIN 成长为事实上的托管抽象,由 Microsoft 和第三方开发的中间件组件生态系统也将增加。

结论

从一开始,Katana 项目的目标就不是创建,因此迫使开发人员学习另一个 Web 框架。 相反,目标是创建一个抽象,为 .NET Web 应用程序开发人员提供比以前更多的选择。 通过将典型 Web 应用程序堆栈的逻辑层分解为一组可替换组件,Katana 项目使整个堆栈中的组件能够以对这些组件有意义的速度进行改进。 通过围绕简单的 OWIN 抽象生成所有组件,Katana 使框架和基于这些组件构建的应用程序能够跨各种不同的服务器和主机进行移植。 通过让开发人员控制堆栈,Katana 可确保开发人员最终选择 Web 堆栈的轻量级或功能丰富程度。

有关 Katana 的详细信息

致谢