小程序制作海报,支持一键生成多张

1. 前言

生成海报是小程序一项寻常普遍的低成本推广方式,在小程序中通过引导用户生成带有小程序二维码的海报发上票圈,来吸引更多的流量。

 

2. 需求分析

在与朋友圈类似的 带有文字描述 和 最多带有9张图片的 列表中,实现

1/ 使用云开发,生成二维码

2/ 点击图片,会根据图片的标签绘制对应模板的海报

3/ 将生成的海报图片进行预览,并提供“保存到本地", 增加用户拒绝授权保存图片到本地相册后的处理

4/ 增加了一键保存的功能:即一键将某个人发的多张朋友圈图片,按照顺序绘制成海报图片保存到本地, 并实时显示绘制进度 

 

3. 实现步骤

3.1  抽象封装canvas绘制组件

弱鸡的我一开始是drawImg, fillText, drawRect 一个接口一个接口分别去绘制图像,文字,矩形,但是扛不住需求变更的速度啊。在改了n个样式之后,卑微的我终于!!!决定把canvas绘制的逻辑抽取出来。

canvas绘制,当然离不开canvas页面标签,所以我们不如把它封装成一个组件 canvasdrawer,这个组件从外部接收一个Json,当接收到新的json时,开始绘制,json中每个item添加一个type,标志它是图像 / 文字 / 矩形,然后调用相应的封装好的绘制方法。

需要注意的是, 当绘制的图像是网路图片时,需要先拿到图片的临时路径才能来进行绘制。当海报参数中存在多张网络图片时,使用Promise.all 异步获取图片临时路径。

绘制完之后,调用 canvasToTempFile 生成临时图片链接,我们可以 triggerEvent 通知父组件该海报已绘制完成,并返回生成的海报的图片临时链接,用于实现预览或者本地保存

 

3.1.1 使用监听observer监听传入的json 是否改变,若改变了再进行绘制

properties: {
  // 传入的json
  painting: {
    type: Object,
    value: {
      view: []
    },
    // 监听传入的json 对象是否发生改变,决定是否需要重新绘制
    observer(newVal, oldVal) {
      // 判断当前有没有在进行绘制的对象
      if (!this.data.isPainting) {
        // 将对象转换成字符串再进行比较
        if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
          // 宽、高为必须的
          if (newVal && newVal.width && newVal.height) {
            this.setData({
              showCanvas: true,
              isPainting: true
            })
            this.readyPigment()
          }
        } else {
          // 如果对象没有发生改变,但是模式又不是“same”模式的话,就出触发事件
          if (newVal && newVal.mode !== 'same') {
            this.triggerEvent('getImage', {
              errMsg: 'canvasdrawer:samme params'
            })
          }
        }
      }
    }
  }
},

 

3.1.2  获取图片列表

// 获取图片列表
getImagesInfo(views) {

  // 1.将获取到的每张图片信息推送到imagelist中
  const imageList = []
  for (let i = 0; i < views.length; i++) {
    if (views[i].type === 'image') {
      imageList.push(this.getImageInfo(views[i].url))
    }
  }

  // 2.临时图片下载,使用promise.all 异步下载,每8张一个异步task
  const loadTask = []
  for (let i = 0; i < Math.ceil(imageList.length / 8); i++) {
    loadTask.push(new Promise((resolve, reject) => {
      Promise.all(imageList.splice(i * 8, 8)).then(res => {
        resolve(res)
      }).catch(res => {
        reject(res)
      })
    }))
  }

  // promise.all返回的res为多个promise返回的数组拼接成的数组,有所有的task返回
  Promise.all(loadTask).then(res => {
    let tempFileList = []
    for (let i = 0; i < res.length; i++) {
      tempFileList = tempFileList.concat(res[i])
    }
    this.setData({
      tempFileList
    })
    this.startPainting()
  })
},

 

3.1.3 若是网络图片,先拿到图片的临时链接

// 获取图片信息
getImageInfo(url) {
  return new Promise((resolve, reject) => {
    // 如果图片有缓存,则从缓存中读取
    if (this.cache[url]) {
      resolve(this.cache[url])
    } else {
      // 匹配图片链接地址是否匹配
      const objExp = new RegExp(/^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/)
      if (objExp.test(url)) {
        wx.getImageInfo({
          src: url,
          complete: res => {
            if (res.errMsg === 'getImageInfo:ok') {
              this.cache[url] = res.path
              resolve(res.path)
            } else {
              this.triggerEvent('getImage', {
                errMsg: 'canvasdrawer:download fail'
              })
              reject(new Error('getImageInfo fail'))
            }
          }
        })
      } else {
        this.cache[url] = url
        resolve(url)
      }
    }
  })
},

 

