小程序海报分享之踩坑日记

        最近项目需要添加小程序海报分享的功能,也就是可以生成商品图片和跳转商品详情页的小程序码(也叫菊花码),然后用户点击保存图片后,添加到手机相册,用户可以直接把已保存的图片发出来,其他人可以通过长按小程序码直接跳转到商品详情页。看起来比较普通的一个需求,上手做起来之后才会发现有各种各样的坑,下面就来看看遇到了那些坑吧。

      一开始肯定是直接文档走起,小程序获取小程序码,刚开始还以为这个小程序码都是在前端生成的,文档一打开,直接看到接口getWXACodeUnlimit下面写着“本接口应在后端服务器调用”,因为生成小程序码需要通过接口传递参数access_token,然后获得access_token之前需要调用另外的接口getAccessToken得到access_token,这里又需要传appid和secret,这些私密字段只能在后台进行传递,那么小程序码就是后台直接返回的了。话不多说,直接上代码,(ps:我们项目是基于mpvue开发的小程序),以下是.vue文件里的template部分代码:

第一步,获取小程序码代码如下:

      //生成海报弹框
      getShareCode(){
        //分享弹出框隐藏
        this.goodSharePopShow = false;
        //请求生成小程序码的接口
        let self = this;
        let layer = this.$refs.layer;
        layer.open({

        });
        this.service.httpRequest({
          methods: "post",
          url: '/XXX-XXX/WXACodeUnlimit/api/getWXACodeUnlimit',
          data: {
            page:'pages/goodDetail/main',
            scene:self.goodsId
          }
        }).then(function (data) {
          if(data.success){
            layer.close();
             //海报弹框显示
            self.shareCodeShow = true;
            self.shareCodeUrl = data.img;
            //获得分享海报画布
            self.getShareCanvas();
          }
        }).catch(function (data) {
          self.$refs.loadbox.show_reset({
            reStart: function () {
              self.init();
            }
          });
        });
      },

url部分的路径是我们后台通过调用小程序文档调用生成小程序码的路径,this.service.httpRequest也是项目封装好的异步请求,这部分代码的写法需要根据自己实际项目的请求来写。

坑1、返回的小程序码不能是base64格式,后台调用微信接口返回的值是,如果调用成功,会直接返回图片二进制内容,如果请求失败,会返回 JSON 格式的数据。一开始返回的是base64格式,在微信开发者工具上是可以显示小程序码的,但是在真机上不行,后来通过百度发现手机上小程序不支持base64格式,那么就只能后台把接口的返回格式换一下咯,通过沟通后,后台会返回小程序码的网络路径。后台修改接口后的代码如下:

      //生成海报弹框
      getShareCode(){
        //分享弹出框隐藏
        this.goodSharePopShow = false;
        //请求生成小程序码的接口
        let self = this;
        let layer = this.$refs.layer;
        layer.open({

        });
        this.service.httpRequest({
          methods: "post",
          url: '/yggjshop-front/WXACodeUnlimit/api/getWXACodeUnlimitUrl',
          data: {
            page:'pages/goodDetail/main',
            scene:self.goodsId
          }
        }).then(function (data) {
          if(data.success){
            layer.close();
             //海报弹框显示
            self.shareCodeShow = true;
            self.shareCodeUrl = self.imgUrl + data.imgurl;
            //获得分享海报画布
            self.getShareCanvas();
          }
        }).catch(function (data) {
          self.$refs.loadbox.show_reset({
            reStart: function () {
              self.init();
            }
          });
        });
      },

可以看到接口名字也从getWXACodeUnlimit改成了getWXACodeUnlimitUrl,然后返回的值也从之前的base64改成了网络路径,这样第一部生成小程序码算是由后台的配合搞定了。

