微信公众号开发入门

2020-07-16  本文已影响0人  木兮君

​ 本文是主要是针对了解微信公众号开发或者进行过一些简单的开发,但是不成体系的开发者。前后端在参与公众号开发期间,主要承担的是各自的开发工作,前后端逻辑隔离较大。本文将从申请测试账号开始,选择常用的公众号功能,带大家体验完整的微信公众号开发的流程

本文较长,至少需要1-2个小时的练习时间,可以收藏起来利用碎片时间学习。

本文后端代码使用nodejs

需要的前期准备:

  1. 对微信公众号开发的基本了解
  2. nodejs基础知识
  3. 可通过公网访问的服务器(没有的可以去百度一个内网穿透的工具)【 http://www.ngrok.cc/

演示代码所在github仓库地址:https://github.com/shb190802/wechat

(一、)前期准备

  1. 打开ngrok网站,注册一个免费的内网穿透隧道(不要http验证用户名和密码!!!)(有自己的服务器可忽略1、2步骤
1.png
  1. 下载下载ngrok客户端,并启动隧道(请阅读ngrok文档)

    2.png
  1. 使用koa搭建本地webserver

    • 新建空白目录,打开cmd,在当前目录下输入【npm init -y】初始化目录
    > npm init -y
    
    • 安装后边要使用到的第三方组件:koa、koa-router、koa-body、koa-static、crypto(加密)、axios、superagent(请求)、xml2js
    > npm i -S koa koa-router koa-body koa-static crypto axios superagent xml2js
    
    • 新增app.js
    const Koa = require('koa')
    const KoaRouter = require('koa-router')
    
    const app = new Koa()
    const router = new KoaRouter()
    
    router.get('/', ctx => {
     ctx.body = 'Hello World!'
    })
    
    app.use(router.routes())
    app.use(router.allowedMethods())
    app.listen(3000, err => {
     console.log(err || 'run in port 3000!')
    })
    
    • 启动服务

      由于后期会经常修改文件,这里使用supervisor来做热启动

    > supervisor app.js
    
    • 访问ngrok赠送域名(显示【Hello World!】即表示内网穿透成功)
    3.png
  1. 去微信申请一个测试的微信公众号

​ 打开: https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login ,使用微信登录,申请一个测试的公众号

4.png 5.png

​ 你会得到一个测试appId和appSecret。底部为可以体验的接口权限列表。

​ 到此,环境准备工作已经结束,你现在拥有一个拥有大部分测试权限的公众号和一个可以给外网提供服务器的公网服务器

(二、)后端服务

1.基础支持-获取access_token

​ 请提前阅读文档【开始开发-获取access token】 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

此类接口不需要服务端响应微信消息,所以本次开发中不将其纳入测试服务中,会单独简历一个文件夹测试此类接口。生产环境请使用定时任务更新access token并自己保存。

​ 新增文件/wechat-api/1.access_token.js,输入以下内容:

const axios = require('axios')
const fs = require('fs')
const { appId, secret } = require('../config')

function getAccessToken () {
    let url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${secret}`
    axios({
        method: 'GET',
        url: url
    }).then(res => {
        console.log(res.data)
        if (!res.data.errcode) {
            fs.writeFileSync('./token.txt', res.data.access_token, 'utf8')
        }
    })
}
getAccessToken()

​ 重新打开一个控制台,进入wechat-api目录,输入【node 1.acces_token.js】

> node 1.access_token.js
10.png

控制台有以上输入,并且wechat-api目录下,新增了一个token.txt即表示access_token获取成功,之后的微信相关api调用,均需要使用到access_token。

2.基础支持-获取微信服务器IP地址

​ 公众号基于安全考虑,要保证收到的消息请求来自微信,则可以使用此接口获取微信的服务器列表。

​ 请提前查看文档【开始开发-获取微信服务器IP地址】

​ 新增文件/wechat-api/2.get_wechat_service_list.js,输入以下内容:

const axios = require('axios')
const fs = require('fs')

function getCallBackIp () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    const url = `https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=${token}`
    axios({
        method: 'GET',
        url: url
    }).then(res => {
        console.log(res.data)
    })
}
getCallBackIp()

​ 在控制台输入:

> node 2.get_wechat_service_list.js
11.png

输出以上内容,接口调用成功!

3.接收消息-验证接口真实性

​ 请提前阅读文档【开始开发-接入指南】 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html

7.png 8.png 9.png

配置接口信息完成!!!

4.接收消息-接收普通消息

​ 请先阅读文档【接收消息-接收普通消息】

​ 此处使用依旧使用/msg接口,不过使用post方式接收

12.png 13.jpg

服务端收到xml消息

14.png 15.png

其他接收其他消息类型此处不再赘述了。自己根据文档练习一下。

5.接收消息-接收事件推送

​ 请先阅读文档【接收消息-接收事件推动】

​ 修改/controller/msg.js 增加对事件处理

...
case 'event':
    console.log('msgType is event', data.xml.Event[0] === 'subscribe' ? '我关注了' : '我取消关注了')
    break
...
16.png

6.发送消息-自动回复用户消息

​ 请先阅读开发文档【发送消息-自动回复】

module.exports = {
    text (opt) {
        return `<xml>
        <ToUserName><![CDATA[${opt.ToUserName}]]></ToUserName>
        <FromUserName><![CDATA[${opt.FromUserName}]]></FromUserName>
        <CreateTime>${~~(new Date / 1000)}</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[${opt.Content}]]></Content>
    </xml>`
    },
    image (opt) {
        return `<xml>
        <ToUserName><![CDATA[${opt.ToUserName}]]></ToUserName>
        <FromUserName><![CDATA[${opt.FromUserName}]]></FromUserName>
        <CreateTime>${~~(new Date / 1000)}</CreateTime>
        <MsgType><![CDATA[image]]></MsgType>
        <Image>
            <MediaId><![CDATA[${opt.MediaId}]]></MediaId>
        </Image>
    </xml>`
    }
}
switch (data.xml.MsgType[0]) {
    case 'text':
        console.log('msgType is text content is:', data.xml.Content[0])
        res = text({
            FromUserName: data.xml.ToUserName[0],
            ToUserName: data.xml.FromUserName[0],
            Content: '回复:' + data.xml.Content[0]
        })
        break
    case 'event':
        console.log('msgType is event', data.xml.Event[0] === 'subscribe' ? '我关注了' : '我取消关注了')
        if (data.xml.Event[0] === 'subscribe') {
            res = text({
                FromUserName: data.xml.ToUserName[0],
                ToUserName: data.xml.FromUserName[0],
                Content: '谢谢关注'
            })
        }
        break
}
17.jpg

7.发送消息-模板消息

​ 请先阅读文档【发送消息-模板消息】

18.png
const axios = require('axios')
const fs = require('fs')

function send () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    let url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`
    let data = {
        "touser": "ovmcWwEQR3fjMYQxkv0S1R2FBwpg",
        "template_id": "ps4BXtj4gqhfQb6V8RSppDPVoXV19G5V7dFnOeoBUJk",
        "url": "http://suohb.com",
        "topcolor": "#FF0000",
        "data": {
            "User": {
                "value": "黄先生",
                "color": "#173177"
            },
            "Date": {
                "value": "06月07日 19时24分",
                "color": "#173177"
            },
            "CardNumber": {
                "value": "0426",
                "color": "#173177"
            },
            "Type": {
                "value": "消费",
                "color": "#173177"
            },
            "Money": {
                "value": "人民币260.00元",
                "color": "#173177"
            },
            "DeadTime": {
                "value": "06月07日19时24分",
                "color": "#173177"
            },
            "Left": {
                "value": "6504.09",
                "color": "#173177"
            }
        }
    }
    axios({
        method: 'post',
        url,
        data
    }).then(res => {
        console.log(res.data)
    })
}
send()
> node 3.template_message.js
19.png 20.jpg

