网上关于node开发公众号的资料相当缺乏,本文旨在以node的视角对公众号开发做一个阐述。
目前公众号主要分为三种:服务号、订阅号、小程序;还有企业微信只针对企业用户使⽤用,暂且不算在内。
微信公众平台:https://mp.weixin.qq.com/
名称 | 服务对象 | 业务类型 | 关注后的位置 |
---|---|---|---|
服务号 | 企业 | 任意 | 联系⼈人列列表 |
订阅号 | 个⼈人或媒体 | 信息传播 | 归纳在订阅号 |
小程序 | 企业 | 任意 | 归纳在最近使⽤用 |
服务号:给企业提供用户管理与业务服务的能⼒,实现业务扩张。 订阅号:给个⼈或媒体提供信息传播的能⼒,与读者建⽴更好的沟通。 小程序:作⽤基本同服务号,比服务号H5应用体验更更好,但无法替代非H5的沟通系统,可以实现互补。
对于个⼈而⾔,无论是学习还是维护——个⼈公众号,只要不不涉及⽀支付环节,注册一个订阅号足以。如果需要⽀付功能,那么需要注册服务号,服务号注册时需要企业相关证书。
参考资料 微信开发者工具说明 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455784140 npm 库 wechat和wechat-api,以及微信开发者文档。
如果需要结合⾃身业务进⾏定制,那么就需要申请成为开发者,然后调用微信提供的api结合⾃身业务进⾏扩展。
测试账号使用文档:https://blog.csdn.net/hzw2312/article/details/69664485
在进⾏公众号开发时,通常会先在测试账号中进⾏开发调试,经测试确认无误后,再把新功能切换到正式账号。
开通测试账号,将具有所有的权限!
2. 记录测试账号的appID与appsecret
3 . 测试账号的服务器配置
4. 测试账号的JS接口安全域名配置
5. 扫⼀扫关注⾃⼰的测试账号,然后会在用户列表⾥展现
6. 创建⼏个模板消息,供将来测试使⽤。
公众号开发时,总是面临各种上传文件服务器的操作,极其不便。而sunny-ngrok提供了内网穿透功能。它可以把你本机的ip发布到外网。
安装sunny-ngrok实现外网的映射:https://www.ngrok.cc/
注册,登录。
点击隧道管理理,打开"开通隧道"
编辑隧道信息-- 填入隧道名(随便填),前置域名(如www.yyy.baidu.com中的yyy,其实就是在该域名下开了一个前缀给你,因此只要写前缀就行了,选一个别⼈人没有⽤过的),本地映射的端⼝,则 是要和web项⽬目的http访问端⼝对应。
确认开通后回到隧道管理。就拿到了隧道id。
下载客户端。放到usr/local下。并在此打开命令行:
./sunny clientid d5324b15e9e99905 你的隧道id
看到这个就配置成功了。下面起一个node服务器来验证一下。
npm init
npm i koa koa-router koa-static koa-bodyparser -S
// index.js
const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const bodyParser = require('koa-bodyparser');
const app = new Koa()
app.use(bodyParser())
const router = new Router()
app.use(static(__dirname + '/'))
app.use(router.routes()); /*启动路由*/
app.use(router.allowedMethods());
app.listen(3000);
然后写一个vue的页面:
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
<script src="https://unpkg.com/vue@2.1.10/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/cube-ui/lib/cube.min.js"></script>
<script src="https://cdn.bootcss.com/qs/6.6.0/qs.js"></script>
<script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<link rel="stylesheet" href="https://unpkg.com/cube-ui/lib/cube.min.css">
<style>
/* .cube-btn {
margin: 10px 0;
} */
</style>
</head>
<body>
<div id="app">
<cube-input v-model="value"></cube-input>
<cube-button @click='click'>Click</cube-button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
value: 'input'
},
methods: {
click: function () {
console.log('click')
}
},
mounted: function () {
},
});
</script>
</body>
</html>
输入http://djtao.free.idcfengye.com/。访问成功:
有一个不错的半官方的库 co-wechat
:https://github.com/node-webot/co-wechat 把它安装了。
然后新建一个配置。
// config.js
module.exports={
appid:测试号的appid,
appsecret:测试号的appsecret,
token:你自己定的token
}
然后根据co-wechat文档写一个接口:
const config=require('./config')
const wechat=require('co-wechat')
router.all('/wechat',wechat(config).middleware(
async message=>{
console.log('wecaht',message);
return `hello world ${message.Content}`
}
))
启动服务器。这时候可以配置测试号了。(服务不启动时,无法通过验证)
此时试一试发消息:
后台console的信息是:
以上这个过程是怎么实现的呢?原理必然是重点。
这是服务器验证微信的过程。
首先简单描述一下微信收发信息流程:
假设我们不需要co-WeChat这个库,自己写这个收发流程。可以是这样:
// index.js
const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const xml2js = require('xml2js') // xml转化为json
const app = new Koa()
const url = require('url')
const conf = require('./conf')
const crypto = require('crypto') // 加密模块
const xmlParser = require('koa-xml-body') //解析xml数据
app.use(xmlParser())
const router = new Router()
app.use(static(__dirname + '/'))
// 验证
router.get('/wechat', ctx => {
console.log('校验url', ctx.url)
const {query} = url.parse(ctx.url, true)
const {
signature, // 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp, // 时间戳
nonce, // 随机数
echostr // 随机字符串
} = query
console.log('wechat', query)
// 将 token timestamp nonce 三个参数进行字典序排序并用sha1加密
let str = [conf.token, timestamp, nonce].sort().join('');
console.log('str',str)
let strSha1 = crypto.createHash('sha1').update(str).digest('hex');
console.log(`自己加密后的字符串为:${strSha1}`);
console.log(`微信传入的加密字符为:${signature}`);
console.log(`两者比较结果为:${signature == strSha1}`);
// 签名对比,相同则按照微信要求返回echostr参数值
if (signature == strSha1) {
ctx.body = echostr
} else {
ctx.body = "你不是微信" }
}
)
// 接受信息
router.post('/wechat', ctx => {
const {xml: msg} = ctx.request.body
console.log('Receive:', msg)
const builder = new xml2js.Builder()
const result = builder.buildObject({
xml: {
ToUserName: msg.FromUserName,
FromUserName: msg.ToUserName,
CreateTime: Date.now(),
MsgType: msg.MsgType,
Content: 'Hello ' + msg.Content
}
})
ctx.body = result
})
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
首先是微信向服务器发送get请求。
微信发出GET请求通常包括4个常见字段。
参数 | 描述 |
---|---|
signature | 加密签名,包括token、timestamp和nonce加密混成 |
timestamp | 时间戳 |
nonce | 随机数n+once |
echostr | 随机字符串 |
而微信发送消息,请求除了带上token、timestamp和nonce,还会带上一个xml数据包。
安全哈希算法(Secure Hash Algorithm)主要适用于数字签名标准 (Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。对于长度小于2^64位的消息, SHA1会产生一个160位的消息摘要。当接收到消息的时候,这个消息摘要可以用来验证数据的完整性。在传输的过程中,数据很可能会发生变化,那么这时候就会产生不同的消息摘要。SHA1有如下特性: 不可以从消息摘要中复原信息;两个不同的消息不会产生同样的消息摘要,(但会有1x10 ^ 48分之一的机 率出现相同的消息摘要,一般使用时忽略)。
哈希: 不可变长 -> 摘要固定长度
实际上微信不仅是收发消息那么简单。
官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
为了简化操作,你可以调用一个库,co-wechat的好基友—— co-wechat-api
https://github.com/node-webot/co-wechat-api
!image-20190804010154636
accesstoken是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用accesstoken。开发者需要进行妥善保存。accesstoken的存储至少要保留512个字符空间。accesstoken的有效期目前为2个小时(7200s),需定时刷新,重复获取将导致上次获取的access_token失效。
公众平台的API调用所需的access_token的使用及生成方式说明:
1、建议公众号开发者使用中控服务器统一获取和刷新accesstoken,其他业务逻辑服务器所使用的accesstoken均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;
2、目前accesstoken的有效期通过返回的expirein来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新accesstoken。在刷新过程中,中控服务器可对外继续输出的老accesstoken,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡;
3、accesstoken的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新accesstoken的接口,这样便于业务服务器在API调用获知accesstoken已超时的情况下,可以触发accesstoken的刷新流程。
公众号和小程序均可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。小程序无需配置IP白名单。
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
需要在后端引入axios,获取token则应该这么写:
router.get('/getTokens',async (ctx)=>{
const APPID=config.appid;
const APPSECRET=config.appsecret;
const api=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const res = await axios.get(api);
console.log(res);
Object.assign(tokenCache, res.data, {
updateTime: Date.now()
});
ctx.body = res.data
})
前端调用这个接口,得到:
因为我经常要用,所以要写一个获取方法:
const getTokens=async function(){
if(!tokenCache.access_token||Date.now()-7200*1000>tokenCache.updateTime){
// console.log(222,Date.now()-7200*1000,tokenCache.updateTime)
const APPID = config.appid;
const APPSECRET = config.appsecret;
const api = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const res = await axios.get(api);
return Object.assign(tokenCache, res.data, {
updateTime: Date.now()
})
}else{
return tokenCache;
}
}
实现关注用户列表
// 获取关注者列表
router.get('/getFollowers', async ctx => {
await getTokens();
const api = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${tokenCache.access_token}`
const res = await axios.get(api)
ctx.body = res.data
})
api接口于api调用,是微信验证我们的服务器。
我想在index.html实现以下功能:
- 实际工作中,通常是用库来实现的。比如用户:
const WechatAPI = require('co-wechat-api');
const api = new WechatAPI(conf.appid, conf.appsecret);
router.get('/getFollowers', async ctx => {
let res = await api.getFollowers();
// 批量获取用户信息
let _res=await api.batchGetUsers(res.data.openid);
ctx.body = _res
})
按照上文的api,token是放到服务器运行内存里的。
获取accesstoken的次数是2000次。每次都调用时不现实的。而且在负载均衡情况下,accesstoken是放node1还是node2呢?
答案是放数据库里。
以mongodb为例:
// mongoose.js
// 连接数据库:
const mongoose = require('mongoose')
const {
Schema
} = mongoose
mongoose.connect('mongodb://localhost:27017/weixin', {
useNewUrlParser: true
}, () => {
console.log('Mongodb connected..')
})
exports.ServerToken = mongoose.model('ServerToken', {
accessToken: String
});
// index.js
const {ServerToken}=require('./mongoose')
const api = new WechatAPI(config.appid,
config.appsecret,
async function () {
return await ServerToken.findOne()
},
async function (token) {
const res = await ServerToken.updateOne({}, token, { upsert: true }) //允许覆盖
}
)
在new出 WechatAPI
实例的时候,实际上提供了第三第四个参数。第一个用于存token,第四个用于存放。
先装依赖:
npm i koa koa-router koa-static koa-socket co-wechat -s
// index.js
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
// 静态文件
const static = require('koa-static')
app.use(static(__dirname + '/'))
const conf = require('./conf')
const wechat = require('co-wechat')
// socket.io
const IO = require('koa-socket')
const io = new IO()
io.attach(app)
app._io.on('connection',socket => {
console.log('socket connection..')
})
// 消息接口
router.all('/wechat', wechat(conf).middleware(
async (message, ctx) => {
console.log('wechart', message)
app._io.emit('chat',message)
return '收到!';
}
))
app.use(router.routes());
/*启动路由*/
app.use(router.allowedMethods());
app.listen(3000);
通过socket.io监听收发消息。
比较简单没什么可说的。
前端通过vue和echarts展现数据
<div id="app">
<div id="chart" style="width:100%;height:50%;"></div>
<cube-button @click="reset">重置</cube-button>
<div class="view-wrapper">
<div class="list" v-for="item in list.slice(0,5)">
<div class="item">
<div class="avatar"></div>
<div class="bubble">
<p>{{ item.Content }}</p>
</div>
</div>
</div>
</div>
</div>
var app = new Vue({
el: "#app",
data: {
list: []
},
watch: {
list: {
handler(newName) {
this.renderChart();
},
immediate: true,
deep: true
}
},
methods: {
initChart() {
const option = {
series: [{
type: "pie",
selectedMode: "single",
radius: [0, "70%"],
label: {
normal: {
position: "inner"
}
},
labelLine: {
normal: {
show: false
}
}
}]
};
this.chart = echarts.init(document.getElementById("chart"));
this.chart.setOption(option);
},
// 刷新图表
renderChart(newName) {
const data = ["1", "2"]
.map(key => ({
name: key,
value: this.list.filter(v => v.Content === key).length
}))
.filter(v => v.value !== 0);
const option = {
series: [{
data
}]
};
this.chart ? this.chart.setOption(option) : "";
},
// 重置
reset() {
this.list = [];
}
},
mounted() {
this.initChart();
const socket = io()
socket.on('chat', msg => {
console.log('chart ...', msg)
this.list.unshift(msg)
})
}
});
效果如下