专栏首页西安-晁州小程序开发知识点总结

小程序开发知识点总结

小程序开发知识点总结

我承认,最近比较懒了,博客也很久没更新了,太对不住自己了,做了一段时间小程序开发,总结了一些知识点,直接上菜。

关于小程序登录

这里放上一张流程图:

说明:
    小程序调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
    开发者服务器以code换取 用户唯一标识openid 和 会话密钥session_key。
    之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

这里的流程用代码来说明下:

1、小程序调用wx.login()获取临时登录凭证code 2、小程序拿到code后,使用wx.request将code连同登录返回的用户信息一起发送至后台接口 3、后台接口收到code和用户信息后,调用微信登录凭证校验接口获取session_key、openid(session_key、openid记得保存数据库,后续请求要用到)

const WX_URL = 'https://api.weixin.qq.com/sns/jscode2session';
let sessionData = await ctx.curl(WX_URL, {
  dataType: 'json',
  data: {
    grant_type: 'authorization_code',
    js_code: code,
    secret: this.config.weixin.secret,
    appid: this.config.weixin.appid,
  },
});
sessionData = sessionData.res.data;

其中:
    sessionData即为包含session_key和openid的对象。

4、签名校验

为什么要签名校验?

其实,就是为了保证数据的完整性,微信发送的数据会进行一次签名,如果不进行签名验证的话,不能确保数据一定来自微信平台,存在被劫持的风险。

如何进行签名校验?

刚说到登录和code一起提交上来的用户信息,其实这里面还包含一个rawData和一个signature,rawData就是原始的用户信息json串,拿到这个串之后要拼接上sessionData里面的session_key,然后使用sha1加密,再和signature比对,ru'guo'y如果一致则校验通过,用代码标识如下:

// 验证用户信息完整性
const signature2 = crypto.createHash('sha1').update(fullUserInfo.rawData + sessionData.session_key).digest('hex');
console.log(fullUserInfo.signature, signature2);
if (fullUserInfo.signature !== signature2) {
  ctx.body = { error: '登录失败' };
  return;
}

5、保存用户信息,使用openid作为唯一标识(同一个appid下用户的openid是一致的,不同的appid下可能不同)

6、返回登陆成功后的内容,包括token等

关于小程序支付

小程序的支付一般都是涉及到订单操作了,整个流程一般如下:

1、小程序发送订单支付请求,入参为订单号或订单id 2、接口收到订单请求后,查询订单详情,包括订单总金额等 3、接口调用微信的统一下单接口,返回一些支付前的参数信息,比如支付的金额、交易编号等

const returnParams = await WeixinSerivce.createWxUnifiedOrder({
    openid: userInfo.weixin_openid,
    body: '可以随意指定哪些内容',
    out_trade_no: orderInfo.order_sn,
    total_fee: (parseFloat(orderInfo.actual_price) * 100),
    spbill_create_ip: '',
  });
  await ctx.service.order.updateOrderPrepayId(orderId, (returnParams.package + '').replace('prepay_id=', ''));

