您當前位置>首頁 » 新聞資訊 » 小(xiǎo)程序相(xiàng)關 >
編程日(rì)曆小(xiǎo)程序,對(duì)小(xiǎo ₩γ)程序雲開(kāi)發和(hé)生(shēng)成分(fēn)享海(hǎ≠♥i)報(bào)的(de)實踐
發表時(shí)間(jiān):2021-2-3
發布人(rén):葵宇科(kē)技(jì)
浏覽次數(shù):91
1、起源
朋(péng)友(yǒu)圈曬的(de)很(hěn)多(duō)的(de)φ±£一(yī)本日(rì)曆書(shū)《了(le)不(bù)™®↕起的(de)程序員(yuán) 2021》,我也(yě)₹σ✔€買了(le),很(hěn)厚,紙(zhǐ)質書(shū)嘛,現(xiàn)在已經很(hě✘"n)少(shǎo)看(kàn)了(le),加上(shàng)這(zhè)是(shì)÷♥一(yī)本日(rì)曆書(shū),希望是(shì)每天都(dōu)打開(σ∑ σkāi)看(kàn)。可(kě)實際上(shàng)的(de)情況是(shì≥" ),要(yào)麽忘記看(kàn)今天的(de)內(nèi)容,要(yào)麽一(yī±♣↕)口氣看(kàn)了(le)好(hǎo)幾天的(de)內↑≈↓↔(nèi)容,然後剩下(xià)幾天又(yòu)不(bù♠≥←★)看(kàn)了(le)。

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

