Fork me on GitHub

微信公众号开发

微信公众号开发

前面做过 HG 项目的微信端,里面用到微信扫码、支付、图片选取、拍照、分享的功能。用到 weixin-js-sdk 和 WeixinJSBridge。

前端开发以Vue代码为例,后端以NodeJS为例。后端开发还需详细阅读开发者文档

1、开发准备

1.1 新建测试号

用于开发和测试环境,产品经理需准备

步骤:

登录【公众号平台】 → 侧边栏开发选择【开发者工具】→ 进入【公众平台测试帐号】→ 扫码确认 → 成功

==注意:测试号的配置都在此页面==

如图:

公众号

1.2 配置公众号

  • 保存 appIDappSecret 和 测试号二维码

    ==注意:及时保存,以免需要的时候找不到==

  • 修改授权回调页面地址

    ==注意:用户确认授权后重定向的地址==

    网页授权

  • 修改接口配置信息、JS接口安全域名

    ==注意:调用 weixin-js-sdk 功能或调用 WeixinJSBridge 页面所在域名==

    修改JS安全域名:

    ​ 公众号主页侧边栏设置选择【公众号设置】→ 选择上面第二个tab【功能设置】第三项

    安全域名

  • 如果有支付功能需要配置授权目录

    ==注意:这个需要在商户平台修改,不是在公众平台==

    授权目录

    举例:

    ​ 现在需要在 http://www.behuntergatherer.com/pay/payment 页面进行支付,就需要设置一个授权目录为 http://www.behuntergatherer.com/pay/ 。在支付页面路由的前一个目录。

    建议:

    ​ 统一一个页面进行支付。比如 http://www.behuntergatherer.com/pay/payment 是支付页面,我在 http://www.behuntergatherer.com/vip/order 页面需要支付,则生成好订单后跳转 http://www.behuntergatherer.com/pay/payment 页面进行支付。

  • 下载安装【Web开发者工具】

    ==注意:开发同学准备,下载地址==

    目的:

    • 页面开发兼容性、布局和浏览器表现不一致
    • 微信接口调试、调用接口有日志

    注意事项:

    • 支付功能不能在本地测试,不能使用测试公众号测试
    • 开发工具可能存在未知 bug,请保持更新

##2、简单config封装

2.1 开发文档

2.2 配置封装

  • 这些接口需要配置以后才能调用
  • API可以配需要的几个,亦可以全部
  • 一次配置只对当前页面有效,跳转页面需要重新配置
2.2.1 JS-SDK接口配置

==注意:需要调用微信JSAPI接口的页面,不能使用h5 pushState进行跳转,会造成无效的签名问题。请使用location.href。==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// utils.js
// 微信配置接口通用方法
const ALL_API_LIST = [
'scanQRCode',
'...'
]

export function configWxApi (jsApiList = ALL_API_LIST) {
return new Promise((resolve, reject) => {
// 使用当前的href获取签名,后端返回配置接口所需要的参数
get('/mdm2/api/getSignature.do', {
url: encodeURIComponent(window.location.href),
})
.then(data => {
wx.config({
debug: process.env.NODE_ENV !== 'production',
appId: data.appId,
timestamp: Number(data.timestamp), // 秒数,不是毫秒
nonceStr: data.nonceStr,
signature: data.signature,
jsApiList,
})

wx.error(function (res) {
Toast('调用微信jsapi返回的状态:' + res.errMsg)
reject()
})

wx.ready(resolve)
})
})
}
2.2.2 WeixinJSBridge
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// utils.js
export function checkWxBridge () {
return new Promise((resolve, reject) => {
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', resolve, false)
} else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', resolve)
document.attachEvent('onWeixinJSBridgeReady', resolve)
}
} else {
resolve()
}
})
}

2.3 配置函数使用

2.3.1 JS-SDK使用

==注意事项:==

  1. 所有调用 JS-SDK 的接口都必须 queryWxApi
  2. 都必须处理错误情况,增加用户体验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 采用Promise封装有两种使用方法,统一选择一个
// app.js
import { configWxApi } from 'utils'

// Promise写法
mounted () {
configWxApi()
.then(res => {
// 配置完成
wx.chooseImage()
})
.catch(err => {
// 配置失败
})
}

// 同步函数写法
async mounted () {
try {
const configResult = await queryWxApi(['scanQRCode'])
// 成功处理
} catch (err) {
// 错误处理
}
}
2.3.2 WeixinJSBridge

==注意事项:==

  1. 所有调用JS-SDK的的接口都必须 checkWxBridge
  2. 都必须处理错误情况,增加用户体验
1
2
3
4
5
6
7
// someComponent.js
import { checkWxBridge } from 'utils'

checkWxBridge().then(() => {
WeixinJSBridge.call('hideOptionMenu')
WeixinJSBridge.invoke('getBrandWCPayRequest')
})

