微信小程序生成分享海报

4,744 阅读7分钟

在小程序开发中,生成海报可谓是个高频需求,这里我总结了下常见的步骤:

  • 主要有文字及背景绘制,用户头像和小程序二维码
  • 注:该案例实现基于mpvue框架,如果使用小程序自有框架或者uniapp等框架需要做少量调整

image.png

资源准备

小程序二维码

在生成海报之前首先要获取小程序二维码,这通常是服务端生成,在这个例子中我们就采用云开发来代替。 云开发的好处就是 FAAS = Functions as a Service , 可以绕开繁琐的后端搭建,让开发专注业务逻辑(对于前端而言则又向全干工程师迈进了一步)

引用大帅老师的一张图,在云开发的模式下,以下传统部署均可省略 image.png

developers.weixin.qq.com/miniprogram…

  • 接口A,可以接受 pages/index/index?param=128byte (10万次)?userid=xxx&from=123213
  • 接口C,官方已经不推荐使用了,方形不建议使用了。
  • 接口B,必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index 根路径前不要 填加 / ,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面

云函数代码如下: getMpCode/index.js

// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数

exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  try {
  //微信官方接口,生成小程序码 (这里为展示采用接口A,实际项目推荐接口B)
    const result = await cloud.openapi.wxacode.get({
        "path": 'page/index/index',
        "width": 430,
        "isHyaline":true
      })
    return result
  } catch (err) {
    return err
  }
}

调用云函数获取二维码:

//用promise封装主要是为了之后在使用时避免发生回调地狱(回调中嵌套回调,层数太深,可读性可维护性差)
 getQrcode(){
    return new Promise((resolve,reject)=>{
         wx.cloud.callFunction({
          name:"getMpCode",
          //获取的数据是buffer类型,需要加上文件头
          success:(res)=>{
            const src="data:image/jpg;base64,"+ wx.arrayBufferToBase64(res.result.buffer);
            resolve(src)
          },
          fail:(err)=>{
            reject(err)
          }
        });  
    })
  },

用户头像

获取用户头像需要用户授权,我这边的做法是将获取到的用户信息保存在本地,绘制海报时如果判断本地无用户头像信息则会弹框引导用户授权。

  //判断本地是否有用户头像信息
  checkUserInfo(){
      const userInfoProfile = wx.getStorageSync("userProfile");
      if(!userInfoProfile){
        return false
      }
      return true
  },
  
  //绘制海报时先判断是否有头像信息
   drawPoster(){
    const self=this;
    if(!self.checkUserInfo()){
      //如果不存在用户信息,则需要弹框引导用户授权获取
      return wx.showModal({
            title: '提示',
            content: '请先授权获取头像信息',
            async success (res) {
              if (res.confirm) {
                //用户授权后获取用户信息
                await self.getUserprofile()
                self.drawPoster();
              } else if (res.cancel) {
              }
            }
          })
    };
    //...
   }
   
  //获取用户信息
  getUserprofile(){
    return new Promise((resolve,reject)=>{
          //该接口需要按钮触发,不能直接在生命周期中使用
          wx.getUserProfile({
          desc:'获取你的昵称,头像',
          success:res=>{
          //获取成功后存入本地
              wx.setStorage({
                  key: "userProfile",
                  data: res.userInfo
              });
              resolve()
          },
          fail: (res)=>{
            console.log(`fail`,res)
            reject()
          }
        })

    })
  },
  
  //从本地获取用户头像
   getHeadImage(){
    return new Promise((resolve,reject)=>{
        //从storage中获取
        const userProfile=wx.getStorageSync(`userProfile`);
        if(!userProfile){
          return reject()
        }
        let {avatarUrl=""}=userProfile;
        resolve(avatarUrl)
     })
   },

image.png image.png

背景图