其中,createWxUnifiedOrder的service如下:

  // 创建统一下单
  createWxUnifiedOrder(payInfo) {
    wxPayment.init({
      appid: this.config.weixin.appid, // 微信小程序appid
      mch_id: this.config.weixin.mch_id, // 商户帐号ID
      apiKey: this.config.weixin.partner_key, // 秘钥
      // pfx: fs.readFileSync('./apiclient_cert.p12'), //微信商户平台证书 (optional,部分API需要使用)
    });
    return new Promise((resolve, reject) => {
      wxPayment.createUnifiedOrder({
        body: payInfo.body, // 商品或支付单简要描述
        out_trade_no: payInfo.out_trade_no, // 商户系统内部的订单号,32个字符内、可包含字母
        total_fee: payInfo.total_fee,
        spbill_create_ip: payInfo.spbill_create_ip,
        notify_url: this.config.weixin.notify_url,
        trade_type: 'JSAPI',
        product_id: payInfo.out_trade_no,
        openid: payInfo.openid, // 用户openid
      }, (err, result) => {
        console.log('统一下单参数返回:', result);
        if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
          const returnParams = {
            appid: result.appid,
            timeStamp: parseInt(Date.now() / 1000) + '',
            nonceStr: result.nonce_str,
            package: 'prepay_id=' + result.prepay_id,
            signType: 'MD5',
          };
          const paramStr = `appId=${returnParams.appid}&nonceStr=${returnParams.nonceStr}&package=${returnParams.package}&signType=${returnParams.signType}&timeStamp=${returnParams.timeStamp}&key=` + this.config.weixin.partner_key;
          returnParams.paySign = md5(paramStr).toUpperCase();
          resolve(returnParams);
        } else {
          reject(result);
        }
      });
    });
  }

4、小程序拿到返回的returnParams后,调用wx.requestPayment的api发送支付请求即可

5、获取支付通知

获取支付通知的目的在于判断用户支付结果,如上面的notify_url,就是支付通知的接口,该接口中我们可以获取到订单的编号等信息,然后将数据库中的订单改为已支付等状态并保存支付数据信息

关于获取用户手机号

获取小程序用户手机号即微信手机号其实比较简单,流程如下:

1、小程序端使用如下button获取encryptedData、iv数据后发送至后台接口

<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> </button> 
...
Page({ 
    getPhoneNumber: function(e) { 
        console.log(e.detail.errMsg) 
        console.log(e.detail.iv) 
        console.log(e.detail.encryptedData) 
        // 这里拿到encryptedData、iv后发送至后台接口
    } 
}) 

2、接口收到encryptedData、iv后,再取出之前保存的当前用户的session_key,进行数据解密即可

const weixinUserInfo = await WeixinSerivce.decryptUserInfoData(currentUser.session_key, encryptedData, iv);

其中decryptUserInfoData如下:

  // 解析微信登录用户数据
  async decryptUserInfoData(sessionKey, encryptedData, iv) {
    // base64 decode
    const _sessionKey = Buffer.from(sessionKey, 'base64');
    encryptedData = Buffer.from(encryptedData, 'base64');
    iv = Buffer.from(iv, 'base64');
    let decoded = '';
    try {
      // 解密
      const decipher = crypto.createDecipheriv('aes-128-cbc', _sessionKey, iv);
      // 设置自动 padding 为 true,删除填充补位
      decipher.setAutoPadding(true);
      decoded = decipher.update(encryptedData, 'binary', 'utf8');
      decoded += decipher.final('utf8');
      decoded = JSON.parse(decoded);
    } catch (err) {
      return '';
    }
    if (decoded.watermark.appid !== this.config.weixin.appid) {
      return '';
    }
    return decoded;
  }

解密之后的weixinUserInfo里面即包含用户电话信息

关于生成小程序二维码

小程序的二维码可以调用微信接口获取,并且支持二维码中携带指定参数,这样其他用户扫描二维码后程序可以获取到之前放进二维码的参数。

广泛的应用场景是商品详情页的分享、用户的分销推广等,只要在分享的二维码中加入商品id或者当前分享人的id等数据,再次扫描之后便可以看到商品详情等数据或者知道新用户是谁推广过来的,当然应用场景还有很多,就不一一列举了。

获取小程序二维码接口如下:

