联网 App 第 4 课:图片上传,压缩后用 Web 客户端传到服务器、列表直接显示网络图片(附 .aia 源码)

第 4 课 图片上传:从相册选一张图,写一句说明,上传后大家都能在列表里看到。

下载 PhotoShare.aia 返回联网 App 专题

这一课学什么

  • 图片和数据分开存:图片文件交给服务端保存,换回一个图片地址;网络微数据库里只存这个地址。
  • 上传前压缩:手机照片动辄好几 MB,压缩后只有几百 KB,上传快、看的人加载也快。
  • 显示网络图片:图像组件的「图片」、列表显示框的图片,都可以直接填 http://… 地址。
图片上传:上传完成图片上传:大家的图片

图为由最终 .aia 执行真实积木、连真实服务端生成的布局预览(列表缩略图是从服务端取回的图片),不是真机截图。

为什么不把图片存进网络微数据库

网络微数据库存的是文本,想存图片只能把图片转成很长的 Base64 文字,体积还会再大三分之一,读写都慢,很多服务对单个值也有大小限制。

更好的分工是:

手机 ──图片文件──▶ 服务端 /upload ──返回──▶ 图片地址 http://…/files/xxxx.jpg
手机 ──地址 + 说明──▶ 网络微数据库(标签 photos)
看的人 ──取 photos──▶ 拿到地址 ──▶ 图像组件直接显示

本专题的服务端已经带了上传功能(服务端教程里下载的就是这个版本):只收 jpg、png、gif、webp 图片,单张不超过 5MB。

准备

服务端照常运行。下载 PhotoShare.aia 导入,填服务地址。

第一步:界面(这一课新增)

组件 名字 要设置的属性
图像选择器 图像选择器_选图 文本:从相册选图
图像 图像_预览 高度 220,勾选「自动缩放图片」
文本输入框 文本输入框_说明  
按钮 按钮_上传 先设为不可用,选了图再启用
列表显示框 列表显示框_相册 布局:图片 + 主文字 + 副文字(布局 4),图像宽高 72
Web 客户端 Web客户端1 超时时间 30000
图片处理 图片处理1  

第二步:选图、压缩、上传

global 选中图片 = ""

when 图像选择器_选图.AfterPicking() {
  选中图片 = 图像选择器_选图.Selection
  图像_预览.Picture = 选中图片
  按钮_上传.Enabled = true
}

when 按钮_上传.Click() {
  var 地址 = 微数据库1.GetValue("serviceUrl", "")
  if 地址 == "" {
    底部提示条1.Show("先在上面填好服务地址并点保存")
  } else {
    // 最长边缩到 1280、质量 80,一般只剩几百 KB
    var 小图 = 图片处理1.Compress(选中图片, 1280, 80)
    if 小图 == "" {
      底部提示条1.Show("这张图片处理不了,换一张试试")
    } else {
      按钮_上传.Enabled = false
      标签_上传状态.Text = join("正在上传(", round(图片处理1.FileSize(小图) / 1024), " KB)……")
      Web客户端1.Url = join(地址, "/upload")
      Web客户端1.PostFile(小图)
    }
  }
}
  • PostFile 把整个文件作为请求内容发给 服务地址/upload,服务端存好后回一段 JSON:{"status": "OK", "url": "http://…/files/xxxx.jpg"}。 本课使用原始文件请求体上传,服务端专门读取请求内容。它与浏览器表单的 multipart/form-data 不同:依赖 PHP $_FILES 的普通上传接口或要求 multipart 的图床,不能直接替换本课 /upload 地址。仅手动设置 Content-Type 也不会生成表单分隔符、文件字段和附加字段。对象存储应以具体接口的上传协议为准,例如预签名 PUT 与 multipart POST 是两种不同流程。现有 PHP 文件上传教程 的旧示例也是读取原始请求体。对要求表单上传的接口,可以改用 Web 客户端原生的 执行POST表单文件请求,设置接口要求的文件字段名、附加字段与请求头。

  • 压缩返回的是新文件的路径;返回空文本表示这张图处理失败(比如选到的其实不是图片)。

中途验证:选一张照片点上传,状态显示「正在上传(xxx KB)……」;电脑浏览器打开服务地址页面,标题里「图片 1 张」。

第三步:拿到地址,登记到相册

global 待保存 = ""