第二步,小程序码生成了之后,自然是需要在页面展示的,图文展示并保存图片,就会想到此处应该需要canvas来解决,因为并不是一张单纯的图片,还有相应的商品名称、价格等信息。小程序刚好也有使用canvas的api,那么就需要绘制画布了,画布绘制代码如下:

      //获得分享海报画布
      getShareCanvas(){
        var that = this;
        let layer = this.$refs.layer;
        //2. canvas绘制文字和图片
        var ctx = wx.createCanvasContext('shareCanvas');
        //商品图片
        var goodsImagePath = that.imgUrl + that.goodsImage;
        //这里是把页面上的数据写入到画布里,具体的坐标需要自行调整
        ctx.drawImage(goodsImagePath, 0, 0, 300, 300);
        ctx.setFontSize(16)
        ctx.fillText(that.goodsName, 15, 330);
        if(that.pointEnable!=1&&that.subGoodsPrice!=0){
          ctx.fillText('¥', 15, 350).setFillStyle('#ffab00');
          ctx.fillText(that.chooseTotalPrice, 35, 350);
        }
        if(that.pointEnable!=0){
          ctx.drawImage(that.staticImgUrl+that.weimajifen, 15, 350, 15, 15);
          ctx.fillText(that.chooseTotalPoint+'积分', 35, 363);
        }
        
        ctx.drawImage(that.shareCodeUrl, 180, 305, 100, 100);

        ctx.draw(false, function() {
          //canvas画布转成图片
          wx.canvasToTempFilePath({
            x: 0,
            y: 0,
            width: 300,
            height: 450,
            destWidth: 300,
            destHeight: 450,
            canvasId: 'shareCanvas',
            fileType: 'jpg',
            quality:1,
            success: function (res) {
              console.log(res);
              that.shareImgSrc = res.tempFilePath;
              if (!res.tempFilePath) {
                wx.showModal({
                  title: '提示',
                  content: '图片绘制中,请稍后重试',
                  showCancel: false
                })
              }
            },
            fail: function (res) {
              console.log(res)
            }
          })
        });
      }

画布绘制完成后,需要调用canvasToTempFilePath把当前画布指定区域的内容导出生成指定大小的图片。在draw()回调里调用该方法才能保证图片导出成功。

坑2、网路图片真机上不能直接在画布里显示。上述代码在微信开发者工具上面运行,可以出现图片和文字信息,都没毛病,但是到了真机上,ios和安卓都是只出现了文字信息,商品详情图片和小程序码图片都没显示出来,但是相关域名都配置上了。然后打印看到的是地址返回不一样;真机返回的是:wxfile://tmp_ef7d896c70dbbba1bedbb50c83f6ee1f.png。开发工具返回:"http://tmp/wxeec88be8ab307f36.o6zAJs3jHQ-GYaUX35iOhX45rqvA.HLGQBJZODyTK7e6afe93405e336a7ee85d72931f7efd.png"。好在项目里面有个积分图片是在本地的ctx.drawImage(that.staticImgUrl+that.weimajifen, 15, 350, 15, 15)这个图片在真机上显示了,然后想到可能是网络图片是需要保存到本地的,看了下文档有downloadFile的api,那就先保存到本地再显示咯,修改后的代码如下:

      //生成海报弹框
      getShareCode(){
        //分享弹出框隐藏
        this.goodSharePopShow = false;
        //请求生成小程序码的接口
        let self = this;
        let layer = this.$refs.layer;
        layer.open({

        });
        this.service.httpRequest({
          methods: "post",
          url: '/yggjshop-front/WXACodeUnlimit/api/getWXACodeUnlimitUrl',
          data: {
            page:'pages/goodDetail/main',
            scene:self.goodsId
          }
        }).then(function (data) {
          if(data.success){
            layer.close();
            //海报弹框显示
            self.shareCodeShow = true;
            //商品图片
            wx.downloadFile({
              url: self.imgUrl + self.goodsImage,
              success(resImg) {
                // 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
                if (resImg.statusCode === 200) {
                  //小程序码图片
                  wx.downloadFile({
                    url: self.imgUrl + data.imgurl,
                    success(resCode) {
                      // 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
                      if (resCode.statusCode === 200) {
                        //获得分享海报画布
                        self.getShareCanvas(resImg.tempFilePath,resCode.tempFilePath);
                      }
                    }
                  })
                }
              }
            })
          }
        }).catch(function (data) {
          self.$refs.loadbox.show_reset({
            reStart: function () {
              self.init();
            }
          });
        });
      },

       可以看到,在获取了小程序码的时候调用wx.downloadFile将生产的码的网络路径保存在本地,然后由于我们还有一个商品详情页的图片,开始尝试了两张图片分别wx.downloadFile,发现没用,所以就只能在一个wx.downloadFile的回调里面再嵌套另一张图片的wx.downloadFile了,不过这样的回调嵌套不能过多,会影响图片显示的速度,个人感觉两张图片就已经够多了,不宜再保存多的图片。获得分享海报画布的防范getShareCanvas里面多了两个参数,这就是两张图片保存之后的路径。

      修改之后的代码在真机上可以显示出图片了,本以为这样就可以进行下一步的保存操作了,哪知道又来一个坑。

坑3、苹果和安卓机生成的画布背景色不一样,ios显示生成的图片如下:

小程序海报分享之踩坑日记_第1张图片

安卓生成的图片显示如下:

小程序海报分享之踩坑日记_第2张图片

可以看到在安卓里面canvas生成的图片背景是黑色的!?在小程序论坛里面也没找到官方关于这个问题的回复,就只能想其他办法了,如果给canvas元素加一个background:#fff行不行呢,试了一下好像不行。那就在绘制画布的时候先画一个白色的背景呢,这个方法是可行的,在获得分享海报画布getShareCanvas方法里添加如下代码:

 //设置底部白色背景
 ctx.save()
 ctx.setFillStyle('#ffffff')
 ctx.fillRect(0,0, 300, 450)
 ctx.restore()

