关于前端:编程日历小程序对小程序云开发和生成分享海报的实践

40次阅读

共计 9264 个字符,预计需要花费 24 分钟才能阅读完成。

原文发表自博客:blog.zhangbing.site

1、起源

朋友圈晒的很多的一本日历书《了不起的程序员 2021》,我也买了,很厚,纸质书嘛,当初曾经很少看了,加上这是一本日历书,心愿是每天都关上看。可实际上的状况是,要么遗记看明天的内容,要么一口气看了好几天的内容,而后剩下几天又不看了。

起初《了不起的程序员 2021》在 Github 开源了。

于是乎!我就想做一个小程序,因为手机每天关上的频率太高了,碎片工夫也很多,加上小程序的不必装置用完即走的长处,使用方便,不会有压力感。

再加上本人还没有一款正儿八经的小程序作品,对当初很火的云开发也没怎么用过,特地是小程序云开发,他他到底用起来爽不爽呢?(很爽!)

于是乎!开干!

2、产品设计

这是最伤脑筋的局部,小程序到底要做成什么样,画个原型图?作为一个『资深』程序员,素来没正经画过原型和设计。不知所措,改用什么工具?尽管我晓得有 Sketch 这个神器,还很多在线设计工具,比方磨刀,但素来没用过啊,最初硬着头皮用磨刀画了画原型,很简陋的原型,就是线框图级别。

如下是我画的原型 ….

磨刀链接:https://modao.cc

<iframe src=”https://modao.cc/app/c9f6d109bce5857e3cb0cb5e1969b3942392e574/embed/v2″ allowTransparency=”true”
style=”width:640px;height:660px”
frameborder=”0″></iframe>

这个过程一直有新的想法,所以改来该去,产品设计花了好几天,学习怎么画原型,实现脑子里乌七八糟的各种想法。

在这个过程中我一直的给本人家需要,一度减少了什么历史上的明天、知乎日历等等各种内容,最初还是被本人 狠心 一一毙掉了,只留下纯正的编程日历内容。

小程序比《了不起的程序员 2021》更好的方面:

  1. 实在配图,《了不起的程序员 2021》的配图都是手绘图,看起来少了点那种味儿。。。
  2. 小程序反对珍藏、分享,这是纸质书先天不具备的
  3. 基于《了不起的程序员 2021》但不是齐全一样,我做了一些小小的批改或减少一些内容。

鉴于对产品和设计不善于,在此诚邀 UI、产品小伙伴,一起租一个团队,有机会一起做一些产品,让咱们的想法能落地,生根发芽。

3、开发

产品设计阶段和开发阶段占用的工夫比大略是 8:2 左右,有了原型开发很快,毕竟也没什么简单的货色。

上面重点说一下分享海报性能的实现吧。

3.1、抉择海报分享计划

在开发分享海报性能之前我也看了下网上大抵的计划,最初我抉择了微信小程序本人的扩大组件:wxml-to-canvas,小程序内通过动态模板和款式绘制 canvas,导出图片,可用于生成分享图等场景。

我为什么不必其余计划:

  • 手写 canvas,太麻烦
  • 后端生成前端获取,太麻烦,我这个小程序很简略没必要
  • 开源小程序海报组件,尝试过一个,感觉也不太好用,有些没文档用起来吃力

上图,是骡子是马拉进去遛遛,下图的的海报就是通过 wxml-to-canvas 动静绘制的。

3.2、引入 wxml-to-canvas 组件

wxml-to-canvas 的限度很多,第一次没教训的话感觉很难用,如果再让我做一次,我就快很多了。

官网的示例只单纯教你怎么生成海报,不足上下文和怎么整合进你的我的项目及逻辑,须要费一下脑子。

Step1. npm 装置,参考 小程序 npm 反对

npm install --save wxml-to-canvas

Step2. JSON 组件申明

{
  "usingComponents": {"wxml-to-canvas": "wxml-to-canvas"}
}

Step3. wxml 引入组件

<view class="share-image-container">
  <wxml-to-canvas
    id="canvas"
    width="{{canvasWidth}}"
    height="{{canvasHeight}}"
  ></wxml-to-canvas>
