大家是怎麼做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