简述H5页面在手机浏览器实现微信分享

时间:2024-02-17 11:25:11

一、调研结果

  • 微信内置浏览器进行分享,只能监听微信自带的分享按钮,自定义分享的图标什么的,不可能主动触发分享,可以引用微信公众平台的自定义分享接口,也就是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接口列表
          });
    • 在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); });
  • 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页面元素,显示一个引导图,引导用户使用右上角的转发功能;

二、具体实现

  1. 实现的功能
    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;
    });
    View Code
  • 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);

三、参考文档

  1. https://www.cnblogs.com/backtozero/p/7064247.html
  2. 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
  3. 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)
  • 遇到问题后会持续补充......