第10篇:网络请求——获取远程数据

本课目标:掌握HTTP网络请求,能从服务器获取数据
**作者:**中文编程倡导者—— 李金雨
预计课时:3课时(135分钟)
难度等级:⭐⭐⭐⭐(高级)


一、开篇引入

1.1 为什么需要网络请求?

前面的课程,我们的数据都是写死在代码里的:

@State 商品列表: object[] = [
  { 名称: "手机", 价格: 2999 },
  { 名称: "电脑", 价格: 5999 }
]

但真实的应用需要从服务器获取数据:

  • 🌤️ 天气应用需要获取实时天气
  • 📰 新闻应用需要获取最新新闻
  • 🛒 电商应用需要获取商品信息
  • 💬 聊天应用需要获取消息记录

1.2 什么是HTTP请求?

HTTP(超文本传输协议)是浏览器和服务器之间通信的"语言"。

就像你去餐厅点菜:

你(客户端)                服务员(服务器)
   │                            │
   │  "我要一份炒饭"(请求)      │
   │ ─────────────────────────> │
   │                            │
   │  "好的,这是您的炒饭"(响应) │
   │ <───────────────────────── │

1.3 本课目标

今天我们要学习:

  1. 什么是HTTP请求
  2. 怎么发送GET请求
  3. 怎么发送POST请求
  4. 怎么处理异步操作
  5. 实战:天气查询、新闻阅读

1.4 预期成果

完成本课后,你能做出这样的应用:

天气查询:                新闻阅读:
┌─────────────┐          ┌─────────────┐
│  🌤️ 天气查询  │          │  📰 今日新闻  │
├─────────────┤          ├─────────────┤
│             │          │             │
│  北京       │          │ 🔥 新闻标题1  │
│  ⛅ 多云     │          │ 新闻摘要...   │
│  25°C       │          │             │
│             │          │ 📰 新闻标题2  │
│  湿度: 60%  │          │ 新闻摘要...   │
│  风速: 3级  │          │             │
│             │          │ 📰 新闻标题3  │
│ [刷新数据]  │          │ 新闻摘要...   │
│             │          │             │
└─────────────┘          └─────────────┘

二、概念讲解

2.1 HTTP基础

请求方法
方法 用途 例子
GET 获取数据 获取天气、获取新闻列表
POST 提交数据 登录、提交表单、发表评论
PUT 更新数据 修改用户信息
DELETE 删除数据 删除文章
状态码

服务器返回的"结果代码":

状态码 含义 说明
200 成功 请求成功,返回了数据
404 未找到 请求的资源不存在
500 服务器错误 服务器出问题了
403 禁止访问 没有权限

2.2 导入http模块

import http from '@ohos.net.http'

2.3 发送GET请求

基本流程
import http from '@ohos.net.http'

async 获取数据() {
  // 1. 创建HTTP请求对象
  let http请求 = http.createHttp()
  
  // 2. 发送请求
  let 响应 = await http请求.request(
    "https://api.example.com/data",    // 请求地址
    { method: http.RequestMethod.GET }  // 请求方法
  )
  
  // 3. 处理响应
  if (响应.responseCode == 200) {
    let 数据 = JSON.parse(响应.result.toString())
    console.log("获取成功:" + JSON.stringify(数据))
  } else {
    console.log("请求失败:" + 响应.responseCode)
  }
}
完整例子
// 完整可运行代码,复制到 Index.ets 即可运行
import http from '@ohos.net.http'

@Entry
@Component
struct Index {
  @State 数据: string = ""
  @State 加载中: boolean = false

  build() {
    Column({ space: 20 }) {
      Button("获取数据")
        .onClick(() => this.获取数据())
      
      if (this.加载中) {
        Text("加载中...")
      } else {
        Text(this.数据 || "点击按钮获取数据")
      }
    }
  }
  