3、后端开发示例

后端一两个常见例子说明

3.1 Access_token

​ ==这是微信开发中最基础的东西,详情见官方文档== 详细文档 => 任意门

  1. 这是什么东西?

    access_token是公众号的==全局唯一==接口调用凭据,公众号调用各接口时都需使用access_token

  2. 有什么属性?

    • access_token的存储至少要保留==512个字符==空间
    • access_token的有效期目前为==2个小时(7200秒)==
    • 重复获取将导致上次获取的access_token==失效==
  3. 怎么获取?

    ​ ==调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。==

    https请求方式: GET

    1
    https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

    参数说明

    | 参数 | 是否必须 | 说明 |
    | ———- | —- | ——————————— |
    | grant_type | 是 | 获取access_token填写client_credential |
    | appid | 是 | 第三方用户唯一凭证 |
    | secret | 是 | 第三方用户唯一凭证密钥,即appsecret |

    返回说明

    正常情况下,微信会返回下述JSON数据包给公众号:

    1
    {"access_token":"ACCESS_TOKEN","expires_in":7200}

开发建议:统一获取和刷新Access_token,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// accessToken.js

module.exports = {
getToken: function (callback) {
// 判断token是否过期,如果过期就重取,未过期就返回
var tokenPath = path.join(__dirname, './token.json')
var token = JSON.parse(fs.readFileSync(tokenPath))
var currTime = utils.getTimeStamp().second
var isExpired = currTime > token.timestamp

if(isExpired) fetchToken(callback)
else callback(token.value)
},
updateToken: function (callback) {
fetchToken(callback)
},
}

function fetchToken (callback) {
var configPath = path.join(__dirname, './config.json')
var config = JSON.parse(fs.readFileSync(configPath))
var param = {
grant_type: 'client_credential',
appid: config.appId,
secret: config.secret,
}
var paramStr = utils.obj2Params(param)
var url = `https://api.weixin.qq.com/cgi-bin/token?${paramStr}`

request(url, function(err, res, body) {
// 记录获取token时间,两小时后过期重取
try {
var resReult = JSON.parse(body);
var newToken = {
value: resReult.access_token,
timestamp: utils.getTimeStamp().second + 7200,
}
var filePath = path.join(__dirname, 'token.json')
fs.writeFileSync(filePath, JSON.stringify(newToken))
callback(newToken.value)
} catch (e) {
console.log('获取token出错,', e)
callback()
}
})
}

// somejs.js
import { getToken } from 'accessToken'

getToken(function(token) {
// 其他业务
})

3.2 获取用户openid

公众号开发中常见的需求 详细文档 => 任意门

  1. openid是啥?

    是加密后的微信号,==每个用户==对==每个公众号==的OpenID是唯一的。对于不同公众号,同一用户的openid不同。

  2. openid能干啥?

    公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称头像性别所在城市语言关注时间

    提示:

    ​ 用户头像链接http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0 修改链接最后一个数字也是就是0,能获取不同尺寸的头像,(有0、46、64、96、132数值可选,0代表640*640正方形头像)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "subscribe": 1, // 用户是否订阅该公众号 0:未关注 1:已关注
    "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
    "nickname": "Band", // 用户的昵称
    "sex": 1, // 性别,1:男性,2:女性,0:未知
    "language": "zh_CN", // 用户的语言,简体中文为zh_CN
    "city": "广州",
    "province": "广东",
    "country": "中国",
    "headimgurl": "LINK_URL", // 用户头像图片链接
    "subscribe_time": 1382694957,// 关注公众号时间
    "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL", //
    "remark": "", // 公众号运营者对粉丝的备注
    "groupid": 0, // 用户所在的分组ID(兼容旧的用户分组接口)
    "tagid_list":[128,2] // 用户被打上的标签ID列表
    }
  3. 怎么拿到openid?

    两种获取的方法(相同接口所带参数不同而已):

    1. 静默授权 snsapi_base,==体验较好,获取到的数据较少==
    2. 用户授权 snsapi_userinfo,==体验较差,用户可能拒绝,获取到的数据多==

    ​ ==注意:对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知。==

3.3 获取签名

==签名在微信开发过程中扮演了一个非常重要的角色,很容易出错== 详细文档 => 任意门

  1. 签名是啥?

    公众号用于调用微信接口的临时票据

  2. 怎么获取?

    官方文档说明 详细文档 => 任意门

3.4 支付功能实现

​ 官方详细文档 => 任意门

4、前端开发示例

4.1 关注快捷入口

​ 微信是反对引导关注的,但是,为了能留住访客。we have to。使用的链接是公众号详情页面下【历史消息】页面。可以点进页面,然后右上角【点点点】点击然后复制链接获取。

