独立开发微信公众号服务的一次复盘

2022-07-14 21:12:24 浏览数 (1)

争与不争其实无关紧要,因为有句话说的好,功到自然成。

前情回顾

上篇文章主要分享了异步编程的一些经验。主要包括回调函数发布订阅Promise,async await以及yield关键字。

今天忍不住要分享一下,公众号的开发经历。主要是实现了自动回复以及获取素材列表两个简单的功能。实现这两个功能以后,其实对于微信公众平台上的其他功能基本上就可以自由接入了。因为基本都是同样的流程。

而之所以要整理这么一个东西,是因为以往的开发过程中,往往在类似的场景中,前端对后端的依赖太重,所有的接口都要等后端去完成,如果前端人员基于Node有一套自己的服务,那么类似的场景岂不是可以自由发挥了?

所以,大致牺牲了一个周的晚上时间,又仔细读了一遍微信公众平台的开发文档,找了些教程,将它的开发流程用Node走了一遍。

开发前准备

在正式开发前需要将文档大致浏览一遍。需要了解accecc_token,消息回复的流程及微信定义的各种消息格式。同时需要申请一个测试账号,以及ngrock用来进行内网穿透,方便我们在本地调试。

  • access_token相关
    • 两个小时刷新
    • 更新后 原有的token失效
    • 有效期 7200s
  • 自动回复消息 五个步骤
    • 处理POST类型控制逻辑,接受xml数据包
    • 解析数据包 消息类型 或 事件类型
    • 拼装定义好的消息
    • 包装成 XML格式
    • 5s 内返回回去
  • 消息格式
代码语言:javascript复制
  <xml><ToUserName><![CDATA[gh_2b36e771f7c0]]></ToUserName>
  <FromUserName><![CDATA[oyT495_dXqAYqtimOcxfNmtoENpA]]></FromUserName>
  <CreateTime>1616689961</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[好]]></Content>
  <MsgId>23145433557740763</MsgId>
  </xml>
  • ngrock 命令
代码语言:javascript复制
# 3001 为本地端口号
lt --port  3001

开发流程

借用公众号开发文档的图片

开发流程

当我们将服务启动,并将服务地址配置到公众号后台的服务器配置中时,服务端会接受到一个来自微信后台的get请求,这个请求会带上这几个参数signature,echostr,nonce,token,timestamp,然后我们将用nonce,token,timestamp这三个参数,按照先排序,后加密的流程得到的字符串跟signature进行比较,如果相同则表名是从微信后台进来的,然后将echostr返给微信后台,我们就可以进行其他业务开发了。

具体的验证逻辑:

代码语言:javascript复制
var sha1 = require('sha1')
var getRawBody  = require('raw-body')
var Wechat = require('./wechat')
var util = require('./util')

module.exports = function (opts,handler) {
  console.log('handler--',handler)
  let wechat  = new Wechat(opts)
  return function *(next){
    // 验证逻辑
    var token = opts.token
    var signature = this.query.signature
    var nonce = this.query.nonce
    var echostr = this.query.echostr
    var timestamp = this.query.timestamp
    var str = [token,timestamp,nonce].sort().join('')
    var sha = sha1(str)
    console.log('opts',opts)
    if(this.method == 'GET'){
      if(sha === signature){
        this.body = echostr   ''
      }
      else {
        this.body = 'wrong'
      }
    }
    else if(this.method == 'POST'){
      if(sha !== signature){
        this.body = 'wrong' 
        return false
      }
      var data = yield getRawBody(this.req,{
        length:this.length,
        limit:'1mb',
        encoding:this.charset
      })
      console.log('data',data.toString())

      // util 包解析xml对象
      let content = yield util.parseXMLAsync(data)
      console.log('content',content)
      let message = yield util.formatMessage(content.xml)
      console.log('message',message)
      this.weixin = message
      // 改变执行上下文
      yield handler.call(this,next)
      // 执行回复逻辑
      wechat.reply.call(this)
    }
  }
}

access_token 更新策略

因为我们在调用对应的接口时,都需要用到access_token,所以需要对获取access_token的方法进行封装,并且要能检测它是否有效,并且能在无效时进行更新。

代码语言:javascript复制
// 更新票据
Wechat.prototype.updateAccessToken = function(){
  
  var appID = this.appID
  var appSecret = this.appSecret
  var url = api.accessToken   `&appid=` appID  '&secret=' appSecret

  return new Promise((resolve,reject)=>{

    request({method:'GET',url:url,json:true,}).then((res)=>{
      var data = res.body || {}
      var now = (new Date().getTime())
      var expires_in = now   (data.expires_in-20)*1000
  
      data.expires_in = expires_in
      resolve(data)
    })
  })
}
// 获取token
Wechat.prototype.fetchAccessToken = function(){
  let self = this
  console.log('fetchAccessToken--->>this',this)
  if(this.access_token && this.expires_in){
    if(this.isValidAccessToken(this)){
      return Promise.resolve(this)
    }
  }
  this.getAccessToken()
  .then(function(data){
    console('getAccessToken--》》data',data)

    try{
      data = JSON.parse(data)
      console('getAccessToken--》》data',data)
      // return
    }
    catch(e){
      return self.updateAccessToken(data)
    }

    if(self.isValidAccessToken(data)){
      return Promise.resolve(data)
    }
    else{
      return self.updateAccessToken()
    }
  })
  .then(function(data){
    self.access_token = data.access_token
    self.expires_in =  data.expires_in

    self.saveAccessToken(data)

    return Promise.resolve(data)
  })
} 

