争与不争其实无关紧要,因为有句话说的好,功到自然成。
前情回顾
上篇文章主要分享了异步编程
的一些经验。主要包括回调函数
,发布订阅
,Promise
,async await
以及yield
关键字。
今天忍不住要分享一下,公众号的开发经历。主要是实现了自动回复
以及获取素材列表
两个简单的功能。实现这两个功能以后,其实对于微信公众平台上的其他功能基本上就可以自由接入了。因为基本都是同样的流程。
而之所以要整理这么一个东西,是因为以往的开发过程中,往往在类似的场景中,前端对后端的依赖太重,所有的接口都要等后端去完成,如果前端人员基于Node有一套自己的服务,那么类似的场景岂不是可以自由发挥了?
所以,大致牺牲了一个周的晚上时间,又仔细读了一遍微信公众平台的开发文档,找了些教程,将它的开发流程用Node走了一遍。
开发前准备
在正式开发前需要将文档大致浏览一遍。需要了解accecc_token
,消息回复的流程及微信定义的各种消息格式。同时需要申请一个测试账号,以及ngrock用来进行内网穿透,方便我们在本地调试。
access_token
相关- 两个小时刷新
- 更新后 原有的token失效
- 有效期 7200s
自动回复消息 五个步骤
- 处理POST类型控制逻辑,接受xml数据包
- 解析数据包 消息类型 或 事件类型
- 拼装定义好的消息
- 包装成 XML格式
- 5s 内返回回去
消息格式
<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 命令
# 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的方法进行封装,并且要能检测它是否有效,并且能在无效时进行更新。
// 更新票据
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基础知识总结