The original article was posted on the blog:

1, the origin of

I also bought a calendar book “The Great Programmer 2021”, which is very thick. It is a paper book, and I seldom read it now. Plus, it is a calendar book, and I hope I can open it every day. What happens is that you either forget today’s content, or you watch it for a few days, and then you don’t watch it for the rest of the day.

Fantastic Programmer 2021 was later made open source on GitHub.

As a result! I just want to make a small program, because the frequency of the mobile phone is too high every day, the fragmentation time is also a lot of, coupled with the advantages of small program without installation and use, easy to use, no sense of pressure.

In addition, they have not a serious small program works, to now very popular cloud development also did not use, especially small program cloud development, he in the end to use up cool? (Cool!)

As a result! Open dry!

2. Product design

This is the most nerve-racking part of the small program to make what, draw a prototype diagram? As a “senior” programmer, I’ve never really prototyped or designed. At a loss, what tools should I use? Although I know that there is Sketch and there are many online design tools, such as knife sharpening, I have never used it. Finally, I brazened my teeth and drew a prototype with the knife sharpening, a very simple prototype, just wireframe level.

Below is my drawing prototype….

Knife sharpening link:

It was a process of constantly coming up with new ideas, so I had to go back and forth, and the product design took a couple of days, learning how to prototype and implementing all the ideas that were popping up in my head.

In this process, I constantly add demands to myself, once added a variety of content such as today in history, Zhihu calendar, etc. Finally, I was cruel to kill one by one, leaving only the pure programming calendar content.

How little programs are better than Fantastic Programmer 2021:

  1. Real graphics. The graphics for Fantastic Programmer 2021 are all hand-drawn, so it doesn’t look like that.
  2. Small program support collection, sharing, which is a paper book innate do not have
  3. Based on Fantastic Programmer 2021 but not exactly the same, I made some minor changes or additions.

Since we are not good at products 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, rooted and germinated.

3, development,

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

The following focuses on the realization of the sharing poster function, other functions are very “ordinary”, if you have any 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 itself: WXML-to-Canvas. In the applet, the static template and style are used to draw Canvas and export pictures, which can be used to generate scenes such as sharing pictures.

Why don’t I use other options:

  • Handwritten canvas, too much trouble
  • Backend generation front-end access, too troublesome, I this small program is very simple is not necessary
  • Open source small program poster components, tried one, feel not too easy to use, some did not document up to use

Above, the mule is pulling a horse for a walk. Below, the poster is dynamically drawn using WXML-to-Canvas.

3.2. Introducing WXML-to-Canvas component

WXML-to-Canvas has a lot of limitations, which makes it difficult to use if I am not experienced at the first time. If you ask me to do it again, I will be much faster.

The official example simply teaches you how to generate the poster. It lacks context and how to integrate it into your project and logic. It takes a lot of brainpower.

Step 1.npm installation, refer to the applet NPM support

npm install --save wxml-to-canvas

Step2. JSON component declaration

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

Step3. WXML introduces the component

<view class="share-image-container">

3.3. Logical explanation for poster sharing

Click the poster sharing button at the bottom of the programming calendar applet to generate a canvas preview picture on the current page, and then regenerate the picture to jump to the poster 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;

This is where the WXML-to-Canvas component is debugged. remove the class and it will look like this:

3.4. JS Gets instance

Step 4.js Get the instance

import RenderCodeToWXML from "./renderCodeWXML.js"; Page({ data: { canvasWidth: 373, canvasHeight: 720, bannerImgHeight: 240, bannerImgWdith: }, renderToCanvas() {wx.showLoading({title: "...");}, renderToCanvas(); }); this.canvas = this.selectComponent("#canvas"); const { canvasWidth, canvasHeight, bannerImgWdith, bannerImgHeight, } =; 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) {res2.eventChannel.emit(" acceptDatafromOpenEnerPage ", {share: acceptDatafromOpenEnerPage ", acceptDatafromOpenEnerPage ", {share: acceptDatafromOpenEnerPage (acceptDatafromOpenEnerPage); res, container: app.globalData.container, tab:, date: app.globalData.dateInfo.strings, } ); }}); }).catch((err) => { wx.hideLoading(); wx.showToast({ title: err, icon: "none", }); }); }});

RenderCodeWXML.js, then call the RenderToCanvas method on Canvas to render it:

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

Finally, call this.extraImage() in p1. Then; Method jumps to the next page and passes the parameters via 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 ( { pData =""); } if ( { pData =""); } if ( { pData =""); } if ( { pMore =[0]; } else if ( {pMore =[0].split(" : ").join(", "); } else { pMore = ""; } if ( { banner = ` <view class="banner"> <image class="banner-image" mode="aspectFit" src="${}" /> </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">${}</text></view> <view><text class="cn">${dateInfo.lunarDate}</text></view> </view> <view><text class="top-center">${}</text></view> <view class="top-right"> <view><text class="en">${}</text></view> <view><text class="cn">${}</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 "> Programmer calendar, </text></view> </view> <view class="qrcode-image"> <image class="image" mode=" AspectFit" src="${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: }, image: {width: 90, height: 90, scale: 0.9, borderRadius: 45,}, appInfo: { flexDirection: "column", justifyContent: "flex-start", alignItems: "flex-start", height: 80, }, avatar: {flex direction: "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}

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

3.5. Notes for WXML-to-Canvas Component

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

  • support<view>,<text>,<image>Three kinds of tags, throughclassmatchingstyleStyles in the.
  • The text must be<text>The tag contains, otherwise it is not displayed. And the width and height must be set. The text width must be determined before it is truncated. So dynamic text can be set according to the number of words, dynamic width.


  • Object attribute values are the CASS camel shape of the corresponding WXML tag. You need to specify the width and height attributes for each element, otherwise it will result in layout errors.
  • When there are more than one className, the lower one takes precedence, and the child element inherits the inheritable attributes of the parent element.
  • Elements are laid out in Flex. Left /top and so on only take effect with absolute positioning.

Because the text must be contained in the

tag and must be set to the width and height, the text width must be determined first. If the width is exceeded, it will automatically truncate. So dynamic text can be set according to the number of words, dynamic width. I recommend setting the background for each element so that you can see how wide and how high the element is rendered. This is as follows:

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

3.6. Poster preview and download page

After generating the canvas and calling the interface to generate the image, we jump to the next page with the parameters. First, let’s have a look at WXML, which is 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 phone </van-button > </view> </view>

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:, 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: "Loading..."); }); const _this = this; wx.getSetting({ success(res) { if (! res.authSetting["scope.writePhotosAlbum"]) { wx.authorize({ scope: "scope.writePhotosAlbum", success() {; }, fail () {wx. ShowToast ({title: "authorization failure," icon: "none,"}); }}); } else {; }}}); }, the save () {wx. SaveImageToPhotosAlbum ({filePath: this data. The SRC, success () {wx. ShowToast ({title: "successfully saved", icon: "none", }); }, fail () {wx. ShowToast ({title: "save failed," icon: "none,"}); }}); }});

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

4, programming calendar small program page screenshot

Finally, a few small program page screenshots

preview preview

5. Subsequent iteration plan

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