1. 程式人生 > 實用技巧 >大家是怎麼做APP介面的版本控制的?歡迎進來看看我的方案。升級版的Versioning

大家是怎麼做APP介面的版本控制的?歡迎進來看看我的方案。升級版的Versioning

背景

APP不同於網站,網站程式一發版,所有使用者看到的都是最新的頁面、呼叫最新的介面,沒有新老版本一說。APP一旦下載到使用者手機上,使用者不更新你拿他一點辦法都沒有,但是隨著業務的調整,同一個介面的請求引數和輸出JSON有變化的話,就需要考慮老版本的相容問題了。

舉個例子:某APP的1.0.0版,服務端使用者資訊介面(api/User/UserInfo)輸出的JSON如下

{
  "status": 200,
  "message": "ok",
  "data": {
    "userId": 10001,
    "userNickName": "oppoic",
    "userName": "
汪傑", "userEmail": "[email protected]", "userBlog": "https://www.cnblogs.com/oppoic/", "userPic": "https://pic.cnblogs.com/avatar/401362/20180415232220.png" } }

1.0.0版上線之後,產品迭代增加需求:一級選單加一個付費使用者入口,APP一開啟就要動態判斷當前使用者是否付費。幾個研發一商量,把使用者是否vip的狀態放到使用者資訊介面最合適。修改後服務端使用者資訊介面(api/User/UserInfo)輸出如下

{
  "status": 200,
  
"message": "ok", "data": { "user": { "id": 10001, "nickName": "oppoic", "name": "汪傑", "email": "[email protected]", "blog": "https://www.cnblogs.com/oppoic/", "pic": "https://pic.cnblogs.com/avatar/401362/20180415232220.png" }, "vip": true } }

修改前後的JSON有如下變化:

1)增加了是否vip欄位;

2)為了結構更清晰,原來的userId、userName、userPic等屬性移到到user物件下;

介面改好了之後APP端通過data.vip就可以判斷當前使用者是否vip了,同時也把原來data.userName改成了data.user.name等等。都改好後升了一個版本號,由原來的1.0.0改成了1.0.1發到了各大商店。

但是一上線就出問題了:升級1.0.1的使用者打開個人資訊頁面正常,但是未升級的1.0.0使用者打開個人資訊頁面報錯

VM211:1 Uncaught TypeError: Cannot read property 'name' of undefined
at <anonymous>:1:16

原因是服務端個人資訊介面的輸出JSON改了,只能適配1.0.1的新版APP,無法滿足老使用者了。

有人說需求變了我就改服務端介面名稱,老使用者調老介面,新使用者調新介面。這個方法是可以,但是介面多了簡直就是一場災難,專案從此無法維護。

路由方案

通過配置路由來區分不同版本的介面

config.Routes.MapHttpRoute(
            name: "DefaultApiWithVersion",
            routeTemplate: "api/v{version}/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
);

APP端請求:api/v1/User/UserInfo,這種方案缺點如下:

1)版本號通常都是三段式的,例:1.0.1,硬套這個的話,服務端要建個名稱空間帶“.”的檔案,C#對“.”支援不太友好。如果要改成1_0_1或1-0-1服務端是可以了,但APP端又得轉下格式,怎麼都有點彆扭;

2)這個路由配得太死,1.0.1就是1.0.1的路由,APP端帶1.0.2來就調不通。實際場景中後端沒法完全對應APP版本,APP甚至連稽核不通過重新提交都需要修改版本號,後端必須能向前相容;

3)最後一個問題最致命:方法不可以複用,1.0.1版本的UserController裡寫了10個方法,1.0.2版只改了其中1個方法,還得把其他9個方法都原樣搬過來。

理想的效果應該是這樣:APP請求介面的時候把版本號(這個版本號就是上架各大商店的版本號)放到請求header裡,服務端根據版本號找控制器,找到了直接呼叫,沒有的話就向前找;同時方法也要向前找,帶著這些需求尋找解決方案。

versioning

找下網路上的方案,aspnet-api-versioning用的最多,原來不止我一個人有這個需求。這個Versioning是微軟出的,同時支援Core,簡直靠譜,建個Web API專案試試

NuGet安裝

Web API:Install-Package Microsoft.AspNet.WebApi.Versioning

Core:Install-Package Microsoft.AspNetCore.Mvc.Versioning