其他模板消息接口,自己根据文档练习一下。

8.用户管理

​ 请先阅读文档【用户管理】

const axios = require('axios')
const fs = require('fs')

// 增加标签
function add_tag () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    let url = `https://api.weixin.qq.com/cgi-bin/tags/create?access_token=${token}`
    let data = {
        "tag": {
            "name": "广东"//标签名
        }
    }
    axios({
        method: 'post',
        url,
        data
    }).then(res => {
        console.log(res.data)
    })
}
add_tag()
21.png
// 批量获取用户信息
function batchUserInfo () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    let url = `https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=${token}`
    let data = {
        "user_list": [
            {
                "openid": "ovmcWwEQR3fjMYQxkv0S1R2FBwpg",
                "lang": "zh_CN"
            }
        ]
    }
    axios({
        method: 'post',
        url,
        data
    }).then(res => {
        console.log(res.data)
    })
}
batchUserInfo()
22.png

其他模板消息接口,自己根据文档练习一下(github上有部分代码)。

9.推广支持-生成带参数二维码

​ 请先阅读文档【推广支持-生成带参数二维码】

​ 带参数的二维码,可以识别用户关注的途径,针对性进行处理

const axios = require('axios')
const fs = require('fs')

function getTicket () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    let url = `https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`
    let data = { "expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": { "scene": { "scene_id": 1 } } }
    axios({
        method: 'post',
        url,
        data
    }).then(res => {
        console.log(res.data)
        console.log('qrcodeUrl', `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${res.data.ticket}`)
    })
}
getTicket()
> node 5.qrcode.js
24.png 23.png

