編程日曆小程序,對小程序雲開發和(hé / huò)生成分享海報的(de)實踐
發表時(shí)間:2021-2-3
發布人(rén):融晨科技
浏覽次數:77
1、起源
朋友圈曬的(de)很多的(de)一本日曆書《了(le/liǎo)不(bù)起的(de)程序員 2021》,我也(yě)買了(le/liǎo),很厚,紙質書嘛,現在(zài)已經很少看了(le/liǎo),加上(shàng)這(zhè)是(shì)一本日曆書,希望是(shì)每天都打開看。可實際上(shàng)的(de)情況是(shì),要(yào / yāo)麽忘記看今天的(de)内容,要(yào / yāo)麽一口氣看了(le/liǎo)好幾天的(de)内容,然後剩下幾天又不(bù)看了(le/liǎo)。

後來(lái)《了(le/liǎo)不(bù)起的(de)程序員 2021》在(zài) Github 開源了(le/liǎo)。
于(yú)是(shì)乎!我就(jiù)想做一個(gè)小程序,因爲(wéi / wèi)手機每天打開的(de)頻率太高了(le/liǎo),碎片時(shí)間也(yě)很多,加上(shàng)小程序的(de)不(bù)用安裝用完即走的(de)優點,使用方便,不(bù)會有壓力感。
再加上(shàng)自己還沒有一款正兒八經的(de)小程序作品,對現在(zài)很火的(de)雲開發也(yě)沒怎麽用過,特别是(shì)小程序雲開發,他(tā)他(tā)到(dào)底用起來(lái)爽不(bù)爽呢?(很爽!)
于(yú)是(shì)乎!開幹!
2、産品設計
這(zhè)是(shì)最傷腦筋的(de)部分,小程序到(dào)底要(yào / yāo)做成什麽樣,畫個(gè)原型圖?作爲(wéi / wèi)一個(gè)『資深』程序員,從來(lái)沒正經畫過原型和(hé / huò)設計。手足無措,改用什麽工具?雖然我知道(dào)有 Sketch 這(zhè)個(gè)神器,還很多在(zài)線設計工具,比如磨刀,但從來(lái)沒用過啊,最後硬着頭皮用磨刀畫了(le/liǎo)畫原型,很簡陋的(de)原型,就(jiù)是(shì)線框圖級别。
這(zhè)個(gè)過程不(bù)斷有新的(de)想法,所以(yǐ)改來(lái)該去,産品設計花了(le/liǎo)好幾天,學習怎麽畫原型,實現腦子(zǐ)裏亂七八糟的(de)各種想法。
在(zài)這(zhè)個(gè)過程中我不(bù)斷的(de)給自己家需求,一度增加了(le/liǎo)什麽曆史上(shàng)的(de)今天、知乎日曆等等各種内容,最後還是(shì)被自己狠心一一斃掉了(le/liǎo),隻留下純粹的(de)編程日曆内容。
鑒于(yú)對産品和(hé / huò)設計不(bù)擅長,在(zài)此誠邀 UI、産品小夥伴,一起租一個(gè)團隊,有機會一起做一些産品,讓我們的(de)想法能落地(dì / de),生根發芽。
3、開發
産品設計階段和(hé / huò)開發階段占用的(de)時(shí)間比大(dà)概是(shì) 8:2 左右,有了(le/liǎo)原型開發很快,畢竟也(yě)沒什麽複雜的(de)東西。
下面重點說(shuō)一下分享海報功能的(de)實現吧。
3.1、選擇海報分享方案
在(zài)開發分享海報功能之(zhī)前我也(yě)看了(le/liǎo)下網上(shàng)大(dà)緻的(de)方案,最後我選擇了(le/liǎo)微信小程序自己的(de)擴展組件:wxml-to-canvas,小程序内通過靜态模闆和(hé / huò)樣式繪制 canvas ,導出(chū)圖片,可用于(yú)生成分享圖等場景。
我爲(wéi / wèi)什麽不(bù)用其他(tā)方案:
- 手寫 canvas,太麻煩
- 後端生成前端獲取,太麻煩,我這(zhè)個(gè)小程序很簡單沒必要(yào / yāo)
- 開源小程序海報組件,嘗試過一個(gè),感覺也(yě)不(bù)太好用,有些沒文檔用起來(lái)吃力
上(shàng)圖,是(shì)騾子(zǐ)是(shì)馬拉出(chū)來(lái)遛遛,下圖的(de)的(de)海報就(jiù)是(shì)通過 wxml-to-canvas 動态繪制的(de)。

