• 简述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接口列表
            });
      • 在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)
    • 遇到问题后会持续补充......

     
    北栀女孩儿
  • 相关阅读:
    个人作业二-举例分析流程图与活动图的区别与联系
    四则运算
    实验四 决策树算法及应用
    实验三 朴素贝叶斯算法及应用
    实验二 K-近邻算法及应用
    实验一 感知器及其应用
    实验三 面向对象分析与设计
    实验二 结构化分析与设计
    实验一 软件开发文档与工具的安装与使用
    ATM管理系统
  • 原文地址:https://www.cnblogs.com/wxh0929/p/14333878.html
Copyright © 2020-2023  润新知