文章目錄
授人以魚不如授人以漁,目的不是爲了教會你具體項目開發,而是學會學習的能力。希望大家分享給你周邊需要的朋友或者同學,說不定大神成長之路有博哥的奠基石。。。
共同學習成長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 —— 通用網絡請求
源碼分析:
/**
* HTTP請求工具類
* @Date 2020-04-26
**/
/**
* 獲取HTTP請求頭
* @param {Object} currentHeader - 需要配置的請求頭
* @return {Object} header
**/
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裏面可以看到這個代碼:
/**
* 處理接口響應內容
*
* @param {String} res
* @param {function} resolve
* @param {function} reject
**/
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源碼分析
/**
* 針對AliYun API的HTTP請求工具類
* @Date 2020-04-26
**/
// 通用網絡請求
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');
};
/**
* 構造規範化的請求字符串
* @param {Object} params 請求參數,不包括Signature
* @return {String} result 格式:key1=value1&key2=value2....
**/
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('&');
};
/**
* 獲取加密字符串
* @param {String} stringToSign 請求方法
* @param {Object} key 簽名key
**/
const _signature = (stringToSign, key) => {
let signature = crypto.HMAC(crypto.SHA1, stringToSign, key, {
asBase64: true
});
return signature;
};
/**
* 處理接口響應內容
*
* @param {String} res
* @param {function} resolve
* @param {function} reject
**/
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 = {
/**
* AliyunApi GET請求
* @param {Object} opts 包含請求參數、請求URL、請求頭
**/
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);
})
});
},
/**
* AliyunApi POST請求
* @param {Object} opts 包含請求參數、請求URL、請求頭
**/
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目錄,當前小程序用到了設備管理。
關於設備管理,請讀者自行查閱
對應代碼如下:
/**
* 針對AliYun 設備管理API請求
* https://help.aliyun.com/document_detail/69893.html?spm=a2c4g.11186623.6.736.5fcd342dh5UEoS
*
* @Date 2020-04-27
**/
import { aliyunApi } from '../../utils/aliyunHttp.js';
const app = getApp();
let deviceApi = {
/**
* 在指定產品下注冊設備
*
* @param {Object} param 請求參數
* {
* DeviceName: xxxx,
* Nickname: xxxx
* }
**/
registerDevice(param = {}) {
let opts = {
data: {
...param,
Action: 'RegisterDevice',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
/**
* 查詢指定設備的詳細信息
*
* @param {Object} param 請求參數
* {
* IotId: xxxxx,
* }
**/
queryDeviceDetail(param = {}) {
let opts = {
data: {
...param,
Action: 'QueryDeviceDetail',
}
};
return aliyunApi.get(opts);
},
/**
* 批量查詢設備詳情。
*
* @param {Object} param 請求參數
* {
* DeviceName: [
* ],
* }
**/
batchQueryDeviceDetail(param = {}) {
let opts = {
data: {
...param,
Action: 'BatchQueryDeviceDetail',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
/**
* 查詢指定產品下的所有設備列表,分頁接口
*
* @param {Object} param 請求參數
* {
* PageSize: xxxx,
* CurrentPage: xxxx
* }
**/
queryDevice(param = {}) {
let opts = {
data: {
...param,
Action: 'QueryDevice',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
/**
* 刪除指定設備
*
* @param {Object} param 請求參數
* {
* IotId: xxxxx,
* }
**/
deleteDevice(param = {}) {
let opts = {
data: {
...param,
Action: 'DeleteDevice',
}
};
return aliyunApi.get(opts);
},
/**
* 查看指定設備的運行狀態
*
* @param {Object} param 請求參數
* {
* IotId: xxxxx,
* }
**/
getDeviceStatus(param = {}) {
let opts = {
data: {
...param,
Action: 'GetDeviceStatus',
}
};
return aliyunApi.get(opts);
},
/**
* 批量查看同一產品下指定設備的運行狀態。
*
* @param {Object} param 請求參數
* {
* IotId: [
* ],
* }
**/
batchGetDeviceState(param = {}) {
let opts = {
data: {
...param,
Action: 'BatchGetDeviceState',
ProductKey: app.aliConfig.ProductKey,
}
};
return aliyunApi.get(opts);
},
/**
* 禁用指定設備
*
* @param {Object} param 請求參數
* {
* IotId: xxxxxx,
* }
**/
disableThing(param = {}) {
let opts = {
data: {
...param,
Action: 'DisableThing',
}
};
return aliyunApi.get(opts);
},
/**
* 解除指定設備的禁用狀態,即啓用被禁用的設備
*
* @param {Object} param 請求參數
* {
* IotId: xxxxxx,
* }
**/
enableThing(param = {}) {
let opts = {
data: {
...param,
Action: 'EnableThing',
}
};
return aliyunApi.get(opts);
},
/**
* 查詢指定設備的屬性記錄
*
* @param {Object} param 請求參數
* {
* Identifier: xxxxxx,
* PageSize: xxxxx,
* IotId: xxxxx
* }
**/
queryDevicePropertyData(param = {}) {
let opts = {
data: {
...param,
Asc: 0,
EndTime: new Date().getTime() + '',
StartTime: '',// 要轉成上線那天的時間
Action: 'QueryDevicePropertyData',
}
};
return aliyunApi.get(opts);
},
/**
* 調用該接口批量修改設備備註名稱
*
* @param {Object} param 請求參數
* {
* DeviceNicknameInfo.N.IotId: xxxxx,
* DeviceNicknameInfo.N.Nickname: xxxxx
* }
**/
updateDeviceNickname(param = {}) {
let opts = {
data: {
...param,
Action: 'BatchUpdateDeviceNickname',
}
};
return aliyunApi.get(opts);
},
/**
* 調用該接口爲指定設備設置屬性值
*
* @param {Object} param 請求參數
* {
* IotId: xxxxx,
* Items: xxxxx
* }
**/
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事業做貢獻。
喜歡的同學,請不要跑了,給博主點個贊,你的點贊是博主前進的動力。