当前位置:   article > 正文

HarmonyOS开发:基于http开源一个网络请求库

HarmonyOS开发:基于http开源一个网络请求库

前言

网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例:

  1. 作者:程序员一鸣
  2. 链接:https://juejin.cn/post/7295397683397181450
  1. // 引入包名
  2. import http from '@ohos.net.http';
  3. // 每一个httpRequest对应一个HTTP请求任务,不可复用
  4. let httpRequest = http.createHttp();
  5. // 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
  6. // 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。8+
  7. httpRequest.on('headersReceive', (header) => {
  8. console.info('header: ' + JSON.stringify(header));
  9. });
  10. httpRequest.request(
  11. // 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
  12. "EXAMPLE_URL",
  13. {
  14. method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
  15. // 开发者根据自身业务需要添加header字段
  16. header: {
  17. 'Content-Type': 'application/json'
  18. },
  19. // 当使用POST请求时此字段用于传递内容
  20. extraData: {
  21. "data": "data to send",
  22. },
  23. expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
  24. usingCache: true, // 可选,默认为true
  25. priority: 1, // 可选,默认为1
  26. connectTimeout: 60000, // 可选,默认为60000ms
  27. readTimeout: 60000, // 可选,默认为60000ms
  28. usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
  29. }, (err, data) => {
  30. if (!err) {
  31. // data.result为HTTP响应内容,可根据业务需要进行解析
  32. console.info('Result:' + JSON.stringify(data.result));
  33. console.info('code:' + JSON.stringify(data.responseCode));
  34. // data.header为HTTP响应头,可根据业务需要进行解析
  35. console.info('header:' + JSON.stringify(data.header));
  36. console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
  37. // 取消订阅HTTP响应头事件
  38. httpRequest.off('headersReceive');
  39. // 当该请求使用完毕时,调用destroy方法主动销毁
  40. httpRequest.destroy();
  41. } else {
  42. console.info('error:' + JSON.stringify(err));
  43. // 取消订阅HTTP响应头事件
  44. httpRequest.off('headersReceive');
  45. // 当该请求使用完毕时,调用destroy方法主动销毁。
  46. httpRequest.destroy();
  47. }
  48. }
  49. );

以上的案例,每次请求书写这么多代码,在实际的开发中,是无法承受的,所以基于此,封装是很有必要的,把公共的部分进行抽取包装,固定不变的参数进行初始化设置,重写基本的请求方式,这是我们封装的基本宗旨。

我们先看一下封装之后的调用方式:

异步请求

  1. Net.get("url").requestString((data) => {
  2. //data 为 返回的json字符串
  3. })

同步请求

  1. const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
  2. //data 为 返回的json字符串

装饰器请求

  1. @GET("url")
  2. private getData():Promise<string> {
  3. return null
  4. }

封装之后,不仅使用起来更加的便捷,而且还拓展了请求类型,满足不同需求的场景。

本篇的文章内容大致如下:

1、net库主要功能点介绍

2、net库快速依赖使用

3、net库全局初始化

4、异步请求介绍

5、同步请求介绍

6、装饰器请求介绍

7、上传下载介绍

8、Dialog加载使用

9、相关总结

一、net库主要功能点介绍

目前net库一期已经开发完毕,har包使用,大家可以看第二项,截止到发文前,所支持的功能如下:

■ 支持全局初始化

■ 支持统一的BaseUrl

■ 支持全局错误拦截

■ 支持全局头参拦截

■ 支持同步方式请求(get/post/delete/put/options/head/trace/connect)

■ 支持异步方式请求(get/post/delete/put/options/head/trace/connect)

■ 支持装饰器方式请求(get/post/delete/put/options/head/trace/connect)

■ 支持dialog加载

■ 支持返回Json字符串

■ 支持返回对象

■ 支持返回数组

■ 支持返回data一层数据

■ 支持上传文件

■ 支持下载文件

□ 数据缓存开发中……

二、net库快速依赖使用

私服和远程依赖,由于权限和审核问题,预计需要等到2024年第一季度面向所有开发者,所以,只能使用本地静态共享包和源码 两种使用方式,本地静态共享包类似Android中的aar依赖,直接复制到项目中即可,目前源码还在优化中,先暴露静态共享包这一使用方式。

本地静态共享包har包使用

首先,下载har包,https://github.com/AbnerMing888/HarmonyOsNet/raw/master/net.har

