阅读 4244

前端对接微信分享功能完全指南

  • 苏格团队
  • 作者:Dee
  • 交流QQ群:855833773

背景

最近,由于公司业务需要,接入了微信web端分享接口。虽然微信的接口文档已经很详细了,但是缺少实战代码。小编搜了一下掘金网站好像也很少这方面的分享(或许是太过简单,大神们都不屑于分享这类经验。当然也有客观原因,现在大多都做小程序了,微信web端的流量被分流了很多。

准备

此为微信公众平台接口文档地址

此为微信公众平台接口测试帐号申请地址

此为获取基础access_token地址

此为微信JS-SDK说明文档地址

以下nodejs端是基于egg框架。

开发

1、申请公众号或测试版公众号

公众号申请(这里不详说公众号申请,有需要自己去官网看文档)用于生产环境的开发。

对于前期开发阶段,可以去微信公众平台接口测试帐号申请,微信扫描二维码即可申请。

注意事项:

1、填写JS接口安全域名,绑定服务器域名

2、扫描“测试号二维码”,把你的微信号加入列表,然后你的微信就有权限进行测试了。没有加入列表的微信号是没有权限使用微信接口的,这里要注意一下。

至此,测试公众号配置完成。

2、获取基础access_token接口。

微信有两个access_token,一个是基础access_token,一个是网页授权access_token,具体区别,如图所述:

我们调用的微信分享接口只需要基础access_token就好。

接口api:

(get)api.weixin.qq.com/cgi-bin/tok…

参数:

grant_type:填写“client_credential”

appid: 公众号appId

secret: 公众号secret

返回值:

成功时

{
    "access_token":"ACCESS_TOKEN",
    "expires_in":7200
}
复制代码

expires_in为该access_token的有限时间,由于微信对于获取accesss_token接口每天有次数限制,所以我们需要把access_token存到服务器里,等到其失效后再重新发起请求。

异常时

{
    "errcode":错误码,
    "errmsg":错误信息
}
复制代码

错误码:

  • -1 系统繁忙,此时请开发者稍候再试

  • 0 请求成功

  • 40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性

  • 40002 请确保grant_type字段值为client_credential

  • 40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置

注意:

1、当遇到错误码为40164(调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置),要去到公众号的白名单列表加上自己服务器的IP,测试公众号账号是不用配置白名单的,所以在把测试公众号参数换成正式公众号参数时,记得配置白名单。

代码片段:

    const { ctx, config } = this;
    let timestamp = new Date().valueOf();
    // 判断缓存里是否有access_token且没有过期,并且获取该access_token时的appId与现在的一致,判断appId是为了避免切换不同公众号配置时没有清缓存出现错误
    // 如果以上条件不能同时满足,则重新请求access_token
    if (!ctx.session.tokenObj || ctx.session.tokenObj.expires_in < timestamp || ctx.session.tokenObj.app_id !== config.wx.appId) {
        const tokenResult = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.wx.appId}&secret=${config.wx.secret}`, {
            dataType: 'json'
        });
        if (tokenResult.status === 200 && tokenResult.data && tokenResult.data.access_token) {
            ctx.session.tokenObj = {
                access_token: tokenResult.data.access_token,
                expires_in: timestamp + tokenResult.data.expires_in * 1000,
                app_id: config.wx.appId
            };
        } else {
            // 记录请求错误日志,方便定位错误
            // 因为该缓存access_token已经不能使用,请求错误时记得把access_token缓存也清空。
            ctx.session.tokenObj = null;
            ctx.logger.error(new Error(`wxconfig: ${JSON.stringify(config.wx)}`));
            ctx.logger.error(new Error(`tokenResult: ${JSON.stringify(tokenResult)}`));
        }
    }
复制代码

3、获取jsapi_ticket

接口api:

(get)api.weixin.qq.com/cgi-bin/tic…

参数:

access_token:上一步请求返回的access_token

type:'jsapi'

返回值:

成功时:

{
"errcode":0,
"errmsg":"ok",
"ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
"expires_in":7200
}
复制代码

与access_token一样,expires_in为该ticket的有限时间,由于微信对于获取ticket接口每天有次数限制,所以我们需要把access_token存到服务器里,等到其失效后再重新发起请求。

异常时:

{
"errcode":42001,
"errmsg":"access_token expired hint: [5bugDA09718938!]"
}
复制代码

代码:

timestamp = new Date().valueOf();
// 判断缓存里是否有jsapi_ticket且没有过期,并且获取该jsapi_ticket时的appId与现在的一致,判断appId是为了避免切换不同公众号配置时没有清缓存出现错误
// 如果以上条件不能同时满足,则重新请求access_token
if (!ctx.session.jsapiObj || ctx.session.jsapiObj.expires_in < timestamp || ctx.session.jsapiObj.app_id !== config.wx.appId) {
    const jsapiResult = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${ctx.session.tokenObj.access_token}&type=jsapi`, {
        dataType: 'json'
    });
    if (jsapiResult.status === 200 && jsapiResult.data && jsapiResult.data.errcode === 0) {
        ctx.session.jsapiObj = {
            ticket: jsapiResult.data.ticket,
            expires_in: timestamp + jsapiResult.data.expires_in * 1000,
            app_id: config.wx.appId
        };
        ctx.logger.error(new Error(`jsapiResult:success: ${JSON.stringify(jsapiResult)}`));
    } else {
        // 记录请求错误日志,方便定位错误
        // 因为该缓存jsapi_ticket已经不能使用,请求错误时记得把jsapi_ticket缓存也清空。
        ctx.session.jsapiObj = null;
        ctx.logger.error(new Error(`wxconfig: ${JSON.stringify(config.wx)}`));
        ctx.logger.error(new Error(`jsapiResult: ${JSON.stringify(jsapiResult)}`));
    }
}
复制代码