  async 获取数据() {
    this.加载中 = true
    
    try {
      let http请求 = http.createHttp()
      let 响应 = await http请求.request(
        "https://api.example.com/data",
        { method: http.RequestMethod.GET }
      )
      
      if (响应.responseCode == 200) {
        this.数据 = 响应.result.toString()
      } else {
        this.数据 = "请求失败:" + 响应.responseCode
      }
    } catch (错误) {
      this.数据 = "网络错误:" + 错误.message
    } finally {
      this.加载中 = false
    }
  }
}

2.4 发送POST请求

提交表单数据
async 提交数据() {
  let http请求 = http.createHttp()
  
  let 响应 = await http请求.request(
    "https://api.example.com/login",
    {
      method: http.RequestMethod.POST,
      header: {
        'Content-Type': 'application/json'
      },
      extraData: JSON.stringify({
        用户名: "张三",
        密码: "123456"
      })
    }
  )
  
  if (响应.responseCode == 200) {
    let 结果 = JSON.parse(响应.result.toString())
    console.log("登录成功")
  }
}

2.5 异步操作

什么是异步?

网络请求需要时间(几秒甚至更久),如果等待时界面卡住,用户体验很差。

异步就是:

  • 发送请求后,不等结果,继续执行其他代码
  • 结果回来后,再处理
async/await
// async 标记这是一个异步函数
async 获取数据() {
  // await 等待请求完成
  let 响应 = await http请求.request(...)
  
  // 请求完成后才执行这里
  console.log("请求完成")
}
Promise
// 不用async/await的写法
获取数据() {
  let http请求 = http.createHttp()
  
  http请求.request("https://api.example.com/data")
    .then((响应) => {
      // 成功时执行
      console.log("成功:" + 响应.result)
    })
    .catch((错误) => {
      // 失败时执行
      console.log("失败:" + 错误.message)
    })
}

2.6 错误处理

async 获取数据() {
  try {
    // 可能出错的代码
    let 响应 = await http请求.request(...)
    
    if (响应.responseCode == 200) {
      // 处理成功
    } else {
      // 处理HTTP错误
      throw new Error("HTTP错误:" + 响应.responseCode)
    }
  } catch (错误) {
    // 捕获所有错误
    console.error("出错了:" + 错误.message)
    
    // 给用户友好的提示
    this.错误信息 = "网络连接失败,请检查网络"
  } finally {
    // 无论成功失败都执行
    this.加载中 = false
  }
}

三、动手实践

3.1 基础练习:天气查询应用

由于无法访问真实API,我们用模拟数据演示:

// 完整可运行代码,复制到 Index.ets 即可运行
import http from '@ohos.net.http'

@Entry
@Component
struct Index {
  @State 城市: string = "北京"
  @State 天气数据: object = null
  @State 加载中: boolean = false
  @State 错误信息: string = ""

  // 模拟天气数据(实际应用中使用真实API)
  模拟数据: object = {
    城市: "北京",
    天气: "多云",
    温度: 25,
    湿度: "60%",
    风速: "3级",
    图标: "⛅"
  }