这里采用的是将图片事先存入云存储中,然后再调用 wx.getImageInfo 将网络图片下载到本地以备后续作图所用

  getBgImg(){
    const self=this
    return new Promise((resolve,reject)=>{
      //获取云存储中的图片
      self.getImgInfo('cloud://cloud1-8gp38tt58f1cad0e.636c-cloud1-8gp38tt58f1cad0e-1306912889/mdata/img/weiwuxian2.jpg').then(res=>{
        console.log(`cloudImg`,res)
        //{errMsg: "getImageInfo:ok", width: 794, height: 794, type: "jpeg", orientation: "up",path: "http://tmp/wsz3Rlfyo5yj279d60b79b9c747d19a192f79e7e9cb8.jpg",type:"jpeg",width:794}
        if(res && res.path){
          resolve(res.path)
        }else{
          reject()
        }
      })
    })
  },
  
  getImgInfo(src){
    return new Promise((resolve, reject) => {
      wx.getImageInfo({
        src: src,
        success: resolve,
        fail: reject
      })
    })
  },

画布的样式

由于在获取临时路劲保存图片的时候用一倍的canvas保存的图片会很模糊,我们需要对canvas画布进行多倍处理,可以以像素比为倍率,这样比较好处理,这里用的是像素比,具体如下

结构样式

   
<template>
   <view class="sharePoster">
     <view>
        <view class="canvasWrap">
          <canvas id="myCanvas" type="2d" style="height:450px;width:300px;"></canvas>
          <img :src="poster" class="poster-img" style="height:450px;width:300px; margin-top:20rpx;">
        </view>
        <button type="primary" style="width:500rpx" @click="drawPoster">绘制海报</button>
        <button type="primary" @click="btnSavePoster" style="width:500rpx;margin-top:20rpx">保存海报</button>
        
     </view>
   </view>
</template>

    <style scoped lang="less">
     .canvasWrap{
       display:flex;
       flex-direction:column;
       align-items:center;
       padding-bottom:20rpx;
       #myCanvas{
         position:absolute;
         left:-1000px; //canvas隐藏在可视区外
       }
     }
    </style>

画布适配

 onReady(){
    this.drawPoster();
 },

drawPoster(){
    const query = wx.createSelectorQuery()
    const self=this;
    ///...用户授权获取头像信息部分
    //获取canvas实例 和 上下文对象
    query.select('#myCanvas')
      .fields({ node: true, size: true })
      .exec(async (res) => {
        self.canvas = res[0].node
        self.ctx = self.canvas.getContext('2d')
        //屏幕像素比
        const dpr = wx.getSystemInfoSync().pixelRatio
        self.canvas.width = res[0].width * dpr
        self.canvas.height = res[0].height * dpr
        //画布放大后绘制比例也相应放大,这样绘图时就能按原尺寸计算
        self.ctx.scale(dpr, dpr)
        //...绘制背景 文字, 获取头像和二维码,绘制头像和二维码
      })
  },

绘制背景和文字

drawPoster(){
    const query = wx.createSelectorQuery()
    const self=this;
    //...判断本地信息,引导用户授权获取头像部分

    query.select('#myCanvas')
      .fields({ node: true, size: true })
      .exec(async (res) => {
        //...画布适配部分
        //背景
        self.ctx.fillStyle='#F5F5F5'
        self.ctx.fillRect(0,0,300,450)
        self.ctx.fillStyle='pink'
        self.ctx.fillRect(0, 0, 300, 300)
        
        //文字
        self.ctx.textBaseline='top'
        self.ctx.textAlign='left'
        self.ctx.fillStyle='#000'
        self.ctx.fontSize=120+'px'
        self.ctx.fillText("我正在掘金写文章 快来帮我点赞吧!", 20, 320)
        //绘制分割线
        self.ctx.moveTo(20, 340)
        self.ctx.lineTo(280, 340)
        self.ctx.strokeStyle = '#333'
        self.ctx.stroke()
        //...获取头像和二维码,绘制头像和二维码,转化成图片
  })
}