3.1.4 绘制完成后,使用wx.canvasToTempFilePath 将canvas转化成临时图片链接,并通知给父级组件

// 将图片保存到本地
saveImageToLocal() {
  const {
    width,
    height
  } = this.data
  wx.canvasToTempFilePath({
    x: 0,
    y: 0,
    width,
    height,
    canvasId: this.data.canvasId,
    complete: res => {
      if (res.errMsg === 'canvasToTempFilePath:ok') {
        this.setData({
          showCanvas: false,
          isPainting: false,
          tempFileList: []
        })
        this.triggerEvent('getImage', {
          tempFilePath: res.tempFilePath,
          errMsg: 'canvasdrawer:ok'
        })
      } else {
        this.triggerEvent('getImage', {
          errMsg: 'canvasdrawer:fail'
        })
      }
    }
  }, this)
}

 

3.2  获取海报参数

对一个有灵魂的海报来说 二维码 是必不可少的,其中,二维码的生成可通过两种方式实现:

1. 需要后台的协助:传输给后台相应的参数,由后台调官方接口去生成二维码,再返回给前端

2. 前端自给自足: 小程序官方增加了云开发,我们可以通过新增云函数中,在云函数中传入参数,生成二维码

注意:生成的二维码是buffer data,需要将其转化为图片链接


/* 获取云开发的小程序码 */
const getQrCode = (path, scene, dataType) => {
  return new Promise((resolve, reject) => {
    let param = {
      page: path,
      scene: scene || '',
      width: 350
    }
    if (!path) {
      reject('must have path')
      return
    }
    if (scene && scene.length > 32) {
      reject('scene\' length must less than 32')
      return
    }

    console.log('小程序码~云参数:', param)
    wx.cloud.callFunction({
      name: 'getNewQrCode',
      data: param
    }).then(res => {
      let data
      if (res.result.errCode == 0) {
        if (dataType == 'base64') {
          data = `data:image/jpeg;base64,${wx.arrayBufferToBase64(res.result.buffer)}`
        }
        if (dataType == 'buffer') {
          data = res.result.buffer
        }
        if (data) {
          resolve(data)
        } else {
          reject('unknow data type')
        }
      } else {
        reject(res.result.errMsg)
      }
    }).catch(err => {
      reject(err)
    })
  })
}

为了使小程序当前生命周期结束时,二维码仍然有效,我们可以调用小程序提供给我们的在用户存储目录写入文件的权限,将生成的二维码图片保存到用户存储目录中(wx.env.USER_DATA_PATH)

/* 图片临时缓存 */
const localQrImageUrl = (buffer) => {
  return new Promise((resolve, reject) => {
    const TMP_IMG_NAME = 'TMP_QR_CODE_';
    // 添加时间戳使之前生成的二维码图片不被覆盖
    const TIME_STAMP = new Date().getTime().toString().substr(-8);
    const filePath = `${wx.env.USER_DATA_PATH}/${TMP_IMG_NAME}${TIME_STAMP}.jpg`;
    // const filePath = `${wx.env.USER_DATA_PATH}/${TMP_IMG_NAME}.jpg`;
    // console.log('临时存储路径:', filePath)
    wx.getFileSystemManager().writeFile({
      filePath,
      data: buffer,
      encoding: 'binary',
      success() {
        // console.log('临时存储路径保存好了:', filePath)
        resolve(filePath)
      },
      fail() {
        wx.getFileSystemManager().rmdir({
          dirPath: `${wx.env.USER_DATA_PATH}/`,
          success() {
            wx.showModal({
              title: "温馨提示",
              content:
                "本地内存不足,已删除该小程序清除本地缓存,请再次点击生成海报"
            });
            reject(new Error("已清除本地缓存,请再次点击生成海报"));
          },
          fail() {
            console.log("删除本地缓存失败");
            // console.log('临时存储路径保存错了')
            reject(new Error("ERROR_BASE64SRC_WRITE"));
          }
        });
      }
    })
  })
}

 

3.3 动态改变海报模板的某些特定参数

比如说多个商品都用到了同一个海报模板,即图片,文字和形状的大小,位置都是一致的,但是图片的链接地址url, 文字的内容text 可能不一样,这个时候我们可以通过一个reqParams 请求参数去动态改变模板;

我们在定义海报模板时,给需要变更内容的字段添加上一个标识。然后在 reqParams 请求参数中携带 { 标识 : 更改后的值},即可实现变更。

/**
 * 动态改变海报模板的参数
 * 拼接示例:{width:, height:, desc:value}
 */
