一般后端提供来的接口文档有些是Swagger, 有些低代码的平台会直接提供Postman, 我们要快速导入Unity的话有很多方法, 如果是普通开发环境的话, 直接导出就行:
PS : 需要先从 Editor 菜单中转换成 OpenAPI3 才行, 要不然生成类型不对.
可以直接得到C#代码, 不过有一些依赖, 比如 RestSharp, Newtonsoft.Json 之类的, 比如一个这样的接口:
生成的代码接口主要内容如下 :
1. 异步接口
2. 数据结构
它这里为了转换安全耍了个小聪明, int类型生成了可空, 估计所有值类型都会是可空的. 并且在函数的输入自动判断了是否可空, 在函数实现的时候也进行了非空判断, 比如上面的type是(*required)标记的, 它的变量输入为 int? 可空的, 而函数实现是下面这样的:
跟接口需求符合. 没有特别需求的话, 直接就能用了, 只需要添加依赖库到Unity就行了.(目前Unity也自带了 newtonsoft.json, 只要找一个 RestSharp 就行了)
我们现在用的UniRx也是可以跟它联动的, 比如这样:
public System.IAsyncResult BeginXXX(object obj) { Debug.LogError("Start"); return Task.Run(async () => { var rateLimit = Task.Delay(System.TimeSpan.FromSeconds(3)); await rateLimit; }); }
把一个 async 过程调用为 IAsyncResult 返回, 就能解决跟 UniRx 的联动了:
Observable.FromAsyncPattern((_, __) => { return BeginXXX(_); }, (_end) => { Debug.LogError("END"); })().Subscribe();
不过这里试了一下, 貌似没有执行到end回调, 先忽略.
PS : 上面的生成代码是基于 ASP.NET 的, Unity里面缺少了一些系统库, 比如 System.ComponentModel.DataAnnotations 这些, 不能用, 要选择 csharp-dotnet2 这个下载才能放在Unity里面用, 不过它生成的代码缺了异步代码, 只有一个同步方法, 无语...
csharp-dotnet2 代码
两个生成的代码差别就是少了异步代码. 如上面所示, Unity 中只能用同步方法, 所以 UniRx 联动只需要进行线程转换就行了, 反而更简单 :
Observable.Start(() => { var DisasterApi = new IO.Swagger.Api.DisasterApi("http://www.baidu.com/"); var netData = DisasterApi.DisasterEventInfoListGet("", 1, ""); return netData; }, Scheduler.ThreadPool).ObserveOnMainThread().Subscribe((data) => { Debug.Log(data.Msg); });
----------------------- 更新 -----------------------
其实 Swagger 提供的代码生成, 是依赖于 swagger-codegen 这个东西的, 可以安装到本地来, 然后它有很多命令行参数来添加可以调整最终生成代码, 不过貌似不像 XSLT 那样可以通过修改模板逻辑来完全控制最终代码样式...
这个东西需要安装到本地而且需要安装Java环境, 并不是很泛用, 就直接忽略了.
那么我们要生成基于 UniRx 的封装代码, 只能通过反射来了. 先把 Swagger 生成的代码扔到工程里面 :
我们从 API 来入手就行了, 从 API 的命名空间来找对应的接口 :
public static List<System.Type> GetNameSpaceTypes(Assembly assembly, string nameSpace) { var types = assembly.GetTypes(); return types.Where((_t) => { return _t.Namespace == nameSpace; }).ToList(); }
然后找出接口类型, 获取所有 MethodInfo 就行了, 因为它生成的代码接口只包含数据获取的函数 :
把 MethodInfo 的变量和返回值都按照逻辑生成, 就能完成函数封装了, 而它的函数调用只需要一个实例即可, 所以实例对象也可以生成出来作为公用对象 :
var sb = new StringBuilder(); var list = InternalModule.Common.ReflectionHelper.GetNameSpaceTypes(XXOOAssembly, "IO.Swagger.Api"); if(list != null && list.Count > 0) { foreach(var type in list) { if(type.IsInterface) { Debug.Log(type.FullName); var methods = type.GetMethods(); if(methods != null && methods.Length > 0) { var interFaceName = type.FullName; var sepPos = interFaceName.LastIndexOf("."); var className = interFaceName.Substring(sepPos + 2); var classNameLower = LowerFirstCase(className); var classFullName = interFaceName.Substring(0, sepPos + 1) + className; sb.AppendLine(string.Concat("private ", classFullName, " ", classNameLower, " = new ", classFullName, "(URL);")); foreach(var method in methods) { var methodName = method.Name; var funcHeader = string.Concat("public UniRx.IObservable<", method.ReturnType.FullName, "> ", methodName, "(", MyEditor.FunctionGen.MethodParamsCodeGen(method, true), ")"); sb.Append(funcHeader); var funcBody = SwaggerFunctionBody.Replace(SwaggerAPI, classNameLower).Replace(NAME, methodName).Replace(PARAMS, MyEditor.FunctionGen.MethodParamsCodeGen(method, false)); sb.AppendLine(funcBody); } sb.AppendLine(""); } } } }
创建变量的方法也很简单 :
public static string MethodParamsCodeGen(System.Reflection.MethodInfo method, bool genType) { string paramaterStr = string.Empty; var paramaters = method.GetParameters(); if(paramaters != null && paramaters.Length > 0) { List<string> list = new List<string>(); foreach(var paramater in paramaters) { if(genType) { string paramaterTypeName = paramater.ParameterType.FullName; var type = paramater.ParameterType; if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { paramaterTypeName = Nullable.GetUnderlyingType(type).FullName + "?"; } list.Add(paramaterTypeName + " " + paramater.Name); } else { list.Add(paramater.Name); } } paramaterStr = string.Join(", ", list); } return paramaterStr; }
因为 Swagger 对于可空对象的生成一定是 T? 这样的, 并且判空是在它函数内部进行的, 不会体现在变量上, 所以这样生成完全跟它的接口一样.
于是就能生成下面的封装代码了 :
private IO.Swagger.Api.DisasterApi DisasterApi = new IO.Swagger.Api.DisasterApi("www.xxx.ooo"); public UniRx.IObservable<IO.Swagger.Model.EventInfoListVO> DisasterEventInfoListGet(System.String time, System.Int32? type, System.String disasterRegionCode) { return Observable.Start(() => { var netData = DisasterApi.DisasterEventInfoListGet(time, type, disasterRegionCode); return netData; }, Scheduler.ThreadPool).ObserveOnMainThread(); }
这样就封装成异步的了, 函数模板直接就是上面这样的 :
public static string SwaggerFunctionBody { get { return @"{ return Observable.Start(() => { var netData = " + string.Concat(SwaggerAPI, ".", NAME, "(", PARAMS, ");") + @" return netData; }, Scheduler.ThreadPool).ObserveOnMainThread(); }"; } }
PS : 目前的逻辑对于增量更新来说是有很大问题的, 因为是硬编码的, 所以在 Swagger 接口变更的话, 导入了新的代码, 会编译错误, 而编译错误之后就无法生成新的代码, 因为读取的始终是前一次编译正确的库, 无法生成新的代码, 所以需要对代码引用进行一次清理, 目前只能通过二次代码生成的方式来解决......
比如参数有变化的情况, 编译错误 :
生成一次新的代码, 不过把原始代码引用去掉 :
这样编译通过了之后, 就可以再次生成新代码了...
可是如果是数据类型被删除了, 那就是最麻烦的情况了, 因为上次编译成功的接口的返回值始终指向一个现在不存在的数据类型 :
始终无法让代码编译通过而获取新的代码生成, 只能手动删除了......这样能支持接口的增改, 删除手动.
--------------------- 更新 ------------------------
发现它这个 ApiClient 在同时进行多个请求的时候, 会出错, 也就是说不能接口共用的意思, 生成代码需要每次请求都生成一个 ApiClient 对象才行 :
public UniRx.IObservable<IO.Swagger.Model.EventInfoListVO> DisasterEventInfoListGet(System.String time, System.Int32? type, System.String disasterRegionCode) { return Observable.Start(() => { return new IO.Swagger.Api.DisasterApi("www.baidu.com").DisasterEventInfoListGet(time, type, disasterRegionCode); }, Scheduler.ThreadPool).ObserveOnMainThread(); }
--------------------- Postman输出相关 ------------------------
Postman 导出文件结构比较简单, 不过因为它是文件夹类型的层级结构, 所以需要进行递归获取数据接口.
先看结构 :
"info" 对应总信息
"item" 就是所有接口了, 这个是层级结构的, 对应 postman 的文件夹
"variable" 是全局变量, 可能会替换一些比如 url 之类的, 我们生成自用结构可以提前替换
下面是典型的接口信息 :
因为有几个文件夹就套了几层, 然后可以得到 url, name, method 等信息, query 中就是请求时向上发送的数据, key 就是变量名称, 如果是一个正确请求, 可以直接用其中的 value 作为默认参数生成代码.
如果 postman 进行了请求, 有了返回值, 那么一样会体现在文件中, 它跟 request 层级相同 :
这个返回的意义就是生成 C# 结构代码, 通过把 "body" 中的 json 转为 C# 结构, 就能完成自动代码了.
首先生成自用结构, 非常简单 :
public class NetAddressInfo { public string url; public RequestType requestType = RequestType.Get; public Dictionary<string, string> queryParams = new Dictionary<string, string>(); // Key, Value -- 自动读取 public Dictionary<string, string> headerParams = new Dictionary<string, string>(); // Key, Value -- 自动读取 } public class NetAddressStruct { public Dictionary<string, NetAddressInfo> infos = new Dictionary<string, NetAddressInfo>(); }
结构中可以保存相关请求信息, 然后保存为自用结构 json, 查找名称就是请求名称, 在生成代码时就自动生成相应名称的对象即可 :
public const string @降雨量趋势变化 = @"降雨量趋势变化"; public class 降雨量趋势变化_Struct { public class Datum { public Common.DataTable time; // string public Common.DataTable rainfall; // double } public Common.DataTable code; // int public Common.DataTable msg; // string public Datum[] data; } /// <summary> /// URL : {{baseUrl}}/weather/rainfall/variation --> Swagger : WeatherRainfallVariation /// </summary> /// <param name="startDay">(Required) 时间,格式为yyyy-MM-dd</param> /// <param name="endDay">(Required) 时间,格式为yyyy-MM-dd</param> /// <param name="period">(Required) 周期:1 按小时 2 按天</param> /// <returns></returns> public IObservable<降雨量趋势变化_Struct> IObservable_降雨量趋势变化(Common.DataTable startDay /*(Required) 时间,格式为yyyy-MM-dd*/, Common.DataTable endDay /*(Required) 时间,格式为yyyy-MM-dd*/, Common.DataTable period /*(Required) 周期:1 按小时 2 按天*/) { return CommonNetDatas.GetNetDataCommonIObservable<降雨量趋势变化_Struct>(netAddressStruct, netResponseStruct, 降雨量趋势变化, new Dictionary<string, string>(){{ "startDay", startDay.ToString() }, { "endDay", endDay.ToString() }, { "period", period.ToString() }}, Authorization: (netAddressStruct.UseToken ? new Dictionary<string, string>() { { "Authorization", netAddressStruct.GetTokenString() } } : null)); }
PS : 发现 Newtonsoft.Json 对于 DataTable 也能反序列化成功, 估计是调用了隐式转换接口, 非常好
有很多时候我们并不想要手动填写变量, 因为有些从 postman 导入来的接口就只使用测试时用的数据作为请求就行了, 那么就可以生成一个 string 类型的带默认参数的传参就行了 :
比如 query 中的 key 就是变量, value 就是默认参数, 这样生成的代码就可以变成 :
public IObservable<Post_Struct> IObservable_XXXX_Post(string where = "FID=0", string geometryType = "esriGeometryEnvelope", string outFields = "*", string returnGeometry = "true", string f = "pjson") { return CommonNetDatas.GetNetDataCommonIObservable<Post_Struct>(netAddressStruct, netResponseStruct, "XXXX", new Dictionary<string, string>(){{ "where", where.ToString() }, { "geometryType", geometryType.ToString() }, { "outFields", outFields.ToString() }, { "returnGeometry", returnGeometry.ToString() }, { "f", f.ToString() }}, Authorization: (netAddressStruct.UseToken ? new Dictionary<string, string>() { { "Authorization", netAddressStruct.GetTokenString() } } : null)); }
然后说到 Token 的问题, 获取 Token 也可以写到 postman 里面去, 这里利用 RestSharp 的同步接口获取就行了, 不过为了效果也可以封装成异步的再套到这个 IObservable 接口就行了.
这里只需要在 postman 中添加一个名为 Token 的获取接口, 能够正确获取的话就能自动添加 Token 了.
public class NetAddressStruct { public NetAddressInfo tokenInfo { get { return infos.TryGetNullableValue("Token"); } } public Dictionary<string, NetAddressInfo> infos = new Dictionary<string, NetAddressInfo>(); public TokenData Token = null; public bool UseToken { get { return tokenInfo != null; } } public string GetToken() { if(UseToken == false) { return string.Empty; } if(Token == null || Token.expired) { Token = CommonNetDatas.GetNetDataCommon<TokenData>(tokenInfo); Token.startTime = System.DateTime.Now; } return Token.access_token; } public string GetTokenString() { var token = GetToken(); if(string.IsNullOrEmpty(token) == false) { return string.Concat(Token.token_type, " ", token); } return string.Empty; } }