1632141849(1).png

绘制头像,二维码和背景图

获取头像,二维码和背景图

前文已叙,代码如下

  drawPoster(){
    const query = wx.createSelectorQuery()
    const self=this;
    //...判断本地信息,引导用户授权获取头像部分

    query.select('#myCanvas')
      .fields({ node: true, size: true })
      .exec(async (res) => {
        //...画布适配部分
        //...背景及文字绘制部分
        wx.showLoading({
          title: '海报生成中'
        });
        //获取头像和二维码和背景图
        const result=await Promise.all([self.getHeadImage(),self.getQrcode(),self.getBgImg()]);
        console.log(result) 
        //["https://thirdwx.qlogo.cn/mmopen/vi_32/Al8jy0dq1soJ…OiczAdv0PBXCt5erf1WZU90csaMsXjR3ERib9BIFJskTQ/132", "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAa4A…ra2vrIXKqra2tra2th8j/Cwh3EyS5PLNGAAAAAElFTkSuQmCC", "http://tmp/wsz3Rlfyo5yj279d60b79b9c747d19a192f79e7e9cb8.jpg"]
        //...绘制头像和二维码,转化成图片
    })
   }
   
   //获取用户头像
   getHeadImage(){
    return new Promise((resolve,reject)=>{
        //从storage中获取
        const userProfile=wx.getStorageSync(`userProfile`);
        if(!userProfile){
          return reject()
        }
        let {avatarUrl=""}=userProfile;
        resolve(avatarUrl)
     })
   },
   
   //获取小程序二维码
   getQrcode(){
    return new Promise((resolve,reject)=>{
         wx.cloud.callFunction({
          name:"getMpCode",
          success:(res)=>{
            const src="data:image/jpg;base64,"+ wx.arrayBufferToBase64(res.result.buffer);
            console.log(`scr`,src)
            resolve(src)
          },
          fail:(err)=>{
            reject(err)
          }
        });  
      })
   },
   //获取背景图
   getBgImg(){
    const self=this
    return new Promise((resolve,reject)=>{
    //获取云存储中的图片
      self.getImgInfo('cloud://cloud1-8gp38tt58f1cad0e.636c-cloud1-8gp38tt58f1cad0e-1306912889/mdata/img/weiwuxian2.jpg').then(res=>{
        console.log(`cloudImg`,res)
        //{errMsg: "getImageInfo:ok", width: 794, height: 794, type: "jpeg", orientation: "up",path: "http://tmp/wsz3Rlfyo5yj279d60b79b9c747d19a192f79e7e9cb8.jpg",type:"jpeg",width:794}
        if(res && res.path){
          resolve(res.path)
        }else{
          reject()
        }
      })
    })
  },

