App Inventor 2 上传文件和图片到服务器教程

« 返回首页

概述

在 App Inventor 2 开发中,上传文件或图片到服务器是一个非常常见的需求。无论是拍照上传、选择本地文件上传,还是将 App 中生成的数据发送到远程服务器,App Inventor 2 的 Web客户端(Web)组件都能胜任。

本文将详细介绍两种主流上传方案:PostFile 方案Base64 编码方案,并提供完整的服务器端 PHP 代码示例。

方案对比

对比项 PostFile 方案 Base64 编码方案
原理 multipart/form-data 格式直接发送文件二进制数据 将文件转为 Base64 字符串,以文本方式发送
积木块 Web1.PostFile Web1.PostText
传输体积 与原文件相同 比 POST 大约 33%(Base64 编码膨胀)
服务器端 标准 $_FILES 接收,简单 需要 base64_decode 解码
适用场景 直接上传文件(拍照、选文件) 需要在发送前修改文件内容、或与其他文本数据一起传输
推荐程度 ⭐⭐⭐ 首选方案 ⭐⭐ 备选方案

推荐:大多数场景下使用 PostFile 方案,更简单高效。


方案一:PostFile 直接上传

原理

Web客户端 组件的 PostFile 方法将指定路径的文件以 multipart/form-data 格式 POST 到服务器 URL。服务器端使用标准的文件上传处理方式即可接收。

组件准备

在 App Inventor 2 设计视图中,添加以下组件:

组件 名称 用途
Button Button_Upload 点击上传按钮
Camera Camera1 拍照组件
ImagePicker ImagePicker1 从相册选择图片
Web Web1 网络请求组件
Label Label_Status 显示上传状态
TextBox TextBox_ServerUrl 输入服务器地址(测试用)

积木块代码

1. 拍照后上传

当 Button_拍照上传.被点击 时:
  调用 Camera1.执行拍照

当 Camera1.拍照完成(图片路径) 时:
  设置 global_CurrentFilePath 为 图片路径
  设置 Label_Status.文本 为 "正在上传..."
  设置 Web1.网址 为 "http://你的服务器地址/upload.php"
  调用 Web1.PostFile(图片路径)

2. 从相册选择图片后上传

当 Button_选择上传.被点击 时:
  调用 ImagePicker1.打开

当 ImagePicker1.选择完成(选择结果) 时:
  如果 选择结果 ≠ "" 则:
    设置 global_CurrentFilePath 为 选择结果
    设置 Label_Status.文本 为 "正在上传..."
    设置 Web1.网址 为 "http://你的服务器地址/upload.php"
    调用 Web1.PostFile(选择结果)

3. 处理上传结果

当 Web1.收到返回内容(响应代码, 响应类型, 响应内容) 时:
  如果 响应代码 = 200 则:
    设置 Label_Status.文本 为 "上传成功!服务器返回:" & 响应内容
  否则:
    设置 Label_Status.文本 为 "上传失败,错误码:" & 响应代码

当 Web1.发生错误(错误信息) 时:
  设置 Label_Status.文本 为 "上传出错:" & 错误信息

方案二:Base64 编码上传

原理

将文件内容通过 File 组件读取为文本,再用 Web客户端 组件的 PostText 方法发送 Base64 编码的字符串到服务器。服务器端解码后保存为文件。

组件准备

在方案一的基础上,额外添加:

组件 名称 用途
File File1 读取文件内容

积木块代码

1. 读取文件并 Base64 编码上传

当 Button_上传.被点击 时:
  设置 Label_Status.文本 为 "正在读取文件..."
  调用 File1.读取文件(调用 File1.转换为文件路径(global_CurrentFilePath))

当 File1.读取完成(文本内容) 时:
  设置 Label_Status.文本 为 "正在上传..."
  设置 Web1.网址 为 "http://你的服务器地址/upload_base64.php"
  设置 global_Base64Data 为 调用 Web1.进行Base64编码(文本内容)
  调用 Web1.PostText("data=" & global_Base64Data, "application/x-www-form-urlencoded")