我发现,这个api基本不会怎么出现错误码,基本上access_token如果没有问题,这个api的调用也不会报错。

常出现的错误多是上一步的access_token缓存策略不合理导致这个接口的access_token参数的值不是有效的access_token。

4、获取签名算法signature值

签名生成规则如下:

参与签名的字段包括

有效的jsapi_ticket、

noncestr(随机字符串)、

timestamp(时间戳)、

url(当前网页的URL,不包含#及其后面部分) 。

对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义

代码:

const jsapi_ticket = ctx.session.jsapiObj.ticket;
const uuidv1 = require('uuid/v1');
const noncestr = uuidv1();
timestamp = new Date().valueOf();
const { url } = ctx.query;
const string1 = `jsapi_ticket=${jsapi_ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`;
const crypto = require('crypto');
const hash = crypto.createHash('sha1');
hash.update(string1);
const signature = hash.digest('hex');
复制代码

注意:

  • 签名用的noncestr和timestamp必须与下一步的wx.config中的nonceStr和timestamp相同。
  • 签名用的url必须是调用JS接口页面的完整URL
  • 出于安全考虑,建议在服务器端实现签名逻辑。
  • 参数先后顺序就jsapi_ticket、noncestr、timestamp、url,这个顺序搞错会导致签名

签名算法这一步很关键,不合理会导致下一步出现错误。具体的错误相对应的解决办法可以查看官方文档,里面很详细。地址:微信JS-SDK说明文档,查看该网站的 附录5-常见错误及解决方法


以上步骤都是node端实现,以下为web端的代码。

5、引入微信js文件

官方js地址:res.wx.qq.com/open/js/jwe…

官方js备用地址: res2.wx.qq.com/open/js/jwe…

6、通过wx.config接口注入权限验证配置

所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。

wx.config({
    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: '', // 必填,公众号的唯一标识
    timestamp: , // 必填,生成签名的时间戳,与生成签名的timestamp要一致
    nonceStr: '', // 必填,生成签名的随机串,与生成签名的nonceStr要一致
    signature: '',// 必填,签名
    jsApiList: [] // 必填,需要使用的JS接口列表
});
复制代码

jsApiList具体可查看:微信JS-SDK说明文档,查看该网站的 附录2-所有JS接口列表

7、调用微信分享接口

代码:

            wx.ready(function() {
                const title = '分享标题';
                const desc = '分享描述';
                const imgUrl = '分享图片链接';
                // 朋友圈
                wx.onMenuShareTimeline({
                    title: title, // 分享标题
                    link: url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                    imgUrl: imgUrl // 分享图标
                });

                // 微信朋友
                wx.onMenuShareAppMessage({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                    imgUrl: imgUrl, // 分享图标
                    type: 'link', // 分享类型,music、video或link,不填默认为link
                    dataUrl: '' // 如果type是music或video,则要提供数据链接,默认为空
                });

                // qq
                wx.onMenuShareQQ({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接
                    imgUrl: imgUrl // 分享图标
                });

                // qq空间
                wx.onMenuShareQZone({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接
                    imgUrl: imgUrl // 分享图标
                });

                // 腾讯微博
                wx.onMenuShareWeibo({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接
                    imgUrl: imgUrl // 分享图标
                });
            });
复制代码

另外还有一些调用成功success事件、调用失败fail事件、用户点击取消分享cancel事件。具体可看:微信JS-SDK说明文档 - JSSDK使用步骤 - 接口调用说明

至此,微信分享接口已经可用了。希望对刚接触微信接口的你有帮助。

全部代码

    // NODE端
    async getWXApiTicket() {
        const { ctx, config } = this;
        let timestamp = new Date().valueOf();
        if (!ctx.session.tokenObj || ctx.session.tokenObj.expires_in < timestamp || ctx.session.tokenObj.app_id !== config.wx.appId) {
            const tokenResult = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.wx.appId}&secret=${config.wx.secret}`, {
                dataType: 'json'
            });
            if (tokenResult.status === 200 && tokenResult.data && tokenResult.data.access_token) {
                ctx.session.tokenObj = {
                    access_token: tokenResult.data.access_token,
                    expires_in: timestamp + tokenResult.data.expires_in * 1000,
                    app_id: config.wx.appId
                };
            } else {
                ctx.session.tokenObj = null;
                ctx.logger.error(new Error(`wxconfig: ${JSON.stringify(config.wx)}`));
                ctx.logger.error(new Error(`tokenResult: ${JSON.stringify(tokenResult)}`));
            }
        }
        let res = {
            code: 500,
            msg: '获取失败'
        };
        if (ctx.session.tokenObj && ctx.session.tokenObj.app_id === config.wx.appId) {
            timestamp = new Date().valueOf();
            if (!ctx.session.jsapiObj || ctx.session.jsapiObj.expires_in < timestamp || ctx.session.jsapiObj.app_id !== config.wx.appId) {
                const jsapiResult = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${ctx.session.tokenObj.access_token}&type=jsapi`, {
                    dataType: 'json'
                });
                if (jsapiResult.status === 200 && jsapiResult.data && jsapiResult.data.errcode === 0) {
                    ctx.session.jsapiObj = {
                        ticket: jsapiResult.data.ticket,
                        expires_in: timestamp + jsapiResult.data.expires_in * 1000,
                        app_id: config.wx.appId
                    };
                } else {
                    ctx.session.jsapiObj = null;
                    ctx.logger.error(new Error(`wxconfig: ${JSON.stringify(config.wx)}`));
                    ctx.logger.error(new Error(`jsapiResult: ${JSON.stringify(jsapiResult)}`));
                }
            }
            if (ctx.session.jsapiObj && ctx.session.jsapiObj.app_id === config.wx.appId) {
                const jsapi_ticket = ctx.session.jsapiObj.ticket;
                const uuidv1 = require('uuid/v1');
                const noncestr = uuidv1();
                timestamp = new Date().valueOf();
                const { url } = ctx.query;
                const string1 = `jsapi_ticket=${jsapi_ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`;
                const crypto = require('crypto');
                const hash = crypto.createHash('sha1');
                hash.update(string1);
                const signature = hash.digest('hex');
                res = {
                    code: 0,
                    data: {
                        nonceStr: noncestr,
                        timestamp,
                        signature,
                        appId: config.wx.appId,
                        jsapi_ticket,
                        string1
                    }
                };
            }
        }
        ctx.body = res;
    }

    // JS端
    const wx = window['wx'];
    const url = location.href.split('#')[0];
    $.get('/getWXApiTicket?url=' + encodeURIComponent(url), function(res) {
        if (res.code === 0) {
            wx.config({
                debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                appId: res.data.appId, // 必填,公众号的唯一标识
                timestamp: res.data.timestamp, // 必填,生成签名的时间戳
                nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
                signature: res.data.signature, // 必填,签名
                jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareQZone', 'onMenuShareWeibo'] // 必填,需要使用的JS接口列表
            });

            wx.ready(function() {
                const title = '分享标题';
                const desc = '分享描述';
                const imgUrl = '分享图片链接';
                // 朋友圈
                wx.onMenuShareTimeline({
                    title: title, // 分享标题
                    link: url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                    imgUrl: imgUrl // 分享图标
                });

                // 微信朋友
                wx.onMenuShareAppMessage({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                    imgUrl: imgUrl, // 分享图标
                    type: 'link', // 分享类型,music、video或link,不填默认为link
                    dataUrl: '' // 如果type是music或video,则要提供数据链接,默认为空
                });

                // qq
                wx.onMenuShareQQ({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接
                    imgUrl: imgUrl // 分享图标
                });

                // qq空间
                wx.onMenuShareQZone({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接
                    imgUrl: imgUrl // 分享图标
                });

                // 腾讯微博
                wx.onMenuShareWeibo({
                    title: title, // 分享标题
                    desc: desc, // 分享描述
                    link: url, // 分享链接
                    imgUrl: imgUrl // 分享图标
                });
            });
        }
    });
复制代码
关注下面的标签,发现更多相似文章
评论

查看更多 >