绘制头像,二维码和背景图

  drawPoster(){
    const query = wx.createSelectorQuery()
    const self=this;
    //...判断本地信息,引导用户授权获取头像部分

    query.select('#myCanvas')
      .fields({ node: true, size: true })
      .exec(async (res) => {
        //...画布适配部分
        //...背景及文字绘制部分
        wx.showLoading({
          title: '海报生成中'
        });
        //获取头像和二维码和背景图
       const result=await Promise.all([self.getHeadImage(),self.getQrcode(),self.getBgImg()]);
        //...绘制头像和二维码和背景图
        await Promise.all([self.drawHeadImg(result[0])], self.drawQRcode2(result[1]),self.drawBgImg(result[2]))
        wx.hideLoading();
      })
   }
   //绘制头像
   drawHeadImg(img){
    const self=this
    return new Promise((resolve,reject)=>{
       //头像img因为是网络图片,一般先要 通过wx.getImageInfo到本地(其实经过我的测试,这步省略也不会报错)
        self.getImgInfo(img).then(res=>{
            let image=self.canvas.createImage();
            image.src=res.path;
            image.onload=()=>{
              self.ctx.save()
              self.ctx.fillStyle='#fff'
              //因为要绘制圆形头像 先作出一个圆形裁切区域,然后再绘制矩形的头像图片,最终效果就是圆形头像
              self.ctx.beginPath()
              self.ctx.arc(70, 400, 50, 0, 2 * Math.PI)
              self.ctx.clip()
              self.ctx.drawImage(image, 20, 350,100,100);
              self.ctx.restore()
              resolve()
            }
        })
     })
   },
   
   getImgInfo(src){
    return new Promise((resolve, reject) => {
      wx.getImageInfo({
        src: src,
        success: resolve,
        fail: reject
      })
    })
  },
  
  //绘制二维码图像
  drawQRcode2(bufferData){
    const self=this;
    return new Promise((resolve,reject)=>{
       let image=self.canvas.createImage();
            //加载buffer数据
            image.src=bufferData;
            image.onload=()=>{
              self.ctx.drawImage(image, 160, 350,100,100);
              resolve()
            }
    })
  },
  //绘制背景图
 drawBgImg(src){
    const self=this;
    return new Promise((resolve,reject)=>{
       let image=self.canvas.createImage();
            image.src=src;
            image.onload=()=>{
              self.ctx.drawImage(image, 0, 0, 300, 300);
              resolve()
            }
    })
  },
  

将canvas图像转化成图片

  drawPoster(){
    const query = wx.createSelectorQuery()
    const self=this;
    //...判断本地信息,引导用户授权获取头像部分

    query.select('#myCanvas')
      .fields({ node: true, size: true })
      .exec(async (res) => {
        //...画布适配部分
        //...背景及文字绘制部分
        wx.showLoading({
          title: '海报生成中'
        });
        //获取头像和二维码和背景图
        const result=await Promise.all([self.getHeadImage(),self.getQrcode(),self.getBgImg()]);
        //...绘制头像和二维码和背景图
        await Promise.all([self.drawHeadImg(result[0])], self.drawQRcode2(result[1]),self.drawBgImg(result[2]))
        wx.hideLoading();
        //将canvas转化成临时路径,用imge加载生成图片呈现在页面
        self.canvas2Img()
        
      })
   }
   
   canvas2Img(){
        const self=this
        setTimeout(() => {
          //将画布保存成临时路径 供展示
          wx.canvasToTempFilePath({
            //因为是2d所以需要传入整个canvas节点
            canvas:self.canvas,
            success: (res) => {
              //将路径保存下来通过image在页面上展示
              self.poster = res.tempFilePath;
            },
            fail:(res)=>{
              console.log(`fail`,res)
            }
          },self)
        }, 200)
   }

image.png

触发按钮将海报保存至本地

  btnSavePoster(){
    const self=this;
    //将本地路径的图片保存至相册
    wx.saveImageToPhotosAlbum({
        filePath: self.poster,
        success: (result) => {
          wx.showToast({
            title: '海报已保存,快去分享给好友吧。',
            icon: 'none'
          })
        },
        //图片保存至相册需要用户授权,如果保存失败拉起授权弹框
        fail:(err)=>{
          console.log(err)
            if(err.errMsg === "saveImageToPhotosAlbum:fail:auth denied" || err.errMsg === "saveImageToPhotosAlbum:fail auth deny" || err.errMsg === "saveImageToPhotosAlbum:fail authorize no response") {
                wx.showModal({
                  title: '提示',
                  content: '需要您授权保存相册',
                  showCancel: false,
                  success: modalSuccess => {
                  wx.openSetting({
                    success(settingdata) {
                      if (settingdata.authSetting['scope.writePhotosAlbum']) {
                                console.log('获取权限成功,给出再次点击图片保存到相册的提示。')
                              }else {
                                console.log('获取权限失败,给出不给权限就无法正常使用的提示')
                              }
                          }
                      })
                    }
                  })
            }
        }
    })
  },