  build() {
    Column({ space: 20 }) {
      // 标题
      Text("🌤️ 天气查询")
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin(20)
      
      // 城市选择
      Row({ space: 10 }) {
        Text("选择城市:")
          .fontSize(16)
        
        Select([
          { value: "北京" },
          { value: "上海" },
          { value: "广州" },
          { value: "深圳" }
        ])
          .selected(this.城市)
          .onSelect((索引: number,: string) => {
            this.城市 =})
      }
      
      // 查询按钮
      Button("查询天气", { type: ButtonType.Capsule })
        .width('80%')
        .height(50)
        .backgroundColor('#2196F3')
        .enabled(!this.加载中)
        .onClick(() => {
          this.查询天气()
        })
      
      // 加载中
      if (this.加载中) {
        Column({ space: 10 }) {
          LoadingProgress()
            .width(50)
            .height(50)
            .color('#2196F3')
          Text("正在查询...")
            .fontSize(14)
            .fontColor('#999999')
        }
        .margin(30)
      }
      
      // 错误信息
      if (this.错误信息 != "") {
        Text(this.错误信息)
          .fontSize(14)
          .fontColor('#F44336')
          .margin(20)
      }
      
      // 天气信息
      if (this.天气数据 != null && !this.加载中) {
        this.天气卡片()
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
  
  @Builder
  天气卡片() {
    Column({ space: 15 }) {
      Text(this.天气数据?.['图标'])
        .fontSize(80)
      
      Text(this.天气数据?.['城市'])
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      Text(`${this.天气数据?.['温度']}°C`)
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2196F3')
      
      Text(this.天气数据?.['天气'])
        .fontSize(20)
        .fontColor('#666666')
      
      Row({ space: 30 }) {
        Column({ space: 5 }) {
          Text("湿度")
            .fontSize(12)
            .fontColor('#999999')
          Text(this.天气数据?.['湿度'])
            .fontSize(16)
        }
        
        Column({ space: 5 }) {
          Text("风速")
            .fontSize(12)
            .fontColor('#999999')
          Text(this.天气数据?.['风速'])
            .fontSize(16)
        }
      }
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(30)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .shadow({ radius: 10, color: '#10000000' })
    .margin({ top: 20 })
  }
  
  async 查询天气() {
    this.加载中 = true
    this.错误信息 = ""
    this.天气数据 = null
    
    try {
      // 模拟网络请求延迟
      await this.延迟(1000)
      
      // 实际应用中使用真实API:
      // let http请求 = http.createHttp()
      // let 响应 = await http请求.request(
      //   `https://api.weather.com/v1/current?city=${this.城市}`,
      //   { method: http.RequestMethod.GET }
      // )
      
      // 模拟数据(替换为真实API响应)
      this.模拟数据.城市 = this.城市
      this.天气数据 = this.模拟数据
      
    } catch (错误) {
      this.错误信息 = "查询失败:" + 错误.message
    } finally {
      this.加载中 = false
    }
  }
  
  延迟(毫秒: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, 毫秒))
  }
}

3.2 进阶练习:新闻列表应用

import http from '@ohos.net.http'

interface 新闻数据 {
  id: string
  标题: string
  摘要: string
  时间: string
  图片?: string
  热度?: number
}

// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
  @State 新闻列表: 新闻数据[] = []
  @State 加载中: boolean = false
  @State 刷新中: boolean = false
  @State 错误信息: string = ""
  @State 当前分类: string = "推荐"

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('📰 今日新闻')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
        
        Button(this.加载中 ? '加载中' : '刷新')
          .fontSize(14)
          .enabled(!this.加载中)
          .backgroundColor('transparent')
          .fontColor('#2196F3')
          .onClick(() => this.加载新闻())
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(20)
      .backgroundColor('#FFFFFF')
      
      // 分类标签
      Scroll() {
        Row({ space: 10 }) {
          ForEach(['推荐', '国内', '国际', '科技', '体育', '娱乐'], (分类: string) => {
            Text(分类)
              .fontSize(14)
              .fontColor(this.当前分类 == 分类 ? '#FFFFFF' : '#666666')
              .padding({ left: 15, right: 15, top: 6, bottom: 6 })
              .backgroundColor(this.当前分类 == 分类 ? '#2196F3' : '#F5F5F5')
              .borderRadius(15)
              .onClick(() => {
                this.当前分类 = 分类
                this.加载新闻()
              })
          })
        }
        .padding(15)
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Horizontal)
      
      // 内容区域
      Stack() {
        if (this.加载中 && this.新闻列表.length == 0) {
          // 首次加载
          Column({ space: 10 }) {
            LoadingProgress()
              .width(50)
              .height(50)
            Text('加载中...')
              .fontColor('#999999')
          }
        } else if (this.错误信息 != '') {
          // 错误状态
          Column({ space: 10 }) {
            Text('❌')
              .fontSize(50)
            Text(this.错误信息)
              .fontColor('#F44336')
            Button('重试')
              .onClick(() => this.加载新闻())
          }
        } else if (this.新闻列表.length == 0) {
          // 空数据
          Column({ space: 10 }) {
            Text('📭')
              .fontSize(60)
            Text('暂无新闻')
              .fontColor('#999999')
          }
        } else {
          // 新闻列表
          this.新闻列表组件()
        }
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  @Builder
  新闻列表组件() {
    List({ space: 10 }) {
      ForEach(this.新闻列表, (新闻: 新闻数据) => {
        ListItem() {
          Column({ space: 10 }) {
            Row({ space: 10 }) {
              // 标题和摘要
              Column({ space: 8 }) {
                Text(新闻.标题)
                  .fontSize(17)
                  .fontWeight(FontWeight.Medium)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                
                Text(新闻.摘要)
                  .fontSize(14)
                  .fontColor('#666666')
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                
                Row({ space: 10 }) {
                  if (新闻.热度 && 新闻.热度 > 1000) {
                    Text('🔥 热门')
                      .fontSize(11)
                      .fontColor('#FFFFFF')
                      .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                      .backgroundColor('#FF5722')
                      .borderRadius(3)
                  }
                  
                  Text(新闻.时间)
                    .fontSize(12)
                    .fontColor('#999999')
                }
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              
              // 图片
              if (新闻.图片) {
                Text(新闻.图片)
                  .fontSize(50)
                  .width(80)
                  .height(80)
                  .backgroundColor('#E3F2FD')
                  .textAlign(TextAlign.Center)
              }
            }
            .width('100%')
          }
          .width('100%')
          .padding(15)
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
        }
      })
    }
    .padding(15)
    .refreshing(this.刷新中)
    .onRefresh(() => {
      this.刷新中 = true
      this.加载新闻()
    })
  }
  
  aboutToAppear() {
    this.加载新闻()
  }
  
  async 加载新闻() {
    if (!this.刷新中) {
      this.加载中 = true
    }
    this.错误信息 = ''
    
    try {
      // 模拟网络延迟
      await this.延迟(800)
      
      // 模拟数据(实际应用替换为真实API)
      this.新闻列表 = this.生成模拟新闻()
      
    } catch (错误) {
      this.错误信息 = '加载失败,请检查网络'
    } finally {
      this.加载中 = false
      this.刷新中 = false
    }
  }
  
  生成模拟新闻(): 新闻数据[] {
    let 基础数据 = [
      { 标题: '科技创新推动经济发展', 摘要: '最新报告显示,科技产业已成为经济增长的主要动力...', 热度: 5000 },
      { 标题: '教育改革新政策发布', 摘要: '教育部发布最新政策,将全面推进素质教育...', 热度: 3000 },
      { 标题: '环保行动取得显著成效', 摘要: '经过持续努力,城市空气质量明显改善...', 热度: 2000 },
      { 标题: '体育赛事精彩回顾', 摘要: '昨晚的比赛中,国家队表现出色,赢得关键胜利...', 热度: 4500 },
      { 标题: '新电影即将上映', 摘要: '备受期待的大片将于下月登陆各大影院...', 热度: 3500 }
    ]
    
    return 基础数据.map((新闻, 索引) => ({
      id: 索引.toString(),
      标题: `[${this.当前分类}] ${新闻.标题}`,
      摘要: 新闻.摘要,
      时间: '2小时前',
      图片: 索引 % 2 == 0 ? '📷' : undefined,
      热度: 新闻.热度
    }))
  }
  
  延迟(毫秒: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, 毫秒))
  }
}

3.3 进阶练习:封装网络请求工具

创建一个可复用的网络请求工具:

// 网络工具.ts
import http from '@ohos.net.http'

export class 网络工具 {
  // 基础URL
  static 基础URL: string = "https://api.example.com"
  