注意File1.转换为文件路径 方法用于将外部路径(如相册返回的路径)转换为 App 可读取的路径。在某些设备上,可能需要使用 content:// 路径的处理方式。

2. 处理上传结果(同方案一)

当 Web1.收到返回内容(响应代码, 响应类型, 响应内容) 时:
  如果 响应代码 = 200 则:
    设置 Label_Status.文本 为 "上传成功!"
  否则:
    设置 Label_Status.文本 为 "上传失败"

当 Web1.发生错误(错误信息) 时:
  设置 Label_Status.文本 为 "上传出错:" & 错误信息

服务器端代码(PHP)

方案一服务器端:upload.php(接收 PostFile)

<?php
/**
 * App Inventor 2 PostFile 上传接收脚本
 * 接收 multipart/form-data 格式的文件上传
 */

// 设置响应头
header('Content-Type: text/plain; charset=utf-8');

// 上传文件保存目录(确保目录存在且有写权限)
$uploadDir = 'uploads/';

if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

// 检查是否有文件上传
if (!isset($_FILES) || empty($_FILES)) {
    // PostFile 上传时,文件字段名默认为空或 "_data"
    http_response_code(400);
    echo "错误:未接收到文件";
    exit;
}

// 获取上传的文件信息
// PostFile 提交时,字段名为 "_data"
$fileKey = array_keys($_FILES)[0];
$file = $_FILES[$fileKey];

if ($file['error'] !== UPLOAD_ERR_OK) {
    $errorMessages = [
        UPLOAD_ERR_INI_SIZE   => '文件超过 php.ini 中 upload_max_filesize 的限制',
        UPLOAD_ERR_FORM_SIZE  => '文件超过表单中 MAX_FILE_SIZE 的限制',
        UPLOAD_ERR_PARTIAL    => '文件只有部分被上传',
        UPLOAD_ERR_NO_FILE    => '没有文件被上传',
        UPLOAD_ERR_NO_TMP_DIR => '找不到临时目录',
        UPLOAD_ERR_CANT_WRITE => '写入磁盘失败',
    ];
    $msg = isset($errorMessages[$file['error']]) 
         ? $errorMessages[$file['error']] 
         : '未知上传错误';
    http_response_code(400);
    echo "错误:" . $msg;
    exit;
}

// 生成安全的文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
if (empty($ext)) {
    // 如果没有扩展名,根据 MIME 类型推断
    $mimeToExt = [
        'image/jpeg' => 'jpg',
        'image/png'  => 'png',
        'image/gif'  => 'gif',
        'image/webp' => 'webp',
        'application/pdf' => 'pdf',
    ];
    $ext = isset($mimeToExt[$file['type']]) ? $mimeToExt[$file['type']] : 'bin';
}

$newName = date('Ymd_His') . '_' . uniqid() . '.' . $ext;
$targetPath = $uploadDir . $newName;

// 移动文件到目标目录
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
    echo "上传成功|" . $newName;
} else {
    http_response_code(500);
    echo "错误:文件保存失败";
}
?>

方案二服务器端:upload_base64.php(接收 Base64 数据)

<?php
/**
 * App Inventor 2 Base64 上传接收脚本
 * 接收 Base64 编码的文件数据
 */

header('Content-Type: text/plain; charset=utf-8');

$uploadDir = 'uploads/';

if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

// 获取 POST 数据
$rawInput = file_get_contents('php://input');

// 方式1:从 application/x-www-form-urlencoded 获取
parse_str($rawInput, $postData);
$base64Data = isset($postData['data']) ? $postData['data'] : '';

if (empty($base64Data)) {
    // 方式2:直接获取原始文本
    $base64Data = $rawInput;
}

// 去除可能的 data URI 前缀
if (preg_match('/^data:[^;]+;base64,(.+)$/s', $base64Data, $matches)) {
    $base64Data = $matches[1];
}

// Base64 解码
$fileData = base64_decode($base64Data, true);