3.2、引入 wxml-to-canvas 組件
wxml-to-canvas 的(de)限制很多,第一次沒經驗的(de)話覺得很難用,如果再讓我做一次,我就(jiù)快很多了(le/liǎo)。
官方的(de)示例隻單純教你怎麽生成海報,缺乏上(shàng)下文和(hé / huò)怎麽整合進你的(de)項目及邏輯,需要(yào / yāo)費一下腦子(zǐ)。
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、海報分享邏輯說(shuō)明
點擊編程日曆小程序底部的(de)海報分享按鈕,在(zài)當前頁面生成 canvas 預覽圖,然後再生成圖片跳轉到(dào)海報圖片預覽和(hé / huò)保存頁面。
上(shàng)面的(de) .share-image-container
類如下:
.share-image-container {
border: 1px solid red;
position: absolute;
transform: translateY(-1000%);
bottom: 0;
z-index: 0;
}
複制代碼
即在(zài)頁面外生成 canvas,也(yě)是(shì)在(zài)這(zhè)裏調試 wxml-to-canvas 組件效果的(de)地(dì / de)方,去掉該類的(de)樣子(zǐ)如下:
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",
});
});
},
});
複制代碼
這(zhè)裏主要(yào / yāo)就(jiù)是(shì)從 renderCodeWXML.js
中獲取 WXML 和(hé / huò) Style,然後調用 canvas 的(de) renderToCanvas
方法進行渲染:
const wxml = renderToWXML.renderWXML();
const style = renderToWXML.renderStyle();
const p1 = this.canvas.renderToCanvas({ wxml, style });
複制代碼
最後在(zài) p1.then
裏調用 this.extraImage();
方法跳轉到(dào)下一個(gè)頁面,并通過 eventChannel.emit
方式傳遞參數。
來(lái)看看 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 = http://www.wxapp-union.com/"";
let pMore = "";
let banner = "";
if (data.data.event) {
pData = http://www.wxapp-union.com/data.data.event.join("");
}
if (data.data.coding) {
pData = http://www.wxapp-union.com/data.data.coding.join("");
}
if (data.data.landmark) {
pData = http://www.wxapp-union.com/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="http://www.wxapp-union.com/${data.data.img.url}" />
</view>`;
}
if (pData.length >= 156) {
pData = http://www.wxapp-union.com/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="http://www.wxapp-union.com/${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;
}
// 省略不(bù)相關代碼
}
複制代碼
該文件就(jiù)是(shì)我們畫海報的(de)地(dì / de)方,就(jiù)是(shì)生成 WXML 和(hé / huò) Style 然後導出(chū) 。
3.5、wxml-to-canvas 組件的(de)注意事項
wxml-to-canvas 組件對 wxml 模闆支持有限 :
- 支持
<view>
、<text>
、<image>
三種标簽,通過class
匹配style
對象中的(de)樣式。 - 文字必須用
<text>
标簽包含,否則不(bù)顯示。并且必須設置寬高。文字寬度必須先确定,超出(chū)則會自動截斷。所以(yǐ)動态文字可以(yǐ)根據字數,動态設置寬度。
樣式方面:
- 對象屬性值爲(wéi / wèi)對應 wxml 标簽的(de) cass 駝峰形式。需爲(wéi / wèi)每個(gè)元素指定 width 和(hé / huò) height 屬性,否則會導緻布局錯誤。
- 存在(zài)多個(gè) className 時(shí),位置靠後的(de)優先級更高,子(zǐ)元素會繼承父級元素的(de)可繼承屬性。
- 元素均爲(wéi / wèi) flex 布局。left/top 等 僅在(zài) absolute 定位下生效。
因爲(wéi / wèi)文字必須用 <text>
标簽包含,并且必須設置寬高,文字寬度必須先确定,超出(chū)則會自動截斷。所以(yǐ)動态文字可以(yǐ)根據字數,動态設置寬度。所以(yǐ)寫布局非常麻煩,我推薦大(dà)家爲(wéi / wèi)每一個(gè)元素設置背景,這(zhè)樣可以(yǐ)看到(dào)元素渲染的(de)範圍和(hé / huò)寬高。如下所示:

borderColor/marginBottom/marginTop
可使用,雖然微信文檔中沒寫。
3.6、海報預覽和(hé / huò)下載頁面
生成 canvas 并調用接口生成圖片後,我們攜帶參數跳轉到(dào)下一個(gè)頁面,先來(lái)看看 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"
>保存到(dào)手機</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",
});
},
});
},
});
複制代碼
以(yǐ)上(shàng)就(jiù)是(shì)我開發海報功能的(de)邏輯和(hé / huò)代碼,僅供參考吧,如果你有相關經驗歡迎讨論交流,留下你的(de)真知灼見吧。
4、編程日曆小程序頁面截圖
最後,分幾張小程序的(de)頁面截圖
預覽 | 預覽 |
---|---|
![]() | ![]() |
![]() | ![]() |
5、後續叠代計劃
增加用戶等級計劃、對應等級可以(yǐ)換一些禮物,其餘的(de)。。。你有什麽想法?歡迎交流!
作者:杭州程序員張張
來(lái)源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出(chū)處。