文章目录
- 1. 前言
- 2. 读者知识要求
- 3. 源码分析
- 3.1 app.xxx —— 小程序入口
- 3.2 utils —— 工具类
- 3.2.1 Http.js —— 通用网络请求
- 3.2.2 aliyunHttp.js —— 针对阿里云物联网的网络请求
- 3.2.2.1 请求结构
- 3.2.2.2 公共参数
- 3.2.2.3 公共返回参数
- 3.2.2.4 签名机制
- 3.2.2.5 aliyunHttp源码分析
- 3.3 model —— 具体业务请求
- 3.4 pages —— 展示页面
- 4. 总结
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石。。。
共同学习成长QQ群
622368884
,不喜勿加,里面有一大群志同道合的探路人
快速导航
单片机菜鸟的博客快速索引(快速找到你要的)
重点说一下,麻烦三连点赞,你的点赞是博主创作的前进动力
。
1. 前言
在上一篇 ESP8266开发之旅 小程序之阿里云篇① “IOT菜鸟”小程序,小白简单配置就可以玩起来中,博主教会大家如何使用该小程序,那么接下来我们就来分析一下源码,力求让大家能了解其中核心代码。
2. 读者知识要求
- 至少了解过
HTML
、CSS
、JS
- 至少了解过 微信小程序
官方开发文档
- 至少要去了解
阿里云物联网开发文档
这是我们小程序业务请求的重点,也是本篇重中之重
3. 源码分析
以下就是IOT菜鸟小程序的源码,麻雀虽小五脏俱全,请读者认真学习,博主会跳过某些内容(比如教你如何创建小程序、如何创建一个page页面),重点讲解我们需要关注的知识点。
-
components 组件
公共显示组件 -
images
显示图标 -
model
具体业务数据模块(重点讲解内容,也是读者后面自定义比较多的地方) -
pages
具体显示页面(会挑控制页面讲解) -
utils
工具类方法(重点讲解内容) -
app.xxx
小程序入口
3.1 app.xxx —— 小程序入口
app.xxx 是整个小程序的入口,我们可以在这里设置一些整个小程序会使用的数据或者初始化内容。
包含文件:
-
app.js
—— 公共JS逻辑,比如一些全局变量
这里定义了我们小程序整个环境需要用到的阿里云物联网配置参数,并且配置参数从存储中读取,如果没有我们就填一些默认值
,开发者也可以填写自己的默认值,这样第一次进入小程序的时候就不需要配置了
。 -
app.json
—— 整个小程序的配置
开发者可以修改成自己想要的名字
。 -
app.wxss
—— 整个小程序的一些css样式
开发者可以把一些公用的css抽取到这里,就不用每个页面都写一遍。
3.2 utils —— 工具类
重点关注:
cryptojs
加密签名相关http.js
通用网络请求相关aliyunHttp.js
阿里云物联网网络请求相关,依赖http.jsstorage.js
存储配置内容timeFormat.js
时间格式化,阿里云物联网对时间格式有要求
这里通过讲解http.js 和 aliyunHttp.js来顺带讲解其他工具类。
3.2.1 Http.js —— 通用网络请求
源码分析:
function getHeader(currentHeader) {
const header = currentHeader || {};
header['content-type'] = header['content-type'] || 'application/json';
return header;
}
function checkNetwork() {
return new Promise((resolve, reject) => {
wx.getNetworkType({
success: function(res) {
// 返回网络类型, 有效值:
// wifi/2g/3g/4g/unknown(Android下不常见的网络类型)/none(无网络)
const networkType = res.networkType;
if (networkType === 'none') {
wx.showModal({
title: '当前网络不可用,请检查网络设置',
confirmText: '重试',
success: function(res) {
if (res.confirm) {
checkNetwork();
} else {
reject(new Error('NetWorkError'));
}
}
});
} else {
resolve();
}
}
});
});
}
export default {
request(url, data, method, headers, complete) {
return new Promise((resolve, reject) => {
checkNetwork().then(() => {
wx.request({
url: url,
data: data,
method: method || 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETe, TRACE, CONNECT
header: getHeader(headers), // 设置请求的 header
success: (res) => {
// HTTP响应code
if (res.statusCode === 200) {
// 需要处理一些公关逻辑,比如公共错误业务code等等
resolve(res.data);
} else {
reject(res);
}
},
fail: (err) => {
reject(err);
},
complete
});
}).catch(err => {
console.log(err);
});
});
},
get(opts = {}) {
return this.request(opts.url, opts.data, 'GET', opts.headers);
},
post(opts = {}) {
return this.request(opts.url, opts.data, 'POST', opts.headers);
},
};
-
我们定义了两个请求
GET
和POST
(底层都是调用request)以及一个自定义的 request 方法,读者可以自定义其他的Method
(HEAD、PUT、DELETE等) -
request是一个
Promise
,先去判断网络情况(没有连接网络,没有就弹出一个model提示用户),之后就是发起网络请求wx.request
-
wx.request中我们会设置一下
Header
(getHeaders)以及处理 HTTP响应Code,等于200才会返回,其他Code抛出异常。
非常简单,都是通用的HTTP协议
3.2.2 aliyunHttp.js —— 针对阿里云物联网的网络请求
- 读者必须先去了解
阿里云物联网云端开发指南
- 重点关注公共逻辑部分
3.2.2.1 请求结构
下面以调用Pub接口向指定Topic发布消息为例:
https://iot.cn-shanghai.aliyuncs.com/?Action=Pub
&Format=XML
&Version=2017-04-20
&Signature=Pc5WB8gokVn0xfeu%2FZV%2BiNM1dgI%3D
&SignatureMethod=HMAC-SHA1
&SignatureNonce=15215528852396
&SignatureVersion=1.0
&AccessKeyId=...
&Timestamp=2017-07-19T12:00:00Z
&RegionId=cn-shanghai
...
- 一个网络请求肯定有
URL
,阿里云物联网这个URL需要根据自己的账号去填写,IOT菜鸟提供了配置页面选择RegionID
- 这是一个GET请求,涉及了一堆参数,包括
公共参数
以及业务参数
3.2.2.2 公共参数
示例:
https://iot.cn-shanghai.aliyuncs.com/
?Format=XML
&Version=2018-01-20
&Signature=Pc5WB8gokVn0xfeu%2FZV%2BiNM1dgI%3D
&SignatureMethod=HMAC-SHA1
&SignatureNonce=15215528852396
&SignatureVersion=1.0
&AccessKeyId=...
&Timestamp=2018-05-20T12:00:00Z
&RegionId=cn-shanghai
3.2.2.3 公共返回参数
API返回结果采用统一格式,返回2xx HTTP状态码代表调用成功;返回4xx或5xx HTTP状态码代表调用失败。调用成功返回的数据格式有XML和JSON两种。可以在发送请求时,指定返回的数据格式。默认为XML格式。
每次接口调用,无论成功与否,系统都会返回一个唯一识别码RequestId
。
我们这里采用JSON。
成功
示例:
{
"RequestId": "4C467B38-3910-447D-87BC-AC049166F216"
}
失败
示例:
{
"RequestId": "8906582E-6722-409A-A6C4-0E7863B733A5",
"Code": "UnsupportedOperation",
"Message": "The specified action is not supported."
}
在失败的时候,最好弹一个toast提示用户。我们可以获取Message内容。
在aliYunHttp里面可以看到这个代码:
function handleResponse(res, resolve, reject) {
let { Success } = res;
if (Success) {
// api调用成功 返回整个数据
resolve && resolve(res);
} else {
// api调用失败
let { RequestId, Code, ErrorMessage} = res;
wx.showToast({ title: `${Code}:${ErrorMessage}`, icon: 'none' });
reject && reject({
RequestId,
Code,
ErrorMessage
});
}
}
3.2.2.4 签名机制
物联网平台会对每个接口访问请求的发送者进行身份验证,所以无论使用HTTP还是HTTPS协议提交请求,都需要在请求中包含签名(Signature
)信息。
内容太多,博主以图示来说明签名的重要内容:
-
构造规范化的请求字符串(
Canonicalized Query String
)
-
构造
签名字符串
-
计算
HMAC
值
-
计算
签名值
-
添加
签名
3.2.2.5 aliyunHttp源码分析
// 通用网络请求
import http from './http.js';
import timeFormat from './timeFormat.js';
// 加密模块
let crypto = require("./cryptojs/cryptojs.js").Crypto;
const app = getApp();
// 配置公共参数
const _defaultParams = () => {
// 阿里云要求的公共请求参数 https://help.aliyun.com/document_detail/30561.html?spm=a2c4g.11186623.6.739.6bc03d291aEGp1
let commonParams = {
Format: 'JSON', // 返回值的类型,支持JSON和XML类型
Version: '2018-01-20', // API版本号
AccessKeyId: app.aliConfig.AccessKeyId, // 阿里云颁发给用户的访问服务所用的密钥ID
// Signature: '', // 签名结果串 需要另外计算 为了方便 不放在公共参数
SignatureMethod: 'HMAC-SHA1', // 签名方式,目前支持HMAC-SHA1
Timestamp: timeFormat.getCurrentUTCTime('{YYYY}-{MM}-{DD}T{HH}:{mm}:{ss}Z'), // 请求的时间戳,日期格式按照ISO8601标准表示,并需要使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。2016-01-04T12:00:00Z
SignatureVersion: '1.0', // 签名算法版本
SignatureNonce: new Date().getTime() + '', // 唯一随机数,用于防止网络重放攻击。用户在不同请求中要使用不同的随机数值
RegionId: app.aliConfig.RegionId, // 设备所在地域(与控制台上的地域对应),如cn-shanghai。
};
return commonParams;
};
// 将数组参数格式化成url传参方式
const _flatArrayList = (target, key, Array) => {
for (let i = 0; i < Array.length; i++) {
let item = Array[i];
if (item && typeof item === 'object') {
const keys = Object.keys(item);
for (let j = 0; j < keys.length; j++) {
target[`${key}.${i + 1}.${keys[j]}`] = item[keys[j]];
}
} else {
target[`${key}.${i + 1}`] = item;
}
}
};
//将所有请求参数展开平面化,考虑到有些接口给到的参数是数组
const _flatParams = (params) => {
let target = {};
let keys = Object.keys(params);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = params[key];
if (Array.isArray(value)) {
_flatArrayList(target, key, value);
} else {
target[key] = value;
}
}
return target;
};
// url编码
const _percentEncode= (str) => {
let result = encodeURIComponent(str);
return result.replace(/\!/g, '%21')
.replace(/\'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A');
};
const _getCanonicalizedQueryString = (params) => {
let list = [];
let flatParams = _flatParams(params);
Object.keys(flatParams).sort().forEach((key) => {
let value = flatParams[key];
list.push([_percentEncode(key), _percentEncode(value)]);
});
let queryList = [];
for (let i = 0; i < list.length; i++) {
let [key, value] = list[i];
queryList.push(key + '=' + value);
}
return queryList.join('&');
};
const _signature = (stringToSign, key) => {
let signature = crypto.HMAC(crypto.SHA1, stringToSign, key, {
asBase64: true
});
return signature;
};
function handleResponse(res, resolve, reject) {
let { Success } = res;
if (Success) {
// api调用成功 返回整个数据
resolve && resolve(res);
} else {
// api调用失败
let { RequestId, Code, ErrorMessage} = res;
wx.showToast({ title: `${Code}:${ErrorMessage}`, icon: 'none' });
reject && reject({
RequestId,
Code,
ErrorMessage
});
}
}
let aliyunApi = {
get(opts = {}) {
opts.url = opts.url || app.aliConfig.EndPoint;
// 获取公共参数
let defaultParams = _defaultParams();
// 合并参数
opts.data = Object.assign(defaultParams,opts.data);
let canonicalizedQueryString = _getCanonicalizedQueryString(opts.data);
let stringToSign = `GET&${_percentEncode('/')}&${_percentEncode(canonicalizedQueryString)}`;
//console.log(stringToSign);
let signature = _signature(stringToSign, app.aliConfig.AccessKeySecret + '&');
// 补上 Signature参数
opts.data = {
...opts.data,
Signature: signature
};
return new Promise((resolve, reject) => {
http.get(opts).then((res) => {
handleResponse(res, resolve, reject);
}).catch(err => {
console.log(err);
})
});
},
post(opts = {}) {
opts.url = opts.url || app.aliConfig.EndPoint;
// 获取公共参数
let defaultParams = _defaultParams();
// 合并参数
opts.data = Object.assign(defaultParams,opts.data);
let canonicalizedQueryString = _getCanonicalizedQueryString(opts.data);
let stringToSign = `POST&${_percentEncode('/')}&${_percentEncode(canonicalizedQueryString)}`;
let signature = _signature(stringToSign, app.aliConfig.AccessKeySecret + '&');
// 补上 Signature参数
opts.data = {
...opts.data,
Signature: signature
};
opts.headers = opts.headers || {};
opts.headers['content-type'] = 'application/x-www-form-urlencoded';
return new Promise((resolve, reject) => {
http.post(opts).then((res) => {
handleResponse(res, resolve, reject);
}).catch(err => {
console.log(err);
})
});
}
};
export { aliyunApi };
注意: 读者需要从 get/post 方法开始阅读,博主在里面注释都非常清晰
公共参数
- 构造规范化的请求字符串 —— _getCanonicalizedQueryString
- 构造
签名字符串
—— stringToSign
let stringToSign = `POST&${_percentEncode('/')}&${_percentEncode(canonicalizedQueryString)}`;
- 生成签名值
到这里,阿里云API底层源码讲解完毕,接下来会讲解具体业务。
3.3 model —— 具体业务请求
具体业务相关内容,博主放在了model目录,当前小程序用到了设备管理。
关于设备管理,请读者自行查阅
对应代码如下:
import { aliyunApi } from '../../utils/aliyunHttp.js';
const app = getApp();
let deviceApi = {
registerDevice(param = {}) {
let opts = {
data: {
...param,
Action: 'RegisterDevice',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
queryDeviceDetail(param = {}) {
let opts = {
data: {
...param,
Action: 'QueryDeviceDetail',
}
};
return aliyunApi.get(opts);
},
batchQueryDeviceDetail(param = {}) {
let opts = {
data: {
...param,
Action: 'BatchQueryDeviceDetail',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
queryDevice(param = {}) {
let opts = {
data: {
...param,
Action: 'QueryDevice',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
deleteDevice(param = {}) {
let opts = {
data: {
...param,
Action: 'DeleteDevice',
}
};
return aliyunApi.get(opts);
},
getDeviceStatus(param = {}) {
let opts = {
data: {
...param,
Action: 'GetDeviceStatus',
}
};
return aliyunApi.get(opts);
},
batchGetDeviceState(param = {}) {
let opts = {
data: {
...param,
Action: 'BatchGetDeviceState',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
disableThing(param = {}) {
let opts = {
data: {
...param,
Action: 'DisableThing',
}
};
return aliyunApi.get(opts);
},
enableThing(param = {}) {
let opts = {
data: {
...param,
Action: 'EnableThing',
}
};
return aliyunApi.get(opts);
},
queryDevicePropertyData(param = {}) {
let opts = {
data: {
...param,
Asc: 0,
EndTime: new Date().getTime() + '',
StartTime: '',// 要转成上线那天的时间
Action: 'QueryDevicePropertyData',
}
};
return aliyunApi.get(opts);
},
updateDeviceNickname(param = {}) {
let opts = {
data: {
...param,
Action: 'BatchUpdateDeviceNickname',
}
};
return aliyunApi.get(opts);
},
setDeviceProperty(param = {}) {
let opts = {
data: {
...param,
Action: 'SetDeviceProperty',
}
};
return aliyunApi.get(opts);
},
};
export { deviceApi };
每个接口都有对应的方法,并且携带上自己的自定义参数,非常简单。
3.4 pages —— 展示页面
展示页面均放在pages下面。目前用到了index和config
我们这里以分析index页面为例。请读者自行看代码注释
//index.js
//获取应用实例
import {deviceApi} from "../../model/aliyun/device";
import {storage} from "../../utils/storage";
const app = getApp();
Page({
_data: {
PageSize: 50,
CurrentPage: 1,
},
data: {
deviceOnLine: [],//在线设备
deviceOffLine: [],//离线设备
deviceUnative: [],//未激活设备
deviceDisable: [],//已禁用设备
showConfig: false,
},
onLoad: function () {
},
onShow() {
if (!app.aliConfig.RegionId || !app.aliConfig.AccessKeyId
|| !app.aliConfig.ProductKey
|| !app.aliConfig.EndPoint){
this.setData({
showConfig: true
});
} else {
this.setData({
showConfig: false
});
wx.showLoading({
title: '努力加载中...',
mask: true
});
this.loadDeviceList();
}
},
onPullDownRefresh() {
this.loadDeviceList();
},
loadDeviceList() {
// 先清空数据
this.setData({
deviceOnLine: [],
deviceOffLine: [],
deviceUnative: [],
deviceDisable: [],
}, () => {
this._data.CurrentPage = 1; //从第一页开始
this.queryDevice(this._data.PageSize, this._data.CurrentPage);
});
},
// 查询设备
queryDevice(pageSize = 50, currentPage = 1) {
// 调用model device的api
deviceApi.queryDevice({
PageSize: pageSize,
CurrentPage: currentPage,
}).then((res) => {
if (res.Data && res.Data.DeviceInfo) {
this.handleDeviceList(res.Data.DeviceInfo);
// 判断是否需要继续请求
if (res.PageCount > this._data.CurrentPage) {
this._data.CurrentPage ++;
this.queryDevice(this._data.PageSize, this._data.CurrentPage);
} else {
this.finishQueryDevice();
}
} else {
this.finishQueryDevice();
}
}).catch(err => {
console.log(err);
this.finishQueryDevice();
});
},
finishQueryDevice(){
wx.stopPullDownRefresh();
wx.hideLoading && wx.hideLoading();
},
// 处理列表返回内容
handleDeviceList(deviceList) {
let deviceOnLine = [];
let deviceOffLine = [];
let deviceUnative = [];
let deviceDisable = [];
// 如果你是试用过了小程序 是不是觉得led1 led2非常熟悉呢?这里就是原因了
if(deviceList && Array.isArray(deviceList)) {
deviceList.forEach((device) => {
let type;
if (device.Nickname.indexOf('_led2') > -1) {
type = '_led2';
} else if (device.Nickname.indexOf('_led3') > -1) {
type = '_led3';
} else if (device.Nickname.indexOf('_lamp1') > -1) {
type = '_lamp1';
} else if (device.Nickname.indexOf('_lamp2') > -1) {
type = '_lamp2';
} else {
type = '_led1';
}
this.setDevice(device,type);
if (device.DeviceStatus === 'ONLINE') {
deviceOnLine.push(device);
} else if (device.DeviceStatus === 'OFFLINE') {
deviceOffLine.push(device);
} else if (device.DeviceStatus === 'UNACTIVE') {
deviceUnative.push(device);
} else if (device.DeviceStatus === 'DISABLE') {
deviceDisable.push(device);
}
});
}
this.setData({
deviceOnLine: this.data.deviceOnLine.concat(deviceOnLine),
deviceOffLine: this.data.deviceOffLine.concat(deviceOffLine),
deviceUnative: this.data.deviceUnative.concat(deviceUnative),
deviceDisable: this.data.deviceDisable.concat(deviceDisable),
});
},
setDevice(device, type) {
device.Nickname = device.Nickname.replace(type, '');
if (device.DeviceStatus === 'ONLINE' || device.DeviceStatus === 'OFFLINE') {
device.Image = `/images/icon${type}_on.png`;
} else {
device.Image = `/images/icon${type}_off.png`;
}
},
// 跳转配置页面
goToConfig() {
console.log('goToConfig');
wx.navigateTo({
url: '/pages/config/config',
});
}
});
4. 总结
本篇主要是简单讲解IOT菜鸟小程序的源码,包括:
- http
- aliyunhttp
- page/index
整体难度不高,也实现了博主的初衷,为IOT事业做贡献。
喜欢的同学,请不要跑了,给博主点个赞,你的点赞是博主前进的动力。