  // GET请求
  static async GET(路径: string, 参数?: object): Promise<object> {
    let url = this.基础URL + 路径
    
    // 添加查询参数
    if (参数) {
      let 查询字符串 = Object.keys(参数)
        .map(=> `${}=${encodeURIComponent(参数[])}`)
        .join('&')
      url += '?' + 查询字符串
    }
    
    return this.发送请求(url, http.RequestMethod.GET)
  }
  
  // POST请求
  static async POST(路径: string, 数据: object): Promise<object> {
    let url = this.基础URL + 路径
    return this.发送请求(url, http.RequestMethod.POST, 数据)
  }
  
  // 发送请求
  private static async 发送请求(
    url: string, 
    方法: http.RequestMethod,
    数据?: object
  ): Promise<object> {
    let http请求 = http.createHttp()
    
    let 选项: http.HttpRequestOptions = {
      method: 方法,
      header: {
        'Content-Type': 'application/json'
      }
    }
    
    if (数据) {
      选项.extraData = JSON.stringify(数据)
    }
    
    try {
      let 响应 = await http请求.request(url, 选项)
      
      if (响应.responseCode == 200) {
        return JSON.parse(响应.result.toString())
      } else {
        throw new Error(`HTTP ${响应.responseCode}`)
      }
    } catch (错误) {
      console.error('请求失败:', 错误)
      throw 错误
    }
  }
}

// 使用示例
import { 网络工具 } from './网络工具'

async 获取用户列表() {
  try {
    let 数据 = await 网络工具.GET('/users')
    console.log('用户列表:', 数据)
  } catch (错误) {
    console.error('获取失败:', 错误)
  }
}

async 登录(用户名: string, 密码: string) {
  try {
    let 结果 = await 网络工具.POST('/login', {
      username: 用户名,
      password: 密码
    })
    console.log('登录成功:', 结果)
  } catch (错误) {
    console.error('登录失败:', 错误)
  }
}

四、知识总结

4.1 核心概念回顾

