14

编程日历小程序,对小程序云开发和生成海报的实践

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI0MDIwNTQ1Mg%3D%3D&%3Bmid=2676496528&%3Bidx=1&%3Bsn=c2d24fcf1bfb292f1f31d75896c5e5bc
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ENfI7z2.png!mobile

可点击 文末 阅读原文 在电脑上阅读,体验更好

1、起源

朋友圈晒的很多的一本日历书《了不起的程序员 2021》,我也买了,很厚,纸质书嘛,现在已经很少看了,加上这是一本日历书,希望是每天都打开看。可实际上的情况是,要么忘记看今天的内容,要么一口气看了好几天的内容,然后剩下几天又不看了。

jiUr2iu.jpg!mobile

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

于是乎!我就想做一个小程序,因为手机每天打开的频率太高了,碎片时间也很多,加上小程序的不用安装用完即走的优点,使用方便,不会有压力感。

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

于是乎!开干!

2、产品设计

这是最伤脑筋的部分,小程序到底要做成什么样,画个原型图?作为一个『资深』程序员,从来没正经画过原型和设计。手足无措,改用什么工具?虽然我知道有 Sketch 这个神器,还很多在线设计工具,比如磨刀,但从来没用过啊,最后硬着头皮用磨刀画了画原型,很简陋的原型,就是线框图级别。

如下是我画的原型....

磨刀链接:https://modao.cc/app/c9f6d109bce5857e3cb0cb5e1969b3942392e574?simulator_type=device&sticky

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

在这个过程中我不断的给自己家需求,一度增加了什么历史上的今天、知乎日历等等各种内容,最后还是被自己 狠心 一一毙掉了,只留下纯粹的编程日历内容。

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

  1. 真实配图,《了不起的程序员 2021》的配图都是手绘图,看起来少了点那种味儿。。。

  2. 小程序支持收藏、分享,这是纸质书先天不具备的

  3. 基于《了不起的程序员 2021》但不是完全一样,我做了一些小小的修改或增加一些内容。

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

3、开发

产品设计阶段和开发阶段占用的时间比大概是 8:2 左右,有了原型开发很快,毕竟也没什么复杂的东西。

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

3.1、选择海报分享方案

在开发分享海报功能之前我也看了下网上大致的方案,最后我选择了微信小程序自己的扩展组件: wxml-to-canvas ,小程序内通过静态模板和样式绘制 canvas ,导出图片,可用于生成分享图等场景。

我为什么不用其他方案:

  • 手写 canvas,太麻烦

  • 后端生成前端获取,太麻烦,我这个小程序很简单没必要

  • 开源小程序海报组件,尝试过一个,感觉也不太好用,有些没文档用起来吃力

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

3e6VVjA.png!mobile

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 组件效果的地方,去掉该类的样子如下:

3yUbArF.png!mobile

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> 标签包含,并且必须设置宽高,文字宽度必须先确定,超出则会自动截断。所以动态文字可以根据字数,动态设置宽度。所以写布局非常麻烦,我推荐大家为每一个元素设置背景,这样可以看到元素渲染的范围和宽高。如下所示:

bAbAJjE.png!mobile

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、编程日历小程序页面截图

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

预览 预览

eiymUbz.png!mobile

bmaeqyU.png!mobile

ERjYru.png!mobile

YzuyiyF.png!mobile

5、后续迭代计划

增加用户等级计划、对应等级可以换一些礼物,其余的。。。你有什么想法?欢迎交流!

粉丝福利

临走前留下, 今天的福利

  • 福利1: 高清PDF《 JavaScript高级程序设计(第4版).pdf》 获取资源请在公众号对话框中回复关键字: FL05 ,如果没有关注请扫下面的二维码。更多福利资料请查看公众号菜单

  • 福利2: 在看+留言 ,我随机抽取一位认真留言的小伙伴,给他发一个红包奖励

最近文章

- END -

点赞 + 在看 + 留言,下一个幸运儿就是你!
走心的分享更容易被抽中~

开奖时间 下期文末

FviIzuN.png!mobile

EJrum2a.gif!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK