前言
之前写了一篇微信小程序使用MQTT over WebSocket连接阿里云IoT物联网平台,介绍了如何使用mqtt.js在微信小程序上连接mqtt服务器,文中顺带提了mqtt.js是支持支付宝小程序的,但是我本人没有实际编写过,后来有小伙伴来问我相关的问题,正好有空,于是稍微研究了一下,踩了不少坑,最后连接上了,以此记录,希望能给后来人一点帮助。
坑点
- 支付宝小程序和开发工具环境目前差距挺大,有时候开发工具能跑到真机就GG
- 支付宝mqtt.js连接参数要传入一个my(mqtt.js文档里没写这点,看了源码才懂)
- 由于支付宝小程序底层变动,以及最新mqtt.js(v4.2.0)版本的改动,导致mqtt.js当前版本v4.2.0未支持支付宝小程序(有人提交了PR,但是没有后续,没合并)
综上所述,如果当前你想在支付宝小程序上使用mqtt.js,只能等合并pr发布的新版本,或者自己下载源码修改编译(注意:开发工具要最新版本,否则会报错FileReader is not constructor(当前v1.13.7))
修改源码步骤:
- 首先git clone https://github.com/mqttjs/MQTT.js.git 把源码下到本地
- 修改入口的判断,及PR#1135改动的部分
对应文件 :lib/connect/index.js
if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ === 'function') {
protocols.mqtt = require('./tcp') protocols.mqtt = require('./tcp')
protocols.tcp = require('./tcp') protocols.tcp = require('./tcp')
protocols.ssl = require('./tls') protocols.ssl = require('./tls')
改成(即把 typeof webpack_require === ‘function’ 条件去掉)
if ((typeof process !== 'undefined' && process.title !== 'browser') ) {
protocols.mqtt = require('./tcp') protocols.mqtt = require('./tcp')
protocols.tcp = require('./tcp') protocols.tcp = require('./tcp')
protocols.ssl = require('./tls') protocols.ssl = require('./tls')
对应文件:lib/connect/ws.js
// eslint-disable-next-line camelcase
var IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function'
改成(同样去掉 typeof webpack_require === ‘function’ 条件),这个地方其实改不改都可以
// eslint-disable-next-line camelcase
var IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser')
- 修改支持支付宝小程序协议部分(对照PR修改即可)
对应文件:lib/connect/ali.js 修改完成如下
'use strict'
var Transform = require('readable-stream').Transform
var duplexify = require('duplexify')
var base64 = require('base64-js')
/* global FileReader */
var my
var proxy
var stream
var isInitialized = false
function buildProxy () {
var proxy = new Transform()
proxy._write = function (chunk, encoding, next) {
const _data = chunk.toString('base64'); //订正mqttjs支付宝小程序使用错误,支付宝data需要传入base64 string
my.sendSocketMessage({
data: _data,
isBuffer: 1,
success: function () {
next()
},
fail: function () {
next(new Error())
}
})
}
proxy._flush = function socketEnd (done) {
my.closeSocket({
success: function () {
done()
}
})
}
return proxy
}
function setDefaultOpts (opts) {
if (!opts.hostname) {
opts.hostname = 'localhost'
}
if (!opts.path) {
opts.path = '/'
}
if (!opts.wsOptions) {
opts.wsOptions = {}
}
}
function buildUrl (opts, client) {
var protocol = opts.protocol === 'alis' ? 'wss' : 'ws'
var url = protocol + '://' + opts.hostname + opts.path
if (opts.port && opts.port !== 80 && opts.port !== 443) {
url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path
}
if (typeof (opts.transformWsUrl) === 'function') {
url = opts.transformWsUrl(url, opts, client)
}
return url
}
function bindEventHandler () {
if (isInitialized) return
isInitialized = true
my.onSocketOpen(function () {
stream.setReadable(proxy)
stream.setWritable(proxy)
stream.emit('connect')
})
my.onSocketMessage(function (res) {
if (typeof res.data === 'string') {
var array = base64.toByteArray(res.data)
var buffer = Buffer.from(array)
proxy.push(buffer)
} else {
var reader = new FileReader()
reader.addEventListener('load', function () {
var data = reader.result
if (data instanceof ArrayBuffer) data = Buffer.from(data)
else data = Buffer.from(data, 'utf8')
proxy.push(data)
})
reader.readAsArrayBuffer(res.data)
}
})
my.onSocketClose(function () {
stream.end()
stream.destroy()
})
my.onSocketError(function (res) {
stream.destroy(res)
})
}
function buildStream (client, opts) {
opts.hostname = opts.hostname || opts.host
if (!opts.hostname) {
throw new Error('Could not determine host. Specify host manually.')
}
var websocketSubProtocol =
(opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3)
? 'mqttv3.1'
: 'mqtt'
setDefaultOpts(opts)
var url = buildUrl(opts, client)
my = opts.my
my.connectSocket({
url: url,
headers : {
"Sec-WebSocket-Protocol" : "mqtt"
}
})
proxy = buildProxy()
stream = duplexify.obj()
bindEventHandler()
return stream
}
module.exports = buildStream
修改完成之后,执行npm install等待编译完成即可在dist目录下看到两个文件:mqtt.js 和mqtt.min.js,前一个是未压缩版本,有日志输出,可以用来调试,后一个是压缩版本,线上环境使用
当然你要是嫌麻烦,可以用我已经编译好的mqtt.js
准备工作(这一步基本就和微信小程序没啥大的区别了)
- 自己编译或者下载我上面已经编译好的mqtt.js包
- 去官方库aliyun-iot-client-sdk下载hmac-sha1算法库hex_hmac_sha1.js(当然也可以使用其他的库,比如crypto-js),点击打开链接然后右键另存为即可
- 下载支付宝小程序开发工具,新建任意项目
- 拷贝mqtt.min.js和hex_hmac_sha1.js到utils目录中去
- 可能支付宝还有其他配置,具体自己看文档了
开始编码
随便在一个页面的js文件中加入以下代码,注意替换参数为自己产品和设备的参数
const crypto = require('../../utils/hex_hmac_sha1.js'); //根据自己存放的路径修改
import {connect} from '../../utils/mqtt.min.js' //根据自己存放的路径修改
// 获取全局 app 实例
const app = getApp();
// 数据管理器
let conn = null;
Page({
// 声明页面数据
data: {
dataLoaded: false,
tasks: [],
taskHandlers: [],
taskCheckers: [],
info:'看看',
message:''
},
// 监听生命周期回调 onLoad
onLoad() {
},
// 监听生命周期回调 onShow
onShow() {
// 同步全局数据到本地
//this.loadData();
},
onHide() {
// TODO: 清理注册事件
},
onTap(){
this.doConnect();
},
doConnect(){
const deviceConfig = {
productKey: "替换",
deviceName: "替换",
deviceSecret: "替换",
regionId: "替换"
};
const options = this.initMqttOptions(deviceConfig);
console.log(options)
//替换productKey为你自己的产品的(注意这里是wxs,不是wss,否则你可能会碰到ws不是构造函数的错误)
const client = connect(`alis://${deviceConfig.productKey}.iot-as-mqtt.${deviceConfig.regionId}.aliyuncs.com`,options)
this.setData({
info:"开始连接.."
})
client.on('connect', ()=> {
console.log('连接服务器成功')
this.setData({
info:"连接服务器成功"
})
//订阅主题,替换productKey和deviceName(这里的主题可能会不一样,具体请查看后台设备Topic列表或使用自定义主题)
client.subscribe(`/${deviceConfig.productKey}/${deviceConfig.deviceName}/get`, function (err) {
if (!err) {
console.log('订阅成功!');
}
})
})
//接收消息监听
client.on('message', (topic, message) =>{
// message is Buffer
console.log('收到消息:'+message.toString())
this.setData({
message:message.toString()
})
//关闭连接 client.end()
})
},
//IoT平台mqtt连接参数初始化
initMqttOptions(deviceConfig) {
const params = {
productKey: deviceConfig.productKey,
deviceName: deviceConfig.deviceName,
timestamp: Date.now(),
clientId: Math.random().toString(36).substr(2),
}
//CONNECT参数
const options = {
keepalive: 60, //60s
clean: true, //cleanSession不保持持久会话
protocolVersion: 4 ,//MQTT v3.1.1
my:my //注意这里的my
}
//1.生成clientId,username,password
options.password = this.signHmacSha1(params, deviceConfig.deviceSecret);
options.clientId = `${params.clientId}|securemode=2,signmethod=hmacsha1,timestamp=${params.timestamp}|`;
options.username = `${params.deviceName}&${params.productKey}`;
return options;
},
/*
生成基于HmacSha1的password
参考文档:https://help.aliyun.com/document_detail/73742.html?#h2-url-1
*/
signHmacSha1(params, deviceSecret) {
let keys = Object.keys(params).sort();
// 按字典序排序
keys = keys.sort();
const list = [];
keys.map((key) => {
list.push(`${key}${params[key]}`);
});
const contentStr = list.join('');
return crypto.hex_hmac_sha1(deviceSecret, contentStr);
}
});
运行代码,点击连接即可看到
至此支付宝小程序使用mqtt.js连接服务器已完成,更多信息可以参考
微信小程序使用MQTT over WebSocket连接阿里云IoT物联网平台
有疑问可以加我QQ:343672271 (备注mqtt)
2020年9月15日 补充:
mqttv4.2.0在模拟器和android机上中文显示正常,但是在苹果机上会乱码,需要自行修改源码ali.js接收数据解码部分,即改成:
my.onSocketMessage(function (res) {
if(res.isBuffer){
let buff=new Buffer.from(res.data,'base64');
proxy.push(buff);
}else if (typeof res.data === 'string') {
var array = base64.toByteArray(res.data)
var buffer = Buffer.from(array)
proxy.push(buffer)
} else {
var reader = new FileReader()
reader.addEventListener('load', function () {
var data = reader.result
if (data instanceof ArrayBuffer) data = Buffer.from(data)
else data = Buffer.from(data, 'utf8')
proxy.push(data)
})
reader.readAsArrayBuffer(res.data)
}
})
增加了其他的解码方式:
if(res.isBuffer){
let buff=new Buffer.from(res.data,'base64');
proxy.push(buff);
}
本解决方案由小伙伴无疆(QQ昵称)提供,本人作为搬运工,编译好了一个版本mqtt.js,不想自己编译的小伙伴自行下载。