下载之后,把har包复制项目中,目录自己创建,如下,我创建了一个libs目录,复制进去

a6317d65264640353fcb1d5139d828cf.jpeg

引入之后,进行同步项目,点击Sync Now即可,当然了你也可以,将鼠标放置在报错处会出现提示,在提示框中点击Run 'ohpm install'。

需要注意,@app/net,是用来区分目录的,可以自己定义,比如@aa/bb等,关于静态共享包的创建和使用,请查看如下我的介绍,这里就不过多介绍。

HarmonyOS开发:走进静态共享包的依赖与使用https://juejin.cn/post/7274982412245876776

查看是否引用成功

无论使用哪种方式进行依赖,最终都会在使用的模块中,生成一个oh_modules文件,并创建源代码文件,有则成功,无则失败,如下:

97eda110e9e73cc2fff2edb2e09ff638.jpeg

三、net库全局初始化

推荐在AbilityStage进行初始化,初始化一次即可,初始化参数可根据项目需要进行选择性使用。

  1. Net.getInstance().init({
  2. baseUrl: "https://www.vipandroid.cn", //设置全局baseurl
  3. connectTimeout: 10000, //设置连接超时
  4. readTimeout: 10000, //设置读取超时
  5. netErrorInterceptor: new MyNetErrorInterceptor(), //设置全局错误拦截,需要自行创建,可在这里进行错误处理
  6. netHeaderInterceptor: new MyNetHeaderInterceptor(), //设置全局头拦截器,需要自行创建
  7. header: {}, //头参数
  8. resultTag: []//接口返回数据参数,比如data,items等等
  9. })

1、初始化属性介绍

初始化属性,根据自己需要选择性使用。

属性类型概述
baseUrlstring一般标记为统一的请求前缀,也就是域名
connectTimeoutnumber连接超时,默认10秒
readTimeoutnumber读取超时,默认10秒
netErrorInterceptorINetErrorInterceptor全局错误拦截器,需继承INetErrorInterceptor
netHeaderInterceptorINetHeaderInterceptor全局请求头拦截器,需继承INetHeaderInterceptor
headerObject全局统一的公共头参数
resultTagArray接口返回数据参数,比如data,items等等

2、设置请求头拦截

关于全局头参数传递,可以通过以上的header参数或者在请求头拦截里均可,如果没有同步等逻辑操作,只是固定的头参数,建议直接使用header参数。

名字自定义,实现INetHeaderInterceptor接口,可在netHeader方法里打印请求头或者追加请求头。

  1. import { HttpHeaderOptions, NetHeaderInterceptor } from '@app/net'
  2. class MyNetHeaderInterceptor implements NetHeaderInterceptor {
  3. getHeader(options: HttpHeaderOptions): Promise<Object> {
  4. //可以进行接口签名,传入头参数
  5. return null
  6. }
  7. }

HttpHeaderOptions对象

返回了一些常用参数,可以用于接口签名等使用。

  1. export class HttpHeaderOptions {
  2. url?: string //请求地址
  3. method?: http.RequestMethod //请求方式
  4. header?: Object //头参数
  5. params?: Object //请求参数
  6. }

3、设置全局错误拦截器

名字自定义,实现INetErrorInterceptor接口,可在httpError方法里进行全局的错误处理,比如统一跳转,统一提示等。

  1. import { NetError } from '@app/net/src/main/ets/error/NetError';
  2. import { INetErrorInterceptor } from '@app/net/src/main/ets/interceptor/INetErrorInterceptor';
  3. export class MyNetErrorInterceptor implements INetErrorInterceptor {
  4. httpError(error: NetError) {
  5. //这里进行拦截错误信息
  6. }
  7. }
NetError对象

可通过如下方法获取错误code和错误描述信息。

  1. /*
  2. * 返回code
  3. * */
  4. getCode():number{
  5. return this.code
  6. }
  7. /*
  8. * 返回message
  9. * */
  10. getMessage():string{
  11. return this.message
  12. }

四、异步请求介绍

1、请求说明

为了方便数据的针对性返回,目前异步请求提供了三种请求方法,在实际的 开发中,大家可以针对需要,选择性使用。

request方法
  1. Net.get("url").request<TestModel>((data) => {
  2. //data 就是返回的TestModel对象
  3. })

