Uniapp APP 端实现 TCP Socket 通信(ZPL 打印实战)

Uniapp APP 端实现 TCP Socket 通信(ZPL 打印实战)

前言

在混合开发中,Uniapp 默认不支持原生 TCP Socket 通信,但在 IoT、打印等场景下(如 Zebra 打印机 ZPL 指令),直接操作 Socket 是刚需。本文基于 APP-PLUS 环境,通过 plus.android 调用 Java 原生 API 实现 Socket 通信,并封装为可复用的工具类。

一、核心实现:SocketManager 工具类

1. 基础功能封装

  • 将之前的代码封装成一个 可复用的模块 (如 socket.js),并在页面中调用。
socket.js(封装 TCP Socket 逻辑)
js 复制代码
// #ifdef APP-PLUS
const SocketManager = {
	socket: null,
	outputStream: null,
	isConnected: false,

	/**
	 * 异步连接 Socket(返回 Promise)
	 * @param {string} ip 服务器 IP
	 * @param {number} port 服务器端口
	 * @param {number} [timeout=5000] 超时时间(毫秒)
	 */
	connect(ip, port, timeout = 5000) {
		return new Promise((resolve, reject) => {
			// 使用 setTimeout 模拟子线程执行(避免 AsyncTask 兼容性问题)
			setTimeout(() => {
				try {
					// 1. 导入 Java 类
					const Socket = plus.android.importClass("java.net.Socket");
					const InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress");
					const DataOutputStream = plus.android.importClass("java.io.DataOutputStream");

					// 2. 创建 Socket 并连接
					this.socket = new Socket();
					this.socket.connect(new InetSocketAddress(ip, port), timeout);

					// 3. 初始化 OutputStream
					this.outputStream = new DataOutputStream(this.socket.getOutputStream());
					this.isConnected = true;
					this.socket.setKeepAlive(true);

					console.log("Socket 连接成功", ip + ":" + port);
					resolve(); // 直接 resolve(已在主线程)
				} catch (e) {
					console.error("Socket 连接失败:", e);
					this.close(); // 失败时清理资源
					reject(e); // 直接 reject(已在主线程)
				}
			}, 0);
		});
	},

	/**
	 * 发送字节数组(Uint8Array)
	 * @param {Uint8Array} data 字节数组
	 */
	send(data) {
		if (!this.isConnected || !this.outputStream) {
			return Promise.reject(new Error("Socket 未连接"));
		}
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				try {
					// 1. 转换 Uint8Array 为 Java byte[]
					const JavaArray = plus.android.importClass("java.lang.reflect.Array");
					const Byte = plus.android.importClass("java.lang.Byte");
					const javaBytes = JavaArray.newInstance(Byte.TYPE, data.length);
					for (let i = 0; i < data.length; i++) {
                                        //JavaScript 的 `TypedArray`(如 `Uint8Array`)范围是 `0~255`,而 Java 的 `byte` 范围是 `-128~127`。直接传递会导致数据截断。
						JavaArray.setByte(javaBytes, i, data[i] > 127 ? data[i] - 256 : data[i]);
					}

					// 2. 写入数据并刷新
					this.outputStream.write(javaBytes);
					this.outputStream.flush();
					resolve();
				} catch (e) {
					console.error("发送数据失败:", e);
					this.close();
					reject(e);
				}
			}, 0);
		});
	},

	/**
	 * 发送字符串(UTF-8)
	 * @param {string} str 字符串
	 */
	sendString(str) {
		if (!this.isConnected || !this.outputStream) {
			return Promise.reject(new Error("Socket 未连接"));
		}
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				try {
					// 1. 转换字符串为 Java byte[](UTF-8)
					const JavaString = plus.android.importClass("java.lang.String");
					const javaStr = new JavaString(str);
					const bytes = javaStr.getBytes("UTF-8");

					// 2. 写入数据并刷新
					this.outputStream.write(bytes);
					this.outputStream.flush();
					resolve();
				} catch (e) {
					console.error("发送字符串失败:", e);
					this.close();
					reject(e);
				}
			}, 0);
		});
	},

	/**
	 * 关闭连接
	 */
	close() {
		// 关闭操作可以在主线程执行
		try {
			this.isConnected = false;
			if (this.outputStream) {
				this.outputStream.close();
				this.outputStream = null;
			}
			if (this.socket) {
				this.socket.close();
				this.socket = null;
			}
			console.log("Socket 已断开");
		} catch (e) {
			console.error("关闭 Socket 时出错:", e);
		}
	},

	/**
	 * 检查连接状态
	 */
	checkConnection() {
		try {
			return this.isConnected &&
				this.socket &&
				!this.socket.isClosed() &&
				this.socket.isConnected();
		} catch (e) {
			return false;
		}
	}
};