修改WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.AddApiVersioning(c =>
        {
            c.ReportApiVersions = true;//是否在請求頭中返回受支援的版本資訊
            c.ApiVersionReader = new HeaderApiVersionReader("version");//header裡版本號的鍵名
            c.AssumeDefaultVersionWhenUnspecified = true;//請求沒有指明版本的情況下是否使用預設的版本
            c.DefaultApiVersion = new ApiVersion(1, 0);//預設的版本號
        });
    }
}

新建兩個UserController

程式碼

namespace webApiVersioning.Controllers.v1
{
    [ApiVersion("1.0")]
    public class UserController : ApiController
    {
        [HttpGet]
        public string Get()
        {
            return "1.0";
        }
    }
}
namespace webApiVersioning.Controllers.v2
{
    [ApiVersion("2.0")]
    public class UserController : ApiController
    {
        [HttpGet]
        public string Get()
        {
            return "2.0";
        }
    }
}

header裡version分別傳1.0和2.0效果如下

從輸出來看,前端呼叫相同的介面,通過傳遞不同的version引數觸發了服務端不同控制器的方法。但是缺點明顯:

1)版本號沒法配置成三段式的,現在所有商店裡APP的版本都是三段式的;

2)跟上面的路由方案一樣,沒法向前找控制器。verson傳3.0,不會自動找到2.0的控制器,不夠靈活;

3)更不支援向前找方法。。。

終極方案

微軟出的Versioning既然不能滿足需求就自己實現一個,難點就一個:根據APP端傳來的版本號觸發對應的控制器。

版本號好獲取,控制器也可以建個User{version}Controller,那怎麼做到APP端帶101的版本號請求UserController,服務端根據傳的101自動轉到User101Controller呢?

利用搜索引擎找到了System.Web.Http.Dispatcher名稱空間下的DefaultHttpControllerSelector類,下面有個GetControllerName方法

// 摘要:
//     獲取指定 System.Net.Http.HttpRequestMessage 的控制器的名稱。
//
// 引數:
//   request:
//     HTTP 請求訊息。
//
// 返回結果:
//     指定 System.Net.Http.HttpRequestMessage 的控制器的名稱。
public virtual string GetControllerName(HttpRequestMessage request)

根據請求request獲取指定控制器名稱,那豈不是可以請求UserController,實際觸發User101Controller了。建一個CustomHeaderControllerSelector繼承DefaultHttpControllerSelector類,重寫下GetControllerName方法試一試

public class CustomHeaderControllerSelector : DefaultHttpControllerSelector
{
    public CustomHeaderControllerSelector(HttpConfiguration cfg) : base(cfg)
    { }

    public override string GetControllerName(HttpRequestMessage request)
    {
        var controllerRequest = base.GetControllerName(request);//User
        return controllerRequest + "101";//User101
    }
}

註冊到Global.asax裡

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configure(WebApiConfig.Register);
        GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new CustomHeaderControllerSelector(GlobalConfiguration.Configuration));
    }
}

新建一個User101Controller

public class User101Controller : ApiController
{
    [HttpGet]
    public string UserInfo()
    {
        return "101";
    }
}

專案裡並沒有UserController,請求下試試

可以看出,請求的是UserController,觸發的是User101Controller,完美了。再建一個User102Controller

public class User102Controller : ApiController
{
    [HttpGet]
    public string UserInfo()
    {
        return "102";
    }
}

完善下GetControllerName方法

public override string GetControllerName(HttpRequestMessage request)
{
    var controllerRequest = base.GetControllerName(request);
    if (request.Headers.Contains("version"))
    {
        var headerVersion = request.Headers.GetValues("version").First();
        if (string.IsNullOrEmpty(headerVersion))
            return controllerRequest;
        else
            return controllerRequest + headerVersion.Replace(".", "");
    }
    return controllerRequest;
}

分別請求下試試

至此,已經實現了和微軟的Versioning一模一樣的功能,並且還支援了三段式的版本號。

下一個問題:如何向前找控制器?例:APP版本號到了1.0.2,但是1.0.2這個版本服務端只改了一個控制器的,大部分控制器都沒有1.0.2版,如何呼叫1.0.2實際觸發1.0.1呢?

場景:APP 1.0.1版上線,服務端有兩個控制器 User101Controller、Employee101Controller。1.0.2版User控制器有介面變化,加了一個User102Controller,Employee控制器無變化不加,1.0.2上線後服務端就有3個控制器了,APP端調所有介面傳的都是當前版本號1.0.2,訪問User觸發的是User102Controller,訪問Employee觸發的是Employee101Controller,這就是所謂的向前相容。