此方法,针对性返回对应的data数据对象,如下json,则会直接返回需要的data对象,不会携带外层的code等其他参数,方便大家直接的拿到数据。

  1. {
  2. "code": 0,
  3. "message": "数据返回成功",
  4. "data": {}
  5. }

如果你的data是一个数组,如下json:

  1. {
  2. "code": 0,
  3. "message": "数据返回成功",
  4. "data": []
  5. }

数组获取

  1. Net.get("url").request<TestModel[]>((data) => {
  2. //data 就是返回的TestModel[]数组
  3. })
  4. //或者如下
  5. Net.get("url").request<Array<TestModel>>((data) => {
  6. //data 就是返回的TestModel数组
  7. })

可能大家有疑问,如果接口返回的json字段不是data怎么办?如下:

举例一

  1. {
  2. "code": 0,
  3. "message": "数据返回成功",
  4. "items": {}
  5. }

举例二

  1. {
  2. "code": 0,
  3. "message": "数据返回成功",
  4. "models": {}
  5. }

虽然网络库中默认取的是json中的data字段,如果您的数据返回类型字段有多种,如上json,可以通过全局初始化resultTag进行传递或者局部setResultTag传递即可。

全局设置接口返回数据参数【推荐】

全局设置,具体设置请查看上边的全局初始化一项,只设置一次即可,不管你有多少种返回参数,都可以统一设置。

  1. Net.getInstance().init({
  2. resultTag: ["data", "items", "models"]//接口返回数据参数,比如data,items等等
  3. })

局部设置接口返回数据参数

通过setResultTag方法设置即可。

  1. Net.get("")
  2. .setResultTag(["items"])
  3. .request<TestModel>((data) => {
  4. })
requestString方法

requestString就比较简单,就是普通的返回请求回来的json字符串。

  1. Net.get("url").requestString((data) => {
  2. //data 为 返回的json字符串
  3. })
requestObject方法

requestObject方法也是获取对象,和request不同的是,它不用设置返回参数,因为它是返回的整个json对应的对象, 也就是包含了code,message等字段。

  1. Net.get("url").requestObject<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. })

为了更好的复用共有字段,你可以抽取一个基类,如下:

  1. export class ApiResult<T> {
  2. code: number
  3. message: string
  4. data: T
  5. }

以后就可以如下请求:

  1. Net.get("url").requestObject<ApiResult<TestModel>>((data) => {
  2. //data 为 返回的ApiResult对象
  3. })
回调函数

回调函数有两个,一个成功一个失败,成功回调必调用,失败可选择性调用。

只带成功

  1. Net.get("url").request<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. })

成功失败都带

  1. Net.get("url").request<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. }, (error) => {
  4. //失败
  5. })

2、get请求

  1. Net.get("url").request<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. })

3、post请求

  1. Net.post("url").request<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. })

4、delete请求

  1. Net.delete("url").request<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. })

5、put请求

  1. Net.put("url").request<TestModel>((data) => {
  2. //data 为 返回的TestModel对象
  3. })

6、其他请求方式

除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options。

  1. OPTIONS
  2. HEAD
  3. TRACE
  4. CONNECT

7、各个方法调用

除了正常的请求方式之外,你也可以调用如下的参数:

方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:

  1. Net.get("url")
  2. .setHeaders({})//单独添加请求头参数
  3. .setBaseUrl("")//单独替换BaseUrl
  4. .setParams({})//单独添加参数
  5. .setConnectTimeout(10000)//单独设置连接超时
  6. .setReadTimeout(10000)//单独设置读取超时
  7. .setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
  8. .setUsingCache(true)//使用缓存,默认为true
  9. .setPriority(1)//设置优先级 默认为1
  10. .setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
  11. .setResultTag([""])//接口返回数据参数,比如data,items等等
  12. .setContext(this.context)//设置上下文,用于上传文件和下载文件
  13. .setCustomDialogController()//传递的dialog控制器,用于展示dialog
  14. .request<TestModel>((data) => {
  15. //data 为 返回的TestModel对象
  16. })

五、同步请求介绍

同步请求需要注意,需要await关键字和async关键字结合使用。

  1. private async getTestModel(){
  2. const testModel = await Net.get("url").returnData<TestModel>()
  3. }

1、请求说明

同步请求和异步请求一样,也是有三种方式,是通过参数的形式,默认直接返回data层数据。

返回data层数据

和异步种的request方法类似,只返回json种的data层对象数据,不会返回code等字段。

  1. private async getData(){
  2. const data = await Net.get("url").returnData<TestModel>()
  3. //data为 返回的 TestModel对象
  4. }