async genReferQRCode(params) {
    console.log(params);
    const ctx = this.ctx;
    let GET_WX_ACODE_URL = '';
    const access_token = await ctx.service.weixin.getAccessToken();
    GET_WX_ACODE_URL = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`;
    const res = await ctx.curl(GET_WX_ACODE_URL, {
      // dataType: 'json',
      method: 'POST',
      contentType: 'json',
      data: {
        scene: params.scene,
        // page: params.page || 'pages/index/index',
      },
      writeStream: require('fs').createWriteStream(params.filePath),
    });
    console.log(res.data);
}

调用的时候其实更简单:

const refer_code = 'qrcode_' + ctx.query.scene + '_' + ctx.service.utils.genRandomReferCode();
const filePath = ctx.app.config.upload.path + '/qrcode/' + refer_code + '.png';
await ctx.service.weixin.genReferQRCode({
  scene: refer_code,
  filePath,
  page: ctx.query.page,
});
if (fs.existsSync(filePath)) {
  ctx.body = {
    img: ctx.app.config.upload.picDomain + '/qrcode/' + refer_code + '.png',
  };
} else {
  ctx.body = {
    error: '生成二维码失败',
  };
  ctx.status = 500;
}

小程序端如何使用websocket实现实时消息推送等服务

其实,小程序是支持websocket的,比如拼多多等小程序,时时会看到页面顶部弹出"谁谁购买了什么商品"之类的消息。

实现websocket也比较简单,我这里用的是eggjs框架,引入的是socket.io,即使用socket.io实现ws协议,具体流程如下:

1、eggjs配置socket.io及redis(redis可以不用,cluster 模式下,使用 redis 可以较为简单的实现 clients/rooms 等信息共享,此处 redis 仅用于存储连接实例信息)

// socket.io配置
  config.io = {
    // init: { wsEngine: 'ws' }, // passed to engine.io
    namespace: {
      '/': {
        connectionMiddleware: [ 'auth' ],
        packetMiddleware: [],
      },
    },
    redis: {
      host: 'redis服务地址',
      port: 6379,
      password: 'redis密码',
    },
  };
  // redis连接配置
  config.redis = {
    client: {
      port: 6379,
      host: 'redis服务地址',
      password: 'redis密码',
      db: 0,
    },
  };

这里我们指定了一个auth中间件,用来做ws链接时的授权等操作,具体后面介绍。

2、在app目录下新建io目录,io目录下新建controller、middleware目录,然后在controller下新建nsp.js用来接收ws消息,middleware下新建auth.js用来做ws授权等其他操作

1)先看下auth.js怎么写的?

'use strict';
// const _ = require('lodash');
const PREFIX = 'room';
module.exports = () => {
  return async (ctx, next) => {
    const { socket, app } = ctx;
    const query = socket.handshake.query;
    // 用户信息
    const { room, userId } = query;
    // 用户加入
    socket.join(room);
    // 备注:此处 app.redis 与插件无关,可用其他存储代替
    await app.redis.sadd(PREFIX + ':' + room, userId);
    console.log('当前加入用户:', userId);
    await next();
    // 用户离开
    socket.leave(room);
    console.log('当前离开用户:', userId);
    await app.redis.srem(`${PREFIX}:${room}`, userId);
  };
};

auth这里,我是直接拿到用户id、room信息之后将用户加入指定room,redis保存的是该room下的用户id集合。用户离开时将用户id移除该集合即可。(这里用到了redis的sorted set这一数据类型)

2)看下nsp.js如何处理用户消息的?

'use strict';
const Controller = require('egg').Controller;
const moment = require('moment');
class NspController extends Controller {
  async exchange() {
    console.log('socket收到消息了', moment.unix(moment().unix()).format('YYYY-MM-DD HH:mm:ss'));
    const { ctx, app } = this;
    const nsp = app.io.of('/');
    const message = ctx.args[0] || {};
    const socket = ctx.socket;
    const client = socket.id;
    try {
      const { target, payload } = message;
      console.log('收到的消息', message);
      if (!target) return;
      const msg = ctx.helper.parseMsg('exchange', payload, { client, target });
      console.log('msg', msg);
      nsp.emit(target, msg);
    } catch (error) {
      app.logger.error(error);
    }
  }
}
module.exports = NspController;

ctx.helper.parseMsg其实只是一个辅助方法,用来将入参整理,具体如下:

'use strict';
module.exports = {
  parseMsg(action, payload = {}, metadata = {}) {
    const meta = Object.assign({}, {
      timestamp: Date.now(),
    }, metadata);
    return {
      meta,
      data: {
        action,
        payload,
      },
    };
  },
};

所以这里的结果是将用户发过来的消息再emit到target中去。

3、eggjs中配置路由ws路由

在router.js中配置如下:

// socket.io
  const { io } = app;
  io.of('/').route('exchange', io.controller.nsp.exchange);

其中exchange表示ws指定的事件使用io.controller.nsp.exchange处理。

4、小程序端引入weapp.socket.io.js(这是因为小程序不支持socket.io,所以需要这个包去做适配)

5、小程序端测试代码:

onLaunch: function () {
    var that = this;
    that.connectWsSocket('notice', 1);
},
...
connectWsSocket(room, userId) {
    const socket = io(`http://192.168.3.55:7002?room=${room}&userId=${userId}`) // 默认进notice的房间,后台直接向该房间推送消息即可
    socket.on('connect', function () {
      console.log('connected')
    });
    socket.on('res', d => {
      console.log('received res: ', d)
    })
    setInterval(function(){
      socket.emit('exchange', {
        payload: {
          title: 'this is a news'
        },
        target: 'res'
      })
    }, 10000);
  },

