前后端分离,或许是现如今最为流行开发方式,包括 UWP、Android 和 IOS 这样的手机客户端都是需要调用后台的 API 来进行数据的交互。
但是这样对前端开发和 APP 开发就会面临这样一个问题:如何知道每个 API 做什么?
可能,有人会在内部形成一份 word 文档、pdf;有人会建立一个单独的站点,然后将 API 的地址,参数等信息列在上面;有人会借助第三方的工具来生成一份文档等。
当然,这基本是取决于不同公司的规范。
说起 API 文档,就想到前段时间做的微信小程序,由于那个不完善的接口文档,从而导致浪费了很大一部分时间去询问接口相关的内容 (用的是老的接口)。
为了处理这个问题,我认为,如果能在写某个 API 的时候就顺带将这个 API 的相关信息一并处理了是最好不过!
不过这并不是让我们写好一个接口后,再去打开 word 等工具去编辑一下这个 API 的信息,这样明显需要花费更多的时间。
下面就针对这一问题,探讨一下在 Nancy 中的实现。
其实,想在 Nancy 中生成 API 文档,是一件十分容易的事,因为作者 thecodejunkie 已经帮我们在 Nancy 内部提前做了一些处理
便于我们的后续扩展,这点还是很贴心的。
下面我们先来写点东西,后面才能写相应的 API 文档。
- public classProductsModule : NancyModule
- {public ProductsModule() :base("/products")
- {Get("/", _ =>
- {returnResponse.AsText("product list");
- },null,"GetProductList");Get("/{productid}", _ =>
- {returnResponse.AsText(_.productid as string);
- },null,"GetProductByProductId");Post("/", _ =>
- {returnResponse.AsText("Add product");
- },null,"AddProduct");//省略部分..}
- }
基本的 CURD,没有太多的必要去解释这些内容。当然这里需要指出一点。
正常情况下,我们基本都是只写前面两个参数的,后面两个参数是可选的。由于我们后面需要用到每个路由的名字
所以我们需要用到这里的第 4 个参数 (当前路由的名字),也就意味着我们要在定义的时候写多一点东西!
注: 1.x 和 2.x 的写法是有区别的!示例用的 2.x 的写法,所以各位要注意这点!
以 GET 为例,方法定义大致如下
API 写好了,下面我们先来简单获取一下这些 api 的相关信息!
前面也提到,我们是要把这个 api 和 api 文档放到同一个站点下面,免去编辑这一步骤!
世间万物都是相辅相成的,我们不想单独编辑,自然就要在代码里面多做一些处理!
新起一个 Module 名为
,将 api 文档的相关内容放到这个 module 中来处理。
- DocModule
- public classDocMudule : NancyModule
- {privateIRouteCacheProvider _routeCacheProvider;public DocMudule(IRouteCacheProvider routeCacheProvider) :base("/docs")
- {this._routeCacheProvider= routeCacheProvider;Get("/", _ =>
- {varrouteDescriptionList = _routeCacheProvider
- .GetCache()
- .SelectMany(x => x.Value)
- .Select(x => x.Item2)
- .Where(x => !string.IsNullOrWhiteSpace(x.Name))
- .ToList();returnResponse.AsJson(routeDescriptionList);
- });
- }
- }
没错,你没看错,就是这几行代码,就可以帮助我们去生成我们想要的 api 文档!其实最主要的是 IRouteCacheProvider 这个接口。
它的具体实现,会在后面的小节讲到,现在先着重于使用!
先调用这个接口的 GetCache 方法,以拿到缓存的路由信息,这个路由信息有必要来看一下它的定义,因为不看它的定义,我们根本就没有办法继续下去!
后续的查找都是依赖于这些缓存信息!
- public interface IRouteCache: IDictionaryint,
- RouteDescription >>> ,
- ICollectionint,
- RouteDescription >>> >,
- IEnumerableint,
- RouteDescription >>> >,
- IEnumerable {
- bool IsEmpty();
- }
看了上面的定义,就可以清楚的知道要用 SelectMany 去拿到那个元组的内容。再取出元组的 RouteDescription。
当然,这个时候我们取到的是所有的路由信息,这些信息都包含了什么内容呢?看看 RouteDescription 的定义就很清晰了。
- public sealed class RouteDescription {
- public RouteDescription(string name, string method, string path, Funcbool > condition);
- //The name of the route
- public string Name {
- get;
- set;
- }
- //The condition that has to be fulfilled inorder for the route to be a valid match.
- public Funcbool > Condition {
- get;
- }
- //The description of what the route is for.
- public string Description {
- get;
- set;
- }
- //Gets or sets the metadata information for a route.
- public RouteMetadata Metadata {
- get;
- set;
- }
- //Gets the method of the route.
- public string Method {
- get;
- }
- //Gets the path that the route will be invoked for.
- public string Path {
- get;
- }
- //Gets or set the segments, for the route, that was returned by the Nancy.Routing.IRouteSegmentExtractor.
- public IEnumerable < string > Segments {
- get;
- set;
- }
- }
在查询之后,我还过滤了那些名字为空的,不让它们显示出来。为什么不显示出来呢?理由也比较简单,像 DocModule,我们只定义了一个路由
而且这个路由在严格意义上并不属于我们 api 的内容,而且这个路由也是没有定义名字的,所以显示出来的意义也不大。
过滤之后,就得到了最终想要的信息!简单起见,这里是先直接 返回一个 json 对象,便于查看有什么内容,便于在逐步完善后再把它结构化。
下面是最简单实现后的大致效果:
在图中,可以看到 GetProductList 和 GetProductByProductId 这两个 api 的基本信息:请求的 method,请求的路径和路由片段。
但是这些信息真的是太少了!连 api 描述都见不到,拿出来,肯定被人狠狠的骂一顿!!
下面我们要尝试丰富一下我们的接口信息!
要让文档充实,总是需要一个切入点,找到切入点,事情就好办了。仔细观察上面的效果图会发现,里面的 metadata 是空的。当然这个也就是丰富文档内容的切入点了。
从前面的定义可以看到,这个 metadata 是一个 RouteMetadata 的实例
- public class RouteMetadata {
- //Creates a new instance of the Nancy.Routing.RouteMetadata class.
- public RouteMetadata(IDictionaryobject > metadata);
- //Gets the raw metadata System.Collections.Generic.IDictionary`2.
- public IDictionaryobject > Raw {
- get;
- }
- //Gets a boolean that indicates if the specific type of metadata is stored.
- public bool Has();
- //Retrieves metadata of the provided type.
- public TMetadata Retrieve();
- }
这里对我们比较重要的是 Raw 这个属性,因为这个是在返回结果中的一部分,它是一个字典,键是类型,值是这个类型对应的实例。
先定义一个
,用于返回路由的 Metadata 信息 (可根据具体情况进行相应的定义)。这个
- CustomRouteMetadata
就是上述字典 Type。
- CustomRouteMetadata
- public class CustomRouteMetadata {
- // group by the module
- public string Group {
- get;
- set;
- }
- // description of the api
- public string Description {
- get;
- set;
- }
- // path of the api
- public string Path {
- get;
- set;
- }
- // http method of the api
- public string Method {
- get;
- set;
- }
- // name of the api
- public string Name {
- get;
- set;
- }
- // segments of the api
- public IEnumerable < string > Segments {
- get;
- set;
- }
- }
定义好我们要显示的东西后,自然要把这些东西用起来,才能体现它们的价值。
要用起来还涉及到一个 MetadataModule,这个命名很像 NancyModule,看上去都是一个 Module。
先定义一个
,让它继承
- ProductsMetadataModule
, 具体实现如下:
- MetadataModule<RouteMetadata>
- public classProductsMetadataModule : MetadataModule
- {
- public ProductsMetadataModule()
- {
- Describe["GetProductList"] = desc =>
- {vardic =newDictionaryType,object>
- {
- {typeof(CustomRouteMetadata),newCustomRouteMetadata
- {
- Group ="Products",
- Description ="Get All Products from Database",
- Path = desc.Path,
- Method = desc.Method,
- Name = desc.Name,
- Segments = desc.Segments}
- }
- };return new RouteMetadata(dic);
- };
- Describe["GetProductByProductId"] = desc =>
- {vardic =newDictionaryType,object>
- {
- {typeof(CustomRouteMetadata),newCustomRouteMetadata
- {
- Group ="Products",
- Description ="Get a Product by product id",
- Path = desc.Path,
- Method = desc.Method,
- Name = desc.Name,
- Segments = desc.Segments}
- }
- };return new RouteMetadata(dic);
- };//省略部分...}
- }
这里的写法就和 1.x 里写 NancyModule 的内容是一样的,应该也是比较熟悉的。就不再累赘了。其中的 desc 是一个委托
。
- Func<RouteDescription, TMetadata>
默认返回的是一个 RouteMetadata 实例,而要创建一个这样的实例还需要一个字典,所以大家能看到上面的代码中定义了一个字典。
并且这个字典包含了我们自己定义的信息,其中 Group 和 Description 是完全的自定义,其他的是从 RouteDescription 中拿。
当然,这里已经开了一个口子,想怎么定义都是可以的!
完成上面的代码之后,再来看看我们显示的结果
可以看到我们添加的 metadata 相关的内容已经出来了!可能这个时候,大家也都发现了,似乎内容有那么点重复的意思!
因为这些重复,就会让人感觉这里比较臃肿,所以我们肯定不需要取出太多重复的东西,目前只需要 metadata 下面的这些就可以了。
下面来对其进行简化!
简化分为两步:
第一步简化:DocModule 的简化。
其实,DocModule 已经是相当的简单了,但是还能在简洁一点点。这里用到了 RetrieveMetadata 这个扩展方法来处理。
前面的做法是拿到路由的信息后,用了两个 Select 来查询,而且查询出来的结果有那么一点臃肿,
而借助扩展方法,可以只取 metadata 里面的内容,也就是前面自定义的内容,这才是我们真正意义上要用到的。
下面是具体实现的示例:
- Get("/", _ =>
- {
- //01
- //var routeDescriptionList = _routeCacheProvider
- // .GetCache()
- // .SelectMany(x => x.Value)
- // .Select(x => x.Item2)
- // .Where(x => !string.IsNullOrWhiteSpace(x.Name))
- // .ToList();
- //return Response.AsJson(routeDescriptionList);
- //02
- var routeDescriptionList = _routeCacheProvider
- .GetCache()
- .RetrieveMetadata<RouteMetadata>()
- .Where(x => x != null);
- return Response.AsJson(routeDescriptionList);
- });
经过第一步简化后,已经过滤了不少重复的信息了,效果如下:
第二步简化:Metadata 的简化
在返回 Metadata 的时候,我们是返回了一个默认的
对象,这个对象相比自定义的
- RouteMetadata
复杂了不少
- CustomRouteMetadata
而且从上面经过第一步简化后的效果图也可以发现,只有 value 节点下面的内容才是 api 文档需要的内容。
所以还要考虑用自定义的这个 CustomRouteMetadata 去代替原来的。
修改如下:
- public classProductsMetadataModule : MetadataModule
- {
- public ProductsMetadataModule()
- {
- Describe["GetProductList"] = desc =>
- {return newCustomRouteMetadata
- {
- Group ="Products",
- Description ="Get All Products from Database",
- Path = desc.Path,
- Method = desc.Method,
- Name = desc.Name,
- Segments = desc.Segments};
- };
- Describe["GetProductByProductId"] = desc =>
- {return newCustomRouteMetadata
- {
- Group ="Products",
- Description ="Get a Product by product id",
- Path = desc.Path,
- Method = desc.Method,
- Name = desc.Name,
- Segments = desc.Segments};
- };//省略部分..}
- }
由于
中的 TMetadata 是自定义的
- MetadataModule<TMetadata>
,所以在返回的时候直接创建一个简单的实例即可
- CustomRouteMetadata
不需要像
那样还要定义一个字典。
- RouteMetadata
同时,还要把
中 RetrieveMetadata 的 TMetadata 也要替换成 CustomRouteMetadata
- DocModule
- varrouteDescriptionList = _routeCacheProvider
- .GetCache()//.RetrieveMetadata<RouteMetadata>() .RetrieveMetadata()
- .Where(x => x !=null);
经过这两步的简化,现在得到的效果就是我们需要的结果了!
最后,当然要专业一点,不能让人只看 json 吧!怎么都要添加一个 html 页面,将这些信息展示出来:
当然,现在看上去还是很丑,文档内容也并不丰富,但是已经把最简单的文档做出来了,想要进一步丰富它就可以自由发挥了。
既然这样简单的代码就能帮助我们去生成 api 文档,很有必要去研究一下 Nancy 帮我们做了什么事!
从最开始的 IRouteCacheProvider 入手,这个接口对应的默认实现 DefaultRouteCacheProvider
- public classDefaultRouteCacheProvider : IRouteCacheProvider, IDiagnosticsProvider
- {/// <summary>
- /// The route cache factory
- /// </summary>
- protected readonlyFunc RouteCacheFactory;
- /// <summary>
- /// Initializes a new instance of the DefaultRouteCacheProvider class.
- /// </summary>
- /// <param name="routeCacheFactory"></param>
- public DefaultRouteCacheProvider(Func routeCacheFactory)
- {
- this.RouteCacheFactory= routeCacheFactory;
- }/// <summary>
- /// Gets an instance of the route cache.
- /// </summary>
- /// <returns>An <see cref="IRouteCache"/> instance.</returns>
- publicIRouteCacheGetCache()
- {return this.RouteCacheFactory();
- }//省略部分..}
里面的 GetCache 方法是直接调用了定义的委托变量。最终是到了 IRouteCache 的实现类 RouteCache,这个类算是一个重点观察对象!
内容有点多,就只贴出部分核心代码了
它在构造函数里去生成了路由的相关信息。
- public RouteCache(
- INancyModuleCatalog moduleCatalog,
- INancyContextFactory contextFactory,
- IRouteSegmentExtractor routeSegmentExtractor,
- IRouteDescriptionProvider routeDescriptionProvider,
- ICultureService cultureService,
- IEnumerable routeMetadataProviders)
- {
- this.routeSegmentExtractor= routeSegmentExtractor;this.routeDescriptionProvider= routeDescriptionProvider;this.routeMetadataProviders= routeMetadataProviders;varrequest =new Request("GET","/","http");using(varcontext = contextFactory.Create(request))
- {this.BuildCache(moduleCatalog.GetAllModules(context));
- }
- }
具体的生成方法如下:遍历所有的 NancyModule,找到每个 Module 的 RouteDescription 集合 (一个 Module 可以包含多个路由)
然后找到每个 RouteDescription 的描述,路由片段和 metadata 的信息。最后把这个 Module 路由信息添加到当前的对象中!
- private void BuildCache(IEnumerable modules)
- {
- foreach(var moduleinmodules)
- {varmoduleType = module.GetType();
- var routes =
- module.Routes.Select(r => r.Description).ToArray();foreach(var routeDescriptioninroutes)
- {
- routeDescription.Description=this.routeDescriptionProvider.GetDescription(module, routeDescription.Path);
- routeDescription.Segments=this.routeSegmentExtractor.Extract(routeDescription.Path).ToArray();
- routeDescription.Metadata=this.GetRouteMetadata(module, routeDescription);
- }this.AddRoutesToCache(routes, moduleType);
- }
- }
前面提到 RouteDescription 的描述,路由片段和 metadata 的信息都是通过额外的方式拿到的,这里主要是拿 metadata 来做说明
毕竟在上面最后的一个例子中,用到的是 metadata 的内容。
先调用定义的私有方法 GetRouteMetadata,这个方法里面的内容是不是和前面的 MetadataModule 有点类似呢,字典和创建 RouteMetadata 的实例。
- privateRouteMetadataGetRouteMetadata(INancyModule module, RouteDescription routeDescription)
- {vardata =newDictionaryobject>();foreach(var providerin this.routeMetadataProviders)
- {vartype = provider.GetMetadataType(module, routeDescription);varmetadata = provider.GetMetadata(module, routeDescription);if(type !=null&& metadata !=null)
- {
- data.Add(type, metadata);
- }
- }return new RouteMetadata(data);
- }
重点的是 provider。这个 provider 来源来 IRouteMetadataProvider,这个接口就两个方法。
Nancy 这个项目中还有一个抽象类是继承了这个接口的。但是这个抽象类是没有默认实现的。
- public abstract classRouteMetadataProvider : IRouteMetadataProvider
- {
- publicTypeGetMetadataType(INancyModule module, RouteDescription routeDescription)
- {return typeof(TMetadata);
- }public object GetMetadata(INancyModule module, RouteDescription routeDescription)
- {return this.GetRouteMetadata(module, routeDescription);
- }protected abstractTMetadataGetRouteMetadata(INancyModule module, RouteDescription routeDescription);
- }
注:前面的原理分析都是基于 Nancy 这个项目。
这个时候,另外一个项目 Nancy.Metadata.Modules 就起作用了。我们编写的 MetadataModule 也是要添加这个的引用才能正常使用的。
从上面编写的 MetadataModule 可以看出这个项目的起点应该是 MetadataModule,而且有关 metadata 的核心也在这里了。
- public abstract classMetadataModule : IMetadataModule where TMetadata : class{private readonlyIDictionary<string, Func> metadata;
- protected MetadataModule()
- {this.metadata=newDictionary<string, Func>();
- }
- // Gets <see cref="RouteMetadataBuilder"/> for describing routes.
- publicRouteMetadataBuilder Describe
- {get{return new RouteMetadataBuilder(this); }
- }// Returns metadata for the given RouteDescription.
- public object GetMetadata(RouteDescription description)
- {if(this.metadata.ContainsKey(description.Name))
- {return this.metadata[description.Name].Invoke(description);
- }return null;
- }// Helper class for configuring a route metadata handler in a module.
- public classRouteMetadataBuilder
- {private readonlyMetadataModule parentModule;
- public RouteMetadataBuilder(MetadataModule metadataModule)
- {
- this.parentModule= metadataModule;
- }// Describes metadata for a route with the specified name.
- publicFunc this[stringname]
- {set{this.AddRouteMetadata(name, value); }
- }protected void AddRouteMetadata(stringname, Func value)
- {
- this.parentModule.metadata.Add(name, value);
- }
- }//省略部分..}
到这里,已经将 GetCache 的内内外外都简单分析了一下。至于扩展方法 RetrieveMetadata 就不在细说了,只是 selectmany 和 select 的一层封装。
本文粗略讲解了如何在 Nancy 中生成 API 文档,以及简单分析了其内部的处理。
下一篇将继续介绍这一块的内容,不过主角是 Swagger。
来源: http://www.cnblogs.com/catcher1994/p/6791352.html