</view>

3.3、海报分享逻辑阐明

点击编程日历小程序底部的海报分享按钮,在以后页面生成 canvas 预览图,而后再生成图片跳转到海报图片预览和保留页面。

下面的 .share-image-container 类如下:

.share-image-container {
  border: 1px solid red;
  position: absolute;
  transform: translateY(-1000%);
  bottom: 0;
  z-index: 0;
}

即在页面外生成 canvas,也是在这里调试 wxml-to-canvas 组件成果的中央,去掉该类的样子如下:

3.4、js 获取实例

Step4. js 获取实例

import RenderCodeToWXML from "./renderCodeWXML.js";

Page({
  data: {
    canvasWidth: 373,
    canvasHeight: 720,
    bannerImgHeight: 240,
    bannerImgWdith: 320,
  },
  renderToCanvas() {
    wx.showLoading({title: "解决中...",});
    this.canvas = this.selectComponent("#canvas");
    const {
      canvasWidth,
      canvasHeight,
      bannerImgWdith,
      bannerImgHeight,
    } = this.data;
    let renderToWXML = new RenderCodeToWXML(
      canvasWidth,
      canvasHeight,
      bannerImgWdith,
      bannerImgHeight
    );

    const wxml = renderToWXML.renderWXML();
    const style = renderToWXML.renderStyle();
    const p1 = this.canvas.renderToCanvas({wxml, style});
    p1.then((res) => {// console.log('container', res.layoutBox)
      app.globalData.container = res;
      this.container = res;
      this.extraImage();}).catch((err) => {wx.hideLoading();
      console.log("err", err);
    });
  },
  extraImage() {const p2 = this.canvas.canvasToTempFilePath();
    p2.then((res) => {wx.hideLoading();
      // app.globalData.share = res
      wx.navigateTo({
        url: "../shareImage/shareImage",
        success: function(res2) {
          // 通过 eventChannel 向被关上页面传送数据
          res2.eventChannel.emit(
            "acceptDataFromOpenerPage",
            {
              share: res,
              container: app.globalData.container,
              tab: app.globalData.tab,
              date: app.globalData.dateInfo.strings,
            }
          );
        },
      });
    }).catch((err) => {wx.hideLoading();
      wx.showToast({
        title: err,
        icon: "none",
      });
    });
  },
});

这里次要就是从 renderCodeWXML.js 中获取 WXML 和 Style,而后调用 canvas 的 renderToCanvas 办法进行渲染:

const wxml = renderToWXML.renderWXML();
const style = renderToWXML.renderStyle();
const p1 = this.canvas.renderToCanvas({wxml, style});

最初在 p1.then 里调用 this.extraImage(); 办法跳转到下一个页面,并通过 eventChannel.emit 形式传递参数。

来看看 renderCodeWXML.js 外面有什么:

const app = getApp();

export default class RenderDataToWXML {
  constructor(
    canvasWidth,
    canvasHeight,
    imgWidth,
    imgHeight
  ) {
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.imgWidth = imgWidth;
    this.imgHeight = imgHeight;
  }

  renderWXML() {const { dateInfo, data, userInfo} = app.globalData;
    const openId = wx.getStorageSync("openId");
    let pData = "";
    let pMore = "";
    let banner = "";

    if (data.data.event) {pData = data.data.event.join("");
    }
    if (data.data.coding) {pData = data.data.coding.join("");
    }
    if (data.data.landmark) {pData = data.data.landmark.join("");
    }

    if (data.data.more) {pMore = data.data.more[0];
    } else if (data.data.people) {pMore = data.data.people[0].split(":").join(",");
    } else {pMore = "";}

    if (data.data.img) {
      banner = `
      <view class="banner">
        <image class="banner-image" mode="aspectFit" src="${data.data.img.url}" />
      </view>`;
    }

    if (pData.length >= 156) {pData = pData.substring(0, 152) + "...";
    }
    if (pMore.length >= 50) {pMore = pMore.substring(0, 48) + "...";
    }

    let avatar = "";
    if (userInfo && userInfo.avatarUrl) {
      avatar = `<view class="avatar">
        <image class="avatar-image" src="${userInfo.avatarUrl}" />
        <text class="avatar-nikename">${userInfo.nickName}邀请你应用 </text>
      </view>`;
    }

    let wxmlMore = pMore;
    if (wxmlMore) {
      wxmlMore = `
        <view>
          <text class="p-more">${pMore}</text>
        </view>
      `;
    }

    const wxml = `
      <view class="container">
        <view class="top">
          <view class="top-left">
            <view><text class="en">${dateInfo.date.monthEN}</text></view>
            <view><text class="cn">${dateInfo.lunarDate}</text></view>
          </view>
          <view><text class="top-center">${dateInfo.date.day}</text></view>
          <view class="top-right">
            <view><text class="en">${dateInfo.date.weekEN}</text></view>
            <view><text class="cn">${dateInfo.date.weekCN}</text></view>
          </view>
        </view>
        ${banner}
        <view class="middle">
          <view>
            <text class="p-data">${pData}</text>
          </view>
          ${wxmlMore}
        </view>
        <view class="qrcode">
          <view class="appinfo">
            ${avatar}
            <view><text class="appname"> 编程日历 </text></view>
            <view><text class="appdesc"> 程序员专属日历,最极客日历 </text></view>
          </view>
          <view class="qrcode-image">
            <image class="image" mode="aspectFit" src="https://7072-programming-calendar-3b8b7a7d082-1304448256.tcb.qcloud.la/qr/${openId}-qr.png?sign=b5a610dc6ae15c9427720ab617a2f18a&t=1609438339" />
          </view>
        </view>
      </view>
      `;
    return wxml;
  }

  // canvas 款式
  renderStyle() {
    const contentWidth = this.canvasWidth - 50;
    const mainColor = "#1296db";
    const style = {
      container: {
        width: this.canvasWidth,
        height: this.canvasHeight,
        backgroundColor: "#fff",
      },
      top: {
        width: this.canvasWidth,
        height: 82,
        backgroundColor: mainColor,
        flexDirection: "row",
        justifyContent: "space-around",
        alignItems: "center",
      },
      topLeft: {
        width: this.canvasWidth / 3,
        height: 82,
        textAlign: "center",
        alignItems: "center",
      },
      topCenter: {
        width: this.canvasWidth / 3,
        height: 82,
        lineHeight: 82,
        fontSize: 72,
        textAlign: "center",
        color: "#ffffff",
      },
      topRight: {
        width: this.canvasWidth / 3,
        height: 82,
      },
      en: {
        width: this.canvasWidth / 3,
        height: 30,
        fontSize: 20,
        textAlign: "center",
        color: "#ffffff",
        marginTop: 15,
      },
      cn: {
        width: this.canvasWidth / 3,
        height: 30,
        textAlign: "center",
        color: "#ffffff",
      },
      banner: {
        width: this.canvasWidth,
        flexDirection: "row",
        justifyContent: "center",
        marginTop: 20,
      },
      bannerImage: {
        width: this.imgWidth,
        height: this.imgHeight,
      },
      middle: {
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        marginTop: 20,
      },
      pData: {
        width: contentWidth,
        height: 170,
        lineHeight: "1.8em",
      },
      pMore: {
        width: contentWidth,
        height: 60,
        lineHeight: "1.8em",
      },
      qrcode: {
        height: 130,
        flexDirection: "row",
        justifyContent: "space-between",
        backgroundColor: "#CCE6FF",
        paddingLeft: 20,
        paddingTop: 20,
      },
      qrcodeImage: {
        width: 90,
        height: 90,
        marginRight: 20,
        borderRadius: 45,
        flexDirection: "row",
        justifyContent: "center",
        alignItems: "center",
        backgroundColor: "#fff",
      },
      image: {
        width: 90,
        height: 90,
        scale: 0.9,
        borderRadius: 45,
      },
      appinfo: {
        flexDirection: "column",
        justifyContent: "flex-start",
        alignItems: "flex-start",
        height: 80,
      },
      avatar: {
        flexDirection: "row",
        justifyContent: "flex-start",
        width: this.canvasWidth / 1.8,
        height: 30,
      },
      avatarImage: {
        width: 30,
        height: 30,
        borderRadius: 15,
        marginRight: 5,
      },
      avatarNikename: {
        width: this.canvasWidth / 1.8,
        height: 22,
        lineHeight: 22,
        marginTop: 5,
      },
      appname: {
        width: this.canvasWidth / 2,
        height: 23,
        fontSize: 16,
        color: "#0081FF",
        marginTop: 8,
        marginLeft: 35,
      },
      appdesc: {
        width: this.canvasWidth / 2,
        height: 20,
        fontSize: 14,
        marginLeft: 35,
      },
    };
    return style;
  }

  // 省略不相干代码
}

该文件就是咱们画海报的中央,就是生成 WXML 和 Style 而后导出

3.5、wxml-to-canvas 组件的注意事项

wxml-to-canvas 组件对 wxml 模板反对无限 :

  • 反对 <view><text><image> 三种标签,通过 class 匹配 style 对象中的款式。
  • 文字必须用 <text> 标签蕴含,否则不显示。并且必须设置宽高。文字宽度必须先确定,超出则会主动截断。所以动静文字能够依据字数,动静设置宽度。

款式方面:

  • 对象属性值为对应 wxml 标签的 cass 驼峰模式 需为每个元素指定 width 和 height 属性,否则会导致布局谬误
  • 存在多个 className 时,地位靠后的优先级更高,子元素会继承父级元素的可继承属性。
  • 元素均为 flex 布局。left/top 等 仅在 absolute 定位下失效。

因为文字必须用 <text> 标签蕴含,并且必须设置宽高,文字宽度必须先确定,超出则会主动截断。所以动静文字能够依据字数,动静设置宽度。所以写布局十分麻烦,我举荐大家为每一个元素设置背景,这样能够看到元素渲染的范畴和宽高。如下所示:

borderColor/marginBottom/marginTop 可应用,尽管微信文档中没写。

3.6、海报预览和下载页面

生成 canvas 并调用接口生成图片后,咱们携带参数跳转到下一个页面,先来看看 WXML,非常简单:

<view>
  <view class="share-container">
    <image
      src="{{src}}"
      mode="widthFix"
      class="image"
      style="height: {{height}}px;"
    ></image>
  </view>
  <view class="save-button">
    <van-button
      bind:tap="saveImage"
      block
      round
      icon="down"
      size="large"
      type="info"
      > 保留到手机 </van-button
    >
  </view>
</view>

js 逻辑

const app = getApp();
Page({
  data: {
    src: "",
    date: "",
    width: "",
    height: "",
  },
  onLoad() {const eventChannel = this.getOpenerEventChannel();
    eventChannel.on("acceptDataFromOpenerPage", (data) => {// console.log("data", data)
      this.setData({
        showPopup: true,
        date: data.date,
        src: data.share.tempFilePath,
        width: data.container.layoutBox.width,
        height: data.container.layoutBox.height,
      });
    });
  },
  getDatestr() {const { strings} = app.globalData.dateInfo;
    return strings;
  },
  saveImage() {
    wx.showLoading({title: "解决中...",});
    const _this = this;
    wx.getSetting({success(res) {if (!res.authSetting["scope.writePhotosAlbum"]) {
          wx.authorize({
            scope: "scope.writePhotosAlbum",
            success() {_this.save();
            },
            fail() {
              wx.showToast({
                title: "受权失败",
                icon: "none",
              });
            },
          });
        } else {_this.save();
        }
      },
    });
  },
  save() {
    wx.saveImageToPhotosAlbum({
      filePath: this.data.src,
      success() {
        wx.showToast({
          title: "保留胜利",
          icon: "none",
        });
      },
      fail() {
        wx.showToast({
          title: "保留失败",
          icon: "none",
        });
      },
    });
  },
});

以上就是我开发海报性能的逻辑和代码,仅供参考吧,如果你有相干教训欢送探讨交换,留下你的远见卓识吧。

4、编程日历小程序页面截图

最初,分几张小程序的页面截图

预览 预览

5、后续迭代打算

减少用户等级打算、对应等级能够换一些礼物,其余的。。。你有什么想法?欢送交换!

正文完
 0