if ($fileData === false) {
    http_response_code(400);
    echo "错误:Base64 解码失败";
    exit;
}

// 通过文件头判断文件类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($fileData);

$mimeToExt = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
    'application/pdf' => 'pdf',
    'text/plain' => 'txt',
];

$ext = isset($mimeToExt[$mime]) ? $mimeToExt[$mime] : 'bin';
$newName = date('Ymd_His') . '_' . uniqid() . '.' . $ext;
$targetPath = $uploadDir . $newName;

// 写入文件
if (file_put_contents($targetPath, $fileData) !== false) {
    echo "上传成功|" . $newName;
} else {
    http_response_code(500);
    echo "错误:文件保存失败";
}
?>

服务器端代码(Node.js)

如果你使用 Node.js 作为服务器端,以下是简单的接收代码示例:

// server.js - Node.js 文件上传服务
const http = require('http');
const formidable = require('formidable');
const fs = require('fs');
const path = require('path');

const UPLOAD_DIR = path.join(__dirname, 'uploads');

if (!fs.existsSync(UPLOAD_DIR)) {
    fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}

const server = http.createServer((req, res) => {
    if (req.method === 'POST' && req.url === '/upload') {
        const form = new formidable.IncomingForm();
        form.uploadDir = UPLOAD_DIR;
        form.keepExtensions = true;

        form.parse(req, (err, fields, files) => {
            if (err) {
                res.writeHead(500, { 'Content-Type': 'text/plain' });
                res.end('错误:上传处理失败');
                return;
            }
            const fileKey = Object.keys(files)[0];
            const file = files[fileKey];
            res.writeHead(200, { 'Content-Type': 'text/plain' });
            res.end('上传成功|' + path.basename(file.filepath));
        });
    } else {
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('Not Found');
    }
});

server.listen(3000, () => {
    console.log('上传服务运行在 http://localhost:3000');
});

上传进度显示

App Inventor 2 的 Web客户端 组件不提供上传进度回调。但你可以通过以下方式改善用户体验:

方式一:显示加载对话框

当 Button_上传.被点击 时:
  调用 Notifier1.显示进度对话框("正在上传", "请稍候...")
  调用 Web1.PostFile(global_CurrentFilePath)

当 Web1.收到返回内容(响应代码, 响应类型, 响应内容) 时:
  调用 Notifier1.关闭进度对话框()
  如果 响应代码 = 200 则:
    调用 Notifier1.显示消息对话框("上传成功!", "提示", "确定")
  否则:
    调用 Notifier1.显示消息对话框("上传失败", "错误", "确定")

当 Web1.发生错误(错误信息) 时:
  调用 Notifier1.关闭进度对话框()
  调用 Notifier1.显示消息对话框("上传出错:" & 错误信息, "错误", "确定")

方式二:使用 Clock 组件模拟进度

当 Button_上传.被点击 时:
  设置 global_Progress 为 0
  设置 Label_Status.文本 为 "上传中 0%"
  设置 Clock1.计时器启用 为 true
  调用 Web1.PostFile(global_CurrentFilePath)

当 Clock1.计时 时:
  如果 global_Progress < 90 则:
    设置 global_Progress 为 global_Progress + 10
    设置 Label_Status.文本 为 "上传中 " & global_Progress & "%"

当 Web1.收到返回内容(响应代码, 响应类型, 响应内容) 时:
  设置 Clock1.计时器启用 为 false
  设置 Label_Status.文本 为 "上传完成 100%"

常见问题与错误处理

1. 网络连接失败

现象:触发 Web1.发生错误 事件,错误信息包含 connecttimeout

解决方案

当 Web1.发生错误(错误信息) 时:
  如果 包含(错误信息, "connect") 或 包含(错误信息, "timeout") 则:
    设置 Label_Status.文本 为 "网络连接失败,请检查网络"
  否则:
    设置 Label_Status.文本 为 "上传出错:" & 错误信息