如此一来,安卓机生成图片的背景色也变成白色了,可以继续往下面做保存图片的操作了。

第三步,保存生成的canvas画布,直接调用小程序保存图片到相册的api---saveImageToPhotosAlbum,保存图片的代码如下:

      //保存至相册
      saveImageToPhotosAlbum(){
        var that = this;
        let layer = this.$refs.layer;
        //当用户点击保存图片时,将图片保存到相册
        wx.saveImageToPhotosAlbum({
          filePath: that.shareImgSrc,
          success(res) {
            console.log(res);
            layer.open({
              type: 1,
              content: "保存成功",
              time: 2,
              imgurl: "success.png"
            });
             //海报弹框隐藏
            that.shareCodeShow = false;
          },
          fail: function (res) {
            console.log(res)
            if (res.errMsg === "saveImageToPhotosAlbum:fail:auth denied") {
              console.log("打开设置窗口");
              wx.openSetting({
                success(settingdata) {
                  console.log(settingdata)
                  if (settingdata.authSetting["scope.writePhotosAlbum"]) {
                    console.log("获取权限成功,再次点击图片保存到相册")
                  } else {
                    console.log("获取权限失败")
                  }
                }
              })
            }
          }
        })
      },

调用前需要用户授权 scope.writePhotosAlbum,然后在真机上调试,新的坑又出现了。

坑4、调用户授权的时候弹框询问是否允许访问手机相册,如果点击确定,那么ios和安卓机都是没有问题可以正常保存图片的,但是,如果点击取消后,在ios上再次点击保存图片会自动跳到如图所示的操作界面:

小程序海报分享之踩坑日记_第3张图片

在安卓机上再次点击保存图片没有任何的作用,在询问弹框点击取消的时候,会saveImageToPhotosAlbum的fail回调,打印出result会发现,开发者工具和ios的fail回调返回的errMsg都是:saveImageToPhotosAlbum:fail auth deny,而安卓的errMsg返回的却是:saveImageToPhotosAlbum:fail:auth denied,难道是因为fail回调里的if判断改成saveImageToPhotosAlbum:fail auth deny || saveImageToPhotosAlbum:fail:auth denied就行了吗,尝试之后发现并不行,虽然进入了fail回调但是不会调用wx.openSetting,额。。。这可能也是ios和安卓的处理机制不一样吧。所以就会出现文章开头.vue文件里面写了两个按钮:

 
 
 
 

一个按钮负责调用wx.openSetting,另一按钮负责保存图片,然后在进页面的时候调用wx.getSetting检测授权与否,从而控制两个按钮的显示和隐藏,为了更好的体验两个按钮的内容都叫“保存图片”,严格意义上讲有一个按钮应该叫“去授权”,检测授权的方法代码如下:

      //检测授权
      checkUserSetting(){
        wx.getSetting({
          success: res => {
            if (res.authSetting['scope.writePhotosAlbum']){
              // 已经授权
              this.openSettingShow = false;
              this.saveBtnShow = true;
            }
          }
        })
      },

如果没授权就需要打开授权的页面,代码如下:

      //打开授权
      openSetting(){
        wx.openSetting({
          success(settingdata) {
            console.log(settingdata)
            if (settingdata.authSetting["scope.writePhotosAlbum"]) {
              console.log("获取权限成功,再次点击图片保存到相册");
            } else {
              console.log("获取权限失败");
            }
          }
        })
      },

把这些都修改好了之后,ios和安卓看起来算是一样的效果,以为可以提测了,然后在另外一个同事的安卓机点击的时候会出现大概30分之一的几率有时候点出海报后,文字部分不见了或者部分背景是黑色,又是尼玛安卓机的奇葩bug。

坑5、安卓机生成海报时,极少数情况下会出现文字部分不见或者部分背景是黑色。

之前背景黑色已经被我用画布背景涂白解决了,为什么现在又有的时候会出现呢?文字不见了的情况应该是文字也被写成白色了导致看不见。突然想到是不是之前在网上找安卓黑色背景问题的时候,有的人说CanvasContext.draw,有延迟需要设置一个setTimeout,当时我这么做了但是没解决掉坑3的问题,那么现在是不是这个原因导致坑5这个时现时不现的问题呢?试一试吧:

      ctx.draw(false,setTimeout(function(){
          //canvas画布转成图片
          wx.canvasToTempFilePath({
            canvasId: 'shareCanvas',
            fileType: 'jpg',
            quality:1,
            success: function (res) {
              console.log(res);
              that.shareImgSrc = res.tempFilePath;
              layer.close();
              if (!res.tempFilePath) {
                wx.showModal({
                  title: '提示',
                  content: '图片绘制中,请稍后重试',
                  showCancel: false
                })
              }
            },
            fail: function (res) {
              console.log(res)
            }
          })
        },300));