消息自动回复

消息自动回复的痛点在于需要对不同的消息格式进行解析,然后按照对应的格式生成对应的回复消息。

代码语言:javascript复制

function compile (ToUserName,FromUserName,MsgType,Content){
  let timeStamp  = new Date().getTime()
  console.log('MsgType====>',MsgType)
  console.log('Content==>',Content)
  if(MsgType === 'text'){
    return `<xml>
      <ToUserName><![CDATA[${FromUserName}]]></ToUserName>
      <FromUserName><![CDATA[${ToUserName}]]></FromUserName>
      <CreateTime>${timeStamp}</CreateTime>
      <MsgType><![CDATA[${MsgType}]]></MsgType>
      <Content><![CDATA[${Content}]]></Content>
    </xml>`
  }else if(MsgType === 'image'){
    return `<xml>
      <ToUserName><![CDATA[${FromUserName}]]></ToUserName>
      <FromUserName><![CDATA[${ToUserName}]]></FromUserName>
      <CreateTime>${timeStamp}</CreateTime>
      <MsgType><![CDATA[${MsgType}]]></MsgType>
      <PicUrl><![CDATA[${picUrl}]]></PicUrl>
    </xml>`
  }else if(MsgType === 'news'){
    return `<xml>
      <ToUserName><![CDATA[${FromUserName}]]></ToUserName>
      <FromUserName><![CDATA[${ToUserName}]]></FromUserName>
      <CreateTime>${timeStamp}</CreateTime>
      <MsgType><![CDATA[${MsgType}]]></MsgType>
      <ArticleCount>${Content.length}</ArticleCount>
      <Articles>
      <item>
          <Title><![CDATA[${Content[0].title}]]></Title>
          <Description><![CDATA[${Content[0].description}]]></Description>
          <PicUrl><![CDATA[${Content[0].picUrl}]]></PicUrl>
          <Url><![CDATA[${Content[0].url}]]></Url>
        </item>
      </Articles>
    </xml>`
  }else if(MsgType === 'voice'){
    return `<xml>
      <ToUserName><![CDATA[${FromUserName}]]></ToUserName>
      <FromUserName><![CDATA[${ToUserName}]]></FromUserName>
      <CreateTime>${timeStamp}</CreateTime>
      <MsgType><![CDATA[${MsgType}]]></MsgType>
      <Voice>
        <MediaId><![CDATA[${Content.MediaId}]]></MediaId>
      </Voice>
    </xml>`
  }
}
exports.compile = compile 

接入素材管理功能

接入素材管理等一系列的功能,如果需要接入的功能比较多,为了方便还是进行了简单的配置化。

代码语言:javascript复制
var api = {
  accessToken:prefix 'token?grant_type=client_credential',
  // 临时素材
  temporary:{
    upload:prefix 'media/upload?'
  },
  // 永久素材
  permanent:{
    upload:prefix 'material/add_material?',
    uploadNews:prefix 'material/add_news?',
    uploadNewsPic:prefix 'media/uploadimg',
    getMeterialList:prefix 'material/batchget_material?'
  }
}

然后在实例上添加了对应的方法:

代码语言:javascript复制
Wechat.prototype.getMaterial = function(type,offset,count){
  let self = this
  let form = {
    "type":type,
    "offset":offset,
    "count":count
  }
  let materialUrl = api.permanent.getMeterialList
  return new Promise((resolve,reject)=>{
    self.fetchAccessToken()
    .then((data)=>{
      console.log('access_token==>',data)
          let url = `${materialUrl}access_token=${data.access_token}`
          request({method:'POST',url:url,body:form,json:true,}).then((res)=>{
            console.log('materialList---》',res.body)
            let _data = res.body || {}
            if(_data){
              resolve(_data)
            }
            else{
              throw new Error('获取素材失败~')
            }
          })
        }).catch(err=>{
          reject(err)
        })
  })
}

总体上遇到的问题

  • 变量拼写错误。
  • this的指向问题。有个地方用call,切换了上下文。
  • 配置JSapi域名回调时,域名已备案但仍然提示未备案。这个需要在微信的论坛里直接找运营让他们确认域名备案结果即可。

其他问题

开发完成,测试无误后,将代码上传至服务器,用pm2,或者forever运行起来。然后nginx将端口代理至80即可。

个人公众号不知道从什么时候开始不支持个人认证了,所以有些权限个人无法使用,比如微信网页开发,因为根本没有网页授权域名,即使配置了js安全接口域名,个人仍然无法进行微信网页开发。

包括一些重要的功能比如支付等,都无法接入。

总结

总体上开发的困难程度并不高,但是需要有合理的逻辑,适时地将一些个功能拆分出来,否则将会有很多if else的判断。

最后,代码在https://gitee.com/mynoe/public_code.git这个仓库中,有兴趣的可以了解一下

javascript基础知识总结

0 人点赞