其他模板消息接口,自己根据文档练习一下。

10.推广支持-长链接转短链接

​ 请先阅读文档

const axios = require('axios')
const fs = require('fs')

function short () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    let url = `https://api.weixin.qq.com/cgi-bin/shorturl?access_token=${token}`
    let data = {
        action: 'long2short',
        long_url: 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQFS8TwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAySVRLVm9MLURjbTAxZGY1bU52MVMAAgTPig1fAwSAOgkA'
    }
    axios({
        method: 'post',
        url,
        data
    }).then(res => {
        console.log(res.data)
    })
}
short()
> node 6.short_url.js
25.png

11.自定义菜单

const axios = require('axios')
const fs = require('fs')

function createMenu () {
    const token = fs.readFileSync('./token.txt')
    let url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${token}`
    let data = {
        "button": [
            {
                "type": "click",
                "name": "今日歌曲",
                "key": "V1001_TODAY_MUSIC"
            },
            {
                "name": "菜单",
                "sub_button": [
                    {
                        "type": "view",
                        "name": "搜索",
                        "url": "http://www.soso.com/"
                    },
                    {
                        "type": "click",
                        "name": "赞一下我们",
                        "key": "V1001_GOOD"
                    }]
            }]
    }
    axios({
        method: 'POST',
        url,
        data
    }).then(res => {
        console.log(res.data)
    })
}
createMenu()
> node 8.menu.js
42.png 43.jpg

其他菜单接口,自己根据文档练习一下。

12.素材管理

const fs = require('fs')
const FormData = require('form-data')
// const axios = require("axios") // axios 上传素材一直报错,暂时没有解决方式
// const request = require('request') // reqeust团队后期不在维护了,所以此处也不使用
const superagent = require('superagent')


// 上传临时素材
function upload () {
    let token = fs.readFileSync('./token.txt', 'utf8')
    let url = `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=image`

    superagent.post(url).attach('media', fs.createReadStream('./img.jpg')).then(res => {
        console.log(res.text)
        let data = JSON.parse(res.text)
        console.log(`https://api.weixin.qq.com/cgi-bin/media/get?access_token=${token}&media_id=${data.media_id}`)
    })
}
upload()
> node 10.material.js
44.png 45.jpg

其他接口,自己根据文档练习一下。

(三、)网页服务

1.网页授权

​ 首先阅读相关文档

1.1获取code

27.png
module.exports.callback = (ctx, next) => {
    const { code } = ctx.query
    ctx.body = `callback page code is:${code}`
}
const { callback } = require('./controller/web')
...
router.get('/callback', callback) // 微信回调地址

https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx222fa789edad09c5&redirect_uri=http%3A%2F%2Fsuohb.free.idcfengye.com%2Fcallback&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
26.png 28.jpg

1.2使用code置换openId

const axios = require('axios')
const { appId, secret } = require('../config')

