简述H5页面在手机浏览器实现微信分享
一、调研结果
- 微信内置浏览器进行分享,只能监听微信自带的分享按钮,自定义分享的图标什么的,不可能主动触发分享,可以引用微信公众平台的自定义分享接口,也就是JSSDK的相关API,文档地址如下:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.htm
- 在微信公众平台注册一个公众号(必须为企业资质的订阅号,并且开通分享接口的权限:需要企业认证并缴费);开发---接口权限
- 设置---公众号设置---功能设置,填写有效的JS接口安全域名;
- 开发---基本设置---IP白名单,填写项目所在的服务器IP地址;
- 在vue项目中引入jssdk,微信为了方便用户使用,将官方的JSSDK发布到了npm上,有一个叫weixin-js-sdk的是针对CommonJs规范提出的,需要使用require引入;另一个是叫weixin-jsapi,是针对ES6提出的,这个时候我们可以使用import方式引入;
- 出于安全考虑,服务端获取签名:
- 获取access_token,有效期7200秒,在服务端进行缓存,请求地址为:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
- 通过第一步拿到的access_token获取jsapi_ticket,有效期7200秒,在服务端进行缓存,请求地址为:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
- 将noncestr(随机字符串), 有效的jsapi_ticket,timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分)按照ASCII码从小到大排序,组织成URL键值对的形式,并对整个字符串进行sha1加密,生成签名;
- 签名地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
- 签名流程图:
-
- 拿到后台返回的参数,在config里面进行配置:
wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: AppId, // 必填,公众号的唯一标识 timestamp: Timestamp, // 必填,生成签名的时间戳 nonceStr: NonceStr, // 必填,生成签名的随机串 signature: Signature, // 必填,签名 jsApiList: [ //JSSDK1.4以后,微信分享功能用新接口,但是在接口注册的时候,必须把新老接口都加上去,不然不起作用 'checkJsApi', 'onMenuShareTimeline', //分享到微信朋友圈 'onMenuShareAppMessage', //分享给微信朋友 'onMenuShareQQ', //分享到QQ 'onMenuShareQZone', //分享到QQ空间 'updateAppMessageShareData', //分享到微信及QQ(新接口) 'updateTimelineShareData' //分享到朋友圈”及“分享到QQ空间(新接口 ] // 必填,需要使用的JS接口列表 });
- 拿到后台返回的参数,在config里面进行配置:
-
- 在wx.ready函数里调用jsApiList参数里面配置的相关api:
//通过ready接口处理成功验证 // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口, // 则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 wx.ready(function() { wx.updateAppMessageShareData(shareInfo); //分享到微信好友或者qq好友 wx.updateTimelineShareData(shareInfo); //分享到朋友圈或者qq空间 }); wx.error(function(res) { // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 console.log(res);
console.log(res.errMsg); });
- 在wx.ready函数里调用jsApiList参数里面配置的相关api:
- UC以及QQ浏览器的分享:由于UC以及QQ两个APP具有微信分享的能力,而两个APP又将微信分享下放到相关页面,因此页面也具有分享到微信的能力;
- ios系统的UC浏览器10.2版本以下不具有分享到微信的能力;
- android系统的UC浏览器9.7版本以下不具有分享到微信的能力;
- ios系统的QQ浏览器5.4版本以下不具有分享到微信的能力;
- android系统的QQ浏览器5.3版本以下不具有分享到微信的能力;
- android系统的QQ浏览器5.4以下以及5.3以上属于低版本浏览器,需要加载低版本bridgeapi:http://3gimg.qq.com/html5/js/qb.js 其余版本需要加载高版本的bridgeapi:http://jsapi.qq.com/get?api=app.share
- 注意:UC浏览器会直接使用截图进行分享,不支持用户自定义图片;
- 其他浏览器分享:由于这些浏览器的页面不具有分享到微信的能力,因此需要业务组件开发时进行”引导式分享“,即可以通过点击H5页面元素,显示一个引导图,引导用户使用右上角的转发功能;
二、具体实现
- 实现的功能
9.7版本以上的UC浏览器在android端,10.2版本以上的UC浏览器在ios端实现分享到微信以及微信朋友圈; 5.3版本以上的QQ浏览器在android端,5.4以上的QQ浏览器在ios端实现分享到微信以及微信朋友圈; 微信内置浏览器实现自定义分享到微信以及微信朋友圈;
- 实现代码
import { promiseAjax } from '../../utils/promiseAjax'; // import wx from 'weixin-jsapi'; import wx from 'weixin-js-sdk'; /** * @WeChat 创建一个微信分享的父类WeChat */ class WeChat { constructor() { this.UA = navigator.appVersion; this.browserPermission = { UC: { forbid: 0, allow: 1 }, QQ: { forbid: 0, lower: 1, higher: 2 } }; this.qqApiSrc = { lower: 'http://3gimg.qq.com/html5/js/qb.js', higher: 'http://jsapi.qq.com/get?api=app.share' }; this.qqBridgeLoaded = false; //qq浏览器下面是否加载好了相应的api文件 this.config = {}; //默认的config数据 } /** * @getOs 判断操作系统的类型 * @return {String} IOS、ANDROID与WEB */ getOs() { if (/(iPhone|iPad|iPod|iOS)/i.test(this.UA)) { return 'IOS'; } else if (/(Android)/i.test(this.UA)) { return 'ANDROID'; } else { return 'WEB'; } } /** * @isUcBrowser 判断是否是UC浏览器 * @return {Number} 0表示禁止 1表示允许 */ isUCBrowser() { if (/UCBrowser/i.test(this.UA)) { if ( (this.getOs() == 'IOS' && this.getVersion('UCBrowser/') < 10.2) || (this.getOs() == 'ANDROID' && this.getVersion('UCBrowser/') < 9.7) ) { return this.browserPermission.UC.forbid; } else { return this.browserPermission.UC.allow; } } else { return this.browserPermission.UC.forbid; } } /** * @isUcBrowser 判断是否是QQ浏览器 * @return {Number} 0表示禁止 1表示低版本允许 2表示高版本允许 */ isQQBrowser() { if (/MQQBrowser/i.test(this.UA)) { if ( (this.getOs() == 'IOS' && this.getVersion('MQQBrowser/') < 5.4) || (this.getOs() == 'ANDROID' && this.getVersion('MQQBrowser/') < 5.3) ) { return this.browserPermission.QQ.forbid; } else { if (this.getOs() == 'ANDROID' && this.getVersion('MQQBrowser/') < 5.4) { return this.browserPermission.QQ.lower; } else { return this.browserPermission.QQ.higher; } } } else { return this.browserPermission.QQ.forbid; } } /** * @isWXBrowser 判断是否是微信内置浏览器 * @return {Boolean} true表示是 false表示不是 */ isWXBrowser() { return /MicroMessenger/i.test(this.UA); } /** * @getVersion 获取浏览器的版本号 * @param {String} sign UCBrowser/MQQBrowser * @return {Number}浏览器的版本号 */ getVersion(sign) { return parseFloat(this.UA.split(sign)[1]); } /** * @UCShare UC浏览器的分享:会直接使用截图 * @return {*} */ UCShare() { // ios 对象:ucbrowser 微信好友:kWeixin 微信朋友圈:kWeixinFriend // android 对象:ucweb 微信好友:WechatFriends 微信朋友圈: WechatTimeline // ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID'] let platform = this.getOs() == 'IOS' && this.config.type == 1 ? 'kWeixin' : this.getOs() == 'IOS' && this.config.type == 2 ? 'kWeixinFriend' : this.getOs() == 'ANDROID' && this.config.type == 1 ? 'WechatFriends' : this.getOs() == 'ANDROID' && this.config.type == 2 ? 'WechatTimeline' : this.throwError(); let shareInfo = [ this.config.title, this.config.description, this.config.url, platform, '', '', '' ]; if ( this.getOs() == 'ANDROID' && window.ucweb && window.ucweb.startRequest ) { window.ucweb.startRequest('shell.page_share', shareInfo); return; } else if ( this.getOs() == 'IOS' && window.ucbrowser && window.ucbrowser.web_share ) { window.ucbrowser.web_share.apply(null, shareInfo); return; } else { this.throwError(); } } /** * @QQShare QQ浏览器的分享 微信好友:1 微信朋友圈:8 * @return {*} */ QQShare(config) { let type = this.config.type == 1 ? 1 : 8; var share = function() { let shareInfo = { title: config.title, description: config.description, url: config.url, img_url: config.mediaData, img_title: config.title, to_app: type, cus_txt: '' }; if (window.browser && window.browser.app) { window.browser.app.share(shareInfo); } else if (window.qb && window.qb.share) { window.qb.share(shareInfo); } else { let errMsg = { moduleName: 'WXShare', interfaceName: 'WXShare', errorCode: 'native_share_N001', errorMessage: '浏览器不支持进行微信分享' }; throw new InteractiveError(errMsg); } }; this.qqBridgeLoaded ? share() : this.loadQQApi(share); } /** * @loadQQApi qq浏览器根据不同版本加载对应的bridge * @param {Function} cb 回调函数 * @return {*} */ loadQQApi(cb) { var qqApiScript = document.createElement('script'); /** * 需要等加载过 qq 的 bridge 脚本之后 * 再去初始化分享组件 */ qqApiScript.onload = function() { cb && cb(); }; qqApiScript.src = this.isQQBrowser() == 1 ? this.qqApiSrc.lower : this.qqApiSrc.higher; document.body.appendChild(qqApiScript); } /** * @wxShare 微信内置浏览器的分享 * @param {*} * @return {*} */ async wxShare() { // 首先通过config接口注入权限验证配置 let url = 'xxx' let data = { headers: {}, body: { Url: this.getOs() == 'IOS' ? '入口地址' : window.location.href.split('#')[0] } }; let config = await promiseAjax('post', url, data); const { AppId, Timestamp, NonceStr, Signature } = config; wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: AppId, // 必填,公众号的唯一标识 timestamp: Timestamp, // 必填,生成签名的时间戳 nonceStr: NonceStr, // 必填,生成签名的随机串 signature: Signature, // 必填,签名 jsApiList: [ //JSSDK1.4以后,微信分享功能用新接口,但是在接口注册的时候,必须把新老接口都加上去,不然不起作用 'checkJsApi', 'onMenuShareTimeline', //分享到微信朋友圈 'onMenuShareAppMessage', //分享给微信朋友 'onMenuShareQQ', //分享到QQ 'onMenuShareQZone', //分享到QQ空间 'updateAppMessageShareData', //分享到微信及QQ(新接口) 'updateTimelineShareData' //分享到朋友圈”及“分享到QQ空间(新接口 ] // 必填,需要使用的JS接口列表 }); let shareInfo = { title: this.config.title, // 分享标题 desc: this.config.description, // 分享描述 link: this.config.url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 type: 'link', //分享类型,music、video或link,不填默认为link imgUrl: this.config.mediaData, // 分享图标 success: function() { // 设置成功 console.log('分享成功'); }, error: function() { console.log('分享失败'); }, cancel: function() { console.log('取消分享'); } }; //通过ready接口处理成功验证 // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口, // 则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 wx.ready(function() { wx.updateAppMessageShareData(shareInfo); //分享到微信好友或者qq好友 wx.updateTimelineShareData(shareInfo); //分享到朋友圈或者qq空间 }); wx.error(function(res) { // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 console.log(res); }); } /** * @throwError 交互类异常 * @param {*} * @return {*} */ throwError() { throw 浏览器不支持进行微信分享; } /** * @share 进行分享 * @param {Object} param 分享的参数对象 * @return {*} */ share(config) { this.config = config; if (this.config.type == undefined) { throw type是空值; } this.isUCBrowser() ? this.UCShare() : this.isQQBrowser() && !this.isWXBrowser() ? this.QQShare(this.config) : this.isWXBrowser() && this.isQQBrowser() && this.getOs() == 'ANDROID' ? this.wxShare() : this.isWXBrowser() && this.getOs() == 'IOS' ? this.wxShare() : this.throwError(); } } /** * @WXShare 创建一个微信分享的实例 * @return {*} */ export const WXShare = new WeChat(); /** * @description 预加载qqbridge */ WXShare.loadQQApi(function() { WXShare.qqBridgeLoaded = true; });
- promise封装一个ajax请求
/** * @promiseAjax 给后台发请求 * @param {String} method 请求的方式 * @param {String} path 请求的url * @param {Object} body post方式传递给后台的数据 * @return {Promise} 返回一个promise对象 */ export function promiseAjax(method, path, body) { return new Promise((resolve, reject) => { var xhr = ''; if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new window.ActiveXObject('Microsoft.XMLHTTP'); // IE6浏览器创建ajax对象 } xhr.open(method, path); xhr.send(JSON.stringify(body)); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { resolve(JSON.parse(xhr.responseText)); } setTimeout(() => { reject(new Error(xhr.statusText)); }, 3000); }; }); }
- 前端的调用方式
let config = { title: '测试用例', desc: '你看这个行不行', mediaData: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603186815698&di=7ec300630a404299c855c73a99773e17&imgtype=0&src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201411%2F04%2F20141104225457_f8mrM.thumb.700_0.jpeg', url: window.location.href, type: type }; await WXShare.share(config);
三、参考文档
- https://www.cnblogs.com/backtozero/p/7064247.html
- https://blog.csdn.net/lgj199505/article/details/103520329?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control
- https://segmentfault.com/a/1190000037552782?utm_source=tag-newest
四、遇到的问题
- invalid signature
1、确认签名算法正确,可用http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign 页面工具进行校验; 2、传给后端的url必须为动态获取,如果后端用decodeURLComponent对url进行解码,我们传的url必须通过encodeURLComponent进行编码; 3、
4、确认config中nonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致;
5.请确保后台返回的appid与你自己关注的公众号相一致 - IOS端二次分享签名失败,报invalid signature
-
原因:
ios设备传的地址为首次进入应用的地址(入口地址),安卓设备为分享页面的地址,以下进行详细解释;
Vue-Router进行路由切换的时候,总是会操作浏览器的历史记录,从而响应页面URL变化。
在JSSDK文档页面有这么一句话:同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复。但根据多次测试情况来看,情况恰好相反,在Android下直接使用 window.location.href 得出的URL进行签名是完全没问题(可能已升级至Android6.2以上版本),在IOS上就不行了。这是因为在IOS上,无论路由切换到哪个页面,实际真正有效的的签名URL是【第一次进入应用时的URL】。比如进入应用首页是: https://m.app.com,需要使用JSSDK的页面A是:https://m.app.com/product1/123,无论从首页进入到A页面之前,中间跳转过多少次路由,最终签名有效的URL还是首页URL。
- 解决办法:
let signUrl = ''; signUrl; function getOs() { if (/(iPhone|iPad|iPod|iOS)/i.test(window.navigator.appVersion)) { return 'IOS' } else { return 'ANDROID' } } // 由于项目是基于所有的页面都需要分享,因此每个页面都进行配置是不切实际的,因此我们希望在vue的路由守卫去完成,beforeEach守卫 // 会导致页面申请签名的时候还是上一个页面,但是到了新页面又没有注册签名,导致invalid signature router.afterEach((to, from, next) => { setTimeout(async () => { if (getOs() === 'IOS') { if (window.entryUrl === '' || window.entryUrl === undefined) { window.entryUrl = window.location.href } signUrl = window.entryUrl } else { // 安卓机 ${project.context}指的是项目的名称 signUrl = `${window.location.origin}/${project.context}${to.fullpath}` } await config({ signUrl: signUrl }) }, 1000) })
-
- require subscribe ----- 这个问题要关注相对应的公众号;
- 当你确保签名算法以及url地址正确后,那大部分都是后台环境的问题,遇到错误之后与后台人员协商一步步找问题,祝好运~~~
- 开启debug测试得出,IOS手机:先报错:the permission value is offline verifying,在弹出config:ok,官方给出的解决方案是
这个错误是因为config没有正确执行,或者是调用的JSAPI没有传入config的jsApiList参数中。建议按如下顺序检查: 1、确认config正确通过 2、如果是在页面加载好时就调用了JSAPI,则必须写在wx.ready的回调中 3、确认config的jsApiList参数包含了这个JSAPI
但是我的代码完全是按照这种方式写的,所以官网没有解决我的问题,所以在查看了其他文档之后,决定采用了定时器,完美解决.
setTimeout(()=>{ wx.ready(function(){ wx.hideAllNonBaseMenuItem() }) },300)
- 遇到问题后会持续补充......
北栀女孩儿