export default SocketManager;
// #endif

2. 关键设计说明

  1. setTimeout 的作用

    • 将 Socket 操作放到事件队列下一轮执行,避免阻塞 UI 线程(Android 原生方法调用是同步的)。
    • 替代方案:可使用 plus.android.invoke 的异步 API(需验证兼容性)。
  2. 数据类型转换

    • Uint8Array 转 Java byte[] 时需处理符号位(data[i] > 127 ? data[i] - 256 : data[i])。
    • 字符串发送使用 String.getBytes("UTF-8") 确保编码正确。
  3. 资源管理

    • 每次操作后检查 isConnected 状态,避免崩溃。
    • close() 方法中置空对象引用,防止内存泄漏

二、实战:ZPL 打印指令发送

1. 打印方法封装

js 复制代码
                   //引入(放入的文件的位置)
                  import SocketManager from "./socket.js"
                        

			async printMaterial(item, ip = "192.168.50.192", port = 9100) {
				if (!item?.itemnum) throw new Error("物资信息不完整");

				const zplTemplate = `
			    ^XA
			    ^FO50,50^A0N,50,50^FD物资编码:${item.itemnum}^FS
			    ^FO50,120^A0N,50,50^FD名称:${item.itemDesc.substring(0, 30)}^FS
			    ^XZ
			  `;

				try {
					// #ifdef APP-PLUS
					await SocketManager.connect(ip, port);
					await SocketManager.sendString(zplTemplate);
					uni.showToast({
						title: "打印成功"
					});
					// #endif
				} catch (error) {
					uni.showToast({
						title: `打印失败: ${error.message}`,
						icon: "error"
					});
					throw error;
				} finally {
					// #ifdef APP-PLUS
					SocketManager.close(); // 确保连接关闭
					// #endif
				}
			}

2. 避坑指南

  1. 连接超时处理

    • Socket建立连接是异步的,默认设置 timeout=5000,避免网络问题导致无响应。
    • 错误捕获后自动关闭连接,防止僵尸连接。
  2. ZPL 指令格式

    • 使用 ^XA^XZ 包裹指令,^FD 插入动态数据。
    • 字段截断(如 substring(0, 30))防止标签溢出。
  3. 平台兼容性

    • 通过 #ifdef APP-PLUS 确保代码仅在 App 端运行。
    • 非 App 端需降级处理(如调用 Web API 或提示用户)。
  4. Android 权限

    • manifest.json 中声明网络权限:。
js 复制代码
    "app-plus": { 
        "permissions": ["android.permission.INTERNET"] ,
        /* 模块配置 */
        "modules" : {
            "Socket" : {} // 显式声明使用 Socket 模块
        },
         // ...其他配置
    }

三、性能优化与扩展建议

  1. 连接复用

    • 当前实现每次打印都新建连接,高频场景可改为长连接(需处理断线重连)。
  2. 数据接收

    • 如需读取打印机响应,需通过 socket.getInputStream() 扩展 SocketManager
  3. 跨平台方案


总结

本文通过封装 SocketManager 实现了 Uniapp 在 App 端的 TCP 通信,重点解决了以下问题:

  1. 原生 Socket 的异步调用封装。
  2. 数据类型转换与资源管理。
  3. 结合 ZPL 打印的实际业务场景。
相关推荐
烛阴8 分钟前
掌握 TypeScript 的边界:any, unknown, void, never 的正确用法与陷阱
前端·javascript·typescript
Jerry40 分钟前
迁移到 Jetpack Compose
前端
FFF-X1 小时前
前端无感刷新 Token 的 Axios 封装方案
前端
qq_589568101 小时前
javaweb开发笔记—— 前端工程化
java·前端
gnip1 小时前
包管理工具的发展
前端
前端工作日常2 小时前
H5 实时摄像头 + 麦克风:完整可运行 Demo 与深度拆解
前端·javascript
韩沛晓2 小时前
uniapp跨域怎么解决
前端·javascript·uni-app
前端工作日常2 小时前
以 Vue 项目为例串联eslint整个流程
前端·eslint
程序员鱼皮2 小时前
太香了!我连夜给项目加上了这套 Java 监控系统
java·前端·程序员
该用户已不存在3 小时前
这几款Rust工具,开发体验直线上升
前端·后端·rust