module.exports.callback = async (ctx, next) => {
    const { code } = ctx.query
    let getOpenIdUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${secret}&code=${code}&grant_type=authorization_code`
    let res = await axios.get(getOpenIdUrl)
    ctx.body = `callback page res is:${JSON.stringify(res.data)}`
}
29.jpg

1.3使用access_token置换用户信息

​ 注:此处的access_token跟基础access_token不同,属于网页使用的access_token

...
let getUserInfoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${res.data.access_token}&openid=${res.data.openid}&lang=zh_CN`
res = await axios.get(getUserInfoUrl)
...
30.jpg

1.4跳转到前端页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页</title>
</head>
<body>
    HOME_PAGE
</body>
</html>
const KayStatic = require("koa-static")
...
app.use(KayStatic('./static'))
...
ctx.response.redirect(`/index.html?openId=${res.data.openid}&nickname=${res.data.nickname}`)
...
31.jpg
http://suohb.free.idcfengye.com/index.html?openId=ovmcWwEQR3fjMYQxkv0S1R2FBwpg&nickname=%E6%9C%A8%E5%85%AE

2.JS-SDK使用权限签名算法

2.1获取jssdk_ticket

​ 请先阅读相关文档

const axios = require('axios')
const fs = require('fs')

function getTicket () {
    const token = fs.readFileSync('./token.txt')
    let url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`
    axios({
        method: 'GET',
        url: url
    }).then(res => {
        console.log(res.data)
        if (!res.data.errcode) {
            fs.writeFileSync('./ticket.txt', res.data.ticket, 'utf8')
        }
    })
}
getTicket()
> node 7.jssdk_ticket.js
32.png

2.2新增权限签名接口

34.png
const axios = require('axios')
const { appId, secret } = require('../config')
const fs = require('fs')
const { sha1, random } = require('../utils')
...
module.exports.jsapi = (ctx, next) => {
    let { url } = ctx.request.body
    let jsapi_ticket = fs.readFileSync('./wechat-api/ticket.txt', 'utf8')
    let noncestr = random(16)
    let timestamp = + new Date()

    let data = {
        jsapi_ticket,
        noncestr,
        timestamp,
        url
    }
    let signature = Object.keys(data).sort().map(item => `${item}=${data[item]}`).join('&')
    signature = sha1(signature)

    ctx.body = { nonceStr: noncestr, timestamp, signature, appId }
}
router.post('/jsapi', jsapi) // 获取jsapi权限
<div id="log" style="word-break: break-all;">HOME_PAGE</div>
<script src="//res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    function getSignature() {
        return axios({
            method: 'post',
            url: 'http://suohb.free.idcfengye.com/jsapi',
            data: {
                url: window.location.href.split('#')[0]
            }
        }).then(res => {
            document.querySelector("#log").innerHTML = JSON.stringify(res.data, '  ')
        })
    }

    getSignature()
</script>
33.png

3.config注入权限验证配置并判断客户端支持情况(wx.checkJsApi)

let jsApiList = ['updateAppMessageShareData', 'updateTimelineShareData', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice', 'onVoicePlayEnd', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'translateVoice', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard']
...
function wxConfig(data) {
    return new Promise((resolve, reject) => {
        wx.config({
            ...data,
            jsApiList
        })
        wx.error(err => {
            reject(err)
        })
        wx.ready(() => {
            resolve()
        })
    })
}

getSignature().then(res => {
    return wxConfig(res)
}).then(() => {
    wx.checkJsApi({
        jsApiList, // 需要检测的JS接口列表,所有JS接口列表见附录2,
        success: function (res) {
            document.querySelector("#log").innerHTML = JSON.stringify(res)
        }
    });
})

4.分享接口

//需在用户可能点击分享按钮前就先调用
function share() {
    let shareData = {
        title: '分享的标题',
        desc: '分享的描述呀!!!',
        link: window.location.href,
        imgUrl: `${window.location.origin}/images/share.png`
    }
    wx.updateAppMessageShareData({
        ...shareData,
        success: function () { }
    })
    wx.ready(function () {
        wx.updateTimelineShareData({
            ...shareData,
            success: function () { }
        })
    });
}

getSignature().then(res => {
    return wxConfig(res)
}).then(() => {
    ...
    share()
})
35.jpg

5.图像接口-拍照或从手机选择图片

<button onclick="chooseImage()">chooseImage</button>
<div id="log" style="word-break: break-all;">HOME_PAGE</div>
<script>
...
    function chooseImage() {
        wx.chooseImage({
            count: 1, // 默认9
            sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
            sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
            success: function (res) {
                var localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
                document.querySelector("#log").innerHTML = `<img src='${localIds[0]}' style='width:90%;height:auto;'/>`
            }
        });
    }
</script>
36.jpg 37.jpg

其他图片接口,请参照文档自己练习

6.音频接口

<button id="vioce" onclick="toogleRecord()">Record</button>
...
<script>
...
    let isInRecord = false
    function toogleRecord() {
        let logger = document.querySelector("#log")
        if (!isInRecord) {
            isInRecord = true
            wx.startRecord()
            logger.innerHTML = '开始录音。。。'
        } else {
            isInRecord = false
            wx.stopRecord({
                success: function (res) {
                    let localId = res.localId;
                    logger.innerHTML = '录音结束,开始播放。。。'
                    wx.playVoice({
                        localId: localId,
                        success: function () {
                            logger.innerHTML = '播放成功!'
                        },
                        fail: function () {
                            logger.innerHTML = '播放失败!'
                        }
                    });
                },
                fail: function () {
                    logger.innerHTML = '录音失败!!!'
                },
                cancel: function () {
                    logger.innerHTML = '取消录音'
                }
            })
        }
    }
</script>
38.jpg

其他音频接口,请参照文档自己练习

7.获取地理位置接口

<button onclick="getLocation()">getLocation</button>
...
<script>
    function getLocation() {
        wx.getLocation({
            type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
            success: function (res) {
                var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
                var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。
                var speed = res.speed; // 速度,以米/每秒计
                var accuracy = res.accuracy; // 位置精度
                document.querySelector("#log").innerHTML = `${longitude},${latitude}`
            }
        });
    }
</script>
39.jpg

8.关闭当前网页窗口接口

<button onclick="closeWindow()">closeWindow</button>
...
<script>
    function closeWindow() {
        wx.closeWindow()
    }
</script>

9.界面操作-隐藏显示按钮

<button onclick="hideMenu()">hideMenu</button>
<button onclick="showMenu()">showMenu</button>
...
<script>
    let menuList = ["menuItem:exposeArticle", "menuItem:setFont", "menuItem:dayMode", "menuItem:nightMode", "menuItem:refresh", "menuItem:profile", "menuItem:addContact", "menuItem:share:appMessage", "menuItem:share:timeline", "menuItem:share:qq", "menuItem:share:weiboApp", "menuItem:favorite", "menuItem:share:facebook", "menuItem:share:QZone", "menuItem:editTag", "menuItem:delete", "menuItem:copyUrl", "menuItem:originPage", "menuItem:readMode", "menuItem:openWithQQBrowser", "menuItem:openWithSafari", "menuItem:share:email", "menuItem:share:brand"]
    function hideMenu() {
        wx.hideAllNonBaseMenuItem()
        wx.hideMenuItems({
            menuList: menuList // 要隐藏的菜单项,只能隐藏“传播类”和“保护类”按钮,所有menu项见附录3
        })
    }
    function showMenu() {
        wx.showAllNonBaseMenuItem()
        wx.showMenuItems({
            menuList: menuList // 要显示的菜单项,所有menu项见附录3
        })
    }
</script>
40.jpg 41.jpg

10.微信扫一扫

<button onclick="scanQRCode()">scanQRCode</button>
...
<script>
    function scanQRCode() {
        wx.scanQRCode({
            needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
            scanType: ["qrCode", "barCode"], // 可以指定扫二维码还是一维码,默认二者都有
            success: function (res) {
                var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
                document.querySelector("#log").innerHTML = result
            }
        })
    }
</script>

到这里,测试公众号的基本功能已经测试完毕,其他相关开发,以后在慢慢整理。

未完待续。。。

git仓库地址:https://github.com/shb190802/wechat

都看到这里,给作者点个赞吧。

上一篇下一篇

猜你喜欢

热点阅读