From 9d58f71eaba451902eaebb3e54e1fd5d4a42a504 Mon Sep 17 00:00:00 2001 From: drake Date: Wed, 2 Aug 2023 15:34:08 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=AE=80=E5=8C=96=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/config.md | 4 +- docs/converter.md | 280 ++++------------------------------ docs/coroutine-request.md | 16 +- docs/customizer-converter.md | 287 +++++++++++++++++++++++++++++++++++ docs/default-response.md | 50 ------ docs/index.md | 100 ++++-------- docs/issues.md | 27 +++- docs/request.md | 119 ++++++--------- docs/scope.md | 48 +++--- docs/sync-request.md | 34 ++--- mkdocs.yml | 23 ++- 12 files changed, 477 insertions(+), 513 deletions(-) create mode 100644 docs/customizer-converter.md delete mode 100644 docs/default-response.md diff --git a/README.md b/README.md index a809a12a0..8b350d175 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - +

diff --git a/docs/config.md b/docs/config.md index 05a16a6fc..ea8eb19ad 100644 --- a/docs/config.md +++ b/docs/config.md @@ -45,10 +45,10 @@ | 函数 | 描述 | |-|-| -| setDebug | 是否输出网络日志, 和`LogRecordInterceptor`互不影响 | +| setDebug | 是否输出网络日志, 和`LogRecordInterceptor`互不影响 | | setSSLCertificate | 配置Https证书 | | trustSSLCertificate | 信任所有Https证书 | -| setConverter | [配置数据转换器](converter.md), 将网络返回的数据转换成你想要的数据结构 | +| setConverter | [配置数据转换器](customizer-converter.md), 将网络返回的数据转换成你想要的数据结构 | | setRequestInterceptor | [配置请求拦截器](interceptor.md), 适用于添加全局请求头/参数 | | setErrorHandler | [配置全局错误处理](error-global.md) | | setDialogFactory | [配置全局对话框](auto-dialog.md) | diff --git a/docs/converter.md b/docs/converter.md index d8d7f1fc3..9dd34c375 100644 --- a/docs/converter.md +++ b/docs/converter.md @@ -1,36 +1,16 @@ -Net可以通过自定义转换器支持任何数据类型转换, 甚至`List>`等嵌套泛型对象 +Net支持请求返回的数据类型取决于你自己的转换器实现(即理论上支持返回任何对象): -例如请求动作Post`指定泛型为Model`, 则转换器NetConverter中的函数`onConvert返回值必须为Model`, 如果转换失败或者发生异常都算请求错误 +# Get<任何对象>("path").await() -> 注意如果你在转换器里面返回null. 那么指定的泛型也应当是可空类型, 例如`Post("api")` - -```kotlin -scopeNetLife { - val userList = Get>("list") { - converter = GsonConverter() - }.await() -} -``` - -
- -如果你要返回映射好的数据模型对象, 那么肯定要求创建转换器的. 本框架由于低耦合原则不自带解析框架 - -[常用的Json转换器-代码示例](https://github.com/liangjingkanji/Net/tree/master/sample/src/main/java/com/drake/net/sample/converter) - - - -## 默认返回数据类型 - -Net支持请求返回的数据类型取决于你的转换器(也就是支持返回任何对象), 默认情况不创建转换器也支持返回以下数据类型 +如果不自定义转换器默认支持返回以下数据类型 | 函数 | 描述 | |-|-| | String | 字符串 | | ByteArray | 字节数组 | | ByteString | 内部定义的一种字符串对象 | -| Response | 最基础的响应 | | File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) | +| Response | 最基础的, 包含全部响应信息的对象(响应体/响应头/请求信息等) | 使用示例 @@ -40,238 +20,32 @@ scopeNetLife { } ``` -??? summary "默认使用的是: [NetConverter.DEFAULT](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)" - ```kotlin - val DEFAULT = object : NetConverter { - - override fun onConvert( - succeed: Type, - response: Response - ): R? { - return when (succeed) { - String::class.java -> response.body?.string() as R - ByteString::class.java -> response.body?.byteString() as R - ByteArray::class.java -> response.body?.bytes() as R - Response::class.java -> response as R - File::class.java -> response.file() as R - else -> throw ConvertException( - response, - "The default converter does not support this type" - ) - } - } - } - ``` - -假设这里没有你需要的数据类型请[自定义转换器](#_3)(例如返回Json或Protocol) - -## 设置转换器 -转换器分为全局和单例, 单例可以覆盖全局的转换器. 如果不设置转换器就会采用默认的转换器 - -=== "全局" - ```kotlin hl_lines="2" - NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { - setConverter(SerializationConverter()) - } - ``` -=== "单例" - ```kotlin hl_lines="3" - scopeNetLife { - tvFragment.text = Get("api"){ - converter = SerializationConverter() - }.await() - } - ``` - -## Json解析库转换器 - -一般业务我们可以直接继承[JSONConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/JSONConvert.kt) -使用自己的JSON解析器解析数据, 完全自定义需求可以直接实现[NetConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)(比如直接转换IO流) - -=== "Gson" - - ```kotlin - class GsonConvert : JSONConvert(code = "code", message = "msg", success = "200") { - val gson = GsonBuilder().serializeNulls().create() - - override fun String.parseBody(succeed: Type): S? { - return gson.fromJson(this, succeed) - } - } - ``` - -=== "kotlin-serialization" - - ```kotlin - class SerializationConverter( - val success: String = "0", - val code: String = "code", - val message: String = "msg" - ) : NetConverter { - - private val jsonDecoder = Json { - ignoreUnknownKeys = true // JSON和数据模型字段可以不匹配 - coerceInputValues = true // 如果JSON字段是Null则使用默认值 - } - - override fun onConvert(succeed: Type, response: Response): R? { - try { - // 此处是为了继承默认转换器支持的返回类型 - return NetConverter.onConvert(succeed, response) - } catch (e: ConvertException) { - val code = response.code - when { - code in 200..299 -> { // 请求成功 - val bodyString = response.body?.string() ?: return null - val kType = response.request.kType() ?: return null - return try { - val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息 - if (json.getString(this.code) == success) { // 对比后端自定义错误码 - bodyString.parseBody(kType) - } else { // 错误码匹配失败, 开始写入错误异常 - val errorMessage = json.optString( - message, - NetConfig.app.getString(com.drake.net.R.string.no_error_message) - ) - throw ResponseException(response, errorMessage) - } - } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON - bodyString.parseBody(kType) - } - } - code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误 - code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误 - else -> throw ConvertException(response) - } - } - } - - fun String.parseBody(succeed: KType): R? { - return jsonDecoder.decodeFromString(Json.serializersModule.serializer(succeed), this) as R - } - } - ``` - - SerializationConverter就是仿照JSONConverter代码实现 - -=== "FastJson" - - ```kotlin - class FastJsonConvert : JSONConvert(code = "code", message = "msg", success = "200") { - - override fun String.parseBody(succeed: Type): S? { - return JSON.parseObject(this, succeed) - } - } - ``` - -=== "Moshi" - - ```kotlin - class MoshiConvert : JSONConvert(code = "code", message = "msg", success = "200") { - val moshi = Moshi.Builder().build() - - override fun String.parseBody(succeed: Type): S? { - return moshi.adapter(succeed).fromJson(this) - } - } - ``` - -1. 使用转换器时请添加其依赖: [GSON](https://github.com/google/gson) | [kotlin-serialization](https://github.com/Kotlin/kotlinx.serialization) | [FastJson](https://github.com/alibaba/fastjson) | [Moshi](https://github.com/square/moshi) -2. 推荐使用 `kotlinx.Serialization`, 其可解析[任何泛型](kotlin-serialization.md) -3. Sample有完整代码示例 - -以上转换器示例是建立在数据结构为以下表格的固定格式下, 如果有特殊的业务可能需要自行修改 - -| 转换器参数 | 描述 | -|-|-| -| code | 即后端定义的`成功码`字段名 | -| message | 即后端定义的`错误消息`字段名 | -| success | 即`成功码`的值等于指定时才算网络请求成功 | - - - -比如截图中的意为, 当返回的Json中包含state字段且值为ok时请求才算是真正成功才会返回数据, 否则都会抛出异常. 其中message为错误信息字段名 - -假设简单的指定名称不能满足你复杂的业务逻辑, 请复制`JSONConvert`源码到你项目中修改或者直接自己实现`NetConverter` - -> 注意解析器(Gson或者Moshi)的解析对象记得定义为类成员, 这样可以不会导致每次解析都要创建一个新的解析对象, 减少内存消耗 -
- -## 自定义转换器 - -通过实现`NetConverter`接口可以编写自己的逻辑网络请求返回的数据, `NetConvert.DEFAULT`为默认的转换器支持返回File/String/Response等 - - - -框架中自带一个`JSONConverter`可以作为参考或者直接使用. 其可以转换JSON数据. - -??? summary "JSONConverter 源码" - ```kotlin - /** - * 常见的JSON转换器实现, 如果不满意继承实现自定义的业务逻辑 - * - * @param success 后端定义为成功状态的错误码值 - * @param code 错误码在JSON中的字段名 - * @param message 错误信息在JSON中的字段名 - */ - abstract class JSONConvert( - val success: String = "0", - val code: String = "code", - val message: String = "msg" - ) : NetConverter { - - override fun onConvert(succeed: Type, response: Response): R? { - try { - // 此处是为了继承默认转换器支持的返回类型 - return NetConverter.onConvert(succeed, response) - } catch (e: ConvertException) { - val code = response.code - when { - code in 200..299 -> { // 请求成功 - val bodyString = response.body?.string() ?: return null - return try { - val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息 - if (json.getString(this.code) == success) { // 对比后端自定义错误码 - bodyString.parseBody(succeed) - } else { // 错误码匹配失败, 开始写入错误异常 - val errorMessage = json.optString( - message, - NetConfig.app.getString(com.drake.net.R.string.no_error_message) - ) - throw ResponseException(response, errorMessage) - } - } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON - bodyString.parseBody(succeed) - } - } - code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误 - code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误 - else -> throw ConvertException(response) +??? summary " +默认使用的是: [NetConverter.DEFAULT](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)" +```kotlin +interface NetConverter { + + @Throws(Throwable::class) + fun onConvert(succeed: Type, response: Response): R? + + companion object DEFAULT : NetConverter { + /** + * 返回结果应当等于泛型对象, 可空 + * @param succeed 请求要求返回的泛型类型 + * @param response 请求响应对象 + */ + override fun onConvert(succeed: Type, response: Response): R? { + return when { + succeed === String::class.java && response.isSuccessful -> response.body?.string() as R + succeed === ByteString::class.java && response.isSuccessful -> response.body?.byteString() as R + succeed is GenericArrayType && succeed.genericComponentType === Byte::class.java && response.isSuccessful -> response.body?.bytes() as R + succeed === File::class.java && response.isSuccessful -> response.file() as R + succeed === Response::class.java -> response as R + else -> throw ConvertException(response, "An exception occurred while converting the NetConverter.DEFAULT") } } } - - /** - * 反序列化JSON - * - * @param succeed JSON对象的类型 - * @receiver 原始字符串 - */ - abstract fun String.parseBody(succeed: Type): R? } - ``` -JSONConvert的核心逻辑 - -1. 判断服务器的错误码 -1. 判断后端自定义的错误码 -1. 如果判断发生错误则抛出一个包含错误信息的异常 -1. 如果都判断成功则开始解析数据并return数据对象 - -在转换器中根据需要你可以在这里加上常见的解密数据, token失效跳转登录, 限制多端登录等逻辑. 日志信息输出请阅读: [日志记录器](log-recorder.md) - -如果是错误信息建议抛出异常, 就可以在全局异常处理器中统一处理, 请阅读:[全局错误处理](error-handle.md) - -
\ No newline at end of file +假设这里没有你需要的数据类型请[自定义转换器](/converter/#_3)(例如返回Json或Protocol) \ No newline at end of file diff --git a/docs/coroutine-request.md b/docs/coroutine-request.md index befff75b1..b5d5596a1 100644 --- a/docs/coroutine-request.md +++ b/docs/coroutine-request.md @@ -1,12 +1,9 @@ -Net在2.0开始引入协程来支持并发和异步, 虽然很多网络框架支持协程, 但是Net对于协程生命周期的控制算得上是独有. -并且Net不仅仅网络请求, 其也支持创建任何异步任务. +Net在2.0开始引入协程并发请求, 虽然很多网络框架支持协程, 但是唯有Net考虑到协程生命周期 -> 这里的`同时/并发/并行`统称为并发(具体是不是并行不需要开发者来考虑) +并且不仅仅网络请求, 其也支持创建任何异步任务
-在上章节已经使用过了网络的并发请求 - -这里再演示同时(并发)请求百度网站`一万次`并且一次取消 +在上章节已经介绍如何发起并发网络请求, 这里再演示同时(并发)网络请求`一万次`并且一次性全部取消 ```kotlin val job = scopeNetLife { @@ -28,9 +25,10 @@ thread { } ``` +
-Net主要推荐使用的是协程请求, 但是同时支持其他方式发起请求 +Net主要使用的协程请求, 但也支持其他方式发起请求 -- 协程请求 - [同步请求](sync-request.md) -- [回调请求](callback.md) \ No newline at end of file +- [回调请求](callback.md) +- 协程请求 diff --git a/docs/customizer-converter.md b/docs/customizer-converter.md new file mode 100644 index 000000000..eca2107f5 --- /dev/null +++ b/docs/customizer-converter.md @@ -0,0 +1,287 @@ +Net可以通过自定义转换器支持任何数据类型转换, 甚至`List>`等嵌套泛型对象 + +例如请求动作Post`指定泛型为Model`, 则转换器NetConverter中的函数`onConvert返回值必须为Model`, +如果转换失败或者发生异常都算请求错误 + +> 注意如果你在转换器里面返回null. 那么指定的泛型也应当是可空类型, 例如`Post("api")` + +```kotlin +scopeNetLife { + val userList = Get>("list") { + converter = GsonConverter() + }.await() +} +``` + +
+ +如果你要返回映射好的数据模型对象, 那么肯定要求创建转换器的. 本框架由于低耦合原则不自带解析框架 + +[常用的Json转换器-代码示例](https://github.com/liangjingkanji/Net/tree/master/sample/src/main/java/com/drake/net/sample/converter) + + + +## 默认返回数据类型 + +Net支持请求返回的数据类型取决于你的转换器(也就是支持返回任何对象), 默认情况不创建转换器也支持返回以下数据类型 + +| 函数 | 描述 | +|-|-| +| String | 字符串 | +| ByteArray | 字节数组 | +| ByteString | 内部定义的一种字符串对象 | +| Response | 最基础的响应 | +| File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) | + +使用示例 + +```kotlin +scopeNetLife { + Get("api").await().headers("响应头名称") // 返回响应头 +} +``` + +??? summary " +默认使用的是: [NetConverter.DEFAULT](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)" +```kotlin +val DEFAULT = object : NetConverter { + + override fun onConvert( + succeed: Type, + response: Response + ): R? { + return when (succeed) { + String::class.java -> response.body?.string() as R + ByteString::class.java -> response.body?.byteString() as R + ByteArray::class.java -> response.body?.bytes() as R + Response::class.java -> response as R + File::class.java -> response.file() as R + else -> throw ConvertException( + response, + "The default converter does not support this type" + ) + } + } + } + ``` + +假设这里没有你需要的数据类型请[自定义转换器](#_3)(例如返回Json或Protocol) + +## 设置转换器 + +转换器分为全局和单例, 单例可以覆盖全局的转换器. 如果不设置转换器就会采用默认的转换器 + +=== "全局" + ```kotlin hl_lines="2" + NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { + setConverter(SerializationConverter()) + } + ``` +=== "单例" + ```kotlin hl_lines="3" + scopeNetLife { + tvFragment.text = Get("api"){ + converter = SerializationConverter() + }.await() + } + ``` + +## Json解析库转换器 + +一般业务我们可以直接继承[JSONConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/JSONConvert.kt) +使用自己的JSON解析器解析数据, +完全自定义需求可以直接实现[NetConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)( +比如直接转换IO流) + +=== "Gson" + + ```kotlin + class GsonConvert : JSONConvert(code = "code", message = "msg", success = "200") { + val gson = GsonBuilder().serializeNulls().create() + + override fun String.parseBody(succeed: Type): S? { + return gson.fromJson(this, succeed) + } + } + ``` + +=== "kotlin-serialization" + + ```kotlin + class SerializationConverter( + val success: String = "0", + val code: String = "code", + val message: String = "msg" + ) : NetConverter { + + private val jsonDecoder = Json { + ignoreUnknownKeys = true // JSON和数据模型字段可以不匹配 + coerceInputValues = true // 如果JSON字段是Null则使用默认值 + } + + override fun onConvert(succeed: Type, response: Response): R? { + try { + // 此处是为了继承默认转换器支持的返回类型 + return NetConverter.onConvert(succeed, response) + } catch (e: ConvertException) { + val code = response.code + when { + code in 200..299 -> { // 请求成功 + val bodyString = response.body?.string() ?: return null + val kType = response.request.kType() ?: return null + return try { + val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息 + if (json.getString(this.code) == success) { // 对比后端自定义错误码 + bodyString.parseBody(kType) + } else { // 错误码匹配失败, 开始写入错误异常 + val errorMessage = json.optString( + message, + NetConfig.app.getString(com.drake.net.R.string.no_error_message) + ) + throw ResponseException(response, errorMessage) + } + } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON + bodyString.parseBody(kType) + } + } + code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误 + code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误 + else -> throw ConvertException(response) + } + } + } + + fun String.parseBody(succeed: KType): R? { + return jsonDecoder.decodeFromString(Json.serializersModule.serializer(succeed), this) as R + } + } + ``` + + SerializationConverter就是仿照JSONConverter代码实现 + +=== "FastJson" + + ```kotlin + class FastJsonConvert : JSONConvert(code = "code", message = "msg", success = "200") { + + override fun String.parseBody(succeed: Type): S? { + return JSON.parseObject(this, succeed) + } + } + ``` + +=== "Moshi" + + ```kotlin + class MoshiConvert : JSONConvert(code = "code", message = "msg", success = "200") { + val moshi = Moshi.Builder().build() + + override fun String.parseBody(succeed: Type): S? { + return moshi.adapter(succeed).fromJson(this) + } + } + ``` + +1. 使用转换器时请添加其依赖: [GSON](https://github.com/google/gson) + | [kotlin-serialization](https://github.com/Kotlin/kotlinx.serialization) + | [FastJson](https://github.com/alibaba/fastjson) | [Moshi](https://github.com/square/moshi) +2. 推荐使用 `kotlinx.Serialization`, 其可解析[任何泛型](kotlin-serialization.md) +3. Sample有完整代码示例 + +以上转换器示例是建立在数据结构为以下表格的固定格式下, 如果有特殊的业务可能需要自行修改 + +| 转换器参数 | 描述 | +|-|-| +| code | 即后端定义的`成功码`字段名 | +| message | 即后端定义的`错误消息`字段名 | +| success | 即`成功码`的值等于指定时才算网络请求成功 | + + + +比如截图中的意为, 当返回的Json中包含state字段且值为ok时请求才算是真正成功才会返回数据, 否则都会抛出异常. +其中message为错误信息字段名 + +假设简单的指定名称不能满足你复杂的业务逻辑, 请复制`JSONConvert` +源码到你项目中修改或者直接自己实现`NetConverter` + +> 注意解析器(Gson或者Moshi)的解析对象记得定义为类成员, 这样可以不会导致每次解析都要创建一个新的解析对象, +> 减少内存消耗 +
+ +## 自定义转换器 + +通过实现`NetConverter`接口可以编写自己的逻辑网络请求返回的数据, `NetConvert.DEFAULT` +为默认的转换器支持返回File/String/Response等 + +框架中自带一个`JSONConverter`可以作为参考或者直接使用. 其可以转换JSON数据. + +??? summary "JSONConverter 源码" +```kotlin +/** +* 常见的JSON转换器实现, 如果不满意继承实现自定义的业务逻辑 +* +* @param success 后端定义为成功状态的错误码值 +* @param code 错误码在JSON中的字段名 +* @param message 错误信息在JSON中的字段名 +*/ +abstract class JSONConvert( +val success: String = "0", +val code: String = "code", +val message: String = "msg" +) : NetConverter { + + override fun onConvert(succeed: Type, response: Response): R? { + try { + // 此处是为了继承默认转换器支持的返回类型 + return NetConverter.onConvert(succeed, response) + } catch (e: ConvertException) { + val code = response.code + when { + code in 200..299 -> { // 请求成功 + val bodyString = response.body?.string() ?: return null + return try { + val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息 + if (json.getString(this.code) == success) { // 对比后端自定义错误码 + bodyString.parseBody(succeed) + } else { // 错误码匹配失败, 开始写入错误异常 + val errorMessage = json.optString( + message, + NetConfig.app.getString(com.drake.net.R.string.no_error_message) + ) + throw ResponseException(response, errorMessage) + } + } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON + bodyString.parseBody(succeed) + } + } + code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误 + code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误 + else -> throw ConvertException(response) + } + } + } + + /** + * 反序列化JSON + * + * @param succeed JSON对象的类型 + * @receiver 原始字符串 + */ + abstract fun String.parseBody(succeed: Type): R? + } + + ``` + +JSONConvert的核心逻辑 + +1. 判断服务器的错误码 +1. 判断后端自定义的错误码 +1. 如果判断发生错误则抛出一个包含错误信息的异常 +1. 如果都判断成功则开始解析数据并return数据对象 + +在转换器中根据需要你可以在这里加上常见的解密数据, token失效跳转登录, 限制多端登录等逻辑. +日志信息输出请阅读: [日志记录器](log-recorder.md) + +如果是错误信息建议抛出异常, 就可以在全局异常处理器中统一处理, 请阅读:[全局错误处理](error-handle.md) + +
\ No newline at end of file diff --git a/docs/default-response.md b/docs/default-response.md deleted file mode 100644 index f0986701c..000000000 --- a/docs/default-response.md +++ /dev/null @@ -1,50 +0,0 @@ -Net支持请求返回的数据类型取决于你自己的转换器实现(即理论上支持返回任何对象): - -# Get<任何对象>("path").await() - -如果不自定义转换器默认支持返回以下数据类型 - -| 函数 | 描述 | -|-|-| -| String | 字符串 | -| ByteArray | 字节数组 | -| ByteString | 内部定义的一种字符串对象 | -| File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) | -| Response | 最基础的, 包含全部响应信息的对象(响应体/响应头/请求信息等) | - -使用示例 - -```kotlin -scopeNetLife { - Get("api").await().headers("响应头名称") // 返回响应头 -} -``` - -??? summary "默认使用的是: [NetConverter.DEFAULT](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)" - ```kotlin - interface NetConverter { - - @Throws(Throwable::class) - fun onConvert(succeed: Type, response: Response): R? - - companion object DEFAULT : NetConverter { - /** - * 返回结果应当等于泛型对象, 可空 - * @param succeed 请求要求返回的泛型类型 - * @param response 请求响应对象 - */ - override fun onConvert(succeed: Type, response: Response): R? { - return when { - succeed === String::class.java && response.isSuccessful -> response.body?.string() as R - succeed === ByteString::class.java && response.isSuccessful -> response.body?.byteString() as R - succeed is GenericArrayType && succeed.genericComponentType === Byte::class.java && response.isSuccessful -> response.body?.bytes() as R - succeed === File::class.java && response.isSuccessful -> response.file() as R - succeed === Response::class.java -> response as R - else -> throw ConvertException(response, "An exception occurred while converting the NetConverter.DEFAULT") - } - } - } - } - ``` - -假设这里没有你需要的数据类型请[自定义转换器](/converter/#_3)(例如返回Json或Protocol) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 01ee9d217..e2138609b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,67 +5,65 @@
-## 前言 - -1. 任何本文档没有提到的功能都可以通过搜索`"OkHttp如何**"`来解决, 因为本框架支持OkHttp所有功能/组件 -1. 建议创建一个Api.kt的`object`单例类存储所有请求路径常量 -1. `Post/Get等`函数属于请求动作. `scope**`等函数属于作用域, 假设你有某个请求需要重复使用建议封装`请求动作`而不是作用域 -1. 如果你觉得文档看不懂或者有歧义那肯定是作者问题, 请反馈给作者或者自我修订 +!!! tip "前言" + - Net没有的功能可以搜索`"OkHttp如何XX"`来实现 + - 强烈建议下载仓库运行sample项目来学习/实践 + - 如果觉得文档看不懂那肯定是作者问题, 请反馈给作者或者自我修订 ## 使用
-这里演示发起网络`请求百度网站`内容的三个步骤 +这里演示发起网络`请求网站内容`的三个步骤 1. 创建作用域 -1. 发起请求 -1. 接收数据 +1. 发起请求动作 +1. 等待数据返回 -=== "单个请求" +=== "简单请求" ```kotlin - scopeNetLife { // 创建作用域 - // 这个大括号内就属于作用域内部 - val data = Get("https://github.com/liangjingkanji/Net/").await() // 发起GET请求并返回`String`类型数据 + scopeNetLife { 创建作用域 + // 这个大括号内就属于作用域内部 + val data = Get("https://github.com/liangjingkanji/Net/").await() // 发起GET请求并返回`String`类型数据 } ``` -=== "串行请求" +=== "同步请求" ```kotlin scopeNetLife { - val data = Get("http://www.baidu.com/").await() // 请求A 发起GET请求并返回数据 - val data = Get("https://github.com/liangjingkanji/Net/").await() // 请求B 将等待A请求完毕后发起GET请求并返回数据 + val userInfo = Get("https://github.com/liangjingkanji/BRV/").await() // 立即请求 + val config = Get("https://github.com/liangjingkanji/Net/"){ + param("userId", userInfo.id) // 使用上个请求的数据作为参数 + }.await() // 请求B 将等待A请求完毕后发起GET请求并返回数据 } - ``` +``` === "并发请求" ```kotlin scopeNetLife { - // 以下两个网络请求属于同时进行中 - val aDeferred = Get("https://github.com/liangjingkanji/Net/") // 发起GET请求并返回一个对象(Deferred)表示"任务A" - val bDeferred = Get("https://github.com/liangjingkanji/Net/") // 发起请求并返回"任务B" - - // 随任务同时进行, 但是数据依然可以按序返回 - val aData = aDeferred.await() // 等待任务A返回数据 - val bData = bDeferred.await() // 等待任务B返回数据 - } + // 以下两个网络请求属于同时进行中 + val getUserInfoAsync = Get("https://github.com/liangjingkanji/Net/") // 立即请求 + val getConfigAsync = Get("https://github.com/liangjingkanji/BRV/") // 立即请求 + + val userInfo = getUserInfoAsync.await() // 等待数据返回 + val config = getConfigAsync.await() + } ``` -多个网络请求放在同一个作用域内就可以统一控制, 如果你的多个网络请求毫无关联, 你可以创建多个作用域. +多个网络请求在同一个作用域内可以统一控制, 如果多个网络请求之间毫无关联, 可以创建多个作用域来请求 -> 多进程或Xposed项目要求[初始化](config.md/#_1) +> 多进程或Xposed项目要求先[初始化](config.md/#_1)
-> 当`Get`或`Post`等函数调用就会开始发起网络请求, `await`只是等待其请求成功返回结果, 所以如果你在`await`后执行的网络请求,这不属于并发(属于串行) +并发请求错误示例 -并发的错误示例 ```kotlin hl_lines="3" scopeNetLife { // 请求A - val aDeferred = Get("https://github.com/liangjingkanji/Net/").await() - // 请求B, 由于上面使用`await()`函数, 所以必须等待A请求返回结果后才会执行B - val bDeferred = Get("https://github.com/liangjingkanji/Net/") + val userInfo = Get("https://github.com/liangjingkanji/Net/").await() + // 由于上面使用`await()`函数, 所以必须等待A请求返回结果后才会执行B + val getConfigAsync = Post("https://github.com/liangjingkanji/Net/") - val bData = bDeferred.await() // 等待任务B返回数据 + val config = getConfigAsync.await() // 等待任务B返回数据 } ``` @@ -81,38 +79,4 @@ scopeNetLife { | Response | 最基础的响应 | | File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) | -非以上类型要求[自定义转换器](converter.md) - -> 转换器的返回值决定你的网络请求的返回结果类型, 你甚至可以返回null, 前提是泛型为可空类型 - - -## RestFul -Net支持RestFul设计风格 - -```kotlin -scopeNetLife { - tvFragment.text = Get("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Post("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Head("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Put("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Patch("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Delete("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Trace("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Options("https://github.com/liangjingkanji/Net/").await() -} -``` - -## 函数 - -默认在IO线程执行网络请求(通过作用域参数可以控制Dispatch调度器), 要求在协程作用域内执行. - -|请求函数|描述| -|-|-| -| [Get](api/-net/com.drake.net/-get.html)|标准Http请求方法| -| [Post](api/-net/com.drake.net/-post.html)|标准Http请求方法| -| [Head](api/-net/com.drake.net/-head.html)|标准Http请求方法| -| [Options](api/-net/com.drake.net/-options.html)|标准Http请求方法| -| [Trace](api/-net/com.drake.net/-trace.html)|标准Http请求方法| -| [Delete](api/-net/com.drake.net/-delete.html)|标准Http请求方法| -| [Put](api/-net/com.drake.net/-put.html)|标准Http请求方法| -| [Patch](api/-net/com.drake.net/-patch.html)|标准Http请求方法| \ No newline at end of file +详细查看[转换器](customizer-converter.md), 非以上类型要求[自定义转换器](customizer-converter.md) \ No newline at end of file diff --git a/docs/issues.md b/docs/issues.md index 5c6bd24ce..d927868b8 100644 --- a/docs/issues.md +++ b/docs/issues.md @@ -1,8 +1,19 @@ -## 常见问题 - -- [networkSecurityConfig导致无法打包](https://github.com/liangjingkanji/Net/issues/57) -- [没有我需要的请求参数类型](https://github.com/liangjingkanji/Net/issues/56) -- [没有我需要的功能](https://github.com/liangjingkanji/Net/issues/58) -- [使用inline reified封装请求函数导致崩溃](https://github.com/liangjingkanji/Net/issues/54) -- [错误提示 toast 内存泄漏](https://github.com/liangjingkanji/Net/issues/65) -- [如何使用Cookie](https://github.com/liangjingkanji/Net/issues/51) +Net最大的特点在于完美支持OkHttp的所有功能组件, +而Android上大部分都是基于OkHttp的网络请求解决方案
+所以如果存在Net没有实现的功能可以百度/谷歌搜索`"OkHttp如何实现XX"`, 然后可以很容易在Net中使用 + +## 低版本兼容 + +如果你是在 Android 5 (API level 21) +以上开发建议使用最新版本: [Net](https://github.com/liangjingkanji/Net)
+如果要求低至 Android 4.4 (API level 19) +请使用兼容版本: [Net-okhttp3](https://github.com/liangjingkanji/Net-okhttp3) + +如果需更低版本支持建议拉取仓库修改`minSdkVersion`后编译成aar使用 + +## 开发者交流 + +- [反馈问题](https://github.com/liangjingkanji/Net/issues) +- [其他开发者提及问题](https://github.com/liangjingkanji/Net/issues) +- [交流社区](https://github.com/liangjingkanji/Net/discussions) +- diff --git a/docs/request.md b/docs/request.md index 1577fafbb..8d37cb98b 100644 --- a/docs/request.md +++ b/docs/request.md @@ -1,73 +1,44 @@ -Net中关于请求的类只有两个类和他们共同的抽象父类 -```kotlin -BaseRequest - |- UrlRequest - |- BodyRequest -``` - - -根据请求方法不同使用的Request也不同 +!!! question "请求参数" + 根据请求方式不同请求参数分为两类 -```kotlin -GET, HEAD, OPTIONS, TRACE, // Url request -POST, DELETE, PUT, PATCH // Body request -``` + UrlRequest: GET, HEAD, OPTIONS, TRACE // Query(请求参数位于Url中)
+ BodyRequest: POST, DELETE, PUT, PATCH // Body(请求体为流) -代码示例 +使用示例 ```kotlin scopeNetLife { - Get("api") { - // this 即为 UrlRequest - }.await() - - Post("api") { - // this 即为 BodyRequest + val userInfo = Post(Api.LOGIN) { + param("username", "drake") + param("password", "6f2961eb44b12123393fff7e449e50b9de2499c6") }.await() } ``` -## 请求参数 - -```kotlin -scopeNetLife { // 创建作用域 - // 这个大括号内就属于作用域内部 - val data = Get("https://github.com/liangjingkanji/Net/"){ - param("u_name", "drake") - param("pwd", "123456") - }.await() // 发起GET请求并返回`String`类型数据 -} -``` - -|请求函数|描述| +|函数|描述| |-|-| -|`param`|支持基础类型/文件/RequestBody/Part| -|`json`|请求参数为JSONObject/JsonArray/String| -|`setQuery/addQuery`|设置/添加Url参数, 如果当前请求为Url请求则该函数等效于`param`函数| +|`param`| Url请求时为Query, Body请求时为表单/文件| +|`json`|JSON字符串| +|`setQuery/addQuery`|Url中的Query参数, 如果当为Url请求则该函数等效`param`| |`setHeader/addHeader`|设置/添加请求头| -如果没有添加文件/流那么就是通过BodyRequest内部的`FormBody`发起请求. 反之就是通过`MultipartBody`发起请求. - -> 当然你可以完全自定义Body来请求, 譬如以下的Json请求 - +## JSON请求 -## Json请求 +这里仅演示三种参数类型上传JSON, 详细请查看方法重载 -这里提供三种创建Json请求的示例代码. 酌情选用 - -=== "JSON键值对(推荐)" +=== "Pair(推荐)" ```kotlin val name = "金城武" val age = 29 val measurements = listOf(100, 100, 100) - - scopeNetLife { - tvFragment.text = Post("api") { - // 只支持基础类型的值, 如果值为对象或者包含对象的集合/数组会导致其值为null - json("name" to name, "age" to age, "measurements" to measurements) - }.await() - } + + scopeNetLife { + tvFragment.text = Post("api") { + // 只支持基础类型的值, 如果值为对象或者包含对象的集合/数组会导致其值为null + json("name" to name, "age" to age, "measurements" to measurements) + }.await() + } ``` === "JSONObject" @@ -87,34 +58,34 @@ scopeNetLife { // 创建作用域 } ``` -=== "自定义的body" +=== "自定义RequestBody" ```kotlin val name = "金城武" val age = 29 val measurements = listOf(100, 100, 100) - + scopeNetLife { tvFragment.text = Post("api") { - body = MyJsonBody(name, age, measurements) + body = CustomizerJSONBody(name, age, measurements) }.await() } ``` -对于某些可能JSON请求参数存在固定值: +个别项目JSON中存在固定值, 开发者不想每次都写一遍: -1. 可以考虑继承RequestBody来扩展出自己的新的Body对象, 然后赋值给`body`字段 -2. 添加请求拦截器[RequestInterceptor](/interceptor/#_1) +1. 实现RequestBody默认添加参数 +2. 使用请求拦截器来添加全局参数 [RequestInterceptor](/interceptor/#_1) -## 自定义请求函数 +## 自定义扩展函数 -前面提到`json(Pair)`函数不支持对象值, 因为框架内部使用的`org.json.JSONObject`其不支持映射对象字段 +前面提到`json()`不能传对象, 因为内部使用`org.json.JSONObject`不支持映射对象字段 -这里可以创建扩展函数来支持你想要的Json解析框架, 比如以下常用的Json解析框架示例 +那我们可以自己创建扩展函数来使用支持解析对象的序列化框架, 如下 === "Gson" ```kotlin fun BodyRequest.gson(vararg body: Pair) { - this.body = Gson().toJson(body.toMap()).toRequestBody(MediaConst.JSON) + this.body = Gson().toJson(body.toMap()).toRequestBody(MediaConst.JSON) } ``` === "FastJson" @@ -135,23 +106,25 @@ scopeNetLife { } ``` -- 举一反三可以创建其他功能自定义的请求函数 -- 扩展函数要求为顶层函数, 即直接在文件中 (kotlin基础语法) - ## 全局请求参数 -对于动态生成的全局请求头或参数都可以通过实现`RequestInterceptor`来设置全局的请求拦截器来添加, 如果RequestInterceptor不满足你的需求可以使用拦截器(Interceptor)来实现 +使用`RequestInterceptor`请求拦截器添加全局参数/请求头, 如果不满足需求可以使用更复杂的 Interceptor` ```kotlin -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { - // 添加请求拦截器, 每次请求都会触发的, 可配置全局/动态参数 - setRequestInterceptor(MyRequestInterceptor()) +class GlobalHeaderInterceptor : RequestInterceptor { + + /** 本方法每次请求发起都会调用, 这里添加的参数可以是动态参数 */ + override fun interceptor(request: BaseRequest) { + request.setHeader("client", "Android") + request.setHeader("token", UserConfig.token) + } } ``` -## 请求函数 - -关于全部的请求配置选项推荐阅读函数文档或者阅读源码. Net提供清晰的函数结构浏览方便直接阅读源码 - - +```kotlin +NetConfig.initialize(Api.HOST, this) { + setRequestInterceptor(GlobalHeaderInterceptor()) +} +``` +更多请求参数相关请阅读Api文档/函数列表 \ No newline at end of file diff --git a/docs/scope.md b/docs/scope.md index 85d631e39..7cff2229d 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -1,28 +1,28 @@ -协程请求要求在协程的作用域中调用, 这里介绍如何创建不同的作用域获取不同的功能 +创建不同协程作用域可以实现不同的功能 -本质上Net的请求动作函数返回的是一个Deferred对象. 可以在任何协程作用域内执行. 但是考虑到完整的生命周期和错误处理等推荐使用Net内部定义的作用域. +实际上Net的请求函数返回的一个`Deferred`, 可以在任何协程作用域内执行, 但是考虑到生命周期/错误处理等建议使用Net提供的作用域函数 -> 发生在Net作用域内的任何异常都会被捕获, 有效减少应用崩溃率. 如果配合[kotlin-serialization](kotlin-serialization.md)还可以解决因服务器返回null字段导致的崩溃 +!!! Success "减少崩溃" + Net所有作用域内抛出异常都会被捕获到全局错误处理器, 可以防止应用崩溃
## 异步任务的作用域 -创建可以捕捉异常的协程作用域, 但是不会触发`NetErrorHandler`(全局错误处理者). 该作用域于一般用于普通的异步任务 +可以捕捉异常的协程作用域, 但不会触发`NetErrorHandler`(全局错误处理器), 该作用域一般用于构建普通异步任务 |函数|描述| |-|-| -|`scope`|创建最基础的作用域, 所有作用域都包含异常捕捉| -|`scopeLife`|创建跟随生命周期取消的作用域| +|`scope`|创建最基础的作用域| +|`scopeLife`|创建跟随生命周期自动取消的作用域| |`ViewModel.scopeLife`|创建跟随ViewModel生命周期的作用域, [如何在ViewModel创建作用域](view-model.md)| ## 网络请求的作用域 -网络请求的作用域可以根据生命周期自动取消网络请求, 发生错误也会自动弹出吐司(可以自定义或者取消), 并且具备一些场景的特殊功能(例如加载对话框, 缺省页, 下拉刷新等) +除异步任务外还适用于网络请求场景的作用域, 对比上面`异步任务的作用域`区别: -网络请求的作用域比上面提到的异步任务的作用域多的区别就是 - -1. 发生错误会触发全局错误处理`NetErrorHandler` -2. 具备一些特殊场景功能, 比如自动下拉刷新, 自动显示加载库等 +1. 发生错误自动吐司(可以自定义或者取消) +2. 发生错误会触发全局错误处理`NetErrorHandler` +3. 具备一些特殊场景功能, 比如根据网络请求结果自动处理下拉刷新/上拉加载/缺省页/加载框的状态 | 函数 | 描述 | |-|-| @@ -33,23 +33,25 @@ |`PageRefreshLayout.scope`|创建跟随[PageRefreshLayout](https://github.com/liangjingkanji/BRV)生命周期的作用域| |`StateLayout.scope`|创建跟随[StateLayout](https://github.com/liangjingkanji/BRV)生命周期的作用域| -
-> PageRefreshLayout/StateLayout 属于[BRV](https://github.com/liangjingkanji/BRV)框架中的布局, 用于支持[自动化缺省页/下拉刷新](auto-state.md) -
- +!!! Failure "区分函数接受者" + 注意`StateLayout.scope`等存在`函数接受者`的方法和`scope`属于两个方法, 严禁混用 -> 如果想了解详细的协程使用方式, 可以查看一篇文章: [最全面的Kotlin协程: Coroutine/Channel/Flow 以及实际应用](https://juejin.im/post/6844904037586829320) +!!! quote "第三方库支持" + PageRefreshLayout/StateLayout 属于第三方开源项目 [BRV](https://github.com/liangjingkanji/BRV) + 框架中的布局, 可用于支持[自动化缺省页/下拉刷新](auto-state.md)
-有时候可能面临嵌套的`scope*`函数或者作用域内有子作用域情况, 这个时候的生命周期是如何 +如果想更了解协程使用方式, +可以阅读一篇文章: [最全面的Kotlin协程: Coroutine/Channel/Flow 以及实际应用](https://juejin.im/post/6844904037586829320) +## 嵌套作用域 -## 嵌套Scope +有时候可能面临内嵌`scopeXX`函数(嵌套作用域), 这时候生命周期如下 ```kotlin hl_lines="5" -scopeNet { +scopeNetLife { val task = Post("api0").await() - scopeNet { + scopeNetLife { val task = Post("api0").await() // 此时发生请求错误 }.catch { // A @@ -59,9 +61,9 @@ scopeNet { } ``` -- 以下嵌套作用域错误将会仅发生在`A`处, 并被捕获, 同时不影响外部`scopeNet`的请求和异常捕获 -- 两个`scopeNet`的异常抛出和捕获互不影响 -- `scopeNet/scopeDialog/scope`等函数同理 +- 错误将在`A`处可以获取到, 且不影响外部`scopeNetLife`的请求 +- 两个`scopeNetLife`的异常抛出和捕获互不影响 +- `scopeXX()`等函数同理 ## 子作用域 diff --git a/docs/sync-request.md b/docs/sync-request.md index 569781cd1..6dcfd61e6 100644 --- a/docs/sync-request.md +++ b/docs/sync-request.md @@ -1,8 +1,13 @@ -Net支持在当前线程执行, 会阻塞当前线程的同步请求 -- `execute` +Net支持在当前线程执行阻塞线程的同步请求 -这里介绍的是不使用协程的同步请求. 由于Android主线程不允许发起网络请求, 这里我们得随便创建一个子线程才可以开发起同步请求 +!!! question "什么是同步请求" + 即上个请求结束才会发起下个请求, 实际上协程也可以实现但是他不会阻塞线程 -=== "同步请求" + 同步请求应用场景一般是在拦截器(执行在子线程)中使用 + +因为Android主线程不允许发起网络请求, 这里我们创建一个子线程来演示 + +=== "返回数据" ```kotlin thread { @@ -12,31 +17,22 @@ Net支持在当前线程执行, 会阻塞当前线程的同步请求 -- `execute } } ``` -=== "toResult" + +=== "返回Result" ```kotlin thread { - val result = Net.post("api").toResult().getOrDefault("请求发生错误, 我这是默认值") + val result = Net.post("api").toResult().getOrDefault("请求发生错误, 我是默认值") tvFragment?.post { tvFragment?.text = result // view要求在主线程更新 } } ``` -1. `execute`在请求发生错误时会抛出异常 -2. `toResult`不会抛出异常, 通过`exception*`函数来获取异常信息, 且支持默认值等特性 +1. `execute`在请求错误时会直接抛出异常 +2. `toResult`不会抛出异常, 可`getOrThrow/exceptionOrNull`等返回异常对象 + + -> 同步请求应用场景一般是在拦截器中使用, 拦截器默认是子线程 -作用域具体介绍可以看[创建作用域](scope.md) -|请求函数|描述| -|-|-| -|Net.get|标准Http请求方法| -|Net.post|标准Http请求方法| -|Net.head|标准Http请求方法| -|Net.options|标准Http请求方法| -|Net.trace|标准Http请求方法| -|Net.delete|标准Http请求方法| -|Net.put|标准Http请求方法| -|Net.patch|标准Http请求方法| diff --git a/mkdocs.yml b/mkdocs.yml index f155b67cf..813af00f1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,24 +27,33 @@ theme: - search.suggest - search.share - content.code.copy + - content.code.annotate plugins: - offline - search: separator: '[\s\-,:!=\[\]()"/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;' lang: - - en - - zh - + - en + - zh markdown_extensions: + - attr_list + - def_list + - md_in_html - toc: permalink: true - pymdownx.tasklist: custom_checkbox: true - admonition + - pymdownx.highlight - pymdownx.details - pymdownx.superfences - pymdownx.inlinehilite - - pymdownx.tabbed + - pymdownx.tabbed: + alternate_style: true + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde nav: - 使用: index.md @@ -54,8 +63,8 @@ nav: - 请求参数: request.md - 全局配置: config.md - 请求结果: - - 默认结果: default-response.md - - 自定义转换器: converter.md + - 转换器: converter.md + - 自定义转换器: customizer-converter.md - 自定义结构解析: convert-special.md - Kotlin-Serialization: kotlin-serialization.md - 数据类生成插件: model-generate.md @@ -92,7 +101,7 @@ nav: - Callback: callback.md - 轮询器/倒计时: interval.md - 社区讨论: https://github.com/liangjingkanji/Net/discussions - - 常见问题: https://github.com/liangjingkanji/Net/blob/master/docs/issues.md + - 常见问题: issues.md - 项目实践: practice.md - 更新日志: updates.md - 3.x文档: api/index.html