前言
前后端分離,或許是現如今最為流行開發方式,包括UWP、Android和IOS這樣的手機客戶端都是需要調用后台的API來進行數據的交互。
但是這樣對前端開發和APP開發就會面臨這樣一個問題:如何知道每個API做什么?
可能,有人會在內部形成一份word文檔、pdf;有人會建立一個單獨的站點,然后將API的地址,參數等信息列在上面;有人會借助第三方的工具來生成一份文檔等。
當然,這基本是取決於不同公司的規范。
說起API文檔,就想到前段時間做的微信小程序,由於那個不完善的接口文檔,從而導致浪費了很大一部分時間去詢問接口相關的內容(用的是老的接口)。
為了處理這個問題,我認為,如果能在寫某個API的時候就順帶將這個API的相關信息一並處理了是最好不過!
不過這並不是讓我們寫好一個接口后,再去打開word等工具去編輯一下這個API的信息,這樣明顯需要花費更多的時間。
下面就針對這一問題,探討一下在Nancy中的實現。
如何實現
其實,想在Nancy中生成API文檔,是一件十分容易的事,因為作者thecodejunkie已經幫我們在Nancy內部提前做了一些處理
便於我們的后續擴展,這點還是很貼心的。
下面我們先來寫點東西,后面才能寫相應的API文檔。
public class ProductsModule : NancyModule
{
public ProductsModule() : base("/products")
{
Get("/", _ =>
{
return Response.AsText("product list");
}, null, "GetProductList");
Get("/{productid}", _ =>
{
return Response.AsText(_.productid as string);
}, null, "GetProductByProductId");
Post("/", _ =>
{
return Response.AsText("Add product");
}, null, "AddProduct");
//省略部分..
}
}
基本的CURD,沒有太多的必要去解釋這些內容。當然這里需要指出一點。
正常情況下,我們基本都是只寫前面兩個參數的,后面兩個參數是可選的。由於我們后面需要用到每個路由的名字
所以我們需要用到這里的第4個參數(當前路由的名字),也就意味着我們要在定義的時候寫多一點東西!
注: 1.x和2.x的寫法是有區別的!示例用的2.x的寫法,所以各位要注意這點!
以GET為例,方法定義大致如下
API寫好了,下面我們先來簡單獲取一下這些api的相關信息!
最簡單的實現
前面也提到,我們是要把這個api和api文檔放到同一個站點下面,免去編輯這一步驟!
世間萬物都是相輔相成的,我們不想單獨編輯,自然就要在代碼里面多做一些處理!
新起一個Module名為DocModule
,將api文檔的相關內容放到這個module中來處理。
public class DocMudule : NancyModule
{
private IRouteCacheProvider _routeCacheProvider;
public DocMudule(IRouteCacheProvider routeCacheProvider) : base("/docs")
{
this._routeCacheProvider = routeCacheProvider;
Get("/", _ =>
{
var routeDescriptionList = _routeCacheProvider
.GetCache()
.SelectMany(x => x.Value)
.Select(x => x.Item2)
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.ToList();
return Response.AsJson(routeDescriptionList);
});
}
}
沒錯,你沒看錯,就是這幾行代碼,就可以幫助我們去生成我們想要的api文檔!其實最主要的是IRouteCacheProvider這個接口。
它的具體實現,會在后面的小節講到,現在先着重於使用!
先調用這個接口的GetCache方法,以拿到緩存的路由信息,這個路由信息有必要來看一下它的定義,因為不看它的定義,我們根本就沒有辦法繼續下去!
后續的查找都是依賴於這些緩存信息!
public interface IRouteCache : IDictionary<Type, List<Tuple<int, RouteDescription>>>, ICollection<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>, IEnumerable<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>, IEnumerable
{
bool IsEmpty();
}
看了上面的定義,就可以清楚的知道要用SelectMany去拿到那個元組的內容。再取出元組的RouteDescription。
當然,這個時候我們取到的是所有的路由信息,這些信息都包含了什么內容呢?看看RouteDescription的定義就很清晰了。
public sealed class RouteDescription
{
public RouteDescription(string name, string method, string path, Func<NancyContext, bool> 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 Func<NancyContext, bool> 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(IDictionary<Type, object> metadata);
//Gets the raw metadata System.Collections.Generic.IDictionary`2.
public IDictionary<Type, object> Raw { get; }
//Gets a boolean that indicates if the specific type of metadata is stored.
public bool Has<TMetadata>();
//Retrieves metadata of the provided type.
public TMetadata Retrieve<TMetadata>();
}
這里對我們比較重要的是Raw這個屬性,因為這個是在返回結果中的一部分,它是一個字典,鍵是類型,值是這個類型對應的實例。
先定義一個CustomRouteMetadata
,用於返回路由的Metadata信息(可根據具體情況進行相應的定義)。這個CustomRouteMetadata
就是上述字典Type。
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 class ProductsMetadataModule : MetadataModule<RouteMetadata>
{
public ProductsMetadataModule()
{
Describe["GetProductList"] = desc =>
{
var dic = new Dictionary<System.Type, object>
{
{
typeof(CustomRouteMetadata),
new CustomRouteMetadata
{
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 =>
{
var dic = new Dictionary<System.Type, object>
{
{
typeof(CustomRouteMetadata),
new CustomRouteMetadata
{
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 class ProductsMetadataModule : MetadataModule<CustomRouteMetadata>
{
public ProductsMetadataModule()
{
Describe["GetProductList"] = desc =>
{
return new CustomRouteMetadata
{
Group = "Products",
Description = "Get All Products from Database",
Path = desc.Path,
Method = desc.Method,
Name = desc.Name,
Segments = desc.Segments
};
};
Describe["GetProductByProductId"] = desc =>
{
return new CustomRouteMetadata
{
Group = "Products",
Description = "Get a Product by product id",
Path = desc.Path,
Method = desc.Method,
Name = desc.Name,
Segments = desc.Segments
};
};
//省略部分..
}
}
由於MetadataModule<TMetadata>
中的TMetadata是自定義的CustomRouteMetadata
,所以在返回的時候直接創建一個簡單的實例即可
不需要像RouteMetadata
那樣還要定義一個字典。
同時,還要把DocModule
中RetrieveMetadata的TMetadata也要替換成CustomRouteMetadata
var routeDescriptionList = _routeCacheProvider
.GetCache()
//.RetrieveMetadata<RouteMetadata>()
.RetrieveMetadata<CustomRouteMetadata>()
.Where(x => x != null);
經過這兩步的簡化,現在得到的效果就是我們需要的結果了!
最后,當然要專業一點,不能讓人只看json吧!怎么都要添加一個html頁面,將這些信息展示出來:
當然,現在看上去還是很丑,文檔內容也並不豐富,但是已經把最簡單的文檔做出來了,想要進一步豐富它就可以自由發揮了。
實現探討
既然這樣簡單的代碼就能幫助我們去生成api文檔,很有必要去研究一下Nancy幫我們做了什么事!
從最開始的IRouteCacheProvider入手,這個接口對應的默認實現DefaultRouteCacheProvider
public class DefaultRouteCacheProvider : IRouteCacheProvider, IDiagnosticsProvider
{
/// <summary>
/// The route cache factory
/// </summary>
protected readonly Func<IRouteCache> RouteCacheFactory;
/// <summary>
/// Initializes a new instance of the DefaultRouteCacheProvider class.
/// </summary>
/// <param name="routeCacheFactory"></param>
public DefaultRouteCacheProvider(Func<IRouteCache> routeCacheFactory)
{
this.RouteCacheFactory = routeCacheFactory;
}
/// <summary>
/// Gets an instance of the route cache.
/// </summary>
/// <returns>An <see cref="IRouteCache"/> instance.</returns>
public IRouteCache GetCache()
{
return this.RouteCacheFactory();
}
//省略部分..
}
里面的GetCache方法是直接調用了定義的委托變量。最終是到了IRouteCache的實現類RouteCache,這個類算是一個重點觀察對象!
內容有點多,就只貼出部分核心代碼了
它在構造函數里去生成了路由的相關信息。
public RouteCache(
INancyModuleCatalog moduleCatalog,
INancyContextFactory contextFactory,
IRouteSegmentExtractor routeSegmentExtractor,
IRouteDescriptionProvider routeDescriptionProvider,
ICultureService cultureService,
IEnumerable<IRouteMetadataProvider> routeMetadataProviders)
{
this.routeSegmentExtractor = routeSegmentExtractor;
this.routeDescriptionProvider = routeDescriptionProvider;
this.routeMetadataProviders = routeMetadataProviders;
var request = new Request("GET", "/", "http");
using (var context = contextFactory.Create(request))
{
this.BuildCache(moduleCatalog.GetAllModules(context));
}
}
具體的生成方法如下:遍歷所有的NancyModule,找到每個Module的RouteDescription集合(一個Module可以包含多個路由)
然后找到每個RouteDescription的描述,路由片段和metadata的信息。最后把這個Module路由信息添加到當前的對象中!
private void BuildCache(IEnumerable<INancyModule> modules)
{
foreach (var module in modules)
{
var moduleType = module.GetType();
var routes =
module.Routes.Select(r => r.Description).ToArray();
foreach (var routeDescription in routes)
{
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的實例。
private RouteMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription)
{
var data = new Dictionary<Type, object>();
foreach (var provider in this.routeMetadataProviders)
{
var type = provider.GetMetadataType(module, routeDescription);
var metadata = provider.GetMetadata(module, routeDescription);
if (type != null && metadata != null)
{
data.Add(type, metadata);
}
}
return new RouteMetadata(data);
}
重點的是provider。這個provider來源來IRouteMetadataProvider,這個接口就兩個方法。
Nancy這個項目中還有一個抽象類是繼承了這個接口的。但是這個抽象類是沒有默認實現的。
public abstract class RouteMetadataProvider<TMetadata> : IRouteMetadataProvider
{
public Type GetMetadataType(INancyModule module, RouteDescription routeDescription)
{
return typeof(TMetadata);
}
public object GetMetadata(INancyModule module, RouteDescription routeDescription)
{
return this.GetRouteMetadata(module, routeDescription);
}
protected abstract TMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription);
}
注:前面的原理分析都是基於Nancy這個項目。
這個時候,另外一個項目Nancy.Metadata.Modules就起作用了。我們編寫的MetadataModule也是要添加這個的引用才能正常使用的。
從上面編寫的MetadataModule可以看出這個項目的起點應該是MetadataModule,而且有關metadata的核心也在這里了。
public abstract class MetadataModule<TMetadata> : IMetadataModule where TMetadata : class
{
private readonly IDictionary<string, Func<RouteDescription, TMetadata>> metadata;
protected MetadataModule()
{
this.metadata = new Dictionary<string, Func<RouteDescription, TMetadata>>();
}
// Gets <see cref="RouteMetadataBuilder"/> for describing routes.
public RouteMetadataBuilder 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 class RouteMetadataBuilder
{
private readonly MetadataModule<TMetadata> parentModule;
public RouteMetadataBuilder(MetadataModule<TMetadata> metadataModule)
{
this.parentModule = metadataModule;
}
// Describes metadata for a route with the specified name.
public Func<RouteDescription, TMetadata> this[string name]
{
set { this.AddRouteMetadata(name, value); }
}
protected void AddRouteMetadata(string name, Func<RouteDescription, TMetadata> value)
{
this.parentModule.metadata.Add(name, value);
}
}
//省略部分..
}
到這里,已經將GetCache的內內外外都簡單分析了一下。至於擴展方法RetrieveMetadata就不在細說了,只是selectmany和select的一層封裝。
寫在最后
本文粗略講解了如何在Nancy中生成API文檔,以及簡單分析了其內部的處理。
下一篇將繼續介紹這一塊的內容,不過主角是Swagger。