changePosterParams() {
  let descList = Object.keys(this.reqParam);
  descList.forEach(desc=>{
      let newTarget = this.reqParam[desc];
      if(desc == 'width'){
          this.poster['width'] = newTarget;
      }else if(desc == 'height'){
          this.poster['height'] = newTarget;
      }else{
          let target = this.getItemByKey(this.poster['views'], desc);
          if(target && target.url){
              target.url = newTarget;
          }
          else if(target && target.content){
              target.content = newTarget;
          }
      }
  })
}

/**
 * 在数组中获取到键desc的值为某一个值的对象
 * 注意:需确保键值只能唯一,否则匹配到一个最后一个匹配到的项,即后面的会覆盖前面的
 * @param {Array} arr 
 * @param {String} keyValue 
 */
getItemByKey(arr,keyValue){
  let targetItem=null;
  arr.forEach(item=>{
      if (item['desc'] == keyValue){
        targetItem=item;
      }
  });
  return targetItem;
}

 

3.4 保存图片到本地相册

当用户授权当然保存没问题,但是当用户拒绝授权怎么办嘞。

小程序现在升级了,再也不能静默再去获取授权了,需要由用户的点击操作才能调起设置页面,引导用户自己打开授权

需注意的是:当没有请求过授权时,是需要先去请求授权,设置页面才会出现该选项的(比如保存到本地相册的权限);

// 保存图片,有处理如果用户不授权的情况
Poster.prototype.trySaveImg = function(imgUrl, successCallback) {
  const _this = this;
  wx.getSetting({
    success(res){
      console.log("用户当前开启的权限情况", res.authSetting);
      // 拒绝过授权,则打开设置页面(设置界面只会出现小程序已经向用户请求过的权限,所以需先向用户请求一次)
      if(res.authSetting['scope.writePhotosAlbum'] === false){
        wx.openSetting({
          success(res){
            // 如果还是没有授权的话
            if(!res.authSetting['scope.writePhotosAlbum']){
              wx.showToast({
                title: '未开启保存权限',
                image: '/images/common/err_tip_icon.png',
                duration: 2000
              })
            }
          }
        });
      }
      // 没有请求过授权或者已授权
      else{
        if(successCallback){
          successCallback(imgUrl);
        }else{
          _this.saveImg(imgUrl);
        }
      }
    }
  })
}

当没有请求过授权或者已授权的情况下,就直接调起保存动作

 // 保存图片
 saveImg(imgUrl) {
  wx.saveImageToPhotosAlbum({
    filePath: imgUrl,
    success(res) {
      wx.showToast({
        title: '保存图片成功',
        image: '/images/common/icon_toast_success.jpg',
        duration: 2000
      })
    },
    fail(err){
      console.log(err)
      wx.showToast({
        title: '未开启保存权限',
        image: '/images/common/err_tip_icon.png',
        duration: 2000
      })
    }
  })
}

 

3.5 一键绘制多张海报

上面我在 3.1.4 步骤中提到生成成功后会通知父级组件,这里我们在父级组件中监听这个生成成功事件,并拿到生成的海报图片,可进行预览并保存在本地

// 当前海报绘制完成, 检测是否还有未完成绘制的海报
eventGetImage(event){
  const {
    tempFilePath,
    errMsg
  } = event.detail

  // 绘制成功
  if (errMsg === 'canvasdrawer:ok') {
    this.setData({
      shareImage: tempFilePath,
      isShowCanvas: false // 重新初始化canvas画报为false,等参数拼接完再去绘制下一张
    });
    this.saveImg(this.data.shareImage); // 保存该张海报图片
  } 

  // 当前海报绘制失败,增加失败图片序号到失败队列中,并绘制开始下一张
  else{
    console.log(errMsg);
    this.data.activeItemIndex++;
    this.data.failItemIndexList.push(this.data.activeItemIndex);
    this.startPainting();
  }
},

检测还有没有未完成的绘制任务

// 检查是否还有绘制任务
if(_this.data.activeItemIndex < _this.data.totalItemsLen-1){
  _this.data.activeItemIndex++;
  _this.startPainting();
}
// 绘制最后一张完成
else{
  _this.setData({
    isDoingSaveAll: false
  })
  wx.hideLoading();

  // 判断有没有绘制失败的
  if(_this.data.failItemIndexList.length > 0){
    let failItemIndexListString = _this.data.failItemIndexList.join(',')
    wx.showToast({
      title: "第"+failItemIndexListString+"张下载失败",
      image: '/images/common/err_tip_icon.png',
      duration: 2000
    });
  }else{
    wx.showToast({
      title: "下载成功",
      image: '/images/common/icon_toast_success.jpg',
      duration: 2000
    });
  }
}

 

4. 总结

github仓库完整代码

如果文章对你有帮助,麻烦点赞哦,一起走花路吧~

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值