  1. HTTP请求:客户端和服务器通信的方式
  2. GET:获取数据
  3. POST:提交数据
  4. 异步操作:不阻塞界面的请求方式
  5. async/await:处理异步的语法糖

4.2 网络请求速查

import http from '@ohos.net.http'

// GET请求
async GET请求() {
  let http请求 = http.createHttp()
  let 响应 = await http请求.request(
    "https://api.example.com/data",
    { method: http.RequestMethod.GET }
  )
  return JSON.parse(响应.result.toString())
}

// POST请求
async POST请求() {
  let http请求 = http.createHttp()
  let 响应 = await http请求.request(
    "https://api.example.com/submit",
    {
      method: http.RequestMethod.POST,
      header: { 'Content-Type': 'application/json' },
      extraData: JSON.stringify({: "值" })
    }
  )
  return JSON.parse(响应.result.toString())
}

4.3 状态码速查

状态码 含义 处理建议
200 成功 正常处理数据
400 请求错误 检查请求参数
401 未授权 需要登录
404 未找到 检查URL
500 服务器错误 稍后重试

4.4 常见错误提醒

错误现象 原因 解决方法
请求失败 网络不通 检查网络连接
解析错误 返回的不是JSON 检查API返回格式
超时 请求时间太长 增加超时时间或优化请求
权限错误 没有访问权限 检查是否需要登录

五、课后作业

5.1 巩固练习(必做)

练习1:用户登录

实现登录功能:

  • 输入用户名和密码
  • 发送POST请求验证
  • 显示登录结果
  • 保存登录状态

练习2:数据列表

实现一个数据列表:

  • 从服务器获取列表数据
  • 支持下拉刷新
  • 支持上拉加载更多
  • 显示加载状态

练习3:图片加载

实现图片展示:

  • 加载网络图片
  • 显示加载占位图
  • 处理加载失败
  • 支持点击查看大图

5.2 创意编程(选做)

创意1:翻译应用

  • 输入文字
  • 选择目标语言
  • 调用翻译API
  • 显示翻译结果

创意2:股票查询

  • 输入股票代码
  • 获取实时股价
  • 显示走势图
  • 添加到自选

创意3:音乐搜索

  • 搜索歌曲
  • 显示搜索结果
  • 播放预览
  • 查看歌词

5.3 下篇预习

下一篇,我们将学习数据持久化,把数据保存到本地。预习问题:

  1. 怎么把数据保存到手机?
  2. 怎么读取保存的数据?
  3. 什么时候需要本地存储?

恭喜你完成了第10篇的学习! 🎉

现在你已经掌握了网络请求,可以从服务器获取数据了。记住:网络请求是应用的血液,数据是应用的生命

下节课,我们将学习如何把数据保存到本地!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