微信小程序:使用render函數在(zài)canvas中布局生成海報圖
發表時(shí)間:2021-1-5
發布人(rén):融晨科技
浏覽次數:454
一個(gè)常見的(de)需求,在(zài)開發微信小程序時(shí),前端需要(yào / yāo)生成海報圖分享,目前常見解決方案如下:
- 使用
htmlCanvas
庫,利用dom來(lái)生成圖片 - 前端使用ctx的(de)api一個(gè)一個(gè)的(de)畫出(chū)來(lái),或者借助一些繪圖工具
- 利用
puppeteer
後端服務,打開相應界面截圖
- 這(zhè)個(gè)庫本身并不(bù)能在(zài)小程序使用,因爲(wéi / wèi)涉及到(dào)dom,在(zài)web端也(yě)有各種兼容性問題比如某個(gè)屬性不(bù)支持
- 這(zhè)個(gè)方案,額。。。可能這(zhè)就(jiù)是(shì)程序員頭發少的(de)原因吧。費盡千辛萬苦畫好,萬一視覺調整一下。。這(zhè)個(gè)方案開發費時(shí)費力,不(bù)好維護。雖然web端有react-canvas,小程序也(yě)有一些工具,但目前都隻是(shì)封裝了(le/liǎo)繪制矩形、文字等方法,對于(yú)布局來(lái)說(shuō)還是(shì)需要(yào / yāo)手動計算寬高以(yǐ)及位置,沒有完全解決痛點。
- 這(zhè)種方案對前端來(lái)說(shuō)是(shì)最完美的(de),也(yě)推薦大(dà)家有條件用這(zhè)個(gè)方案,前端寫好頁面放到(dào)服務上(shàng),然後再挂一個(gè)服務訪問這(zhè)個(gè)頁面來(lái)截圖,因爲(wéi / wèi)開發和(hé / huò)截圖的(de)都是(shì)chromium,基本不(bù)存在(zài)兼容性問題。但是(shì)這(zhè)種方案會非常耗費服務器資源,每次截圖都要(yào / yāo)打開一個(gè)新的(de)浏覽器tab,并且截圖耗時(shí)比較長,對于(yú)一些公司來(lái)說(shuō)可能無法接受。
easy-canvas實現了(le/liǎo)在(zài)canvas中創建文檔流,api極易上(shàng)手基本沒有學習成本,可以(yǐ)很輕松的(de)支持組件化開發,并且沒有第三方依賴,隻要(yào / yāo)支持标準的(de)canvas就(jiù)可以(yǐ)使用,在(zài)實現基本功能的(de)基礎上(shàng)添加了(le/liǎo)事件、scroll-view等支持。基礎版支持小程序、web。
如果使用過render函數的(de)肯定很熟悉使用方式了(le/liǎo),相關屬性在(zài)項目裏以(yǐ)及示例裏都有介紹,本篇文章就(jiù)不(bù)過多介紹,基本使用如下:
npm install easy-canvas-layout --save
複制代碼
import easyCanvas from 'easy-canvas-layout'
// 首先綁定圖層
const layer = easyCanvas.createLayer(ctx, {
dpr: 2,
width: 300,
height: 600,
canvas // 小程序環境必傳
})
// 創建node
// c(tag,options,children)
const node = easyCanvas.createElement((c) => {
return c('view', {
styles: { backgroundColor:'#000' }, // 樣式
attrs:{}, // 屬性 比如src
on:{} // 事件 如click load
},
[
c('text',{color:'#fff'},'Hello World')
])
})
// mount
node.mount(layer)
複制代碼
vue中使用
另外在(zài)基礎版本上(shàng),封裝了(le/liǎo)相應的(de)vue組件,相比render函數,要(yào / yāo)簡潔易懂很多,基本使用如下:
npm install vue-easy-canvas --save
複制代碼
import easyCanvas from 'vue-easy-canvas'
Vue.use(easyCanvas)
複制代碼
<ec-canvas :width="300" :height="600">
<ec-scroll-view :styles="{height:600}">
<ec-view :styles="styles.imageWrapper">
<ec-image
src="https://tse1-mm.cn.bing.net/th/id/OIP.Dkj8fnK1SsPHIBmAN9XnUAHaNK?pid=Api&rs=1"
:styles="styles.image"
mode="aspectFill"></ec-image>
<ec-view :styles="styles.homeTitleWrapper">
<ec-text>easyCanvas</ec-text>
</ec-view>
</ec-view>
<ec-view :styles="styles.itemWrapper"
v-for="(item,index) in examples"
:key="index"
:on="{
click(e){
window.location.href = http://www.wxapp-union.com/host + item.url
}
}">
<ec-view :styles="styles.title">
<ec-text>{{item.title}}</ec-text>
</ec-view>
<ec-view :styles="styles.desc">
<ec-text>{{item.desc}}</ec-text>
</ec-view>
</ec-view>
</ec-scroll-view>
</ec-canvas>
複制代碼
支持元素
-
view
基本元素,類似div -
text
文本 支持自動換行以(yǐ)及超過省略等功能,目前text實現爲(wéi / wèi)inline-block -
image
圖片src
mode
支持aspectFit以(yǐ)及aspectFill,其他(tā)css特性同web 支持load
事件監聽圖片加載并且繪制完成 -
scroll-view
滾動容器,需要(yào / yāo)在(zài)樣式裏設置direction
支持x、y、xy,并且設置具體尺寸 設置renderOnDemand
隻繪制可見部分
屬性使用像素的(de)地(dì / de)方統一使用數字
-
display
block | inline-block | flex, text默認是(shì)inline-block的(de) -
width
auto 100% Number 這(zhè)裏盒模型使用border-box,不(bù)可修改 -
height
-
flex
flex不(bù)支持auto,固定寬度直接使用width -
minWidth
maxWidth
minHeight
maxHeight
如果設置了(le/liǎo)具體寬度高度不(bù)生效 -
margin
marginLeft
,marginRight
,marginTop
,marginBottom
margin支持數組縮寫例如 [10,20][10,20,10,20] -
paddingLeft
,paddingRight
,paddingTop
,paddingBottom
同上(shàng) -
backgroundColor
-
borderRadius
-
borderWidth
borderTopWidth
... 細邊框直接設置0.5 -
borderColor
-
lineHeight
字體相關的(de)隻在(zài)text内有效 -
color
-
fontSize
-
textAlign
left right center -
textIndent
Number -
verticalAlign
top middle bottom -
justifyContent
flex-start center flex-end flex布局 水平方向對其 -
alignItems
flex-start center flex-end flex布局 垂直方向對其 -
maxLine
最大(dà)行數,超出(chū)自動省略号,隻支持在(zài)text中使用 -
whiteSpace
normal nowrap 控制換行,不(bù)能控制字體,隻能控制inline-block -
overflow
hidden 如果添加了(le/liǎo)圓角,會自動加上(shàng) hidden -
flexDirection
-
borderStyle
dash Array 詳見ctx.setLineDash() -
shadowBlur
設置了(le/liǎo)陰影會自動加上(shàng) overflow:hidden; -
shadowColor
-
shadowOffsetX
-
shadowOffsetY
-
position
static
absolute
-
opacity
Number
例如這(zhè)個(gè)組件庫裏的(de)button組件
正常來(lái)說(shuō)我們寫一個(gè)按鈕
.button{
display:inline-block;
background:green;
color:#fff;
font-size:14px;
padding:4px 12px;
text-align:center;
border-radius:4px;
}
複制代碼
在(zài)easyCanvas中的(de)寫法
function Button(c){
return c('view',{
styles:{
display:'inline-block',
backgroundColor:'green',
color:'#fff',
fontSize:14,
padding:[4,12],
textAlign:'center',
borderRadius:4
}
},[
c('text',{},'按鈕')
])
}
複制代碼
是(shì)不(bù)是(shì)覺得很熟悉很簡單,讓我們來(lái)寫一個(gè)可以(yǐ)接受參數的(de)按鈕
function Button(c, { attrs, styles, on }, content) {
const size = attrs.size || 'medium'
const nums = SIZE[size]
let _styles = Object.assign({
backgroundColor: THEME[attrs.type.toUpperCase() || 'info'],
display: 'inline-block',
borderRadius: 2,
color: '#fff',
lineHeight: nums.lineHeight,
padding: nums.padding,
fontSize: nums.fontSize
}, styles || {})
if (attrs.plain) {
_styles.color = THEME[attrs.type.toUpperCase()]
_styles.borderWidth = 0.5
_styles.borderColor = THEME[attrs.type.toUpperCase()]
_styles.backgroundColor = PLAIN_THEME[attrs.type.toUpperCase() || 'info']
}
if (attrs.round) {
_styles.borderRadius = nums.borderRadius
}
return c('view', {
attrs: Object.assign({
}, attrs || {}),
styles: _styles,
on: on || {},
}, typeof content === 'string' ? [c('text', {}, content)] : content)
}
複制代碼
這(zhè)樣在(zài)使用的(de)地(dì / de)方可以(yǐ)傳入參數,像這(zhè)樣,也(yě)就(jiù)是(shì)大(dà)家在(zài)demo裏看到(dào)的(de)
Button(c, {
attrs: { type: 'primary', plain: true },
}, '主要(yào / yāo)按鈕'),
Button(c, {
attrs: { type: 'success', plain: true },
}, '成功按鈕'),
Button(c, {
attrs: { type: 'info', plain: true },
}, '信息按鈕'),
Button(c, {
attrs: { type: 'warning', plain: true },
}, '警告按鈕'),
Button(c, {
attrs: { type: 'error', plain: true },
}, '危險按鈕'),
複制代碼
并且,easyCanvas支持注冊全局組件,方便調用,其他(tā)參數請看項目使用文檔
// 注冊全局組件
easyCanvas.component('button',Button)
// 使用全局組件
function Page(c){
return c('button',{
attrs: { type: 'warning', plain: true },
}, '警告按鈕')
}
複制代碼
另外easyCanvas内置了(le/liǎo)事件管理器,可以(yǐ)支持類似web中的(de)事件,從父級向子(zǐ)級執行捕獲,子(zǐ)級再向父級冒泡。
首先需要(yào / yāo)讓canvas元素接管事件
// canvas元素監聽鼠标事件
canvas.ontouchstart = ontouchstart
canvas.ontouchmove = ontouchmove
canvas.ontouchend = ontouchend
canvas.onmousedown = ontouchstart
canvas.onmousemove = ontouchmove
canvas.onmouseup = ontouchend
canvas.onmousewheel = onmousewheel
// 将事件交給事件管理器接管 需要(yào / yāo)注意的(de)是(shì),這(zhè)裏的(de)坐标是(shì)相對于(yú)canvas元素的(de)坐标,而(ér)不(bù)是(shì)屏幕
function ontouchstart(e) {
e.preventDefault()
layer.eventManager.touchstart(e.pageX || e.touches[0].pageX || 0, e.pageY || e.touches[0].pageY || 0)
}
function ontouchmove(e) {
e.preventDefault()
layer.eventManager.touchmove(e.pageX || e.touches[0].pageX || 0, e.pageY || e.touches[0].pageY || 0)
}
function ontouchend(e) {
e.preventDefault()
layer.eventManager.touchend(
e.pageX || e.changedTouches[0].pageX || 0,
e.pageY || e.changedTouches[0].pageY || 0
)
}
function onClick(e) {
e.preventDefault()
layer.eventManager.click(e.pageX, e.pageY)
}
function onmousewheel(e){
e.preventDefault()
layer.eventManager.mousewheel(e.pageX,e.pageY,-e.deltaX,-e.deltaY)
}
複制代碼
接管到(dào)事件後,我們就(jiù)可以(yǐ)在(zài)元素内監聽事件了(le/liǎo)
c('button',{
id:'測試按鈕',
on:{
click(e){
// 阻止冒泡到(dào)父級
e.stopPropagation()
alert(e.currentTarget.id) // alert 測試按鈕
}
}
},'點我點我')
複制代碼
目前支持的(de)鼠标事件有: click、touchstart、touchmove、touchend、mousewheel。
圖片支持 load、error事件
另外,支持在(zài)layer中監聽所有圖片請求完成,比如我們需要(yào / yāo)在(zài)圖片加載完成,reflow布局并且重新渲染後立即生成圖片:
easyCanvas.createLayer(ctx, {
dpr,
width,
height,
lifecycle: {
onEffectSuccess(res) {
// 所有圖片加載成功
},
onEffectFail(res) {
// 有圖片加載失敗
},
onEffectComplete(){
// 隻要(yào / yāo)加載結束就(jiù)會調用
// 生成圖片...
}
}
})
複制代碼
easyCanvas還支持在(zài)初始渲染後對元素進行操作
// 獲取元素 key爲(wéi / wèi)attrs中定義的(de)
el.getElementBy(key,value)
// 增加元素
el.appendChild(element)
el.prependChild(element)
el.append(element) // 加在(zài)當前元素後
el.prepend(element)
// 删除元素
el.removeChild(element)
el.remove()
// 修改樣式 内部會根據樣式判斷是(shì)否需要(yào / yāo)reflow還是(shì)僅僅repaint就(jiù)足夠
el.setStyles(styles)
複制代碼
demo中點擊左側右側定位代碼
c('view', {
on: {
click(e) {
const target = layer.getElementBy('id', item.en)[0]
if (!target || e.currentTarget === lastSelect) return
const scrollView = layer.getElementBy('id', 'main')[1]
scrollView.scrollTo({ y: target.y })
e.currentTarget.setStyles({ backgroundColor: '#f1f1f1', color: '#333' })
if (lastSelect) lastSelect.setStyles({ backgroundColor: '' })
lastSelect = e.currentTarget
}
},
styles: {
padding: 10,
color: '#666',
fontSize: 16
}
}, [c('text', {}, item.en + ' ' + item.zh)]))
複制代碼
Ending
本篇文章主要(yào / yāo)介紹項目背景以(yǐ)及基本使用,也(yě)是(shì)爲(wéi / wèi)了(le/liǎo)給自己打個(gè)廣告吧:) 後面會寫實現原理以(yǐ)及一些坑,歡迎各位交流,感謝閱讀!
作者:FiyN
來(lái)源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出(chū)處。