关注展示

4.2 接口使用【支付】(WeixinJSBridge)

​ 官方详细文档 => 任意门

4.2.1 功能开发

==注意事项==

  1. 时间戳为秒数,部分系统取到的值为毫秒级,需要转换成秒(10位数字)
  2. package 内容是”prepay_id=是预订单id(统一下单任意门
  3. paySignparams 所有参数根据微信签名规则生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { checkWxBridge } from 'utils'

someFunction () {
get('/wx/fetchSign').then(data => {
// data 需要的数据
// appId, timeStamp, nonceStr, package, paySign
const params = Object.assign(data, {signType: 'MD5'})

checkWxBridge().then(() => {
WeixinJSBridge.invoke('getBrandWCPayRequest', params, res => {
// res 数据结构 {err_msg: 'get_brand_wcpay_request:ok'}
// err_msg 可能的值 ok、fail、cancel
})
})
})
}
4.2.2 测试号测试支付

测试号是没有支付功能,需要用正式的公众号,而且需要有支付权限的公众号(个人公众号不可支付)。在测试环境使用正式环境的openid去实现支付。

1
2
3
4
5
6
7
8
9
10
11
12
13
var openIdMap = {
'测试公众号openId': '正式公众号openId',
'正式公众号openId': '测试公众号openId',
}

// 测试环境
1. 【前端】购买请求
2. 【后端】用户购买(测试公众号openId)
3. 【后端】统一下单用(openIdMap[测试公众号openId])
4. 【前端】拿到prepay_id,WeixinJSBridge.invoke('getBrandWCPayRequest')支付
5. 【前端】用户支付请求 => 微信
6. 【后端】收到支付成功或回调(可异步或同步),通知前端
7. 【后端】保存相应数据到(openIdMap[正式公众号openId])

4.3 头像剪切功能(JS-SDK)

CRMWeb 项目使用的 VueJS,使用的是npm相关包 vue-croppa

4.3.1 通过Input选择
  1. vue-croppa 直接选择图片
  2. 输出裁剪后的图片
4.3.2 通过weixin-js-sdk选择
  1. 页面签名

    1
    2
    3
    4
    5
    import { queryWxApi } from 'utils' 

    mounted () {
    queryWxApi().then(res => this.wxConfSucc = true)
    }
  2. 选择本地图片或拍照,获取到 localId

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    wx.chooseImage({
    count: 1, // 可选择张数
    sizeType: ['original', 'compressed'],
    sourceType: [
    'album', // 从相册选
    'camera',// 拍照
    ],
    success: (res) => {
    let localId = res.localIds[0]
    },
    })
  3. 通过 localId 上传到微信服务器拿到 serverId(只能保存3天)

    1
    2
    3
    4
    5
    6
    7
    wx.uploadImage({
    localId: localId, // 本地ID
    isShowProgressTips: 1, // 是否显示上传进度
    success: res => {
    const serverId = res.serverId
    },
    })
  4. 服务端通过 serverId 下载图片到自己服务器,并返回图片链接给前端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    croppa(
    v-model="croppa",
    :width="coroppaStyle.width"
    :height="coroppaStyle.height"
    :disable-click-to-choose="false"
    :show-remove-button="false"
    :initial-position="position"
    )
    img(
    :src="showImgUrl"
    slot="initial"
    )

5、常见错误

调用 config 接的时候传入参数 debug: true 可以开启 debug 模式,页面会 alert 出错误信息。以下为常见错误及解决方法:

5.1 invalid url domain

当前页面所在域名与使用的 corpid 没有绑定(可在该企业号的应用可信域名中配置域名)。

5.2 ==invalid signature==

签名错误,建议按如下顺序检查:

  1. 确认签名算法正确,可用 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign 页面工具进行校验。
  2. 确认 config 中 nonceStr, timestamp 与用以签名中的对应 noncestr, timestamp一致。
  3. 确认 url 是页面完整的 url (请在当前页面 alert(location.href.split(‘#’)[0]) 确认),包括 http(s):// 部分,以及’?’后面的 GET 参数部分,但不包括 ‘#’hash 后面的部分。
  4. 确认 config 中的 appid 与用来获取 jsapi_ticket 的 corpid 一致。
  5. 确保一定缓存 access_token 和 jsapi_ticket。
  6. 可能获取 access_token 次数超过一天限制的数量。
  7. 使用 pushState 来实现 web app 的页面会导致签名失败,改用 location.href。
  8. 确保你获取用来签名的 url 是动态获取的,动态页面可参见实例代码中 php 的实现方式。如果是 html 的静态页面在前端通过 ajax 将 url 传到后台签名,前端需要用js获取当前页面除去’#’hash部分的链接(可用 location.href.split(‘#’)[0] 获取,而且需要==encodeURIComponent==),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。

5.3 ==the permission value is offline verifying==

这个错误是因为 config 没有正确执行,或者是调用的 JSAPI 没有传入 config 的 jsApiList 参数中。建议按如下顺序检查:

  1. 确认 config 正确通过。
  2. 如果是在页面加载好时就调用了 JSAPI,则必须写在 wx.ready 的回调中。
  3. 确认 config 的 jsApiList 参数包含了这个 JSAPI。

5.4 permission denied

​ 该企业号没有权限使用这个 JSAPI(部分接口需要认证之后才能使用)

5.5 function not exist

​ 当前客户端版本不支持该接口,请升级到新版体验。

5.6 6.0.1版本config:ok,6.0.2版本之后不ok

​ 因为6.0.2版本之前没有做权限验证,所以 config 都是ok,但这并不意味着你 config 中的签名是 ok 的,请在6.0.2检验是否生成正确的签名以保证 config 在高版本中也 ok。

5.7 ==在iOS和Android都无法分享==

​ 请确认企业号已经认证,只有认证的企业号才具有分享相关接口权限,如果确实已经认证,则要检查监听接口是否在wx.ready回调函数中触发

5.8 服务上线之后无法获取jsapi_ticket

​ 因为 access_token 和 jsapi_ticket 必须要在自己的服务器缓存,否则上线后会触发频率限制。请确保一定对token 和 ticket 做缓存以减少服务器请求,不仅可以避免触发频率限制,还加快你们自己的服务速度。目前为了方便测试提供了1w的获取量,超过阀值后,服务将不再可用,请确保在服务上线前一定全局缓存 access_toke和 jsapi_ticket,两者有效期均为7200秒(以返回结果中的 expires_in 为准),否则一旦上线触发频率限制,服务将不再可用

5.9 uploadImage传多图

​ 目前只支持一次上传一张,多张图片需等前一张图片上传之后再调用该接口。

5.10 对本地选择的图片进行预览

​ chooseImage 接口本身就支持预览,不需要额外支持。

5.11 出现config:fail错误

​ 这是由于传入的 config 参数不全导致,请确保传入正确 appId、timestamp、nonceStr、signature 和需要使用的 jsApiList。

5.12 使用pushState来实现web app的页面会导致签名失败

​ 此问题已在Android6.2中修复。在IOS中此问题依然存在。

5.13 Android uploadImage在chooseImage的回调中有时候会不执行

​ Android6.2 会解决此问题,若需支持低版本可以把调用 uploadImage 放在 setTimeout 中延迟 100ms 解决。

5.14 getLocation坐标在openLocation有偏差

​ 因为 getLocation 返回的是gps坐标,openLocation 打开的腾讯地图为火星坐标,需要第三方自己做转换,6.2 版本开始已经支持直接获取火星坐标。

5.15 ==未关注测试号==

​ 通过 snsapi_base 方式获取未关注公众号的用户 openid。测试号是不可以的。

6、附录

6.1 微信JSAPI列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[
'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',
]

6.2 WeixinJSBridge 接口列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
// 类型一
'hideOptionMenu', // 隐藏右上角按钮
'showOptionMenu', // 显示右上角按钮
'hideToolbar', // 隐藏底部工具栏
'showToolbar', // 显示底部工具栏
// 类型二
'getNetworkType', // 获取网络状态
'getBrandWCPayRequest', // 调用支付
]

// 类型一
WeixinJSBridge.call('hideToolbar')
// 类型二
WeixinJSBridge.invoke('getBrandWCPayRequest')

6.3 所有菜单项列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
// 基本类
'menuItem:exposeArticle' // 举报
'menuItem:setFont' // 调整字体
'menuItem:dayMode' // 日间模式
'menuItem:nightMode' // 夜间模式
'menuItem:refresh' // 刷新
'menuItem:profile' // 查看公众号(已添加)
'menuItem:addContact' // 查看公众号(未添加)
// 传播类
'menuItem:share:appMessage' // 发送给朋友
'menuItem:share:timeline' // 分享到朋友圈
'menuItem:share:qq' // 分享到QQ
'menuItem:share:weiboApp' // 分享到Weibo
'menuItem:favorite' // 收藏
'menuItem:share:facebook' // 分享到FB
// 保护类
'menuItem:jsDebug' // 调试
'menuItem:editTag' // 编辑标签
'menuItem:delete' // 删除
'menuItem:copyUrl' // 复制链接
'menuItem:originPage' // 原网页
'menuItem:readMode' // 阅读模式
'menuItem:openWithQQBrowser' // 在QQ浏览器中打开
'menuItem:openWithSafari' // 在Safari中打开
'menuItem:share:email' // 邮件
'menuItem:share:brand' // 一些特殊公众号
]
-------------我是有底线哒-------------