排查清单

  • 检查手机是否联网
  • 检查服务器 URL 是否正确(注意使用 http://https://
  • 如果是本地测试,确保手机和电脑在同一局域网,且使用电脑的局域网 IP(如 http://192.168.1.100/upload.php
  • 不要使用 localhost127.0.0.1,这指向手机自身

2. 服务器返回 413 错误

现象:响应代码为 413(Request Entity Too Large)。

原因:上传文件超过了服务器的限制。

解决方法:修改 PHP 配置 php.ini

upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 60

修改后重启 Web 服务器。

3. 文件路径问题

现象:PostFile 调用后,服务器未收到文件或收到空文件。

原因:App Inventor 2 中不同来源的文件路径格式不同:

来源 路径示例
Camera 拍照 /storage/emulated/0/PIC_123.jpg
ImagePicker 选择 content://com.android.providers.media.documents/document/image%3A123
App 内部文件 /data/data/pkg.name/files/myfile.txt
ASD 目录 /storage/emulated/0/Android/data/pkg.name/files/myfile.txt

提示:对于 ImagePicker 返回的 content:// 路径,PostFile 方法通常可以直接处理。如果遇到问题,可以先通过 File 组件将文件复制到 ASD 目录再上传。

4. 权限问题

确保 App 拥有必要的权限。在 App Inventor 2 中:

  • Android 11+(API 30+):App 默认使用 ASD(App Specific Directory),无需额外存储权限
  • 读取相册图片:ImagePicker 组件会自动处理权限请求
  • 网络权限:App Inventor 2 编译的 App 默认包含网络权限

5. HTTPS 与 HTTP

从 Android 9(API 28)开始,默认不允许明文 HTTP 请求。

  • 推荐:使用 HTTPS 服务器
  • 测试用:可以在服务器端配置允许 HTTP,或在 App Inventor 2 的 Screen 组件属性中确认目标 API Level

完整示例流程

以下是一个”拍照 + 上传”的完整流程:

步骤一:拍照

当 Button_拍照上传.被点击 时:
  调用 Camera1.执行拍照

步骤二:拍照完成后上传

当 Camera1.拍照完成(图片路径) 时:
  设置 Image1.图片 为 图片路径
  设置 Label_Status.文本 为 "正在上传..."
  设置 Web1.网址 为 "https://你的服务器.com/upload.php"
  调用 Web1.PostFile(图片路径)

步骤三:处理上传结果

当 Web1.收到返回内容(响应代码, 响应类型, 响应内容) 时:
  如果 响应代码 = 200 则:
    设置 Label_Status.文本 为 "✅ 上传成功!"
  否则:
    设置 Label_Status.文本 为 "❌ 上传失败:" & 响应代码

当 Web1.发生错误(错误信息) 时:
  设置 Label_Status.文本 为 "❌ 上传出错:" & 错误信息

安全建议

  1. 服务器端验证文件类型:不要仅依赖客户端传递的文件名,应在服务器端检查文件 MIME 类型和文件内容
  2. 限制文件大小:在服务器端设置合理的文件大小上限
  3. 重命名文件:不要使用客户端原始文件名,使用随机生成的文件名防止路径遍历攻击
  4. 使用 HTTPS:上传过程加密传输,保护数据安全
  5. 目录权限:上传目录不应允许执行脚本(如 PHP),可配置 .htaccess
# uploads/.htaccess - 禁止执行 PHP
<FilesMatch "\.php$">
    Order Deny,Allow
    Deny from all
</FilesMatch>

总结

要点 说明
推荐方案 使用 Web1.PostFile 直接上传,简单高效
服务器端 PHP 使用 $_FILES 接收文件,标准且简单
错误处理 始终在 Web1.发生错误Web1.收到返回内容 中检查结果
进度显示 使用 Notifier 组件显示加载对话框
安全 服务器端验证文件类型、限制大小、重命名文件

掌握文件上传功能后,你可以实现用户头像上传、图片分享、文档提交等多种实用功能。


© App Inventor 2 中文网 ©2025 · 本文内容遵循 CC BY-SA 4.0 协议

文档反馈