ESP8266开发之旅 小程序之阿里云篇② “IOT菜鸟”小程序,源码分析,创作自己的小程序

   日期:2020-05-05     浏览:102    评论:0    
核心提示:文章目录1.前言2. 代码结构![在这里插入图片描述](https://img-blog.csdnijson

文章目录

    • 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. 读者知识要求

  • 至少了解过 HTMLCSSJS
  • 至少了解过 微信小程序官方开发文档
  • 至少要去了解阿里云物联网开发文档

    这是我们小程序业务请求的重点,也是本篇重中之重

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.js
  • storage.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);
    },
};

  • 我们定义了两个请求 GETPOST(底层都是调用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事业做贡献。



喜欢的同学,请不要跑了,给博主点个赞,你的点赞是博主前进的动力。

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服