一、調研結果
- 微信內置瀏覽器進行分享,只能監聽微信自帶的分享按鈕,自定義分享的圖標什么的,不可能主動觸發分享,可以引用微信公眾平台的自定義分享接口,也就是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)
- 遇到問題后會持續補充......