这里我们监听res的事件,每隔10s发送一条消息至后台,并且指定消息接收的target事件为res,可以看到如下效果:

这是服务端效果:

这样就搭建了一个小程序端的ws环境了,后续可以根据这个进行随意消息通知,或者一些实时图表展示的场景也可以使用这个实现。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • redis操作基本命令

    Redis—— Remote Dictionary Server,它是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Val...

    用户1141560
  • golang的xml、json解析

    xml golang的xml处理主要应用Unmarshal、Marshal方法实现,解析一个xml到struct如下,首先是xml文件: <?xml versi...

    用户1141560
  • golang学习之struct

    结构体定义的一般方式如下: type identifier struct { field1 type1 field2 type2 ......

    用户1141560
  • 不会PS抠图?来试试这招吧!

    之前写Photoshop处理实验动物大体图片的内容,讲过使用钢笔工具抠除背景这一方法。

    Mark Chen
  • 七个简单但棘手的 JS 面试问题[每日前端夜话0xD4]

    如果你参加 JavaScript 高级开发面试,那么很有可能在编码面试中被问到一些棘手的问题。

    疯狂的技术宅
  • 分析Silverlight跨域调用

    分析Silverlight跨域调用 在silverlight开发的过程中不免要遇到跨域的问题,在这里以跨域调用Webservice为例子来分析一下跨域的问题。...

    用户1172164
  • GitHub 系列之「向GitHub 提交代码」1.SSH2.生成SSH key3.GitHub 上添加 SSH key4.Push & Pull5.提交代码

    你拥有了一个 GitHub 账号之后,就可以自由的 clone 或者下载其他项目,也可以创建自己的项目,但是你没法提交代码。仔细想想也知道,肯定不可能随意就能提...

    意气相许的许
  • LeetCode 946. 验证栈序列(栈)

    给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返...

    Michael阿明
  • 十年来,编程领域有什么重要进展?

    编程语言层出不穷,然而内核是万变不离其宗。我个人看法觉得是以下几个方面的变化比较明显。 语言本身: 1. 工业标准 网页标准有 W3C 控制,尤其是浏览器的开发...

    用户1667431
  • 小小的公共库,大大的耦合,你痛过吗?

    什么是耦合? 耦合,是架构中,本来不相干的代码、模块、服务、系统因为某些原因联系在一起,各自独立性差,影响则相互影响,变动则相互变动的一种架构状态。 感官上,怎...

    架构师之路

扫码关注云+社区

领取腾讯云代金券