Originally published in blog: blog.zhangbing.site

1, the origin of

A calendar book “Great Programmer 2021”, a lot of friends circle sun, I also bought, very thick, paper book, now has rarely read, plus this is a calendar book, I hope is open every day to see. What happens is that you either forget to read today’s content, or you read it for a few days and then stop reading it for the rest of the day.

Great Programmer 2021 has since been open-source on Github.

As a result! I just want to make a small program, because the frequency of mobile phone is too high every day, fragmentation time is also a lot of, plus the small program does not need to install the advantages of disposable, easy to use, there is no sense of pressure.

In addition to their own has not a serious small program works, now very hot cloud development also did not use, especially small program cloud development, he he in the end use up cool? (Great!)

As a result! Open dry!

2. Product design

This is the most nerve-racking part, what is the little program going to do, draw a prototype? As a “senior” programmer, I’ve never actually drawn prototypes and designs. Confused, what tools should I use? Although I knew there was a great tool called Sketch, there are many online design tools, such as knife sharpening, but I never used it. Finally, I bravely used knife sharpening to draw a prototype, a very simple prototype, which is the level of wireframing.

Here is the prototype I drew….

Knife sharpening link: modao.cc

It was a process of constantly coming up with new ideas, so it was a process of going back and forth, and product design took a couple of days, learning how to prototype and implement all the ideas that were floating around in my head.

In this process, I continue to add their own needs, once added what in history today, zhihu calendar and so on all kinds of content, and finally was killed by their own cruel one by one, leaving only pure programming calendar content.

How applets are better than Great Programmer 2021:

  1. The great Programmer 2021 is a game with real graphics, which is a little bit less of a taste…
  2. Small programs support collection, sharing, which is innate paper books do not have
  3. Based on Great Programmer 2021 but not exactly the same, I made a few minor changes or additions.

The final product can be pulled down in section 4 below to see the screenshot of the interface.

🤝 in view of not being good at product and design, we sincerely invite UI and product partners to form a team together and have the opportunity to make some products together, so that our ideas can be grounded and take root.

3, development,

The time ratio between the product design phase and the development phase is about 8:2. With prototyping, development is very fast. After all, there is nothing complicated.

The following focus on the implementation of the sharing poster function, other functions are very “ordinary”, if you have what other want to know the point can leave a message.

3.1. Select the poster sharing scheme

Before developing the function of sharing posters, I also looked at the general scheme on the Internet. Finally, I chose the extension component of wechat applet: WXML-to-Canvas. In the applet, static templates and styles are used to draw canvas and export pictures, which can be used to generate shared pictures and other scenes.

Why I didn’t use another plan:

  • Writing canvas is too troublesome
  • Back-end generation front-end access, too troublesome, I this small procedure is very simple and unnecessary
  • Open source small program poster component, tried a, the feeling is not very easy to use, some no documentation to use up

Above, a mule is pulled out for a walk, and the poster below is dynamically drawn using wXML-to-canvas.

3.2. Introduce the WXML-to-Canvas component

Wxml-to-canvas has a lot of limitations, and it is difficult to use if I am inexperienced at the first time. If I have to do it again, I will be much faster.

The official example simply teaches you how to generate posters, lacks context and how to integrate it into your project and logic, requires a bit of thinking.

Step1.npm installation, refer to small program NPM support

npm install --save wxml-to-canvas
Copy the code

Step2. JSON component declaration

{
  "usingComponents": {
    "wxml-to-canvas": "wxml-to-canvas"}}Copy the code

Step3. WXML introduces components

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

3.3 poster sharing logic

Click the poster share button at the bottom of the programming calendar applet to generate a Canvas preview on the current page, and then regenerate it into a picture to jump to the poster picture preview and Save page.

The.share-image-container class above looks like this:

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

This is where the wXML-to-Canvas component is debuggable. If this class is removed, it looks like this:

3.4. Obtaining the instance of JS

Step4.js to obtain the instance

import RenderCodeToWXML from "./renderCodeWXML.js";

Page({
  data: {
    canvasWidth: 373.canvasHeight: 720.bannerImgHeight: 240.bannerImgWdith: 320,},renderToCanvas() {
    wx.showLoading({
      title: "Under treatment..."});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) {
          // Send data to the opened page via 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"}); }); }});Copy the code

RenderToCanvas gets the WXML and Style from renderCodeWxml. js and calls renderToCanvas to render:

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

Finally, call this.extraimage () in p1.then; Method jumps to the next page and passes the parameters through eventChannel.emit.

Let’s see what’s in 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}Invite you to use </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 style
  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.8 em",},pMore: {
        width: contentWidth,
        height: 60.lineHeight: "1.8 em",},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;
  }

  // omit irrelevant code
}
Copy the code

This file is where we draw the poster, which is to generate the WXML and Style and export it.

3.5. Considerations for the WXML-to-Canvas component

The WXML-to-Canvas component has limited support for WXML templates:

  • support<view>,<text>,<image>Three tags, passclassmatchingstyleThe style in the object.
  • Words must be used<text>Tag contains, otherwise not displayed. The width and height must be set. The width of the text must be determined first, and will be truncated automatically if it exceeds it. So dynamic text can be set according to the number of words, dynamic width.

Style:

  • The value of the object attribute is the CASS hump of the corresponding WXML tag. Specify width and height attributes for each element, otherwise layout errors will result.
  • When there are multiple classnames, the next one takes precedence, and the child element inherits the inheritable attributes of the parent element.
  • The elements are all flex layout. Left/Top are valid only for absolute positioning.

Because the text must be included with the

tag, and the width and height must be set, the text width must be determined first and truncated automatically if it exceeds it. So dynamic text can be set according to the number of words, dynamic width. So writing a layout is a hassle, and I recommend setting a background for each element so that you can see the range and width of the element rendering. As follows:

BorderColor marginBottom/marginTop can use, although WeChat didn’t write in the document.

3.6 Poster preview and download page

After the canvas is generated and the interface is called to generate the image, we jump to the next page with parameters. Let’s take a look at WXML first, very simple:

<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"
      >Save to mobile </van-button ></view>
</view>
Copy the code

Js logic

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: "Under treatment..."});const _this = this;
    wx.getSetting({
      success(res) {
        if(! res.authSetting["scope.writePhotosAlbum"]) {
          wx.authorize({
            scope: "scope.writePhotosAlbum".success() {
              _this.save();
            },
            fail() {
              wx.showToast({
                title: "Authorization failed".icon: "none"}); }}); }else{ _this.save(); }}}); },save() {
    wx.saveImageToPhotosAlbum({
      filePath: this.data.src,
      success() {
        wx.showToast({
          title: "Saved successfully".icon: "none"}); },fail() {
        wx.showToast({
          title: "Save failed".icon: "none"}); }}); }});Copy the code

The above is my logic and code for poster function development, just for reference, if you have relevant experience welcome to discuss and exchange, leave your insights.

4. Screenshot of programming calendar applet page

Finally, a few small program page screenshots

preview preview

5. Plan for subsequent iterations

Increased user level plan, corresponding level can be exchanged for some gifts, the rest… What do you have in mind? Welcome to exchange!