如何實現呢?還是在GetControllerName方法裡做文章

如圖DefaultHttpControllerSelector裡還有另一個虛方法GetControllerMapping,這個方法可以獲取當前專案裡所有的控制器名稱。這就簡單了:獲取到的是User101、User102,如果請求來的是User102直接返回User102,如果請求來的是User103,那麼挨個比大小,還是返回User102。此方法可行,但是思考了一下,每次請求過來都要做如下操作:

1)獲取專案所有控制器;

2)擷取控制器名稱後的版本號;

3)排序;

4)比大小。

步驟多,有點浪費效能,而且還容易出錯。直接使用配置的方式,只比大小,其他步驟都省略。新建PublicConst.cs,把專案所有的控制器和對應的版本號都寫到VersionList這個全域性變數裡

public class PublicConst
{
    public static List<Vsn> VersionList
    {
        get
        {
            return new List<Vsn>()
            {
                //注:版本必須【由小到大】新增
                new Vsn() { Controller = "User", Ver = "1.0.1" },
                new Vsn() { Controller = "User", Ver = "1.0.2" },
            };
        }
    }
}

public class Vsn
{
    public string Controller { get; set; }
    public string Ver { get; set; }
}

現在請求過來是1.0.3,從配置裡拿到了1.0.1和1.0.2,常規做法是根據“.”分割,然後挨個比數字大小,還要考慮4段、5段以及段數不一樣的版本號比大小,這樣程式碼寫著就沒邊了,還容易出bug。System名稱空間下有Version類,調方法就可以比大小,用起來

public static string CompareVersion(string version, List<string> vsns)
{
    var newestVersion = string.Empty;
    if (string.IsNullOrEmpty(version) || vsns.Count == 0)
    {
        return newestVersion;
    }

    Version verCurrent = new Version(version);
    for (int i = vsns.Count - 1; i >= 0; i--)//倒敘查提高效率:新版的使用者比老使用者多
    {
        if (verCurrent >= new Version(vsns[i]))
        {
            newestVersion = vsns[i];
            break;
        }
    }
    return newestVersion;
}

按照上面的思路繼續改造下GetControllerName方法

public override string GetControllerName(HttpRequestMessage request)
{
    var controllerRequest = base.GetControllerName(request);
    var controllerWithVersion = controllerRequest;
    if (request.Headers.Contains("version"))
    {
        var headerVersion = request.Headers.GetValues("version").First();
        if (!string.IsNullOrEmpty(headerVersion))
        {
            controllerWithVersion = controllerRequest + headerVersion.Replace(".", "");
            if (!GetControllerMapping().ContainsKey(controllerWithVersion))
            {
                var lsVersion = PublicConst.VersionList.Where(x => x.Controller == controllerRequest).ToList().ConvertAll(t => t.Ver);
                var newestVersion = Utils.CompareVersion(headerVersion, lsVersion);
                if (!string.IsNullOrEmpty(newestVersion))
                    controllerWithVersion = controllerRequest + newestVersion.Replace(".", "");
                else
                    controllerWithVersion = controllerRequest;
            }
        }
    }
    return controllerWithVersion;
}

呼叫試試

由圖可見,已經能向前找控制器了。還剩一個問題:如何向前找方法?場景:APP已經更新到1.0.3了,但是有一個方法一直沒有修改,還在1.0.1的控制器裡面,1.0.2裡面都沒有這個方法。這個時候直接調1.0.3的控制器可以定位到1.0.2控制器沒問題,但是1.0.2控制器下沒有這個方法,如何轉到1.0.1控制器,並呼叫其下面的這個方法呢?

回頭看看剛才的介面配置物件VersionList

public static List<Vsn> VersionList
{
    get
    {
        return new List<Vsn>()
        {
            //注:版本必須【由小到大】新增
            new Vsn() { Controller = "User", Ver = "1.0.1" },
            new Vsn() { Controller = "User", Ver = "1.0.2" },
        };
    }
}

在這個裡面配置每個版本控制器裡面包含的方法,每次對比版本的時候判斷下方法是否存在即可

public static List<Vsn> VersionList
{
    get
    {
        return new List<Vsn>()
        {
            //注:版本必須【由小到大】新增
            new Vsn() { Controller = "User", Ver = "1.0.1", Actions = new List<string>(){ "Get","Test"} },
      
new Vsn() { Controller = "User", Ver = "1.0.2", Actions = new List<string>(){ "Get"} },
     };
  }
}

弊端:方法多了,維護太費勁,怎麼能實現自動向前找方法呢?這裡就要返璞歸真了,想想面向物件三大特徵:繼承、封裝和多型

使用繼承的方式即可。第一個版本全是虛方法,後面每個版本繼承上一個版本,如果介面有修改就override上一個版本的介面即可。

User101Controller程式碼:

public class User101Controller : ApiController
{
    [HttpGet]
    public virtual string UserInfo()
    {
        return "101";
    }

    [HttpGet]
    public virtual string Test()
    {
        return "Test:101";
    }
}

User102Controller程式碼:

public class User102Controller : User101Controller
{
    [HttpGet]
    public override string UserInfo()
    {
        return "102";
    }
}

請求下試試

根據VersionList維護的版本號,沒有User103就找User102,但是User102下面並沒有Test方法,Test方法在User101下面,這個時候由於User102繼承了User101,也可以找到User101下的Test方法。

APP 1.0.3版本User控制器沒有變化就不建控制器,1.0.4的時候User控制器加了一個Test2方法繼續virtual,等待被下一個版本override

public class User104Controller : User102Controller
{
    [HttpGet]
    public virtual string Test2()
    {
        return "Test2:104";
    }
}

記得維護到VersionList

public static List<Vsn> VersionList
{
    get
    {
        return new List<Vsn>()
        {
            //注:版本必須【由小到大】新增
            new Vsn() { Controller = "User", Ver = "1.0.1" },
            new Vsn() { Controller = "User", Ver = "1.0.2" },
            new Vsn() { Controller = "User", Ver = "1.0.4" },
        };
    }
}

看下最終成果

至此,大功告成。

以上是我的方案,僅拋磚引玉,歡迎大家補充。 本文地址:https://www.cnblogs.com/oppoic/p/13367878.html

思考

看下核心的GetControllerName方法

 1 public override string GetControllerName(HttpRequestMessage request)
 2 {
 3     var controllerRequest = base.GetControllerName(request);
 4     var controllerWithVersion = controllerRequest;
 5     if (request.Headers.Contains("version"))
 6     {
 7         var headerVersion = request.Headers.GetValues("version").First();
 8         if (!string.IsNullOrEmpty(headerVersion))
 9         {
10             controllerWithVersion = controllerRequest + headerVersion.Replace(".", "");
11             if (!GetControllerMapping().ContainsKey(controllerWithVersion))
12             {
13                 var lsVersion = PublicConst.VersionList.Where(x => x.Controller == controllerRequest).ToList().ConvertAll(t => t.Ver);
14                 var newestVersion = Utils.CompareVersion(headerVersion, lsVersion);
15                 if (!string.IsNullOrEmpty(newestVersion))
16                     controllerWithVersion = controllerRequest + newestVersion.Replace(".", "");
17                 else
18                     controllerWithVersion = controllerRequest;
19             }
20         }
21     }
22     return controllerWithVersion;
23 }

本方案向前找控制器是通過對比版本大小實現的,如果介面訪問頻率超高(比如APP日活百萬),有個優化方案:看看上面程式碼11行,如果當前請求的控制器不存在就向前找,把線上APP最新版本的控制器都建一遍,直接繼承上一個控制器,不需要重寫任何方法,這樣就不用對比版本了,提升了效能。

另外再看版本號維護類VersionList

public static List<Vsn> VersionList
{
    get
    {
        return new List<Vsn>()
        {
            //注:版本必須【由小到大】新增
            new Vsn() { Controller = "User", Ver = "1.0.1" },
            new Vsn() { Controller = "User", Ver = "1.0.2" },
            new Vsn() { Controller = "User", Ver = "1.0.4" },
        };
    }
}

版本號對比是挨個對比,沒法集中對比,所以必須按順序維護起來,但是如果哪天粗心搞錯了,或者添加了重複的版本號怎麼處理?在Application_Start專案啟動的時候檢測下這個VersionList即可。

最後說下版本號,User1110Cotroller到底表示哪個版本?11.1.0還是1.11.0,這塊也可以優化下,建成User1_11_0Controller就可以了。

環境

Visual Studio 2017 .NET Framework 4.5.2 Web API

參考

https://www.cnblogs.com/linhuiy/p/12668535.html

https://www.dotnetcurry.com/aspnet/1185/aspnet-web-api-versioning-angularjs-app

https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Http/Dispatcher/DefaultHttpControllerSelector.cs