就在draw()的回调里加了一个300毫秒的延时,相当于用canvasToTempFilePath把canvas画布转成图片时往后延迟了300毫秒,先让canvas画布绘制完全再转图片。可能是老天看我踩得坑够多了,我是抱着试一试的心态改的,后面自己做测试的时候没有出现坑5这种极偶然出现的bug了。

      这里还有一个跟业务有关的坑,那就是用户从分享出去的小程序码扫码进入或者长按识别进入跳转的是详情页,这个时候如果用户想回到首页会发现回不去,因为小程序默认进的第一个页面就是首页,有人说加一个悬浮的回到首页的按钮就行了撒,嗯,很多小程序都是这么做的,不过我们的客户就很坚决,要做不一样的,页面要简介按钮越少越好,那么就做一个从分享即进入的判断呗,从扫码进入的加一个悬浮的回到首页按钮,正常操作进入详情页的不要按钮(因为可以直接点回退返回)。

坑6、扫码分享的进入可以通过传入的scene来确定是否添加返回首页按钮,普通分享的却不能判断。。。

      扫码分享的判断代码如下:

    //小程序
    onLoad(query) {
      // scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene
      this.scene = decodeURIComponent(query.scene);
    },

通过onLoad把获取到的scene赋值给页面data定义的scene参数,然后在methods里的页面初始化方法init()里根据scene的存在与否控制返回首页按钮的显示和隐藏:

      init() {
        //初始化数据
        this.initData();
        if (this.$store.state.userInfo.wmid != '') {
          this.can_buy = true
        }
        let parameter = this.getCurrentPageUrlWithArgs();
        if(this.scene!='' && this.scene != "undefined"){
          this.goodsId = this.scene;
          this.ifWxScan = true//从分享的扫码进来才会出现返回首页按钮
        }else{
          this.goodsId = parameter.goodDetail;
          console.log(parameter);
          if(Object.getOwnPropertyNames(parameter).length==2){//从普通分享进入parameter对象包括goodDetail会有两个属性值
            this.ifWxScan = true//从分享的扫码进来才会出现返回首页按钮,这里是从普通分享进来的情况
          }else{
            this.ifWxScan = false//从分享的扫码进来才会出现返回首页按钮
          }
        }
        //获取商品信息和规格
        this.getGoodsinfodata(this.goodsId);
        //获取商品详情
        this.getGoodsBodydata(this.goodsId);
        //获取用户评价
        this.getUserEvaluatedata(this.goodsId);
        //请求购物车列表接口获得购物车商品总数量
        this.getCartList();
        //检查授权
        this.checkUserSetting();
      },

      扫码分享进入的好判断,那普通分享的呢?或者通过小程序右上角三个点点击出现的“转发”呢?普通分享代码如下:

   //小程序自带普通分享
    onShareAppMessage(){
      return {
        title: '威马积分商城',
        desc: this.goodsName,
        path: '/pages/goodDetail/main?goodDetail='+this.goodsId,
        imageUrl:this.imgUrl + this.goodsImage
      }
    },

      开始我想的是在path里面也添加一个scene参数来判断,比如:

path: '/pages/goodDetail/main?goodDetail='+this.goodsId +'scene=1',

    这样就可以取scene的值来判断了,在手机上这个分享出去的东西点击进入之后,详情页一篇空白,看来多加参数上去是不行的,后来我把手机小程序调试模式打开,发现通过分享链接进来的页面参数和直接进页面的参数不一样,通过分享链接进来的页面参数如(左)图,普通进入页面参数如(右)图:

小程序海报分享之踩坑日记_第4张图片小程序海报分享之踩坑日记_第5张图片

可以看到一个有两个参数一个只有一个参数,我们通过path多传一个参数不行,自己却可以生成一个uuid的参数,好吧,反正有不一样的地方那就可以进行判断了,我是通过判断传参数的对象长度Object.getOwnPropertyNames(parameter).length来确定是同普通分享进入页面的,即传了2个参数,其他情况就是直接进入页面,不需要添加返回首页的按钮了。

      至此,关于小程序海报分享的几个大坑总算被我踩完了,万万没想到一个这样的功能却有这么多千奇百怪的坑,有小程序自身的也有业务的,应该还有一些其他的小坑我没有总结,至少总结出来的这些坑的话,相信其他人应该也会碰到。

    最后把这次海报分享的有关代码一次性贴出来,vue文件