返回Json对象

和异步种的requestObject方法类似,会返回整个json对象,包含code等字段。

  1. private async getData(){
  2. const data = await Net.get("url").returnData<TestModel>(ReturnDataType.OBJECT)
  3. //data为 返回的 TestModel对象
  4. }
返回Json字符串

和异步种的requestString方法类似。

  1. private async getData(){
  2. const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
  3. //data为 返回的 json字符串
  4. }
返回错误

异步方式有回调错误,同步方式如果发生错误,也会直接返回错误,结构如下:

  1. {
  2. "code": 0,
  3. "message": "错误信息"
  4. }

除了以上的错误捕获之外,你也可以全局异常捕获,

2、get请求

const data = await Net.get("url").returnData<TestModel>()

3、post请求

const data = await Net.post("url").returnData<TestModel>()

4、delete请求

const data = await Net.delete("url").returnData<TestModel>()

5、put请求

const data = await Net.put("url").returnData<TestModel>()

6、其他请求方式

除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options

  1. OPTIONS
  2. HEAD
  3. TRACE
  4. CONNECT

7、各个方法调用

除了正常的请求方式之外,你也可以调用如下的参数:

方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:

  1. const data = await Net.get("url")
  2. .setHeaders({})//单独添加请求头参数
  3. .setBaseUrl("")//单独替换BaseUrl
  4. .setParams({})//单独添加参数
  5. .setConnectTimeout(10000)//单独设置连接超时
  6. .setReadTimeout(10000)//单独设置读取超时
  7. .setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
  8. .setUsingCache(true)//使用缓存,默认为true
  9. .setPriority(1)//设置优先级 默认为1
  10. .setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
  11. .setResultTag([""])//接口返回数据参数,比如data,items等等
  12. .setContext(this.context)//设置上下文,用于上传文件和下载文件
  13. .setCustomDialogController()//传递的dialog控制器,用于展示dialog
  14. .returnData<TestModel>()
  15. //data为 返回的 TestModel对象

六、装饰器请求介绍

网络库允许使用装饰器的方式发起请求,也就是通过注解的方式,目前采取的是装饰器方法的形式。

1、请求说明

装饰器和同步异步有所区别,只返回两种数据类型,一种是json字符串,一种是json对象,暂时不提供返回data层数据。 在使用的时候,您可以单独创建工具类或者ViewModel或者直接使用,都可以。

返回json字符串
  1. @GET("url")
  2. private getData():Promise<string> {
  3. return null
  4. }
返回json对象
  1. @GET("url")
  2. private getData():Promise<TestModel> {
  3. return null
  4. }

2、get请求

  1. @GET("url")
  2. private getData():Promise<TestModel> {
  3. return null
  4. }

3、post请求

  1. @POST("url")
  2. private getData():Promise<TestModel> {
  3. return null
  4. }

4、delete请求

  1. @DELETE("url")
  2. private getData():Promise<TestModel> {
  3. return null
  4. }

5、put请求

  1. @PUT("url")
  2. private getData():Promise<TestModel> {
  3. return null
  4. }

6、其他请求方式

除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如@OPTIONS。

  1. OPTIONS
  2. HEAD
  3. TRACE
  4. CONNECT

当然,大家也可以使用统一的NET装饰器,只不过需要自己设置请求方法,代码如下:

  1. @NET("url", { method: http.RequestMethod.POST })
  2. private getData():Promise<string> {
  3. return null
  4. }

7、装饰器参数传递

直接参数传递

直接参数,在调用装饰器请求时,后面添加即可,一般针对固定参数。

  1. @GET("url", {
  2. baseUrl: "", //baseUrl
  3. header: {}, //头参数
  4. params: {}, //入参
  5. connectTimeout: 1000, //连接超时
  6. readTimeout: 1000, //读取超时
  7. isReturnJson: true//默认false 返回Json字符串,默认返回json对象
  8. })
  9. private getData():Promise<string> {
  10. return null
  11. }
动态参数传递

动态参数适合参数可变的情况下传递,比如分页等情况。

  1. @GET("url")
  2. private getData(data? : HttpOptions):Promise<string> {
  3. return null
  4. }

调用时传递

  1. private async doHttp(){
  2. const data = await this.getData({
  3. baseUrl: "", //baseUrl
  4. header: {}, //头参数
  5. params: {}, //入参
  6. connectTimeout: 1000, //连接超时
  7. readTimeout: 1000, //读取超时
  8. isReturnJson: true//默认false 返回Json字符串,默认返回json对象
  9. })
  10. }
装饰器参数传递

使用DATA装饰器,DATA必须在上!

  1. @DATA({
  2. baseUrl: "", //baseUrl
  3. header: {}, //头参数
  4. params: {}, //入参
  5. connectTimeout: 1000, //连接超时
  6. readTimeout: 1000, //读取超时
  7. isReturnJson: true//默认false 返回Json字符串,默认返回json对象
  8. })
  9. @GET("url")
  10. private getData():Promise<string> {
  11. return null
  12. }

七、上传下载介绍

1、上传文件TypeScript

  1. Net.uploadFile("")//上传的地址
  2. .setUploadFiles([])//上传的文件 [{ filename: "test", name: "test", uri: "internal://cache/test.jpg", type: "jpg" }]
  3. .setUploadData([])//上传的参数 [{ name: "name123", value: "123" }]
  4. .setProgress((receivedSize, totalSize) => {
  5. //监听上传进度
  6. })
  7. .request((data) => {
  8. if (data == UploadTaskState.COMPLETE) {
  9. //上传完成
  10. }
  11. })
方法介绍
方法类型概述
uploadFilestring上传的地址
setUploadFilesArray上传的文件数组
setUploadDataArray上传的参数数组
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求上传,data类型为UploadTaskState,有三种状态:START(开始),COMPLETE(完成),ERROR(错误)
其他方法
删除上传进度监听
uploadRequest.removeProgressCallback()
删除上传任务
  1. uploadRequest.deleteUploadTask((result) => {
  2. if (result) {
  3. //成功
  4. } else {
  5. //失败
  6. }
  7. })

2、下载文件

  1. Net.downLoadFile("http://10.47.24.237:8888/harmony/log.har")
  2. .setContext(EntryAbility.context)
  3. .setFilePath(EntryAbility.filePath)
  4. .setProgress((receivedSize, totalSize) => {
  5. //监听下载进度
  6. })
  7. .request((data) => {
  8. if (data == DownloadTaskState.COMPLETE) {
  9. //下载完成
  10. }
  11. })
方法介绍
方法类型概述
downLoadFilestring下载的地址
setContextContext上下文
setFilePathstring下载后保存的路径
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求下载,data类型为DownloadTaskState,有四种状态:START(开始),COMPLETE(完成),PAUSE(暂停),REMOVE(结束)
其他方法
移除下载的任务
  1. downLoadRequest.deleteDownloadTask((result) => {
  2. if (result) {
  3. //移除成功
  4. } else {
  5. //移除失败
  6. }
  7. })
暂停下载任务
  1. downLoadRequest.suspendDownloadTask((result) => {
  2. if (result) {
  3. //暂停成功
  4. } else {
  5. //暂停失败
  6. }
  7. })
重新启动下载任务
  1. downLoadRequest.restoreDownloadTask((result) => {
  2. if (result) {
  3. //成功
  4. } else {
  5. //失败
  6. }
  7. })
删除监听下载进度
downLoadRequest.removeProgressCallback()

八、Dialog加载使用

b8af423baca671385503537e21bf352c.jpeg

1、定义dialog控制器

NetLoadingDialog是net包中自带的,菊花状弹窗,如果和实际业务不一致,可以更换。

  1. private mCustomDialogController = new CustomDialogController({
  2. builder: NetLoadingDialog({
  3. loadingText: '请等待...'
  4. }),
  5. autoCancel: false,
  6. customStyle: true
  7. })

2、调用传递控制器方法

此方法会自动显示和隐藏dialog,如果觉得不合适,大家可以自己定义即可。

setCustomDialogController(this.mCustomDialogController)

九、相关总结

开发环境如下:

  1. DevEco Studio 4.0 Beta2,Build Version: 4.0.0.400
  2. Api版本:9
  3. hvigorVersion:3.0.2

目前呢,暂时不支持缓存,后续会逐渐加上,大家在使用的过程中,需要任何的问题,都可以进行反馈,都会第一时间进行解决。

关注我获取更多知识或者投稿

4c1418d0dff7bdee10348f07806ba682.jpeg

70b90ae467f271866baa3938525b1292.jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/236760
推荐阅读
相关标签
  

闽ICP备14008679号