3.2、引入 wxml-to-canvas 組件(jiàn)
wxml-to-canvas 的(de)限制(zhì)很(hěn)多(duō),第一(yī)次沒©'經驗的(de)話(huà)覺得(de)很(hěn)難用(yòng),如(rú)果β★±ε再讓我做(zuò)一(yī)次,我就(jiù)快(kuài)很(hěn)多(duō)了(le) δ¥∏。
官方的(de)示例隻單純教你(nǐ)怎麽生(shēng)成海(hǎi)報(bàα¶α€o),缺乏上(shàng)下(xià)文(wén)和(hé)怎麽整合進你(nǐ)的(de)項目≤••✘及邏輯,需要(yào)費(fèi)一(yī)下(xià)腦(nǎo)↕> 子(zǐ)。
Step1. npm 安裝,參考 小(xiǎo)程序 npm 支持
npm install --save wxml-to-canvas
複制(zhì)代碼
Step2. JSON 組件(jiàn)聲明(míng)
{
"usingComponents": {
"wxml-to-canvas": "wxml-to-canvas"
}
}
複制(zhì)代碼
Step3. wxml 引入組件(jiàn)
<view class="share-image-container">
<wxml-to-canvas
id="canvas"
width="{{canvasWidth}}"
height="{{canvasHeight}}"
></wxml-to-canvas>
</view>
複制(zhì)代碼
3.3、海(hǎi)報(bào)分(fēn)享邏輯說(shuō)明(míng)×Ω
點擊編程日(rì)曆小(xiǎo)程序底部的(de)海(hǎi)報(bào)分(fēn)享π"Ω按鈕,在當前頁面生(shēng)成 canvas 預覽圖,然後再生(shēng)成圖片跳₽>₽γ(tiào)轉到(dào)海(hǎi)報(bào)圖片預覽和(hé)保存頁面。
上(shàng)面的(de) .share-image-container
類如(rú)下(xià):
.share-image-container {
border: 1px solid red;
position: absolute;
transform: translateY(-1000%);
bottom: 0;
z-index: 0;
}
複制(zhì)代碼
即在頁面外(wài)生(shēng)成 canvas,也(yě)是(shì)在這(zhè)裡(™↓ πlǐ)調試 wxml-to-canvas 組件(jiàn)效↑≥π果的(de)地(dì)方,去(qù)掉該類的(de)樣子(zǐ)如(rú)下(x✔×δ'ià):
3.4、js 獲取實例
Step4. js 獲取實例
import RenderCodeToWXML from "./renderCodeWXML.js";
Page({
data: {
canvasWidth: 373,
canvasHeight: 720,
bannerImgHeight: 240,
bannerImgWdith: 320,
},
renderToCanvas() {
wx.showLoading({
title: "處理(lǐ)中...",
});
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, st₽←yle });
p1.then((res) => {
// console.log('container', res.l £¶ayoutBox)
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) {
// 通(tōng)過eventChannel向被打開(kāi)頁§≤≠γ面傳送數(shù)據
res2.eventChannel.emit(
"acceptDataFromOpenerPage",
{
share: res,
container: app.globalData.container,
tab: app.globalData.tab,
date: app.globalData.dateInfo.st☆β<rings,
}
);
♥→¥},
});
}).catch((err) => {
wx.hideLoading();
wx.showToβ↕ast({
title: err,
icon: "none",
});
});
},
π≥});
複制(zhì)代碼
這(zhè)裡(lǐ)主要(yào)就(jiù)是(shì)從(♦"σ cóng) renderCodeWXML.js
中獲取 WXML 和(hé) Style,然後調用(yò∞✔₹§ng) canvas 的(de) renderToCanvas
方法進行(xíng)渲染:
const wxml = renderToWXML.renderWXML();
const style = renderToWXML.renderStyle ✔λγ();
const p1 = this.canvas.renderToCanvas({ wxml, style });
複制(zhì)代碼
最後在 p1.then
裡(lǐ)調用(yòng) this.extraImage();
方法跳(tiào)轉到(dào)下(xià)一(yī)個(gè)頁面,并通(tō★¥ng)過 eventChannel.emit
方式傳遞參數(shù)。
來(lái)看(kàn)看(kàn) renderCodeWXML.js
裡(lǐ)面有(yǒu)什(shén)麽:
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) {
pDat•♣✔↕a = http://www.wxapp-union.com/data±γ≥.data.event.join("");
}
if (data.data.coding) {
pData = http://www.wδ≠<xapp-union.com/data.data.coding.join↔Ω€("");
}
if (data.data.landmark) {
pData = http://ww↓πw.wxapp-union.com/data.data.land •≠≤mark.join("");
}
if (data.data.more) {
pMore = data.dataεφ₹.more[0];
} else if (data.data.people) {
pMore = dat©€↑a.data.people[0].split(":").join(",");
} else {
pMore = "";
}
if (data.data.img) {
bann÷&er = `
<view class="banner">
♥∞<•
<image class="bannγ→§er-image" mode="aspectFit" src="http://www.wx'§φφapp-union.com/${data.data.img.url}" />
</view>`;
}
if (pData.length >= 156) {
pData = http://www.wxap ≈p-union.com/pData.substring(0, 152) + "...";
}
if (pMore.length >= 50) {
pMore = pMore.substring(0, 48) + "...";
}
let avatar = "";
if (userInfo && userInfo.ava↔π$☆tarUrl) {
avatar = `<view class="avatar">
<image c&♣lass="avatar-image" src="http://www.wxapp-ᱩunion.com/${userInfo.avatarUrl}" />
<text class="avatar-nikenam≈γe">${userInfo.nickName}邀請(qǐng)你(nǐ)使用(yòng)</text>
ε☆ </view>`;
}
let wxmlMore = pMore;
if (wxmlMore) {
wxmlMore = `
<view>
&¶€lt;text class="p-more">${pMore}</text>
</view>
`;
}
const wxml = `
<view class="container">
↓↓ <view class="top">
<vi©γew 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-ri♣₹®ght">
<view₽✔€><text class="en">"↕♠';${dateInfo.date.weekEN}</text></view>
<viewγ←><text class="cn">${dateInfo.date.weekCN}</text></view>
δ§≈ </view>
</vi✔☆☆ew>
${banner}
<view class="middle">
<≥→≤view>
<text class₹↓≠↑="p-data">${pData}</text>
</view>
${wxmlMore}
</view>
<view class="qrγ£code">
<view class="appin±★fo">
${avatar}
<view><text class="appname"&↓α§"gt;編程日(rì)曆</text></α¥♣♠view>
<view>$★λ✔;<text class="appdesc"&•≠←↔gt;程序員(yuán)專屬日(rì)曆,最極客日(rì)曆</text></•∞<☆view>
</view>✔•>;
<view class="qrcode-i↔≤mage">
<image cl∏♣∞ass="image" mode="aspectFit" src="https:λ♦>ε//7072-programming-calendar-3b8b7a7d08✘φ2-1304448256.tcb.qcloud.la♦δ/qr/${openId}-qr.png?sign=b5a610dc6ae15c9427720ab617aλ"≤"2f18a&t=1609438339" />σΩ®¥
</view>
</←≥view>
</view&g ♥t;
`;
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ù)相(xiàng)關代碼
}
複制(zhì)代碼
該文(wén)件(jiàn)就(jiù)是(shì)我們畫(huà)海♥φ$(hǎi)報(bào)的(de)地(dì)方,就(jiù)是(shì)生(shēng)成 WXML₽γ 和(hé) Style 然後導出 。
3.5、wxml-to-canvas 組件(jiàn)的(de)注意事(sγ®hì)項
wxml-to-canvas 組件(jiàn)對(duì) wxml 模闆支持有(yǒu)限 :
- 支持
<view>
、<text>
、<image>
三種标簽,通(tōng)過class
匹配style
對(duì)象中的(de)樣式。 - 文(wén)字必須用(yòng)
<text>
标簽包含,否則不(bù)顯示。并且必須設置寬高(gāo)。文(wén)字寬度Ω"必須先确定,超出則會(huì)自(zì)動截斷。所以動态文(wén)字可(kě)以根據<§字數(shù),動态設置寬度。
樣式方面:
- 對(duì)象屬性值為(wèi)對(duì)應 wxml 标簽的(de) cass 駝峰形式。需為(wèi)每個(gè)元素指定 width 和(hé) hei×™✔ght 屬性,否則會(huì)導緻布局錯(cuò)誤。
- 存在多(duō)個(gè) className 時(shí)∞"φ,位置靠後的(de)優先級更高(gāo),子(zǐ)元素會(huγ♥ì)繼承父級元素的(de)可(kě)繼承屬性。
- 元素均為(wèi) flex 布局。left/top 等 僅在 absolute 定位下(xià)生(shēn★ ✔>g)效。
因為(wèi)文(wén)字必須用(yòng) <text>
标簽包含,并且必須設置寬高(gāo),文(wén)字寬度必✔≠須先确定,超出則會(huì)自(zì)動截斷。所以動态文(wén)字可(kě)以根αβ據字數(shù),動态設置寬度。所以寫布局非常麻煩,我推薦大(dà)家(jiā)為(wèi)€♦<每一(yī)個(gè)元素設置背景,這(zhè)樣可(kě)以看(kàn)到(dào)元素渲染的™α(de)範圍和(hé)寬高(gāo)。如(rú)下(xià)所示:

borderColor/marginBottom/margin✘¶Top
可(kě)使用(yòng),雖然微(wēi)信文(wén)檔中沒寫。
3.6、海(hǎi)報(bào)預覽和(hé)下(xià)載頁面
生(shēng)成 canvas 并調用(yòng)接口生(shēng)成圖片後,我們攜帶參數(s'≤₩€hù)跳(tiào)轉到(dào)下(xià)一(yī)個(gè)頁面,先來×☆≤(lái)看(kàn)看(kàn) 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)手機(jī)</van-button
>
</view>
</view>
複制(zhì)代碼
js 邏輯
const app = getApp();
Page({
data: {
src: "",
date: "",
width: "",
height: "",
},
onLoad() {
const eventChannel = this.getOpenerEventChannel();
eventChannel.o βn("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: "處理(lǐ)中...",
});
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.saveImageToPhotosAl≠bum({
filePath: this.data.src,
success() {
wx.showToast({
title: "保存成功",
icon: "none",
});
},
fail() {
wx.showToast({
title: "保存失敗",
icon: "none",
});
},
});
}φ★≤,
});
複制(zhì)代碼
以上(shàng)就(jiù)是(shì)我開(kāi)發海(hǎi)報(bào)功能(nδ$éng)的(de)邏輯和(hé)代碼,僅供參考吧(ba),如(rú)果你(n↕λǐ)有(yǒu)相(xiàng)關經驗歡迎討(tǎo)論交流,留下(xià)你(n✔✔♠≥ǐ)的(de)真知(zhī)灼見(jiàn)吧(ba)。
4、編程日(rì)曆小(xiǎo)程序頁面截圖
最後,分(fēn)幾張小(xiǎo)程序的(de)頁♦φε面截圖
預覽 | 預覽 |
---|---|
![]() | ![]() |
![]() | ![]() |
5、後續叠代計(jì)劃
增加用(yòng)戶等級計(jì)劃、對(duì)應等級可(¥♣♠≤kě)以換一(yī)些(xiē)禮物(wù),其餘的(de)。。。你(nǐ)有φ♣(yǒu)什(shén)麽想法?歡迎交流!
作(zuò)者:杭州程序員(yuán)張張
來(lái)源:掘金(jīn)
著作(zuò)權歸作(zuò)者所有(yǒu)。商業(yè)轉♣♥載請(qǐng)聯系作(zuò)者獲得(de)授權,非商業(yè)轉載請(q×✔ǐng)注明(míng)出處。