1. 程式人生 > >淺析如何在Nancy中生成API文檔

淺析如何在Nancy中生成API文檔

2.x span build inter 修改 ace 目前 前端開發 strac

原文:淺析如何在Nancy中生成API文檔

前言

前後端分離,或許是現如今最為流行開發方式,包括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對象,便於查看有什麽內容,便於在逐步完善後再把它結構化。

下面是最簡單實現後的大致效果:

技術分享圖片

在圖中,可以看到GetProductListGetProductByProductId這兩個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那樣還要定義一個字典。

同時,還要把DocModuleRetrieveMetadata的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。

淺析如何在Nancy中生成API文檔