when Web客户端1.GotText(url, responseCode, responseType, responseContent) {
  if responseCode == 200 {
    var 结果 = Web客户端1.JsonTextDecodeWithDictionaries(responseContent)
    var 图片地址 = dictget(结果, "url", "")
    图像_预览.Picture = 图片地址
    待保存 = {"url": 图片地址, "note": trim(文本输入框_说明.Text), "time": 计时器1.FormatDateTime(计时器1.Now(), "MM-dd HH:mm")}
    网络微数据库1.GetValue("photos")
  } else {
    按钮_上传.Enabled = true
    if responseCode == 413 {
      标签_上传状态.Text = "上传失败:图片超过 5MB"
    } else if responseCode == 400 {
      标签_上传状态.Text = "上传失败:只支持 jpg、png、gif、webp 图片"
    } else if responseCode == 404 {
      标签_上传状态.Text = "上传失败:服务地址不对(口令那一段抄错了?)"
    } else {
      标签_上传状态.Text = join("上传失败,服务器返回 ", responseCode)
    }
  }
}

拿到地址后把预览图换成服务器上的那张——能显示出来,就说明上传真的成功了,地址也能访问。接下来和第 2 课的留言一样:先取 photos 列表,追加 待保存,再存回去(.aia 里还把列表限制在最近 50 张)。

中途验证:电脑浏览器打开状态里那张图片的地址(可以在服务地址页面的 photos 里找到),能看到压缩后的图片。

第四步:连不上服务端时

when Web客户端1.TimedOut(url) {
  按钮_上传.Enabled = true
  标签_上传状态.Text = "上传超时:网络太慢,过一会儿再试"
}

when Screen1.ErrorOccurred(component, functionName, errorNumber, message) {
  if functionName == "PostFile" {
    按钮_上传.Enabled = true
    标签_上传状态.Text = "上传失败:连不上服务端。检查服务端窗口还开着吗、手机和电脑是同一个 WiFi 吗?"
  } else {
    底部提示条1.Show(message)
  }
}

这里有个容易漏的坑:服务端没开、地址连不上时,Web 客户端不会触发「请求超时时」,而是报到屏幕的「出现错误时」(错误号 1104)。只写了超时事件的话,按钮会一直是灰的。

第五步:列表里直接显示网络图片

global 显示顺序 = []

procedure 显示(列表) {
  显示顺序 = []
  var 项 = []
  for i = length(列表) to 1 step -1 {
    var 条 = select(列表, i)
    listAdd(显示顺序, 条)
    var 说明 = dictget(条, "note", "")
    listAdd(项, 列表显示框_相册.CreateElement(choose(说明 == "", "(没写说明)", 说明), dictget(条, "time", ""), dictget(条, "url", "")))
  }
  列表显示框_相册.Elements = 项
}

when 列表显示框_相册.AfterPicking() {
  var 条 = select(显示顺序, 列表显示框_相册.SelectionIndex)
  图像_预览.Picture = dictget(条, "url", "")
}
  • 「创建列表元素」的第三个参数就是图片,直接给网络地址;列表显示框会自己下载、缓存。
  • 列表是倒序显示的,点击时的「选中项索引」对应的是倒序后的顺序,所以另存一份 显示顺序 来取。

跑起来看看

  • 上传完成:上面的大图换成服务器上的图,提示「上传成功」,说明清空,等下一张。
  • 另一台手机打开:刷新后列表里有这张图和说明。
  • 点列表里的一张:上面显示大图。
  • 关掉服务端再上传:提示连不上,按钮恢复;开回服务端再点上传就成功了。

常见问题

现象 原因与办法
一直「正在上传」,按钮是灰的 缺了「出现错误时」的处理,服务端连不上时没有事件恢复按钮。照第四步补上
提示服务地址不对 口令抄错,服务端返回 404
提示图片超过 5MB 没压缩就上传了。确认传给 PostFile 的是压缩后的路径
列表里有文字没图片 地址打不开:手机能打开服务地址页面吗?服务端换了电脑或 IP 后,旧图片地址里的 IP 也就失效了
图片方向是横的 压缩会按照片里的方向信息自动摆正;直接上传原图才可能出现
服务端重启后图片还在吗 在。图片存在服务端文件夹的 files 里,备份时连同 tinywebdb.sqlite3 一起备份

动手改一改

  1. 在压缩前显示原图大小、压缩后显示新大小,看看压缩省了多少。
  2. 上传前加一个「拍照」按钮(照相机组件),拍完直接上传。

下一课

第 5 课 带图商品墙:把前四课串起来——发布带图片的商品、搜索、看详情、下架自己的商品。

用到的组件

图像选择器 · 图片处理 · Web 客户端 · 图像 · 列表显示框 · 网络微数据库

验证范围

最终 .aia 已通过正式编辑器载入与保存重开(0 错误)、后台导入和 Android APK 编译;连真实服务端执行积木检查 19 项(按 Web 客户端的分块方式真实上传、取回的图片是压缩后的 1280×960 JPEG、竖图缩到 720×1280、口令错误 404、压缩失败、服务端关掉时的出现错误时、重试成功等),见验证记录。真机相册选图、真机运行与 AI 伴侣联调尚未测试。

文档反馈